From 0159a601af54845f154fbd739ae0f135120b4b2e Mon Sep 17 00:00:00 2001 From: mathsman5133 Date: Sat, 5 Oct 2019 17:01:32 +1000 Subject: Create the !mention command --- bot/cogs/utils.py | 37 +++++++++++++++++++++++++++++++++---- bot/constants.py | 7 +++++++ config-default.yml | 4 ++++ tests/cogs/test_information.py | 7 +++---- 4 files changed, 47 insertions(+), 8 deletions(-) diff --git a/bot/cogs/utils.py b/bot/cogs/utils.py index b6cecdc7c..b1c289807 100644 --- a/bot/cogs/utils.py +++ b/bot/cogs/utils.py @@ -1,15 +1,16 @@ import logging import re import unicodedata +from asyncio import TimeoutError, sleep from email.parser import HeaderParser from io import StringIO from typing import Tuple -from discord import Colour, Embed -from discord.ext.commands import Bot, Cog, Context, command +from discord import Colour, Embed, Message +from discord.ext.commands import Bot, Cog, Context, RoleConverter, command -from bot.constants import Channels, STAFF_ROLES -from bot.decorators import in_channel +from bot.constants import Channels, MODERATION_ROLES, Mention, STAFF_ROLES +from bot.decorators import in_channel, with_role log = logging.getLogger(__name__) @@ -128,6 +129,34 @@ class Utils(Cog): await ctx.send(embed=embed) + @command() + @with_role(*MODERATION_ROLES) + async def mention(self, ctx: Context, *, role: RoleConverter) -> None: + """Set a role to be mentionable for a limited time.""" + if role.mentionable: + await ctx.send(f'{role} (ID: {role.id}) is already mentionable!') + return + + await role.edit(mentionable=True, reason=f'!mention done by {ctx.author} ' + f'(ID: {ctx.author.id})') + + await ctx.send(f'{role} (ID: {role.id}) has been made mentionable. ' + f'I will reset it in {Mention.message_timeout} seconds,' + f' or when you send a message mentioning this role.') + + def check(m: Message) -> bool: + return role in m.role_mentions + + try: + await self.bot.wait_for('message', check=check, timeout=Mention.message_timeout) + await sleep(Mention.reset_delay) + except TimeoutError: + pass + + await role.edit(mentionable=False) + await ctx.send(f'{ctx.author.mention}, ' + f'I have reset {role} (ID: {role.id}) to be unmentionable.') + def setup(bot: Bot) -> None: """Utils cog load.""" diff --git a/bot/constants.py b/bot/constants.py index 1deeaa3b8..f4f45eb2c 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -475,6 +475,13 @@ class Free(metaclass=YAMLGetter): cooldown_per: float +class Mention(metaclass=YAMLGetter): + section = 'mention' + + message_timeout: int + reset_delay: int + + class RedirectOutput(metaclass=YAMLGetter): section = 'redirect_output' diff --git a/config-default.yml b/config-default.yml index 38b26f64f..827ae4619 100644 --- a/config-default.yml +++ b/config-default.yml @@ -347,6 +347,10 @@ free: cooldown_rate: 1 cooldown_per: 60.0 +mention: + message_timeout: 300 + reset_delay: 5 + redirect_output: delete_invocation: true delete_delay: 15 diff --git a/tests/cogs/test_information.py b/tests/cogs/test_information.py index 184bd2595..986e73a65 100644 --- a/tests/cogs/test_information.py +++ b/tests/cogs/test_information.py @@ -9,7 +9,6 @@ from discord import ( CategoryChannel, Colour, Permissions, - Role, TextChannel, VoiceChannel, ) @@ -69,7 +68,7 @@ def test_roles_info_command(cog, ctx): def test_role_info_command(cog, ctx): - dummy_role = MagicMock(spec=Role) + dummy_role = MagicMock() dummy_role.name = "Dummy" dummy_role.colour = Colour.blurple() dummy_role.id = 112233445566778899 @@ -77,7 +76,7 @@ def test_role_info_command(cog, ctx): dummy_role.permissions = Permissions(0) dummy_role.members = [ctx.author] - admin_role = MagicMock(spec=Role) + admin_role = MagicMock() admin_role.name = "Admin" admin_role.colour = Colour.red() admin_role.id = 998877665544332211 @@ -90,7 +89,7 @@ def test_role_info_command(cog, ctx): cog.role_info.can_run = AsyncMock() cog.role_info.can_run.return_value = True - coroutine = cog.role_info.callback(cog, ctx, dummy_role, admin_role) + coroutine = cog.role_info.callback(cog, ctx, [dummy_role, admin_role]) assert asyncio.run(coroutine) is None -- cgit v1.2.3 From ade137df048846a9f376282ce70b12422bade378 Mon Sep 17 00:00:00 2001 From: mathsman5133 Date: Sat, 5 Oct 2019 17:17:31 +1000 Subject: revert back `tests.cogs.test_information` --- tests/cogs/test_information.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/cogs/test_information.py b/tests/cogs/test_information.py index 986e73a65..3f365c901 100644 --- a/tests/cogs/test_information.py +++ b/tests/cogs/test_information.py @@ -9,6 +9,7 @@ from discord import ( CategoryChannel, Colour, Permissions, + Role, TextChannel, VoiceChannel, ) @@ -47,7 +48,7 @@ def ctx(moderator_role, simple_ctx): def test_roles_info_command(cog, ctx): - everyone_role = MagicMock() + everyone_role = MagicMock(spec=Role) everyone_role.name = '@everyone' # should be excluded in the output ctx.author.roles.append(everyone_role) ctx.guild.roles = ctx.author.roles @@ -76,7 +77,7 @@ def test_role_info_command(cog, ctx): dummy_role.permissions = Permissions(0) dummy_role.members = [ctx.author] - admin_role = MagicMock() + admin_role = MagicMock(spec=Role) admin_role.name = "Admin" admin_role.colour = Colour.red() admin_role.id = 998877665544332211 @@ -89,7 +90,7 @@ def test_role_info_command(cog, ctx): cog.role_info.can_run = AsyncMock() cog.role_info.can_run.return_value = True - coroutine = cog.role_info.callback(cog, ctx, [dummy_role, admin_role]) + coroutine = cog.role_info.callback(cog, ctx, dummy_role, admin_role) assert asyncio.run(coroutine) is None -- cgit v1.2.3 From f1522adf11d204c9aaf372f13407541d4f5a0e44 Mon Sep 17 00:00:00 2001 From: mathsman5133 Date: Sat, 5 Oct 2019 17:20:19 +1000 Subject: revert back `tests.cogs.test_information`. I got them in the wrong order... --- tests/cogs/test_information.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/cogs/test_information.py b/tests/cogs/test_information.py index 3f365c901..184bd2595 100644 --- a/tests/cogs/test_information.py +++ b/tests/cogs/test_information.py @@ -48,7 +48,7 @@ def ctx(moderator_role, simple_ctx): def test_roles_info_command(cog, ctx): - everyone_role = MagicMock(spec=Role) + everyone_role = MagicMock() everyone_role.name = '@everyone' # should be excluded in the output ctx.author.roles.append(everyone_role) ctx.guild.roles = ctx.author.roles @@ -69,7 +69,7 @@ def test_roles_info_command(cog, ctx): def test_role_info_command(cog, ctx): - dummy_role = MagicMock() + dummy_role = MagicMock(spec=Role) dummy_role.name = "Dummy" dummy_role.colour = Colour.blurple() dummy_role.id = 112233445566778899 -- cgit v1.2.3 From 5eda4431411a85dbab3be44bd527c3bb0badee7c Mon Sep 17 00:00:00 2001 From: mathsman5133 Date: Sat, 5 Oct 2019 20:33:07 +1000 Subject: add requested changes for review --- bot/cogs/utils.py | 43 ++++++++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/bot/cogs/utils.py b/bot/cogs/utils.py index b1c289807..c38d2709a 100644 --- a/bot/cogs/utils.py +++ b/bot/cogs/utils.py @@ -2,12 +2,13 @@ import logging import re import unicodedata from asyncio import TimeoutError, sleep +from contextlib import suppress from email.parser import HeaderParser from io import StringIO from typing import Tuple -from discord import Colour, Embed, Message -from discord.ext.commands import Bot, Cog, Context, RoleConverter, command +from discord import Colour, Embed, Message, Role +from discord.ext.commands import Bot, Cog, Context, command from bot.constants import Channels, MODERATION_ROLES, Mention, STAFF_ROLES from bot.decorators import in_channel, with_role @@ -129,33 +130,45 @@ class Utils(Cog): await ctx.send(embed=embed) + @staticmethod + def readable_time(seconds: int) -> str: + minutes, seconds = divmod(seconds, 60) + + if minutes: + fmt = '{m}min {s}sec' + else: + fmt = '{s}sec' + + return fmt.format(m=minutes, s=seconds) + @command() @with_role(*MODERATION_ROLES) - async def mention(self, ctx: Context, *, role: RoleConverter) -> None: + async def mention(self, ctx: Context, *, role: Role) -> None: """Set a role to be mentionable for a limited time.""" if role.mentionable: - await ctx.send(f'{role} (ID: {role.id}) is already mentionable!') + await ctx.send(f"{role} is already mentionable!") return - await role.edit(mentionable=True, reason=f'!mention done by {ctx.author} ' - f'(ID: {ctx.author.id})') + await role.edit(reason=f"Role unlocked by {ctx.author}", mentionable=True) - await ctx.send(f'{role} (ID: {role.id}) has been made mentionable. ' - f'I will reset it in {Mention.message_timeout} seconds,' - f' or when you send a message mentioning this role.') + await ctx.send( + f"{role} has been made mentionable. I will reset it in " + f"{self.readable_time(Mention.message_timeout)} seconds, or when a staff member mentions this role." + ) def check(m: Message) -> bool: + if not any(m.id in MODERATION_ROLES for m in m.author.roles): + await ctx.send(f"{ctx.author.mention}, {m.author} has mentioned the role you set to mentionable.") + return False + return role in m.role_mentions - try: - await self.bot.wait_for('message', check=check, timeout=Mention.message_timeout) + with suppress(TimeoutError): + await self.bot.wait_for("message", check=check, timeout=Mention.message_timeout) await sleep(Mention.reset_delay) - except TimeoutError: - pass await role.edit(mentionable=False) - await ctx.send(f'{ctx.author.mention}, ' - f'I have reset {role} (ID: {role.id}) to be unmentionable.') + await ctx.send(f"{ctx.author.mention}, I have reset {role} to be unmentionable.") def setup(bot: Bot) -> None: -- cgit v1.2.3 From 08ef78fa6f5a98da29abdb4f97a9f5513e09fb7e Mon Sep 17 00:00:00 2001 From: mathsman5133 Date: Sat, 5 Oct 2019 20:48:10 +1000 Subject: properly send message with `asyncio.run` --- bot/cogs/utils.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/bot/cogs/utils.py b/bot/cogs/utils.py index c38d2709a..fd30f4321 100644 --- a/bot/cogs/utils.py +++ b/bot/cogs/utils.py @@ -1,7 +1,7 @@ import logging import re import unicodedata -from asyncio import TimeoutError, sleep +from asyncio import TimeoutError, sleep, run from contextlib import suppress from email.parser import HeaderParser from io import StringIO @@ -157,8 +157,12 @@ class Utils(Cog): ) def check(m: Message) -> bool: - if not any(m.id in MODERATION_ROLES for m in m.author.roles): - await ctx.send(f"{ctx.author.mention}, {m.author} has mentioned the role you set to mentionable.") + """Checks that the message contains the role mention and the user is a staff member.""" + if not any(m.id in MODERATION_ROLES for m in m.author.roles) and role in m.role_mentions: + run(ctx.send( + f"{ctx.author.mention}, {m.author} has mentioned the role you set to mentionable." + ) + ) return False return role in m.role_mentions -- cgit v1.2.3 From dffe89248c42839e690dcfe4db115b9ef12fdb39 Mon Sep 17 00:00:00 2001 From: mathsman5133 Date: Sat, 5 Oct 2019 20:49:42 +1000 Subject: fix linter --- bot/cogs/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bot/cogs/utils.py b/bot/cogs/utils.py index fd30f4321..32f7ee208 100644 --- a/bot/cogs/utils.py +++ b/bot/cogs/utils.py @@ -1,7 +1,7 @@ import logging import re import unicodedata -from asyncio import TimeoutError, sleep, run +from asyncio import TimeoutError, run, sleep from contextlib import suppress from email.parser import HeaderParser from io import StringIO @@ -132,6 +132,7 @@ class Utils(Cog): @staticmethod def readable_time(seconds: int) -> str: + """Returns a number of seconds into a human-readable minutes/seconds combination.""" minutes, seconds = divmod(seconds, 60) if minutes: -- cgit v1.2.3 From d79e89e573a426c48d6c254add707f3f819327e8 Mon Sep 17 00:00:00 2001 From: mathsman5133 Date: Sun, 6 Oct 2019 16:51:21 +1100 Subject: Update !mention - Lock the role if the message wait has timed out - Sleep, lock role and send notification if mention by staff member found. - Lock role and send notification if mention by non-staff member found. --- bot/cogs/utils.py | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/bot/cogs/utils.py b/bot/cogs/utils.py index 32f7ee208..117bff373 100644 --- a/bot/cogs/utils.py +++ b/bot/cogs/utils.py @@ -1,8 +1,7 @@ import logging import re import unicodedata -from asyncio import TimeoutError, run, sleep -from contextlib import suppress +from asyncio import TimeoutError, sleep from email.parser import HeaderParser from io import StringIO from typing import Tuple @@ -158,22 +157,30 @@ class Utils(Cog): ) def check(m: Message) -> bool: - """Checks that the message contains the role mention and the user is a staff member.""" - if not any(m.id in MODERATION_ROLES for m in m.author.roles) and role in m.role_mentions: - run(ctx.send( - f"{ctx.author.mention}, {m.author} has mentioned the role you set to mentionable." - ) - ) - return False - + """Checks that the message contains the role mention.""" return role in m.role_mentions - with suppress(TimeoutError): - await self.bot.wait_for("message", check=check, timeout=Mention.message_timeout) + try: + msg = await self.bot.wait_for("message", check=check, timeout=Mention.message_timeout) + except TimeoutError: + await role.edit(mentionable=False, reason=f"Automatic role lock - timeout.") + await ctx.send(f"{ctx.author.mention}, you took too long. I have reset {role} to be unmentionable.") + return + + if any(r.id in MODERATION_ROLES for r in msg.author.roles): await sleep(Mention.reset_delay) + await role.edit(mentionable=False, reason=f"Automatic role lock by {msg.author}") + await ctx.send( + f"{ctx.author.mention}, I have reset {role} to be unmentionable as " + f"{msg.author if msg.author != ctx.author else 'you'} sent a message mentioning it." + ) + return - await role.edit(mentionable=False) - await ctx.send(f"{ctx.author.mention}, I have reset {role} to be unmentionable.") + await role.edit(mentionable=False, reason=f"Automatic role lock - unauthorised use by {msg.author}") + await ctx.send( + f"{ctx.author.mention}, I have reset {role} to be unmentionable " + f"as I detected unauthorised use by {msg.author} (ID: {msg.author.id})." + ) def setup(bot: Bot) -> None: -- cgit v1.2.3 From d3edb192d286d420a31d8d6a137c58dae97b3e91 Mon Sep 17 00:00:00 2001 From: mathsman5133 Date: Sat, 12 Oct 2019 12:59:36 +1100 Subject: Use `bot.utils.humanize_delta`, tidy bot response, remove stray f from f-string --- bot/cogs/utils.py | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/bot/cogs/utils.py b/bot/cogs/utils.py index 117bff373..9306c8986 100644 --- a/bot/cogs/utils.py +++ b/bot/cogs/utils.py @@ -6,11 +6,13 @@ from email.parser import HeaderParser from io import StringIO from typing import Tuple +from dateutil import relativedelta from discord import Colour, Embed, Message, Role from discord.ext.commands import Bot, Cog, Context, command from bot.constants import Channels, MODERATION_ROLES, Mention, STAFF_ROLES from bot.decorators import in_channel, with_role +from bot.utils.time import humanize_delta log = logging.getLogger(__name__) @@ -129,18 +131,6 @@ class Utils(Cog): await ctx.send(embed=embed) - @staticmethod - def readable_time(seconds: int) -> str: - """Returns a number of seconds into a human-readable minutes/seconds combination.""" - minutes, seconds = divmod(seconds, 60) - - if minutes: - fmt = '{m}min {s}sec' - else: - fmt = '{s}sec' - - return fmt.format(m=minutes, s=seconds) - @command() @with_role(*MODERATION_ROLES) async def mention(self, ctx: Context, *, role: Role) -> None: @@ -151,9 +141,9 @@ class Utils(Cog): await role.edit(reason=f"Role unlocked by {ctx.author}", mentionable=True) + human_time = humanize_delta(relativedelta.relativedelta(seconds=Mention.message_timeout)) await ctx.send( - f"{role} has been made mentionable. I will reset it in " - f"{self.readable_time(Mention.message_timeout)} seconds, or when a staff member mentions this role." + f"{role} has been made mentionable. I will reset it in {human_time}, or when someone mentions this role." ) def check(m: Message) -> bool: @@ -163,7 +153,7 @@ class Utils(Cog): try: msg = await self.bot.wait_for("message", check=check, timeout=Mention.message_timeout) except TimeoutError: - await role.edit(mentionable=False, reason=f"Automatic role lock - timeout.") + await role.edit(mentionable=False, reason="Automatic role lock - timeout.") await ctx.send(f"{ctx.author.mention}, you took too long. I have reset {role} to be unmentionable.") return -- cgit v1.2.3