From 7b311196613e358557a14aa75f01e8a54ab3e698 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 7 Sep 2020 15:49:31 -0700 Subject: Sync: remove confirmation The confirmation was intended to be a safe guard against cache issues that would cause a huge number of roles/users to deleted after syncing. With `wait_until_guild_available`, such cache issue shouldn't arise. Therefore, this feature is obsolete. Resolve #1075 --- bot/cogs/sync/syncers.py | 170 +------------------ bot/constants.py | 7 - config-default.yml | 4 - tests/bot/cogs/sync/test_base.py | 357 ++------------------------------------- 4 files changed, 20 insertions(+), 518 deletions(-) diff --git a/bot/cogs/sync/syncers.py b/bot/cogs/sync/syncers.py index f7ba811bc..b3819a1e1 100644 --- a/bot/cogs/sync/syncers.py +++ b/bot/cogs/sync/syncers.py @@ -1,15 +1,11 @@ import abc -import asyncio import logging import typing as t from collections import namedtuple -from functools import partial -import discord -from discord import Guild, HTTPException, Member, Message, Reaction, User +from discord import Guild from discord.ext.commands import Context -from bot import constants from bot.api import ResponseCodeError from bot.bot import Bot @@ -25,9 +21,6 @@ _Diff = namedtuple('Diff', ('created', 'updated', 'deleted')) class Syncer(abc.ABC): """Base class for synchronising the database with objects in the Discord cache.""" - _CORE_DEV_MENTION = f"<@&{constants.Roles.core_developers}> " - _REACTION_EMOJIS = (constants.Emojis.check_mark, constants.Emojis.cross_mark) - def __init__(self, bot: Bot) -> None: self.bot = bot @@ -37,112 +30,6 @@ class Syncer(abc.ABC): """The name of the syncer; used in output messages and logging.""" raise NotImplementedError # pragma: no cover - async def _send_prompt(self, message: t.Optional[Message] = None) -> t.Optional[Message]: - """ - Send a prompt to confirm or abort a sync using reactions and return the sent message. - - If a message is given, it is edited to display the prompt and reactions. Otherwise, a new - message is sent to the dev-core channel and mentions the core developers role. If the - channel cannot be retrieved, return None. - """ - log.trace(f"Sending {self.name} sync confirmation prompt.") - - msg_content = ( - f'Possible cache issue while syncing {self.name}s. ' - f'More than {constants.Sync.max_diff} {self.name}s were changed. ' - f'React to confirm or abort the sync.' - ) - - # Send to core developers if it's an automatic sync. - if not message: - log.trace("Message not provided for confirmation; creating a new one in dev-core.") - channel = self.bot.get_channel(constants.Channels.dev_core) - - if not channel: - log.debug("Failed to get the dev-core channel from cache; attempting to fetch it.") - try: - channel = await self.bot.fetch_channel(constants.Channels.dev_core) - except HTTPException: - log.exception( - f"Failed to fetch channel for sending sync confirmation prompt; " - f"aborting {self.name} sync." - ) - return None - - allowed_roles = [discord.Object(constants.Roles.core_developers)] - message = await channel.send( - f"{self._CORE_DEV_MENTION}{msg_content}", - allowed_mentions=discord.AllowedMentions(everyone=False, roles=allowed_roles) - ) - else: - await message.edit(content=msg_content) - - # Add the initial reactions. - log.trace(f"Adding reactions to {self.name} syncer confirmation prompt.") - for emoji in self._REACTION_EMOJIS: - await message.add_reaction(emoji) - - return message - - def _reaction_check( - self, - author: Member, - message: Message, - reaction: Reaction, - user: t.Union[Member, User] - ) -> bool: - """ - Return True if the `reaction` is a valid confirmation or abort reaction on `message`. - - If the `author` of the prompt is a bot, then a reaction by any core developer will be - considered valid. Otherwise, the author of the reaction (`user`) will have to be the - `author` of the prompt. - """ - # For automatic syncs, check for the core dev role instead of an exact author - has_role = any(constants.Roles.core_developers == role.id for role in user.roles) - return ( - reaction.message.id == message.id - and not user.bot - and (has_role if author.bot else user == author) - and str(reaction.emoji) in self._REACTION_EMOJIS - ) - - async def _wait_for_confirmation(self, author: Member, message: Message) -> bool: - """ - Wait for a confirmation reaction by `author` on `message` and return True if confirmed. - - Uses the `_reaction_check` function to determine if a reaction is valid. - - If there is no reaction within `bot.constants.Sync.confirm_timeout` seconds, return False. - To acknowledge the reaction (or lack thereof), `message` will be edited. - """ - # Preserve the core-dev role mention in the message edits so users aren't confused about - # where notifications came from. - mention = self._CORE_DEV_MENTION if author.bot else "" - - reaction = None - try: - log.trace(f"Waiting for a reaction to the {self.name} syncer confirmation prompt.") - reaction, _ = await self.bot.wait_for( - 'reaction_add', - check=partial(self._reaction_check, author, message), - timeout=constants.Sync.confirm_timeout - ) - except asyncio.TimeoutError: - # reaction will remain none thus sync will be aborted in the finally block below. - log.debug(f"The {self.name} syncer confirmation prompt timed out.") - - if str(reaction) == constants.Emojis.check_mark: - log.trace(f"The {self.name} syncer was confirmed.") - await message.edit(content=f':ok_hand: {mention}{self.name} sync will proceed.') - return True - else: - log.info(f"The {self.name} syncer was aborted or timed out!") - await message.edit( - content=f':warning: {mention}{self.name} sync aborted or timed out!' - ) - return False - @abc.abstractmethod async def _get_diff(self, guild: Guild) -> _Diff: """Return the difference between the cache of `guild` and the database.""" @@ -153,34 +40,6 @@ class Syncer(abc.ABC): """Perform the API calls for synchronisation.""" raise NotImplementedError # pragma: no cover - async def _get_confirmation_result( - self, - diff_size: int, - author: Member, - message: t.Optional[Message] = None - ) -> t.Tuple[bool, t.Optional[Message]]: - """ - Prompt for confirmation and return a tuple of the result and the prompt message. - - `diff_size` is the size of the diff of the sync. If it is greater than - `bot.constants.Sync.max_diff`, the prompt will be sent. The `author` is the invoked of the - sync and the `message` is an extant message to edit to display the prompt. - - If confirmed or no confirmation was needed, the result is True. The returned message will - either be the given `message` or a new one which was created when sending the prompt. - """ - log.trace(f"Determining if confirmation prompt should be sent for {self.name} syncer.") - if diff_size > constants.Sync.max_diff: - message = await self._send_prompt(message) - if not message: - return False, None # Couldn't get channel. - - confirmed = await self._wait_for_confirmation(author, message) - if not confirmed: - return False, message # Sync aborted. - - return True, message - async def sync(self, guild: Guild, ctx: t.Optional[Context] = None) -> None: """ Synchronise the database with the cache of `guild`. @@ -191,24 +50,8 @@ class Syncer(abc.ABC): """ log.info(f"Starting {self.name} syncer.") - message = None - author = self.bot.user - if ctx: - message = await ctx.send(f"📊 Synchronising {self.name}s.") - author = ctx.author - + message = await ctx.send(f"📊 Synchronising {self.name}s.") if ctx else None diff = await self._get_diff(guild) - diff_dict = diff._asdict() # Ugly method for transforming the NamedTuple into a dict - totals = {k: len(v) for k, v in diff_dict.items() if v is not None} - diff_size = sum(totals.values()) - - confirmed, message = await self._get_confirmation_result(diff_size, author, message) - if not confirmed: - return - - # Preserve the core-dev role mention in the message edits so users aren't confused about - # where notifications came from. - mention = self._CORE_DEV_MENTION if author.bot else "" try: await self._sync(diff) @@ -217,11 +60,14 @@ class Syncer(abc.ABC): # Don't show response text because it's probably some really long HTML. results = f"status {e.status}\n```{e.response_json or 'See log output for details'}```" - content = f":x: {mention}Synchronisation of {self.name}s failed: {results}" + content = f":x: Synchronisation of {self.name}s failed: {results}" else: - results = ", ".join(f"{name} `{total}`" for name, total in totals.items()) + diff_dict = diff._asdict() # Ugly method for transforming the NamedTuple into a dict + results = (f"{name} `{len(val)}`" for name, val in diff_dict.items() if val is not None) + results = ", ".join(results) + log.info(f"{self.name} syncer finished: {results}.") - content = f":ok_hand: {mention}Synchronisation of {self.name}s complete: {results}" + content = f":ok_hand: Synchronisation of {self.name}s complete: {results}" if message: await message.edit(content=content) diff --git a/bot/constants.py b/bot/constants.py index 17fe34e95..3129354d3 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -563,13 +563,6 @@ class RedirectOutput(metaclass=YAMLGetter): delete_delay: int -class Sync(metaclass=YAMLGetter): - section = 'sync' - - confirm_timeout: int - max_diff: int - - class PythonNews(metaclass=YAMLGetter): section = 'python_news' diff --git a/config-default.yml b/config-default.yml index 6e7cff92d..d48739002 100644 --- a/config-default.yml +++ b/config-default.yml @@ -460,10 +460,6 @@ redirect_output: delete_invocation: true delete_delay: 15 -sync: - confirm_timeout: 300 - max_diff: 10 - duck_pond: threshold: 5 custom_emojis: diff --git a/tests/bot/cogs/sync/test_base.py b/tests/bot/cogs/sync/test_base.py index 70aea2bab..c3456f724 100644 --- a/tests/bot/cogs/sync/test_base.py +++ b/tests/bot/cogs/sync/test_base.py @@ -1,12 +1,9 @@ -import asyncio import unittest from unittest import mock -import discord -from bot import constants from bot.api import ResponseCodeError -from bot.cogs.sync.syncers import Syncer, _Diff +from bot.cogs.sync.syncers import Syncer from tests import helpers @@ -30,280 +27,16 @@ class SyncerBaseTests(unittest.TestCase): Syncer(self.bot) -class SyncerSendPromptTests(unittest.IsolatedAsyncioTestCase): - """Tests for sending the sync confirmation prompt.""" - - def setUp(self): - self.bot = helpers.MockBot() - self.syncer = TestSyncer(self.bot) - - def mock_get_channel(self): - """Fixture to return a mock channel and message for when `get_channel` is used.""" - self.bot.reset_mock() - - mock_channel = helpers.MockTextChannel() - mock_message = helpers.MockMessage() - - mock_channel.send.return_value = mock_message - self.bot.get_channel.return_value = mock_channel - - return mock_channel, mock_message - - def mock_fetch_channel(self): - """Fixture to return a mock channel and message for when `fetch_channel` is used.""" - self.bot.reset_mock() - - mock_channel = helpers.MockTextChannel() - mock_message = helpers.MockMessage() - - self.bot.get_channel.return_value = None - mock_channel.send.return_value = mock_message - self.bot.fetch_channel.return_value = mock_channel - - return mock_channel, mock_message - - async def test_send_prompt_edits_and_returns_message(self): - """The given message should be edited to display the prompt and then should be returned.""" - msg = helpers.MockMessage() - ret_val = await self.syncer._send_prompt(msg) - - msg.edit.assert_called_once() - self.assertIn("content", msg.edit.call_args[1]) - self.assertEqual(ret_val, msg) - - async def test_send_prompt_gets_dev_core_channel(self): - """The dev-core channel should be retrieved if an extant message isn't given.""" - subtests = ( - (self.bot.get_channel, self.mock_get_channel), - (self.bot.fetch_channel, self.mock_fetch_channel), - ) - - for method, mock_ in subtests: - with self.subTest(method=method, msg=mock_.__name__): - mock_() - await self.syncer._send_prompt() - - method.assert_called_once_with(constants.Channels.dev_core) - - async def test_send_prompt_returns_none_if_channel_fetch_fails(self): - """None should be returned if there's an HTTPException when fetching the channel.""" - self.bot.get_channel.return_value = None - self.bot.fetch_channel.side_effect = discord.HTTPException(mock.MagicMock(), "test error!") - - ret_val = await self.syncer._send_prompt() - - self.assertIsNone(ret_val) - - async def test_send_prompt_sends_and_returns_new_message_if_not_given(self): - """A new message mentioning core devs should be sent and returned if message isn't given.""" - for mock_ in (self.mock_get_channel, self.mock_fetch_channel): - with self.subTest(msg=mock_.__name__): - mock_channel, mock_message = mock_() - ret_val = await self.syncer._send_prompt() - - mock_channel.send.assert_called_once() - self.assertIn(self.syncer._CORE_DEV_MENTION, mock_channel.send.call_args[0][0]) - self.assertEqual(ret_val, mock_message) - - async def test_send_prompt_adds_reactions(self): - """The message should have reactions for confirmation added.""" - extant_message = helpers.MockMessage() - subtests = ( - (extant_message, lambda: (None, extant_message)), - (None, self.mock_get_channel), - (None, self.mock_fetch_channel), - ) - - for message_arg, mock_ in subtests: - subtest_msg = "Extant message" if mock_.__name__ == "" else mock_.__name__ - - with self.subTest(msg=subtest_msg): - _, mock_message = mock_() - await self.syncer._send_prompt(message_arg) - - calls = [mock.call(emoji) for emoji in self.syncer._REACTION_EMOJIS] - mock_message.add_reaction.assert_has_calls(calls) - - -class SyncerConfirmationTests(unittest.IsolatedAsyncioTestCase): - """Tests for waiting for a sync confirmation reaction on the prompt.""" - - def setUp(self): - self.bot = helpers.MockBot() - self.syncer = TestSyncer(self.bot) - self.core_dev_role = helpers.MockRole(id=constants.Roles.core_developers) - - @staticmethod - def get_message_reaction(emoji): - """Fixture to return a mock message an reaction from the given `emoji`.""" - message = helpers.MockMessage() - reaction = helpers.MockReaction(emoji=emoji, message=message) - - return message, reaction - - def test_reaction_check_for_valid_emoji_and_authors(self): - """Should return True if authors are identical or are a bot and a core dev, respectively.""" - user_subtests = ( - ( - helpers.MockMember(id=77), - helpers.MockMember(id=77), - "identical users", - ), - ( - helpers.MockMember(id=77, bot=True), - helpers.MockMember(id=43, roles=[self.core_dev_role]), - "bot author and core-dev reactor", - ), - ) - - for emoji in self.syncer._REACTION_EMOJIS: - for author, user, msg in user_subtests: - with self.subTest(author=author, user=user, emoji=emoji, msg=msg): - message, reaction = self.get_message_reaction(emoji) - ret_val = self.syncer._reaction_check(author, message, reaction, user) - - self.assertTrue(ret_val) - - def test_reaction_check_for_invalid_reactions(self): - """Should return False for invalid reaction events.""" - valid_emoji = self.syncer._REACTION_EMOJIS[0] - subtests = ( - ( - helpers.MockMember(id=77), - *self.get_message_reaction(valid_emoji), - helpers.MockMember(id=43, roles=[self.core_dev_role]), - "users are not identical", - ), - ( - helpers.MockMember(id=77, bot=True), - *self.get_message_reaction(valid_emoji), - helpers.MockMember(id=43), - "reactor lacks the core-dev role", - ), - ( - helpers.MockMember(id=77, bot=True, roles=[self.core_dev_role]), - *self.get_message_reaction(valid_emoji), - helpers.MockMember(id=77, bot=True, roles=[self.core_dev_role]), - "reactor is a bot", - ), - ( - helpers.MockMember(id=77), - helpers.MockMessage(id=95), - helpers.MockReaction(emoji=valid_emoji, message=helpers.MockMessage(id=26)), - helpers.MockMember(id=77), - "messages are not identical", - ), - ( - helpers.MockMember(id=77), - *self.get_message_reaction("InVaLiD"), - helpers.MockMember(id=77), - "emoji is invalid", - ), - ) - - for *args, msg in subtests: - kwargs = dict(zip(("author", "message", "reaction", "user"), args)) - with self.subTest(**kwargs, msg=msg): - ret_val = self.syncer._reaction_check(*args) - self.assertFalse(ret_val) - - async def test_wait_for_confirmation(self): - """The message should always be edited and only return True if the emoji is a check mark.""" - subtests = ( - (constants.Emojis.check_mark, True, None), - ("InVaLiD", False, None), - (None, False, asyncio.TimeoutError), - ) - - for emoji, ret_val, side_effect in subtests: - for bot in (True, False): - with self.subTest(emoji=emoji, ret_val=ret_val, side_effect=side_effect, bot=bot): - # Set up mocks - message = helpers.MockMessage() - member = helpers.MockMember(bot=bot) - - self.bot.wait_for.reset_mock() - self.bot.wait_for.return_value = (helpers.MockReaction(emoji=emoji), None) - self.bot.wait_for.side_effect = side_effect - - # Call the function - actual_return = await self.syncer._wait_for_confirmation(member, message) - - # Perform assertions - self.bot.wait_for.assert_called_once() - self.assertIn("reaction_add", self.bot.wait_for.call_args[0]) - - message.edit.assert_called_once() - kwargs = message.edit.call_args[1] - self.assertIn("content", kwargs) - - # Core devs should only be mentioned if the author is a bot. - if bot: - self.assertIn(self.syncer._CORE_DEV_MENTION, kwargs["content"]) - else: - self.assertNotIn(self.syncer._CORE_DEV_MENTION, kwargs["content"]) - - self.assertIs(actual_return, ret_val) - - class SyncerSyncTests(unittest.IsolatedAsyncioTestCase): """Tests for main function orchestrating the sync.""" def setUp(self): self.bot = helpers.MockBot(user=helpers.MockMember(bot=True)) self.syncer = TestSyncer(self.bot) + self.guild = helpers.MockGuild() - async def test_sync_respects_confirmation_result(self): - """The sync should abort if confirmation fails and continue if confirmed.""" - mock_message = helpers.MockMessage() - subtests = ( - (True, mock_message), - (False, None), - ) - - for confirmed, message in subtests: - with self.subTest(confirmed=confirmed): - self.syncer._sync.reset_mock() - self.syncer._get_diff.reset_mock() - - diff = _Diff({1, 2, 3}, {4, 5}, None) - self.syncer._get_diff.return_value = diff - self.syncer._get_confirmation_result = mock.AsyncMock( - return_value=(confirmed, message) - ) - - guild = helpers.MockGuild() - await self.syncer.sync(guild) - - self.syncer._get_diff.assert_called_once_with(guild) - self.syncer._get_confirmation_result.assert_called_once() - - if confirmed: - self.syncer._sync.assert_called_once_with(diff) - else: - self.syncer._sync.assert_not_called() - - async def test_sync_diff_size(self): - """The diff size should be correctly calculated.""" - subtests = ( - (6, _Diff({1, 2}, {3, 4}, {5, 6})), - (5, _Diff({1, 2, 3}, None, {4, 5})), - (0, _Diff(None, None, None)), - (0, _Diff(set(), set(), set())), - ) - - for size, diff in subtests: - with self.subTest(size=size, diff=diff): - self.syncer._get_diff.reset_mock() - self.syncer._get_diff.return_value = diff - self.syncer._get_confirmation_result = mock.AsyncMock(return_value=(False, None)) - - guild = helpers.MockGuild() - await self.syncer.sync(guild) - - self.syncer._get_diff.assert_called_once_with(guild) - self.syncer._get_confirmation_result.assert_called_once() - self.assertEqual(self.syncer._get_confirmation_result.call_args[0][0], size) + # Make sure `_get_diff` returns a MagicMock, not an AsyncMock + self.syncer._get_diff.return_value = mock.MagicMock() async def test_sync_message_edited(self): """The message should be edited if one was sent, even if the sync has an API error.""" @@ -316,89 +49,23 @@ class SyncerSyncTests(unittest.IsolatedAsyncioTestCase): for message, side_effect, should_edit in subtests: with self.subTest(message=message, side_effect=side_effect, should_edit=should_edit): self.syncer._sync.side_effect = side_effect - self.syncer._get_confirmation_result = mock.AsyncMock( - return_value=(True, message) - ) - guild = helpers.MockGuild() - await self.syncer.sync(guild) + await self.syncer.sync(self.guild) if should_edit: message.edit.assert_called_once() self.assertIn("content", message.edit.call_args[1]) - async def test_sync_confirmation_context_redirect(self): - """If ctx is given, a new message should be sent and author should be ctx's author.""" - mock_member = helpers.MockMember() + async def test_sync_message_sent(self): + """If ctx is given, a new message should be sent.""" subtests = ( - (None, self.bot.user, None), - (helpers.MockContext(author=mock_member), mock_member, helpers.MockMessage()), + (None, None), + (helpers.MockContext(), helpers.MockMessage()), ) - for ctx, author, message in subtests: - with self.subTest(ctx=ctx, author=author, message=message): - if ctx is not None: - ctx.send.return_value = message - - # Make sure `_get_diff` returns a MagicMock, not an AsyncMock - self.syncer._get_diff.return_value = mock.MagicMock() - - self.syncer._get_confirmation_result = mock.AsyncMock(return_value=(False, None)) - - guild = helpers.MockGuild() - await self.syncer.sync(guild, ctx) + for ctx, message in subtests: + with self.subTest(ctx=ctx, message=message): + await self.syncer.sync(self.guild, ctx) if ctx is not None: ctx.send.assert_called_once() - - self.syncer._get_confirmation_result.assert_called_once() - self.assertEqual(self.syncer._get_confirmation_result.call_args[0][1], author) - self.assertEqual(self.syncer._get_confirmation_result.call_args[0][2], message) - - @mock.patch.object(constants.Sync, "max_diff", new=3) - async def test_confirmation_result_small_diff(self): - """Should always return True and the given message if the diff size is too small.""" - author = helpers.MockMember() - expected_message = helpers.MockMessage() - - for size in (3, 2): # pragma: no cover - with self.subTest(size=size): - self.syncer._send_prompt = mock.AsyncMock() - self.syncer._wait_for_confirmation = mock.AsyncMock() - - coro = self.syncer._get_confirmation_result(size, author, expected_message) - result, actual_message = await coro - - self.assertTrue(result) - self.assertEqual(actual_message, expected_message) - self.syncer._send_prompt.assert_not_called() - self.syncer._wait_for_confirmation.assert_not_called() - - @mock.patch.object(constants.Sync, "max_diff", new=3) - async def test_confirmation_result_large_diff(self): - """Should return True if confirmed and False if _send_prompt fails or aborted.""" - author = helpers.MockMember() - mock_message = helpers.MockMessage() - - subtests = ( - (True, mock_message, True, "confirmed"), - (False, None, False, "_send_prompt failed"), - (False, mock_message, False, "aborted"), - ) - - for expected_result, expected_message, confirmed, msg in subtests: # pragma: no cover - with self.subTest(msg=msg): - self.syncer._send_prompt = mock.AsyncMock(return_value=expected_message) - self.syncer._wait_for_confirmation = mock.AsyncMock(return_value=confirmed) - - coro = self.syncer._get_confirmation_result(4, author) - actual_result, actual_message = await coro - - self.syncer._send_prompt.assert_called_once_with(None) # message defaults to None - self.assertIs(actual_result, expected_result) - self.assertEqual(actual_message, expected_message) - - if expected_message: - self.syncer._wait_for_confirmation.assert_called_once_with( - author, expected_message - ) -- cgit v1.2.3 From 9de2bbec0c1ab1462b5fd3b6e59b544060b3d472 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 7 Sep 2020 16:01:47 -0700 Subject: Fix test for sync message being edited --- tests/bot/cogs/sync/test_base.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/bot/cogs/sync/test_base.py b/tests/bot/cogs/sync/test_base.py index c3456f724..8d6f48333 100644 --- a/tests/bot/cogs/sync/test_base.py +++ b/tests/bot/cogs/sync/test_base.py @@ -49,8 +49,10 @@ class SyncerSyncTests(unittest.IsolatedAsyncioTestCase): for message, side_effect, should_edit in subtests: with self.subTest(message=message, side_effect=side_effect, should_edit=should_edit): self.syncer._sync.side_effect = side_effect + ctx = helpers.MockContext() + ctx.send.return_value = message - await self.syncer.sync(self.guild) + await self.syncer.sync(self.guild, ctx) if should_edit: message.edit.assert_called_once() -- cgit v1.2.3 From 73c21c6fda0472cd2eabaa3ffc0b58b0782ecf84 Mon Sep 17 00:00:00 2001 From: Mark Date: Tue, 22 Sep 2020 12:28:01 -0700 Subject: Sync: refactor conditional for sending message The ternary is a bit confusing. Co-authored-by: Numerlor <25886452+Numerlor@users.noreply.github.com> --- bot/cogs/sync/syncers.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bot/cogs/sync/syncers.py b/bot/cogs/sync/syncers.py index b3819a1e1..e2013dafd 100644 --- a/bot/cogs/sync/syncers.py +++ b/bot/cogs/sync/syncers.py @@ -50,7 +50,10 @@ class Syncer(abc.ABC): """ log.info(f"Starting {self.name} syncer.") - message = await ctx.send(f"📊 Synchronising {self.name}s.") if ctx else None + if ctx: + message = await ctx.send(f"📊 Synchronising {self.name}s.") + else: + message = None diff = await self._get_diff(guild) try: -- cgit v1.2.3 From 9a90d6b16e0ec61c023a916ec58d05b6142a6e2d Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Wed, 23 Sep 2020 13:25:23 -0700 Subject: Sync: remove _asdict comment The comment doesn't contribute anything. --- bot/exts/backend/sync/_syncers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/backend/sync/_syncers.py b/bot/exts/backend/sync/_syncers.py index e2013dafd..a07a93eab 100644 --- a/bot/exts/backend/sync/_syncers.py +++ b/bot/exts/backend/sync/_syncers.py @@ -65,7 +65,7 @@ class Syncer(abc.ABC): results = f"status {e.status}\n```{e.response_json or 'See log output for details'}```" content = f":x: Synchronisation of {self.name}s failed: {results}" else: - diff_dict = diff._asdict() # Ugly method for transforming the NamedTuple into a dict + diff_dict = diff._asdict() results = (f"{name} `{len(val)}`" for name, val in diff_dict.items() if val is not None) results = ", ".join(results) -- cgit v1.2.3 From f81920ad6427490a06061cfb8533828b26735dcf Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 26 Sep 2020 12:23:20 -0700 Subject: Sync: update sync() docstring --- bot/exts/backend/sync/_syncers.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bot/exts/backend/sync/_syncers.py b/bot/exts/backend/sync/_syncers.py index a07a93eab..3d4a09df3 100644 --- a/bot/exts/backend/sync/_syncers.py +++ b/bot/exts/backend/sync/_syncers.py @@ -44,9 +44,7 @@ class Syncer(abc.ABC): """ Synchronise the database with the cache of `guild`. - If the differences between the cache and the database are greater than - `bot.constants.Sync.max_diff`, then a confirmation prompt will be sent to the dev-core - channel. The confirmation can be optionally redirect to `ctx` instead. + If `ctx` is given, send a message with the results. """ log.info(f"Starting {self.name} syncer.") -- cgit v1.2.3 From 107ca75eedb2cdc140df9b9116b53998bfd61cfe Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sun, 27 Sep 2020 14:27:36 +0200 Subject: Add the video to the welcome DM. This rewords the welcome DM, and adds the new Welcome To Python Discord video to it. --- bot/exts/moderation/verification.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/bot/exts/moderation/verification.py b/bot/exts/moderation/verification.py index 6bbe81701..e9ab2c816 100644 --- a/bot/exts/moderation/verification.py +++ b/bot/exts/moderation/verification.py @@ -21,12 +21,15 @@ log = logging.getLogger(__name__) # Sent via DMs once user joins the guild ON_JOIN_MESSAGE = f""" -Hello! Welcome to Python Discord! +Welcome to Python Discord! -As a new user, you have read-only access to a few select channels to give you a taste of what our server is like. +To show you what kind of community we are, we've created this video: +https://youtu.be/ZH26PuX3re0 -In order to see the rest of the channels and to send messages, you first have to accept our rules. To do so, \ -please visit <#{constants.Channels.verification}>. Thank you! +As a new user, you have read-only access to a few select channels to give you a taste of what our server is like. \ +In order to see the rest of the channels and to send messages, you first have to accept our rules. + +Please visit <#{constants.Channels.verification}> to get started. Thank you! """ # Sent via DMs once user verifies -- cgit v1.2.3 From 2032391d50c16d15ab71fb0b29081c89bf77e751 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sun, 27 Sep 2020 14:41:36 +0200 Subject: Relock Pipfile to update async-redis. This also bumps minor versions of several other packages. I've spun up the bot and played around with it, and run all unit tests. Everything still seems to be in order. --- Pipfile.lock | 132 +++++++++++++++++++++++++++++++---------------------------- 1 file changed, 70 insertions(+), 62 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index f75852081..4c63277de 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -86,12 +86,12 @@ "fakeredis" ], "hashes": [ - "sha256:407aed1aad97bf22f690eca5369806d22eefc8ca104a52c1f1bd47dd6db45fc2", - "sha256:563aaff79ec611a92a0ad78e39ff159e3a4b4cf0bea41e061de5f3701a17d50c" + "sha256:6be8a657d724ccbcfb1946d29a80c3478c5f9ecd2f78a0a26d2f4013a622258f", + "sha256:c25e4fff73f64d20645254783c3224a4c49e083e3fab67c44f17af944c5e26af" ], "index": "pypi", "markers": "python_version ~= '3.7'", - "version": "==0.1.2" + "version": "==0.1.4" }, "async-timeout": { "hashes": [ @@ -119,12 +119,12 @@ }, "beautifulsoup4": { "hashes": [ - "sha256:73cc4d115b96f79c7d77c1c7f7a0a8d4c57860d1041df407dd1aae7f07a77fd7", - "sha256:a6237df3c32ccfaee4fd201c8f5f9d9df619b93121d01353a64a73ce8c6ef9a8", - "sha256:e718f2342e2e099b640a34ab782407b7b676f47ee272d6739e60b8ea23829f2c" + "sha256:1edf5e39f3a5bc6e38b235b369128416c7239b34f692acccececb040233032a1", + "sha256:5dfe44f8fddc89ac5453f02659d3ab1668f2c0d9684839f0785037e8c6d9ac8d", + "sha256:645d833a828722357038299b7f6879940c11dddd95b900fe5387c258b72bb883" ], "index": "pypi", - "version": "==4.9.1" + "version": "==4.9.2" }, "certifi": { "hashes": [ @@ -135,36 +135,44 @@ }, "cffi": { "hashes": [ - "sha256:0da50dcbccd7cb7e6c741ab7912b2eff48e85af217d72b57f80ebc616257125e", - "sha256:12a453e03124069b6896107ee133ae3ab04c624bb10683e1ed1c1663df17c13c", - "sha256:15419020b0e812b40d96ec9d369b2bc8109cc3295eac6e013d3261343580cc7e", - "sha256:15a5f59a4808f82d8ec7364cbace851df591c2d43bc76bcbe5c4543a7ddd1bf1", - "sha256:23e44937d7695c27c66a54d793dd4b45889a81b35c0751ba91040fe825ec59c4", - "sha256:29c4688ace466a365b85a51dcc5e3c853c1d283f293dfcc12f7a77e498f160d2", - "sha256:57214fa5430399dffd54f4be37b56fe22cedb2b98862550d43cc085fb698dc2c", - "sha256:577791f948d34d569acb2d1add5831731c59d5a0c50a6d9f629ae1cefd9ca4a0", - "sha256:6539314d84c4d36f28d73adc1b45e9f4ee2a89cdc7e5d2b0a6dbacba31906798", - "sha256:65867d63f0fd1b500fa343d7798fa64e9e681b594e0a07dc934c13e76ee28fb1", - "sha256:672b539db20fef6b03d6f7a14b5825d57c98e4026401fce838849f8de73fe4d4", - "sha256:6843db0343e12e3f52cc58430ad559d850a53684f5b352540ca3f1bc56df0731", - "sha256:7057613efefd36cacabbdbcef010e0a9c20a88fc07eb3e616019ea1692fa5df4", - "sha256:76ada88d62eb24de7051c5157a1a78fd853cca9b91c0713c2e973e4196271d0c", - "sha256:837398c2ec00228679513802e3744d1e8e3cb1204aa6ad408b6aff081e99a487", - "sha256:8662aabfeab00cea149a3d1c2999b0731e70c6b5bac596d95d13f643e76d3d4e", - "sha256:95e9094162fa712f18b4f60896e34b621df99147c2cee216cfa8f022294e8e9f", - "sha256:99cc66b33c418cd579c0f03b77b94263c305c389cb0c6972dac420f24b3bf123", - "sha256:9b219511d8b64d3fa14261963933be34028ea0e57455baf6781fe399c2c3206c", - "sha256:ae8f34d50af2c2154035984b8b5fc5d9ed63f32fe615646ab435b05b132ca91b", - "sha256:b9aa9d8818c2e917fa2c105ad538e222a5bce59777133840b93134022a7ce650", - "sha256:bf44a9a0141a082e89c90e8d785b212a872db793a0080c20f6ae6e2a0ebf82ad", - "sha256:c0b48b98d79cf795b0916c57bebbc6d16bb43b9fc9b8c9f57f4cf05881904c75", - "sha256:da9d3c506f43e220336433dffe643fbfa40096d408cb9b7f2477892f369d5f82", - "sha256:e4082d832e36e7f9b2278bc774886ca8207346b99f278e54c9de4834f17232f7", - "sha256:e4b9b7af398c32e408c00eb4e0d33ced2f9121fd9fb978e6c1b57edd014a7d15", - "sha256:e613514a82539fc48291d01933951a13ae93b6b444a88782480be32245ed4afa", - "sha256:f5033952def24172e60493b68717792e3aebb387a8d186c43c020d9363ee7281" - ], - "version": "==1.14.2" + "sha256:005f2bfe11b6745d726dbb07ace4d53f057de66e336ff92d61b8c7e9c8f4777d", + "sha256:09e96138280241bd355cd585148dec04dbbedb4f46128f340d696eaafc82dd7b", + "sha256:0b1ad452cc824665ddc682400b62c9e4f5b64736a2ba99110712fdee5f2505c4", + "sha256:0ef488305fdce2580c8b2708f22d7785ae222d9825d3094ab073e22e93dfe51f", + "sha256:15f351bed09897fbda218e4db5a3d5c06328862f6198d4fb385f3e14e19decb3", + "sha256:22399ff4870fb4c7ef19fff6eeb20a8bbf15571913c181c78cb361024d574579", + "sha256:23e5d2040367322824605bc29ae8ee9175200b92cb5483ac7d466927a9b3d537", + "sha256:2791f68edc5749024b4722500e86303a10d342527e1e3bcac47f35fbd25b764e", + "sha256:2f9674623ca39c9ebe38afa3da402e9326c245f0f5ceff0623dccdac15023e05", + "sha256:3363e77a6176afb8823b6e06db78c46dbc4c7813b00a41300a4873b6ba63b171", + "sha256:33c6cdc071ba5cd6d96769c8969a0531be2d08c2628a0143a10a7dcffa9719ca", + "sha256:3b8eaf915ddc0709779889c472e553f0d3e8b7bdf62dab764c8921b09bf94522", + "sha256:3cb3e1b9ec43256c4e0f8d2837267a70b0e1ca8c4f456685508ae6106b1f504c", + "sha256:3eeeb0405fd145e714f7633a5173318bd88d8bbfc3dd0a5751f8c4f70ae629bc", + "sha256:44f60519595eaca110f248e5017363d751b12782a6f2bd6a7041cba275215f5d", + "sha256:4d7c26bfc1ea9f92084a1d75e11999e97b62d63128bcc90c3624d07813c52808", + "sha256:529c4ed2e10437c205f38f3691a68be66c39197d01062618c55f74294a4a4828", + "sha256:6642f15ad963b5092d65aed022d033c77763515fdc07095208f15d3563003869", + "sha256:85ba797e1de5b48aa5a8427b6ba62cf69607c18c5d4eb747604b7302f1ec382d", + "sha256:8f0f1e499e4000c4c347a124fa6a27d37608ced4fe9f7d45070563b7c4c370c9", + "sha256:a624fae282e81ad2e4871bdb767e2c914d0539708c0f078b5b355258293c98b0", + "sha256:b0358e6fefc74a16f745afa366acc89f979040e0cbc4eec55ab26ad1f6a9bfbc", + "sha256:bbd2f4dfee1079f76943767fce837ade3087b578aeb9f69aec7857d5bf25db15", + "sha256:bf39a9e19ce7298f1bd6a9758fa99707e9e5b1ebe5e90f2c3913a47bc548747c", + "sha256:c11579638288e53fc94ad60022ff1b67865363e730ee41ad5e6f0a17188b327a", + "sha256:c150eaa3dadbb2b5339675b88d4573c1be3cb6f2c33a6c83387e10cc0bf05bd3", + "sha256:c53af463f4a40de78c58b8b2710ade243c81cbca641e34debf3396a9640d6ec1", + "sha256:cb763ceceae04803adcc4e2d80d611ef201c73da32d8f2722e9d0ab0c7f10768", + "sha256:cc75f58cdaf043fe6a7a6c04b3b5a0e694c6a9e24050967747251fb80d7bce0d", + "sha256:d80998ed59176e8cba74028762fbd9b9153b9afc71ea118e63bbf5d4d0f9552b", + "sha256:de31b5164d44ef4943db155b3e8e17929707cac1e5bd2f363e67a56e3af4af6e", + "sha256:e66399cf0fc07de4dce4f588fc25bfe84a6d1285cc544e67987d22663393926d", + "sha256:f0620511387790860b249b9241c2f13c3a80e21a73e0b861a2df24e9d6f56730", + "sha256:f4eae045e6ab2bb54ca279733fe4eb85f1effda392666308250714e01907f394", + "sha256:f92cdecb618e5fa4658aeb97d5eb3d2f47aa94ac6477c6daf0f306c5a3b9e6b1", + "sha256:f92f789e4f9241cd262ad7a555ca2c648a98178a953af117ef7fad46aa1d5591" + ], + "version": "==1.14.3" }, "chardet": { "hashes": [ @@ -575,11 +583,11 @@ }, "sentry-sdk": { "hashes": [ - "sha256:1a086486ff9da15791f294f6e9915eb3747d161ef64dee2d038a4d0b4a369b24", - "sha256:45486deb031cea6bbb25a540d7adb4dd48cd8a1cc31e6a5ce9fb4f792a572e9a" + "sha256:c9c0fa1412bad87104c4eee8dd36c7bbf60b0d92ae917ab519094779b22e6d9a", + "sha256:e159f7c919d19ae86e5a4ff370fccc45149fab461fbeb93fb5a735a0b33a9cb1" ], "index": "pypi", - "version": "==0.17.6" + "version": "==0.17.8" }, "six": { "hashes": [ @@ -608,7 +616,7 @@ "sha256:1634eea42ab371d3d346309b93df7870a88610f0725d47528be902a0d95ecc55", "sha256:a59dc181727e95d25f781f0eb4fd1825ff45590ec8ff49eadfd7f1a537cc0232" ], - "markers": "python_version >= '3.5'", + "markers": "python_version >= '3.0'", "version": "==2.0.1" }, "sphinx": { @@ -685,26 +693,26 @@ }, "yarl": { "hashes": [ - "sha256:040b237f58ff7d800e6e0fd89c8439b841f777dd99b4a9cca04d6935564b9409", - "sha256:17668ec6722b1b7a3a05cc0167659f6c95b436d25a36c2d52db0eca7d3f72593", - "sha256:3a584b28086bc93c888a6c2aa5c92ed1ae20932f078c46509a66dce9ea5533f2", - "sha256:4439be27e4eee76c7632c2427ca5e73703151b22cae23e64adb243a9c2f565d8", - "sha256:48e918b05850fffb070a496d2b5f97fc31d15d94ca33d3d08a4f86e26d4e7c5d", - "sha256:9102b59e8337f9874638fcfc9ac3734a0cfadb100e47d55c20d0dc6087fb4692", - "sha256:9b930776c0ae0c691776f4d2891ebc5362af86f152dd0da463a6614074cb1b02", - "sha256:b3b9ad80f8b68519cc3372a6ca85ae02cc5a8807723ac366b53c0f089db19e4a", - "sha256:bc2f976c0e918659f723401c4f834deb8a8e7798a71be4382e024bcc3f7e23a8", - "sha256:c22c75b5f394f3d47105045ea551e08a3e804dc7e01b37800ca35b58f856c3d6", - "sha256:c52ce2883dc193824989a9b97a76ca86ecd1fa7955b14f87bf367a61b6232511", - "sha256:ce584af5de8830d8701b8979b18fcf450cef9a382b1a3c8ef189bedc408faf1e", - "sha256:da456eeec17fa8aa4594d9a9f27c0b1060b6a75f2419fe0c00609587b2695f4a", - "sha256:db6db0f45d2c63ddb1a9d18d1b9b22f308e52c83638c26b422d520a815c4b3fb", - "sha256:df89642981b94e7db5596818499c4b2219028f2a528c9c37cc1de45bf2fd3a3f", - "sha256:f18d68f2be6bf0e89f1521af2b1bb46e66ab0018faafa81d70f358153170a317", - "sha256:f379b7f83f23fe12823085cd6b906edc49df969eb99757f58ff382349a3303c6" + "sha256:04a54f126a0732af75e5edc9addeaa2113e2ca7c6fce8974a63549a70a25e50e", + "sha256:3cc860d72ed989f3b1f3abbd6ecf38e412de722fb38b8f1b1a086315cf0d69c5", + "sha256:5d84cc36981eb5a8533be79d6c43454c8e6a39ee3118ceaadbd3c029ab2ee580", + "sha256:5e447e7f3780f44f890360ea973418025e8c0cdcd7d6a1b221d952600fd945dc", + "sha256:61d3ea3c175fe45f1498af868879c6ffeb989d4143ac542163c45538ba5ec21b", + "sha256:67c5ea0970da882eaf9efcf65b66792557c526f8e55f752194eff8ec722c75c2", + "sha256:6f6898429ec3c4cfbef12907047136fd7b9e81a6ee9f105b45505e633427330a", + "sha256:7ce35944e8e61927a8f4eb78f5bc5d1e6da6d40eadd77e3f79d4e9399e263921", + "sha256:b7c199d2cbaf892ba0f91ed36d12ff41ecd0dde46cbf64ff4bfe997a3ebc925e", + "sha256:c15d71a640fb1f8e98a1423f9c64d7f1f6a3a168f803042eaf3a5b5022fde0c1", + "sha256:c22607421f49c0cb6ff3ed593a49b6a99c6ffdeaaa6c944cdda83c2393c8864d", + "sha256:c604998ab8115db802cc55cb1b91619b2831a6128a62ca7eea577fc8ea4d3131", + "sha256:d088ea9319e49273f25b1c96a3763bf19a882cff774d1792ae6fba34bd40550a", + "sha256:db9eb8307219d7e09b33bcb43287222ef35cbcf1586ba9472b0a4b833666ada1", + "sha256:e31fef4e7b68184545c3d68baec7074532e077bd1906b040ecfba659737df188", + "sha256:e32f0fb443afcfe7f01f95172b66f279938fbc6bdaebe294b0ff6747fb6db020", + "sha256:fcbe419805c9b20db9a51d33b942feddbf6e7fb468cb20686fd7089d4164c12a" ], "markers": "python_version >= '3.5'", - "version": "==1.5.1" + "version": "==1.6.0" } }, "develop": { @@ -857,11 +865,11 @@ }, "identify": { "hashes": [ - "sha256:c770074ae1f19e08aadbda1c886bc6d0cb55ffdc503a8c0fe8699af2fc9664ae", - "sha256:d02d004568c5a01261839a05e91705e3e9f5c57a3551648f9b3fb2b9c62c0f62" + "sha256:7c22c384a2c9b32c5cc891d13f923f6b2653aa83e2d75d8f79be240d6c86c4f4", + "sha256:da683bfb7669fa749fc7731f378229e2dbf29a1d1337cbde04106f02236eb29d" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.5.3" + "version": "==1.5.5" }, "mccabe": { "hashes": [ -- cgit v1.2.3