From fb9cbe434fc4531d117e6b8bdbd778dc4e9803a5 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Wed, 7 Jul 2021 22:24:21 +0300 Subject: Create events ext, prepare jams cog for file split --- bot/exts/events/__init__.py | 0 bot/exts/events/code_jams/__init__.py | 8 ++ bot/exts/events/code_jams/_cog.py | 176 ++++++++++++++++++++++++++++++++ bot/exts/filters/antimalware.py | 2 +- bot/exts/filters/antispam.py | 2 +- bot/exts/filters/filtering.py | 2 +- bot/exts/utils/jams.py | 176 -------------------------------- tests/bot/exts/events/__init__.py | 0 tests/bot/exts/events/test_code_jams.py | 174 +++++++++++++++++++++++++++++++ tests/bot/exts/utils/test_jams.py | 174 ------------------------------- 10 files changed, 361 insertions(+), 353 deletions(-) create mode 100644 bot/exts/events/__init__.py create mode 100644 bot/exts/events/code_jams/__init__.py create mode 100644 bot/exts/events/code_jams/_cog.py delete mode 100644 bot/exts/utils/jams.py create mode 100644 tests/bot/exts/events/__init__.py create mode 100644 tests/bot/exts/events/test_code_jams.py delete mode 100644 tests/bot/exts/utils/test_jams.py diff --git a/bot/exts/events/__init__.py b/bot/exts/events/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/bot/exts/events/code_jams/__init__.py b/bot/exts/events/code_jams/__init__.py new file mode 100644 index 000000000..16e81e365 --- /dev/null +++ b/bot/exts/events/code_jams/__init__.py @@ -0,0 +1,8 @@ +from bot.bot import Bot + + +def setup(bot: Bot) -> None: + """Load the CodeJams cog.""" + from bot.exts.events.code_jams._cog import CodeJams + + bot.add_cog(CodeJams(bot)) diff --git a/bot/exts/events/code_jams/_cog.py b/bot/exts/events/code_jams/_cog.py new file mode 100644 index 000000000..87ae847f6 --- /dev/null +++ b/bot/exts/events/code_jams/_cog.py @@ -0,0 +1,176 @@ +import csv +import logging +import typing as t +from collections import defaultdict + +import discord +from discord.ext import commands + +from bot.bot import Bot +from bot.constants import Categories, Channels, Emojis, Roles + +log = logging.getLogger(__name__) + +MAX_CHANNELS = 50 +CATEGORY_NAME = "Code Jam" +TEAM_LEADERS_COLOUR = 0x11806a + + +class CodeJams(commands.Cog): + """Manages the code-jam related parts of our server.""" + + def __init__(self, bot: Bot): + self.bot = bot + + @commands.group() + @commands.has_any_role(Roles.admins) + async def codejam(self, ctx: commands.Context) -> None: + """A Group of commands for managing Code Jams.""" + if ctx.invoked_subcommand is None: + await ctx.send_help(ctx.command) + + @codejam.command() + async def create(self, ctx: commands.Context, csv_file: t.Optional[str]) -> None: + """ + Create code-jam teams from a CSV file or a link to one, specifying the team names, leaders and members. + + The CSV file must have 3 columns: 'Team Name', 'Team Member Discord ID', and 'Team Leader'. + + This will create the text channels for the teams, and give the team leaders their roles. + """ + async with ctx.typing(): + if csv_file: + async with self.bot.http_session.get(csv_file) as response: + if response.status != 200: + await ctx.send(f"Got a bad response from the URL: {response.status}") + return + + csv_file = await response.text() + + elif ctx.message.attachments: + csv_file = (await ctx.message.attachments[0].read()).decode("utf8") + else: + raise commands.BadArgument("You must include either a CSV file or a link to one.") + + teams = defaultdict(list) + reader = csv.DictReader(csv_file.splitlines()) + + for row in reader: + member = ctx.guild.get_member(int(row["Team Member Discord ID"])) + + if member is None: + log.trace(f"Got an invalid member ID: {row['Team Member Discord ID']}") + continue + + teams[row["Team Name"]].append((member, row["Team Leader"].upper() == "Y")) + + team_leaders = await ctx.guild.create_role(name="Code Jam Team Leaders", colour=TEAM_LEADERS_COLOUR) + + for team_name, members in teams.items(): + await self.create_team_channel(ctx.guild, team_name, members, team_leaders) + + await self.create_team_leader_channel(ctx.guild, team_leaders) + await ctx.send(f"{Emojis.check_mark} Created Code Jam with {len(teams)} teams.") + + async def get_category(self, guild: discord.Guild) -> discord.CategoryChannel: + """ + Return a code jam category. + + If all categories are full or none exist, create a new category. + """ + for category in guild.categories: + if category.name == CATEGORY_NAME and len(category.channels) < MAX_CHANNELS: + return category + + return await self.create_category(guild) + + async def create_category(self, guild: discord.Guild) -> discord.CategoryChannel: + """Create a new code jam category and return it.""" + log.info("Creating a new code jam category.") + + category_overwrites = { + guild.default_role: discord.PermissionOverwrite(read_messages=False), + guild.me: discord.PermissionOverwrite(read_messages=True) + } + + category = await guild.create_category_channel( + CATEGORY_NAME, + overwrites=category_overwrites, + reason="It's code jam time!" + ) + + await self.send_status_update( + guild, f"Created a new category with the ID {category.id} for this Code Jam's team channels." + ) + + return category + + @staticmethod + def get_overwrites( + members: list[tuple[discord.Member, bool]], + guild: discord.Guild, + ) -> dict[t.Union[discord.Member, discord.Role], discord.PermissionOverwrite]: + """Get code jam team channels permission overwrites.""" + team_channel_overwrites = { + guild.default_role: discord.PermissionOverwrite(read_messages=False), + guild.get_role(Roles.code_jam_event_team): discord.PermissionOverwrite(read_messages=True) + } + + for member, _ in members: + team_channel_overwrites[member] = discord.PermissionOverwrite( + read_messages=True + ) + + return team_channel_overwrites + + async def create_team_channel( + self, + guild: discord.Guild, + team_name: str, + members: list[tuple[discord.Member, bool]], + team_leaders: discord.Role + ) -> None: + """Create the team's text channel.""" + await self.add_team_leader_roles(members, team_leaders) + + # Get permission overwrites and category + team_channel_overwrites = self.get_overwrites(members, guild) + code_jam_category = await self.get_category(guild) + + # Create a text channel for the team + await code_jam_category.create_text_channel( + team_name, + overwrites=team_channel_overwrites, + ) + + async def create_team_leader_channel(self, guild: discord.Guild, team_leaders: discord.Role) -> None: + """Create the Team Leader Chat channel for the Code Jam team leaders.""" + category: discord.CategoryChannel = guild.get_channel(Categories.summer_code_jam) + + team_leaders_chat = await category.create_text_channel( + name="team-leaders-chat", + overwrites={ + guild.default_role: discord.PermissionOverwrite(read_messages=False), + team_leaders: discord.PermissionOverwrite(read_messages=True) + } + ) + + await self.send_status_update(guild, f"Created {team_leaders_chat.mention} in the {category} category.") + + async def send_status_update(self, guild: discord.Guild, message: str) -> None: + """Inform the events lead with a status update when the command is ran.""" + channel: discord.TextChannel = guild.get_channel(Channels.code_jam_planning) + + await channel.send(f"<@&{Roles.events_lead}>\n\n{message}") + + @staticmethod + async def add_team_leader_roles(members: list[tuple[discord.Member, bool]], team_leaders: discord.Role) -> None: + """Assign team leader role, the jammer role and their team role.""" + for member, is_leader in members: + if is_leader: + await member.add_roles(team_leaders) + + +def setup(bot: Bot) -> None: + """Load the CodeJams cog.""" + bot.add_cog(CodeJams(bot)) diff --git a/bot/exts/filters/antimalware.py b/bot/exts/filters/antimalware.py index 4c4836c88..3f6213db3 100644 --- a/bot/exts/filters/antimalware.py +++ b/bot/exts/filters/antimalware.py @@ -7,7 +7,7 @@ from discord.ext.commands import Cog from bot.bot import Bot from bot.constants import Channels, Filter, URLs -from bot.exts.utils.jams import CATEGORY_NAME as JAM_CATEGORY_NAME +from bot.exts.events.code_jams._cog import CATEGORY_NAME as JAM_CATEGORY_NAME log = logging.getLogger(__name__) diff --git a/bot/exts/filters/antispam.py b/bot/exts/filters/antispam.py index 48c3aa5a6..124905cb4 100644 --- a/bot/exts/filters/antispam.py +++ b/bot/exts/filters/antispam.py @@ -17,8 +17,8 @@ from bot.constants import ( Guild as GuildConfig, Icons, ) from bot.converters import Duration +from bot.exts.events.code_jams._cog import CATEGORY_NAME as JAM_CATEGORY_NAME from bot.exts.moderation.modlog import ModLog -from bot.exts.utils.jams import CATEGORY_NAME as JAM_CATEGORY_NAME from bot.utils import lock, scheduling from bot.utils.messages import format_user, send_attachments diff --git a/bot/exts/filters/filtering.py b/bot/exts/filters/filtering.py index 16aaf11cf..0810425e2 100644 --- a/bot/exts/filters/filtering.py +++ b/bot/exts/filters/filtering.py @@ -19,8 +19,8 @@ from bot.constants import ( Channels, Colours, Filter, Guild, Icons, URLs ) +from bot.exts.events.code_jams._cog import CATEGORY_NAME as JAM_CATEGORY_NAME from bot.exts.moderation.modlog import ModLog -from bot.exts.utils.jams import CATEGORY_NAME as JAM_CATEGORY_NAME from bot.utils.messages import format_user from bot.utils.regex import INVITE_RE from bot.utils.scheduling import Scheduler diff --git a/bot/exts/utils/jams.py b/bot/exts/utils/jams.py deleted file mode 100644 index 87ae847f6..000000000 --- a/bot/exts/utils/jams.py +++ /dev/null @@ -1,176 +0,0 @@ -import csv -import logging -import typing as t -from collections import defaultdict - -import discord -from discord.ext import commands - -from bot.bot import Bot -from bot.constants import Categories, Channels, Emojis, Roles - -log = logging.getLogger(__name__) - -MAX_CHANNELS = 50 -CATEGORY_NAME = "Code Jam" -TEAM_LEADERS_COLOUR = 0x11806a - - -class CodeJams(commands.Cog): - """Manages the code-jam related parts of our server.""" - - def __init__(self, bot: Bot): - self.bot = bot - - @commands.group() - @commands.has_any_role(Roles.admins) - async def codejam(self, ctx: commands.Context) -> None: - """A Group of commands for managing Code Jams.""" - if ctx.invoked_subcommand is None: - await ctx.send_help(ctx.command) - - @codejam.command() - async def create(self, ctx: commands.Context, csv_file: t.Optional[str]) -> None: - """ - Create code-jam teams from a CSV file or a link to one, specifying the team names, leaders and members. - - The CSV file must have 3 columns: 'Team Name', 'Team Member Discord ID', and 'Team Leader'. - - This will create the text channels for the teams, and give the team leaders their roles. - """ - async with ctx.typing(): - if csv_file: - async with self.bot.http_session.get(csv_file) as response: - if response.status != 200: - await ctx.send(f"Got a bad response from the URL: {response.status}") - return - - csv_file = await response.text() - - elif ctx.message.attachments: - csv_file = (await ctx.message.attachments[0].read()).decode("utf8") - else: - raise commands.BadArgument("You must include either a CSV file or a link to one.") - - teams = defaultdict(list) - reader = csv.DictReader(csv_file.splitlines()) - - for row in reader: - member = ctx.guild.get_member(int(row["Team Member Discord ID"])) - - if member is None: - log.trace(f"Got an invalid member ID: {row['Team Member Discord ID']}") - continue - - teams[row["Team Name"]].append((member, row["Team Leader"].upper() == "Y")) - - team_leaders = await ctx.guild.create_role(name="Code Jam Team Leaders", colour=TEAM_LEADERS_COLOUR) - - for team_name, members in teams.items(): - await self.create_team_channel(ctx.guild, team_name, members, team_leaders) - - await self.create_team_leader_channel(ctx.guild, team_leaders) - await ctx.send(f"{Emojis.check_mark} Created Code Jam with {len(teams)} teams.") - - async def get_category(self, guild: discord.Guild) -> discord.CategoryChannel: - """ - Return a code jam category. - - If all categories are full or none exist, create a new category. - """ - for category in guild.categories: - if category.name == CATEGORY_NAME and len(category.channels) < MAX_CHANNELS: - return category - - return await self.create_category(guild) - - async def create_category(self, guild: discord.Guild) -> discord.CategoryChannel: - """Create a new code jam category and return it.""" - log.info("Creating a new code jam category.") - - category_overwrites = { - guild.default_role: discord.PermissionOverwrite(read_messages=False), - guild.me: discord.PermissionOverwrite(read_messages=True) - } - - category = await guild.create_category_channel( - CATEGORY_NAME, - overwrites=category_overwrites, - reason="It's code jam time!" - ) - - await self.send_status_update( - guild, f"Created a new category with the ID {category.id} for this Code Jam's team channels." - ) - - return category - - @staticmethod - def get_overwrites( - members: list[tuple[discord.Member, bool]], - guild: discord.Guild, - ) -> dict[t.Union[discord.Member, discord.Role], discord.PermissionOverwrite]: - """Get code jam team channels permission overwrites.""" - team_channel_overwrites = { - guild.default_role: discord.PermissionOverwrite(read_messages=False), - guild.get_role(Roles.code_jam_event_team): discord.PermissionOverwrite(read_messages=True) - } - - for member, _ in members: - team_channel_overwrites[member] = discord.PermissionOverwrite( - read_messages=True - ) - - return team_channel_overwrites - - async def create_team_channel( - self, - guild: discord.Guild, - team_name: str, - members: list[tuple[discord.Member, bool]], - team_leaders: discord.Role - ) -> None: - """Create the team's text channel.""" - await self.add_team_leader_roles(members, team_leaders) - - # Get permission overwrites and category - team_channel_overwrites = self.get_overwrites(members, guild) - code_jam_category = await self.get_category(guild) - - # Create a text channel for the team - await code_jam_category.create_text_channel( - team_name, - overwrites=team_channel_overwrites, - ) - - async def create_team_leader_channel(self, guild: discord.Guild, team_leaders: discord.Role) -> None: - """Create the Team Leader Chat channel for the Code Jam team leaders.""" - category: discord.CategoryChannel = guild.get_channel(Categories.summer_code_jam) - - team_leaders_chat = await category.create_text_channel( - name="team-leaders-chat", - overwrites={ - guild.default_role: discord.PermissionOverwrite(read_messages=False), - team_leaders: discord.PermissionOverwrite(read_messages=True) - } - ) - - await self.send_status_update(guild, f"Created {team_leaders_chat.mention} in the {category} category.") - - async def send_status_update(self, guild: discord.Guild, message: str) -> None: - """Inform the events lead with a status update when the command is ran.""" - channel: discord.TextChannel = guild.get_channel(Channels.code_jam_planning) - - await channel.send(f"<@&{Roles.events_lead}>\n\n{message}") - - @staticmethod - async def add_team_leader_roles(members: list[tuple[discord.Member, bool]], team_leaders: discord.Role) -> None: - """Assign team leader role, the jammer role and their team role.""" - for member, is_leader in members: - if is_leader: - await member.add_roles(team_leaders) - - -def setup(bot: Bot) -> None: - """Load the CodeJams cog.""" - bot.add_cog(CodeJams(bot)) diff --git a/tests/bot/exts/events/__init__.py b/tests/bot/exts/events/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/bot/exts/events/test_code_jams.py b/tests/bot/exts/events/test_code_jams.py new file mode 100644 index 000000000..d7b8aa4d2 --- /dev/null +++ b/tests/bot/exts/events/test_code_jams.py @@ -0,0 +1,174 @@ +import unittest +from unittest.mock import AsyncMock, MagicMock, create_autospec + +from discord import CategoryChannel +from discord.ext.commands import BadArgument + +from bot.constants import Roles +from bot.exts.events.code_jams import _cog +from tests.helpers import ( + MockAttachment, MockBot, MockCategoryChannel, MockContext, + MockGuild, MockMember, MockRole, MockTextChannel +) + +TEST_CSV = b"""\ +Team Name,Team Member Discord ID,Team Leader +Annoyed Alligators,12345,Y +Annoyed Alligators,54321,N +Oscillating Otters,12358,Y +Oscillating Otters,74832,N +Oscillating Otters,19903,N +Annoyed Alligators,11111,N +""" + + +def get_mock_category(channel_count: int, name: str) -> CategoryChannel: + """Return a mocked code jam category.""" + category = create_autospec(CategoryChannel, spec_set=True, instance=True) + category.name = name + category.channels = [MockTextChannel() for _ in range(channel_count)] + + return category + + +class JamCodejamCreateTests(unittest.IsolatedAsyncioTestCase): + """Tests for `codejam create` command.""" + + def setUp(self): + self.bot = MockBot() + self.admin_role = MockRole(name="Admins", id=Roles.admins) + self.command_user = MockMember([self.admin_role]) + self.guild = MockGuild([self.admin_role]) + self.ctx = MockContext(bot=self.bot, author=self.command_user, guild=self.guild) + self.cog = _cog.CodeJams(self.bot) + + async def test_message_without_attachments(self): + """If no link or attachments are provided, commands.BadArgument should be raised.""" + self.ctx.message.attachments = [] + + with self.assertRaises(BadArgument): + await self.cog.create(self.cog, self.ctx, None) + + async def test_result_sending(self): + """Should call `ctx.send` when everything goes right.""" + self.ctx.message.attachments = [MockAttachment()] + self.ctx.message.attachments[0].read = AsyncMock() + self.ctx.message.attachments[0].read.return_value = TEST_CSV + + team_leaders = MockRole() + + self.guild.get_member.return_value = MockMember() + + self.ctx.guild.create_role = AsyncMock() + self.ctx.guild.create_role.return_value = team_leaders + self.cog.create_team_channel = AsyncMock() + self.cog.create_team_leader_channel = AsyncMock() + self.cog.add_roles = AsyncMock() + + await self.cog.create(self.cog, self.ctx, None) + + self.cog.create_team_channel.assert_awaited() + self.cog.create_team_leader_channel.assert_awaited_once_with( + self.ctx.guild, team_leaders + ) + self.ctx.send.assert_awaited_once() + + async def test_link_returning_non_200_status(self): + """When the URL passed returns a non 200 status, it should send a message informing them.""" + self.bot.http_session.get.return_value = mock = MagicMock() + mock.status = 404 + await self.cog.create(self.cog, self.ctx, "https://not-a-real-link.com") + + self.ctx.send.assert_awaited_once() + + async def test_category_doesnt_exist(self): + """Should create a new code jam category.""" + subtests = ( + [], + [get_mock_category(_cog.MAX_CHANNELS, _cog.CATEGORY_NAME)], + [get_mock_category(_cog.MAX_CHANNELS - 2, "other")], + ) + + self.cog.send_status_update = AsyncMock() + + for categories in subtests: + self.cog.send_status_update.reset_mock() + self.guild.reset_mock() + self.guild.categories = categories + + with self.subTest(categories=categories): + actual_category = await self.cog.get_category(self.guild) + + self.cog.send_status_update.assert_called_once() + self.guild.create_category_channel.assert_awaited_once() + category_overwrites = self.guild.create_category_channel.call_args[1]["overwrites"] + + self.assertFalse(category_overwrites[self.guild.default_role].read_messages) + self.assertTrue(category_overwrites[self.guild.me].read_messages) + self.assertEqual(self.guild.create_category_channel.return_value, actual_category) + + async def test_category_channel_exist(self): + """Should not try to create category channel.""" + expected_category = get_mock_category(_cog.MAX_CHANNELS - 2, _cog.CATEGORY_NAME) + self.guild.categories = [ + get_mock_category(_cog.MAX_CHANNELS - 2, "other"), + expected_category, + get_mock_category(0, _cog.CATEGORY_NAME), + ] + + actual_category = await self.cog.get_category(self.guild) + self.assertEqual(expected_category, actual_category) + + async def test_channel_overwrites(self): + """Should have correct permission overwrites for users and roles.""" + leader = (MockMember(), True) + members = [leader] + [(MockMember(), False) for _ in range(4)] + overwrites = self.cog.get_overwrites(members, self.guild) + + for member, _ in members: + self.assertTrue(overwrites[member].read_messages) + + async def test_team_channels_creation(self): + """Should create a text channel for a team.""" + team_leaders = MockRole() + members = [(MockMember(), True)] + [(MockMember(), False) for _ in range(5)] + category = MockCategoryChannel() + category.create_text_channel = AsyncMock() + + self.cog.get_overwrites = MagicMock() + self.cog.get_category = AsyncMock() + self.cog.get_category.return_value = category + self.cog.add_team_leader_roles = AsyncMock() + + await self.cog.create_team_channel(self.guild, "my-team", members, team_leaders) + self.cog.add_team_leader_roles.assert_awaited_once_with(members, team_leaders) + self.cog.get_overwrites.assert_called_once_with(members, self.guild) + self.cog.get_category.assert_awaited_once_with(self.guild) + + category.create_text_channel.assert_awaited_once_with( + "my-team", + overwrites=self.cog.get_overwrites.return_value + ) + + async def test_jam_roles_adding(self): + """Should add team leader role to leader and jam role to every team member.""" + leader_role = MockRole(name="Team Leader") + + leader = MockMember() + members = [(leader, True)] + [(MockMember(), False) for _ in range(4)] + await self.cog.add_team_leader_roles(members, leader_role) + + leader.add_roles.assert_awaited_once_with(leader_role) + for member, is_leader in members: + if not is_leader: + member.add_roles.assert_not_awaited() + + +class CodeJamSetup(unittest.TestCase): + """Test for `setup` function of `CodeJam` cog.""" + + def test_setup(self): + """Should call `bot.add_cog`.""" + bot = MockBot() + _cog.setup(bot) + bot.add_cog.assert_called_once() diff --git a/tests/bot/exts/utils/test_jams.py b/tests/bot/exts/utils/test_jams.py deleted file mode 100644 index 368a15476..000000000 --- a/tests/bot/exts/utils/test_jams.py +++ /dev/null @@ -1,174 +0,0 @@ -import unittest -from unittest.mock import AsyncMock, MagicMock, create_autospec - -from discord import CategoryChannel -from discord.ext.commands import BadArgument - -from bot.constants import Roles -from bot.exts.utils import jams -from tests.helpers import ( - MockAttachment, MockBot, MockCategoryChannel, MockContext, - MockGuild, MockMember, MockRole, MockTextChannel -) - -TEST_CSV = b"""\ -Team Name,Team Member Discord ID,Team Leader -Annoyed Alligators,12345,Y -Annoyed Alligators,54321,N -Oscillating Otters,12358,Y -Oscillating Otters,74832,N -Oscillating Otters,19903,N -Annoyed Alligators,11111,N -""" - - -def get_mock_category(channel_count: int, name: str) -> CategoryChannel: - """Return a mocked code jam category.""" - category = create_autospec(CategoryChannel, spec_set=True, instance=True) - category.name = name - category.channels = [MockTextChannel() for _ in range(channel_count)] - - return category - - -class JamCodejamCreateTests(unittest.IsolatedAsyncioTestCase): - """Tests for `codejam create` command.""" - - def setUp(self): - self.bot = MockBot() - self.admin_role = MockRole(name="Admins", id=Roles.admins) - self.command_user = MockMember([self.admin_role]) - self.guild = MockGuild([self.admin_role]) - self.ctx = MockContext(bot=self.bot, author=self.command_user, guild=self.guild) - self.cog = jams.CodeJams(self.bot) - - async def test_message_without_attachments(self): - """If no link or attachments are provided, commands.BadArgument should be raised.""" - self.ctx.message.attachments = [] - - with self.assertRaises(BadArgument): - await self.cog.create(self.cog, self.ctx, None) - - async def test_result_sending(self): - """Should call `ctx.send` when everything goes right.""" - self.ctx.message.attachments = [MockAttachment()] - self.ctx.message.attachments[0].read = AsyncMock() - self.ctx.message.attachments[0].read.return_value = TEST_CSV - - team_leaders = MockRole() - - self.guild.get_member.return_value = MockMember() - - self.ctx.guild.create_role = AsyncMock() - self.ctx.guild.create_role.return_value = team_leaders - self.cog.create_team_channel = AsyncMock() - self.cog.create_team_leader_channel = AsyncMock() - self.cog.add_roles = AsyncMock() - - await self.cog.create(self.cog, self.ctx, None) - - self.cog.create_team_channel.assert_awaited() - self.cog.create_team_leader_channel.assert_awaited_once_with( - self.ctx.guild, team_leaders - ) - self.ctx.send.assert_awaited_once() - - async def test_link_returning_non_200_status(self): - """When the URL passed returns a non 200 status, it should send a message informing them.""" - self.bot.http_session.get.return_value = mock = MagicMock() - mock.status = 404 - await self.cog.create(self.cog, self.ctx, "https://not-a-real-link.com") - - self.ctx.send.assert_awaited_once() - - async def test_category_doesnt_exist(self): - """Should create a new code jam category.""" - subtests = ( - [], - [get_mock_category(jams.MAX_CHANNELS, jams.CATEGORY_NAME)], - [get_mock_category(jams.MAX_CHANNELS - 2, "other")], - ) - - self.cog.send_status_update = AsyncMock() - - for categories in subtests: - self.cog.send_status_update.reset_mock() - self.guild.reset_mock() - self.guild.categories = categories - - with self.subTest(categories=categories): - actual_category = await self.cog.get_category(self.guild) - - self.cog.send_status_update.assert_called_once() - self.guild.create_category_channel.assert_awaited_once() - category_overwrites = self.guild.create_category_channel.call_args[1]["overwrites"] - - self.assertFalse(category_overwrites[self.guild.default_role].read_messages) - self.assertTrue(category_overwrites[self.guild.me].read_messages) - self.assertEqual(self.guild.create_category_channel.return_value, actual_category) - - async def test_category_channel_exist(self): - """Should not try to create category channel.""" - expected_category = get_mock_category(jams.MAX_CHANNELS - 2, jams.CATEGORY_NAME) - self.guild.categories = [ - get_mock_category(jams.MAX_CHANNELS - 2, "other"), - expected_category, - get_mock_category(0, jams.CATEGORY_NAME), - ] - - actual_category = await self.cog.get_category(self.guild) - self.assertEqual(expected_category, actual_category) - - async def test_channel_overwrites(self): - """Should have correct permission overwrites for users and roles.""" - leader = (MockMember(), True) - members = [leader] + [(MockMember(), False) for _ in range(4)] - overwrites = self.cog.get_overwrites(members, self.guild) - - for member, _ in members: - self.assertTrue(overwrites[member].read_messages) - - async def test_team_channels_creation(self): - """Should create a text channel for a team.""" - team_leaders = MockRole() - members = [(MockMember(), True)] + [(MockMember(), False) for _ in range(5)] - category = MockCategoryChannel() - category.create_text_channel = AsyncMock() - - self.cog.get_overwrites = MagicMock() - self.cog.get_category = AsyncMock() - self.cog.get_category.return_value = category - self.cog.add_team_leader_roles = AsyncMock() - - await self.cog.create_team_channel(self.guild, "my-team", members, team_leaders) - self.cog.add_team_leader_roles.assert_awaited_once_with(members, team_leaders) - self.cog.get_overwrites.assert_called_once_with(members, self.guild) - self.cog.get_category.assert_awaited_once_with(self.guild) - - category.create_text_channel.assert_awaited_once_with( - "my-team", - overwrites=self.cog.get_overwrites.return_value - ) - - async def test_jam_roles_adding(self): - """Should add team leader role to leader and jam role to every team member.""" - leader_role = MockRole(name="Team Leader") - - leader = MockMember() - members = [(leader, True)] + [(MockMember(), False) for _ in range(4)] - await self.cog.add_team_leader_roles(members, leader_role) - - leader.add_roles.assert_awaited_once_with(leader_role) - for member, is_leader in members: - if not is_leader: - member.add_roles.assert_not_awaited() - - -class CodeJamSetup(unittest.TestCase): - """Test for `setup` function of `CodeJam` cog.""" - - def test_setup(self): - """Should call `bot.add_cog`.""" - bot = MockBot() - jams.setup(bot) - bot.add_cog.assert_called_once() -- cgit v1.2.3 From 698660004b13273371baefa1f41ce2f908a3431f Mon Sep 17 00:00:00 2001 From: mbaruh Date: Thu, 8 Jul 2021 02:16:33 +0300 Subject: Move jam channels creation to separate file The channel creations are static and clutter the cog class. We want to add more commands to the cog, so we move the static functions away to a separate file first. --- bot/exts/events/code_jams/_channels.py | 113 ++++++++++++++++++++++++++++++++ bot/exts/events/code_jams/_cog.py | 113 ++------------------------------ bot/exts/filters/antimalware.py | 2 +- bot/exts/filters/antispam.py | 2 +- bot/exts/filters/filtering.py | 2 +- tests/bot/exts/events/test_code_jams.py | 64 +++++++++--------- 6 files changed, 150 insertions(+), 146 deletions(-) create mode 100644 bot/exts/events/code_jams/_channels.py diff --git a/bot/exts/events/code_jams/_channels.py b/bot/exts/events/code_jams/_channels.py new file mode 100644 index 000000000..8b199a3c2 --- /dev/null +++ b/bot/exts/events/code_jams/_channels.py @@ -0,0 +1,113 @@ +import logging +import typing as t + +import discord + +from bot.constants import Categories, Channels, Roles + +log = logging.getLogger(__name__) + +MAX_CHANNELS = 50 +CATEGORY_NAME = "Code Jam" + + +async def _get_category(guild: discord.Guild) -> discord.CategoryChannel: + """ + Return a code jam category. + + If all categories are full or none exist, create a new category. + """ + for category in guild.categories: + if category.name == CATEGORY_NAME and len(category.channels) < MAX_CHANNELS: + return category + + return await _create_category(guild) + + +async def _create_category(guild: discord.Guild) -> discord.CategoryChannel: + """Create a new code jam category and return it.""" + log.info("Creating a new code jam category.") + + category_overwrites = { + guild.default_role: discord.PermissionOverwrite(read_messages=False), + guild.me: discord.PermissionOverwrite(read_messages=True) + } + + category = await guild.create_category_channel( + CATEGORY_NAME, + overwrites=category_overwrites, + reason="It's code jam time!" + ) + + await _send_status_update( + guild, f"Created a new category with the ID {category.id} for this Code Jam's team channels." + ) + + return category + + +def _get_overwrites( + members: list[tuple[discord.Member, bool]], + guild: discord.Guild, +) -> dict[t.Union[discord.Member, discord.Role], discord.PermissionOverwrite]: + """Get code jam team channels permission overwrites.""" + team_channel_overwrites = { + guild.default_role: discord.PermissionOverwrite(read_messages=False), + guild.get_role(Roles.code_jam_event_team): discord.PermissionOverwrite(read_messages=True) + } + + for member, _ in members: + team_channel_overwrites[member] = discord.PermissionOverwrite( + read_messages=True + ) + + return team_channel_overwrites + + +async def create_team_channel( + guild: discord.Guild, + team_name: str, + members: list[tuple[discord.Member, bool]], + team_leaders: discord.Role +) -> None: + """Create the team's text channel.""" + await _add_team_leader_roles(members, team_leaders) + + # Get permission overwrites and category + team_channel_overwrites = _get_overwrites(members, guild) + code_jam_category = await _get_category(guild) + + # Create a text channel for the team + await code_jam_category.create_text_channel( + team_name, + overwrites=team_channel_overwrites, + ) + + +async def create_team_leader_channel(guild: discord.Guild, team_leaders: discord.Role) -> None: + """Create the Team Leader Chat channel for the Code Jam team leaders.""" + category: discord.CategoryChannel = guild.get_channel(Categories.summer_code_jam) + + team_leaders_chat = await category.create_text_channel( + name="team-leaders-chat", + overwrites={ + guild.default_role: discord.PermissionOverwrite(read_messages=False), + team_leaders: discord.PermissionOverwrite(read_messages=True) + } + ) + + await _send_status_update(guild, f"Created {team_leaders_chat.mention} in the {category} category.") + + +async def _send_status_update(guild: discord.Guild, message: str) -> None: + """Inform the events lead with a status update when the command is ran.""" + channel: discord.TextChannel = guild.get_channel(Channels.code_jam_planning) + + await channel.send(f"<@&{Roles.events_lead}>\n\n{message}") + + +async def _add_team_leader_roles(members: list[tuple[discord.Member, bool]], team_leaders: discord.Role) -> None: + """Assign team leader role, the jammer role and their team role.""" + for member, is_leader in members: + if is_leader: + await member.add_roles(team_leaders) diff --git a/bot/exts/events/code_jams/_cog.py b/bot/exts/events/code_jams/_cog.py index 87ae847f6..2d0873de7 100644 --- a/bot/exts/events/code_jams/_cog.py +++ b/bot/exts/events/code_jams/_cog.py @@ -3,16 +3,14 @@ import logging import typing as t from collections import defaultdict -import discord from discord.ext import commands from bot.bot import Bot -from bot.constants import Categories, Channels, Emojis, Roles +from bot.constants import Emojis, Roles +from bot.exts.events.code_jams import _channels log = logging.getLogger(__name__) -MAX_CHANNELS = 50 -CATEGORY_NAME = "Code Jam" TEAM_LEADERS_COLOUR = 0x11806a @@ -67,110 +65,7 @@ class CodeJams(commands.Cog): team_leaders = await ctx.guild.create_role(name="Code Jam Team Leaders", colour=TEAM_LEADERS_COLOUR) for team_name, members in teams.items(): - await self.create_team_channel(ctx.guild, team_name, members, team_leaders) + await _channels.create_team_channel(ctx.guild, team_name, members, team_leaders) - await self.create_team_leader_channel(ctx.guild, team_leaders) + await _channels.create_team_leader_channel(ctx.guild, team_leaders) await ctx.send(f"{Emojis.check_mark} Created Code Jam with {len(teams)} teams.") - - async def get_category(self, guild: discord.Guild) -> discord.CategoryChannel: - """ - Return a code jam category. - - If all categories are full or none exist, create a new category. - """ - for category in guild.categories: - if category.name == CATEGORY_NAME and len(category.channels) < MAX_CHANNELS: - return category - - return await self.create_category(guild) - - async def create_category(self, guild: discord.Guild) -> discord.CategoryChannel: - """Create a new code jam category and return it.""" - log.info("Creating a new code jam category.") - - category_overwrites = { - guild.default_role: discord.PermissionOverwrite(read_messages=False), - guild.me: discord.PermissionOverwrite(read_messages=True) - } - - category = await guild.create_category_channel( - CATEGORY_NAME, - overwrites=category_overwrites, - reason="It's code jam time!" - ) - - await self.send_status_update( - guild, f"Created a new category with the ID {category.id} for this Code Jam's team channels." - ) - - return category - - @staticmethod - def get_overwrites( - members: list[tuple[discord.Member, bool]], - guild: discord.Guild, - ) -> dict[t.Union[discord.Member, discord.Role], discord.PermissionOverwrite]: - """Get code jam team channels permission overwrites.""" - team_channel_overwrites = { - guild.default_role: discord.PermissionOverwrite(read_messages=False), - guild.get_role(Roles.code_jam_event_team): discord.PermissionOverwrite(read_messages=True) - } - - for member, _ in members: - team_channel_overwrites[member] = discord.PermissionOverwrite( - read_messages=True - ) - - return team_channel_overwrites - - async def create_team_channel( - self, - guild: discord.Guild, - team_name: str, - members: list[tuple[discord.Member, bool]], - team_leaders: discord.Role - ) -> None: - """Create the team's text channel.""" - await self.add_team_leader_roles(members, team_leaders) - - # Get permission overwrites and category - team_channel_overwrites = self.get_overwrites(members, guild) - code_jam_category = await self.get_category(guild) - - # Create a text channel for the team - await code_jam_category.create_text_channel( - team_name, - overwrites=team_channel_overwrites, - ) - - async def create_team_leader_channel(self, guild: discord.Guild, team_leaders: discord.Role) -> None: - """Create the Team Leader Chat channel for the Code Jam team leaders.""" - category: discord.CategoryChannel = guild.get_channel(Categories.summer_code_jam) - - team_leaders_chat = await category.create_text_channel( - name="team-leaders-chat", - overwrites={ - guild.default_role: discord.PermissionOverwrite(read_messages=False), - team_leaders: discord.PermissionOverwrite(read_messages=True) - } - ) - - await self.send_status_update(guild, f"Created {team_leaders_chat.mention} in the {category} category.") - - async def send_status_update(self, guild: discord.Guild, message: str) -> None: - """Inform the events lead with a status update when the command is ran.""" - channel: discord.TextChannel = guild.get_channel(Channels.code_jam_planning) - - await channel.send(f"<@&{Roles.events_lead}>\n\n{message}") - - @staticmethod - async def add_team_leader_roles(members: list[tuple[discord.Member, bool]], team_leaders: discord.Role) -> None: - """Assign team leader role, the jammer role and their team role.""" - for member, is_leader in members: - if is_leader: - await member.add_roles(team_leaders) - - -def setup(bot: Bot) -> None: - """Load the CodeJams cog.""" - bot.add_cog(CodeJams(bot)) diff --git a/bot/exts/filters/antimalware.py b/bot/exts/filters/antimalware.py index 3f6213db3..0eedeb0fb 100644 --- a/bot/exts/filters/antimalware.py +++ b/bot/exts/filters/antimalware.py @@ -7,7 +7,7 @@ from discord.ext.commands import Cog from bot.bot import Bot from bot.constants import Channels, Filter, URLs -from bot.exts.events.code_jams._cog import CATEGORY_NAME as JAM_CATEGORY_NAME +from bot.exts.events.code_jams._channels import CATEGORY_NAME as JAM_CATEGORY_NAME log = logging.getLogger(__name__) diff --git a/bot/exts/filters/antispam.py b/bot/exts/filters/antispam.py index 124905cb4..1830e23b8 100644 --- a/bot/exts/filters/antispam.py +++ b/bot/exts/filters/antispam.py @@ -17,7 +17,7 @@ from bot.constants import ( Guild as GuildConfig, Icons, ) from bot.converters import Duration -from bot.exts.events.code_jams._cog import CATEGORY_NAME as JAM_CATEGORY_NAME +from bot.exts.events.code_jams._channels import CATEGORY_NAME as JAM_CATEGORY_NAME from bot.exts.moderation.modlog import ModLog from bot.utils import lock, scheduling from bot.utils.messages import format_user, send_attachments diff --git a/bot/exts/filters/filtering.py b/bot/exts/filters/filtering.py index 0810425e2..10cc7885d 100644 --- a/bot/exts/filters/filtering.py +++ b/bot/exts/filters/filtering.py @@ -19,7 +19,7 @@ from bot.constants import ( Channels, Colours, Filter, Guild, Icons, URLs ) -from bot.exts.events.code_jams._cog import CATEGORY_NAME as JAM_CATEGORY_NAME +from bot.exts.events.code_jams._channels import CATEGORY_NAME as JAM_CATEGORY_NAME from bot.exts.moderation.modlog import ModLog from bot.utils.messages import format_user from bot.utils.regex import INVITE_RE diff --git a/tests/bot/exts/events/test_code_jams.py b/tests/bot/exts/events/test_code_jams.py index d7b8aa4d2..b9ee1e363 100644 --- a/tests/bot/exts/events/test_code_jams.py +++ b/tests/bot/exts/events/test_code_jams.py @@ -1,14 +1,15 @@ import unittest -from unittest.mock import AsyncMock, MagicMock, create_autospec +from unittest.mock import AsyncMock, MagicMock, create_autospec, patch from discord import CategoryChannel from discord.ext.commands import BadArgument from bot.constants import Roles -from bot.exts.events.code_jams import _cog +from bot.exts.events import code_jams +from bot.exts.events.code_jams import _channels, _cog from tests.helpers import ( MockAttachment, MockBot, MockCategoryChannel, MockContext, - MockGuild, MockMember, MockRole, MockTextChannel + MockGuild, MockMember, MockRole, MockTextChannel, autospec ) TEST_CSV = b"""\ @@ -49,7 +50,9 @@ class JamCodejamCreateTests(unittest.IsolatedAsyncioTestCase): with self.assertRaises(BadArgument): await self.cog.create(self.cog, self.ctx, None) - async def test_result_sending(self): + @patch.object(_channels, "create_team_channel") + @patch.object(_channels, "create_team_leader_channel") + async def test_result_sending(self, create_leader_channel, create_team_channel): """Should call `ctx.send` when everything goes right.""" self.ctx.message.attachments = [MockAttachment()] self.ctx.message.attachments[0].read = AsyncMock() @@ -61,14 +64,12 @@ class JamCodejamCreateTests(unittest.IsolatedAsyncioTestCase): self.ctx.guild.create_role = AsyncMock() self.ctx.guild.create_role.return_value = team_leaders - self.cog.create_team_channel = AsyncMock() - self.cog.create_team_leader_channel = AsyncMock() self.cog.add_roles = AsyncMock() await self.cog.create(self.cog, self.ctx, None) - self.cog.create_team_channel.assert_awaited() - self.cog.create_team_leader_channel.assert_awaited_once_with( + create_team_channel.assert_awaited() + create_leader_channel.assert_awaited_once_with( self.ctx.guild, team_leaders ) self.ctx.send.assert_awaited_once() @@ -81,25 +82,24 @@ class JamCodejamCreateTests(unittest.IsolatedAsyncioTestCase): self.ctx.send.assert_awaited_once() - async def test_category_doesnt_exist(self): + @patch.object(_channels, "_send_status_update") + async def test_category_doesnt_exist(self, update): """Should create a new code jam category.""" subtests = ( [], - [get_mock_category(_cog.MAX_CHANNELS, _cog.CATEGORY_NAME)], - [get_mock_category(_cog.MAX_CHANNELS - 2, "other")], + [get_mock_category(_channels.MAX_CHANNELS, _channels.CATEGORY_NAME)], + [get_mock_category(_channels.MAX_CHANNELS - 2, "other")], ) - self.cog.send_status_update = AsyncMock() - for categories in subtests: - self.cog.send_status_update.reset_mock() + update.reset_mock() self.guild.reset_mock() self.guild.categories = categories with self.subTest(categories=categories): - actual_category = await self.cog.get_category(self.guild) + actual_category = await _channels._get_category(self.guild) - self.cog.send_status_update.assert_called_once() + update.assert_called_once() self.guild.create_category_channel.assert_awaited_once() category_overwrites = self.guild.create_category_channel.call_args[1]["overwrites"] @@ -109,45 +109,41 @@ class JamCodejamCreateTests(unittest.IsolatedAsyncioTestCase): async def test_category_channel_exist(self): """Should not try to create category channel.""" - expected_category = get_mock_category(_cog.MAX_CHANNELS - 2, _cog.CATEGORY_NAME) + expected_category = get_mock_category(_channels.MAX_CHANNELS - 2, _channels.CATEGORY_NAME) self.guild.categories = [ - get_mock_category(_cog.MAX_CHANNELS - 2, "other"), + get_mock_category(_channels.MAX_CHANNELS - 2, "other"), expected_category, - get_mock_category(0, _cog.CATEGORY_NAME), + get_mock_category(0, _channels.CATEGORY_NAME), ] - actual_category = await self.cog.get_category(self.guild) + actual_category = await _channels._get_category(self.guild) self.assertEqual(expected_category, actual_category) async def test_channel_overwrites(self): """Should have correct permission overwrites for users and roles.""" leader = (MockMember(), True) members = [leader] + [(MockMember(), False) for _ in range(4)] - overwrites = self.cog.get_overwrites(members, self.guild) + overwrites = _channels._get_overwrites(members, self.guild) for member, _ in members: self.assertTrue(overwrites[member].read_messages) - async def test_team_channels_creation(self): + @patch.object(_channels, "_get_overwrites") + @patch.object(_channels, "_get_category") + @autospec(_channels, "_add_team_leader_roles", pass_mocks=False) + async def test_team_channels_creation(self, get_category, get_overwrites): """Should create a text channel for a team.""" team_leaders = MockRole() members = [(MockMember(), True)] + [(MockMember(), False) for _ in range(5)] category = MockCategoryChannel() category.create_text_channel = AsyncMock() - self.cog.get_overwrites = MagicMock() - self.cog.get_category = AsyncMock() - self.cog.get_category.return_value = category - self.cog.add_team_leader_roles = AsyncMock() - - await self.cog.create_team_channel(self.guild, "my-team", members, team_leaders) - self.cog.add_team_leader_roles.assert_awaited_once_with(members, team_leaders) - self.cog.get_overwrites.assert_called_once_with(members, self.guild) - self.cog.get_category.assert_awaited_once_with(self.guild) + get_category.return_value = category + await _channels.create_team_channel(self.guild, "my-team", members, team_leaders) category.create_text_channel.assert_awaited_once_with( "my-team", - overwrites=self.cog.get_overwrites.return_value + overwrites=get_overwrites.return_value ) async def test_jam_roles_adding(self): @@ -156,7 +152,7 @@ class JamCodejamCreateTests(unittest.IsolatedAsyncioTestCase): leader = MockMember() members = [(leader, True)] + [(MockMember(), False) for _ in range(4)] - await self.cog.add_team_leader_roles(members, leader_role) + await _channels._add_team_leader_roles(members, leader_role) leader.add_roles.assert_awaited_once_with(leader_role) for member, is_leader in members: @@ -170,5 +166,5 @@ class CodeJamSetup(unittest.TestCase): def test_setup(self): """Should call `bot.add_cog`.""" bot = MockBot() - _cog.setup(bot) + code_jams.setup(bot) bot.add_cog.assert_called_once() -- cgit v1.2.3 From d4271b0c1df1e7acbbf694c9f585bacc25edaecd Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Thu, 8 Jul 2021 22:09:12 +0300 Subject: More code jam functionality - An info embed with team the member is in. The team is decided by finding in which channel the member has overwrites. - Command to move a member from one team to another by changing the permissions of the appropriate team channels. - A command to end the code jam and delete all the team channels and categories. --- bot/exts/events/code_jams/_cog.py | 105 +++++++++++++++++++++++++++++++++++++- 1 file changed, 104 insertions(+), 1 deletion(-) diff --git a/bot/exts/events/code_jams/_cog.py b/bot/exts/events/code_jams/_cog.py index 2d0873de7..39577a5c3 100644 --- a/bot/exts/events/code_jams/_cog.py +++ b/bot/exts/events/code_jams/_cog.py @@ -3,6 +3,8 @@ import logging import typing as t from collections import defaultdict +import discord +from discord import Colour, Embed, Guild, Member from discord.ext import commands from bot.bot import Bot @@ -20,7 +22,9 @@ class CodeJams(commands.Cog): def __init__(self, bot: Bot): self.bot = bot - @commands.group() + self.end_counter = 0 + + @commands.group(aliases=("cj", "jam")) @commands.has_any_role(Roles.admins) async def codejam(self, ctx: commands.Context) -> None: """A Group of commands for managing Code Jams.""" @@ -69,3 +73,102 @@ class CodeJams(commands.Cog): await _channels.create_team_leader_channel(ctx.guild, team_leaders) await ctx.send(f"{Emojis.check_mark} Created Code Jam with {len(teams)} teams.") + + @codejam.command() + @commands.has_any_role(Roles.admins) + async def end(self, ctx: commands.Context) -> None: + """ + Call it three times while spinning around for it all to end. + + Deletes all code jam channels and wipes the cache. + """ + self.end_counter += 1 + if self.end_counter == 1: + await ctx.send("Are you sure about that?") + return + if self.end_counter == 2: + await ctx.send("Are you *really really* sure about that?") + return + + self.end_counter = 0 + + for category in self.jam_categories(ctx.guild): + for channel in category.channels: + await channel.delete(reason="Code jam ended.") + await category.delete(reason="Code jam ended.") + + await ctx.message.add_reaction(Emojis.check_mark) + + @codejam.command() + @commands.has_any_role(Roles.admins, Roles.code_jam_event_team) + async def info(self, ctx: commands.Context, member: Member) -> None: + """ + Send an info embed about the member with the team they're in. + + The team is found by searching the permissions of the team channels. + """ + channel = self.team_channel(ctx.guild, member) + if not channel: + await ctx.send(":x: I can't find the team channel for this member.") + return + + embed = Embed( + title=str(member), + colour=Colour.blurple() + ) + embed.add_field(name="Team", value=self.team_name(channel), inline=True) + + await ctx.send(embed=embed) + + @codejam.command() + @commands.has_any_role(Roles.admins) + async def move(self, ctx: commands.Context, member: Member, new_team_name: str) -> None: + """Move participant from one team to another by changing the user's permissions for the relevant channels.""" + old_team_channel = self.team_channel(ctx.guild, member) + if not old_team_channel: + await ctx.send(":x: I can't find the team channel for this member.") + return + + if old_team_channel.name == new_team_name or self.team_name(old_team_channel) == new_team_name: + await ctx.send(f"`{member}` is already in `{new_team_name}`.") + return + + new_team_channel = self.team_channel(ctx.guild, new_team_name) + if not new_team_channel: + await ctx.send(f":x: I can't find a team channel named `{new_team_name}`.") + return + + await old_team_channel.set_permissions(member, overwrite=None, reason=f"Participant moved to {new_team_name}") + await new_team_channel.set_permissions( + member, + overwrite=discord.PermissionOverwrite(read_messages=True), + reason=f"Participant moved from {old_team_channel.name}" + ) + + await ctx.send( + f"Participant moved from `{self.team_name(old_team_channel)}` to `{self.team_name(new_team_channel)}`." + ) + + @staticmethod + def jam_categories(guild: Guild) -> list[discord.CategoryChannel]: + """Get all the code jam team categories.""" + return [category for category in guild.categories if category.name == _channels.CATEGORY_NAME] + + @staticmethod + def team_channel(guild: Guild, criterion: t.Union[str, Member]) -> t.Optional[discord.TextChannel]: + """Get a team channel through either a participant or the team name.""" + for category in CodeJams.jam_categories(guild): + for channel in category.channels: + if isinstance(channel, discord.TextChannel): + if ( + # If it's a string. + criterion == channel.name or criterion == CodeJams.team_name(channel) + # If it's a member. + or criterion in channel.overwrites + ): + return channel + + @staticmethod + def team_name(channel: discord.TextChannel) -> str: + """Retrieves the team name from the given channel.""" + return channel.name.replace("-", " ").title() -- cgit v1.2.3 From bc390bcb66d060aeba29c835d6c5cceb3d366626 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Thu, 8 Jul 2021 23:14:47 +0300 Subject: Added command to remove from team --- bot/exts/events/code_jams/_cog.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/bot/exts/events/code_jams/_cog.py b/bot/exts/events/code_jams/_cog.py index 39577a5c3..862e53a13 100644 --- a/bot/exts/events/code_jams/_cog.py +++ b/bot/exts/events/code_jams/_cog.py @@ -149,6 +149,18 @@ class CodeJams(commands.Cog): f"Participant moved from `{self.team_name(old_team_channel)}` to `{self.team_name(new_team_channel)}`." ) + @codejam.command() + @commands.has_any_role(Roles.admins) + async def remove(self, ctx: commands.Context, member: Member) -> None: + """Removes the participant from their team. Does not remove the participants or leader roles.""" + channel = self.team_channel(ctx.guild, member) + if not channel: + await ctx.send(":x: I can't find the team channel for this member.") + return + + await channel.set_permissions(member, overwrite=None, reason="Participant removed from the team.") + await ctx.send(f"Removed the participant from `{self.team_name(channel)}`.") + @staticmethod def jam_categories(guild: Guild) -> list[discord.CategoryChannel]: """Get all the code jam team categories.""" -- cgit v1.2.3 From fdc6f2387ee9894c21f84fb41d9a909a52e42971 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Thu, 8 Jul 2021 23:23:34 +0300 Subject: Fix end command docstring --- bot/exts/events/code_jams/_cog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/events/code_jams/_cog.py b/bot/exts/events/code_jams/_cog.py index 862e53a13..83e2e18ce 100644 --- a/bot/exts/events/code_jams/_cog.py +++ b/bot/exts/events/code_jams/_cog.py @@ -78,9 +78,9 @@ class CodeJams(commands.Cog): @commands.has_any_role(Roles.admins) async def end(self, ctx: commands.Context) -> None: """ - Call it three times while spinning around for it all to end. + Deletes all code jam channels. - Deletes all code jam channels and wipes the cache. + Call it three times while spinning around for it all to end. """ self.end_counter += 1 if self.end_counter == 1: -- cgit v1.2.3 From 7049a0052b375d8fa28288ab8ca63f7b99c069ed Mon Sep 17 00:00:00 2001 From: D0rs4n <41237606+D0rs4n@users.noreply.github.com> Date: Thu, 8 Jul 2021 13:52:28 +0200 Subject: Create `join_role_stats` function in helpers Add `join_role_stats` function that joins the relevant information (number of members) of the given roles into one group under a pre-specified `name` --- bot/utils/helpers.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/bot/utils/helpers.py b/bot/utils/helpers.py index 3501a3933..1da5da5a3 100644 --- a/bot/utils/helpers.py +++ b/bot/utils/helpers.py @@ -1,6 +1,8 @@ from abc import ABCMeta -from typing import Optional +from types import List +from typing import Dict, Optional +from discord import Guild from discord.ext.commands import CogMeta @@ -30,3 +32,11 @@ def has_lines(string: str, count: int) -> bool: def pad_base64(data: str) -> str: """Return base64 `data` with padding characters to ensure its length is a multiple of 4.""" return data + "=" * (-len(data) % 4) + + +def join_role_stats(role_ids: List[int], name: str, guild: Guild) -> Dict[str, int]: + """Return a dict object with the number of `members` of each role given, and the `name` for this joined group.""" + members = [] + for role_id in role_ids: + members += guild.get_role(role_id).members + return {name: len(set(members))} -- cgit v1.2.3 From a8e30f6c67cd64a5cf5a1ab0b3668a06a9621485 Mon Sep 17 00:00:00 2001 From: D0rs4n <41237606+D0rs4n@users.noreply.github.com> Date: Thu, 8 Jul 2021 13:53:48 +0200 Subject: Add `Leads` role(s) to the server information embed --- bot/exts/info/information.py | 7 ++++++- bot/utils/helpers.py | 3 +-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index 1b1243118..677b5d1f7 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -17,8 +17,10 @@ from bot.decorators import in_whitelist from bot.pagination import LinePaginator from bot.utils.channel import is_mod_channel, is_staff_channel from bot.utils.checks import cooldown_with_role_bypass, has_no_roles_check, in_whitelist_check +from bot.utils.helpers import join_role_stats from bot.utils.time import humanize_delta, time_since + log = logging.getLogger(__name__) @@ -50,7 +52,10 @@ class Information(Cog): constants.Roles.owners, constants.Roles.contributors, ) ) - return {role.name.title(): len(role.members) for role in roles} + role_stats = {role.name.title(): len(role.members) for role in roles} + role_stats.update( + **join_role_stats([constants.Roles.project_leads, constants.Roles.domain_leads], "Leads", guild)) + return role_stats def get_extended_server_info(self, ctx: Context) -> str: """Return additional server info only visible in moderation channels.""" diff --git a/bot/utils/helpers.py b/bot/utils/helpers.py index 1da5da5a3..b0d17c3b8 100644 --- a/bot/utils/helpers.py +++ b/bot/utils/helpers.py @@ -1,6 +1,5 @@ from abc import ABCMeta -from types import List -from typing import Dict, Optional +from typing import Dict, List, Optional from discord import Guild from discord.ext.commands import CogMeta -- cgit v1.2.3 From 19cd66fa1615ed8f220cef8dbff5f826c7d4670c Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sun, 25 Jul 2021 00:39:22 +0300 Subject: Improved codejam end confirmation The command now sends the details of all channels about to be deleted to the pasting service, and confirmation is done through a reaction by the invoker of the command within a limited time (10 seconds). --- bot/exts/events/code_jams/_cog.py | 74 +++++++++++++++++++++++++++++++-------- 1 file changed, 59 insertions(+), 15 deletions(-) diff --git a/bot/exts/events/code_jams/_cog.py b/bot/exts/events/code_jams/_cog.py index 83e2e18ce..d0c206b5e 100644 --- a/bot/exts/events/code_jams/_cog.py +++ b/bot/exts/events/code_jams/_cog.py @@ -1,3 +1,4 @@ +import asyncio import csv import logging import typing as t @@ -10,10 +11,12 @@ from discord.ext import commands from bot.bot import Bot from bot.constants import Emojis, Roles from bot.exts.events.code_jams import _channels +from bot.utils.services import send_to_paste_service log = logging.getLogger(__name__) TEAM_LEADERS_COLOUR = 0x11806a +DELETION_REACTION = "\U0001f4a5" class CodeJams(commands.Cog): @@ -22,8 +25,6 @@ class CodeJams(commands.Cog): def __init__(self, bot: Bot): self.bot = bot - self.end_counter = 0 - @commands.group(aliases=("cj", "jam")) @commands.has_any_role(Roles.admins) async def codejam(self, ctx: commands.Context) -> None: @@ -80,24 +81,67 @@ class CodeJams(commands.Cog): """ Deletes all code jam channels. - Call it three times while spinning around for it all to end. + Displays a confirmation message with the categories and channels to be deleted. Pressing the added reaction + deletes those channels. """ - self.end_counter += 1 - if self.end_counter == 1: - await ctx.send("Are you sure about that?") - return - if self.end_counter == 2: - await ctx.send("Are you *really really* sure about that?") + def predicate_deletion_emoji_reaction(reaction: discord.Reaction, user: discord.User) -> bool: + """Return True if the reaction :boom: was added by the context message author on this message.""" + return ( + reaction.message.id == message.id + and user.id == ctx.author.id + and str(reaction) == DELETION_REACTION + ) + + # A copy of the list of channels is stored. This is to make sure that we delete precisely the channels displayed + # in the confirmation message. + categories = self.jam_categories(ctx.guild) + category_channels = {category: category.channels.copy() for category in categories} + + confirmation_message = await self._build_confirmation_message(category_channels) + message = await ctx.send(confirmation_message) + await message.add_reaction(DELETION_REACTION) + try: + await self.bot.wait_for( + 'reaction_add', + check=predicate_deletion_emoji_reaction, + timeout=10 + ) + + except asyncio.TimeoutError: + await message.clear_reaction(DELETION_REACTION) return - self.end_counter = 0 + else: + await message.clear_reaction(DELETION_REACTION) + for category, channels in category_channels.items(): + for channel in channels: + await channel.delete(reason="Code jam ended.") + await category.delete(reason="Code jam ended.") - for category in self.jam_categories(ctx.guild): - for channel in category.channels: - await channel.delete(reason="Code jam ended.") - await category.delete(reason="Code jam ended.") + await message.add_reaction(Emojis.check_mark) + + @staticmethod + async def _build_confirmation_message( + categories: dict[discord.CategoryChannel, list[discord.abc.GuildChannel]] + ) -> str: + """Sends details of the channels to be deleted to the pasting service, and formats the confirmation message.""" + def channel_repr(channel: discord.abc.GuildChannel) -> str: + """Formats the channel name and ID and a readable format.""" + return f"{channel.name} ({channel.id})" + + def format_category_info(category: discord.CategoryChannel, channels: list[discord.abc.GuildChannel]) -> str: + """Displays the category and the channels within it in a readable format.""" + return f"{channel_repr(category)}:" + "".join(f"\n - {channel_repr(channel)}" for channel in channels) + + deletion_details = "\n\n".join( + format_category_info(category, channels) for category, channels in categories.items() + ) + + url = await send_to_paste_service(deletion_details) + if url is None: + url = "**Unable to send deletion details to the pasting service.**" - await ctx.message.add_reaction(Emojis.check_mark) + return f"Are you sure you want to delete all code jam channels?\n\nThe channels to be deleted: {url}" @codejam.command() @commands.has_any_role(Roles.admins, Roles.code_jam_event_team) -- cgit v1.2.3 From b2b9d23469960bcf9c8a0e17af7a6c0d51dda58a Mon Sep 17 00:00:00 2001 From: D0rs4n <41237606+D0rs4n@users.noreply.github.com> Date: Wed, 28 Jul 2021 14:45:21 +0200 Subject: Handle non-existent roles in `join_role_stats` --- bot/utils/helpers.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/bot/utils/helpers.py b/bot/utils/helpers.py index b0d17c3b8..cb1d46411 100644 --- a/bot/utils/helpers.py +++ b/bot/utils/helpers.py @@ -2,7 +2,7 @@ from abc import ABCMeta from typing import Dict, List, Optional from discord import Guild -from discord.ext.commands import CogMeta +from discord.ext.commands import BadArgument, CogMeta class CogABCMeta(CogMeta, ABCMeta): @@ -37,5 +37,8 @@ def join_role_stats(role_ids: List[int], name: str, guild: Guild) -> Dict[str, i """Return a dict object with the number of `members` of each role given, and the `name` for this joined group.""" members = [] for role_id in role_ids: - members += guild.get_role(role_id).members + if (role := guild.get_role(role_id)) is None: + raise BadArgument("Unable to fetch role data, the specified role does not exist.") + else: + members += role.members return {name: len(set(members))} -- cgit v1.2.3 From ed00aad9245769137a47e3e536bfa0da93b63917 Mon Sep 17 00:00:00 2001 From: D0rs4n <41237606+D0rs4n@users.noreply.github.com> Date: Thu, 29 Jul 2021 11:12:56 +0200 Subject: Modify error handling in join_role_stats and move it to the Information Cog --- bot/exts/info/information.py | 17 ++++++++++++++--- bot/utils/helpers.py | 16 ++-------------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index eef18298c..54616a1c6 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -3,7 +3,7 @@ import logging import pprint import textwrap from collections import defaultdict -from typing import Any, DefaultDict, Dict, Mapping, Optional, Tuple, Union +from typing import Any, DefaultDict, Dict, List, Mapping, Optional, Tuple, Union import rapidfuzz from discord import AllowedMentions, Colour, Embed, Guild, Message, Role @@ -17,7 +17,6 @@ from bot.decorators import in_whitelist from bot.pagination import LinePaginator from bot.utils.channel import is_mod_channel, is_staff_channel from bot.utils.checks import cooldown_with_role_bypass, has_no_roles_check, in_whitelist_check -from bot.utils.helpers import join_role_stats from bot.utils.time import TimestampFormats, discord_timestamp, humanize_delta @@ -43,6 +42,17 @@ class Information(Cog): return channel_counter + @staticmethod + def join_role_stats(role_ids: List[int], name: str, guild: Guild) -> Dict[str, int]: + """Return a dictionary with the number of `members` of each role given, and the `name` for this joined group.""" + members = [] + for role_id in role_ids: + if (role := guild.get_role(role_id)) is None: + raise ValueError(f"Could not fetch data for role {role} for server embed.") + else: + members += role.members + return {name: len(set(members))} + @staticmethod def get_member_counts(guild: Guild) -> Dict[str, int]: """Return the total number of members for certain roles in `guild`.""" @@ -54,7 +64,8 @@ class Information(Cog): ) role_stats = {role.name.title(): len(role.members) for role in roles} role_stats.update( - **join_role_stats([constants.Roles.project_leads, constants.Roles.domain_leads], "Leads", guild)) + **Information.join_role_stats([constants.Roles.project_leads, constants.Roles.domain_leads], "Leads", guild) + ) return role_stats def get_extended_server_info(self, ctx: Context) -> str: diff --git a/bot/utils/helpers.py b/bot/utils/helpers.py index cb1d46411..3501a3933 100644 --- a/bot/utils/helpers.py +++ b/bot/utils/helpers.py @@ -1,8 +1,7 @@ from abc import ABCMeta -from typing import Dict, List, Optional +from typing import Optional -from discord import Guild -from discord.ext.commands import BadArgument, CogMeta +from discord.ext.commands import CogMeta class CogABCMeta(CogMeta, ABCMeta): @@ -31,14 +30,3 @@ def has_lines(string: str, count: int) -> bool: def pad_base64(data: str) -> str: """Return base64 `data` with padding characters to ensure its length is a multiple of 4.""" return data + "=" * (-len(data) % 4) - - -def join_role_stats(role_ids: List[int], name: str, guild: Guild) -> Dict[str, int]: - """Return a dict object with the number of `members` of each role given, and the `name` for this joined group.""" - members = [] - for role_id in role_ids: - if (role := guild.get_role(role_id)) is None: - raise BadArgument("Unable to fetch role data, the specified role does not exist.") - else: - members += role.members - return {name: len(set(members))} -- cgit v1.2.3 From 43240643642fae3fe92413189e99d772fdebf562 Mon Sep 17 00:00:00 2001 From: D0rs4n <41237606+D0rs4n@users.noreply.github.com> Date: Fri, 30 Jul 2021 03:25:25 +0200 Subject: Update join_role_stats, add new custom error - Add new custom error to handle non-existent roles in the Information cog - Update join_role_stats to use built in generics for typing --- bot/errors.py | 12 ++++++++++++ bot/exts/info/information.py | 24 +++++++++++++----------- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/bot/errors.py b/bot/errors.py index 46efb6d4f..ce2371f56 100644 --- a/bot/errors.py +++ b/bot/errors.py @@ -41,3 +41,15 @@ class BrandingMisconfiguration(RuntimeError): """Raised by the Branding cog when a misconfigured event is encountered.""" pass + + +class NonExistentRoleError(ValueError): + """ + Raised by the Information Cog when encountering a role that does not exist. + + Attributes: + `role_id` -- the ID of the role that does not exist + """ + + def __init__(self, role_id: int): + super().__init__(f"Could not fetch data for role {role_id}") diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index 54616a1c6..78a9b2122 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -3,7 +3,7 @@ import logging import pprint import textwrap from collections import defaultdict -from typing import Any, DefaultDict, Dict, List, Mapping, Optional, Tuple, Union +from typing import Any, DefaultDict, Mapping, Optional, Tuple, Union import rapidfuzz from discord import AllowedMentions, Colour, Embed, Guild, Message, Role @@ -14,12 +14,12 @@ from bot.api import ResponseCodeError from bot.bot import Bot from bot.converters import FetchedMember from bot.decorators import in_whitelist +from bot.errors import NonExistentRoleError from bot.pagination import LinePaginator from bot.utils.channel import is_mod_channel, is_staff_channel from bot.utils.checks import cooldown_with_role_bypass, has_no_roles_check, in_whitelist_check from bot.utils.time import TimestampFormats, discord_timestamp, humanize_delta - log = logging.getLogger(__name__) @@ -43,25 +43,27 @@ class Information(Cog): return channel_counter @staticmethod - def join_role_stats(role_ids: List[int], name: str, guild: Guild) -> Dict[str, int]: + def join_role_stats(role_ids: list[int], name: str, guild: Guild) -> dict[str, int]: """Return a dictionary with the number of `members` of each role given, and the `name` for this joined group.""" members = [] for role_id in role_ids: if (role := guild.get_role(role_id)) is None: - raise ValueError(f"Could not fetch data for role {role} for server embed.") + raise NonExistentRoleError(role_id) else: members += role.members return {name: len(set(members))} @staticmethod - def get_member_counts(guild: Guild) -> Dict[str, int]: + def get_member_counts(guild: Guild) -> dict[str, int]: """Return the total number of members for certain roles in `guild`.""" - roles = ( - guild.get_role(role_id) for role_id in ( - constants.Roles.helpers, constants.Roles.mod_team, constants.Roles.admins, - constants.Roles.owners, constants.Roles.contributors, - ) - ) + role_ids = [constants.Roles.helpers, constants.Roles.mod_team, constants.Roles.admins, + constants.Roles.owners, constants.Roles.contributors] + roles = [] + for role_id in role_ids: + if (role := guild.get_role(role_id)) is None: + raise NonExistentRoleError(role_id) + else: + roles.append(role) role_stats = {role.name.title(): len(role.members) for role in roles} role_stats.update( **Information.join_role_stats([constants.Roles.project_leads, constants.Roles.domain_leads], "Leads", guild) -- cgit v1.2.3 From 3c91cfb852de5e4d01e8e1778e366ddc59e57f0f Mon Sep 17 00:00:00 2001 From: D0rs4n <41237606+D0rs4n@users.noreply.github.com> Date: Sun, 1 Aug 2021 18:53:04 +0200 Subject: Update join_role_stats and NonExistentError to be clear --- bot/errors.py | 3 ++- bot/exts/info/information.py | 13 +++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/bot/errors.py b/bot/errors.py index ce2371f56..69c588f4a 100644 --- a/bot/errors.py +++ b/bot/errors.py @@ -52,4 +52,5 @@ class NonExistentRoleError(ValueError): """ def __init__(self, role_id: int): - super().__init__(f"Could not fetch data for role {role_id}") + self.role_id = role_id + super().__init__(f"Could not fetch data for role {self.role_id}") diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index 78a9b2122..62fde5473 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -47,10 +47,10 @@ class Information(Cog): """Return a dictionary with the number of `members` of each role given, and the `name` for this joined group.""" members = [] for role_id in role_ids: - if (role := guild.get_role(role_id)) is None: - raise NonExistentRoleError(role_id) + if (role := guild.get_role(role_id)) is not None: + members.append(role.members) else: - members += role.members + raise NonExistentRoleError(role_id) return {name: len(set(members))} @staticmethod @@ -60,10 +60,11 @@ class Information(Cog): constants.Roles.owners, constants.Roles.contributors] roles = [] for role_id in role_ids: - if (role := guild.get_role(role_id)) is None: - raise NonExistentRoleError(role_id) - else: + if (role := guild.get_role(role_id)) is not None: roles.append(role) + else: + raise NonExistentRoleError(role_id) + role_stats = {role.name.title(): len(role.members) for role in roles} role_stats.update( **Information.join_role_stats([constants.Roles.project_leads, constants.Roles.domain_leads], "Leads", guild) -- cgit v1.2.3 From 587e52e1bd05eaf66035b6089cb37149399c8851 Mon Sep 17 00:00:00 2001 From: wookie184 Date: Sun, 1 Aug 2021 19:58:46 +0100 Subject: Add commands to turn automatic review posting on or off --- bot/exts/recruitment/talentpool/_cog.py | 59 ++++++++++++++++++++++++++++++--- 1 file changed, 55 insertions(+), 4 deletions(-) diff --git a/bot/exts/recruitment/talentpool/_cog.py b/bot/exts/recruitment/talentpool/_cog.py index 03326cab2..5d90701bf 100644 --- a/bot/exts/recruitment/talentpool/_cog.py +++ b/bot/exts/recruitment/talentpool/_cog.py @@ -5,12 +5,13 @@ from io import StringIO from typing import Union import discord +from async_rediscache import RedisCache from discord import Color, Embed, Member, PartialMessage, RawReactionActionEvent, User from discord.ext.commands import Cog, Context, group, has_any_role from bot.api import ResponseCodeError from bot.bot import Bot -from bot.constants import Channels, Emojis, Guild, MODERATION_ROLES, STAFF_ROLES, Webhooks +from bot.constants import Channels, Emojis, Guild, MODERATION_ROLES, Roles, STAFF_ROLES, Webhooks from bot.converters import FetchedMember from bot.exts.moderation.watchchannels._watchchannel import WatchChannel from bot.exts.recruitment.talentpool._review import Reviewer @@ -25,6 +26,8 @@ log = logging.getLogger(__name__) class TalentPool(WatchChannel, Cog, name="Talentpool"): """Relays messages of helper candidates to a watch channel to observe them.""" + talentpool_settings = RedisCache() + def __init__(self, bot: Bot) -> None: super().__init__( bot, @@ -37,7 +40,18 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): ) self.reviewer = Reviewer(self.__class__.__name__, bot, self) - self.bot.loop.create_task(self.reviewer.reschedule_reviews()) + self.bot.loop.create_task(self.schedule_autoreviews()) + + async def schedule_autoreviews(self) -> None: + """Reschedule reviews for active nominations if autoreview is enabled.""" + if await self.autoreview_enabled(): + await self.reviewer.reschedule_reviews() + else: + self.log.trace('Not scheduling reviews as autoreview is disabled.') + + async def autoreview_enabled(self) -> bool: + """Return whether automatic posting of nomination reviews is enabled.""" + return await self.talentpool_settings.get('autoreview_enabled', True) @group(name='talentpool', aliases=('tp', 'talent', 'nomination', 'n'), invoke_without_command=True) @has_any_role(*MODERATION_ROLES) @@ -45,6 +59,42 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): """Highlights the activity of helper nominees by relaying their messages to the talent pool channel.""" await ctx.send_help(ctx.command) + @nomination_group.group(name='autoreview', invoke_without_command=True) + @has_any_role(*MODERATION_ROLES) + async def nomination_autoreview_group(self, ctx: Context) -> None: + """Commands for enabling or disabling autoreview.""" + await ctx.send_help(ctx.command) + + @nomination_autoreview_group.command(name='on') + @has_any_role(Roles.admins) + async def autoreview_on(self, ctx: Context) -> None: + """ + Turn on automatic posting of reviews. + + This will post reviews up to one day overdue, older nominations can be + manually reviewed with `!tp post_review `. + """ + await self.talentpool_settings.set('autoreview_enabled', True) + await self.reviewer.reschedule_reviews() + await ctx.send(':white_check_mark: Autoreview turned on') + + @nomination_autoreview_group.command(name='off') + @has_any_role(Roles.admins) + async def autoreview_off(self, ctx: Context) -> None: + """Turn off automatic posting of reviews.""" + await self.talentpool_settings.set('autoreview_enabled', False) + self.reviewer.cancel_all() + await ctx.send(':white_check_mark: Autoreview turned off') + + @has_any_role(*MODERATION_ROLES) + @nomination_autoreview_group.command(name='status') + async def autoreview_status(self, ctx: Context) -> None: + """Show whether automatic posting of reviews is enabled or disabled.""" + if await self.autoreview_enabled(): + await ctx.send('Autoreview is currently enabled') + else: + await ctx.send('Autoreview is currently disabled') + @nomination_group.command(name='watched', aliases=('all', 'list'), root_aliases=("nominees",)) @has_any_role(*MODERATION_ROLES) async def watched_command( @@ -190,7 +240,7 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): self.watched_users[user.id] = response_data - if user.id not in self.reviewer: + if await self.autoreview_enabled() and user.id not in self.reviewer: self.reviewer.schedule_review(user.id) history = await self.bot.api_client.get( @@ -403,7 +453,8 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): ) self._remove_user(user_id) - self.reviewer.cancel(user_id) + if await self.autoreview_enabled(): + self.reviewer.cancel(user_id) return True -- cgit v1.2.3 From 3bdec8eaddfc8bfc6644f61a3aa7e5842757149c Mon Sep 17 00:00:00 2001 From: wookie184 Date: Mon, 2 Aug 2021 08:45:10 +0100 Subject: Small code improvements and added 'ar' alias --- bot/exts/recruitment/talentpool/_cog.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/bot/exts/recruitment/talentpool/_cog.py b/bot/exts/recruitment/talentpool/_cog.py index 5d90701bf..d0558f1f3 100644 --- a/bot/exts/recruitment/talentpool/_cog.py +++ b/bot/exts/recruitment/talentpool/_cog.py @@ -18,6 +18,7 @@ from bot.exts.recruitment.talentpool._review import Reviewer from bot.pagination import LinePaginator from bot.utils import time +AUTOREVIEW_ENABLED_KEY = 'autoreview_enabled' REASON_MAX_CHARS = 1000 log = logging.getLogger(__name__) @@ -51,7 +52,7 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): async def autoreview_enabled(self) -> bool: """Return whether automatic posting of nomination reviews is enabled.""" - return await self.talentpool_settings.get('autoreview_enabled', True) + return await self.talentpool_settings.get(AUTOREVIEW_ENABLED_KEY, True) @group(name='talentpool', aliases=('tp', 'talent', 'nomination', 'n'), invoke_without_command=True) @has_any_role(*MODERATION_ROLES) @@ -59,7 +60,7 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): """Highlights the activity of helper nominees by relaying their messages to the talent pool channel.""" await ctx.send_help(ctx.command) - @nomination_group.group(name='autoreview', invoke_without_command=True) + @nomination_group.group(name='autoreview', aliases=('ar',), invoke_without_command=True) @has_any_role(*MODERATION_ROLES) async def nomination_autoreview_group(self, ctx: Context) -> None: """Commands for enabling or disabling autoreview.""" @@ -71,10 +72,10 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): """ Turn on automatic posting of reviews. - This will post reviews up to one day overdue, older nominations can be + This will post reviews up to one day overdue. Older nominations can be manually reviewed with `!tp post_review `. """ - await self.talentpool_settings.set('autoreview_enabled', True) + await self.talentpool_settings.set(AUTOREVIEW_ENABLED_KEY, True) await self.reviewer.reschedule_reviews() await ctx.send(':white_check_mark: Autoreview turned on') @@ -82,12 +83,12 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): @has_any_role(Roles.admins) async def autoreview_off(self, ctx: Context) -> None: """Turn off automatic posting of reviews.""" - await self.talentpool_settings.set('autoreview_enabled', False) + await self.talentpool_settings.set(AUTOREVIEW_ENABLED_KEY, False) self.reviewer.cancel_all() await ctx.send(':white_check_mark: Autoreview turned off') - @has_any_role(*MODERATION_ROLES) @nomination_autoreview_group.command(name='status') + @has_any_role(*MODERATION_ROLES) async def autoreview_status(self, ctx: Context) -> None: """Show whether automatic posting of reviews is enabled or disabled.""" if await self.autoreview_enabled(): -- cgit v1.2.3 From 792c5a474b5d5f0a4ad58c536f86cad5a5bf3e18 Mon Sep 17 00:00:00 2001 From: wookie184 Date: Mon, 2 Aug 2021 17:25:59 +0100 Subject: Rename commands from on/off to enable/disable Added on/off as aliases --- bot/exts/recruitment/talentpool/_cog.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/bot/exts/recruitment/talentpool/_cog.py b/bot/exts/recruitment/talentpool/_cog.py index d0558f1f3..aff2aa023 100644 --- a/bot/exts/recruitment/talentpool/_cog.py +++ b/bot/exts/recruitment/talentpool/_cog.py @@ -66,26 +66,26 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): """Commands for enabling or disabling autoreview.""" await ctx.send_help(ctx.command) - @nomination_autoreview_group.command(name='on') + @nomination_autoreview_group.command(name='enable', aliases=('on',)) @has_any_role(Roles.admins) - async def autoreview_on(self, ctx: Context) -> None: + async def autoreview_enable(self, ctx: Context) -> None: """ - Turn on automatic posting of reviews. + Enable automatic posting of reviews. This will post reviews up to one day overdue. Older nominations can be manually reviewed with `!tp post_review `. """ await self.talentpool_settings.set(AUTOREVIEW_ENABLED_KEY, True) await self.reviewer.reschedule_reviews() - await ctx.send(':white_check_mark: Autoreview turned on') + await ctx.send(':white_check_mark: Autoreview enabled') - @nomination_autoreview_group.command(name='off') + @nomination_autoreview_group.command(name='disable', aliases=('off',)) @has_any_role(Roles.admins) - async def autoreview_off(self, ctx: Context) -> None: - """Turn off automatic posting of reviews.""" + async def autoreview_disable(self, ctx: Context) -> None: + """Disable automatic posting of reviews.""" await self.talentpool_settings.set(AUTOREVIEW_ENABLED_KEY, False) self.reviewer.cancel_all() - await ctx.send(':white_check_mark: Autoreview turned off') + await ctx.send(':white_check_mark: Autoreview disabled') @nomination_autoreview_group.command(name='status') @has_any_role(*MODERATION_ROLES) -- cgit v1.2.3 From 20997b425168890636a04b270e7433fa82c60fa6 Mon Sep 17 00:00:00 2001 From: D0rs4n <41237606+D0rs4n@users.noreply.github.com> Date: Mon, 2 Aug 2021 18:28:05 +0200 Subject: Optimize Information Cog's join_role_stats and get_member counts --- bot/errors.py | 3 ++- bot/exts/info/information.py | 20 ++++++++------------ 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/bot/errors.py b/bot/errors.py index 69c588f4a..5209997f9 100644 --- a/bot/errors.py +++ b/bot/errors.py @@ -52,5 +52,6 @@ class NonExistentRoleError(ValueError): """ def __init__(self, role_id: int): + super().__init__(f"Could not fetch data for role {role_id}") + self.role_id = role_id - super().__init__(f"Could not fetch data for role {self.role_id}") diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index 62fde5473..2621e15b1 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -43,31 +43,27 @@ class Information(Cog): return channel_counter @staticmethod - def join_role_stats(role_ids: list[int], name: str, guild: Guild) -> dict[str, int]: + def join_role_stats(role_ids: list[int], guild: Guild, name: Optional[str] = None) -> dict[str, int]: """Return a dictionary with the number of `members` of each role given, and the `name` for this joined group.""" - members = [] + members = int() for role_id in role_ids: if (role := guild.get_role(role_id)) is not None: - members.append(role.members) + members += len(role.members) else: raise NonExistentRoleError(role_id) - return {name: len(set(members))} + return {name or role.name.title(): members} @staticmethod def get_member_counts(guild: Guild) -> dict[str, int]: """Return the total number of members for certain roles in `guild`.""" role_ids = [constants.Roles.helpers, constants.Roles.mod_team, constants.Roles.admins, constants.Roles.owners, constants.Roles.contributors] - roles = [] - for role_id in role_ids: - if (role := guild.get_role(role_id)) is not None: - roles.append(role) - else: - raise NonExistentRoleError(role_id) - role_stats = {role.name.title(): len(role.members) for role in roles} + role_stats = {} + for role_id in role_ids: + role_stats.update(Information.join_role_stats([role_id], guild)) role_stats.update( - **Information.join_role_stats([constants.Roles.project_leads, constants.Roles.domain_leads], "Leads", guild) + Information.join_role_stats([constants.Roles.project_leads, constants.Roles.domain_leads], guild, "Leads") ) return role_stats -- cgit v1.2.3 From 8bb2ffc4f7bb324c7940fb88ae1b698f56f8ce67 Mon Sep 17 00:00:00 2001 From: D0rs4n <41237606+D0rs4n@users.noreply.github.com> Date: Mon, 2 Aug 2021 20:42:56 +0200 Subject: Improve code consistency of join_role_stats and NonExistentRoleError --- bot/errors.py | 2 +- bot/exts/info/information.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/errors.py b/bot/errors.py index 5209997f9..5785faa44 100644 --- a/bot/errors.py +++ b/bot/errors.py @@ -45,7 +45,7 @@ class BrandingMisconfiguration(RuntimeError): class NonExistentRoleError(ValueError): """ - Raised by the Information Cog when encountering a role that does not exist. + Raised by the Information Cog when encountering a Role that does not exist. Attributes: `role_id` -- the ID of the role that does not exist diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index 2621e15b1..b879e1330 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -45,7 +45,7 @@ class Information(Cog): @staticmethod def join_role_stats(role_ids: list[int], guild: Guild, name: Optional[str] = None) -> dict[str, int]: """Return a dictionary with the number of `members` of each role given, and the `name` for this joined group.""" - members = int() + members = 0 for role_id in role_ids: if (role := guild.get_role(role_id)) is not None: members += len(role.members) -- cgit v1.2.3 From c1fcab3ba4767cac481fa32ebaf2db31b108faa4 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 2 Aug 2021 19:41:30 -0700 Subject: Fix TypeError when infraction append is not given a reason Fix #1706 --- bot/exts/moderation/infraction/management.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/moderation/infraction/management.py b/bot/exts/moderation/infraction/management.py index 4b0cb78a5..3094159cd 100644 --- a/bot/exts/moderation/infraction/management.py +++ b/bot/exts/moderation/infraction/management.py @@ -81,7 +81,7 @@ class ModManagement(commands.Cog): """ old_reason = infraction["reason"] - if old_reason is not None: + if old_reason is not None and reason is not None: add_period = not old_reason.endswith((".", "!", "?")) reason = old_reason + (". " if add_period else " ") + reason -- cgit v1.2.3 From 53f8ea442994a8f5a465d49031894be0ae17748a Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 2 Aug 2021 20:11:17 -0700 Subject: Catch 404 error when waiting to delete message Sometimes the message is deleted before the function gets around to it. Fixes BOT-1JD Fixes BOT-1K3 Fixes BOT-1JE --- bot/utils/messages.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/bot/utils/messages.py b/bot/utils/messages.py index 90672fba2..a82096b1c 100644 --- a/bot/utils/messages.py +++ b/bot/utils/messages.py @@ -97,11 +97,14 @@ async def wait_for_deletion( ) try: - await bot.instance.wait_for('reaction_add', check=check, timeout=timeout) - except asyncio.TimeoutError: - await message.clear_reactions() - else: - await message.delete() + try: + await bot.instance.wait_for('reaction_add', check=check, timeout=timeout) + except asyncio.TimeoutError: + await message.clear_reactions() + else: + await message.delete() + except discord.NotFound: + log.trace(f"wait_for_deletion: message {message.id} deleted prematurely.") async def send_attachments( -- cgit v1.2.3 From 8bfc0d7a35b0c49a787e498280cffb4841ffa14e Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 2 Aug 2021 20:12:48 -0700 Subject: Reduce imports in utils/messages by qualifying names --- bot/utils/messages.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/bot/utils/messages.py b/bot/utils/messages.py index a82096b1c..abeb04021 100644 --- a/bot/utils/messages.py +++ b/bot/utils/messages.py @@ -7,8 +7,6 @@ from io import BytesIO from typing import Callable, List, Optional, Sequence, Union import discord -from discord import Message, MessageType, Reaction, User -from discord.errors import HTTPException from discord.ext.commands import Context import bot @@ -53,7 +51,7 @@ def reaction_check( log.trace(f"Removing reaction {reaction} by {user} on {reaction.message.id}: disallowed user.") scheduling.create_task( reaction.message.remove_reaction(reaction.emoji, user), - suppressed_exceptions=(HTTPException,), + suppressed_exceptions=(discord.HTTPException,), name=f"remove_reaction-{reaction}-{reaction.message.id}-{user}" ) return False @@ -153,7 +151,7 @@ async def send_attachments( large.append(attachment) else: log.info(f"{failure_msg} because it's too large.") - except HTTPException as e: + except discord.HTTPException as e: if link_large and e.status == 413: large.append(attachment) else: @@ -174,8 +172,8 @@ async def send_attachments( async def count_unique_users_reaction( message: discord.Message, - reaction_predicate: Callable[[Reaction], bool] = lambda _: True, - user_predicate: Callable[[User], bool] = lambda _: True, + reaction_predicate: Callable[[discord.Reaction], bool] = lambda _: True, + user_predicate: Callable[[discord.User], bool] = lambda _: True, count_bots: bool = True ) -> int: """ @@ -195,7 +193,7 @@ async def count_unique_users_reaction( return len(unique_users) -async def pin_no_system_message(message: Message) -> bool: +async def pin_no_system_message(message: discord.Message) -> bool: """Pin the given message, wait a couple of seconds and try to delete the system message.""" await message.pin() @@ -203,7 +201,7 @@ async def pin_no_system_message(message: Message) -> bool: await asyncio.sleep(2) # Search for the system message in the last 10 messages async for historical_message in message.channel.history(limit=10): - if historical_message.type == MessageType.pins_add: + if historical_message.type == discord.MessageType.pins_add: await historical_message.delete() return True -- cgit v1.2.3 From 5aa0022692eefb64a4e69da24e73745d949ba562 Mon Sep 17 00:00:00 2001 From: wookie184 Date: Tue, 3 Aug 2021 12:16:39 +0100 Subject: Remove bot prefix from docstring and change single quotes to double --- bot/exts/recruitment/talentpool/_cog.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/bot/exts/recruitment/talentpool/_cog.py b/bot/exts/recruitment/talentpool/_cog.py index aff2aa023..d2b1d7c02 100644 --- a/bot/exts/recruitment/talentpool/_cog.py +++ b/bot/exts/recruitment/talentpool/_cog.py @@ -18,7 +18,7 @@ from bot.exts.recruitment.talentpool._review import Reviewer from bot.pagination import LinePaginator from bot.utils import time -AUTOREVIEW_ENABLED_KEY = 'autoreview_enabled' +AUTOREVIEW_ENABLED_KEY = "autoreview_enabled" REASON_MAX_CHARS = 1000 log = logging.getLogger(__name__) @@ -48,7 +48,7 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): if await self.autoreview_enabled(): await self.reviewer.reschedule_reviews() else: - self.log.trace('Not scheduling reviews as autoreview is disabled.') + self.log.trace("Not scheduling reviews as autoreview is disabled.") async def autoreview_enabled(self) -> bool: """Return whether automatic posting of nomination reviews is enabled.""" @@ -60,41 +60,41 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): """Highlights the activity of helper nominees by relaying their messages to the talent pool channel.""" await ctx.send_help(ctx.command) - @nomination_group.group(name='autoreview', aliases=('ar',), invoke_without_command=True) + @nomination_group.group(name="autoreview", aliases=("ar",), invoke_without_command=True) @has_any_role(*MODERATION_ROLES) async def nomination_autoreview_group(self, ctx: Context) -> None: """Commands for enabling or disabling autoreview.""" await ctx.send_help(ctx.command) - @nomination_autoreview_group.command(name='enable', aliases=('on',)) + @nomination_autoreview_group.command(name="enable", aliases=("on",)) @has_any_role(Roles.admins) async def autoreview_enable(self, ctx: Context) -> None: """ Enable automatic posting of reviews. This will post reviews up to one day overdue. Older nominations can be - manually reviewed with `!tp post_review `. + manually reviewed with the `tp post_review ` command. """ await self.talentpool_settings.set(AUTOREVIEW_ENABLED_KEY, True) await self.reviewer.reschedule_reviews() - await ctx.send(':white_check_mark: Autoreview enabled') + await ctx.send(":white_check_mark: Autoreview enabled") - @nomination_autoreview_group.command(name='disable', aliases=('off',)) + @nomination_autoreview_group.command(name="disable", aliases=("off",)) @has_any_role(Roles.admins) async def autoreview_disable(self, ctx: Context) -> None: """Disable automatic posting of reviews.""" await self.talentpool_settings.set(AUTOREVIEW_ENABLED_KEY, False) self.reviewer.cancel_all() - await ctx.send(':white_check_mark: Autoreview disabled') + await ctx.send(":white_check_mark: Autoreview disabled") - @nomination_autoreview_group.command(name='status') + @nomination_autoreview_group.command(name="status") @has_any_role(*MODERATION_ROLES) async def autoreview_status(self, ctx: Context) -> None: """Show whether automatic posting of reviews is enabled or disabled.""" if await self.autoreview_enabled(): - await ctx.send('Autoreview is currently enabled') + await ctx.send("Autoreview is currently enabled") else: - await ctx.send('Autoreview is currently disabled') + await ctx.send("Autoreview is currently disabled") @nomination_group.command(name='watched', aliases=('all', 'list'), root_aliases=("nominees",)) @has_any_role(*MODERATION_ROLES) -- cgit v1.2.3 From def62f6129e5b24a52f1cc0cafc616edfa44f3b2 Mon Sep 17 00:00:00 2001 From: wookie184 Date: Tue, 3 Aug 2021 12:23:57 +0100 Subject: Add check for redundant change --- bot/exts/recruitment/talentpool/_cog.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/bot/exts/recruitment/talentpool/_cog.py b/bot/exts/recruitment/talentpool/_cog.py index d2b1d7c02..e7ad314e8 100644 --- a/bot/exts/recruitment/talentpool/_cog.py +++ b/bot/exts/recruitment/talentpool/_cog.py @@ -75,6 +75,10 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): This will post reviews up to one day overdue. Older nominations can be manually reviewed with the `tp post_review ` command. """ + if await self.autoreview_enabled(): + await ctx.send(":x: Autoreview is already enabled") + return + await self.talentpool_settings.set(AUTOREVIEW_ENABLED_KEY, True) await self.reviewer.reschedule_reviews() await ctx.send(":white_check_mark: Autoreview enabled") @@ -83,6 +87,10 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): @has_any_role(Roles.admins) async def autoreview_disable(self, ctx: Context) -> None: """Disable automatic posting of reviews.""" + if not await self.autoreview_enabled(): + await ctx.send(":x: Autoreview is already disabled") + return + await self.talentpool_settings.set(AUTOREVIEW_ENABLED_KEY, False) self.reviewer.cancel_all() await ctx.send(":white_check_mark: Autoreview disabled") -- cgit v1.2.3 From 5057ff6058c6cdaf4c1ac91544779f615d0e4d39 Mon Sep 17 00:00:00 2001 From: wookie184 Date: Tue, 3 Aug 2021 18:29:57 +0100 Subject: Add comment on RedisCache --- bot/exts/recruitment/talentpool/_cog.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bot/exts/recruitment/talentpool/_cog.py b/bot/exts/recruitment/talentpool/_cog.py index e7ad314e8..80bd48534 100644 --- a/bot/exts/recruitment/talentpool/_cog.py +++ b/bot/exts/recruitment/talentpool/_cog.py @@ -27,6 +27,8 @@ log = logging.getLogger(__name__) class TalentPool(WatchChannel, Cog, name="Talentpool"): """Relays messages of helper candidates to a watch channel to observe them.""" + # RedisCache[str, bool] + # Can contain a single key, "autoreview_enabled", with the value a bool indicating if autoreview is enabled. talentpool_settings = RedisCache() def __init__(self, bot: Bot) -> None: -- cgit v1.2.3 From a8869b4d60512b173871c886321b261cbc4acca9 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Tue, 3 Aug 2021 20:37:00 +0100 Subject: Force utf-8 decoding when querying metabase Some discord usernames contain unicode characters, which causes an decoding error as chardet isn't 100% and can't falsely detect the response text as Windows-1254. --- bot/exts/moderation/metabase.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/moderation/metabase.py b/bot/exts/moderation/metabase.py index db5f04d83..e9faf7240 100644 --- a/bot/exts/moderation/metabase.py +++ b/bot/exts/moderation/metabase.py @@ -115,12 +115,12 @@ class Metabase(Cog): try: async with self.bot.http_session.post(url, headers=self.headers, raise_for_status=True) as resp: if extension == "csv": - out = await resp.text() + out = await resp.text(encoding="utf-8") # Save the output for use with int e self.exports[question_id] = list(csv.DictReader(StringIO(out))) elif extension == "json": - out = await resp.json() + out = await resp.json(encoding="utf-8") # Save the output for use with int e self.exports[question_id] = out -- cgit v1.2.3 From c390ca427422c5532848eaf43cd23029ff38f0f6 Mon Sep 17 00:00:00 2001 From: Mark Date: Wed, 4 Aug 2021 08:48:23 -0700 Subject: Suppress 403 error when sending DEFCON reject DM (#1711) 403 occurs if the user has DMs disabled. Fixes BOT-137 --- bot/exts/moderation/defcon.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/bot/exts/moderation/defcon.py b/bot/exts/moderation/defcon.py index 9801d45ad..6ac077b93 100644 --- a/bot/exts/moderation/defcon.py +++ b/bot/exts/moderation/defcon.py @@ -9,7 +9,7 @@ from typing import Optional, Union from aioredis import RedisError from async_rediscache import RedisCache from dateutil.relativedelta import relativedelta -from discord import Colour, Embed, Member, User +from discord import Colour, Embed, Forbidden, Member, User from discord.ext import tasks from discord.ext.commands import Cog, Context, group, has_any_role @@ -118,10 +118,12 @@ class Defcon(Cog): try: await member.send(REJECTION_MESSAGE.format(user=member.mention)) - message_sent = True + except Forbidden: + log.debug(f"Cannot send DEFCON rejection DM to {member}: DMs disabled") except Exception: - log.exception(f"Unable to send rejection message to user: {member}") + # Broadly catch exceptions because DM isn't critical, but it's imperative to kick them. + log.exception(f"Error sending DEFCON rejection message to {member}") await member.kick(reason="DEFCON active, user is too new") self.bot.stats.incr("defcon.leaves") -- cgit v1.2.3 From 75862768ebd4b182e713add91cf705c6fcfc70f8 Mon Sep 17 00:00:00 2001 From: Objectivitix <79152594+Objectivitix@users.noreply.github.com> Date: Wed, 4 Aug 2021 13:40:43 -0400 Subject: Reorder user roles in !user command from highest to lowest (#1719) Reorder user roles in !user command from highest to lowest --- bot/exts/info/information.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index bb713eef1..800a68821 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -243,7 +243,9 @@ class Information(Cog): if on_server: joined = discord_timestamp(user.joined_at, TimestampFormats.RELATIVE) - roles = ", ".join(role.mention for role in user.roles[1:]) + # The 0 is for excluding the default @everyone role, + # and the -1 is for reversing the order of the roles to highest to lowest in hierarchy. + roles = ", ".join(role.mention for role in user.roles[:0:-1]) membership = {"Joined": joined, "Verified": not user.pending, "Roles": roles or None} if not is_mod_channel(ctx.channel): membership.pop("Verified") -- cgit v1.2.3 From 5fc4b67323a241193b262765d1e3503bc72f6ced Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 7 Aug 2021 15:54:59 -0700 Subject: Incidents: catch 404s Fixes BOT-ZN --- bot/exts/moderation/incidents.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/bot/exts/moderation/incidents.py b/bot/exts/moderation/incidents.py index 0e479d33f..561e0251e 100644 --- a/bot/exts/moderation/incidents.py +++ b/bot/exts/moderation/incidents.py @@ -143,7 +143,14 @@ async def add_signals(incident: discord.Message) -> None: log.trace(f"Skipping emoji as it's already been placed: {signal_emoji}") else: log.trace(f"Adding reaction: {signal_emoji}") - await incident.add_reaction(signal_emoji.value) + try: + await incident.add_reaction(signal_emoji.value) + except discord.NotFound as e: + if e.code != 10008: + raise + + log.trace(f"Couldn't react with signal because message {incident.id} was deleted; skipping incident") + return class Incidents(Cog): @@ -288,14 +295,20 @@ class Incidents(Cog): members_roles: t.Set[int] = {role.id for role in member.roles} if not members_roles & ALLOWED_ROLES: # Intersection is truthy on at least 1 common element log.debug(f"Removing invalid reaction: user {member} is not permitted to send signals") - await incident.remove_reaction(reaction, member) + try: + await incident.remove_reaction(reaction, member) + except discord.NotFound: + log.trace("Couldn't remove reaction because the reaction or its message was deleted") return try: signal = Signal(reaction) except ValueError: log.debug(f"Removing invalid reaction: emoji {reaction} is not a valid signal") - await incident.remove_reaction(reaction, member) + try: + await incident.remove_reaction(reaction, member) + except discord.NotFound: + log.trace("Couldn't remove reaction because the reaction or its message was deleted") return log.trace(f"Received signal: {signal}") @@ -313,7 +326,10 @@ class Incidents(Cog): confirmation_task = self.make_confirmation_task(incident, timeout) log.trace("Deleting original message") - await incident.delete() + try: + await incident.delete() + except discord.NotFound: + log.trace("Couldn't delete message because it was already deleted") log.trace(f"Awaiting deletion confirmation: {timeout=} seconds") try: -- cgit v1.2.3 From b8d959c4ec2b07a49334a6fafb5f203495ec610d Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 7 Aug 2021 15:59:54 -0700 Subject: Code block: catch 404s when editing or deleting the message Fixes BOT-J2 --- bot/exts/info/codeblock/_cog.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/bot/exts/info/codeblock/_cog.py b/bot/exts/info/codeblock/_cog.py index 9094d9d15..9a0705d2b 100644 --- a/bot/exts/info/codeblock/_cog.py +++ b/bot/exts/info/codeblock/_cog.py @@ -177,10 +177,13 @@ class CodeBlockCog(Cog, name="Code Block"): if not bot_message: return - if not instructions: - log.info("User's incorrect code block has been fixed. Removing instructions message.") - await bot_message.delete() - del self.codeblock_message_ids[payload.message_id] - else: - log.info("Message edited but still has invalid code blocks; editing the instructions.") - await bot_message.edit(embed=self.create_embed(instructions)) + try: + if not instructions: + log.info("User's incorrect code block was fixed. Removing instructions message.") + await bot_message.delete() + del self.codeblock_message_ids[payload.message_id] + else: + log.info("Message edited but still has invalid code blocks; editing instructions.") + await bot_message.edit(embed=self.create_embed(instructions)) + except discord.NotFound: + log.debug("Could not find instructions message; it was probably deleted.") -- cgit v1.2.3 From dde617e19e570df2a6a57b36679b307cda496217 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 7 Aug 2021 16:03:41 -0700 Subject: Duck pond: abort if reaction's message or author can't be found Fixes BOT-1J7 --- bot/exts/fun/duck_pond.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/bot/exts/fun/duck_pond.py b/bot/exts/fun/duck_pond.py index c78b9c141..d02912545 100644 --- a/bot/exts/fun/duck_pond.py +++ b/bot/exts/fun/duck_pond.py @@ -171,8 +171,14 @@ class DuckPond(Cog): if not self.is_helper_viewable(channel): return - message = await channel.fetch_message(payload.message_id) + try: + message = await channel.fetch_message(payload.message_id) + except discord.NotFound: + return # Message was deleted. + member = discord.utils.get(message.guild.members, id=payload.user_id) + if not member: + return # Member left or wasn't in the cache. # Was the message sent by a human staff member? if not self.is_staff(message.author) or message.author.bot: -- cgit v1.2.3 From c4e0faeb6f1e1430bc75578c49567d884072ce34 Mon Sep 17 00:00:00 2001 From: aru Date: Sun, 8 Aug 2021 09:27:29 -0400 Subject: change dockerfile python version to latest 3.9.x --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index c285898dc..4d8592590 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.9.5-slim +FROM python:3.9-slim # Set pip to have no saved cache ENV PIP_NO_CACHE_DIR=false \ -- cgit v1.2.3 From 3d395b225c2bf0231273409244845be87e851d39 Mon Sep 17 00:00:00 2001 From: stalkerx Date: Mon, 9 Aug 2021 17:31:24 -0500 Subject: Fixed error message to match true value When the limit was raised, the error message was not edited to reflect the change. https://github.com/python-discord/bot/commit/6f45d6896adb3f05962733cec8e5db199def20bc --- bot/pagination.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/pagination.py b/bot/pagination.py index 90d7c84ee..26caa7db0 100644 --- a/bot/pagination.py +++ b/bot/pagination.py @@ -75,7 +75,7 @@ class LinePaginator(Paginator): raise ValueError(f"scale_to_size must be >= max_size. ({scale_to_size} < {max_size})") if scale_to_size > 4000: - raise ValueError(f"scale_to_size must be <= 2,000 characters. ({scale_to_size} > 4000)") + raise ValueError(f"scale_to_size must be <= 4,000 characters. ({scale_to_size} > 4000)") self.scale_to_size = scale_to_size - len(suffix) self.max_lines = max_lines -- cgit v1.2.3 From fa4b4694a6ec33cd1ee5cb2825aaf7c561dc3d90 Mon Sep 17 00:00:00 2001 From: vcokltfre Date: Tue, 10 Aug 2021 15:31:57 +0100 Subject: chore: remove some newlines in the blocking tag --- bot/resources/tags/blocking.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/bot/resources/tags/blocking.md b/bot/resources/tags/blocking.md index 31d91294c..5554d7eba 100644 --- a/bot/resources/tags/blocking.md +++ b/bot/resources/tags/blocking.md @@ -1,9 +1,7 @@ **Why do we need asynchronous programming?** - Imagine that you're coding a Discord bot and every time somebody uses a command, you need to get some information from a database. But there's a catch: the database servers are acting up today and take a whole 10 seconds to respond. If you do **not** use asynchronous methods, your whole bot will stop running until it gets a response from the database. How do you fix this? Asynchronous programming. **What is asynchronous programming?** - An asynchronous program utilises the `async` and `await` keywords. An asynchronous program pauses what it's doing and does something else whilst it waits for some third-party service to complete whatever it's supposed to do. Any code within an `async` context manager or function marked with the `await` keyword indicates to Python, that whilst this operation is being completed, it can do something else. For example: ```py @@ -14,13 +12,10 @@ import discord async def ping(ctx): await ctx.send("Pong!") ``` - **What does the term "blocking" mean?** - A blocking operation is wherever you do something without `await`ing it. This tells Python that this step must be completed before it can do anything else. Common examples of blocking operations, as simple as they may seem, include: outputting text, adding two numbers and appending an item onto a list. Most common Python libraries have an asynchronous version available to use in asynchronous contexts. **`async` libraries** - The standard async library - `asyncio` Asynchronous web requests - `aiohttp` Talking to PostgreSQL asynchronously - `asyncpg` -- cgit v1.2.3 From 10c3b34fb143247c1f40da3392e410ddd54ee818 Mon Sep 17 00:00:00 2001 From: TizzySaurus <47674925+TizzySaurus@users.noreply.github.com> Date: Wed, 11 Aug 2021 09:01:12 +0100 Subject: Update DORMANT_MSG to be compatible with str.format() --- bot/exts/help_channels/_message.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bot/exts/help_channels/_message.py b/bot/exts/help_channels/_message.py index befacd263..e4816044c 100644 --- a/bot/exts/help_channels/_message.py +++ b/bot/exts/help_channels/_message.py @@ -30,14 +30,14 @@ AVAILABLE_TITLE = "Available help channel" AVAILABLE_FOOTER = "Closes after a period of inactivity, or when you send !close." DORMANT_MSG = f""" -This help channel has been marked as **dormant**, and has been moved into the **Help: Dormant** \ +This help channel has been marked as **dormant**, and has been moved into the **{}** \ category at the bottom of the channel list. It is no longer possible to send messages in this \ channel until it becomes available again. If your question wasn't answered yet, you can claim a new help channel from the \ -**Help: Available** category by simply asking your question again. Consider rephrasing the \ +**{}** category by simply asking your question again. Consider rephrasing the \ question to maximize your chance of getting a good answer. If you're not sure how, have a look \ -through our guide for **[asking a good question]({ASKING_GUIDE_URL})**. +through our guide for **[asking a good question]({})**. """ -- cgit v1.2.3 From f038479a785a3ac2b737eba214a4366037bb9575 Mon Sep 17 00:00:00 2001 From: TizzySaurus <47674925+TizzySaurus@users.noreply.github.com> Date: Wed, 11 Aug 2021 09:04:20 +0100 Subject: Update DORMANT_MSG to allow kwargs in str.format() --- bot/exts/help_channels/_message.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bot/exts/help_channels/_message.py b/bot/exts/help_channels/_message.py index e4816044c..27b506b33 100644 --- a/bot/exts/help_channels/_message.py +++ b/bot/exts/help_channels/_message.py @@ -30,14 +30,14 @@ AVAILABLE_TITLE = "Available help channel" AVAILABLE_FOOTER = "Closes after a period of inactivity, or when you send !close." DORMANT_MSG = f""" -This help channel has been marked as **dormant**, and has been moved into the **{}** \ +This help channel has been marked as **dormant**, and has been moved into the **{dormant}** \ category at the bottom of the channel list. It is no longer possible to send messages in this \ channel until it becomes available again. If your question wasn't answered yet, you can claim a new help channel from the \ -**{}** category by simply asking your question again. Consider rephrasing the \ +**{available}** category by simply asking your question again. Consider rephrasing the \ question to maximize your chance of getting a good answer. If you're not sure how, have a look \ -through our guide for **[asking a good question]({})**. +through our guide for **[asking a good question]({asking_guide})**. """ -- cgit v1.2.3 From ae0d725be15c06ba66a6b3b557d6a09e454056ca Mon Sep 17 00:00:00 2001 From: TizzySaurus <47674925+TizzySaurus@users.noreply.github.com> Date: Wed, 11 Aug 2021 09:10:37 +0100 Subject: Update embed sent when channel moves to dormant category --- bot/exts/help_channels/_cog.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index 35658d117..6f8472069 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -387,7 +387,14 @@ class HelpChannels(commands.Cog): ) log.trace(f"Sending dormant message for #{channel} ({channel.id}).") - embed = discord.Embed(description=_message.DORMANT_MSG) + available_category = self.bot.get_channel(constants.Categories.help_available) + embed = discord.Embed( + description=_message.DORMANT_MSG.format( + dormant=channel.category.name, + available=available_category.name, + asking_guide=_message.ASKING_GUIDE_URL + ) + ) await channel.send(embed=embed) log.trace(f"Pushing #{channel} ({channel.id}) into the channel queue.") -- cgit v1.2.3 From d6c92ada2af5cdfcd577ed4a14c4e66ba654842f Mon Sep 17 00:00:00 2001 From: TizzySaurus <47674925+TizzySaurus@users.noreply.github.com> Date: Wed, 11 Aug 2021 09:20:49 +0100 Subject: Make DORMANT_MSG a string instead of f-string --- bot/exts/help_channels/_message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/help_channels/_message.py b/bot/exts/help_channels/_message.py index 27b506b33..cf070be83 100644 --- a/bot/exts/help_channels/_message.py +++ b/bot/exts/help_channels/_message.py @@ -29,7 +29,7 @@ AVAILABLE_TITLE = "Available help channel" AVAILABLE_FOOTER = "Closes after a period of inactivity, or when you send !close." -DORMANT_MSG = f""" +DORMANT_MSG = """ This help channel has been marked as **dormant**, and has been moved into the **{dormant}** \ category at the bottom of the channel list. It is no longer possible to send messages in this \ channel until it becomes available again. -- cgit v1.2.3 From e77a0d91ec1623e5f0312e906e66077ad3890fbc Mon Sep 17 00:00:00 2001 From: TizzySaurus <47674925+TizzySaurus@users.noreply.github.com> Date: Wed, 11 Aug 2021 09:57:02 +0100 Subject: Change bot.get_channel to utils.channels.try_get_channel --- bot/exts/help_channels/_cog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index 6f8472069..03e891271 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -387,7 +387,7 @@ class HelpChannels(commands.Cog): ) log.trace(f"Sending dormant message for #{channel} ({channel.id}).") - available_category = self.bot.get_channel(constants.Categories.help_available) + available_category = await channel_utils.try_get_channel(constants.Categories.help_available) embed = discord.Embed( description=_message.DORMANT_MSG.format( dormant=channel.category.name, -- cgit v1.2.3 From 99bfb15037ce7a6f88a403f8c1d2ab36d852874a Mon Sep 17 00:00:00 2001 From: TizzySaurus <47674925+TizzySaurus@users.noreply.github.com> Date: Wed, 11 Aug 2021 12:34:14 +0100 Subject: Fetch dormant category rather than use channel.category channel.category doesn't get updated in cache so the category ends up still linking to "In Use", whereas we want the "Dormant". --- bot/exts/help_channels/_cog.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index 03e891271..afaf9b0bd 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -387,10 +387,11 @@ class HelpChannels(commands.Cog): ) log.trace(f"Sending dormant message for #{channel} ({channel.id}).") + dormant_category = await channel_utils.try_get_channel(constants.Categories.help_dormant) available_category = await channel_utils.try_get_channel(constants.Categories.help_available) embed = discord.Embed( description=_message.DORMANT_MSG.format( - dormant=channel.category.name, + dormant=dormant_category.name, available=available_category.name, asking_guide=_message.ASKING_GUIDE_URL ) -- cgit v1.2.3 From aef5584849c812a16a51475d544cb0d2abc9a1d4 Mon Sep 17 00:00:00 2001 From: TizzySaurus <47674925+TizzySaurus@users.noreply.github.com> Date: Wed, 11 Aug 2021 16:03:53 +0100 Subject: Remove added punctuation from reminder --- bot/exts/utils/reminders.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/utils/reminders.py b/bot/exts/utils/reminders.py index 7b8c5c4b3..441b0353f 100644 --- a/bot/exts/utils/reminders.py +++ b/bot/exts/utils/reminders.py @@ -181,7 +181,7 @@ class Reminders(Cog): ) # Let's not use a codeblock to keep emojis and mentions working. Embeds are safe anyway. - embed.description = f"Here's your reminder: {reminder['content']}." + embed.description = f"Here's your reminder: {reminder['content']}" if reminder.get("jump_url"): # keep backward compatibility embed.description += f"\n[Jump back to when you created the reminder]({reminder['jump_url']})" -- cgit v1.2.3 From 4d62dadc4d19f0c17d5cda8748c3fc520f2adccf Mon Sep 17 00:00:00 2001 From: ChrisJL Date: Thu, 12 Aug 2021 00:30:39 +0100 Subject: Remove role pings when a helper vote is posted (#1744) --- bot/exts/recruitment/talentpool/_review.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/recruitment/talentpool/_review.py b/bot/exts/recruitment/talentpool/_review.py index 3a1e66970..74056dbf5 100644 --- a/bot/exts/recruitment/talentpool/_review.py +++ b/bot/exts/recruitment/talentpool/_review.py @@ -15,7 +15,7 @@ from discord.ext.commands import Context from bot.api import ResponseCodeError from bot.bot import Bot -from bot.constants import Channels, Colours, Emojis, Guild, Roles +from bot.constants import Channels, Colours, Emojis, Guild from bot.utils.messages import count_unique_users_reaction, pin_no_system_message from bot.utils.scheduling import Scheduler from bot.utils.time import get_time_delta, time_since @@ -118,7 +118,7 @@ class Reviewer: f"I tried to review the user with ID `{user_id}`, but they don't appear to be on the server :pensive:" ), None - opening = f"<@&{Roles.mod_team}> <@&{Roles.admins}>\n{member.mention} ({member}) for Helper!" + opening = f"{member.mention} ({member}) for Helper!" current_nominations = "\n\n".join( f"**<@{entry['actor']}>:** {entry['reason'] or '*no reason given*'}" -- cgit v1.2.3 From 4ecbb2e821f9f0452fddb49161989f68f98dbd38 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Thu, 12 Aug 2021 12:38:56 +0100 Subject: fix: Nomination message now checks historic and new style nominations Previously nomination messages had role pings in them, now they don't as we moved them into a thread. Due to this, we need to detect both in the interim of historic nominations existing. A 'proper' fix for this is to store the nomination message IDs when we post them against the nomination object in the site api. We are planing to work on this soon, this commit is a short term fix. --- bot/exts/recruitment/talentpool/_review.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/bot/exts/recruitment/talentpool/_review.py b/bot/exts/recruitment/talentpool/_review.py index 74056dbf5..4d496a1f7 100644 --- a/bot/exts/recruitment/talentpool/_review.py +++ b/bot/exts/recruitment/talentpool/_review.py @@ -33,10 +33,12 @@ MAX_MESSAGE_SIZE = 2000 # Maximum amount of characters allowed in an embed MAX_EMBED_SIZE = 4000 -# Regex finding the user ID of a user mention -MENTION_RE = re.compile(r"<@!?(\d+?)>") -# Regex matching role pings -ROLE_MENTION_RE = re.compile(r"<@&\d+>") +# Regex for finding the first message of a nomination, and extracting the nominee. +# Historic nominations will have 2 role mentions at the start, new ones won't, optionally match for this. +NOMINATION_MESSAGE_REGEX = re.compile( + r"(?:<@&\d+> <@&\d+>\n)*?<@!?(\d+?)> \(.+#\d{4}\) for Helper!\n\n\*\*Nominated by:\*\*", + re.MULTILINE +) class Reviewer: @@ -142,14 +144,14 @@ class Reviewer: """Archive this vote to #nomination-archive.""" message = await message.fetch() - # We consider the first message in the nomination to contain the two role pings + # We consider the first message in the nomination to contain the user ping, username#discrim, and fixed text messages = [message] - if not len(ROLE_MENTION_RE.findall(message.content)) >= 2: + if not NOMINATION_MESSAGE_REGEX.search(message.content): with contextlib.suppress(NoMoreItems): async for new_message in message.channel.history(before=message.created_at): messages.append(new_message) - if len(ROLE_MENTION_RE.findall(new_message.content)) >= 2: + if NOMINATION_MESSAGE_REGEX.search(new_message.content): break log.debug(f"Found {len(messages)} messages: {', '.join(str(m.id) for m in messages)}") @@ -161,7 +163,7 @@ class Reviewer: content = "".join(parts) # We assume that the first user mentioned is the user that we are voting on - user_id = int(MENTION_RE.search(content).group(1)) + user_id = int(NOMINATION_MESSAGE_REGEX.search(content).group(1)) # Get reaction counts reviewed = await count_unique_users_reaction( -- cgit v1.2.3 From 94f03bcc5bc6b7264d0fc32c403b60bf7ec9ac20 Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Sun, 15 Aug 2021 23:10:59 +0300 Subject: Add default value to csv_file Co-authored-by: Bluenix --- bot/exts/events/code_jams/_cog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/events/code_jams/_cog.py b/bot/exts/events/code_jams/_cog.py index d0c206b5e..78f375416 100644 --- a/bot/exts/events/code_jams/_cog.py +++ b/bot/exts/events/code_jams/_cog.py @@ -33,7 +33,7 @@ class CodeJams(commands.Cog): await ctx.send_help(ctx.command) @codejam.command() - async def create(self, ctx: commands.Context, csv_file: t.Optional[str]) -> None: + async def create(self, ctx: commands.Context, csv_file: t.Optional[str] = None) -> None: """ Create code-jam teams from a CSV file or a link to one, specifying the team names, leaders and members. -- cgit v1.2.3 From be2c547586e80924bf416785bc354bbc7c392cfd Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sun, 15 Aug 2021 23:45:41 +0300 Subject: Docstring corrections Co-authored-by: Bluenix --- bot/exts/events/code_jams/_channels.py | 2 +- bot/exts/events/code_jams/_cog.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bot/exts/events/code_jams/_channels.py b/bot/exts/events/code_jams/_channels.py index 8b199a3c2..34ff0ad41 100644 --- a/bot/exts/events/code_jams/_channels.py +++ b/bot/exts/events/code_jams/_channels.py @@ -107,7 +107,7 @@ async def _send_status_update(guild: discord.Guild, message: str) -> None: async def _add_team_leader_roles(members: list[tuple[discord.Member, bool]], team_leaders: discord.Role) -> None: - """Assign team leader role, the jammer role and their team role.""" + """Assign the team leader role to the team leaders.""" for member, is_leader in members: if is_leader: await member.add_roles(team_leaders) diff --git a/bot/exts/events/code_jams/_cog.py b/bot/exts/events/code_jams/_cog.py index d0c206b5e..b2ea97c38 100644 --- a/bot/exts/events/code_jams/_cog.py +++ b/bot/exts/events/code_jams/_cog.py @@ -79,9 +79,9 @@ class CodeJams(commands.Cog): @commands.has_any_role(Roles.admins) async def end(self, ctx: commands.Context) -> None: """ - Deletes all code jam channels. + Delete all code jam channels. - Displays a confirmation message with the categories and channels to be deleted. Pressing the added reaction + A confirmation message is displayed with the categories and channels to be deleted.. Pressing the added reaction deletes those channels. """ def predicate_deletion_emoji_reaction(reaction: discord.Reaction, user: discord.User) -> bool: @@ -196,7 +196,7 @@ class CodeJams(commands.Cog): @codejam.command() @commands.has_any_role(Roles.admins) async def remove(self, ctx: commands.Context, member: Member) -> None: - """Removes the participant from their team. Does not remove the participants or leader roles.""" + """Remove the participant from their team. Does not remove the participants or leader roles.""" channel = self.team_channel(ctx.guild, member) if not channel: await ctx.send(":x: I can't find the team channel for this member.") -- cgit v1.2.3 From a5b761791f1162f2854775d87b00c687bf397a0f Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sun, 15 Aug 2021 23:51:02 +0300 Subject: Add team name to audit log reason Interestingly enough, the reason doesn't seem to be displayed for channel permission overrides. --- bot/exts/events/code_jams/_cog.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bot/exts/events/code_jams/_cog.py b/bot/exts/events/code_jams/_cog.py index b2ea97c38..64f886f0a 100644 --- a/bot/exts/events/code_jams/_cog.py +++ b/bot/exts/events/code_jams/_cog.py @@ -202,7 +202,11 @@ class CodeJams(commands.Cog): await ctx.send(":x: I can't find the team channel for this member.") return - await channel.set_permissions(member, overwrite=None, reason="Participant removed from the team.") + await channel.set_permissions( + member, + overwrite=None, + reason=f"Participant removed from the team {self.team_name(channel)}." + ) await ctx.send(f"Removed the participant from `{self.team_name(channel)}`.") @staticmethod -- cgit v1.2.3 From 73187a35c7349c661e50f4a429ae30560ee92ddd Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sun, 15 Aug 2021 23:57:23 +0300 Subject: Add `cj end` timeout message, improve style Co-authored-by: Bluenix --- bot/exts/events/code_jams/_cog.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bot/exts/events/code_jams/_cog.py b/bot/exts/events/code_jams/_cog.py index 64f886f0a..e385f6441 100644 --- a/bot/exts/events/code_jams/_cog.py +++ b/bot/exts/events/code_jams/_cog.py @@ -109,6 +109,7 @@ class CodeJams(commands.Cog): except asyncio.TimeoutError: await message.clear_reaction(DELETION_REACTION) + await ctx.send("Command timed out.", reference=message) return else: @@ -131,7 +132,7 @@ class CodeJams(commands.Cog): def format_category_info(category: discord.CategoryChannel, channels: list[discord.abc.GuildChannel]) -> str: """Displays the category and the channels within it in a readable format.""" - return f"{channel_repr(category)}:" + "".join(f"\n - {channel_repr(channel)}" for channel in channels) + return f"{channel_repr(category)}:\n" + "\n".join(" - " + channel_repr(channel) for channel in channels) deletion_details = "\n\n".join( format_category_info(category, channels) for category, channels in categories.items() -- cgit v1.2.3 From 44adebc02aaedaf96634988019205d6b853d22db Mon Sep 17 00:00:00 2001 From: wookie184 Date: Mon, 16 Aug 2021 17:57:16 +0100 Subject: Remove the admins mention from the modmail tag --- bot/resources/tags/modmail.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/resources/tags/modmail.md b/bot/resources/tags/modmail.md index 412468174..8ac19c8a7 100644 --- a/bot/resources/tags/modmail.md +++ b/bot/resources/tags/modmail.md @@ -6,4 +6,4 @@ It supports attachments, codeblocks, and reactions. As communication happens ove **To use it, simply send a direct message to the bot.** -Should there be an urgent and immediate need for a moderator or admin to look at a channel, feel free to ping the <@&831776746206265384> or <@&267628507062992896> role instead. +Should there be an urgent and immediate need for a moderator to look at a channel, feel free to ping the <@&831776746206265384> role instead. -- cgit v1.2.3