From 81711d77750be55a62a927b1c90f0eaf773e0567 Mon Sep 17 00:00:00 2001 From: ks123 Date: Tue, 3 Mar 2020 16:21:53 +0200 Subject: Created file for moderation utils tests + added setUp to this. --- tests/bot/cogs/moderation/test_utils.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 tests/bot/cogs/moderation/test_utils.py diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py new file mode 100644 index 000000000..ed1d1ed59 --- /dev/null +++ b/tests/bot/cogs/moderation/test_utils.py @@ -0,0 +1,12 @@ +import unittest + + +from tests.helpers import MockBot, MockContext + + +class ModerationUtilsTests(unittest.TestCase): + """Tests Moderation utils.""" + + def setUp(self) -> None: + self.bot = MockBot() + self.ctx = MockContext(bot=self.bot) -- cgit v1.2.3 From 154969022cf62bd4a2bab2f7492ded08bb26ffba Mon Sep 17 00:00:00 2001 From: ks123 Date: Tue, 3 Mar 2020 16:32:13 +0200 Subject: (Moderation Utils Tests): Added imports, modified tests class instance and created new params for tests class --- tests/bot/cogs/moderation/test_utils.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index ed1d1ed59..7d47715d4 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -1,12 +1,14 @@ import unittest +from unittest.mock import AsyncMock +from tests.helpers import MockBot, MockContext, MockMember -from tests.helpers import MockBot, MockContext - -class ModerationUtilsTests(unittest.TestCase): +class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): """Tests Moderation utils.""" - def setUp(self) -> None: + def setUp(self): self.bot = MockBot() - self.ctx = MockContext(bot=self.bot) + self.member = MockMember(id=1234) + self.ctx = MockContext(bot=self.bot, author=self.member) + self.bot.api_client.get = AsyncMock() -- cgit v1.2.3 From fa6a0ae59958ce143f6a7acfbd41e477e940fa84 Mon Sep 17 00:00:00 2001 From: ks123 Date: Tue, 3 Mar 2020 17:29:20 +0200 Subject: (Moderation Utils Tests): Created tests for `has_active_infraction` function --- tests/bot/cogs/moderation/test_utils.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index 7d47715d4..d25fbfcb5 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -1,6 +1,7 @@ import unittest from unittest.mock import AsyncMock +from bot.cogs.moderation.utils import has_active_infraction from tests.helpers import MockBot, MockContext, MockMember @@ -12,3 +13,26 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): self.member = MockMember(id=1234) self.ctx = MockContext(bot=self.bot, author=self.member) self.bot.api_client.get = AsyncMock() + + async def test_user_has_active_infraction_true(self): + """Test does `has_active_infraction` return that user have active infraction.""" + self.bot.api_client.get.return_value = [{ + "id": 1, + "inserted_at": "2018-11-22T07:24:06.132307Z", + "expires_at": "5018-11-20T15:52:00Z", + "active": True, + "user": 1234, + "actor": 1234, + "type": "ban", + "reason": "Test", + "hidden": False + }] + self.assertTrue(await has_active_infraction(self.ctx, self.member, "ban"), "User should have active infraction") + + async def test_user_has_active_infraction_false(self): + """Test does `has_active_infraction` return that user don't have active infractions.""" + self.bot.api_client.get.return_value = [] + self.assertFalse( + await has_active_infraction(self.ctx, self.member, "ban"), + "User shouldn't have active infraction" + ) -- cgit v1.2.3 From 98f7a3777152b32bfda24f9d5add938479827c85 Mon Sep 17 00:00:00 2001 From: ks123 Date: Wed, 4 Mar 2020 18:15:54 +0200 Subject: (Moderation Utils Tests): Created tests for `notify_infraction` function. --- tests/bot/cogs/moderation/test_utils.py | 93 ++++++++++++++++++++++++++++++++- 1 file changed, 91 insertions(+), 2 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index d25fbfcb5..89f853262 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -1,8 +1,25 @@ import unittest from unittest.mock import AsyncMock -from bot.cogs.moderation.utils import has_active_infraction -from tests.helpers import MockBot, MockContext, MockMember +from discord import Embed + +from bot.cogs.moderation.utils import has_active_infraction, notify_infraction +from bot.constants import Colours, Icons +from tests.helpers import MockBot, MockContext, MockMember, MockUser + +RULES_URL = "https://pythondiscord.com/pages/rules" +APPEAL_EMAIL = "appeals@pythondiscord.com" + +INFRACTION_TITLE = f"Please review our rules over at {RULES_URL}" +INFRACTION_APPEAL_FOOTER = f"To appeal this infraction, send an e-mail to {APPEAL_EMAIL}" +INFRACTION_AUTHOR_NAME = "Infraction information" +INFRACTION_COLOR = Colours.soft_red + +INFRACTION_DESCRIPTION_TEMPLATE = ( + "\n**Type:** {type}\n" + "**Expires:** {expires}\n" + "**Reason:** {reason}\n" +) class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): @@ -11,6 +28,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): def setUp(self): self.bot = MockBot() self.member = MockMember(id=1234) + self.user = MockUser(id=1234) self.ctx = MockContext(bot=self.bot, author=self.member) self.bot.api_client.get = AsyncMock() @@ -36,3 +54,74 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): await has_active_infraction(self.ctx, self.member, "ban"), "User shouldn't have active infraction" ) + + async def test_notify_infraction(self): + """Test does `notify_infraction` create correct embed.""" + test_cases = [ + { + "args": (self.user, "ban", "2020-02-26 09:20 (23 hours and 59 minutes)"), + "expected_output": { + "description": INFRACTION_DESCRIPTION_TEMPLATE.format(**{ + "type": "Ban", + "expires": "2020-02-26 09:20 (23 hours and 59 minutes)", + "reason": "No reason provided." + }), + "icon_url": Icons.token_removed, + "footer": INFRACTION_APPEAL_FOOTER + } + }, + { + "args": (self.user, "warning", None, "Test reason."), + "expected_output": { + "description": INFRACTION_DESCRIPTION_TEMPLATE.format(**{ + "type": "Warning", + "expires": "N/A", + "reason": "Test reason." + }), + "icon_url": Icons.token_removed, + "footer": Embed.Empty + } + }, + { + "args": (self.user, "note", None, None, Icons.defcon_denied), + "expected_output": { + "description": INFRACTION_DESCRIPTION_TEMPLATE.format(**{ + "type": "Note", + "expires": "N/A", + "reason": "No reason provided." + }), + "icon_url": Icons.defcon_denied, + "footer": Embed.Empty + } + }, + { + "args": (self.user, "mute", "2020-02-26 09:20 (23 hours and 59 minutes)", "Test", Icons.defcon_denied), + "expected_output": { + "description": INFRACTION_DESCRIPTION_TEMPLATE.format(**{ + "type": "Mute", + "expires": "2020-02-26 09:20 (23 hours and 59 minutes)", + "reason": "Test" + }), + "icon_url": Icons.defcon_denied, + "footer": INFRACTION_APPEAL_FOOTER + } + } + ] + + for case in test_cases: + args = case["args"] + expected = case["expected_output"] + + with self.subTest(args=case["args"], expected=case["expected_output"]): + await notify_infraction(*args) + + embed: Embed = self.user.send.call_args[1]["embed"] + + self.assertEqual(embed.title, INFRACTION_TITLE) + self.assertEqual(embed.colour.value, INFRACTION_COLOR) + self.assertEqual(embed.url, RULES_URL) + self.assertEqual(embed.author.name, INFRACTION_AUTHOR_NAME) + self.assertEqual(embed.author.url, RULES_URL) + self.assertEqual(embed.author.icon_url, expected["icon_url"]) + self.assertEqual(embed.footer.text, expected["footer"]) + self.assertEqual(embed.description, expected["description"]) -- cgit v1.2.3 From 4a746fc60b6c51e20e1fab92726665092405f93d Mon Sep 17 00:00:00 2001 From: ks123 Date: Wed, 4 Mar 2020 18:38:30 +0200 Subject: (Moderation Utils Tests): Created tests for `notify_pardon` function. --- tests/bot/cogs/moderation/test_utils.py | 41 +++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index 89f853262..05e71e695 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock from discord import Embed -from bot.cogs.moderation.utils import has_active_infraction, notify_infraction +from bot.cogs.moderation.utils import has_active_infraction, notify_infraction, notify_pardon from bot.constants import Colours, Icons from tests.helpers import MockBot, MockContext, MockMember, MockUser @@ -21,6 +21,8 @@ INFRACTION_DESCRIPTION_TEMPLATE = ( "**Reason:** {reason}\n" ) +PARDON_COLOR = Colours.soft_green + class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): """Tests Moderation utils.""" @@ -112,7 +114,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): args = case["args"] expected = case["expected_output"] - with self.subTest(args=case["args"], expected=case["expected_output"]): + with self.subTest(args=args, expected=expected): await notify_infraction(*args) embed: Embed = self.user.send.call_args[1]["embed"] @@ -125,3 +127,38 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): self.assertEqual(embed.author.icon_url, expected["icon_url"]) self.assertEqual(embed.footer.text, expected["footer"]) self.assertEqual(embed.description, expected["description"]) + + async def test_notify_pardon(self): + """Test does `notify_pardon` create correct embed.""" + test_cases = [ + { + "args": (self.user, "Test title", "Example content"), + "expected_output": { + "description": "Example content", + "title": "Test title", + "icon_url": Icons.user_verified + } + }, + { + "args": (self.user, "Test title 1", "Example content 1", Icons.user_update), + "expected_output": { + "description": "Example content 1", + "title": "Test title 1", + "icon_url": Icons.user_update + } + } + ] + + for case in test_cases: + args = case["args"] + expected = case["expected_output"] + + with self.subTest(args=args, expected=expected): + await notify_pardon(*args) + + embed: Embed = self.user.send.call_args[1]["embed"] + + self.assertEqual(embed.description, expected["description"]) + self.assertEqual(embed.colour.value, PARDON_COLOR) + self.assertEqual(embed.author.name, expected["title"]) + self.assertEqual(embed.author.icon_url, expected["icon_url"]) -- cgit v1.2.3 From 615ffaa97cb14d83c7c57e0efd675aae0b58abd1 Mon Sep 17 00:00:00 2001 From: ks123 Date: Wed, 4 Mar 2020 19:10:36 +0200 Subject: (Moderation Utils Tests): Created tests for `post_user` function. --- tests/bot/cogs/moderation/test_utils.py | 60 ++++++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index 05e71e695..c8c1f9e1a 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -3,7 +3,8 @@ from unittest.mock import AsyncMock from discord import Embed -from bot.cogs.moderation.utils import has_active_infraction, notify_infraction, notify_pardon +from bot.api import ResponseCodeError +from bot.cogs.moderation.utils import has_active_infraction, notify_infraction, notify_pardon, post_user from bot.constants import Colours, Icons from tests.helpers import MockBot, MockContext, MockMember, MockUser @@ -162,3 +163,60 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): self.assertEqual(embed.colour.value, PARDON_COLOR) self.assertEqual(embed.author.name, expected["title"]) self.assertEqual(embed.author.icon_url, expected["icon_url"]) + + async def test_post_user(self): + """Test does `post_user` work correctly.""" + test_cases = [ + { + "args": (self.ctx, self.user), + "post_result": [ + { + "id": 1234, + "avatar": "test", + "name": "Test", + "discriminator": 1234, + "roles": [ + 1234, + 5678 + ], + "in_guild": True + } + ], + "raise_error": False + }, + { + "args": (self.ctx, self.user), + "post_result": [ + { + "id": 1234, + "avatar": "test", + "name": "Test", + "discriminator": 1234, + "roles": [ + 1234, + 5678 + ], + "in_guild": True + } + ], + "raise_error": True + } + ] + + for case in test_cases: + args = case["args"] + expected = case["post_result"] + error = case["raise_error"] + + with self.subTest(args=args, result=expected, error=error): + self.ctx.bot.api_client.post.return_value = expected + + if error: + self.ctx.bot.api_client.post.side_effect = ResponseCodeError(AsyncMock(response_code=400), expected) + + result = await post_user(*args) + + if error: + self.assertIsNone(result) + else: + self.assertEqual(result, expected) -- cgit v1.2.3 From af71a7775d190a11ed92c0d88b52801cdf3804d8 Mon Sep 17 00:00:00 2001 From: ks123 Date: Wed, 4 Mar 2020 19:26:40 +0200 Subject: (Moderation Utils Tests): Created tests for `send_private_embed` function + Fixed errors. --- tests/bot/cogs/moderation/test_utils.py | 47 ++++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index c8c1f9e1a..c1cc11724 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -1,10 +1,13 @@ import unittest +from typing import Union from unittest.mock import AsyncMock -from discord import Embed +from discord import Embed, Forbidden, HTTPException, NotFound from bot.api import ResponseCodeError -from bot.cogs.moderation.utils import has_active_infraction, notify_infraction, notify_pardon, post_user +from bot.cogs.moderation.utils import ( + has_active_infraction, notify_infraction, notify_pardon, post_user, send_private_embed +) from bot.constants import Colours, Icons from tests.helpers import MockBot, MockContext, MockMember, MockUser @@ -212,7 +215,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): self.ctx.bot.api_client.post.return_value = expected if error: - self.ctx.bot.api_client.post.side_effect = ResponseCodeError(AsyncMock(response_code=400), expected) + self.ctx.bot.api_client.post.side_effect = ResponseCodeError(AsyncMock(), expected) result = await post_user(*args) @@ -220,3 +223,41 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): self.assertIsNone(result) else: self.assertEqual(result, expected) + + async def test_send_private_embed(self): + """Test does `send_private_embed` return correct value.""" + test_cases = [ + { + "args": (self.user, Embed(title="Test", description="Test val")), + "expected_output": True, + "raised_exception": None + }, + { + "args": (self.user, Embed(title="Test", description="Test val")), + "expected_output": False, + "raised_exception": HTTPException + }, + { + "args": (self.user, Embed(title="Test", description="Test val")), + "expected_output": False, + "raised_exception": Forbidden + }, + { + "args": (self.user, Embed(title="Test", description="Test val")), + "expected_output": False, + "raised_exception": NotFound + } + ] + + for case in test_cases: + args = case["args"] + expected = case["expected_output"] + raised: Union[Forbidden, HTTPException, NotFound, None] = case["raised_exception"] + + with self.subTest(args=args, expected=expected, raised=raised): + if raised: + self.user.send.side_effect = raised(AsyncMock(), AsyncMock()) + + result = await send_private_embed(*args) + + self.assertEqual(result, expected) -- cgit v1.2.3 From 1c7675ba55342e29fa3e3b82cf36a6e321f76bf8 Mon Sep 17 00:00:00 2001 From: ks123 Date: Thu, 5 Mar 2020 08:31:14 +0200 Subject: (Moderation Utils Tests): Created tests for `post_infraction` function, created __init__.py for moderation tests --- tests/bot/cogs/moderation/__init__.py | 0 tests/bot/cogs/moderation/test_utils.py | 69 ++++++++++++++++++++++++++++++++- 2 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 tests/bot/cogs/moderation/__init__.py diff --git a/tests/bot/cogs/moderation/__init__.py b/tests/bot/cogs/moderation/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index c1cc11724..984a8aa41 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -1,4 +1,5 @@ import unittest +from datetime import datetime from typing import Union from unittest.mock import AsyncMock @@ -6,7 +7,7 @@ from discord import Embed, Forbidden, HTTPException, NotFound from bot.api import ResponseCodeError from bot.cogs.moderation.utils import ( - has_active_infraction, notify_infraction, notify_pardon, post_user, send_private_embed + has_active_infraction, notify_infraction, notify_pardon, post_infraction, post_user, send_private_embed ) from bot.constants import Colours, Icons from tests.helpers import MockBot, MockContext, MockMember, MockUser @@ -261,3 +262,69 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): result = await send_private_embed(*args) self.assertEqual(result, expected) + + async def test_post_infraction(self): + """Test does `post_infraction` return correct value.""" + test_cases = [ + { + "args": (self.ctx, self.member, "ban", "Test Ban"), + "expected_output": [ + { + "id": 1, + "inserted_at": "2018-11-22T07:24:06.132307Z", + "expires_at": "5018-11-20T15:52:00Z", + "active": True, + "user": 1234, + "actor": 1234, + "type": "ban", + "reason": "Test Ban", + "hidden": False + } + ], + "raised_error": None + }, + { + "args": (self.ctx, self.member, "note", "Test Ban"), + "expected_output": None, + "raised_error": ResponseCodeError(AsyncMock(), AsyncMock()) + }, + { + "args": (self.ctx, self.member, "mute", "Test Ban"), + "expected_output": None, + "raised_error": ResponseCodeError(AsyncMock(), {'user': 1234}) + }, + { + "args": (self.ctx, self.member, "ban", "Test Ban", datetime.now()), + "expected_output": [ + { + "id": 1, + "inserted_at": "2018-11-22T07:24:06.132307Z", + "expires_at": "5018-11-20T15:52:00Z", + "active": True, + "user": 1234, + "actor": 1234, + "type": "ban", + "reason": "Test Ban", + "hidden": False + } + ], + "raised_error": None + }, + ] + + for case in test_cases: + args = case["args"] + expected = case["expected_output"] + raised = case["raised_error"] + + with self.subTest(args=args, expected=expected, raised=raised): + if raised: + self.ctx.bot.api_client.post.side_effect = raised + + self.ctx.bot.api_client.post.return_value = expected + + result = await post_infraction(*args) + + self.assertEqual(result, expected) + + self.ctx.bot.api_client.post.reset_mock(side_effect=True) -- cgit v1.2.3 From 3b3b9f72807fe4c2dfaedb98aa714150b01d46ba Mon Sep 17 00:00:00 2001 From: ks123 Date: Thu, 5 Mar 2020 08:34:01 +0200 Subject: (Moderation Utils Tests): `send_private_embed` moved exception creating from cases testing to test cases listing, added side_effect resetting. --- tests/bot/cogs/moderation/test_utils.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index 984a8aa41..2a07cdc6b 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -236,17 +236,17 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): { "args": (self.user, Embed(title="Test", description="Test val")), "expected_output": False, - "raised_exception": HTTPException + "raised_exception": HTTPException(AsyncMock(), AsyncMock()) }, { "args": (self.user, Embed(title="Test", description="Test val")), "expected_output": False, - "raised_exception": Forbidden + "raised_exception": Forbidden(AsyncMock(), AsyncMock()) }, { "args": (self.user, Embed(title="Test", description="Test val")), "expected_output": False, - "raised_exception": NotFound + "raised_exception": NotFound(AsyncMock(), AsyncMock()) } ] @@ -257,12 +257,14 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): with self.subTest(args=args, expected=expected, raised=raised): if raised: - self.user.send.side_effect = raised(AsyncMock(), AsyncMock()) + self.user.send.side_effect = raised result = await send_private_embed(*args) self.assertEqual(result, expected) + self.user.send.reset_mock(side_effect=True) + async def test_post_infraction(self): """Test does `post_infraction` return correct value.""" test_cases = [ -- cgit v1.2.3 From 30e090be63c96b5844087c979a37a321ccd170df Mon Sep 17 00:00:00 2001 From: ks123 Date: Thu, 5 Mar 2020 08:43:18 +0200 Subject: (Moderation Utils Tests): Moved `has_active_infraction` tests to one test. --- tests/bot/cogs/moderation/test_utils.py | 57 ++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index 2a07cdc6b..18794136c 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -39,28 +39,41 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): self.ctx = MockContext(bot=self.bot, author=self.member) self.bot.api_client.get = AsyncMock() - async def test_user_has_active_infraction_true(self): - """Test does `has_active_infraction` return that user have active infraction.""" - self.bot.api_client.get.return_value = [{ - "id": 1, - "inserted_at": "2018-11-22T07:24:06.132307Z", - "expires_at": "5018-11-20T15:52:00Z", - "active": True, - "user": 1234, - "actor": 1234, - "type": "ban", - "reason": "Test", - "hidden": False - }] - self.assertTrue(await has_active_infraction(self.ctx, self.member, "ban"), "User should have active infraction") - - async def test_user_has_active_infraction_false(self): - """Test does `has_active_infraction` return that user don't have active infractions.""" - self.bot.api_client.get.return_value = [] - self.assertFalse( - await has_active_infraction(self.ctx, self.member, "ban"), - "User shouldn't have active infraction" - ) + async def test_user_has_active_infraction(self): + """Test does `has_active_infraction` return correct value.""" + test_cases = [ + { + "args": (self.ctx, self.member, "ban"), + "get_return_value": [], + "expected_output": False + }, + { + "args": (self.ctx, self.member, "ban"), + "get_return_value": [{ + "id": 1, + "inserted_at": "2018-11-22T07:24:06.132307Z", + "expires_at": "5018-11-20T15:52:00Z", + "active": True, + "user": 1234, + "actor": 1234, + "type": "ban", + "reason": "Test", + "hidden": False + }], + "expected_output": True + } + ] + + for case in test_cases: + args = case["args"] + return_value = case["get_return_value"] + expected = case["expected_output"] + + with self.subTest(args=args, return_value=return_value, expected=expected): + self.bot.api_client.get.return_value = return_value + + result = await has_active_infraction(*args) + self.assertEqual(result, expected) async def test_notify_infraction(self): """Test does `notify_infraction` create correct embed.""" -- cgit v1.2.3 From 9211baaf987277b115bd1e2092f69f29389ac887 Mon Sep 17 00:00:00 2001 From: ks123 Date: Thu, 5 Mar 2020 15:26:35 +0200 Subject: (Moderation Utils Tests): Removed unnecessary `AsyncMock()` from `__init__` (`self.bot.api_client.get`) --- tests/bot/cogs/moderation/test_utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index 18794136c..60d7efa5e 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -37,7 +37,6 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): self.member = MockMember(id=1234) self.user = MockUser(id=1234) self.ctx = MockContext(bot=self.bot, author=self.member) - self.bot.api_client.get = AsyncMock() async def test_user_has_active_infraction(self): """Test does `has_active_infraction` return correct value.""" -- cgit v1.2.3 From 7d988453fe6536df06f47aeef9b5ff36f5d64c39 Mon Sep 17 00:00:00 2001 From: ks123 Date: Thu, 5 Mar 2020 15:32:25 +0200 Subject: (Moderation Utils Tests): Use `bot.cogs.moderation.utils`'s `RULES_URL` instead creating new one --- tests/bot/cogs/moderation/test_utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index 60d7efa5e..ea5aadc59 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -7,12 +7,11 @@ from discord import Embed, Forbidden, HTTPException, NotFound from bot.api import ResponseCodeError from bot.cogs.moderation.utils import ( - has_active_infraction, notify_infraction, notify_pardon, post_infraction, post_user, send_private_embed + RULES_URL, has_active_infraction, notify_infraction, notify_pardon, post_infraction, post_user, send_private_embed ) from bot.constants import Colours, Icons from tests.helpers import MockBot, MockContext, MockMember, MockUser -RULES_URL = "https://pythondiscord.com/pages/rules" APPEAL_EMAIL = "appeals@pythondiscord.com" INFRACTION_TITLE = f"Please review our rules over at {RULES_URL}" -- cgit v1.2.3 From b0ae911f3ecb4c5229c6944f5fed77eced2fc79b Mon Sep 17 00:00:00 2001 From: ks123 Date: Thu, 5 Mar 2020 15:52:43 +0200 Subject: (Moderation Utils Tests): Added following new assertions to `has_active_infraction` tests: `ctx.send` and `bot.api_client.get` calling. --- tests/bot/cogs/moderation/test_utils.py | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index ea5aadc59..40159f6d9 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -43,7 +43,13 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): { "args": (self.ctx, self.member, "ban"), "get_return_value": [], - "expected_output": False + "expected_output": False, + "get_call": { + "active": "true", + "type": "ban", + "user__id": str(self.member.id) + }, + "send_params": None }, { "args": (self.ctx, self.member, "ban"), @@ -58,7 +64,16 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): "reason": "Test", "hidden": False }], - "expected_output": True + "expected_output": True, + "get_call": { + "active": "true", + "type": "ban", + "user__id": str(self.member.id) + }, + "send_params": ( + f":x: According to my records, this user already has a ban infraction. " + f"See infraction **#1**." + ) } ] @@ -66,12 +81,21 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): args = case["args"] return_value = case["get_return_value"] expected = case["expected_output"] + get = case["get_call"] + send_vals = case["send_params"] - with self.subTest(args=args, return_value=return_value, expected=expected): + with self.subTest(args=args, return_value=return_value, expected=expected, get=get, send_vals=send_vals): self.bot.api_client.get.return_value = return_value result = await has_active_infraction(*args) self.assertEqual(result, expected) + self.bot.api_client.get.assert_awaited_once_with("bot/infractions", params=get) + + if result: + self.ctx.send.assert_awaited_once_with(send_vals) + + self.bot.api_client.get.reset_mock() + self.ctx.send.reset_mock() async def test_notify_infraction(self): """Test does `notify_infraction` create correct embed.""" -- cgit v1.2.3 From 05c3a21f34b5cc87ac5e439b0256fffc10682f54 Mon Sep 17 00:00:00 2001 From: ks123 Date: Thu, 5 Mar 2020 16:39:36 +0200 Subject: (Moderation Utils Tests): Added new assertions to `post_infraction`, added `ctx.send` raising errors, added check for return values and `send_private_embed` call. --- tests/bot/cogs/moderation/test_utils.py | 44 ++++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index 40159f6d9..609ec2642 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -1,7 +1,7 @@ import unittest from datetime import datetime from typing import Union -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, patch from discord import Embed, Forbidden, HTTPException, NotFound @@ -97,8 +97,9 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): self.bot.api_client.get.reset_mock() self.ctx.send.reset_mock() - async def test_notify_infraction(self): - """Test does `notify_infraction` create correct embed.""" + @patch("bot.cogs.moderation.utils.send_private_embed") + async def test_notify_infraction(self, send_private_embed_mock): + """Test does `notify_infraction` create correct result.""" test_cases = [ { "args": (self.user, "ban", "2020-02-26 09:20 (23 hours and 59 minutes)"), @@ -109,8 +110,10 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): "reason": "No reason provided." }), "icon_url": Icons.token_removed, - "footer": INFRACTION_APPEAL_FOOTER - } + "footer": INFRACTION_APPEAL_FOOTER, + }, + "send_result": True, + "send_raise": None }, { "args": (self.user, "warning", None, "Test reason."), @@ -122,7 +125,9 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): }), "icon_url": Icons.token_removed, "footer": Embed.Empty - } + }, + "send_result": False, + "send_raise": Forbidden(AsyncMock(), AsyncMock()) }, { "args": (self.user, "note", None, None, Icons.defcon_denied), @@ -134,7 +139,9 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): }), "icon_url": Icons.defcon_denied, "footer": Embed.Empty - } + }, + "send_result": False, + "send_raise": NotFound(AsyncMock(), AsyncMock()) }, { "args": (self.user, "mute", "2020-02-26 09:20 (23 hours and 59 minutes)", "Test", Icons.defcon_denied), @@ -146,18 +153,28 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): }), "icon_url": Icons.defcon_denied, "footer": INFRACTION_APPEAL_FOOTER - } + }, + "send_result": False, + "send_raise": HTTPException(AsyncMock(), AsyncMock()) } ] for case in test_cases: args = case["args"] expected = case["expected_output"] + send, send_raise = case["send_result"], case["send_raise"] - with self.subTest(args=args, expected=expected): - await notify_infraction(*args) + with self.subTest(args=args, expected=expected, send=send, send_raise=send_raise): + if send_raise: + self.ctx.send.side_effect = send_raise - embed: Embed = self.user.send.call_args[1]["embed"] + send_private_embed_mock.return_value = send + + result = await notify_infraction(*args) + + self.assertEqual(send, result) + + embed = send_private_embed_mock.call_args[0][1] self.assertEqual(embed.title, INFRACTION_TITLE) self.assertEqual(embed.colour.value, INFRACTION_COLOR) @@ -168,6 +185,11 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): self.assertEqual(embed.footer.text, expected["footer"]) self.assertEqual(embed.description, expected["description"]) + send_private_embed_mock.assert_awaited_once_with(args[0], embed) + + self.ctx.send.reset_mock(side_effect=True) + send_private_embed_mock.reset_mock() + async def test_notify_pardon(self): """Test does `notify_pardon` create correct embed.""" test_cases = [ -- cgit v1.2.3 From 87e5bdb3ff8f591e05e5eb410bbc5139afcc8d23 Mon Sep 17 00:00:00 2001 From: ks123 Date: Thu, 5 Mar 2020 16:46:15 +0200 Subject: (Moderation Utils Tests): Added new assertions to `notify_pardon`, added `ctx.send` raising errors, added check for return values and `send_private_embed` call. --- tests/bot/cogs/moderation/test_utils.py | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index 609ec2642..f38f4557b 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -190,8 +190,9 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): self.ctx.send.reset_mock(side_effect=True) send_private_embed_mock.reset_mock() - async def test_notify_pardon(self): - """Test does `notify_pardon` create correct embed.""" + @patch("bot.cogs.moderation.utils.send_private_embed") + async def test_notify_pardon(self, send_private_embed_mock): + """Test does `notify_pardon` create correct result.""" test_cases = [ { "args": (self.user, "Test title", "Example content"), @@ -199,7 +200,9 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): "description": "Example content", "title": "Test title", "icon_url": Icons.user_verified - } + }, + "send_result": True, + "send_raise": None }, { "args": (self.user, "Test title 1", "Example content 1", Icons.user_update), @@ -207,24 +210,39 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): "description": "Example content 1", "title": "Test title 1", "icon_url": Icons.user_update - } + }, + "send_result": False, + "send_raise": NotFound(AsyncMock(), AsyncMock()) } ] for case in test_cases: args = case["args"] expected = case["expected_output"] + send, send_raise = case["send_result"], case["send_raise"] with self.subTest(args=args, expected=expected): - await notify_pardon(*args) + if send_raise: + self.ctx.send.side_effect = send_raise - embed: Embed = self.user.send.call_args[1]["embed"] + send_private_embed_mock.return_value = send + + result = await notify_pardon(*args) + + self.assertEqual(send, result) + + embed = send_private_embed_mock.call_args[0][1] self.assertEqual(embed.description, expected["description"]) self.assertEqual(embed.colour.value, PARDON_COLOR) self.assertEqual(embed.author.name, expected["title"]) self.assertEqual(embed.author.icon_url, expected["icon_url"]) + send_private_embed_mock.assert_awaited_once_with(args[0], embed) + + self.ctx.send.reset_mock(side_effect=True) + send_private_embed_mock.reset_mock() + async def test_post_user(self): """Test does `post_user` work correctly.""" test_cases = [ -- cgit v1.2.3 From ded64749940525ea9b1f613560e4e30ec74c0c01 Mon Sep 17 00:00:00 2001 From: ks123 Date: Thu, 5 Mar 2020 16:54:00 +0200 Subject: (Moderation Utils Tests): Added API POST call assertion to `test_post_user`. --- tests/bot/cogs/moderation/test_utils.py | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index f38f4557b..847ba8465 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -258,10 +258,18 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): 1234, 5678 ], - "in_guild": True + "in_guild": False } ], - "raise_error": False + "raise_error": False, + "payload": { + "avatar_hash": getattr(self.user, "avatar", 0), + "discriminator": int(getattr(self.user, "discriminator", 0)), + "id": self.user.id, + "in_guild": False, + "name": getattr(self.user, "name", "Name unknown"), + "roles": [] + } }, { "args": (self.ctx, self.user), @@ -275,10 +283,18 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): 1234, 5678 ], - "in_guild": True + "in_guild": False } ], - "raise_error": True + "raise_error": True, + "payload": { + "avatar_hash": getattr(self.user, "avatar", 0), + "discriminator": int(getattr(self.user, "discriminator", 0)), + "id": self.user.id, + "in_guild": False, + "name": getattr(self.user, "name", "Name unknown"), + "roles": [] + } } ] @@ -286,8 +302,9 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): args = case["args"] expected = case["post_result"] error = case["raise_error"] + payload = case["payload"] - with self.subTest(args=args, result=expected, error=error): + with self.subTest(args=args, result=expected, error=error, payload=payload): self.ctx.bot.api_client.post.return_value = expected if error: @@ -300,6 +317,8 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): else: self.assertEqual(result, expected) + self.bot.api_client.post.assert_awaited_once_with("bot/users", json=payload) + async def test_send_private_embed(self): """Test does `send_private_embed` return correct value.""" test_cases = [ -- cgit v1.2.3 From d8a00abd3860df68dab1805f213f6467085d78fd Mon Sep 17 00:00:00 2001 From: ks123 Date: Thu, 5 Mar 2020 16:57:02 +0200 Subject: (Moderation Utils Tests): Added `user.send` call assertion to `test_send_private_embed`. --- tests/bot/cogs/moderation/test_utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index 847ba8465..300f0b80d 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -356,6 +356,8 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): result = await send_private_embed(*args) self.assertEqual(result, expected) + if expected: + args[0].send.assert_awaited_once_with(embed=args[1]) self.user.send.reset_mock(side_effect=True) -- cgit v1.2.3 From c1b97d0d6132175910ca8e66d35e908444ef512f Mon Sep 17 00:00:00 2001 From: ks123 Date: Thu, 5 Mar 2020 19:54:44 +0200 Subject: (Moderation Utils Tests): Added additional assertions to `post_infraction` test. --- tests/bot/cogs/moderation/test_utils.py | 62 ++++++++++++++++++++++++++++----- 1 file changed, 54 insertions(+), 8 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index 300f0b80d..c5b8f380f 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -361,8 +361,10 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): self.user.send.reset_mock(side_effect=True) - async def test_post_infraction(self): + @patch("bot.cogs.moderation.utils.post_user") + async def test_post_infraction(self, post_user_mock): """Test does `post_infraction` return correct value.""" + now = datetime.now() test_cases = [ { "args": (self.ctx, self.member, "ban", "Test Ban"), @@ -379,20 +381,44 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): "hidden": False } ], - "raised_error": None + "raised_error": None, + "payload": { + "actor": self.ctx.message.author.id, + "hidden": False, + "reason": "Test Ban", + "type": "ban", + "user": self.member.id, + "active": True + } }, { "args": (self.ctx, self.member, "note", "Test Ban"), "expected_output": None, - "raised_error": ResponseCodeError(AsyncMock(), AsyncMock()) + "raised_error": ResponseCodeError(AsyncMock(), AsyncMock()), + "payload": { + "actor": self.ctx.message.author.id, + "hidden": False, + "reason": "Test Ban", + "type": "note", + "user": self.member.id, + "active": True + } }, { "args": (self.ctx, self.member, "mute", "Test Ban"), "expected_output": None, - "raised_error": ResponseCodeError(AsyncMock(), {'user': 1234}) + "raised_error": ResponseCodeError(AsyncMock(status=400), {'user': 1234}), + "payload": { + "actor": self.ctx.message.author.id, + "hidden": False, + "reason": "Test Ban", + "type": "mute", + "user": self.member.id, + "active": True + } }, { - "args": (self.ctx, self.member, "ban", "Test Ban", datetime.now()), + "args": (self.ctx, self.member, "ban", "Test Ban", now, True, False), "expected_output": [ { "id": 1, @@ -406,7 +432,16 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): "hidden": False } ], - "raised_error": None + "raised_error": None, + "payload": { + "actor": self.ctx.message.author.id, + "hidden": True, + "reason": "Test Ban", + "type": "ban", + "user": self.member.id, + "active": False, + "expires_at": now.isoformat() + } }, ] @@ -414,15 +449,26 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): args = case["args"] expected = case["expected_output"] raised = case["raised_error"] + payload = case["payload"] + + with self.subTest(args=args, expected=expected, raised=raised, payload=payload): + self.ctx.bot.api_client.post.reset_mock(side_effect=True) + post_user_mock.reset_mock() - with self.subTest(args=args, expected=expected, raised=raised): if raised: self.ctx.bot.api_client.post.side_effect = raised + post_user_mock.return_value = "foo" + self.ctx.bot.api_client.post.return_value = expected result = await post_infraction(*args) self.assertEqual(result, expected) - self.ctx.bot.api_client.post.reset_mock(side_effect=True) + if not raised: + self.bot.api_client.post.assert_awaited_once_with("bot/infractions", json=payload) + + if hasattr(raised, "status") and hasattr(raised, "response_json"): + if raised.status == 400 and "user" in raised.response_json: + post_user_mock.assert_awaited_once_with(args[0], args[1]) -- cgit v1.2.3 From 7dfac36ab5d513fada631e6d473915e05eafe778 Mon Sep 17 00:00:00 2001 From: ks123 Date: Thu, 5 Mar 2020 20:01:34 +0200 Subject: (Moderation Utils Tests): Fixed errors, added checks before assertions for errors --- tests/bot/cogs/moderation/test_utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index c5b8f380f..7f94f20e8 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -317,7 +317,10 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): else: self.assertEqual(result, expected) - self.bot.api_client.post.assert_awaited_once_with("bot/users", json=payload) + if not error: + self.bot.api_client.post.assert_awaited_once_with("bot/users", json=payload) + + self.bot.api_client.post.reset_mock(side_effect=True) async def test_send_private_embed(self): """Test does `send_private_embed` return correct value.""" -- cgit v1.2.3 From 1e0170481624d4a5ec52058cd4a57dd461439fd4 Mon Sep 17 00:00:00 2001 From: ks123 Date: Sun, 8 Mar 2020 08:02:21 +0200 Subject: (Moderation Utils Tests): Fixed docstrings, added more information to these. --- tests/bot/cogs/moderation/test_utils.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index 7f94f20e8..e2345ea37 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -38,7 +38,9 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): self.ctx = MockContext(bot=self.bot, author=self.member) async def test_user_has_active_infraction(self): - """Test does `has_active_infraction` return correct value.""" + """ + Test does `has_active_infraction` return call at least once `ctx.send` API get, check does return correct bool. + """ test_cases = [ { "args": (self.ctx, self.member, "ban"), @@ -99,7 +101,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): @patch("bot.cogs.moderation.utils.send_private_embed") async def test_notify_infraction(self, send_private_embed_mock): - """Test does `notify_infraction` create correct result.""" + """Test does `notify_infraction` create correct embed and return correct boolean.""" test_cases = [ { "args": (self.user, "ban", "2020-02-26 09:20 (23 hours and 59 minutes)"), @@ -192,7 +194,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): @patch("bot.cogs.moderation.utils.send_private_embed") async def test_notify_pardon(self, send_private_embed_mock): - """Test does `notify_pardon` create correct result.""" + """Test does `notify_pardon` create correct embed and return correct bool.""" test_cases = [ { "args": (self.user, "Test title", "Example content"), @@ -244,7 +246,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): send_private_embed_mock.reset_mock() async def test_post_user(self): - """Test does `post_user` work correctly.""" + """Test does `post_user` handle errors and results correctly.""" test_cases = [ { "args": (self.ctx, self.user), @@ -323,7 +325,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): self.bot.api_client.post.reset_mock(side_effect=True) async def test_send_private_embed(self): - """Test does `send_private_embed` return correct value.""" + """Test does `send_private_embed` return correct bool.""" test_cases = [ { "args": (self.user, Embed(title="Test", description="Test val")), @@ -366,7 +368,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): @patch("bot.cogs.moderation.utils.post_user") async def test_post_infraction(self, post_user_mock): - """Test does `post_infraction` return correct value.""" + """Test does `post_infraction` call functions correctly and return `None` or `Dict`.""" now = datetime.now() test_cases = [ { -- cgit v1.2.3 From 87ecf72a328b05c922d1f7c0d6e8a1c86ab405c8 Mon Sep 17 00:00:00 2001 From: ks123 Date: Sun, 8 Mar 2020 08:07:04 +0200 Subject: (Moderation Utils Tests): Removed large `utils` parts import, use import `utils` instead and added `utils` before variables and function that was imported directly before. --- tests/bot/cogs/moderation/test_utils.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index e2345ea37..6722c2d16 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -6,15 +6,13 @@ from unittest.mock import AsyncMock, patch from discord import Embed, Forbidden, HTTPException, NotFound from bot.api import ResponseCodeError -from bot.cogs.moderation.utils import ( - RULES_URL, has_active_infraction, notify_infraction, notify_pardon, post_infraction, post_user, send_private_embed -) +from bot.cogs.moderation import utils from bot.constants import Colours, Icons from tests.helpers import MockBot, MockContext, MockMember, MockUser APPEAL_EMAIL = "appeals@pythondiscord.com" -INFRACTION_TITLE = f"Please review our rules over at {RULES_URL}" +INFRACTION_TITLE = f"Please review our rules over at {utils.RULES_URL}" INFRACTION_APPEAL_FOOTER = f"To appeal this infraction, send an e-mail to {APPEAL_EMAIL}" INFRACTION_AUTHOR_NAME = "Infraction information" INFRACTION_COLOR = Colours.soft_red @@ -89,7 +87,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): with self.subTest(args=args, return_value=return_value, expected=expected, get=get, send_vals=send_vals): self.bot.api_client.get.return_value = return_value - result = await has_active_infraction(*args) + result = await utils.has_active_infraction(*args) self.assertEqual(result, expected) self.bot.api_client.get.assert_awaited_once_with("bot/infractions", params=get) @@ -172,7 +170,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): send_private_embed_mock.return_value = send - result = await notify_infraction(*args) + result = await utils.notify_infraction(*args) self.assertEqual(send, result) @@ -180,9 +178,9 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): self.assertEqual(embed.title, INFRACTION_TITLE) self.assertEqual(embed.colour.value, INFRACTION_COLOR) - self.assertEqual(embed.url, RULES_URL) + self.assertEqual(embed.url, utils.RULES_URL) self.assertEqual(embed.author.name, INFRACTION_AUTHOR_NAME) - self.assertEqual(embed.author.url, RULES_URL) + self.assertEqual(embed.author.url, utils.RULES_URL) self.assertEqual(embed.author.icon_url, expected["icon_url"]) self.assertEqual(embed.footer.text, expected["footer"]) self.assertEqual(embed.description, expected["description"]) @@ -229,7 +227,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): send_private_embed_mock.return_value = send - result = await notify_pardon(*args) + result = await utils.notify_pardon(*args) self.assertEqual(send, result) @@ -312,7 +310,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): if error: self.ctx.bot.api_client.post.side_effect = ResponseCodeError(AsyncMock(), expected) - result = await post_user(*args) + result = await utils.post_user(*args) if error: self.assertIsNone(result) @@ -358,7 +356,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): if raised: self.user.send.side_effect = raised - result = await send_private_embed(*args) + result = await utils.send_private_embed(*args) self.assertEqual(result, expected) if expected: @@ -467,7 +465,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): self.ctx.bot.api_client.post.return_value = expected - result = await post_infraction(*args) + result = await utils.post_infraction(*args) self.assertEqual(result, expected) -- cgit v1.2.3 From 2faa982722f2e9ed9a0710e0030a6078ecab421a Mon Sep 17 00:00:00 2001 From: ks123 Date: Sun, 8 Mar 2020 08:10:29 +0200 Subject: (Moderation Utils Tests): Hard-coded API get request params for `has_active_infraction` test. --- tests/bot/cogs/moderation/test_utils.py | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index 6722c2d16..56bf6d67e 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -44,11 +44,6 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): "args": (self.ctx, self.member, "ban"), "get_return_value": [], "expected_output": False, - "get_call": { - "active": "true", - "type": "ban", - "user__id": str(self.member.id) - }, "send_params": None }, { @@ -65,11 +60,6 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): "hidden": False }], "expected_output": True, - "get_call": { - "active": "true", - "type": "ban", - "user__id": str(self.member.id) - }, "send_params": ( f":x: According to my records, this user already has a ban infraction. " f"See infraction **#1**." @@ -81,15 +71,18 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): args = case["args"] return_value = case["get_return_value"] expected = case["expected_output"] - get = case["get_call"] send_vals = case["send_params"] - with self.subTest(args=args, return_value=return_value, expected=expected, get=get, send_vals=send_vals): + with self.subTest(args=args, return_value=return_value, expected=expected, send_vals=send_vals): self.bot.api_client.get.return_value = return_value result = await utils.has_active_infraction(*args) self.assertEqual(result, expected) - self.bot.api_client.get.assert_awaited_once_with("bot/infractions", params=get) + self.bot.api_client.get.assert_awaited_once_with("bot/infractions", params={ + "active": "true", + "type": "ban", + "user__id": str(self.member.id) + }) if result: self.ctx.send.assert_awaited_once_with(send_vals) -- cgit v1.2.3 From 94d3b1303ca55039e19a65043da3abe1ef09280b Mon Sep 17 00:00:00 2001 From: ks123 Date: Sun, 8 Mar 2020 08:29:01 +0200 Subject: (Moderation Utils Tests): Cleaned up `has_active_infraction` test cases, hard-coded args, moved mocks resetting to beginning of subtest, added `ctx.send` check only is infraction nr and type in sent string. --- tests/bot/cogs/moderation/test_utils.py | 41 +++++++++------------------------ 1 file changed, 11 insertions(+), 30 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index 56bf6d67e..5868da61f 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -41,43 +41,26 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): """ test_cases = [ { - "args": (self.ctx, self.member, "ban"), "get_return_value": [], "expected_output": False, - "send_params": None + "infraction_nr": None }, { - "args": (self.ctx, self.member, "ban"), - "get_return_value": [{ - "id": 1, - "inserted_at": "2018-11-22T07:24:06.132307Z", - "expires_at": "5018-11-20T15:52:00Z", - "active": True, - "user": 1234, - "actor": 1234, - "type": "ban", - "reason": "Test", - "hidden": False - }], + "get_return_value": [{"id": 1}], "expected_output": True, - "send_params": ( - f":x: According to my records, this user already has a ban infraction. " - f"See infraction **#1**." - ) + "infraction_nr": "**#1**" } ] for case in test_cases: - args = case["args"] - return_value = case["get_return_value"] - expected = case["expected_output"] - send_vals = case["send_params"] + with self.subTest(return_value=case["get_return_value"], expected=case["expected_output"]): + self.bot.api_client.get.reset_mock() + self.ctx.send.reset_mock() - with self.subTest(args=args, return_value=return_value, expected=expected, send_vals=send_vals): - self.bot.api_client.get.return_value = return_value + self.bot.api_client.get.return_value = case["get_return_value"] - result = await utils.has_active_infraction(*args) - self.assertEqual(result, expected) + result = await utils.has_active_infraction(self.ctx, self.member, "ban") + self.assertEqual(result, case["expected_output"]) self.bot.api_client.get.assert_awaited_once_with("bot/infractions", params={ "active": "true", "type": "ban", @@ -85,10 +68,8 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): }) if result: - self.ctx.send.assert_awaited_once_with(send_vals) - - self.bot.api_client.get.reset_mock() - self.ctx.send.reset_mock() + self.assertTrue(case["infraction_nr"] in self.ctx.send.call_args[0][0]) + self.assertTrue("ban" in self.ctx.send.call_args[0][0]) @patch("bot.cogs.moderation.utils.send_private_embed") async def test_notify_infraction(self, send_private_embed_mock): -- cgit v1.2.3 From f4bb6849f8f345ff99f6295e707aa0712af070a7 Mon Sep 17 00:00:00 2001 From: ks123 Date: Sun, 8 Mar 2020 08:36:42 +0200 Subject: (Moderation Utils Tests): Removed `Dict` unpacking in `notify_infraction` test. --- tests/bot/cogs/moderation/test_utils.py | 40 ++++++++++++++++----------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index 5868da61f..d6e300c89 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -78,11 +78,11 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): { "args": (self.user, "ban", "2020-02-26 09:20 (23 hours and 59 minutes)"), "expected_output": { - "description": INFRACTION_DESCRIPTION_TEMPLATE.format(**{ - "type": "Ban", - "expires": "2020-02-26 09:20 (23 hours and 59 minutes)", - "reason": "No reason provided." - }), + "description": INFRACTION_DESCRIPTION_TEMPLATE.format( + type="Ban", + expires="2020-02-26 09:20 (23 hours and 59 minutes)", + reason="No reason provided." + ), "icon_url": Icons.token_removed, "footer": INFRACTION_APPEAL_FOOTER, }, @@ -92,11 +92,11 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): { "args": (self.user, "warning", None, "Test reason."), "expected_output": { - "description": INFRACTION_DESCRIPTION_TEMPLATE.format(**{ - "type": "Warning", - "expires": "N/A", - "reason": "Test reason." - }), + "description": INFRACTION_DESCRIPTION_TEMPLATE.format( + type="Warning", + expires="N/A", + reason="Test reason." + ), "icon_url": Icons.token_removed, "footer": Embed.Empty }, @@ -106,11 +106,11 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): { "args": (self.user, "note", None, None, Icons.defcon_denied), "expected_output": { - "description": INFRACTION_DESCRIPTION_TEMPLATE.format(**{ - "type": "Note", - "expires": "N/A", - "reason": "No reason provided." - }), + "description": INFRACTION_DESCRIPTION_TEMPLATE.format( + type="Note", + expires="N/A", + reason="No reason provided." + ), "icon_url": Icons.defcon_denied, "footer": Embed.Empty }, @@ -120,11 +120,11 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): { "args": (self.user, "mute", "2020-02-26 09:20 (23 hours and 59 minutes)", "Test", Icons.defcon_denied), "expected_output": { - "description": INFRACTION_DESCRIPTION_TEMPLATE.format(**{ - "type": "Mute", - "expires": "2020-02-26 09:20 (23 hours and 59 minutes)", - "reason": "Test" - }), + "description": INFRACTION_DESCRIPTION_TEMPLATE.format( + type="Mute", + expires="2020-02-26 09:20 (23 hours and 59 minutes)", + reason="Test" + ), "icon_url": Icons.defcon_denied, "footer": INFRACTION_APPEAL_FOOTER }, -- cgit v1.2.3 From 2870472eae2f62982283d160378ca6953231da4e Mon Sep 17 00:00:00 2001 From: ks123 Date: Sun, 8 Mar 2020 08:39:40 +0200 Subject: (Moderation Utils Tests): Removed unnecessary `ctx.send` `side_effect` and removed these in test cases too in `notify_infraction` test. --- tests/bot/cogs/moderation/test_utils.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index d6e300c89..5ab279391 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -86,8 +86,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): "icon_url": Icons.token_removed, "footer": INFRACTION_APPEAL_FOOTER, }, - "send_result": True, - "send_raise": None + "send_result": True }, { "args": (self.user, "warning", None, "Test reason."), @@ -100,8 +99,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): "icon_url": Icons.token_removed, "footer": Embed.Empty }, - "send_result": False, - "send_raise": Forbidden(AsyncMock(), AsyncMock()) + "send_result": False }, { "args": (self.user, "note", None, None, Icons.defcon_denied), @@ -114,8 +112,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): "icon_url": Icons.defcon_denied, "footer": Embed.Empty }, - "send_result": False, - "send_raise": NotFound(AsyncMock(), AsyncMock()) + "send_result": False }, { "args": (self.user, "mute", "2020-02-26 09:20 (23 hours and 59 minutes)", "Test", Icons.defcon_denied), @@ -128,19 +125,16 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): "icon_url": Icons.defcon_denied, "footer": INFRACTION_APPEAL_FOOTER }, - "send_result": False, - "send_raise": HTTPException(AsyncMock(), AsyncMock()) + "send_result": False } ] for case in test_cases: args = case["args"] expected = case["expected_output"] - send, send_raise = case["send_result"], case["send_raise"] + send = case["send_result"] - with self.subTest(args=args, expected=expected, send=send, send_raise=send_raise): - if send_raise: - self.ctx.send.side_effect = send_raise + with self.subTest(args=args, expected=expected, send=send): send_private_embed_mock.return_value = send -- cgit v1.2.3 From f4fff7139ddffa08b12973d69c8f4bd7c47c0224 Mon Sep 17 00:00:00 2001 From: ks123 Date: Sun, 8 Mar 2020 08:41:23 +0200 Subject: (Moderation Utils Tests): Removed unnecessary `ctx.send` mock resetting, moved `send_private_embed` mock reset to beginning of subtest. --- tests/bot/cogs/moderation/test_utils.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index 5ab279391..5637ff508 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -135,9 +135,9 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): send = case["send_result"] with self.subTest(args=args, expected=expected, send=send): + send_private_embed_mock.reset_mock() send_private_embed_mock.return_value = send - result = await utils.notify_infraction(*args) self.assertEqual(send, result) @@ -155,9 +155,6 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): send_private_embed_mock.assert_awaited_once_with(args[0], embed) - self.ctx.send.reset_mock(side_effect=True) - send_private_embed_mock.reset_mock() - @patch("bot.cogs.moderation.utils.send_private_embed") async def test_notify_pardon(self, send_private_embed_mock): """Test does `notify_pardon` create correct embed and return correct bool.""" -- cgit v1.2.3 From 8c638bfa67c5e471089fad199bf2c5d64c0be163 Mon Sep 17 00:00:00 2001 From: ks123 Date: Sun, 8 Mar 2020 08:49:32 +0200 Subject: (Moderation Utils Tests): Moved `notify_infraction` embed check from dict to `Embed`. --- tests/bot/cogs/moderation/test_utils.py | 71 ++++++++++++++++++++------------- 1 file changed, 43 insertions(+), 28 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index 5637ff508..9844c02f9 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -77,54 +77,76 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): test_cases = [ { "args": (self.user, "ban", "2020-02-26 09:20 (23 hours and 59 minutes)"), - "expected_output": { - "description": INFRACTION_DESCRIPTION_TEMPLATE.format( + "expected_output": Embed( + title=INFRACTION_TITLE, + description=INFRACTION_DESCRIPTION_TEMPLATE.format( type="Ban", expires="2020-02-26 09:20 (23 hours and 59 minutes)", reason="No reason provided." ), - "icon_url": Icons.token_removed, - "footer": INFRACTION_APPEAL_FOOTER, - }, + colour=INFRACTION_COLOR, + url=utils.RULES_URL + ).set_author( + name=INFRACTION_AUTHOR_NAME, + url=utils.RULES_URL, + icon_url=Icons.token_removed + ).set_footer(text=INFRACTION_APPEAL_FOOTER), "send_result": True }, { "args": (self.user, "warning", None, "Test reason."), - "expected_output": { - "description": INFRACTION_DESCRIPTION_TEMPLATE.format( + "expected_output": Embed( + title=INFRACTION_TITLE, + description=INFRACTION_DESCRIPTION_TEMPLATE.format( type="Warning", expires="N/A", reason="Test reason." ), - "icon_url": Icons.token_removed, - "footer": Embed.Empty - }, + colour=INFRACTION_COLOR, + url=utils.RULES_URL + ).set_author( + name=INFRACTION_AUTHOR_NAME, + url=utils.RULES_URL, + icon_url=Icons.token_removed + ), "send_result": False }, { "args": (self.user, "note", None, None, Icons.defcon_denied), - "expected_output": { - "description": INFRACTION_DESCRIPTION_TEMPLATE.format( + "expected_output": Embed( + title=INFRACTION_TITLE, + description=INFRACTION_DESCRIPTION_TEMPLATE.format( type="Note", expires="N/A", reason="No reason provided." ), - "icon_url": Icons.defcon_denied, - "footer": Embed.Empty - }, + colour=INFRACTION_COLOR, + url=utils.RULES_URL + ).set_author( + name=INFRACTION_AUTHOR_NAME, + url=utils.RULES_URL, + icon_url=Icons.defcon_denied + ), "send_result": False }, { "args": (self.user, "mute", "2020-02-26 09:20 (23 hours and 59 minutes)", "Test", Icons.defcon_denied), - "expected_output": { - "description": INFRACTION_DESCRIPTION_TEMPLATE.format( + "expected_output": Embed( + title=INFRACTION_TITLE, + description=INFRACTION_DESCRIPTION_TEMPLATE.format( type="Mute", expires="2020-02-26 09:20 (23 hours and 59 minutes)", reason="Test" ), - "icon_url": Icons.defcon_denied, - "footer": INFRACTION_APPEAL_FOOTER - }, + colour=INFRACTION_COLOR, + url=utils.RULES_URL + ).set_author( + name=INFRACTION_AUTHOR_NAME, + url=utils.RULES_URL, + icon_url=Icons.defcon_denied + ).set_footer( + text=INFRACTION_APPEAL_FOOTER + ), "send_result": False } ] @@ -144,14 +166,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): embed = send_private_embed_mock.call_args[0][1] - self.assertEqual(embed.title, INFRACTION_TITLE) - self.assertEqual(embed.colour.value, INFRACTION_COLOR) - self.assertEqual(embed.url, utils.RULES_URL) - self.assertEqual(embed.author.name, INFRACTION_AUTHOR_NAME) - self.assertEqual(embed.author.url, utils.RULES_URL) - self.assertEqual(embed.author.icon_url, expected["icon_url"]) - self.assertEqual(embed.footer.text, expected["footer"]) - self.assertEqual(embed.description, expected["description"]) + self.assertEqual(embed.to_dict(), expected.to_dict()) send_private_embed_mock.assert_awaited_once_with(args[0], embed) -- cgit v1.2.3 From 4260d3cf60f01f0de55a95290dd038d8a5c079ca Mon Sep 17 00:00:00 2001 From: ks123 Date: Sun, 8 Mar 2020 09:01:18 +0200 Subject: (Moderation Utils Tests): Removed unnecessary `ctx.send` `side_effect` from `notify_pardon`, applied changes to test cases. --- tests/bot/cogs/moderation/test_utils.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index 9844c02f9..3616b3cf0 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -181,8 +181,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): "title": "Test title", "icon_url": Icons.user_verified }, - "send_result": True, - "send_raise": None + "send_result": True }, { "args": (self.user, "Test title 1", "Example content 1", Icons.user_update), @@ -191,24 +190,21 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): "title": "Test title 1", "icon_url": Icons.user_update }, - "send_result": False, - "send_raise": NotFound(AsyncMock(), AsyncMock()) + "send_result": False } ] for case in test_cases: args = case["args"] expected = case["expected_output"] - send, send_raise = case["send_result"], case["send_raise"] + send = case["send_result"] with self.subTest(args=args, expected=expected): - if send_raise: - self.ctx.send.side_effect = send_raise + send_private_embed_mock.reset_mock() send_private_embed_mock.return_value = send result = await utils.notify_pardon(*args) - self.assertEqual(send, result) embed = send_private_embed_mock.call_args[0][1] @@ -220,9 +216,6 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): send_private_embed_mock.assert_awaited_once_with(args[0], embed) - self.ctx.send.reset_mock(side_effect=True) - send_private_embed_mock.reset_mock() - async def test_post_user(self): """Test does `post_user` handle errors and results correctly.""" test_cases = [ -- cgit v1.2.3 From b01c2cd813b2df1f8a12e7b493e5085f4a8b9a6e Mon Sep 17 00:00:00 2001 From: ks123 Date: Sun, 8 Mar 2020 09:05:21 +0200 Subject: (Moderation Utils Tests): Moved `expected_output` from `Dict` to `discord.Embed` in `notify_pardon` test. --- tests/bot/cogs/moderation/test_utils.py | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index 3616b3cf0..f8fbee4e2 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -176,20 +176,18 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): test_cases = [ { "args": (self.user, "Test title", "Example content"), - "expected_output": { - "description": "Example content", - "title": "Test title", - "icon_url": Icons.user_verified - }, + "expected_output": Embed( + description="Example content", + colour=PARDON_COLOR + ).set_author(name="Test title", icon_url=Icons.user_verified), "send_result": True }, { "args": (self.user, "Test title 1", "Example content 1", Icons.user_update), - "expected_output": { - "description": "Example content 1", - "title": "Test title 1", - "icon_url": Icons.user_update - }, + "expected_output": Embed( + description="Example content 1", + colour=PARDON_COLOR + ).set_author(name="Test title 1", icon_url=Icons.user_update), "send_result": False } ] @@ -208,11 +206,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): self.assertEqual(send, result) embed = send_private_embed_mock.call_args[0][1] - - self.assertEqual(embed.description, expected["description"]) - self.assertEqual(embed.colour.value, PARDON_COLOR) - self.assertEqual(embed.author.name, expected["title"]) - self.assertEqual(embed.author.icon_url, expected["icon_url"]) + self.assertEqual(embed.to_dict(), expected.to_dict()) send_private_embed_mock.assert_awaited_once_with(args[0], embed) -- cgit v1.2.3 From fc8b796d3c9d88cff959e8d5035bf62a257a7c9c Mon Sep 17 00:00:00 2001 From: ks123 Date: Sun, 8 Mar 2020 10:33:11 +0200 Subject: (Moderation Utils Tests): Added new check to `post_user` test (`ctx.send` content test), improved test cases. --- tests/bot/cogs/moderation/test_utils.py | 51 +++++++++++---------------------- 1 file changed, 16 insertions(+), 35 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index f8fbee4e2..5e9c627bb 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -212,54 +212,31 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): async def test_post_user(self): """Test does `post_user` handle errors and results correctly.""" + user = MockUser(avatar="abc", discriminator=5678, id=1234, name="Test user") test_cases = [ { - "args": (self.ctx, self.user), - "post_result": [ - { - "id": 1234, - "avatar": "test", - "name": "Test", - "discriminator": 1234, - "roles": [ - 1234, - 5678 - ], - "in_guild": False - } - ], + "args": (self.ctx, user), + "post_result": "bar", "raise_error": False, "payload": { - "avatar_hash": getattr(self.user, "avatar", 0), - "discriminator": int(getattr(self.user, "discriminator", 0)), + "avatar_hash": "abc", + "discriminator": 5678, "id": self.user.id, "in_guild": False, - "name": getattr(self.user, "name", "Name unknown"), + "name": "Test user", "roles": [] } }, { - "args": (self.ctx, self.user), - "post_result": [ - { - "id": 1234, - "avatar": "test", - "name": "Test", - "discriminator": 1234, - "roles": [ - 1234, - 5678 - ], - "in_guild": False - } - ], + "args": (self.ctx, self.member), + "post_result": "foo", "raise_error": True, "payload": { - "avatar_hash": getattr(self.user, "avatar", 0), - "discriminator": int(getattr(self.user, "discriminator", 0)), - "id": self.user.id, + "avatar_hash": 0, + "discriminator": 0, + "id": self.member.id, "in_guild": False, - "name": getattr(self.user, "name", "Name unknown"), + "name": "Name unknown", "roles": [] } } @@ -276,6 +253,8 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): if error: self.ctx.bot.api_client.post.side_effect = ResponseCodeError(AsyncMock(), expected) + err = self.ctx.bot.api_client.post.side_effect + err.status = 400 result = await utils.post_user(*args) @@ -286,6 +265,8 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): if not error: self.bot.api_client.post.assert_awaited_once_with("bot/users", json=payload) + else: + self.assertTrue(str(err.status) in self.ctx.send.call_args[0][0]) self.bot.api_client.post.reset_mock(side_effect=True) -- cgit v1.2.3 From 50582f1eeae46d25653eb545455e720df1d4b162 Mon Sep 17 00:00:00 2001 From: ks123 Date: Sun, 8 Mar 2020 10:37:43 +0200 Subject: (Moderation Utils Tests): Hard-coded args for `send_private_embed` test. --- tests/bot/cogs/moderation/test_utils.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index 5e9c627bb..b6bf1a96e 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -1,6 +1,5 @@ import unittest from datetime import datetime -from typing import Union from unittest.mock import AsyncMock, patch from discord import Embed, Forbidden, HTTPException, NotFound @@ -272,43 +271,40 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): async def test_send_private_embed(self): """Test does `send_private_embed` return correct bool.""" + embed = Embed(title="Test", description="Test val") + test_cases = [ { - "args": (self.user, Embed(title="Test", description="Test val")), "expected_output": True, "raised_exception": None }, { - "args": (self.user, Embed(title="Test", description="Test val")), "expected_output": False, "raised_exception": HTTPException(AsyncMock(), AsyncMock()) }, { - "args": (self.user, Embed(title="Test", description="Test val")), "expected_output": False, "raised_exception": Forbidden(AsyncMock(), AsyncMock()) }, { - "args": (self.user, Embed(title="Test", description="Test val")), "expected_output": False, "raised_exception": NotFound(AsyncMock(), AsyncMock()) } ] for case in test_cases: - args = case["args"] expected = case["expected_output"] - raised: Union[Forbidden, HTTPException, NotFound, None] = case["raised_exception"] + raised = case["raised_exception"] - with self.subTest(args=args, expected=expected, raised=raised): + with self.subTest(expected=expected, raised=raised): if raised: self.user.send.side_effect = raised - result = await utils.send_private_embed(*args) + result = await utils.send_private_embed(self.user, embed) self.assertEqual(result, expected) if expected: - args[0].send.assert_awaited_once_with(embed=args[1]) + self.user.send.assert_awaited_once_with(embed=embed) self.user.send.reset_mock(side_effect=True) -- cgit v1.2.3 From 4b211f5278dc5e14871468d05d0414d2b2f7de3c Mon Sep 17 00:00:00 2001 From: ks123 Date: Sun, 8 Mar 2020 10:38:56 +0200 Subject: (Moderation Utils Tests): Removed unnecessary `if` check from `send_private_embed` test --- tests/bot/cogs/moderation/test_utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index b6bf1a96e..7291e42c6 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -297,8 +297,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): raised = case["raised_exception"] with self.subTest(expected=expected, raised=raised): - if raised: - self.user.send.side_effect = raised + self.user.send.side_effect = raised result = await utils.send_private_embed(self.user, embed) -- cgit v1.2.3 From 181971424f4e6c494f8ecb8f75919e27b784dcf5 Mon Sep 17 00:00:00 2001 From: ks123 Date: Sun, 8 Mar 2020 10:40:58 +0200 Subject: (Moderation Utils Tests): Moved mock resetting to beginning of subtest in `post_user` and `send_private_embed` test. --- tests/bot/cogs/moderation/test_utils.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index 7291e42c6..d43269b19 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -248,6 +248,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): payload = case["payload"] with self.subTest(args=args, result=expected, error=error, payload=payload): + self.bot.api_client.post.reset_mock(side_effect=True) self.ctx.bot.api_client.post.return_value = expected if error: @@ -267,8 +268,6 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): else: self.assertTrue(str(err.status) in self.ctx.send.call_args[0][0]) - self.bot.api_client.post.reset_mock(side_effect=True) - async def test_send_private_embed(self): """Test does `send_private_embed` return correct bool.""" embed = Embed(title="Test", description="Test val") @@ -297,6 +296,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): raised = case["raised_exception"] with self.subTest(expected=expected, raised=raised): + self.user.send.reset_mock(side_effect=True) self.user.send.side_effect = raised result = await utils.send_private_embed(self.user, embed) @@ -305,8 +305,6 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): if expected: self.user.send.assert_awaited_once_with(embed=embed) - self.user.send.reset_mock(side_effect=True) - @patch("bot.cogs.moderation.utils.post_user") async def test_post_infraction(self, post_user_mock): """Test does `post_infraction` call functions correctly and return `None` or `Dict`.""" -- cgit v1.2.3 From 708e2165ff44b19d31bd6f2d8fdd7d3b408a9ef3 Mon Sep 17 00:00:00 2001 From: ks123 Date: Thu, 12 Mar 2020 19:51:17 +0200 Subject: (Moderation Utils Tests): Create extra new tests set for `post_infraction` testing, removed old. --- tests/bot/cogs/moderation/test_utils.py | 163 ++++++++++++-------------------- 1 file changed, 60 insertions(+), 103 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index d43269b19..f34a56d50 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -1,6 +1,6 @@ import unittest from datetime import datetime -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, MagicMock, patch from discord import Embed, Forbidden, HTTPException, NotFound @@ -305,114 +305,71 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): if expected: self.user.send.assert_awaited_once_with(embed=embed) - @patch("bot.cogs.moderation.utils.post_user") - async def test_post_infraction(self, post_user_mock): - """Test does `post_infraction` call functions correctly and return `None` or `Dict`.""" - now = datetime.now() - test_cases = [ - { - "args": (self.ctx, self.member, "ban", "Test Ban"), - "expected_output": [ - { - "id": 1, - "inserted_at": "2018-11-22T07:24:06.132307Z", - "expires_at": "5018-11-20T15:52:00Z", - "active": True, - "user": 1234, - "actor": 1234, - "type": "ban", - "reason": "Test Ban", - "hidden": False - } - ], - "raised_error": None, - "payload": { - "actor": self.ctx.message.author.id, - "hidden": False, - "reason": "Test Ban", - "type": "ban", - "user": self.member.id, - "active": True - } - }, - { - "args": (self.ctx, self.member, "note", "Test Ban"), - "expected_output": None, - "raised_error": ResponseCodeError(AsyncMock(), AsyncMock()), - "payload": { - "actor": self.ctx.message.author.id, - "hidden": False, - "reason": "Test Ban", - "type": "note", - "user": self.member.id, - "active": True - } - }, - { - "args": (self.ctx, self.member, "mute", "Test Ban"), - "expected_output": None, - "raised_error": ResponseCodeError(AsyncMock(status=400), {'user': 1234}), - "payload": { - "actor": self.ctx.message.author.id, - "hidden": False, - "reason": "Test Ban", - "type": "mute", - "user": self.member.id, - "active": True - } - }, - { - "args": (self.ctx, self.member, "ban", "Test Ban", now, True, False), - "expected_output": [ - { - "id": 1, - "inserted_at": "2018-11-22T07:24:06.132307Z", - "expires_at": "5018-11-20T15:52:00Z", - "active": True, - "user": 1234, - "actor": 1234, - "type": "ban", - "reason": "Test Ban", - "hidden": False - } - ], - "raised_error": None, - "payload": { - "actor": self.ctx.message.author.id, - "hidden": True, - "reason": "Test Ban", - "type": "ban", - "user": self.member.id, - "active": False, - "expires_at": now.isoformat() - } - }, - ] - for case in test_cases: - args = case["args"] - expected = case["expected_output"] - raised = case["raised_error"] - payload = case["payload"] +class TestPostInfraction(unittest.IsolatedAsyncioTestCase): + """Tests for `post_infraction` function.""" - with self.subTest(args=args, expected=expected, raised=raised, payload=payload): - self.ctx.bot.api_client.post.reset_mock(side_effect=True) - post_user_mock.reset_mock() + def setUp(self): + self.bot = MockBot() + self.member = MockMember(id=1234) + self.user = MockUser(id=1234) + self.ctx = MockContext(bot=self.bot, author=self.member) - if raised: - self.ctx.bot.api_client.post.side_effect = raised + async def test_normal_post_infraction(self): + """Test does `post_infraction` return correct value when no errors raise.""" + now = datetime.now() + payload = { + "actor": self.ctx.message.author.id, + "hidden": True, + "reason": "Test reason", + "type": "ban", + "user": self.member.id, + "active": False, + "expires_at": now.isoformat() + } - post_user_mock.return_value = "foo" + self.ctx.bot.api_client.post.return_value = "foo" + actual = await utils.post_infraction(self.ctx, self.member, "ban", "Test reason", now, True, False) - self.ctx.bot.api_client.post.return_value = expected + self.assertEqual(actual, "foo") + self.ctx.bot.api_client.post.assert_awaited_once_with("bot/infractions", json=payload) - result = await utils.post_infraction(*args) + async def test_unknown_error_post_infraction(self): + """Test does `post_infraction` send info about fail to chat (`ctx.send`).""" + self.ctx.bot.api_client.post.side_effect = ResponseCodeError(AsyncMock(), AsyncMock()) + self.ctx.bot.api_client.post.side_effect.status = 500 - self.assertEqual(result, expected) + actual = await utils.post_infraction(self.ctx, self.user, "ban", "Test reason") + self.assertIsNone(actual) - if not raised: - self.bot.api_client.post.assert_awaited_once_with("bot/infractions", json=payload) + self.assertTrue("500" in self.ctx.send.call_args[0][0]) - if hasattr(raised, "status") and hasattr(raised, "response_json"): - if raised.status == 400 and "user" in raised.response_json: - post_user_mock.assert_awaited_once_with(args[0], args[1]) + @patch("bot.cogs.moderation.utils.post_user") + async def test_user_not_found_none_post_infraction(self, post_user_mock): + """Test does `post_infraction` return `None` correctly due can't create new user.""" + self.bot.api_client.post.side_effect = ResponseCodeError(MagicMock(status=400), {"user": "foo"}) + post_user_mock.return_value = None + + actual = await utils.post_infraction(self.ctx, self.user, "mute", "Test reason") + self.assertIsNone(actual) + post_user_mock.assert_awaited_once_with(self.ctx, self.user) + + @patch("bot.cogs.moderation.utils.post_user") + async def test_first_fail_second_success_user_post_infraction(self, post_user_mock): + """Test does `post_infraction` fail first time and return correct result 2nd time when new user posted.""" + payload = { + "actor": self.ctx.message.author.id, + "hidden": False, + "reason": "Test reason", + "type": "mute", + "user": self.user.id, + "active": True + } + + self.bot.api_client.post.side_effect = [ResponseCodeError(MagicMock(status=400), {"user": "foo"}), "foo"] + post_user_mock.return_value = "bar" + + actual = await utils.post_infraction(self.ctx, self.user, "mute", "Test reason") + self.assertEqual(actual, "foo") + self.bot.api_client.post.assert_awaited_once_with("bot/infractions", json=payload) + post_user_mock.assert_awaited_once_with(self.ctx, self.user) -- cgit v1.2.3 From f793c0772c7b1c5c4edb457fb372e9a57a8200ff Mon Sep 17 00:00:00 2001 From: ks123 Date: Thu, 12 Mar 2020 19:54:37 +0200 Subject: (Moderation Utils Tests): Added params to variable in `has_active_infraction` test. --- tests/bot/cogs/moderation/test_utils.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index f34a56d50..3432ff595 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -56,15 +56,17 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): self.bot.api_client.get.reset_mock() self.ctx.send.reset_mock() + params = { + "active": "true", + "type": "ban", + "user__id": str(self.member.id) + } + self.bot.api_client.get.return_value = case["get_return_value"] result = await utils.has_active_infraction(self.ctx, self.member, "ban") self.assertEqual(result, case["expected_output"]) - self.bot.api_client.get.assert_awaited_once_with("bot/infractions", params={ - "active": "true", - "type": "ban", - "user__id": str(self.member.id) - }) + self.bot.api_client.get.assert_awaited_once_with("bot/infractions", params=params) if result: self.assertTrue(case["infraction_nr"] in self.ctx.send.call_args[0][0]) -- cgit v1.2.3 From cd1193ec09c5259ff2f2c5906faf20ed788326c9 Mon Sep 17 00:00:00 2001 From: ks123 Date: Thu, 12 Mar 2020 20:00:24 +0200 Subject: (Moderation Utils Tests): Moved embed generating to test cases loop from test cases listing, added icon to test cases in `notify_pardon` test --- tests/bot/cogs/moderation/test_utils.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index 3432ff595..7f5e441b7 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -177,27 +177,27 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): test_cases = [ { "args": (self.user, "Test title", "Example content"), - "expected_output": Embed( - description="Example content", - colour=PARDON_COLOR - ).set_author(name="Test title", icon_url=Icons.user_verified), + "icon": Icons.user_verified, "send_result": True }, { - "args": (self.user, "Test title 1", "Example content 1", Icons.user_update), - "expected_output": Embed( - description="Example content 1", - colour=PARDON_COLOR - ).set_author(name="Test title 1", icon_url=Icons.user_update), + "args": (self.user, "Test title", "Example content", Icons.user_update), + "icon": Icons.user_update, "send_result": False } ] for case in test_cases: args = case["args"] - expected = case["expected_output"] send = case["send_result"] + expected = Embed( + description="Example content", + colour=PARDON_COLOR).set_author( + name="Test title", + icon_url=case["icon"] + ) + with self.subTest(args=args, expected=expected): send_private_embed_mock.reset_mock() -- cgit v1.2.3 From 6376b6a47033c51b80d32e0cf00e6d13ca9d05c3 Mon Sep 17 00:00:00 2001 From: ks123 Date: Thu, 12 Mar 2020 20:03:00 +0200 Subject: (Moderation Utils Tests): Removed unnecessary symbols from `has_active_infraction` test `infraction_nr` variable and changes this to more unique number. --- tests/bot/cogs/moderation/test_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index 7f5e441b7..2f66904d8 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -45,9 +45,9 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): "infraction_nr": None }, { - "get_return_value": [{"id": 1}], + "get_return_value": [{"id": 123987}], "expected_output": True, - "infraction_nr": "**#1**" + "infraction_nr": "123987" } ] -- cgit v1.2.3 From f93be96dd484e4b484a3a7e8c1c3b79062b6c386 Mon Sep 17 00:00:00 2001 From: ks123 Date: Thu, 12 Mar 2020 20:05:03 +0200 Subject: (Moderation Utils Tests): Fixed formatting in `notify_infraction` test. --- tests/bot/cogs/moderation/test_utils.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index 2f66904d8..61fb618d4 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -145,9 +145,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): name=INFRACTION_AUTHOR_NAME, url=utils.RULES_URL, icon_url=Icons.defcon_denied - ).set_footer( - text=INFRACTION_APPEAL_FOOTER - ), + ).set_footer(text=INFRACTION_APPEAL_FOOTER), "send_result": False } ] -- cgit v1.2.3 From b70d2fc557bb2bdbc32f905132a0e80272f174a2 Mon Sep 17 00:00:00 2001 From: ks123 Date: Thu, 12 Mar 2020 20:07:27 +0200 Subject: (Moderation Utils Tests): Hard-coded `self.ctx` argument to `post_user` test, renamed current `args` to `user`, applied this in code. --- tests/bot/cogs/moderation/test_utils.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index 61fb618d4..3f721d182 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -214,7 +214,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): user = MockUser(avatar="abc", discriminator=5678, id=1234, name="Test user") test_cases = [ { - "args": (self.ctx, user), + "user": user, "post_result": "bar", "raise_error": False, "payload": { @@ -227,7 +227,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): } }, { - "args": (self.ctx, self.member), + "user": self.member, "post_result": "foo", "raise_error": True, "payload": { @@ -242,12 +242,12 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): ] for case in test_cases: - args = case["args"] + test_user = case["user"] expected = case["post_result"] error = case["raise_error"] payload = case["payload"] - with self.subTest(args=args, result=expected, error=error, payload=payload): + with self.subTest(user=test_user, result=expected, error=error, payload=payload): self.bot.api_client.post.reset_mock(side_effect=True) self.ctx.bot.api_client.post.return_value = expected @@ -256,7 +256,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): err = self.ctx.bot.api_client.post.side_effect err.status = 400 - result = await utils.post_user(*args) + result = await utils.post_user(self.ctx, test_user) if error: self.assertIsNone(result) -- cgit v1.2.3 From 4724b66c4a337b1735f7b08cb60c4cbcb68a6e3c Mon Sep 17 00:00:00 2001 From: ks123 Date: Thu, 12 Mar 2020 20:13:53 +0200 Subject: (Moderation Utils Tests): Move errors from booleans to actual errors in `post_user` test. --- tests/bot/cogs/moderation/test_utils.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index 3f721d182..9afa5ab0b 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -216,7 +216,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): { "user": user, "post_result": "bar", - "raise_error": False, + "raise_error": None, "payload": { "avatar_hash": "abc", "discriminator": 5678, @@ -229,7 +229,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): { "user": self.member, "post_result": "foo", - "raise_error": True, + "raise_error": ResponseCodeError(MagicMock(status=400), "foo"), "payload": { "avatar_hash": 0, "discriminator": 0, @@ -251,10 +251,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): self.bot.api_client.post.reset_mock(side_effect=True) self.ctx.bot.api_client.post.return_value = expected - if error: - self.ctx.bot.api_client.post.side_effect = ResponseCodeError(AsyncMock(), expected) - err = self.ctx.bot.api_client.post.side_effect - err.status = 400 + self.ctx.bot.api_client.post.side_effect = error result = await utils.post_user(self.ctx, test_user) @@ -266,7 +263,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): if not error: self.bot.api_client.post.assert_awaited_once_with("bot/users", json=payload) else: - self.assertTrue(str(err.status) in self.ctx.send.call_args[0][0]) + self.assertTrue(str(error.status) in self.ctx.send.call_args[0][0]) async def test_send_private_embed(self): """Test does `send_private_embed` return correct bool.""" -- cgit v1.2.3 From 35ffc216e62a46aa6ba6fcb7d0717354d176e175 Mon Sep 17 00:00:00 2001 From: ks123 Date: Thu, 12 Mar 2020 20:15:28 +0200 Subject: (Moderation Utils Tests): Added call check for `ctx.send` in `post_user` test. --- tests/bot/cogs/moderation/test_utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index 9afa5ab0b..e0af13a46 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -263,6 +263,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): if not error: self.bot.api_client.post.assert_awaited_once_with("bot/users", json=payload) else: + self.ctx.send.assert_awaited_once() self.assertTrue(str(error.status) in self.ctx.send.call_args[0][0]) async def test_send_private_embed(self): -- cgit v1.2.3 From fe504bd30360df0c18fbfccfb27d2652ac19e9b8 Mon Sep 17 00:00:00 2001 From: ks123 Date: Thu, 12 Mar 2020 20:19:43 +0200 Subject: (Moderation Utils Tests): Added mock reset due fail. --- tests/bot/cogs/moderation/test_utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index e0af13a46..2cba37e3a 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -355,6 +355,8 @@ class TestPostInfraction(unittest.IsolatedAsyncioTestCase): @patch("bot.cogs.moderation.utils.post_user") async def test_first_fail_second_success_user_post_infraction(self, post_user_mock): """Test does `post_infraction` fail first time and return correct result 2nd time when new user posted.""" + self.bot.api_client.post.reset_mock() + payload = { "actor": self.ctx.message.author.id, "hidden": False, -- cgit v1.2.3 From 5ac2aa48109f16e96195dda60e3a70b65b9562fa Mon Sep 17 00:00:00 2001 From: Karlis S <45097959+ks129@users.noreply.github.com> Date: Thu, 12 Mar 2020 21:10:23 +0200 Subject: (Moderation Utils Tests): Removed `once` from `post_infraction` test due tests failing. --- tests/bot/cogs/moderation/test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index 2cba37e3a..e23585c99 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -371,5 +371,5 @@ class TestPostInfraction(unittest.IsolatedAsyncioTestCase): actual = await utils.post_infraction(self.ctx, self.user, "mute", "Test reason") self.assertEqual(actual, "foo") - self.bot.api_client.post.assert_awaited_once_with("bot/infractions", json=payload) + self.bot.api_client.post.assert_awaited_with("bot/infractions", json=payload) post_user_mock.assert_awaited_once_with(self.ctx, self.user) -- cgit v1.2.3 From dac8e758201e938c5e694efb63b96485fb771274 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 12 Mar 2020 19:22:27 -0700 Subject: Revise docstrings for moderation util tests --- tests/bot/cogs/moderation/test_utils.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index e23585c99..ca951250f 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -36,7 +36,9 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): async def test_user_has_active_infraction(self): """ - Test does `has_active_infraction` return call at least once `ctx.send` API get, check does return correct bool. + Should request the API for active infractions and return `True` if the user has one or `False` otherwise. + + A message should be sent to the context indicating a user already has an infraction, if that's the case. """ test_cases = [ { @@ -74,7 +76,11 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): @patch("bot.cogs.moderation.utils.send_private_embed") async def test_notify_infraction(self, send_private_embed_mock): - """Test does `notify_infraction` create correct embed and return correct boolean.""" + """ + Should send an embed of a certain format as a DM and return `True` if DM successful. + + Appealable infractions should have the appeal message in the embed's footer. + """ test_cases = [ { "args": (self.user, "ban", "2020-02-26 09:20 (23 hours and 59 minutes)"), @@ -171,7 +177,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): @patch("bot.cogs.moderation.utils.send_private_embed") async def test_notify_pardon(self, send_private_embed_mock): - """Test does `notify_pardon` create correct embed and return correct bool.""" + """Should send an embed of a certain format as a DM and return `True` if DM successful.""" test_cases = [ { "args": (self.user, "Test title", "Example content"), @@ -210,7 +216,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): send_private_embed_mock.assert_awaited_once_with(args[0], embed) async def test_post_user(self): - """Test does `post_user` handle errors and results correctly.""" + """Should POST a new user and return the response if successful or otherwise send an error message.""" user = MockUser(avatar="abc", discriminator=5678, id=1234, name="Test user") test_cases = [ { @@ -267,7 +273,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): self.assertTrue(str(error.status) in self.ctx.send.call_args[0][0]) async def test_send_private_embed(self): - """Test does `send_private_embed` return correct bool.""" + """Should DM the user and return `True` on success or `False` on failure.""" embed = Embed(title="Test", description="Test val") test_cases = [ @@ -305,7 +311,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): class TestPostInfraction(unittest.IsolatedAsyncioTestCase): - """Tests for `post_infraction` function.""" + """Tests for the `post_infraction` function.""" def setUp(self): self.bot = MockBot() @@ -314,7 +320,7 @@ class TestPostInfraction(unittest.IsolatedAsyncioTestCase): self.ctx = MockContext(bot=self.bot, author=self.member) async def test_normal_post_infraction(self): - """Test does `post_infraction` return correct value when no errors raise.""" + """Should return response from POST request if there are no errors.""" now = datetime.now() payload = { "actor": self.ctx.message.author.id, @@ -333,7 +339,7 @@ class TestPostInfraction(unittest.IsolatedAsyncioTestCase): self.ctx.bot.api_client.post.assert_awaited_once_with("bot/infractions", json=payload) async def test_unknown_error_post_infraction(self): - """Test does `post_infraction` send info about fail to chat (`ctx.send`).""" + """Should send an error message to chat when a non-400 error occurs.""" self.ctx.bot.api_client.post.side_effect = ResponseCodeError(AsyncMock(), AsyncMock()) self.ctx.bot.api_client.post.side_effect.status = 500 @@ -344,7 +350,7 @@ class TestPostInfraction(unittest.IsolatedAsyncioTestCase): @patch("bot.cogs.moderation.utils.post_user") async def test_user_not_found_none_post_infraction(self, post_user_mock): - """Test does `post_infraction` return `None` correctly due can't create new user.""" + """Should abort and return `None` when a new user fails to be posted.""" self.bot.api_client.post.side_effect = ResponseCodeError(MagicMock(status=400), {"user": "foo"}) post_user_mock.return_value = None @@ -354,7 +360,7 @@ class TestPostInfraction(unittest.IsolatedAsyncioTestCase): @patch("bot.cogs.moderation.utils.post_user") async def test_first_fail_second_success_user_post_infraction(self, post_user_mock): - """Test does `post_infraction` fail first time and return correct result 2nd time when new user posted.""" + """Should post the user if they don't exist, POST infraction again, and return the response if successful.""" self.bot.api_client.post.reset_mock() payload = { -- cgit v1.2.3 From 3043dd1d565943f180a5ae16e46e6daa531466c7 Mon Sep 17 00:00:00 2001 From: Karlis S Date: Fri, 13 Mar 2020 07:25:43 +0000 Subject: (Moderation Utils Tests): Added 2 call check to `post_infraction` test. --- tests/bot/cogs/moderation/test_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index ca951250f..659884d93 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -1,6 +1,6 @@ import unittest from datetime import datetime -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, call, patch from discord import Embed, Forbidden, HTTPException, NotFound @@ -377,5 +377,5 @@ class TestPostInfraction(unittest.IsolatedAsyncioTestCase): actual = await utils.post_infraction(self.ctx, self.user, "mute", "Test reason") self.assertEqual(actual, "foo") - self.bot.api_client.post.assert_awaited_with("bot/infractions", json=payload) + self.bot.api_client.post.assert_has_awaits([call("bot/infractions", json=payload)] * 2) post_user_mock.assert_awaited_once_with(self.ctx, self.user) -- cgit v1.2.3 From 7806e11a017698ff43494a1fa1a908e11bb63e33 Mon Sep 17 00:00:00 2001 From: Karlis S Date: Fri, 13 Mar 2020 07:27:50 +0000 Subject: (Moderation Utils Tests): Removed unnecessary mock resetting. --- tests/bot/cogs/moderation/test_utils.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index 659884d93..03f086ba9 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -361,8 +361,6 @@ class TestPostInfraction(unittest.IsolatedAsyncioTestCase): @patch("bot.cogs.moderation.utils.post_user") async def test_first_fail_second_success_user_post_infraction(self, post_user_mock): """Should post the user if they don't exist, POST infraction again, and return the response if successful.""" - self.bot.api_client.post.reset_mock() - payload = { "actor": self.ctx.message.author.id, "hidden": False, -- cgit v1.2.3 From 1052ad4213348ede7ec6e495d32e21b3818153e0 Mon Sep 17 00:00:00 2001 From: Karlis S Date: Fri, 13 Mar 2020 07:31:00 +0000 Subject: (Moderation Utils Tests): Moved `return_value` to `patch` decorator. --- tests/bot/cogs/moderation/test_utils.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index 03f086ba9..6702372d6 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -348,17 +348,16 @@ class TestPostInfraction(unittest.IsolatedAsyncioTestCase): self.assertTrue("500" in self.ctx.send.call_args[0][0]) - @patch("bot.cogs.moderation.utils.post_user") + @patch("bot.cogs.moderation.utils.post_user", return_value=None) async def test_user_not_found_none_post_infraction(self, post_user_mock): """Should abort and return `None` when a new user fails to be posted.""" self.bot.api_client.post.side_effect = ResponseCodeError(MagicMock(status=400), {"user": "foo"}) - post_user_mock.return_value = None actual = await utils.post_infraction(self.ctx, self.user, "mute", "Test reason") self.assertIsNone(actual) post_user_mock.assert_awaited_once_with(self.ctx, self.user) - @patch("bot.cogs.moderation.utils.post_user") + @patch("bot.cogs.moderation.utils.post_user", return_value="bar") async def test_first_fail_second_success_user_post_infraction(self, post_user_mock): """Should post the user if they don't exist, POST infraction again, and return the response if successful.""" payload = { @@ -371,7 +370,6 @@ class TestPostInfraction(unittest.IsolatedAsyncioTestCase): } self.bot.api_client.post.side_effect = [ResponseCodeError(MagicMock(status=400), {"user": "foo"}), "foo"] - post_user_mock.return_value = "bar" actual = await utils.post_infraction(self.ctx, self.user, "mute", "Test reason") self.assertEqual(actual, "foo") -- cgit v1.2.3 From 1d486096e20dde3bcf6bc95ced0557840625e84d Mon Sep 17 00:00:00 2001 From: Karlis S Date: Fri, 13 Mar 2020 07:32:21 +0000 Subject: (Moderation Utils Tests): Fixed formatting in `notify_pardon` test. --- tests/bot/cogs/moderation/test_utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index 6702372d6..2e4c31836 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -197,7 +197,8 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): expected = Embed( description="Example content", - colour=PARDON_COLOR).set_author( + colour=PARDON_COLOR + ).set_author( name="Test title", icon_url=case["icon"] ) -- cgit v1.2.3 From 897b378a09c1a058f10a92aa23c21e2f737e6819 Mon Sep 17 00:00:00 2001 From: Karlis S Date: Fri, 13 Mar 2020 17:42:39 +0000 Subject: (Moderation Utils Tests): Removed Infraction Color constant. --- tests/bot/cogs/moderation/test_utils.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index 2e4c31836..f30e85b12 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -14,7 +14,6 @@ APPEAL_EMAIL = "appeals@pythondiscord.com" INFRACTION_TITLE = f"Please review our rules over at {utils.RULES_URL}" INFRACTION_APPEAL_FOOTER = f"To appeal this infraction, send an e-mail to {APPEAL_EMAIL}" INFRACTION_AUTHOR_NAME = "Infraction information" -INFRACTION_COLOR = Colours.soft_red INFRACTION_DESCRIPTION_TEMPLATE = ( "\n**Type:** {type}\n" @@ -91,7 +90,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): expires="2020-02-26 09:20 (23 hours and 59 minutes)", reason="No reason provided." ), - colour=INFRACTION_COLOR, + colour=Colours.soft_red, url=utils.RULES_URL ).set_author( name=INFRACTION_AUTHOR_NAME, @@ -109,7 +108,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): expires="N/A", reason="Test reason." ), - colour=INFRACTION_COLOR, + colour=Colours.soft_red, url=utils.RULES_URL ).set_author( name=INFRACTION_AUTHOR_NAME, @@ -127,7 +126,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): expires="N/A", reason="No reason provided." ), - colour=INFRACTION_COLOR, + colour=Colours.soft_red, url=utils.RULES_URL ).set_author( name=INFRACTION_AUTHOR_NAME, @@ -145,7 +144,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): expires="2020-02-26 09:20 (23 hours and 59 minutes)", reason="Test" ), - colour=INFRACTION_COLOR, + colour=Colours.soft_red, url=utils.RULES_URL ).set_author( name=INFRACTION_AUTHOR_NAME, -- cgit v1.2.3 From ea2e8bbe320996d4292f958240e231d622d0481d Mon Sep 17 00:00:00 2001 From: Karlis S Date: Fri, 13 Mar 2020 17:45:15 +0000 Subject: (Moderation Utils Tests): Removed Pardon Color constant. --- tests/bot/cogs/moderation/test_utils.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index f30e85b12..52bdb5fbc 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -21,8 +21,6 @@ INFRACTION_DESCRIPTION_TEMPLATE = ( "**Reason:** {reason}\n" ) -PARDON_COLOR = Colours.soft_green - class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): """Tests Moderation utils.""" @@ -196,7 +194,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): expected = Embed( description="Example content", - colour=PARDON_COLOR + colour=Colours.soft_green ).set_author( name="Test title", icon_url=case["icon"] -- cgit v1.2.3 From 99e1239f4734d0ed34688fa77d5094f8984b9209 Mon Sep 17 00:00:00 2001 From: Karlis S Date: Fri, 13 Mar 2020 18:03:40 +0000 Subject: (Mod Utils + Tests): Moved constants from tests to utils, applied change --- bot/cogs/moderation/utils.py | 28 ++++++++++++++++------- tests/bot/cogs/moderation/test_utils.py | 40 ++++++++++++--------------------- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/bot/cogs/moderation/utils.py b/bot/cogs/moderation/utils.py index 5052b9048..8121a0af8 100644 --- a/bot/cogs/moderation/utils.py +++ b/bot/cogs/moderation/utils.py @@ -28,6 +28,18 @@ UserObject = t.Union[discord.Member, discord.User] UserSnowflake = t.Union[UserObject, discord.Object] Infraction = t.Dict[str, t.Union[str, int, bool]] +APPEAL_EMAIL = "appeals@pythondiscord.com" + +INFRACTION_TITLE = f"Please review our rules over at {RULES_URL}" +INFRACTION_APPEAL_FOOTER = f"To appeal this infraction, send an e-mail to {APPEAL_EMAIL}" +INFRACTION_AUTHOR_NAME = "Infraction information" + +INFRACTION_DESCRIPTION_TEMPLATE = ( + "\n**Type:** {type}\n" + "**Expires:** {expires}\n" + "**Reason:** {reason}\n" +) + async def post_user(ctx: Context, user: UserSnowflake) -> t.Optional[dict]: """ @@ -132,21 +144,21 @@ async def notify_infraction( log.trace(f"Sending {user} a DM about their {infr_type} infraction.") embed = discord.Embed( - description=textwrap.dedent(f""" - **Type:** {infr_type.capitalize()} - **Expires:** {expires_at or "N/A"} - **Reason:** {reason or "No reason provided."} - """), + description=INFRACTION_DESCRIPTION_TEMPLATE.format( + type=infr_type.capitalize(), + expires=expires_at or "N/A", + reason=reason or "No reason provided." + ), colour=Colours.soft_red ) - embed.set_author(name="Infraction information", icon_url=icon_url, url=RULES_URL) - embed.title = f"Please review our rules over at {RULES_URL}" + embed.set_author(name=INFRACTION_AUTHOR_NAME, icon_url=icon_url, url=RULES_URL) + embed.title = INFRACTION_TITLE embed.url = RULES_URL if infr_type in APPEALABLE_INFRACTIONS: embed.set_footer( - text="To appeal this infraction, send an e-mail to appeals@pythondiscord.com" + text=INFRACTION_APPEAL_FOOTER ) return await send_private_embed(user, embed) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index 52bdb5fbc..4f81a2477 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -9,18 +9,6 @@ from bot.cogs.moderation import utils from bot.constants import Colours, Icons from tests.helpers import MockBot, MockContext, MockMember, MockUser -APPEAL_EMAIL = "appeals@pythondiscord.com" - -INFRACTION_TITLE = f"Please review our rules over at {utils.RULES_URL}" -INFRACTION_APPEAL_FOOTER = f"To appeal this infraction, send an e-mail to {APPEAL_EMAIL}" -INFRACTION_AUTHOR_NAME = "Infraction information" - -INFRACTION_DESCRIPTION_TEMPLATE = ( - "\n**Type:** {type}\n" - "**Expires:** {expires}\n" - "**Reason:** {reason}\n" -) - class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): """Tests Moderation utils.""" @@ -82,8 +70,8 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): { "args": (self.user, "ban", "2020-02-26 09:20 (23 hours and 59 minutes)"), "expected_output": Embed( - title=INFRACTION_TITLE, - description=INFRACTION_DESCRIPTION_TEMPLATE.format( + title=utils.INFRACTION_TITLE, + description=utils.INFRACTION_DESCRIPTION_TEMPLATE.format( type="Ban", expires="2020-02-26 09:20 (23 hours and 59 minutes)", reason="No reason provided." @@ -91,17 +79,17 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): colour=Colours.soft_red, url=utils.RULES_URL ).set_author( - name=INFRACTION_AUTHOR_NAME, + name=utils.INFRACTION_AUTHOR_NAME, url=utils.RULES_URL, icon_url=Icons.token_removed - ).set_footer(text=INFRACTION_APPEAL_FOOTER), + ).set_footer(text=utils.INFRACTION_APPEAL_FOOTER), "send_result": True }, { "args": (self.user, "warning", None, "Test reason."), "expected_output": Embed( - title=INFRACTION_TITLE, - description=INFRACTION_DESCRIPTION_TEMPLATE.format( + title=utils.INFRACTION_TITLE, + description=utils.INFRACTION_DESCRIPTION_TEMPLATE.format( type="Warning", expires="N/A", reason="Test reason." @@ -109,7 +97,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): colour=Colours.soft_red, url=utils.RULES_URL ).set_author( - name=INFRACTION_AUTHOR_NAME, + name=utils.INFRACTION_AUTHOR_NAME, url=utils.RULES_URL, icon_url=Icons.token_removed ), @@ -118,8 +106,8 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): { "args": (self.user, "note", None, None, Icons.defcon_denied), "expected_output": Embed( - title=INFRACTION_TITLE, - description=INFRACTION_DESCRIPTION_TEMPLATE.format( + title=utils.INFRACTION_TITLE, + description=utils.INFRACTION_DESCRIPTION_TEMPLATE.format( type="Note", expires="N/A", reason="No reason provided." @@ -127,7 +115,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): colour=Colours.soft_red, url=utils.RULES_URL ).set_author( - name=INFRACTION_AUTHOR_NAME, + name=utils.INFRACTION_AUTHOR_NAME, url=utils.RULES_URL, icon_url=Icons.defcon_denied ), @@ -136,8 +124,8 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): { "args": (self.user, "mute", "2020-02-26 09:20 (23 hours and 59 minutes)", "Test", Icons.defcon_denied), "expected_output": Embed( - title=INFRACTION_TITLE, - description=INFRACTION_DESCRIPTION_TEMPLATE.format( + title=utils.INFRACTION_TITLE, + description=utils.INFRACTION_DESCRIPTION_TEMPLATE.format( type="Mute", expires="2020-02-26 09:20 (23 hours and 59 minutes)", reason="Test" @@ -145,10 +133,10 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): colour=Colours.soft_red, url=utils.RULES_URL ).set_author( - name=INFRACTION_AUTHOR_NAME, + name=utils.INFRACTION_AUTHOR_NAME, url=utils.RULES_URL, icon_url=Icons.defcon_denied - ).set_footer(text=INFRACTION_APPEAL_FOOTER), + ).set_footer(text=utils.INFRACTION_APPEAL_FOOTER), "send_result": False } ] -- cgit v1.2.3 From b1098cf64805ec850822d8b9301e1b72153db1f1 Mon Sep 17 00:00:00 2001 From: Karlis S Date: Fri, 13 Mar 2020 18:09:33 +0000 Subject: (Mod Utils): Removed unnecessary `textwrap` import --- bot/cogs/moderation/utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bot/cogs/moderation/utils.py b/bot/cogs/moderation/utils.py index 8121a0af8..5fcfeb7c7 100644 --- a/bot/cogs/moderation/utils.py +++ b/bot/cogs/moderation/utils.py @@ -1,5 +1,4 @@ import logging -import textwrap import typing as t from datetime import datetime -- cgit v1.2.3 From 72d2f662ff84c8bfca448870e8d7e60777301a68 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Thu, 14 May 2020 19:39:45 +0300 Subject: Mod Utils Tests: Replace `has_active_infraction` with `get_active_infraction` --- tests/bot/cogs/moderation/test_utils.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index 4f81a2477..248adbcb8 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -19,21 +19,21 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): self.user = MockUser(id=1234) self.ctx = MockContext(bot=self.bot, author=self.member) - async def test_user_has_active_infraction(self): + async def test_user_get_active_infraction(self): """ - Should request the API for active infractions and return `True` if the user has one or `False` otherwise. + Should request the API for active infractions and return infraction if the user has one or `None` otherwise. A message should be sent to the context indicating a user already has an infraction, if that's the case. """ test_cases = [ { "get_return_value": [], - "expected_output": False, + "expected_output": None, "infraction_nr": None }, { "get_return_value": [{"id": 123987}], - "expected_output": True, + "expected_output": {"id": 123987}, "infraction_nr": "123987" } ] @@ -51,7 +51,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): self.bot.api_client.get.return_value = case["get_return_value"] - result = await utils.has_active_infraction(self.ctx, self.member, "ban") + result = await utils.get_active_infraction(self.ctx, self.member, "ban") self.assertEqual(result, case["expected_output"]) self.bot.api_client.get.assert_awaited_once_with("bot/infractions", params=params) -- cgit v1.2.3 From 8f0bc2e34ba2d741b3a7c89ad0437299b649153d Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Thu, 11 Jun 2020 18:54:33 +0300 Subject: Mod Utils: Add missing import what was removed Restore `textwrap` import that was removed with merge. --- bot/cogs/moderation/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bot/cogs/moderation/utils.py b/bot/cogs/moderation/utils.py index 44dca7c9f..2acaf37f9 100644 --- a/bot/cogs/moderation/utils.py +++ b/bot/cogs/moderation/utils.py @@ -1,4 +1,5 @@ import logging +import textwrap import typing as t from datetime import datetime -- cgit v1.2.3 From 6c58ecb647b046c6a9a1e2b6d9b4d0e0f326e9bd Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Fri, 12 Jun 2020 16:54:55 +0300 Subject: Remove deprecated avatar hash in `test_post_user` --- tests/bot/cogs/moderation/test_utils.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index 248adbcb8..596f077b5 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -203,14 +203,13 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): async def test_post_user(self): """Should POST a new user and return the response if successful or otherwise send an error message.""" - user = MockUser(avatar="abc", discriminator=5678, id=1234, name="Test user") + user = MockUser(discriminator=5678, id=1234, name="Test user") test_cases = [ { "user": user, "post_result": "bar", "raise_error": None, "payload": { - "avatar_hash": "abc", "discriminator": 5678, "id": self.user.id, "in_guild": False, @@ -223,7 +222,6 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): "post_result": "foo", "raise_error": ResponseCodeError(MagicMock(status=400), "foo"), "payload": { - "avatar_hash": 0, "discriminator": 0, "id": self.member.id, "in_guild": False, -- cgit v1.2.3 From 4cc6f759f53ebe31d5025ff902189ab211409d4f Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Fri, 12 Jun 2020 19:30:30 +0300 Subject: Implement description shortening to infraction notify tests --- tests/bot/cogs/moderation/test_utils.py | 35 +++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index 596f077b5..363d8938a 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -1,3 +1,4 @@ +import textwrap import unittest from datetime import datetime from unittest.mock import AsyncMock, MagicMock, call, patch @@ -71,11 +72,11 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): "args": (self.user, "ban", "2020-02-26 09:20 (23 hours and 59 minutes)"), "expected_output": Embed( title=utils.INFRACTION_TITLE, - description=utils.INFRACTION_DESCRIPTION_TEMPLATE.format( + description=textwrap.shorten(utils.INFRACTION_DESCRIPTION_TEMPLATE.format( type="Ban", expires="2020-02-26 09:20 (23 hours and 59 minutes)", reason="No reason provided." - ), + ), width=2048, placeholder="..."), colour=Colours.soft_red, url=utils.RULES_URL ).set_author( @@ -89,11 +90,11 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): "args": (self.user, "warning", None, "Test reason."), "expected_output": Embed( title=utils.INFRACTION_TITLE, - description=utils.INFRACTION_DESCRIPTION_TEMPLATE.format( + description=textwrap.shorten(utils.INFRACTION_DESCRIPTION_TEMPLATE.format( type="Warning", expires="N/A", reason="Test reason." - ), + ), width=2048, placeholder="..."), colour=Colours.soft_red, url=utils.RULES_URL ).set_author( @@ -107,11 +108,11 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): "args": (self.user, "note", None, None, Icons.defcon_denied), "expected_output": Embed( title=utils.INFRACTION_TITLE, - description=utils.INFRACTION_DESCRIPTION_TEMPLATE.format( + description=textwrap.shorten(utils.INFRACTION_DESCRIPTION_TEMPLATE.format( type="Note", expires="N/A", reason="No reason provided." - ), + ), width=2048, placeholder="..."), colour=Colours.soft_red, url=utils.RULES_URL ).set_author( @@ -125,11 +126,11 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): "args": (self.user, "mute", "2020-02-26 09:20 (23 hours and 59 minutes)", "Test", Icons.defcon_denied), "expected_output": Embed( title=utils.INFRACTION_TITLE, - description=utils.INFRACTION_DESCRIPTION_TEMPLATE.format( + description=textwrap.shorten(utils.INFRACTION_DESCRIPTION_TEMPLATE.format( type="Mute", expires="2020-02-26 09:20 (23 hours and 59 minutes)", reason="Test" - ), + ), width=2048, placeholder="..."), colour=Colours.soft_red, url=utils.RULES_URL ).set_author( @@ -138,6 +139,24 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): icon_url=Icons.defcon_denied ).set_footer(text=utils.INFRACTION_APPEAL_FOOTER), "send_result": False + }, + { + "args": (self.user, "mute", None, "foo bar" * 4000, Icons.defcon_denied), + "expected_output": Embed( + title=utils.INFRACTION_TITLE, + description=textwrap.shorten(utils.INFRACTION_DESCRIPTION_TEMPLATE.format( + type="Mute", + expires="N/A", + reason="foo bar" * 4000 + ), width=2048, placeholder="..."), + colour=Colours.soft_red, + url=utils.RULES_URL + ).set_author( + name=utils.INFRACTION_AUTHOR_NAME, + url=utils.RULES_URL, + icon_url=Icons.defcon_denied + ).set_footer(text=utils.INFRACTION_APPEAL_FOOTER), + "send_result": True } ] -- cgit v1.2.3 From 0d0f4318dc2c08d87d473ecb2d66a5622d36cf9d Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Fri, 12 Jun 2020 19:53:00 +0300 Subject: Increase coverage of moderation utils tests --- tests/bot/cogs/moderation/test_utils.py | 41 +++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index 363d8938a..77f926a48 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -30,12 +30,20 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): { "get_return_value": [], "expected_output": None, - "infraction_nr": None + "infraction_nr": None, + "send_msg": True }, { "get_return_value": [{"id": 123987}], "expected_output": {"id": 123987}, - "infraction_nr": "123987" + "infraction_nr": "123987", + "send_msg": False + }, + { + "get_return_value": [{"id": 123987}], + "expected_output": {"id": 123987}, + "infraction_nr": "123987", + "send_msg": True } ] @@ -52,13 +60,16 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): self.bot.api_client.get.return_value = case["get_return_value"] - result = await utils.get_active_infraction(self.ctx, self.member, "ban") + result = await utils.get_active_infraction(self.ctx, self.member, "ban", send_msg=case["send_msg"]) self.assertEqual(result, case["expected_output"]) self.bot.api_client.get.assert_awaited_once_with("bot/infractions", params=params) - if result: + if case["send_msg"] and case["get_return_value"]: + self.ctx.send.assert_awaited_once() self.assertTrue(case["infraction_nr"] in self.ctx.send.call_args[0][0]) self.assertTrue("ban" in self.ctx.send.call_args[0][0]) + else: + self.ctx.send.assert_not_awaited() @patch("bot.cogs.moderation.utils.send_private_embed") async def test_notify_infraction(self, send_private_embed_mock): @@ -220,9 +231,11 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): send_private_embed_mock.assert_awaited_once_with(args[0], embed) - async def test_post_user(self): + @patch("bot.cogs.moderation.utils.log") + async def test_post_user(self, log_mock): """Should POST a new user and return the response if successful or otherwise send an error message.""" user = MockUser(discriminator=5678, id=1234, name="Test user") + some_mock = MagicMock(discriminator=3333) test_cases = [ { "user": user, @@ -247,6 +260,18 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): "name": "Name unknown", "roles": [] } + }, + { + "user": some_mock, + "post_result": "bar", + "raise_error": None, + "payload": { + "discriminator": some_mock.discriminator, + "id": some_mock.id, + "in_guild": False, + "name": some_mock.name, + "roles": [] + } } ] @@ -257,6 +282,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): payload = case["payload"] with self.subTest(user=test_user, result=expected, error=error, payload=payload): + log_mock.reset_mock() self.bot.api_client.post.reset_mock(side_effect=True) self.ctx.bot.api_client.post.return_value = expected @@ -275,6 +301,11 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): self.ctx.send.assert_awaited_once() self.assertTrue(str(error.status) in self.ctx.send.call_args[0][0]) + if isinstance(test_user, MagicMock): + log_mock.debug.assert_called_once() + else: + log_mock.debug.assert_not_called() + async def test_send_private_embed(self): """Should DM the user and return `True` on success or `False` on failure.""" embed = Embed(title="Test", description="Test val") -- cgit v1.2.3 From 52df57f53ccec7467afaa64535697aa9cd3a0740 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Wed, 24 Jun 2020 14:38:10 +0300 Subject: Mod Utils: Remove unnecessary line splitting on embed footer adding --- bot/cogs/moderation/utils.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bot/cogs/moderation/utils.py b/bot/cogs/moderation/utils.py index 2acaf37f9..5df282f80 100644 --- a/bot/cogs/moderation/utils.py +++ b/bot/cogs/moderation/utils.py @@ -170,9 +170,7 @@ async def notify_infraction( embed.url = RULES_URL if infr_type in APPEALABLE_INFRACTIONS: - embed.set_footer( - text=INFRACTION_APPEAL_FOOTER - ) + embed.set_footer(text=INFRACTION_APPEAL_FOOTER) return await send_private_embed(user, embed) -- cgit v1.2.3 From 5f0490aad5a8d22a5f05dc6debdb3485a0ed9671 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Wed, 24 Jun 2020 14:45:51 +0300 Subject: Mod Utils Tests: Move INFRACTION_DESCRIPTION_TEMPLATE to tests file --- bot/cogs/moderation/utils.py | 6 ------ tests/bot/cogs/moderation/test_utils.py | 16 +++++++++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/bot/cogs/moderation/utils.py b/bot/cogs/moderation/utils.py index 5df282f80..104baf528 100644 --- a/bot/cogs/moderation/utils.py +++ b/bot/cogs/moderation/utils.py @@ -34,12 +34,6 @@ INFRACTION_TITLE = f"Please review our rules over at {RULES_URL}" INFRACTION_APPEAL_FOOTER = f"To appeal this infraction, send an e-mail to {APPEAL_EMAIL}" INFRACTION_AUTHOR_NAME = "Infraction information" -INFRACTION_DESCRIPTION_TEMPLATE = ( - "\n**Type:** {type}\n" - "**Expires:** {expires}\n" - "**Reason:** {reason}\n" -) - async def post_user(ctx: Context, user: UserSnowflake) -> t.Optional[dict]: """ diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index 77f926a48..dde5b438d 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -10,6 +10,12 @@ from bot.cogs.moderation import utils from bot.constants import Colours, Icons from tests.helpers import MockBot, MockContext, MockMember, MockUser +INFRACTION_DESCRIPTION_TEMPLATE = ( + "\n**Type:** {type}\n" + "**Expires:** {expires}\n" + "**Reason:** {reason}\n" +) + class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): """Tests Moderation utils.""" @@ -83,7 +89,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): "args": (self.user, "ban", "2020-02-26 09:20 (23 hours and 59 minutes)"), "expected_output": Embed( title=utils.INFRACTION_TITLE, - description=textwrap.shorten(utils.INFRACTION_DESCRIPTION_TEMPLATE.format( + description=textwrap.shorten(INFRACTION_DESCRIPTION_TEMPLATE.format( type="Ban", expires="2020-02-26 09:20 (23 hours and 59 minutes)", reason="No reason provided." @@ -101,7 +107,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): "args": (self.user, "warning", None, "Test reason."), "expected_output": Embed( title=utils.INFRACTION_TITLE, - description=textwrap.shorten(utils.INFRACTION_DESCRIPTION_TEMPLATE.format( + description=textwrap.shorten(INFRACTION_DESCRIPTION_TEMPLATE.format( type="Warning", expires="N/A", reason="Test reason." @@ -119,7 +125,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): "args": (self.user, "note", None, None, Icons.defcon_denied), "expected_output": Embed( title=utils.INFRACTION_TITLE, - description=textwrap.shorten(utils.INFRACTION_DESCRIPTION_TEMPLATE.format( + description=textwrap.shorten(INFRACTION_DESCRIPTION_TEMPLATE.format( type="Note", expires="N/A", reason="No reason provided." @@ -137,7 +143,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): "args": (self.user, "mute", "2020-02-26 09:20 (23 hours and 59 minutes)", "Test", Icons.defcon_denied), "expected_output": Embed( title=utils.INFRACTION_TITLE, - description=textwrap.shorten(utils.INFRACTION_DESCRIPTION_TEMPLATE.format( + description=textwrap.shorten(INFRACTION_DESCRIPTION_TEMPLATE.format( type="Mute", expires="2020-02-26 09:20 (23 hours and 59 minutes)", reason="Test" @@ -155,7 +161,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): "args": (self.user, "mute", None, "foo bar" * 4000, Icons.defcon_denied), "expected_output": Embed( title=utils.INFRACTION_TITLE, - description=textwrap.shorten(utils.INFRACTION_DESCRIPTION_TEMPLATE.format( + description=textwrap.shorten(INFRACTION_DESCRIPTION_TEMPLATE.format( type="Mute", expires="N/A", reason="foo bar" * 4000 -- cgit v1.2.3 From 9a80e9cf2fea30f9760f5fd0a2d2f21ad5c828b4 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Wed, 24 Jun 2020 15:01:05 +0300 Subject: Mod Utils Tests: Move some test cases to `namedtuple` --- tests/bot/cogs/moderation/test_utils.py | 95 ++++++++++----------------------- 1 file changed, 29 insertions(+), 66 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index dde5b438d..e54c0d240 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -1,5 +1,6 @@ import textwrap import unittest +from collections import namedtuple from datetime import datetime from unittest.mock import AsyncMock, MagicMock, call, patch @@ -32,29 +33,15 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): A message should be sent to the context indicating a user already has an infraction, if that's the case. """ + test_case = namedtuple("test_case", ["get_return_value", "expected_output", "infraction_nr", "send_msg"]) test_cases = [ - { - "get_return_value": [], - "expected_output": None, - "infraction_nr": None, - "send_msg": True - }, - { - "get_return_value": [{"id": 123987}], - "expected_output": {"id": 123987}, - "infraction_nr": "123987", - "send_msg": False - }, - { - "get_return_value": [{"id": 123987}], - "expected_output": {"id": 123987}, - "infraction_nr": "123987", - "send_msg": True - } + test_case([], None, None, True), + test_case([{"id": 123987}], {"id": 123987}, "123987", False), + test_case([{"id": 123987}], {"id": 123987}, "123987", True) ] for case in test_cases: - with self.subTest(return_value=case["get_return_value"], expected=case["expected_output"]): + with self.subTest(return_value=case.get_return_value, expected=case.expected_output): self.bot.api_client.get.reset_mock() self.ctx.send.reset_mock() @@ -64,15 +51,15 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): "user__id": str(self.member.id) } - self.bot.api_client.get.return_value = case["get_return_value"] + self.bot.api_client.get.return_value = case.get_return_value - result = await utils.get_active_infraction(self.ctx, self.member, "ban", send_msg=case["send_msg"]) - self.assertEqual(result, case["expected_output"]) + result = await utils.get_active_infraction(self.ctx, self.member, "ban", send_msg=case.send_msg) + self.assertEqual(result, case.expected_output) self.bot.api_client.get.assert_awaited_once_with("bot/infractions", params=params) - if case["send_msg"] and case["get_return_value"]: + if case.send_msg and case.get_return_value: self.ctx.send.assert_awaited_once() - self.assertTrue(case["infraction_nr"] in self.ctx.send.call_args[0][0]) + self.assertTrue(case.infraction_nr in self.ctx.send.call_args[0][0]) self.assertTrue("ban" in self.ctx.send.call_args[0][0]) else: self.ctx.send.assert_not_awaited() @@ -199,43 +186,33 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): @patch("bot.cogs.moderation.utils.send_private_embed") async def test_notify_pardon(self, send_private_embed_mock): """Should send an embed of a certain format as a DM and return `True` if DM successful.""" + test_case = namedtuple("test_case", ["args", "icon", "send_result"]) test_cases = [ - { - "args": (self.user, "Test title", "Example content"), - "icon": Icons.user_verified, - "send_result": True - }, - { - "args": (self.user, "Test title", "Example content", Icons.user_update), - "icon": Icons.user_update, - "send_result": False - } + test_case((self.user, "Test title", "Example content"), Icons.user_verified, True), + test_case((self.user, "Test title", "Example content", Icons.user_update), Icons.user_update, False) ] for case in test_cases: - args = case["args"] - send = case["send_result"] - expected = Embed( description="Example content", colour=Colours.soft_green ).set_author( name="Test title", - icon_url=case["icon"] + icon_url=case.icon ) - with self.subTest(args=args, expected=expected): + with self.subTest(args=case.args, expected=expected): send_private_embed_mock.reset_mock() - send_private_embed_mock.return_value = send + send_private_embed_mock.return_value = case.send_result - result = await utils.notify_pardon(*args) - self.assertEqual(send, result) + result = await utils.notify_pardon(*case.args) + self.assertEqual(case.send_result, result) embed = send_private_embed_mock.call_args[0][1] self.assertEqual(embed.to_dict(), expected.to_dict()) - send_private_embed_mock.assert_awaited_once_with(args[0], embed) + send_private_embed_mock.assert_awaited_once_with(case.args[0], embed) @patch("bot.cogs.moderation.utils.log") async def test_post_user(self, log_mock): @@ -316,37 +293,23 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): """Should DM the user and return `True` on success or `False` on failure.""" embed = Embed(title="Test", description="Test val") + test_case = namedtuple("test_case", ["expected_output", "raised_exception"]) test_cases = [ - { - "expected_output": True, - "raised_exception": None - }, - { - "expected_output": False, - "raised_exception": HTTPException(AsyncMock(), AsyncMock()) - }, - { - "expected_output": False, - "raised_exception": Forbidden(AsyncMock(), AsyncMock()) - }, - { - "expected_output": False, - "raised_exception": NotFound(AsyncMock(), AsyncMock()) - } + test_case(True, None), + test_case(False, HTTPException(AsyncMock(), AsyncMock())), + test_case(False, Forbidden(AsyncMock(), AsyncMock())), + test_case(False, NotFound(AsyncMock(), AsyncMock())) ] for case in test_cases: - expected = case["expected_output"] - raised = case["raised_exception"] - - with self.subTest(expected=expected, raised=raised): + with self.subTest(expected=case.expected_output, raised=case.raised_exception): self.user.send.reset_mock(side_effect=True) - self.user.send.side_effect = raised + self.user.send.side_effect = case.raised_exception result = await utils.send_private_embed(self.user, embed) - self.assertEqual(result, expected) - if expected: + self.assertEqual(result, case.expected_output) + if case.expected_output: self.user.send.assert_awaited_once_with(embed=embed) -- cgit v1.2.3 From 024633a470d86d84189c714d194e750507f47d47 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Wed, 24 Jun 2020 15:03:38 +0300 Subject: Mod Utils Tests: Change `True` assert to `In` assert for message check --- tests/bot/cogs/moderation/test_utils.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index e54c0d240..aaa0861e5 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -59,8 +59,9 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): if case.send_msg and case.get_return_value: self.ctx.send.assert_awaited_once() - self.assertTrue(case.infraction_nr in self.ctx.send.call_args[0][0]) - self.assertTrue("ban" in self.ctx.send.call_args[0][0]) + sent_message = self.ctx.send.call_args[0][0] + self.assertIn(case.infraction_nr, sent_message) + self.assertIn("ban", sent_message) else: self.ctx.send.assert_not_awaited() -- cgit v1.2.3 From c205f6303a6533cee6cb02cf85dba30b43e0630f Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Wed, 24 Jun 2020 15:04:31 +0300 Subject: Mod Utils Tests: Remove unnecessary `user` from test name --- tests/bot/cogs/moderation/test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index aaa0861e5..a104b969a 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -27,7 +27,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): self.user = MockUser(id=1234) self.ctx = MockContext(bot=self.bot, author=self.member) - async def test_user_get_active_infraction(self): + async def test_get_active_infraction(self): """ Should request the API for active infractions and return infraction if the user has one or `None` otherwise. -- cgit v1.2.3 From 2123cdb2f7f438491093ef0195cedd432466f9b8 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Wed, 24 Jun 2020 15:13:38 +0300 Subject: Remove case variable definitions in `test_notify_infraction` --- tests/bot/cogs/moderation/test_utils.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index a104b969a..7e8e6d9f0 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -166,23 +166,19 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): ] for case in test_cases: - args = case["args"] - expected = case["expected_output"] - send = case["send_result"] - - with self.subTest(args=args, expected=expected, send=send): + with self.subTest(args=case["args"], expected=case["expected_output"], send=case["send_result"]): send_private_embed_mock.reset_mock() - send_private_embed_mock.return_value = send - result = await utils.notify_infraction(*args) + send_private_embed_mock.return_value = case["send_result"] + result = await utils.notify_infraction(*case["args"]) - self.assertEqual(send, result) + self.assertEqual(case["send_result"], result) embed = send_private_embed_mock.call_args[0][1] - self.assertEqual(embed.to_dict(), expected.to_dict()) + self.assertEqual(embed.to_dict(), case["expected"].to_dict()) - send_private_embed_mock.assert_awaited_once_with(args[0], embed) + send_private_embed_mock.assert_awaited_once_with(case["args"][0], embed) @patch("bot.cogs.moderation.utils.send_private_embed") async def test_notify_pardon(self, send_private_embed_mock): -- cgit v1.2.3 From efde49c677650599b097955a1606dae0d122c97d Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Wed, 24 Jun 2020 15:17:53 +0300 Subject: Sync keys, variable names and kwargs in `test_post_user` --- tests/bot/cogs/moderation/test_utils.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index 7e8e6d9f0..b434737ea 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -256,32 +256,32 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): ] for case in test_cases: - test_user = case["user"] - expected = case["post_result"] - error = case["raise_error"] + user = case["user"] + post_result = case["post_result"] + raise_error = case["raise_error"] payload = case["payload"] - with self.subTest(user=test_user, result=expected, error=error, payload=payload): + with self.subTest(user=user, post_result=post_result, raise_error=raise_error, payload=payload): log_mock.reset_mock() self.bot.api_client.post.reset_mock(side_effect=True) - self.ctx.bot.api_client.post.return_value = expected + self.ctx.bot.api_client.post.return_value = post_result - self.ctx.bot.api_client.post.side_effect = error + self.ctx.bot.api_client.post.side_effect = raise_error - result = await utils.post_user(self.ctx, test_user) + result = await utils.post_user(self.ctx, user) - if error: + if raise_error: self.assertIsNone(result) else: - self.assertEqual(result, expected) + self.assertEqual(result, post_result) - if not error: + if not raise_error: self.bot.api_client.post.assert_awaited_once_with("bot/users", json=payload) else: self.ctx.send.assert_awaited_once() - self.assertTrue(str(error.status) in self.ctx.send.call_args[0][0]) + self.assertTrue(str(raise_error.status) in self.ctx.send.call_args[0][0]) - if isinstance(test_user, MagicMock): + if isinstance(user, MagicMock): log_mock.debug.assert_called_once() else: log_mock.debug.assert_not_called() -- cgit v1.2.3 From 3d4c50c498647a6537eef747e84690f8852d388c Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Wed, 24 Jun 2020 15:18:49 +0300 Subject: Replace `True` test with `In` test on `test_post_user` --- tests/bot/cogs/moderation/test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index b434737ea..5be703bc6 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -279,7 +279,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): self.bot.api_client.post.assert_awaited_once_with("bot/users", json=payload) else: self.ctx.send.assert_awaited_once() - self.assertTrue(str(raise_error.status) in self.ctx.send.call_args[0][0]) + self.assertIn(str(raise_error.status), self.ctx.send.call_args[0][0]) if isinstance(user, MagicMock): log_mock.debug.assert_called_once() -- cgit v1.2.3 From 4430e590ece503c262419324e2bc47dbaa5823d2 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Wed, 24 Jun 2020 15:21:00 +0300 Subject: Merge 2 if-else branches is `test_post_user` --- tests/bot/cogs/moderation/test_utils.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index 5be703bc6..f4c634936 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -272,14 +272,11 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): if raise_error: self.assertIsNone(result) + self.ctx.send.assert_awaited_once() + self.assertIn(str(raise_error.status), self.ctx.send.call_args[0][0]) else: self.assertEqual(result, post_result) - - if not raise_error: self.bot.api_client.post.assert_awaited_once_with("bot/users", json=payload) - else: - self.ctx.send.assert_awaited_once() - self.assertIn(str(raise_error.status), self.ctx.send.call_args[0][0]) if isinstance(user, MagicMock): log_mock.debug.assert_called_once() -- cgit v1.2.3 From 136ebd22a73318620e8a3fa6136d28f5390ddeaf Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Wed, 24 Jun 2020 15:22:18 +0300 Subject: Remove unnecessary `log.debug` assert in `test_post_user` --- tests/bot/cogs/moderation/test_utils.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index f4c634936..e6eac6831 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -211,8 +211,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): send_private_embed_mock.assert_awaited_once_with(case.args[0], embed) - @patch("bot.cogs.moderation.utils.log") - async def test_post_user(self, log_mock): + async def test_post_user(self): """Should POST a new user and return the response if successful or otherwise send an error message.""" user = MockUser(discriminator=5678, id=1234, name="Test user") some_mock = MagicMock(discriminator=3333) @@ -262,7 +261,6 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): payload = case["payload"] with self.subTest(user=user, post_result=post_result, raise_error=raise_error, payload=payload): - log_mock.reset_mock() self.bot.api_client.post.reset_mock(side_effect=True) self.ctx.bot.api_client.post.return_value = post_result @@ -278,11 +276,6 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): self.assertEqual(result, post_result) self.bot.api_client.post.assert_awaited_once_with("bot/users", json=payload) - if isinstance(user, MagicMock): - log_mock.debug.assert_called_once() - else: - log_mock.debug.assert_not_called() - async def test_send_private_embed(self): """Should DM the user and return `True` on success or `False` on failure.""" embed = Embed(title="Test", description="Test val") -- cgit v1.2.3 From b2a70712ac4aaa067edfbb7a8940cf9b78f44e53 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Wed, 24 Jun 2020 15:28:22 +0300 Subject: Add other parameters to `test_post_user` `not_user` mock --- tests/bot/cogs/moderation/test_utils.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index e6eac6831..f89f41d25 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -214,7 +214,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): async def test_post_user(self): """Should POST a new user and return the response if successful or otherwise send an error message.""" user = MockUser(discriminator=5678, id=1234, name="Test user") - some_mock = MagicMock(discriminator=3333) + not_user = MagicMock(discriminator=3333, id=5678, name="Wrong user") test_cases = [ { "user": user, @@ -241,14 +241,14 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): } }, { - "user": some_mock, + "user": not_user, "post_result": "bar", "raise_error": None, "payload": { - "discriminator": some_mock.discriminator, - "id": some_mock.id, + "discriminator": not_user.discriminator, + "id": not_user.id, "in_guild": False, - "name": some_mock.name, + "name": not_user.name, "roles": [] } } -- cgit v1.2.3 From 9b1538878221c966b62dc5c9d0be2af1fd475325 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Wed, 24 Jun 2020 15:29:52 +0300 Subject: Fix test case key name in `test_notify_infraction` --- tests/bot/cogs/moderation/test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index f89f41d25..c4d0d6f16 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -176,7 +176,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): embed = send_private_embed_mock.call_args[0][1] - self.assertEqual(embed.to_dict(), case["expected"].to_dict()) + self.assertEqual(embed.to_dict(), case["expected_output"].to_dict()) send_private_embed_mock.assert_awaited_once_with(case["args"][0], embed) -- cgit v1.2.3 From 7b89d2cfad91cc9a56565ebc7700f4858814f149 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Wed, 24 Jun 2020 15:36:34 +0300 Subject: Move infraction description template back to main file, apply it there --- bot/cogs/moderation/utils.py | 18 +++++++++++++----- tests/bot/cogs/moderation/test_utils.py | 16 +++++----------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/bot/cogs/moderation/utils.py b/bot/cogs/moderation/utils.py index 104baf528..cbef3420a 100644 --- a/bot/cogs/moderation/utils.py +++ b/bot/cogs/moderation/utils.py @@ -34,6 +34,12 @@ INFRACTION_TITLE = f"Please review our rules over at {RULES_URL}" INFRACTION_APPEAL_FOOTER = f"To appeal this infraction, send an e-mail to {APPEAL_EMAIL}" INFRACTION_AUTHOR_NAME = "Infraction information" +INFRACTION_DESCRIPTION_TEMPLATE = ( + "\n**Type:** {type}\n" + "**Expires:** {expires}\n" + "**Reason:** {reason}\n" +) + async def post_user(ctx: Context, user: UserSnowflake) -> t.Optional[dict]: """ @@ -148,11 +154,13 @@ async def notify_infraction( """DM a user about their new infraction and return True if the DM is successful.""" log.trace(f"Sending {user} a DM about their {infr_type} infraction.") - text = textwrap.dedent(f""" - **Type:** {infr_type.capitalize()} - **Expires:** {expires_at or "N/A"} - **Reason:** {reason or "No reason provided."} - """) + text = textwrap.dedent( + INFRACTION_DESCRIPTION_TEMPLATE.format( + type=infr_type.capitalize(), + expires=expires_at or "N/A", + reason=reason or "No reason provided." + ) + ) embed = discord.Embed( description=textwrap.shorten(text, width=2048, placeholder="..."), diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index c4d0d6f16..c35c0edf5 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -11,12 +11,6 @@ from bot.cogs.moderation import utils from bot.constants import Colours, Icons from tests.helpers import MockBot, MockContext, MockMember, MockUser -INFRACTION_DESCRIPTION_TEMPLATE = ( - "\n**Type:** {type}\n" - "**Expires:** {expires}\n" - "**Reason:** {reason}\n" -) - class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): """Tests Moderation utils.""" @@ -77,7 +71,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): "args": (self.user, "ban", "2020-02-26 09:20 (23 hours and 59 minutes)"), "expected_output": Embed( title=utils.INFRACTION_TITLE, - description=textwrap.shorten(INFRACTION_DESCRIPTION_TEMPLATE.format( + description=textwrap.shorten(utils.INFRACTION_DESCRIPTION_TEMPLATE.format( type="Ban", expires="2020-02-26 09:20 (23 hours and 59 minutes)", reason="No reason provided." @@ -95,7 +89,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): "args": (self.user, "warning", None, "Test reason."), "expected_output": Embed( title=utils.INFRACTION_TITLE, - description=textwrap.shorten(INFRACTION_DESCRIPTION_TEMPLATE.format( + description=textwrap.shorten(utils.INFRACTION_DESCRIPTION_TEMPLATE.format( type="Warning", expires="N/A", reason="Test reason." @@ -113,7 +107,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): "args": (self.user, "note", None, None, Icons.defcon_denied), "expected_output": Embed( title=utils.INFRACTION_TITLE, - description=textwrap.shorten(INFRACTION_DESCRIPTION_TEMPLATE.format( + description=textwrap.shorten(utils.INFRACTION_DESCRIPTION_TEMPLATE.format( type="Note", expires="N/A", reason="No reason provided." @@ -131,7 +125,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): "args": (self.user, "mute", "2020-02-26 09:20 (23 hours and 59 minutes)", "Test", Icons.defcon_denied), "expected_output": Embed( title=utils.INFRACTION_TITLE, - description=textwrap.shorten(INFRACTION_DESCRIPTION_TEMPLATE.format( + description=textwrap.shorten(utils.INFRACTION_DESCRIPTION_TEMPLATE.format( type="Mute", expires="2020-02-26 09:20 (23 hours and 59 minutes)", reason="Test" @@ -149,7 +143,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): "args": (self.user, "mute", None, "foo bar" * 4000, Icons.defcon_denied), "expected_output": Embed( title=utils.INFRACTION_TITLE, - description=textwrap.shorten(INFRACTION_DESCRIPTION_TEMPLATE.format( + description=textwrap.shorten(utils.INFRACTION_DESCRIPTION_TEMPLATE.format( type="Mute", expires="N/A", reason="foo bar" * 4000 -- cgit v1.2.3 From 5aaa7df5e72c5063b1eb59fe71c7dea745f18f48 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Wed, 24 Jun 2020 15:39:08 +0300 Subject: Remove unnecessary `textwrap.dedent` in `notify_infraction` --- bot/cogs/moderation/utils.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/bot/cogs/moderation/utils.py b/bot/cogs/moderation/utils.py index cbef3420a..8b36210be 100644 --- a/bot/cogs/moderation/utils.py +++ b/bot/cogs/moderation/utils.py @@ -154,12 +154,10 @@ async def notify_infraction( """DM a user about their new infraction and return True if the DM is successful.""" log.trace(f"Sending {user} a DM about their {infr_type} infraction.") - text = textwrap.dedent( - INFRACTION_DESCRIPTION_TEMPLATE.format( - type=infr_type.capitalize(), - expires=expires_at or "N/A", - reason=reason or "No reason provided." - ) + text = INFRACTION_DESCRIPTION_TEMPLATE.format( + type=infr_type.capitalize(), + expires=expires_at or "N/A", + reason=reason or "No reason provided." ) embed = discord.Embed( -- cgit v1.2.3 From 0c9fc3a1bbaf590d7ccf8737ffffcfb4b1b5b1b8 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Wed, 24 Jun 2020 15:40:46 +0300 Subject: Reorder tests order to match with original file --- tests/bot/cogs/moderation/test_utils.py | 130 ++++++++++++++++---------------- 1 file changed, 65 insertions(+), 65 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index c35c0edf5..0f6f9c469 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -21,6 +21,71 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): self.user = MockUser(id=1234) self.ctx = MockContext(bot=self.bot, author=self.member) + async def test_post_user(self): + """Should POST a new user and return the response if successful or otherwise send an error message.""" + user = MockUser(discriminator=5678, id=1234, name="Test user") + not_user = MagicMock(discriminator=3333, id=5678, name="Wrong user") + test_cases = [ + { + "user": user, + "post_result": "bar", + "raise_error": None, + "payload": { + "discriminator": 5678, + "id": self.user.id, + "in_guild": False, + "name": "Test user", + "roles": [] + } + }, + { + "user": self.member, + "post_result": "foo", + "raise_error": ResponseCodeError(MagicMock(status=400), "foo"), + "payload": { + "discriminator": 0, + "id": self.member.id, + "in_guild": False, + "name": "Name unknown", + "roles": [] + } + }, + { + "user": not_user, + "post_result": "bar", + "raise_error": None, + "payload": { + "discriminator": not_user.discriminator, + "id": not_user.id, + "in_guild": False, + "name": not_user.name, + "roles": [] + } + } + ] + + for case in test_cases: + user = case["user"] + post_result = case["post_result"] + raise_error = case["raise_error"] + payload = case["payload"] + + with self.subTest(user=user, post_result=post_result, raise_error=raise_error, payload=payload): + self.bot.api_client.post.reset_mock(side_effect=True) + self.ctx.bot.api_client.post.return_value = post_result + + self.ctx.bot.api_client.post.side_effect = raise_error + + result = await utils.post_user(self.ctx, user) + + if raise_error: + self.assertIsNone(result) + self.ctx.send.assert_awaited_once() + self.assertIn(str(raise_error.status), self.ctx.send.call_args[0][0]) + else: + self.assertEqual(result, post_result) + self.bot.api_client.post.assert_awaited_once_with("bot/users", json=payload) + async def test_get_active_infraction(self): """ Should request the API for active infractions and return infraction if the user has one or `None` otherwise. @@ -205,71 +270,6 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): send_private_embed_mock.assert_awaited_once_with(case.args[0], embed) - async def test_post_user(self): - """Should POST a new user and return the response if successful or otherwise send an error message.""" - user = MockUser(discriminator=5678, id=1234, name="Test user") - not_user = MagicMock(discriminator=3333, id=5678, name="Wrong user") - test_cases = [ - { - "user": user, - "post_result": "bar", - "raise_error": None, - "payload": { - "discriminator": 5678, - "id": self.user.id, - "in_guild": False, - "name": "Test user", - "roles": [] - } - }, - { - "user": self.member, - "post_result": "foo", - "raise_error": ResponseCodeError(MagicMock(status=400), "foo"), - "payload": { - "discriminator": 0, - "id": self.member.id, - "in_guild": False, - "name": "Name unknown", - "roles": [] - } - }, - { - "user": not_user, - "post_result": "bar", - "raise_error": None, - "payload": { - "discriminator": not_user.discriminator, - "id": not_user.id, - "in_guild": False, - "name": not_user.name, - "roles": [] - } - } - ] - - for case in test_cases: - user = case["user"] - post_result = case["post_result"] - raise_error = case["raise_error"] - payload = case["payload"] - - with self.subTest(user=user, post_result=post_result, raise_error=raise_error, payload=payload): - self.bot.api_client.post.reset_mock(side_effect=True) - self.ctx.bot.api_client.post.return_value = post_result - - self.ctx.bot.api_client.post.side_effect = raise_error - - result = await utils.post_user(self.ctx, user) - - if raise_error: - self.assertIsNone(result) - self.ctx.send.assert_awaited_once() - self.assertIn(str(raise_error.status), self.ctx.send.call_args[0][0]) - else: - self.assertEqual(result, post_result) - self.bot.api_client.post.assert_awaited_once_with("bot/users", json=payload) - async def test_send_private_embed(self): """Should DM the user and return `True` on success or `False` on failure.""" embed = Embed(title="Test", description="Test val") -- cgit v1.2.3 From 1a812b7c3ef7048d8058c8c5a7d5e3afd0f86317 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Thu, 25 Jun 2020 11:48:02 +0300 Subject: Remove unnecessary if statement from send_private_embed test --- tests/bot/cogs/moderation/test_utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index 0f6f9c469..029719669 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -290,8 +290,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): result = await utils.send_private_embed(self.user, embed) self.assertEqual(result, case.expected_output) - if case.expected_output: - self.user.send.assert_awaited_once_with(embed=embed) + self.user.send.assert_awaited_once_with(embed=embed) class TestPostInfraction(unittest.IsolatedAsyncioTestCase): -- cgit v1.2.3 From 604c6a7a09d7826870fb384b98e0a6d1463721b4 Mon Sep 17 00:00:00 2001 From: Karlis S Date: Mon, 6 Jul 2020 14:24:55 +0000 Subject: Restore newlines for `notify_infraction` embed description Truncate reason instead full content to avoid removing newlines --- bot/cogs/moderation/utils.py | 6 +++--- tests/bot/cogs/moderation/test_utils.py | 22 +++++++++++----------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/bot/cogs/moderation/utils.py b/bot/cogs/moderation/utils.py index 8b36210be..95820404a 100644 --- a/bot/cogs/moderation/utils.py +++ b/bot/cogs/moderation/utils.py @@ -35,7 +35,7 @@ INFRACTION_APPEAL_FOOTER = f"To appeal this infraction, send an e-mail to {APPEA INFRACTION_AUTHOR_NAME = "Infraction information" INFRACTION_DESCRIPTION_TEMPLATE = ( - "\n**Type:** {type}\n" + "**Type:** {type}\n" "**Expires:** {expires}\n" "**Reason:** {reason}\n" ) @@ -157,11 +157,11 @@ async def notify_infraction( text = INFRACTION_DESCRIPTION_TEMPLATE.format( type=infr_type.capitalize(), expires=expires_at or "N/A", - reason=reason or "No reason provided." + reason=textwrap.shorten(reason, 1000, placeholder="...") if reason else "No reason provided." ) embed = discord.Embed( - description=textwrap.shorten(text, width=2048, placeholder="..."), + description=text, colour=Colours.soft_red ) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index 029719669..c9a4e4040 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -136,11 +136,11 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): "args": (self.user, "ban", "2020-02-26 09:20 (23 hours and 59 minutes)"), "expected_output": Embed( title=utils.INFRACTION_TITLE, - description=textwrap.shorten(utils.INFRACTION_DESCRIPTION_TEMPLATE.format( + description=utils.INFRACTION_DESCRIPTION_TEMPLATE.format( type="Ban", expires="2020-02-26 09:20 (23 hours and 59 minutes)", reason="No reason provided." - ), width=2048, placeholder="..."), + ), colour=Colours.soft_red, url=utils.RULES_URL ).set_author( @@ -154,11 +154,11 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): "args": (self.user, "warning", None, "Test reason."), "expected_output": Embed( title=utils.INFRACTION_TITLE, - description=textwrap.shorten(utils.INFRACTION_DESCRIPTION_TEMPLATE.format( + description=utils.INFRACTION_DESCRIPTION_TEMPLATE.format( type="Warning", expires="N/A", reason="Test reason." - ), width=2048, placeholder="..."), + ), colour=Colours.soft_red, url=utils.RULES_URL ).set_author( @@ -172,11 +172,11 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): "args": (self.user, "note", None, None, Icons.defcon_denied), "expected_output": Embed( title=utils.INFRACTION_TITLE, - description=textwrap.shorten(utils.INFRACTION_DESCRIPTION_TEMPLATE.format( + description=utils.INFRACTION_DESCRIPTION_TEMPLATE.format( type="Note", expires="N/A", reason="No reason provided." - ), width=2048, placeholder="..."), + ), colour=Colours.soft_red, url=utils.RULES_URL ).set_author( @@ -190,11 +190,11 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): "args": (self.user, "mute", "2020-02-26 09:20 (23 hours and 59 minutes)", "Test", Icons.defcon_denied), "expected_output": Embed( title=utils.INFRACTION_TITLE, - description=textwrap.shorten(utils.INFRACTION_DESCRIPTION_TEMPLATE.format( + description=utils.INFRACTION_DESCRIPTION_TEMPLATE.format( type="Mute", expires="2020-02-26 09:20 (23 hours and 59 minutes)", reason="Test" - ), width=2048, placeholder="..."), + ), colour=Colours.soft_red, url=utils.RULES_URL ).set_author( @@ -208,11 +208,11 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): "args": (self.user, "mute", None, "foo bar" * 4000, Icons.defcon_denied), "expected_output": Embed( title=utils.INFRACTION_TITLE, - description=textwrap.shorten(utils.INFRACTION_DESCRIPTION_TEMPLATE.format( + description=utils.INFRACTION_DESCRIPTION_TEMPLATE.format( type="Mute", expires="N/A", - reason="foo bar" * 4000 - ), width=2048, placeholder="..."), + reason=textwrap.shorten("foo bar" * 4000, 1000, placeholder="...") + ), colour=Colours.soft_red, url=utils.RULES_URL ).set_author( -- cgit v1.2.3 From b1777fb0d93ce329c7dc4120d510ebc81ede2920 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 20 Jul 2020 09:30:26 -0700 Subject: Clean up imports --- bot/utils/messages.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/bot/utils/messages.py b/bot/utils/messages.py index 670289941..63bda877c 100644 --- a/bot/utils/messages.py +++ b/bot/utils/messages.py @@ -6,8 +6,7 @@ import re from io import BytesIO from typing import List, Optional, Sequence, Union -from discord import Client, Colour, Embed, File, Member, Message, Reaction, TextChannel, Webhook -from discord.abc import Snowflake +import discord from discord.errors import HTTPException from discord.ext.commands import Context @@ -17,12 +16,12 @@ log = logging.getLogger(__name__) async def wait_for_deletion( - message: Message, - user_ids: Sequence[Snowflake], + message: discord.Message, + user_ids: Sequence[discord.abc.Snowflake], deletion_emojis: Sequence[str] = (Emojis.trashcan,), timeout: float = 60 * 5, attach_emojis: bool = True, - client: Optional[Client] = None + client: Optional[discord.Client] = None ) -> None: """ Wait for up to `timeout` seconds for a reaction by any of the specified `user_ids` to delete the message. @@ -42,7 +41,7 @@ async def wait_for_deletion( for emoji in deletion_emojis: await message.add_reaction(emoji) - def check(reaction: Reaction, user: Member) -> bool: + def check(reaction: discord.Reaction, user: discord.Member) -> bool: """Check that the deletion emoji is reacted by the appropriate user.""" return ( reaction.message.id == message.id @@ -56,8 +55,8 @@ async def wait_for_deletion( async def send_attachments( - message: Message, - destination: Union[TextChannel, Webhook], + message: discord.Message, + destination: Union[discord.TextChannel, discord.Webhook], link_large: bool = True ) -> List[str]: """ @@ -81,9 +80,9 @@ async def send_attachments( if attachment.size <= destination.guild.filesize_limit - 512: with BytesIO() as file: await attachment.save(file, use_cached=True) - attachment_file = File(file, filename=attachment.filename) + attachment_file = discord.File(file, filename=attachment.filename) - if isinstance(destination, TextChannel): + if isinstance(destination, discord.TextChannel): msg = await destination.send(file=attachment_file) urls.append(msg.attachments[0].url) else: @@ -104,10 +103,10 @@ async def send_attachments( if link_large and large: desc = "\n".join(f"[{attachment.filename}]({attachment.url})" for attachment in large) - embed = Embed(description=desc) + embed = discord.Embed(description=desc) embed.set_footer(text="Attachments exceed upload size limit.") - if isinstance(destination, TextChannel): + if isinstance(destination, discord.TextChannel): await destination.send(embed=embed) else: await destination.send( @@ -138,8 +137,8 @@ def sub_clyde(username: Optional[str]) -> Optional[str]: async def send_denial(ctx: Context, reason: str) -> None: """Send an embed denying the user with the given reason.""" - embed = Embed() - embed.colour = Colour.red() + embed = discord.Embed() + embed.colour = discord.Colour.red() embed.title = random.choice(NEGATIVE_REPLIES) embed.description = reason -- cgit v1.2.3 From d35c603c8fb252335d58451d2310fd2b55585e22 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 20 Jul 2020 10:31:41 -0700 Subject: Add util function to format user names This will be used a lot when sending mod logs and will help with reducing redundancy and maintaining consistency. --- bot/utils/messages.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/bot/utils/messages.py b/bot/utils/messages.py index 63bda877c..31825d4a7 100644 --- a/bot/utils/messages.py +++ b/bot/utils/messages.py @@ -9,6 +9,7 @@ from typing import List, Optional, Sequence, Union import discord from discord.errors import HTTPException from discord.ext.commands import Context +from discord.utils import escape_markdown from bot.constants import Emojis, NEGATIVE_REPLIES @@ -143,3 +144,9 @@ async def send_denial(ctx: Context, reason: str) -> None: embed.description = reason await ctx.send(embed=embed) + + +def format_user(user: discord.abc.User) -> str: + """Return a string for `user` which has their mention and name#discriminator.""" + name = escape_markdown(str(user)) + return f"{user.mention} ({name})" -- cgit v1.2.3 From 090b42d0c46957091681bc8f4666523513ccdc28 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 20 Jul 2020 11:55:02 -0700 Subject: Superstarify: use user mentions in mod logs `format_user` isn't used in the apply mod log cause it already shows both the old and new nicknames elsewhere. --- bot/cogs/moderation/superstarify.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bot/cogs/moderation/superstarify.py b/bot/cogs/moderation/superstarify.py index 867de815a..f22e7e741 100644 --- a/bot/cogs/moderation/superstarify.py +++ b/bot/cogs/moderation/superstarify.py @@ -12,6 +12,7 @@ from bot import constants from bot.bot import Bot from bot.converters import Expiry from bot.utils.checks import with_role_check +from bot.utils.messages import format_user from bot.utils.time import format_infraction from . import utils from .scheduler import InfractionScheduler @@ -181,8 +182,8 @@ class Superstarify(InfractionScheduler, Cog): title="Member achieved superstardom", thumbnail=member.avatar_url_as(static_format="png"), text=textwrap.dedent(f""" - Member: {member.mention} (`{member.id}`) - Actor: {ctx.message.author} + Member: {member.mention} + Actor: {ctx.message.author.mention} Expires: {expiry_str} Old nickname: `{old_nick}` New nickname: `{forced_nick}` @@ -221,7 +222,7 @@ class Superstarify(InfractionScheduler, Cog): ) return { - "Member": f"{user.mention}(`{user.id}`)", + "Member": format_user(user), "DM": "Sent" if notified else "**Failed**" } -- cgit v1.2.3 From 830e231b681315b74480a312111e5cabc5ca2167 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 20 Jul 2020 11:55:45 -0700 Subject: Superstarify: escape Markdown in nicknames --- bot/cogs/moderation/superstarify.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bot/cogs/moderation/superstarify.py b/bot/cogs/moderation/superstarify.py index f22e7e741..b23588b1c 100644 --- a/bot/cogs/moderation/superstarify.py +++ b/bot/cogs/moderation/superstarify.py @@ -7,6 +7,7 @@ from pathlib import Path from discord import Colour, Embed, Member from discord.ext.commands import Cog, Context, command +from discord.utils import escape_markdown from bot import constants from bot.bot import Bot @@ -139,7 +140,6 @@ class Superstarify(InfractionScheduler, Cog): infraction = await utils.post_infraction(ctx, member, "superstar", reason, duration, active=True) id_ = infraction["id"] - old_nick = member.display_name forced_nick = self.get_nick(id_, member.id) expiry_str = format_infraction(infraction["expires_at"]) @@ -149,6 +149,9 @@ class Superstarify(InfractionScheduler, Cog): await member.edit(nick=forced_nick, reason=reason) self.schedule_expiration(infraction) + old_nick = escape_markdown(member.display_name) + forced_nick = escape_markdown(forced_nick) + # Send a DM to the user to notify them of their new infraction. await utils.notify_infraction( user=member, -- cgit v1.2.3 From dfd2d8c1d0a21d92ea0a0875650c814e6fbbff4f Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 20 Jul 2020 12:06:31 -0700 Subject: Moderation: remove multiple active infractions check The API was change a long time ago to not allow such a situation. --- bot/cogs/moderation/scheduler.py | 36 ------------------------------------ 1 file changed, 36 deletions(-) diff --git a/bot/cogs/moderation/scheduler.py b/bot/cogs/moderation/scheduler.py index 601e238c9..0b0bc9eb7 100644 --- a/bot/cogs/moderation/scheduler.py +++ b/bot/cogs/moderation/scheduler.py @@ -243,42 +243,6 @@ class InfractionScheduler: id_ = response[0]['id'] footer = f"ID: {id_}" - # If multiple active infractions were found, mark them as inactive in the database - # and cancel their expiration tasks. - if len(response) > 1: - log.info( - f"Found more than one active {infr_type} infraction for user {user.id}; " - "deactivating the extra active infractions too." - ) - - footer = f"Infraction IDs: {', '.join(str(infr['id']) for infr in response)}" - - log_note = f"Found multiple **active** {infr_type} infractions in the database." - if "Note" in log_text: - log_text["Note"] = f" {log_note}" - else: - log_text["Note"] = log_note - - # deactivate_infraction() is not called again because: - # 1. Discord cannot store multiple active bans or assign multiples of the same role - # 2. It would send a pardon DM for each active infraction, which is redundant - for infraction in response[1:]: - id_ = infraction['id'] - try: - # Mark infraction as inactive in the database. - await self.bot.api_client.patch( - f"bot/infractions/{id_}", - json={"active": False} - ) - except ResponseCodeError: - log.exception(f"Failed to deactivate infraction #{id_} ({infr_type})") - # This is simpler and cleaner than trying to concatenate all the errors. - log_text["Failure"] = "See bot's logs for details." - - # Cancel pending expiration task. - if infraction["expires_at"] is not None: - self.scheduler.cancel(infraction["id"]) - # Accordingly display whether the user was successfully notified via DM. dm_emoji = "" if log_text.get("DM") == "Sent": -- cgit v1.2.3 From b5dce5b6d5ee096a3fd4daec0e72405159aee87d Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 20 Jul 2020 14:44:04 -0700 Subject: Moderation: use user mentions in mod logs --- bot/cogs/moderation/infractions.py | 3 ++- bot/cogs/moderation/management.py | 14 +++++--------- bot/cogs/moderation/scheduler.py | 15 +++++++-------- 3 files changed, 14 insertions(+), 18 deletions(-) diff --git a/bot/cogs/moderation/infractions.py b/bot/cogs/moderation/infractions.py index 8df642428..5404991e8 100644 --- a/bot/cogs/moderation/infractions.py +++ b/bot/cogs/moderation/infractions.py @@ -13,6 +13,7 @@ from bot.constants import Event from bot.converters import Expiry, FetchedMember from bot.decorators import respect_role_hierarchy from bot.utils.checks import with_role_check +from bot.utils.messages import format_user from . import utils from .scheduler import InfractionScheduler from .utils import UserSnowflake @@ -316,7 +317,7 @@ class Infractions(InfractionScheduler, commands.Cog): icon_url=utils.INFRACTION_ICONS["mute"][1] ) - log_text["Member"] = f"{user.mention}(`{user.id}`)" + log_text["Member"] = format_user(user) log_text["DM"] = "Sent" if notified else "**Failed**" else: log.info(f"Failed to unmute user {user_id}: user not found") diff --git a/bot/cogs/moderation/management.py b/bot/cogs/moderation/management.py index 672bb0e9c..e0a86ee59 100644 --- a/bot/cogs/moderation/management.py +++ b/bot/cogs/moderation/management.py @@ -11,7 +11,7 @@ from bot import constants from bot.bot import Bot from bot.converters import Expiry, InfractionSearchQuery, allowed_strings, proxy_user from bot.pagination import LinePaginator -from bot.utils import time +from bot.utils import messages, time from bot.utils.checks import in_whitelist_check, with_role_check from . import utils from .infractions import Infractions @@ -154,16 +154,12 @@ class ModManagement(commands.Cog): user = ctx.guild.get_member(user_id) if user: - user_text = f"{user.mention} (`{user.id}`)" + user_text = messages.format_user(user) thumbnail = user.avatar_url_as(static_format="png") else: - user_text = f"`{user_id}`" + user_text = f"<@{user_id}>" thumbnail = None - # The infraction's actor - actor_id = new_infraction['actor'] - actor = ctx.guild.get_member(actor_id) or f"`{actor_id}`" - await self.mod_log.send_log_message( icon_url=constants.Icons.pencil, colour=discord.Colour.blurple(), @@ -171,8 +167,8 @@ class ModManagement(commands.Cog): thumbnail=thumbnail, text=textwrap.dedent(f""" Member: {user_text} - Actor: {actor} - Edited by: {ctx.message.author}{log_text} + Actor: <@{new_infraction['actor']}> + Edited by: {ctx.message.author.mention}{log_text} """) ) diff --git a/bot/cogs/moderation/scheduler.py b/bot/cogs/moderation/scheduler.py index 0b0bc9eb7..6323bd55a 100644 --- a/bot/cogs/moderation/scheduler.py +++ b/bot/cogs/moderation/scheduler.py @@ -13,8 +13,7 @@ from bot import constants from bot.api import ResponseCodeError from bot.bot import Bot from bot.constants import Colours, STAFF_CHANNELS -from bot.utils import time -from bot.utils.scheduling import Scheduler +from bot.utils import messages, scheduling, time from . import utils from .modlog import ModLog from .utils import UserSnowflake @@ -27,7 +26,7 @@ class InfractionScheduler: def __init__(self, bot: Bot, supported_infractions: t.Container[str]): self.bot = bot - self.scheduler = Scheduler(self.__class__.__name__) + self.scheduler = scheduling.Scheduler(self.__class__.__name__) self.bot.loop.create_task(self.reschedule_infractions(supported_infractions)) @@ -193,8 +192,8 @@ class InfractionScheduler: title=f"Infraction {log_title}: {infr_type}", thumbnail=user.avatar_url_as(static_format="png"), text=textwrap.dedent(f""" - Member: {user.mention} (`{user.id}`) - Actor: {ctx.message.author}{dm_log_text}{expiry_log_text} + Member: {messages.format_user(user)} + Actor: {ctx.message.author.mention}{dm_log_text}{expiry_log_text} Reason: {reason} """), content=log_content, @@ -237,8 +236,8 @@ class InfractionScheduler: # Deactivate the infraction and cancel its scheduled expiration task. log_text = await self.deactivate_infraction(response[0], send_log=False) - log_text["Member"] = f"{user.mention}(`{user.id}`)" - log_text["Actor"] = str(ctx.message.author) + log_text["Member"] = messages.format_user(user) + log_text["Actor"] = ctx.message.author.mention log_content = None id_ = response[0]['id'] footer = f"ID: {id_}" @@ -316,7 +315,7 @@ class InfractionScheduler: log_content = None log_text = { "Member": f"<@{user_id}>", - "Actor": str(self.bot.get_user(actor) or actor), + "Actor": f"<@{actor}>", "Reason": infraction["reason"], "Created": created, } -- cgit v1.2.3 From 1a87af51c2484b455e546af976f2a427c9134e72 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 20 Jul 2020 14:10:22 -0700 Subject: Use user mentions in mod logs --- bot/cogs/antispam.py | 4 ++-- bot/cogs/clean.py | 3 ++- bot/cogs/defcon.py | 3 ++- bot/cogs/token_remover.py | 6 +++--- bot/cogs/verification.py | 3 ++- bot/cogs/webhook_remover.py | 5 +++-- 6 files changed, 14 insertions(+), 10 deletions(-) diff --git a/bot/cogs/antispam.py b/bot/cogs/antispam.py index 0bcca578d..e6fcb079c 100644 --- a/bot/cogs/antispam.py +++ b/bot/cogs/antispam.py @@ -19,7 +19,7 @@ from bot.constants import ( STAFF_ROLES, ) from bot.converters import Duration -from bot.utils.messages import send_attachments +from bot.utils.messages import format_user, send_attachments log = logging.getLogger(__name__) @@ -67,7 +67,7 @@ class DeletionContext: async def upload_messages(self, actor_id: int, modlog: ModLog) -> None: """Method that takes care of uploading the queue and posting modlog alert.""" - triggered_by_users = ", ".join(f"{m} (`{m.id}`)" for m in self.members.values()) + triggered_by_users = ", ".join(format_user(m) for m in self.members.values()) mod_alert_message = ( f"**Triggered by:** {triggered_by_users}\n" diff --git a/bot/cogs/clean.py b/bot/cogs/clean.py index f436e531a..c36ff3aba 100644 --- a/bot/cogs/clean.py +++ b/bot/cogs/clean.py @@ -179,7 +179,8 @@ class Clean(Cog): target_channels = ", ".join(channel.mention for channel in channels) message = ( - f"**{len(message_ids)}** messages deleted in {target_channels} by **{ctx.author.name}**\n\n" + f"**{len(message_ids)}** messages deleted in {target_channels} by " + f"{ctx.author.name.mention}\n\n" f"A log of the deleted messages can be found [here]({log_url})." ) diff --git a/bot/cogs/defcon.py b/bot/cogs/defcon.py index 4c0ad5914..a7dd4670e 100644 --- a/bot/cogs/defcon.py +++ b/bot/cogs/defcon.py @@ -12,6 +12,7 @@ from bot.bot import Bot from bot.cogs.moderation import ModLog from bot.constants import Channels, Colours, Emojis, Event, Icons, Roles from bot.decorators import with_role +from bot.utils.messages import format_user log = logging.getLogger(__name__) @@ -107,7 +108,7 @@ class Defcon(Cog): self.bot.stats.incr("defcon.leaves") message = ( - f"{member} (`{member.id}`) was denied entry because their account is too new." + f"{format_user(member)} was denied entry because their account is too new." ) if not message_sent: diff --git a/bot/cogs/token_remover.py b/bot/cogs/token_remover.py index ef979f222..67d6918ab 100644 --- a/bot/cogs/token_remover.py +++ b/bot/cogs/token_remover.py @@ -11,11 +11,12 @@ from bot import utils from bot.bot import Bot from bot.cogs.moderation import ModLog from bot.constants import Channels, Colours, Event, Icons +from bot.utils.messages import format_user log = logging.getLogger(__name__) LOG_MESSAGE = ( - "Censored a seemingly valid token sent by {author} (`{author_id}`) in {channel}, " + "Censored a seemingly valid token sent by {author} in {channel}, " "token was `{user_id}.{timestamp}.{hmac}`" ) DELETION_MESSAGE_TEMPLATE = ( @@ -111,8 +112,7 @@ class TokenRemover(Cog): def format_log_message(msg: Message, token: Token) -> str: """Return the log message to send for `token` being censored in `msg`.""" return LOG_MESSAGE.format( - author=msg.author, - author_id=msg.author.id, + author=format_user(msg.author), channel=msg.channel.mention, user_id=token.user_id, timestamp=token.timestamp, diff --git a/bot/cogs/verification.py b/bot/cogs/verification.py index ae156cf70..f4cdc7059 100644 --- a/bot/cogs/verification.py +++ b/bot/cogs/verification.py @@ -9,6 +9,7 @@ from bot.bot import Bot from bot.cogs.moderation import ModLog from bot.decorators import in_whitelist, without_role from bot.utils.checks import InWhitelistCheckFailure, without_role_check +from bot.utils.messages import format_user log = logging.getLogger(__name__) @@ -66,7 +67,7 @@ class Verification(Cog): ) embed_text = ( - f"{message.author.mention} sent a message in " + f"{format_user(message.author)} sent a message in " f"{message.channel.mention} that contained user and/or role mentions." f"\n\n**Original message:**\n>>> {message.content}" ) diff --git a/bot/cogs/webhook_remover.py b/bot/cogs/webhook_remover.py index 543869215..b70e29a79 100644 --- a/bot/cogs/webhook_remover.py +++ b/bot/cogs/webhook_remover.py @@ -7,6 +7,7 @@ from discord.ext.commands import Cog from bot.bot import Bot from bot.cogs.moderation.modlog import ModLog from bot.constants import Channels, Colours, Event, Icons +from bot.utils.messages import format_user WEBHOOK_URL_RE = re.compile(r"((?:https?://)?discordapp\.com/api/webhooks/\d+/)\S+/?", re.I) @@ -45,8 +46,8 @@ class WebhookRemover(Cog): await msg.channel.send(ALERT_MESSAGE_TEMPLATE.format(user=msg.author.mention)) message = ( - f"{msg.author} (`{msg.author.id}`) posted a Discord webhook URL " - f"to #{msg.channel}. Webhook URL was `{redacted_url}`" + f"{format_user(msg.author)} posted a Discord webhook URL to {msg.channel.mention}. " + f"Webhook URL was `{redacted_url}`" ) log.debug(message) -- cgit v1.2.3 From e261c608fb139baa4f99fb2be23bd3e928919107 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 20 Jul 2020 15:26:22 -0700 Subject: Filtering: refactor sending of mod log A lot of redundant code existed between the message and eval filters. --- bot/cogs/filtering.py | 106 ++++++++++++++++++++++---------------------------- 1 file changed, 47 insertions(+), 59 deletions(-) diff --git a/bot/cogs/filtering.py b/bot/cogs/filtering.py index bd665f424..019a7bde2 100644 --- a/bot/cogs/filtering.py +++ b/bot/cogs/filtering.py @@ -221,36 +221,7 @@ class Filtering(Cog): if _filter["type"] == "filter": filter_triggered = True - # We do not have to check against DM channels since !eval cannot be used there. - channel_str = f"in {msg.channel.mention}" - - message_content, additional_embeds, additional_embeds_msg = self._add_stats( - filter_name, match, result - ) - - message = ( - f"The {filter_name} {_filter['type']} was triggered " - f"by **{msg.author}** " - f"(`{msg.author.id}`) {channel_str} using !eval with " - f"[the following message]({msg.jump_url}):\n\n" - f"{message_content}" - ) - - log.debug(message) - - # Send pretty mod log embed to mod-alerts - await self.mod_log.send_log_message( - icon_url=Icons.filtering, - colour=Colour(Colours.soft_red), - title=f"{_filter['type'].title()} triggered!", - text=message, - thumbnail=msg.author.avatar_url_as(static_format="png"), - channel_id=Channels.mod_alerts, - ping_everyone=Filter.ping_everyone, - additional_embeds=additional_embeds, - additional_embeds_msg=additional_embeds_msg - ) - + await self._send_log(filter_name, _filter["type"], match, msg, result) break # We don't want multiple filters to trigger return filter_triggered @@ -312,39 +283,56 @@ class Filtering(Cog): self.schedule_msg_delete(data) log.trace(f"Offensive message {msg.id} will be deleted on {delete_date}") - if is_private: - channel_str = "via DM" - else: - channel_str = f"in {msg.channel.mention}" + await self._send_log(filter_name, _filter["type"], match, msg) + break # We don't want multiple filters to trigger - message_content, additional_embeds, additional_embeds_msg = self._add_stats( - filter_name, match, msg.content - ) + async def _send_log( + self, + filter_name: str, + filter_type: str, + match: Union[re.Match, dict, bool, List[discord.Embed]], + msg: discord.Message, + eval_content: Optional[str] = None + ) -> None: + """Send a mod log for a triggered filter.""" + if msg.channel.type is discord.ChannelType.private: + channel_str = "via DM" + else: + channel_str = f"in {msg.channel.mention}" - message = ( - f"The {filter_name} {_filter['type']} was triggered " - f"by **{msg.author}** " - f"(`{msg.author.id}`) {channel_str} with [the " - f"following message]({msg.jump_url}):\n\n" - f"{message_content}" - ) + if eval_content is None: + # It's not an eval, so use the message's contents to get stats. + eval_content = msg.content + else: + # This variable name is a bit misleading but whatever. + channel_str += " using !eval" - log.debug(message) - - # Send pretty mod log embed to mod-alerts - await self.mod_log.send_log_message( - icon_url=Icons.filtering, - colour=Colour(Colours.soft_red), - title=f"{_filter['type'].title()} triggered!", - text=message, - thumbnail=msg.author.avatar_url_as(static_format="png"), - channel_id=Channels.mod_alerts, - ping_everyone=Filter.ping_everyone, - additional_embeds=additional_embeds, - additional_embeds_msg=additional_embeds_msg - ) + message_content, additional_embeds, additional_embeds_msg = self._add_stats( + filter_name, match, eval_content + ) - break # We don't want multiple filters to trigger + message = ( + f"The {filter_name} {filter_type} was triggered " + f"by **{msg.author}** " + f"(`{msg.author.id}`) {channel_str} with [the " + f"following message]({msg.jump_url}):\n\n" + f"{message_content}" + ) + + log.debug(message) + + # Send pretty mod log embed to mod-alerts + await self.mod_log.send_log_message( + icon_url=Icons.filtering, + colour=Colour(Colours.soft_red), + title=f"{filter_type.title()} triggered!", + text=message, + thumbnail=msg.author.avatar_url_as(static_format="png"), + channel_id=Channels.mod_alerts, + ping_everyone=Filter.ping_everyone, + additional_embeds=additional_embeds, + additional_embeds_msg=additional_embeds_msg + ) def _add_stats(self, name: str, match: Union[re.Match, dict, bool, List[discord.Embed]], content: str) -> Tuple[ str, Optional[List[discord.Embed]], Optional[str] -- cgit v1.2.3 From 04fb8339ad18192636ddd14bf35ddf0295c71583 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 20 Jul 2020 15:43:31 -0700 Subject: Filtering: refactor _add_stats to return a NamedTuple --- bot/cogs/filtering.py | 52 ++++++++++++++++++++++++++------------------------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/bot/cogs/filtering.py b/bot/cogs/filtering.py index 019a7bde2..2118f03a2 100644 --- a/bot/cogs/filtering.py +++ b/bot/cogs/filtering.py @@ -2,7 +2,7 @@ import asyncio import logging import re from datetime import datetime, timedelta -from typing import List, Mapping, Optional, Tuple, Union +from typing import List, Mapping, NamedTuple, Optional, Union import dateutil import discord.errors @@ -46,6 +46,7 @@ TOKEN_WATCHLIST_PATTERNS = [ WATCHLIST_PATTERNS = WORD_WATCHLIST_PATTERNS + TOKEN_WATCHLIST_PATTERNS DAYS_BETWEEN_ALERTS = 3 +OFFENSIVE_MSG_DELETE_TIME = timedelta(days=Filter.offensive_msg_delete_days) def expand_spoilers(text: str) -> str: @@ -56,7 +57,15 @@ def expand_spoilers(text: str) -> str: ) -OFFENSIVE_MSG_DELETE_TIME = timedelta(days=Filter.offensive_msg_delete_days) +FilterMatch = Union[re.Match, dict, bool, List[discord.Embed]] + + +class Stats(NamedTuple): + """Additional stats on a triggered filter to append to a mod log.""" + + message_content: str + additional_embeds: Optional[List[discord.Embed]] + additional_embeds_msg: Optional[str] class Filtering(Cog): @@ -221,7 +230,9 @@ class Filtering(Cog): if _filter["type"] == "filter": filter_triggered = True - await self._send_log(filter_name, _filter["type"], match, msg, result) + stats = self._add_stats(filter_name, match, result) + await self._send_log(filter_name, _filter["type"], msg, stats, is_eval=True) + break # We don't want multiple filters to trigger return filter_triggered @@ -283,16 +294,19 @@ class Filtering(Cog): self.schedule_msg_delete(data) log.trace(f"Offensive message {msg.id} will be deleted on {delete_date}") - await self._send_log(filter_name, _filter["type"], match, msg) + stats = self._add_stats(filter_name, match, msg.content) + await self._send_log(filter_name, _filter["type"], msg, stats) + break # We don't want multiple filters to trigger async def _send_log( self, filter_name: str, filter_type: str, - match: Union[re.Match, dict, bool, List[discord.Embed]], msg: discord.Message, - eval_content: Optional[str] = None + stats: Stats, + *, + is_eval: bool = False, ) -> None: """Send a mod log for a triggered filter.""" if msg.channel.type is discord.ChannelType.private: @@ -300,23 +314,13 @@ class Filtering(Cog): else: channel_str = f"in {msg.channel.mention}" - if eval_content is None: - # It's not an eval, so use the message's contents to get stats. - eval_content = msg.content - else: - # This variable name is a bit misleading but whatever. - channel_str += " using !eval" - - message_content, additional_embeds, additional_embeds_msg = self._add_stats( - filter_name, match, eval_content - ) - + eval_msg = "using !eval" if is_eval else "" message = ( f"The {filter_name} {filter_type} was triggered " f"by **{msg.author}** " - f"(`{msg.author.id}`) {channel_str} with [the " + f"(`{msg.author.id}`) {channel_str} {eval_msg}with [the " f"following message]({msg.jump_url}):\n\n" - f"{message_content}" + f"{stats.message_content}" ) log.debug(message) @@ -330,13 +334,11 @@ class Filtering(Cog): thumbnail=msg.author.avatar_url_as(static_format="png"), channel_id=Channels.mod_alerts, ping_everyone=Filter.ping_everyone, - additional_embeds=additional_embeds, - additional_embeds_msg=additional_embeds_msg + additional_embeds=stats.additional_embeds, + additional_embeds_msg=stats.additional_embeds_msg ) - def _add_stats(self, name: str, match: Union[re.Match, dict, bool, List[discord.Embed]], content: str) -> Tuple[ - str, Optional[List[discord.Embed]], Optional[str] - ]: + def _add_stats(self, name: str, match: FilterMatch, content: str) -> Stats: """Adds relevant statistical information to the relevant filter and increments the bot's stats.""" # Word and match stats for watch_regex if name == "watch_regex": @@ -373,7 +375,7 @@ class Filtering(Cog): additional_embeds = match additional_embeds_msg = "With the following embed(s):" - return message_content, additional_embeds, additional_embeds_msg + return Stats(message_content, additional_embeds, additional_embeds_msg) @staticmethod def _check_filter(msg: Message) -> bool: -- cgit v1.2.3 From 820173d4770361d437309169a8d6c7e48db89ec8 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 20 Jul 2020 15:48:00 -0700 Subject: Filtering: use user mentions in mod logs --- bot/cogs/filtering.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/bot/cogs/filtering.py b/bot/cogs/filtering.py index 2118f03a2..07a603988 100644 --- a/bot/cogs/filtering.py +++ b/bot/cogs/filtering.py @@ -17,6 +17,7 @@ from bot.constants import ( Channels, Colours, Filter, Icons, URLs ) +from bot.utils.messages import format_user from bot.utils.redis_cache import RedisCache from bot.utils.scheduling import Scheduler @@ -190,8 +191,8 @@ class Filtering(Cog): log.info(f"Sending bad nickname alert for '{member.display_name}' ({member.id}).") log_string = ( - f"**User:** {member.mention} (`{member.id}`)\n" - f"**Display Name:** {member.display_name}\n" + f"**User:** {format_user(member)}\n" + f"**Display Name:** {escape_markdown(member.display_name)}\n" f"**Bad Matches:** {', '.join(match.group() for match in matches)}" ) @@ -316,10 +317,8 @@ class Filtering(Cog): eval_msg = "using !eval" if is_eval else "" message = ( - f"The {filter_name} {filter_type} was triggered " - f"by **{msg.author}** " - f"(`{msg.author.id}`) {channel_str} {eval_msg}with [the " - f"following message]({msg.jump_url}):\n\n" + f"The {filter_name} {filter_type} was triggered by {format_user(msg.author)} " + f"{channel_str} {eval_msg}with [the following message]({msg.jump_url}):\n\n" f"{stats.message_content}" ) -- cgit v1.2.3 From fbf1094b32ec489f75d186017a9c680b2bd6a263 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 20 Jul 2020 16:03:11 -0700 Subject: ModLog: use user mentions --- bot/cogs/moderation/modlog.py | 34 ++++++++++++---------------------- 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/bot/cogs/moderation/modlog.py b/bot/cogs/moderation/modlog.py index 0a63f57b8..724651ecd 100644 --- a/bot/cogs/moderation/modlog.py +++ b/bot/cogs/moderation/modlog.py @@ -12,10 +12,10 @@ from deepdiff import DeepDiff from discord import Colour from discord.abc import GuildChannel from discord.ext.commands import Cog, Context -from discord.utils import escape_markdown from bot.bot import Bot from bot.constants import Categories, Channels, Colours, Emojis, Event, Guild as GuildConstant, Icons, URLs +from bot.utils.messages import format_user from bot.utils.time import humanize_delta log = logging.getLogger(__name__) @@ -392,7 +392,7 @@ class ModLog(Cog, name="ModLog"): await self.send_log_message( Icons.user_ban, Colours.soft_red, - "User banned", f"{member} (`{member.id}`)", + "User banned", format_user(member), thumbnail=member.avatar_url_as(static_format="png"), channel_id=Channels.user_log ) @@ -403,8 +403,7 @@ class ModLog(Cog, name="ModLog"): if member.guild.id != GuildConstant.id: return - member_str = escape_markdown(str(member)) - message = f"{member_str} (`{member.id}`)" + message = format_user(member) now = datetime.utcnow() difference = abs(relativedelta(now, member.created_at)) @@ -430,10 +429,9 @@ class ModLog(Cog, name="ModLog"): self._ignored[Event.member_remove].remove(member.id) return - member_str = escape_markdown(str(member)) await self.send_log_message( Icons.sign_out, Colours.soft_red, - "User left", f"{member_str} (`{member.id}`)", + "User left", format_user(member), thumbnail=member.avatar_url_as(static_format="png"), channel_id=Channels.user_log ) @@ -448,10 +446,9 @@ class ModLog(Cog, name="ModLog"): self._ignored[Event.member_unban].remove(member.id) return - member_str = escape_markdown(str(member)) await self.send_log_message( Icons.user_unban, Colour.blurple(), - "User unbanned", f"{member_str} (`{member.id}`)", + "User unbanned", format_user(member), thumbnail=member.avatar_url_as(static_format="png"), channel_id=Channels.mod_log ) @@ -511,8 +508,7 @@ class ModLog(Cog, name="ModLog"): for item in sorted(changes): message += f"{Emojis.bullet} {item}\n" - member_str = escape_markdown(str(after)) - message = f"**{member_str}** (`{after.id}`)\n{message}" + message = f"{format_user(after)}\n{message}" await self.send_log_message( icon_url=Icons.user_update, @@ -545,17 +541,16 @@ class ModLog(Cog, name="ModLog"): if author.bot: return - author_str = escape_markdown(str(author)) if channel.category: response = ( - f"**Author:** {author_str} (`{author.id}`)\n" + f"**Author:** {format_user(author)}\n" f"**Channel:** {channel.category}/#{channel.name} (`{channel.id}`)\n" f"**Message ID:** `{message.id}`\n" "\n" ) else: response = ( - f"**Author:** {author_str} (`{author.id}`)\n" + f"**Author:** {format_user(author)}\n" f"**Channel:** #{channel.name} (`{channel.id}`)\n" f"**Message ID:** `{message.id}`\n" "\n" @@ -641,9 +636,6 @@ class ModLog(Cog, name="ModLog"): if msg_before.content == msg_after.content: return - author = msg_before.author - author_str = escape_markdown(str(author)) - channel = msg_before.channel channel_name = f"{channel.category}/#{channel.name}" if channel.category else f"#{channel.name}" @@ -675,7 +667,7 @@ class ModLog(Cog, name="ModLog"): content_after.append(sub) response = ( - f"**Author:** {author_str} (`{author.id}`)\n" + f"**Author:** {format_user(msg_before.author)}\n" f"**Channel:** {channel_name} (`{channel.id}`)\n" f"**Message ID:** `{msg_before.id}`\n" "\n" @@ -727,12 +719,11 @@ class ModLog(Cog, name="ModLog"): self._cached_edits.remove(event.message_id) return - author = message.author channel = message.channel channel_name = f"{channel.category}/#{channel.name}" if channel.category else f"#{channel.name}" before_response = ( - f"**Author:** {author} (`{author.id}`)\n" + f"**Author:** {format_user(message.author)}\n" f"**Channel:** {channel_name} (`{channel.id}`)\n" f"**Message ID:** `{message.id}`\n" "\n" @@ -740,7 +731,7 @@ class ModLog(Cog, name="ModLog"): ) after_response = ( - f"**Author:** {author} (`{author.id}`)\n" + f"**Author:** {format_user(message.author)}\n" f"**Channel:** {channel_name} (`{channel.id}`)\n" f"**Message ID:** `{message.id}`\n" "\n" @@ -818,9 +809,8 @@ class ModLog(Cog, name="ModLog"): if not changes: return - member_str = escape_markdown(str(member)) message = "\n".join(f"{Emojis.bullet} {item}" for item in sorted(changes)) - message = f"**{member_str}** (`{member.id}`)\n{message}" + message = f"{format_user(member)}\n{message}" await self.send_log_message( icon_url=icon, -- cgit v1.2.3 From 480dbef17e0ef6dc58c06dfd69a787725df001be Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 21 Jul 2020 09:55:12 -0700 Subject: Use user mentions in infraction search results Using our expanded API is more efficient than making a request to the Discord API for potentially every user in the search results. The data may not be up to date, but that's an acceptable compromise. --- bot/cogs/moderation/management.py | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/bot/cogs/moderation/management.py b/bot/cogs/moderation/management.py index e0a86ee59..b4c69acc2 100644 --- a/bot/cogs/moderation/management.py +++ b/bot/cogs/moderation/management.py @@ -6,6 +6,7 @@ from datetime import datetime import discord from discord.ext import commands from discord.ext.commands import Context +from discord.utils import escape_markdown from bot import constants from bot.bot import Bot @@ -187,7 +188,7 @@ class ModManagement(commands.Cog): async def search_user(self, ctx: Context, user: t.Union[discord.User, proxy_user]) -> None: """Search for infractions by member.""" infraction_list = await self.bot.api_client.get( - 'bot/infractions', + 'bot/infractions/expanded', params={'user__id': str(user.id)} ) embed = discord.Embed( @@ -200,7 +201,7 @@ class ModManagement(commands.Cog): async def search_reason(self, ctx: Context, reason: str) -> None: """Search for infractions by their reason. Use Re2 for matching.""" infraction_list = await self.bot.api_client.get( - 'bot/infractions', + 'bot/infractions/expanded', params={'search': reason} ) embed = discord.Embed( @@ -237,37 +238,43 @@ class ModManagement(commands.Cog): max_size=1000 ) - def infraction_to_string(self, infraction: utils.Infraction) -> str: + def infraction_to_string(self, infraction: t.Dict[str, t.Any]) -> str: """Convert the infraction object to a string representation.""" - actor_id = infraction["actor"] - guild = self.bot.get_guild(constants.Guild.id) - actor = guild.get_member(actor_id) active = infraction["active"] - user_id = infraction["user"] - hidden = infraction["hidden"] + user = infraction["user"] + expires_at = infraction["expires_at"] created = time.format_infraction(infraction["inserted_at"]) + # Format the user string. + if user_obj := self.bot.get_user(user["id"]): + # The user is in the cache. + user_str = messages.format_user(user_obj) + else: + # Use the user data retrieved from the DB. + name = escape_markdown(user['name']) + user_str = f"<@{user['id']}> ({name}#{user['discriminator']})" + if active: - remaining = time.until_expiration(infraction["expires_at"]) or "Expired" + remaining = time.until_expiration(expires_at) or "Expired" else: remaining = "Inactive" - if infraction["expires_at"] is None: + if expires_at is None: expires = "*Permanent*" else: date_from = datetime.strptime(created, time.INFRACTION_FORMAT) - expires = time.format_infraction_with_duration(infraction["expires_at"], date_from) + expires = time.format_infraction_with_duration(expires_at, date_from) lines = textwrap.dedent(f""" {"**===============**" if active else "==============="} Status: {"__**Active**__" if active else "Inactive"} - User: {self.bot.get_user(user_id)} (`{user_id}`) + User: {user_str} Type: **{infraction["type"]}** - Shadow: {hidden} + Shadow: {infraction["hidden"]} Created: {created} Expires: {expires} Remaining: {remaining} - Actor: {actor.mention if actor else actor_id} + Actor: <@{infraction["actor"]["id"]}> ID: `{infraction["id"]}` Reason: {infraction["reason"] or "*None*"} {"**===============**" if active else "==============="} -- cgit v1.2.3 From abe46b7ee2496c5676e7c5b8c809358d2fab90a5 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 21 Jul 2020 10:37:26 -0700 Subject: Fix test for token remover log message --- tests/bot/cogs/test_token_remover.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/bot/cogs/test_token_remover.py b/tests/bot/cogs/test_token_remover.py index 3349caa73..1c7267f56 100644 --- a/tests/bot/cogs/test_token_remover.py +++ b/tests/bot/cogs/test_token_remover.py @@ -240,8 +240,7 @@ class TokenRemoverTests(unittest.IsolatedAsyncioTestCase): self.assertEqual(return_value, log_message.format.return_value) log_message.format.assert_called_once_with( - author=self.msg.author, - author_id=self.msg.author.id, + author=f"{self.msg.author.mention} ({self.msg.author})", channel=self.msg.channel.mention, user_id=token.user_id, timestamp=token.timestamp, -- cgit v1.2.3 From 5d953b55ef7db247c009d5caae61fa3b1df8ff3e Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 10 Aug 2020 11:31:55 -0700 Subject: Concatenate string in one line --- bot/cogs/moderation/modlog.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bot/cogs/moderation/modlog.py b/bot/cogs/moderation/modlog.py index 724651ecd..5d3055796 100644 --- a/bot/cogs/moderation/modlog.py +++ b/bot/cogs/moderation/modlog.py @@ -403,11 +403,10 @@ class ModLog(Cog, name="ModLog"): if member.guild.id != GuildConstant.id: return - message = format_user(member) now = datetime.utcnow() difference = abs(relativedelta(now, member.created_at)) - message += "\n\n**Account age:** " + humanize_delta(difference) + message = format_user(member) + "\n\n**Account age:** " + humanize_delta(difference) if difference.days < 1 and difference.months < 1 and difference.years < 1: # New user account! message = f"{Emojis.new} {message}" -- cgit v1.2.3 From 5051b5aeccd1f33ba34b2dd2af03511343b60efd Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 10 Aug 2020 11:34:51 -0700 Subject: Use format_user in token remover test The point of format_user is to have a consistent format across the code base. That should apply to tests too. --- tests/bot/cogs/test_token_remover.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/bot/cogs/test_token_remover.py b/tests/bot/cogs/test_token_remover.py index 1c7267f56..5dee6922e 100644 --- a/tests/bot/cogs/test_token_remover.py +++ b/tests/bot/cogs/test_token_remover.py @@ -9,6 +9,7 @@ from bot import constants from bot.cogs import token_remover from bot.cogs.moderation import ModLog from bot.cogs.token_remover import Token, TokenRemover +from bot.utils.messages import format_user from tests.helpers import MockBot, MockMessage, autospec @@ -240,7 +241,7 @@ class TokenRemoverTests(unittest.IsolatedAsyncioTestCase): self.assertEqual(return_value, log_message.format.return_value) log_message.format.assert_called_once_with( - author=f"{self.msg.author.mention} ({self.msg.author})", + author=format_user(self.msg.author), channel=self.msg.channel.mention, user_id=token.user_id, timestamp=token.timestamp, -- cgit v1.2.3 From 3c6cb81e6e36aa3e7321a92ea4e6625f99b3ce7c Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 10 Aug 2020 11:42:41 -0700 Subject: Zero-fill discriminators in infraction searches --- bot/cogs/moderation/management.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/cogs/moderation/management.py b/bot/cogs/moderation/management.py index b4c69acc2..56a601cb7 100644 --- a/bot/cogs/moderation/management.py +++ b/bot/cogs/moderation/management.py @@ -252,7 +252,7 @@ class ModManagement(commands.Cog): else: # Use the user data retrieved from the DB. name = escape_markdown(user['name']) - user_str = f"<@{user['id']}> ({name}#{user['discriminator']})" + user_str = f"<@{user['id']}> ({name}#{user['discriminator']:04})" if active: remaining = time.until_expiration(expires_at) or "Expired" -- cgit v1.2.3 From ddd5536c731c57e7f995727fcb631a83d54d09d2 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 11 Aug 2020 15:54:47 -0700 Subject: Replace InfractionSearchQuery with a generic Snowflake converter It's unnecessarily precise to do an fetch user API call in order to distinguish between a user and a reason. Furthermore, a User object isn't actually required for infraction searches - only an ID is. --- bot/cogs/moderation/management.py | 4 ++-- bot/converters.py | 49 ++++++++++++++++++++++++++++++--------- 2 files changed, 40 insertions(+), 13 deletions(-) diff --git a/bot/cogs/moderation/management.py b/bot/cogs/moderation/management.py index 56a601cb7..af736d4de 100644 --- a/bot/cogs/moderation/management.py +++ b/bot/cogs/moderation/management.py @@ -10,7 +10,7 @@ from discord.utils import escape_markdown from bot import constants from bot.bot import Bot -from bot.converters import Expiry, InfractionSearchQuery, allowed_strings, proxy_user +from bot.converters import Expiry, Snowflake, allowed_strings, proxy_user from bot.pagination import LinePaginator from bot.utils import messages, time from bot.utils.checks import in_whitelist_check, with_role_check @@ -177,7 +177,7 @@ class ModManagement(commands.Cog): # region: Search infractions @infraction_group.group(name="search", invoke_without_command=True) - async def infraction_search_group(self, ctx: Context, query: InfractionSearchQuery) -> None: + async def infraction_search_group(self, ctx: Context, query: Snowflake) -> None: """Searches for infractions in the database.""" if isinstance(query, discord.User): await ctx.invoke(self.search_user, query) diff --git a/bot/converters.py b/bot/converters.py index 1358cbf1e..4c41d0ece 100644 --- a/bot/converters.py +++ b/bot/converters.py @@ -10,6 +10,7 @@ import discord from aiohttp import ClientConnectorError from dateutil.relativedelta import relativedelta from discord.ext.commands import BadArgument, Bot, Context, Converter, IDConverter, UserConverter +from discord.utils import DISCORD_EPOCH, snowflake_time from bot.api import ResponseCodeError from bot.constants import URLs @@ -17,6 +18,8 @@ from bot.utils.regex import INVITE_RE log = logging.getLogger(__name__) +DISCORD_EPOCH_DT = datetime.utcfromtimestamp(DISCORD_EPOCH / 1000) + def allowed_strings(*values, preserve_case: bool = False) -> t.Callable[[str], str]: """ @@ -172,17 +175,42 @@ class ValidURL(Converter): return url -class InfractionSearchQuery(Converter): - """A converter that checks if the argument is a Discord user, and if not, falls back to a string.""" +class Snowflake(IDConverter): + """ + Converts to an int if the argument is a valid Discord snowflake. + + A snowflake is valid if: + + * It consists of 15-21 digits (0-9) + * Its parsed datetime is after the Discord epoch + * Its parsed datetime is less than 1 day after the current time + """ + + async def convert(self, ctx: Context, arg: str) -> int: + """ + Ensure `arg` matches the ID pattern and its timestamp is in range. + + Return `arg` as an int if it's a valid snowflake. + """ + error = f"Invalid snowflake {arg!r}" + + if not self._get_id_match(arg): + raise BadArgument(error) + + snowflake = int(arg) - @staticmethod - async def convert(ctx: Context, arg: str) -> t.Union[discord.Member, str]: - """Check if the argument is a Discord user, and if not, falls back to a string.""" try: - maybe_snowflake = arg.strip("<@!>") - return await ctx.bot.fetch_user(maybe_snowflake) - except (discord.NotFound, discord.HTTPException): - return arg + time = snowflake_time(snowflake) + except (OverflowError, OSError) as e: + # Not sure if this can ever even happen, but let's be safe. + raise BadArgument(f"{error}: {e}") + + if time < DISCORD_EPOCH_DT: + raise BadArgument(f"{error}: timestamp is before the Discord epoch.") + elif (datetime.utcnow() - time).days >= 1: + raise BadArgument(f"{error}: timestamp is too far into the future.") + + return snowflake class Subreddit(Converter): @@ -447,8 +475,7 @@ class UserMentionOrID(UserConverter): """ Converts to a `discord.User`, but only if a mention or userID is provided. - Unlike the default `UserConverter`, it does allow conversion from name, or name#descrim. - + Unlike the default `UserConverter`, it doesn't allow conversion from a name or name#descrim. This is useful in cases where that lookup strategy would lead to ambiguity. """ -- cgit v1.2.3 From b01e854e3870eb90ef2cb9dec70040f4a673387d Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 11 Aug 2020 16:37:33 -0700 Subject: Create a UserMention converter --- bot/cogs/moderation/management.py | 6 +++--- bot/converters.py | 18 +++++++++++++++++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/bot/cogs/moderation/management.py b/bot/cogs/moderation/management.py index af736d4de..c2cca5352 100644 --- a/bot/cogs/moderation/management.py +++ b/bot/cogs/moderation/management.py @@ -10,7 +10,7 @@ from discord.utils import escape_markdown from bot import constants from bot.bot import Bot -from bot.converters import Expiry, Snowflake, allowed_strings, proxy_user +from bot.converters import Expiry, Snowflake, UserMention, allowed_strings, proxy_user from bot.pagination import LinePaginator from bot.utils import messages, time from bot.utils.checks import in_whitelist_check, with_role_check @@ -177,9 +177,9 @@ class ModManagement(commands.Cog): # region: Search infractions @infraction_group.group(name="search", invoke_without_command=True) - async def infraction_search_group(self, ctx: Context, query: Snowflake) -> None: + async def infraction_search_group(self, ctx: Context, query: t.Union[UserMention, Snowflake, str]) -> None: """Searches for infractions in the database.""" - if isinstance(query, discord.User): + if isinstance(query, int): await ctx.invoke(self.search_user, query) else: await ctx.invoke(self.search_reason, query) diff --git a/bot/converters.py b/bot/converters.py index 4c41d0ece..4cfd663ba 100644 --- a/bot/converters.py +++ b/bot/converters.py @@ -2,6 +2,7 @@ import logging import re import typing as t from datetime import datetime +from functools import partial from ssl import CertificateError import dateutil.parser @@ -19,6 +20,7 @@ from bot.utils.regex import INVITE_RE log = logging.getLogger(__name__) DISCORD_EPOCH_DT = datetime.utcfromtimestamp(DISCORD_EPOCH / 1000) +RE_USER_MENTION = re.compile(r"<@!?([0-9]+)>$") def allowed_strings(*values, preserve_case: bool = False) -> t.Callable[[str], str]: @@ -481,7 +483,7 @@ class UserMentionOrID(UserConverter): async def convert(self, ctx: Context, argument: str) -> discord.User: """Convert the `arg` to a `discord.User`.""" - match = self._get_id_match(argument) or re.match(r'<@!?([0-9]+)>$', argument) + match = self._get_id_match(argument) or RE_USER_MENTION.match(argument) if match is not None: return await super().convert(ctx, argument) @@ -534,5 +536,19 @@ class FetchedUser(UserConverter): raise BadArgument(f"User `{arg}` does not exist") +def _snowflake_from_regex(pattern: t.Pattern, arg: str) -> int: + """ + Extract the snowflake from `arg` using a regex `pattern` and return it as an int. + + The snowflake is expected to be within the first capture group in `pattern`. + """ + match = pattern.match(arg) + if not match: + raise BadArgument(f"Mention {str!r} is invalid.") + + return int(match.group(1)) + + Expiry = t.Union[Duration, ISODateTime] FetchedMember = t.Union[discord.Member, FetchedUser] +UserMention = partial(_snowflake_from_regex, RE_USER_MENTION) -- cgit v1.2.3 From 24f1e6521422be436cec90165506005e6aad4969 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 25 Aug 2020 10:51:17 -0700 Subject: Info: simplify channel redirection for the user command `in_whitelist_check` is a convenient utility that does the same thing as the previous code. --- bot/cogs/information.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/bot/cogs/information.py b/bot/cogs/information.py index 55ecb2836..00d2b731e 100644 --- a/bot/cogs/information.py +++ b/bot/cogs/information.py @@ -15,7 +15,7 @@ from bot import constants from bot.bot import Bot from bot.decorators import in_whitelist, with_role from bot.pagination import LinePaginator -from bot.utils.checks import InWhitelistCheckFailure, cooldown_with_role_bypass, with_role_check +from bot.utils.checks import cooldown_with_role_bypass, in_whitelist_check, with_role_check from bot.utils.time import time_since log = logging.getLogger(__name__) @@ -201,14 +201,10 @@ class Information(Cog): await ctx.send("You may not use this command on users other than yourself.") return - # Non-staff may only do this in #bot-commands - if not with_role_check(ctx, *constants.STAFF_ROLES): - if not ctx.channel.id == constants.Channels.bot_commands: - raise InWhitelistCheckFailure(constants.Channels.bot_commands) - - embed = await self.create_user_embed(ctx, user) - - await ctx.send(embed=embed) + # Will redirect to #bot-commands if it fails. + if in_whitelist_check(ctx, roles=constants.STAFF_ROLES): + embed = await self.create_user_embed(ctx, user) + await ctx.send(embed=embed) async def create_user_embed(self, ctx: Context, user: Member) -> Embed: """Creates an embed containing information on the `user`.""" -- cgit v1.2.3 From c14eea25265754d8f4e82ef564db1d01ae4f0ca7 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 25 Aug 2020 11:00:52 -0700 Subject: Info: show verbose infractions if user cmd invoked in modmail Fix #1125 --- bot/cogs/information.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/bot/cogs/information.py b/bot/cogs/information.py index 00d2b731e..81d326d3f 100644 --- a/bot/cogs/information.py +++ b/bot/cogs/information.py @@ -273,8 +273,14 @@ class Information(Cog): ) ] + # Use getattr to future-proof for commands invoked via DMs. + show_verbose = ( + ctx.channel.id in constants.MODERATION_CHANNELS + or getattr(ctx.channel, "category_id", None) == constants.Categories.modmail + ) + # Show more verbose output in moderation channels for infractions and nominations - if ctx.channel.id in constants.MODERATION_CHANNELS: + if show_verbose: fields.append(await self.expanded_user_infraction_counts(user)) fields.append(await self.user_nomination_counts(user)) else: -- cgit v1.2.3 From 6efd6e5f22afc79d493b08d764e3fa666f9c8033 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Wed, 26 Aug 2020 18:12:00 -0700 Subject: Fix infraction counts being shown in wrong channels They should only be shown in mod channels, not all staff channels. Fix #1127 --- bot/cogs/moderation/scheduler.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bot/cogs/moderation/scheduler.py b/bot/cogs/moderation/scheduler.py index 051f6c52c..13180c2b6 100644 --- a/bot/cogs/moderation/scheduler.py +++ b/bot/cogs/moderation/scheduler.py @@ -12,7 +12,7 @@ from discord.ext.commands import Context from bot import constants from bot.api import ResponseCodeError from bot.bot import Bot -from bot.constants import Colours, STAFF_CHANNELS +from bot.constants import Colours, MODERATION_CHANNELS from bot.utils import time from bot.utils.scheduling import Scheduler from . import utils @@ -137,9 +137,9 @@ class InfractionScheduler: ) if reason: end_msg = f" (reason: {textwrap.shorten(reason, width=1500, placeholder='...')})" - elif ctx.channel.id not in STAFF_CHANNELS: + elif ctx.channel.id not in MODERATION_CHANNELS: log.trace( - f"Infraction #{id_} context is not in a staff channel; omitting infraction count." + f"Infraction #{id_} context is not in a mod channel; omitting infraction count." ) else: log.trace(f"Fetching total infraction count for {user}.") -- cgit v1.2.3 From ac97a3220b6bc04aa3b46567a96ef7606d156687 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Wed, 26 Aug 2020 18:37:42 -0700 Subject: Fix user command tests Mocking the bot commands channel constant no longer worked after switching to `in_whitelist_check`. --- tests/bot/cogs/test_information.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/tests/bot/cogs/test_information.py b/tests/bot/cogs/test_information.py index 77b0ddf17..3438635f1 100644 --- a/tests/bot/cogs/test_information.py +++ b/tests/bot/cogs/test_information.py @@ -532,10 +532,13 @@ class UserCommandTests(unittest.TestCase): self.moderator = helpers.MockMember(id=2, name="riffautae", roles=[self.moderator_role]) self.target = helpers.MockMember(id=3, name="__fluzz__") + # There's no way to mock the channel constant without deferring imports. The constant is + # used as a default value for a parameter, which gets defined upon import. + self.bot_command_channel = helpers.MockTextChannel(id=constants.Channels.bot_commands) + def test_regular_member_cannot_target_another_member(self, constants): """A regular user should not be able to use `!user` targeting another user.""" constants.MODERATION_ROLES = [self.moderator_role.id] - ctx = helpers.MockContext(author=self.author) asyncio.run(self.cog.user_info.callback(self.cog, ctx, self.target)) @@ -546,8 +549,6 @@ class UserCommandTests(unittest.TestCase): """A regular user should not be able to use this command outside of bot-commands.""" constants.MODERATION_ROLES = [self.moderator_role.id] constants.STAFF_ROLES = [self.moderator_role.id] - constants.Channels.bot_commands = 50 - ctx = helpers.MockContext(author=self.author, channel=helpers.MockTextChannel(id=100)) msg = "Sorry, but you may only use this command within <#50>." @@ -558,9 +559,7 @@ class UserCommandTests(unittest.TestCase): def test_regular_user_may_use_command_in_bot_commands_channel(self, create_embed, constants): """A regular user should be allowed to use `!user` targeting themselves in bot-commands.""" constants.STAFF_ROLES = [self.moderator_role.id] - constants.Channels.bot_commands = 50 - - ctx = helpers.MockContext(author=self.author, channel=helpers.MockTextChannel(id=50)) + ctx = helpers.MockContext(author=self.author, channel=self.bot_command_channel) asyncio.run(self.cog.user_info.callback(self.cog, ctx)) @@ -568,12 +567,10 @@ class UserCommandTests(unittest.TestCase): ctx.send.assert_called_once() @unittest.mock.patch("bot.cogs.information.Information.create_user_embed", new_callable=unittest.mock.AsyncMock) - def test_regular_user_can_explicitly_target_themselves(self, create_embed, constants): + def test_regular_user_can_explicitly_target_themselves(self, create_embed, _): """A user should target itself with `!user` when a `user` argument was not provided.""" constants.STAFF_ROLES = [self.moderator_role.id] - constants.Channels.bot_commands = 50 - - ctx = helpers.MockContext(author=self.author, channel=helpers.MockTextChannel(id=50)) + ctx = helpers.MockContext(author=self.author, channel=self.bot_command_channel) asyncio.run(self.cog.user_info.callback(self.cog, ctx, self.author)) @@ -584,8 +581,6 @@ class UserCommandTests(unittest.TestCase): def test_staff_members_can_bypass_channel_restriction(self, create_embed, constants): """Staff members should be able to bypass the bot-commands channel restriction.""" constants.STAFF_ROLES = [self.moderator_role.id] - constants.Channels.bot_commands = 50 - ctx = helpers.MockContext(author=self.moderator, channel=helpers.MockTextChannel(id=200)) asyncio.run(self.cog.user_info.callback(self.cog, ctx)) @@ -598,7 +593,6 @@ class UserCommandTests(unittest.TestCase): """A moderator should be able to use `!user` targeting another user.""" constants.MODERATION_ROLES = [self.moderator_role.id] constants.STAFF_ROLES = [self.moderator_role.id] - ctx = helpers.MockContext(author=self.moderator, channel=helpers.MockTextChannel(id=50)) asyncio.run(self.cog.user_info.callback(self.cog, ctx, self.target)) -- cgit v1.2.3 From 203abc99ad104ddef068ef2ddddff37a9982c47d Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Wed, 2 Sep 2020 13:42:45 -0700 Subject: Constants: remove staff_channels No longer being used anywhere. --- bot/constants.py | 2 -- config-default.yml | 9 --------- 2 files changed, 11 deletions(-) diff --git a/bot/constants.py b/bot/constants.py index f3db80279..b3825ea02 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -472,7 +472,6 @@ class Guild(metaclass=YAMLGetter): moderation_roles: List[int] modlog_blacklist: List[int] reminder_whitelist: List[int] - staff_channels: List[int] staff_roles: List[int] @@ -624,7 +623,6 @@ MODERATION_ROLES = Guild.moderation_roles STAFF_ROLES = Guild.staff_roles # Channel combinations -STAFF_CHANNELS = Guild.staff_channels MODERATION_CHANNELS = Guild.moderation_channels # Bot replies diff --git a/config-default.yml b/config-default.yml index 8c0092e76..9d29b9a96 100644 --- a/config-default.yml +++ b/config-default.yml @@ -199,15 +199,6 @@ guild: big_brother_logs: &BB_LOGS 468507907357409333 talent_pool: &TALENT_POOL 534321732593647616 - staff_channels: - - *ADMINS - - *ADMIN_SPAM - - *DEFCON - - *HELPERS - - *MODS - - *MOD_SPAM - - *ORGANISATION - moderation_channels: - *ADMINS - *ADMIN_SPAM -- cgit v1.2.3 From 0351a23513ccf5d9d8ab0637a0bfc4043796b0dc Mon Sep 17 00:00:00 2001 From: kosayoda Date: Sat, 5 Sep 2020 09:01:45 +0800 Subject: Detect pings after removing codeblocks. --- bot/rules/everyone_ping.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/bot/rules/everyone_ping.py b/bot/rules/everyone_ping.py index 89d9fe570..8fc03b924 100644 --- a/bot/rules/everyone_ping.py +++ b/bot/rules/everyone_ping.py @@ -8,8 +8,12 @@ from bot.constants import Colours, Guild, NEGATIVE_REPLIES # Generate regex for checking for pings: guild_id = Guild.id -EVERYONE_RE_INLINE_CODE = re.compile(rf"^(?!`).*@everyone.*(?!`)$|^(?!`).*<@&{guild_id}>.*(?!`)$") -EVERYONE_RE_MULTILINE_CODE = re.compile(rf"^(?!```).*@everyone.*(?!```)$|^(?!```).*<@&{guild_id}>.*(?!```)$") +EVERYONE_PING_RE = re.compile(rf"@everyone|<@&{guild_id}>") +CODE_BLOCK_RE = re.compile( + r"(?P``?)[^`]+?(?P=delim)(?!`+)" # Inline codeblock + r"|```(.+?)```", # Multiline codeblock + re.DOTALL | re.MULTILINE +) async def apply( @@ -22,10 +26,9 @@ async def apply( everyone_messages_count = 0 for msg in relevant_messages: - num_everyone_pings_inline = len(re.findall(EVERYONE_RE_INLINE_CODE, msg.content)) - num_everyone_pings_multiline = len(re.findall(EVERYONE_RE_MULTILINE_CODE, msg.content)) - if num_everyone_pings_inline and num_everyone_pings_multiline: - everyone_messages_count += 1 + content = CODE_BLOCK_RE.sub("", msg.content) # Remove codeblocks in the message + if matches := len(EVERYONE_PING_RE.findall(content)): + everyone_messages_count += matches if everyone_messages_count > config["max"]: # Send the channel an embed giving the user more info: -- cgit v1.2.3 From 3189afa9a02fc0333b4d814a48f17122af768345 Mon Sep 17 00:00:00 2001 From: kosayoda Date: Sat, 5 Sep 2020 09:03:46 +0800 Subject: Add test for everyone_ping rule. --- tests/bot/rules/test_everyone_ping.py | 102 ++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 tests/bot/rules/test_everyone_ping.py diff --git a/tests/bot/rules/test_everyone_ping.py b/tests/bot/rules/test_everyone_ping.py new file mode 100644 index 000000000..3ecc43cdc --- /dev/null +++ b/tests/bot/rules/test_everyone_ping.py @@ -0,0 +1,102 @@ +from typing import Iterable + +from bot.rules import everyone_ping +from tests.bot.rules import DisallowedCase, RuleTest +from tests.helpers import MockGuild, MockMessage + +NUM_GUILD_MEMBERS = 100 + + +def make_msg(author: str, message: str) -> MockMessage: + """Build a message with `message` as the content sent.""" + mocked_guild = MockGuild(member_count=NUM_GUILD_MEMBERS) + return MockMessage(author=author, content=message, guild=mocked_guild) + + +class EveryonePingRuleTest(RuleTest): + """Tests the `everyone_ping` antispam rule.""" + + def setUp(self): + self.apply = everyone_ping.apply + self.config = { + "max": 0, # Max allowed @everyone pings per user + "interval": 10, + } + + async def test_disallows_everyone_ping(self): + """Cases with an @everyone ping.""" + cases = ( + DisallowedCase( + [make_msg("bob", "@everyone")], + ("bob",), + 1 + ), + DisallowedCase( + [make_msg("bob", "Let me ping @everyone in the server.")], + ("bob",), + 1 + ), + DisallowedCase( + [make_msg("bob", "`codeblock message` and @everyone ping")], + ("bob",), + 1 + ), + DisallowedCase( + [make_msg("bob", "`sandwich` @everyone `ping between codeblocks`.")], + ("bob",), + 1 + ), + DisallowedCase( + [make_msg("bob", "This is a multiline\n@everyone\nping.")], + ("bob",), + 1 + ), + # Not actually valid code blocks + DisallowedCase( + [make_msg("bob", "`@everyone``")], + ("bob",), + 1 + ), + DisallowedCase( + [make_msg("bob", "`@everyone``````")], + ("bob",), + 1 + ), + DisallowedCase( + [make_msg("bob", "``@everyone``````")], + ("bob",), + 1 + ), + ) + + await self.run_disallowed(cases) + + async def test_allows_inline_codeblock_everyone_ping(self): + """Cases with an @everyone ping in an inline codeblock.""" + cases = ( + [make_msg("bob", "Codeblock has `@everyone` ping.")], + [make_msg("bob", "Multiple `codeblocks` including `@everyone` ping.")], + [make_msg("bob", "This is a valid ``inline @everyone` ping.")], + ) + + await self.run_allowed(cases) + + async def test_allows_multiline_codeblock_everyone_ping(self): + """Cases with an @everyone ping in a multiline codeblock.""" + cases = ( + [make_msg("bob", "```Multiline codeblock has\nan `@everyone` ping.```")], + [make_msg("bob", "``` `@everyone``` ` `")], + ) + + await self.run_allowed(cases) + + def relevant_messages(self, case: DisallowedCase) -> Iterable[MockMessage]: + last_message = case.recent_messages[0] + return tuple( + msg + for msg in case.recent_messages + if msg.author == last_message.author + ) + + def get_report(self, case: DisallowedCase) -> str: + return "pinged the everyone role" -- cgit v1.2.3 From 8c39b74fa757e5770ee74bb8013b23dc1347a0d5 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 5 Sep 2020 19:16:54 -0700 Subject: Remove with_role decorator in favour of has_any_role `with_role` is obsolete because discord.py provides `has_any_role`. --- bot/cogs/bot.py | 11 +++++------ bot/cogs/clean.py | 17 ++++++++--------- bot/cogs/defcon.py | 13 ++++++------- bot/cogs/doc.py | 7 +++---- bot/cogs/eval.py | 7 +++---- bot/cogs/information.py | 8 ++++---- bot/cogs/jams.py | 3 +-- bot/cogs/off_topic_names.py | 15 +++++++-------- bot/cogs/reddit.py | 5 ++--- bot/cogs/utils.py | 6 +++--- bot/cogs/watchchannels/bigbrother.py | 13 ++++++------- bot/cogs/watchchannels/talentpool.py | 19 +++++++++---------- bot/decorators.py | 10 +--------- 13 files changed, 58 insertions(+), 76 deletions(-) diff --git a/bot/cogs/bot.py b/bot/cogs/bot.py index ddd1cef8d..6b8269729 100644 --- a/bot/cogs/bot.py +++ b/bot/cogs/bot.py @@ -5,13 +5,12 @@ import time from typing import Optional, Tuple from discord import Embed, Message, RawMessageUpdateEvent, TextChannel -from discord.ext.commands import Cog, Context, command, group +from discord.ext.commands import Cog, Context, command, group, has_any_role from bot.bot import Bot from bot.cogs.token_remover import TokenRemover from bot.cogs.webhook_remover import WEBHOOK_URL_RE from bot.constants import Categories, Channels, DEBUG_MODE, Guild, MODERATION_ROLES, Roles, URLs -from bot.decorators import with_role from bot.utils.messages import wait_for_deletion log = logging.getLogger(__name__) @@ -39,13 +38,13 @@ class BotCog(Cog, name="Bot"): self.codeblock_message_ids = {} @group(invoke_without_command=True, name="bot", hidden=True) - @with_role(Roles.verified) + @has_any_role(Roles.verified) async def botinfo_group(self, ctx: Context) -> None: """Bot informational commands.""" await ctx.send_help(ctx.command) @botinfo_group.command(name='about', aliases=('info',), hidden=True) - @with_role(Roles.verified) + @has_any_role(Roles.verified) async def about_command(self, ctx: Context) -> None: """Get information about the bot.""" embed = Embed( @@ -63,7 +62,7 @@ class BotCog(Cog, name="Bot"): await ctx.send(embed=embed) @command(name='echo', aliases=('print',)) - @with_role(*MODERATION_ROLES) + @has_any_role(*MODERATION_ROLES) async def echo_command(self, ctx: Context, channel: Optional[TextChannel], *, text: str) -> None: """Repeat the given message in either a specified channel or the current channel.""" if channel is None: @@ -72,7 +71,7 @@ class BotCog(Cog, name="Bot"): await channel.send(text) @command(name='embed') - @with_role(*MODERATION_ROLES) + @has_any_role(*MODERATION_ROLES) async def embed_command(self, ctx: Context, channel: Optional[TextChannel], *, text: str) -> None: """Send the input within an embed to either a specified channel or the current channel.""" embed = Embed(description=text) diff --git a/bot/cogs/clean.py b/bot/cogs/clean.py index f436e531a..7f8873e36 100644 --- a/bot/cogs/clean.py +++ b/bot/cogs/clean.py @@ -5,14 +5,13 @@ from typing import Iterable, Optional from discord import Colour, Embed, Message, TextChannel, User from discord.ext import commands -from discord.ext.commands import Cog, Context, group +from discord.ext.commands import Cog, Context, group, has_any_role from bot.bot import Bot from bot.cogs.moderation import ModLog from bot.constants import ( Channels, CleanMessages, Colours, Event, Icons, MODERATION_ROLES, NEGATIVE_REPLIES ) -from bot.decorators import with_role log = logging.getLogger(__name__) @@ -192,13 +191,13 @@ class Clean(Cog): ) @group(invoke_without_command=True, name="clean", aliases=["purge"]) - @with_role(*MODERATION_ROLES) + @has_any_role(*MODERATION_ROLES) async def clean_group(self, ctx: Context) -> None: """Commands for cleaning messages in channels.""" await ctx.send_help(ctx.command) @clean_group.command(name="user", aliases=["users"]) - @with_role(*MODERATION_ROLES) + @has_any_role(*MODERATION_ROLES) async def clean_user( self, ctx: Context, @@ -210,7 +209,7 @@ class Clean(Cog): await self._clean_messages(amount, ctx, user=user, channels=channels) @clean_group.command(name="all", aliases=["everything"]) - @with_role(*MODERATION_ROLES) + @has_any_role(*MODERATION_ROLES) async def clean_all( self, ctx: Context, @@ -221,7 +220,7 @@ class Clean(Cog): await self._clean_messages(amount, ctx, channels=channels) @clean_group.command(name="bots", aliases=["bot"]) - @with_role(*MODERATION_ROLES) + @has_any_role(*MODERATION_ROLES) async def clean_bots( self, ctx: Context, @@ -232,7 +231,7 @@ class Clean(Cog): await self._clean_messages(amount, ctx, bots_only=True, channels=channels) @clean_group.command(name="regex", aliases=["word", "expression"]) - @with_role(*MODERATION_ROLES) + @has_any_role(*MODERATION_ROLES) async def clean_regex( self, ctx: Context, @@ -244,7 +243,7 @@ class Clean(Cog): await self._clean_messages(amount, ctx, regex=regex, channels=channels) @clean_group.command(name="message", aliases=["messages"]) - @with_role(*MODERATION_ROLES) + @has_any_role(*MODERATION_ROLES) async def clean_message(self, ctx: Context, message: Message) -> None: """Delete all messages until certain message, stop cleaning after hitting the `message`.""" await self._clean_messages( @@ -255,7 +254,7 @@ class Clean(Cog): ) @clean_group.command(name="stop", aliases=["cancel", "abort"]) - @with_role(*MODERATION_ROLES) + @has_any_role(*MODERATION_ROLES) async def clean_cancel(self, ctx: Context) -> None: """If there is an ongoing cleaning process, attempt to immediately cancel it.""" self.cleaning = False diff --git a/bot/cogs/defcon.py b/bot/cogs/defcon.py index 9087ac454..64d47c6c6 100644 --- a/bot/cogs/defcon.py +++ b/bot/cogs/defcon.py @@ -6,12 +6,11 @@ from datetime import datetime, timedelta from enum import Enum from discord import Colour, Embed, Member -from discord.ext.commands import Cog, Context, group +from discord.ext.commands import Cog, Context, group, has_any_role from bot.bot import Bot from bot.cogs.moderation import ModLog from bot.constants import Channels, Colours, Emojis, Event, Icons, MODERATION_ROLES, Roles -from bot.decorators import with_role log = logging.getLogger(__name__) @@ -119,7 +118,7 @@ class Defcon(Cog): ) @group(name='defcon', aliases=('dc',), invoke_without_command=True) - @with_role(*MODERATION_ROLES) + @has_any_role(*MODERATION_ROLES) async def defcon_group(self, ctx: Context) -> None: """Check the DEFCON status or run a subcommand.""" await ctx.send_help(ctx.command) @@ -163,7 +162,7 @@ class Defcon(Cog): self.bot.stats.gauge("defcon.threshold", days) @defcon_group.command(name='enable', aliases=('on', 'e'), root_aliases=("defon",)) - @with_role(*MODERATION_ROLES) + @has_any_role(*MODERATION_ROLES) async def enable_command(self, ctx: Context) -> None: """ Enable DEFCON mode. Useful in a pinch, but be sure you know what you're doing! @@ -176,7 +175,7 @@ class Defcon(Cog): await self.update_channel_topic() @defcon_group.command(name='disable', aliases=('off', 'd'), root_aliases=("defoff",)) - @with_role(*MODERATION_ROLES) + @has_any_role(*MODERATION_ROLES) async def disable_command(self, ctx: Context) -> None: """Disable DEFCON mode. Useful in a pinch, but be sure you know what you're doing!""" self.enabled = False @@ -184,7 +183,7 @@ class Defcon(Cog): await self.update_channel_topic() @defcon_group.command(name='status', aliases=('s',)) - @with_role(*MODERATION_ROLES) + @has_any_role(*MODERATION_ROLES) async def status_command(self, ctx: Context) -> None: """Check the current status of DEFCON mode.""" embed = Embed( @@ -196,7 +195,7 @@ class Defcon(Cog): await ctx.send(embed=embed) @defcon_group.command(name='days') - @with_role(*MODERATION_ROLES) + @has_any_role(*MODERATION_ROLES) async def days_command(self, ctx: Context, days: int) -> None: """Set how old an account must be to join the server, in days, with DEFCON mode enabled.""" self.days = timedelta(days=days) diff --git a/bot/cogs/doc.py b/bot/cogs/doc.py index 30c793c75..e50b9b32b 100644 --- a/bot/cogs/doc.py +++ b/bot/cogs/doc.py @@ -21,7 +21,6 @@ from urllib3.exceptions import ProtocolError from bot.bot import Bot from bot.constants import MODERATION_ROLES, RedirectOutput from bot.converters import ValidPythonIdentifier, ValidURL -from bot.decorators import with_role from bot.pagination import LinePaginator from bot.utils.messages import wait_for_deletion @@ -396,7 +395,7 @@ class Doc(commands.Cog): await wait_for_deletion(msg, (ctx.author.id,), client=self.bot) @docs_group.command(name='set', aliases=('s',)) - @with_role(*MODERATION_ROLES) + @commands.has_any_role(*MODERATION_ROLES) async def set_command( self, ctx: commands.Context, package_name: ValidPythonIdentifier, base_url: ValidURL, inventory_url: InventoryURL @@ -433,7 +432,7 @@ class Doc(commands.Cog): await ctx.send(f"Added package `{package_name}` to database and refreshed inventory.") @docs_group.command(name='delete', aliases=('remove', 'rm', 'd')) - @with_role(*MODERATION_ROLES) + @commands.has_any_role(*MODERATION_ROLES) async def delete_command(self, ctx: commands.Context, package_name: ValidPythonIdentifier) -> None: """ Removes the specified package from the database. @@ -450,7 +449,7 @@ class Doc(commands.Cog): await ctx.send(f"Successfully deleted `{package_name}` and refreshed inventory.") @docs_group.command(name="refresh", aliases=("rfsh", "r")) - @with_role(*MODERATION_ROLES) + @commands.has_any_role(*MODERATION_ROLES) async def refresh_command(self, ctx: commands.Context) -> None: """Refresh inventories and send differences to channel.""" old_inventories = set(self.base_urls) diff --git a/bot/cogs/eval.py b/bot/cogs/eval.py index eb8bfb1cf..468831365 100644 --- a/bot/cogs/eval.py +++ b/bot/cogs/eval.py @@ -9,11 +9,10 @@ from io import StringIO from typing import Any, Optional, Tuple import discord -from discord.ext.commands import Cog, Context, group +from discord.ext.commands import Cog, Context, group, has_any_role from bot.bot import Bot from bot.constants import Roles -from bot.decorators import with_role from bot.interpreter import Interpreter log = logging.getLogger(__name__) @@ -174,14 +173,14 @@ async def func(): # (None,) -> Any await ctx.send(f"```py\n{out}```", embed=embed) @group(name='internal', aliases=('int',)) - @with_role(Roles.owners, Roles.admins) + @has_any_role(Roles.owners, Roles.admins) async def internal_group(self, ctx: Context) -> None: """Internal commands. Top secret!""" if not ctx.invoked_subcommand: await ctx.send_help(ctx.command) @internal_group.command(name='eval', aliases=('e',)) - @with_role(Roles.admins, Roles.owners) + @has_any_role(Roles.admins, Roles.owners) async def eval(self, ctx: Context, *, code: str) -> None: """Run eval in a REPL-like format.""" code = code.strip("`") diff --git a/bot/cogs/information.py b/bot/cogs/information.py index 55ecb2836..abfbcb84e 100644 --- a/bot/cogs/information.py +++ b/bot/cogs/information.py @@ -8,12 +8,12 @@ from typing import Any, Mapping, Optional, Tuple, Union from discord import ChannelType, Colour, CustomActivity, Embed, Guild, Member, Message, Role, Status, utils from discord.abc import GuildChannel -from discord.ext.commands import BucketType, Cog, Context, Paginator, command, group +from discord.ext.commands import BucketType, Cog, Context, Paginator, command, group, has_any_role from discord.utils import escape_markdown from bot import constants from bot.bot import Bot -from bot.decorators import in_whitelist, with_role +from bot.decorators import in_whitelist from bot.pagination import LinePaginator from bot.utils.checks import InWhitelistCheckFailure, cooldown_with_role_bypass, with_role_check from bot.utils.time import time_since @@ -76,7 +76,7 @@ class Information(Cog): channel_type_list = sorted(channel_type_list) return "\n".join(channel_type_list) - @with_role(*constants.MODERATION_ROLES) + @has_any_role(*constants.MODERATION_ROLES) @command(name="roles") async def roles_info(self, ctx: Context) -> None: """Returns a list of all roles and their corresponding IDs.""" @@ -96,7 +96,7 @@ class Information(Cog): await LinePaginator.paginate(role_list, ctx, embed, empty=False) - @with_role(*constants.MODERATION_ROLES) + @has_any_role(*constants.MODERATION_ROLES) @command(name="role") async def role_info(self, ctx: Context, *roles: Union[Role, str]) -> None: """ diff --git a/bot/cogs/jams.py b/bot/cogs/jams.py index b3102db2f..1c0988343 100644 --- a/bot/cogs/jams.py +++ b/bot/cogs/jams.py @@ -7,7 +7,6 @@ from more_itertools import unique_everseen from bot.bot import Bot from bot.constants import Roles -from bot.decorators import with_role log = logging.getLogger(__name__) @@ -22,7 +21,7 @@ class CodeJams(commands.Cog): self.bot = bot @commands.command() - @with_role(Roles.admins) + @commands.has_any_role(Roles.admins) async def createteam(self, ctx: commands.Context, team_name: str, members: commands.Greedy[Member]) -> None: """ Create team channels (voice and text) in the Code Jams category, assign roles, and add overwrites for the team. diff --git a/bot/cogs/off_topic_names.py b/bot/cogs/off_topic_names.py index ce95450e0..b9d235fa2 100644 --- a/bot/cogs/off_topic_names.py +++ b/bot/cogs/off_topic_names.py @@ -4,13 +4,12 @@ import logging from datetime import datetime, timedelta from discord import Colour, Embed -from discord.ext.commands import Cog, Context, group +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, MODERATION_ROLES from bot.converters import OffTopicName -from bot.decorators import with_role from bot.pagination import LinePaginator CHANNELS = (Channels.off_topic_0, Channels.off_topic_1, Channels.off_topic_2) @@ -67,13 +66,13 @@ class OffTopicNames(Cog): self.updater_task = self.bot.loop.create_task(coro) @group(name='otname', aliases=('otnames', 'otn'), invoke_without_command=True) - @with_role(*MODERATION_ROLES) + @has_any_role(*MODERATION_ROLES) async def otname_group(self, ctx: Context) -> None: """Add or list items from the off-topic channel name rotation.""" await ctx.send_help(ctx.command) @otname_group.command(name='add', aliases=('a',)) - @with_role(*MODERATION_ROLES) + @has_any_role(*MODERATION_ROLES) async def add_command(self, ctx: Context, *, name: OffTopicName) -> None: """ Adds a new off-topic name to the rotation. @@ -96,7 +95,7 @@ class OffTopicNames(Cog): await self._add_name(ctx, name) @otname_group.command(name='forceadd', aliases=('fa',)) - @with_role(*MODERATION_ROLES) + @has_any_role(*MODERATION_ROLES) async def force_add_command(self, ctx: Context, *, name: OffTopicName) -> None: """Forcefully adds a new off-topic name to the rotation.""" await self._add_name(ctx, name) @@ -109,7 +108,7 @@ class OffTopicNames(Cog): await ctx.send(f":ok_hand: Added `{name}` to the names list.") @otname_group.command(name='delete', aliases=('remove', 'rm', 'del', 'd')) - @with_role(*MODERATION_ROLES) + @has_any_role(*MODERATION_ROLES) async def delete_command(self, ctx: Context, *, name: OffTopicName) -> None: """Removes a off-topic name from the rotation.""" await self.bot.api_client.delete(f'bot/off-topic-channel-names/{name}') @@ -118,7 +117,7 @@ class OffTopicNames(Cog): await ctx.send(f":ok_hand: Removed `{name}` from the names list.") @otname_group.command(name='list', aliases=('l',)) - @with_role(*MODERATION_ROLES) + @has_any_role(*MODERATION_ROLES) async def list_command(self, ctx: Context) -> None: """ Lists all currently known off-topic channel names in a paginator. @@ -138,7 +137,7 @@ class OffTopicNames(Cog): await ctx.send(embed=embed) @otname_group.command(name='search', aliases=('s',)) - @with_role(*MODERATION_ROLES) + @has_any_role(*MODERATION_ROLES) async def search_command(self, ctx: Context, *, query: OffTopicName) -> None: """Search for an off-topic name.""" result = await self.bot.api_client.get('bot/off-topic-channel-names') diff --git a/bot/cogs/reddit.py b/bot/cogs/reddit.py index 5d9e2c20b..635162308 100644 --- a/bot/cogs/reddit.py +++ b/bot/cogs/reddit.py @@ -8,14 +8,13 @@ from typing import List from aiohttp import BasicAuth, ClientError from discord import Colour, Embed, TextChannel -from discord.ext.commands import Cog, Context, group +from discord.ext.commands import Cog, Context, group, has_any_role from discord.ext.tasks import loop from discord.utils import escape_markdown from bot.bot import Bot from bot.constants import Channels, ERROR_REPLIES, Emojis, Reddit as RedditConfig, STAFF_ROLES, Webhooks from bot.converters import Subreddit -from bot.decorators import with_role from bot.pagination import LinePaginator from bot.utils.messages import sub_clyde @@ -282,7 +281,7 @@ class Reddit(Cog): await ctx.send(content=f"Here are this week's top {subreddit} posts!", embed=embed) - @with_role(*STAFF_ROLES) + @has_any_role(*STAFF_ROLES) @reddit_group.command(name="subreddits", aliases=("subs",)) async def subreddits_command(self, ctx: Context) -> None: """Send a paginated embed of all the subreddits we're relaying.""" diff --git a/bot/cogs/utils.py b/bot/cogs/utils.py index d96abbd5a..6b6941064 100644 --- a/bot/cogs/utils.py +++ b/bot/cogs/utils.py @@ -7,11 +7,11 @@ from io import StringIO from typing import Tuple, Union from discord import Colour, Embed, utils -from discord.ext.commands import BadArgument, Cog, Context, clean_content, command +from discord.ext.commands import BadArgument, Cog, Context, clean_content, command, has_any_role from bot.bot import Bot from bot.constants import Channels, MODERATION_ROLES, STAFF_ROLES -from bot.decorators import in_whitelist, with_role +from bot.decorators import in_whitelist from bot.pagination import LinePaginator from bot.utils import messages @@ -224,7 +224,7 @@ class Utils(Cog): await ctx.send(embed=embed) @command(aliases=("poll",)) - @with_role(*MODERATION_ROLES) + @has_any_role(*MODERATION_ROLES) async def vote(self, ctx: Context, title: clean_content(fix_channel_mentions=True), *options: str) -> None: """ Build a quick voting poll with matching reactions with the provided options. diff --git a/bot/cogs/watchchannels/bigbrother.py b/bot/cogs/watchchannels/bigbrother.py index 11ab8917a..af0354cf8 100644 --- a/bot/cogs/watchchannels/bigbrother.py +++ b/bot/cogs/watchchannels/bigbrother.py @@ -2,13 +2,12 @@ import logging import textwrap from collections import ChainMap -from discord.ext.commands import Cog, Context, group +from discord.ext.commands import Cog, Context, group, has_any_role from bot.bot import Bot from bot.cogs.moderation.utils import post_infraction from bot.constants import Channels, MODERATION_ROLES, Webhooks from bot.converters import FetchedMember -from bot.decorators import with_role from .watchchannel import WatchChannel log = logging.getLogger(__name__) @@ -28,13 +27,13 @@ class BigBrother(WatchChannel, Cog, name="Big Brother"): ) @group(name='bigbrother', aliases=('bb',), invoke_without_command=True) - @with_role(*MODERATION_ROLES) + @has_any_role(*MODERATION_ROLES) async def bigbrother_group(self, ctx: Context) -> None: """Monitors users by relaying their messages to the Big Brother watch channel.""" await ctx.send_help(ctx.command) @bigbrother_group.command(name='watched', aliases=('all', 'list')) - @with_role(*MODERATION_ROLES) + @has_any_role(*MODERATION_ROLES) async def watched_command( self, ctx: Context, oldest_first: bool = False, update_cache: bool = True ) -> None: @@ -49,7 +48,7 @@ class BigBrother(WatchChannel, Cog, name="Big Brother"): await self.list_watched_users(ctx, oldest_first=oldest_first, update_cache=update_cache) @bigbrother_group.command(name='oldest') - @with_role(*MODERATION_ROLES) + @has_any_role(*MODERATION_ROLES) async def oldest_command(self, ctx: Context, update_cache: bool = True) -> None: """ Shows Big Brother monitored users ordered by oldest watched. @@ -60,7 +59,7 @@ class BigBrother(WatchChannel, Cog, name="Big Brother"): await ctx.invoke(self.watched_command, oldest_first=True, update_cache=update_cache) @bigbrother_group.command(name='watch', aliases=('w',), root_aliases=('watch',)) - @with_role(*MODERATION_ROLES) + @has_any_role(*MODERATION_ROLES) async def watch_command(self, ctx: Context, user: FetchedMember, *, reason: str) -> None: """ Relay messages sent by the given `user` to the `#big-brother` channel. @@ -71,7 +70,7 @@ class BigBrother(WatchChannel, Cog, name="Big Brother"): await self.apply_watch(ctx, user, reason) @bigbrother_group.command(name='unwatch', aliases=('uw',), root_aliases=('unwatch',)) - @with_role(*MODERATION_ROLES) + @has_any_role(*MODERATION_ROLES) async def unwatch_command(self, ctx: Context, user: FetchedMember, *, reason: str) -> None: """Stop relaying messages by the given `user`.""" await self.apply_unwatch(ctx, user, reason) diff --git a/bot/cogs/watchchannels/talentpool.py b/bot/cogs/watchchannels/talentpool.py index 76d6fe9bd..d0a829f4e 100644 --- a/bot/cogs/watchchannels/talentpool.py +++ b/bot/cogs/watchchannels/talentpool.py @@ -4,13 +4,12 @@ from collections import ChainMap from typing import Union from discord import Color, Embed, Member, User -from discord.ext.commands import Cog, Context, group +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, Guild, MODERATION_ROLES, STAFF_ROLES, Webhooks from bot.converters import FetchedMember -from bot.decorators import with_role from bot.pagination import LinePaginator from bot.utils import time from .watchchannel import WatchChannel @@ -32,13 +31,13 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): ) @group(name='talentpool', aliases=('tp', 'talent', 'nomination', 'n'), invoke_without_command=True) - @with_role(*MODERATION_ROLES) + @has_any_role(*MODERATION_ROLES) async def nomination_group(self, ctx: Context) -> None: """Highlights the activity of helper nominees by relaying their messages to the talent pool channel.""" await ctx.send_help(ctx.command) @nomination_group.command(name='watched', aliases=('all', 'list'), root_aliases=("nominees",)) - @with_role(*MODERATION_ROLES) + @has_any_role(*MODERATION_ROLES) async def watched_command( self, ctx: Context, oldest_first: bool = False, update_cache: bool = True ) -> None: @@ -53,7 +52,7 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): await self.list_watched_users(ctx, oldest_first=oldest_first, update_cache=update_cache) @nomination_group.command(name='oldest') - @with_role(*MODERATION_ROLES) + @has_any_role(*MODERATION_ROLES) async def oldest_command(self, ctx: Context, update_cache: bool = True) -> None: """ Shows talent pool monitored users ordered by oldest nomination. @@ -64,7 +63,7 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): await ctx.invoke(self.watched_command, oldest_first=True, update_cache=update_cache) @nomination_group.command(name='watch', aliases=('w', 'add', 'a'), root_aliases=("nominate",)) - @with_role(*STAFF_ROLES) + @has_any_role(*STAFF_ROLES) async def watch_command(self, ctx: Context, user: FetchedMember, *, reason: str) -> None: """ Relay messages sent by the given `user` to the `#talent-pool` channel. @@ -129,7 +128,7 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): await ctx.send(msg) @nomination_group.command(name='history', aliases=('info', 'search')) - @with_role(*MODERATION_ROLES) + @has_any_role(*MODERATION_ROLES) async def history_command(self, ctx: Context, user: FetchedMember) -> None: """Shows the specified user's nomination history.""" result = await self.bot.api_client.get( @@ -158,7 +157,7 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): ) @nomination_group.command(name='unwatch', aliases=('end', ), root_aliases=("unnominate",)) - @with_role(*MODERATION_ROLES) + @has_any_role(*MODERATION_ROLES) async def unwatch_command(self, ctx: Context, user: FetchedMember, *, reason: str) -> None: """ Ends the active nomination of the specified user with the given reason. @@ -171,13 +170,13 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): await ctx.send(":x: The specified user does not have an active nomination") @nomination_group.group(name='edit', aliases=('e',), invoke_without_command=True) - @with_role(*MODERATION_ROLES) + @has_any_role(*MODERATION_ROLES) async def nomination_edit_group(self, ctx: Context) -> None: """Commands to edit nominations.""" await ctx.send_help(ctx.command) @nomination_edit_group.command(name='reason') - @with_role(*MODERATION_ROLES) + @has_any_role(*MODERATION_ROLES) async def edit_reason_command(self, ctx: Context, nomination_id: int, *, reason: str) -> None: """ Edits the reason/unnominate reason for the nomination with the given `id` depending on the status. diff --git a/bot/decorators.py b/bot/decorators.py index 500197c89..bdb224039 100644 --- a/bot/decorators.py +++ b/bot/decorators.py @@ -12,7 +12,7 @@ from discord.ext import commands from discord.ext.commands import Cog, Context from bot.constants import Channels, ERROR_REPLIES, RedirectOutput -from bot.utils.checks import in_whitelist_check, with_role_check, without_role_check +from bot.utils.checks import in_whitelist_check, without_role_check log = logging.getLogger(__name__) @@ -45,14 +45,6 @@ def in_whitelist( return commands.check(predicate) -def with_role(*role_ids: int) -> Callable: - """Returns True if the user has any one of the roles in role_ids.""" - async def predicate(ctx: Context) -> bool: - """With role checker predicate.""" - return with_role_check(ctx, *role_ids) - return commands.check(predicate) - - def without_role(*role_ids: int) -> Callable: """Returns True if the user does not have any of the roles in role_ids.""" async def predicate(ctx: Context) -> bool: -- cgit v1.2.3 From 24657d4fc84208b4ab334db5c2a2c91cb2449219 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 5 Sep 2020 19:53:08 -0700 Subject: Implement the without_role decorator by negating has_any_role --- bot/decorators.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/bot/decorators.py b/bot/decorators.py index bdb224039..56028ad8a 100644 --- a/bot/decorators.py +++ b/bot/decorators.py @@ -6,13 +6,12 @@ from functools import wraps from typing import Callable, Container, Optional, Union from weakref import WeakValueDictionary -from discord import Colour, Embed, Member -from discord.errors import NotFound +from discord import Colour, Embed, Member, NotFound from discord.ext import commands from discord.ext.commands import Cog, Context from bot.constants import Channels, ERROR_REPLIES, RedirectOutput -from bot.utils.checks import in_whitelist_check, without_role_check +from bot.utils.checks import in_whitelist_check log = logging.getLogger(__name__) @@ -45,10 +44,22 @@ def in_whitelist( return commands.check(predicate) -def without_role(*role_ids: int) -> Callable: - """Returns True if the user does not have any of the roles in role_ids.""" +def without_role(*roles: Union[str, int]) -> Callable: + """ + Returns True if the user does not have any of the roles specified. + + `roles` are the names or IDs of the disallowed roles. + """ async def predicate(ctx: Context) -> bool: - return without_role_check(ctx, *role_ids) + try: + await commands.has_any_role(*roles).predicate(ctx) + except commands.MissingAnyRole: + return True + else: + # This error is never shown to users, so don't bother trying to make it too pretty. + roles_ = ", ".join(f"'{item}'" for item in roles) + raise commands.CheckFailure(f"You have at least one of the disallowed roles: {roles_}") + return commands.check(predicate) -- cgit v1.2.3 From b0a2ebb87986856240b5e20967424d035998ac77 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 5 Sep 2020 20:08:59 -0700 Subject: Use has_any_role's predicate directly In some places, it's more appropriate than using with_role_check since it will raise CheckFailures. This applies to `cog_check`s and other things which effectively act as command checks. --- bot/cogs/dm_relay.py | 6 +++--- bot/cogs/extensions.py | 5 ++--- bot/cogs/filter_lists.py | 7 +++---- bot/cogs/help_channels.py | 7 +++---- bot/cogs/moderation/infractions.py | 5 ++--- bot/cogs/moderation/management.py | 6 +++--- bot/cogs/moderation/silence.py | 5 ++--- bot/cogs/moderation/slowmode.py | 7 +++---- bot/cogs/moderation/superstarify.py | 7 +++---- 9 files changed, 24 insertions(+), 31 deletions(-) diff --git a/bot/cogs/dm_relay.py b/bot/cogs/dm_relay.py index 0d8f340b4..7a3fe49bb 100644 --- a/bot/cogs/dm_relay.py +++ b/bot/cogs/dm_relay.py @@ -10,7 +10,7 @@ from bot import constants from bot.bot import Bot from bot.converters import UserMentionOrID from bot.utils import RedisCache -from bot.utils.checks import in_whitelist_check, with_role_check +from bot.utils.checks import in_whitelist_check from bot.utils.messages import send_attachments from bot.utils.webhooks import send_webhook @@ -105,10 +105,10 @@ class DMRelay(Cog): except discord.HTTPException: log.exception("Failed to send an attachment to the webhook") - def cog_check(self, ctx: commands.Context) -> bool: + async def cog_check(self, ctx: commands.Context) -> bool: """Only allow moderators to invoke the commands in this cog.""" checks = [ - with_role_check(ctx, *constants.MODERATION_ROLES), + await commands.has_any_role(*constants.MODERATION_ROLES).predicate(ctx), in_whitelist_check( ctx, channels=[constants.Channels.dm_log], diff --git a/bot/cogs/extensions.py b/bot/cogs/extensions.py index 396e406b0..5977e6f3c 100644 --- a/bot/cogs/extensions.py +++ b/bot/cogs/extensions.py @@ -11,7 +11,6 @@ from discord.ext.commands import Context, group from bot.bot import Bot from bot.constants import Emojis, MODERATION_ROLES, Roles, URLs from bot.pagination import LinePaginator -from bot.utils.checks import with_role_check log = logging.getLogger(__name__) @@ -219,9 +218,9 @@ class Extensions(commands.Cog): return msg, error_msg # This cannot be static (must have a __func__ attribute). - def cog_check(self, ctx: Context) -> bool: + async def cog_check(self, ctx: Context) -> bool: """Only allow moderators and core developers to invoke the commands in this cog.""" - return with_role_check(ctx, *MODERATION_ROLES, Roles.core_developers) + return await commands.has_any_role(*MODERATION_ROLES, Roles.core_developers).predicate(ctx) # This cannot be static (must have a __func__ attribute). async def cog_command_error(self, ctx: Context, error: Exception) -> None: diff --git a/bot/cogs/filter_lists.py b/bot/cogs/filter_lists.py index c15adc461..232c1e48b 100644 --- a/bot/cogs/filter_lists.py +++ b/bot/cogs/filter_lists.py @@ -2,14 +2,13 @@ import logging from typing import Optional from discord import Colour, Embed -from discord.ext.commands import BadArgument, Cog, Context, IDConverter, group +from discord.ext.commands import BadArgument, Cog, Context, IDConverter, group, has_any_role from bot import constants from bot.api import ResponseCodeError from bot.bot import Bot from bot.converters import ValidDiscordServerInvite, ValidFilterListType from bot.pagination import LinePaginator -from bot.utils.checks import with_role_check log = logging.getLogger(__name__) @@ -263,9 +262,9 @@ class FilterLists(Cog): """Syncs both allowlists and denylists with the API.""" await self._sync_data(ctx) - def cog_check(self, ctx: Context) -> bool: + async def cog_check(self, ctx: Context) -> bool: """Only allow moderators to invoke the commands in this cog.""" - return with_role_check(ctx, *constants.MODERATION_ROLES) + return await has_any_role(*constants.MODERATION_ROLES).predicate(ctx) def setup(bot: Bot) -> None: diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 0f9cac89e..17142071f 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -14,7 +14,6 @@ from discord.ext import commands from bot import constants from bot.bot import Bot from bot.utils import RedisCache -from bot.utils.checks import with_role_check from bot.utils.scheduling import Scheduler log = logging.getLogger(__name__) @@ -196,12 +195,12 @@ class HelpChannels(commands.Cog): return True log.trace(f"{ctx.author} is not the help channel claimant, checking roles.") - role_check = with_role_check(ctx, *constants.HelpChannels.cmd_whitelist) + has_role = await commands.has_any_role(*constants.HelpChannels.cmd_whitelist).predicate(ctx) - if role_check: + if has_role: self.bot.stats.incr("help.dormant_invoke.staff") - return role_check + return has_role @commands.command(name="close", aliases=["dormant", "solved"], enabled=False) async def close_command(self, ctx: commands.Context) -> None: diff --git a/bot/cogs/moderation/infractions.py b/bot/cogs/moderation/infractions.py index 8df642428..8f0def2bc 100644 --- a/bot/cogs/moderation/infractions.py +++ b/bot/cogs/moderation/infractions.py @@ -12,7 +12,6 @@ from bot.bot import Bot from bot.constants import Event from bot.converters import Expiry, FetchedMember from bot.decorators import respect_role_hierarchy -from bot.utils.checks import with_role_check from . import utils from .scheduler import InfractionScheduler from .utils import UserSnowflake @@ -357,9 +356,9 @@ class Infractions(InfractionScheduler, commands.Cog): # endregion # This cannot be static (must have a __func__ attribute). - def cog_check(self, ctx: Context) -> bool: + async def cog_check(self, ctx: Context) -> bool: """Only allow moderators to invoke the commands in this cog.""" - return with_role_check(ctx, *constants.MODERATION_ROLES) + return await commands.has_any_role(*constants.MODERATION_ROLES).predicate(ctx) # This cannot be static (must have a __func__ attribute). async def cog_command_error(self, ctx: Context, error: Exception) -> None: diff --git a/bot/cogs/moderation/management.py b/bot/cogs/moderation/management.py index 672bb0e9c..83342ac90 100644 --- a/bot/cogs/moderation/management.py +++ b/bot/cogs/moderation/management.py @@ -12,7 +12,7 @@ from bot.bot import Bot from bot.converters import Expiry, InfractionSearchQuery, allowed_strings, proxy_user from bot.pagination import LinePaginator from bot.utils import time -from bot.utils.checks import in_whitelist_check, with_role_check +from bot.utils.checks import in_whitelist_check from . import utils from .infractions import Infractions from .modlog import ModLog @@ -282,10 +282,10 @@ class ModManagement(commands.Cog): # endregion # This cannot be static (must have a __func__ attribute). - def cog_check(self, ctx: Context) -> bool: + async def cog_check(self, ctx: Context) -> bool: """Only allow moderators inside moderator channels to invoke the commands in this cog.""" checks = [ - with_role_check(ctx, *constants.MODERATION_ROLES), + await commands.has_any_role(*constants.MODERATION_ROLES).predicate(ctx), in_whitelist_check( ctx, channels=constants.MODERATION_CHANNELS, diff --git a/bot/cogs/moderation/silence.py b/bot/cogs/moderation/silence.py index f8a6592bc..ecc9f8d22 100644 --- a/bot/cogs/moderation/silence.py +++ b/bot/cogs/moderation/silence.py @@ -10,7 +10,6 @@ from discord.ext.commands import Context from bot.bot import Bot from bot.constants import Channels, Emojis, Guild, MODERATION_ROLES, Roles from bot.converters import HushDurationConverter -from bot.utils.checks import with_role_check from bot.utils.scheduling import Scheduler log = logging.getLogger(__name__) @@ -160,6 +159,6 @@ class Silence(commands.Cog): asyncio.create_task(self._mod_alerts_channel.send(message)) # This cannot be static (must have a __func__ attribute). - def cog_check(self, ctx: Context) -> bool: + async def cog_check(self, ctx: Context) -> bool: """Only allow moderators to invoke the commands in this cog.""" - return with_role_check(ctx, *MODERATION_ROLES) + return await commands.has_any_role(*MODERATION_ROLES).predicate(ctx) diff --git a/bot/cogs/moderation/slowmode.py b/bot/cogs/moderation/slowmode.py index 1d055afac..efd862aa5 100644 --- a/bot/cogs/moderation/slowmode.py +++ b/bot/cogs/moderation/slowmode.py @@ -4,12 +4,11 @@ from typing import Optional from dateutil.relativedelta import relativedelta from discord import TextChannel -from discord.ext.commands import Cog, Context, group +from discord.ext.commands import Cog, Context, group, has_any_role from bot.bot import Bot from bot.constants import Emojis, MODERATION_ROLES from bot.converters import DurationDelta -from bot.decorators import with_role_check from bot.utils import time log = logging.getLogger(__name__) @@ -87,9 +86,9 @@ class Slowmode(Cog): f'{Emojis.check_mark} The slowmode delay for {channel.mention} has been reset to 0 seconds.' ) - def cog_check(self, ctx: Context) -> bool: + async def cog_check(self, ctx: Context) -> bool: """Only allow moderators to invoke the commands in this cog.""" - return with_role_check(ctx, *MODERATION_ROLES) + return await has_any_role(*MODERATION_ROLES).predicate(ctx) def setup(bot: Bot) -> None: diff --git a/bot/cogs/moderation/superstarify.py b/bot/cogs/moderation/superstarify.py index 867de815a..081c2d0b9 100644 --- a/bot/cogs/moderation/superstarify.py +++ b/bot/cogs/moderation/superstarify.py @@ -6,12 +6,11 @@ import typing as t from pathlib import Path from discord import Colour, Embed, Member -from discord.ext.commands import Cog, Context, command +from discord.ext.commands import Cog, Context, command, has_any_role from bot import constants from bot.bot import Bot from bot.converters import Expiry -from bot.utils.checks import with_role_check from bot.utils.time import format_infraction from . import utils from .scheduler import InfractionScheduler @@ -234,6 +233,6 @@ class Superstarify(InfractionScheduler, Cog): return rng.choice(STAR_NAMES) # This cannot be static (must have a __func__ attribute). - def cog_check(self, ctx: Context) -> bool: + async def cog_check(self, ctx: Context) -> bool: """Only allow moderators to invoke the commands in this cog.""" - return with_role_check(ctx, *constants.MODERATION_ROLES) + return await has_any_role(*constants.MODERATION_ROLES).predicate(ctx) -- cgit v1.2.3 From 0c6215f3f6e7247a5e13199931f733a8e203047e Mon Sep 17 00:00:00 2001 From: kosayoda Date: Mon, 7 Sep 2020 21:08:54 +0800 Subject: Remove everyone_ping rule from antispam. The feature will be moved to the filtering cog. --- bot/cogs/antispam.py | 1 - bot/rules/__init__.py | 1 - bot/rules/everyone_ping.py | 44 --------------- config-default.yml | 4 -- tests/bot/rules/test_everyone_ping.py | 102 ---------------------------------- 5 files changed, 152 deletions(-) delete mode 100644 bot/rules/everyone_ping.py delete mode 100644 tests/bot/rules/test_everyone_ping.py diff --git a/bot/cogs/antispam.py b/bot/cogs/antispam.py index b8939113f..5c97621fb 100644 --- a/bot/cogs/antispam.py +++ b/bot/cogs/antispam.py @@ -36,7 +36,6 @@ RULE_FUNCTION_MAPPING = { 'mentions': rules.apply_mentions, 'newlines': rules.apply_newlines, 'role_mentions': rules.apply_role_mentions, - 'everyone_ping': rules.apply_everyone_ping, } diff --git a/bot/rules/__init__.py b/bot/rules/__init__.py index 8a69cadee..a01ceae73 100644 --- a/bot/rules/__init__.py +++ b/bot/rules/__init__.py @@ -10,4 +10,3 @@ from .links import apply as apply_links from .mentions import apply as apply_mentions from .newlines import apply as apply_newlines from .role_mentions import apply as apply_role_mentions -from .everyone_ping import apply as apply_everyone_ping diff --git a/bot/rules/everyone_ping.py b/bot/rules/everyone_ping.py deleted file mode 100644 index 8fc03b924..000000000 --- a/bot/rules/everyone_ping.py +++ /dev/null @@ -1,44 +0,0 @@ -import random -import re -from typing import Dict, Iterable, List, Optional, Tuple - -from discord import Embed, Member, Message - -from bot.constants import Colours, Guild, NEGATIVE_REPLIES - -# Generate regex for checking for pings: -guild_id = Guild.id -EVERYONE_PING_RE = re.compile(rf"@everyone|<@&{guild_id}>") -CODE_BLOCK_RE = re.compile( - r"(?P``?)[^`]+?(?P=delim)(?!`+)" # Inline codeblock - r"|```(.+?)```", # Multiline codeblock - re.DOTALL | re.MULTILINE -) - - -async def apply( - last_message: Message, - recent_messages: List[Message], - config: Dict[str, int], -) -> Optional[Tuple[str, Iterable[Member], Iterable[Message]]]: - """Detects if a user has sent an '@everyone' ping.""" - relevant_messages = tuple(msg for msg in recent_messages if msg.author == last_message.author) - - everyone_messages_count = 0 - for msg in relevant_messages: - content = CODE_BLOCK_RE.sub("", msg.content) # Remove codeblocks in the message - if matches := len(EVERYONE_PING_RE.findall(content)): - everyone_messages_count += matches - - if everyone_messages_count > config["max"]: - # Send the channel an embed giving the user more info: - embed_text = f"Please don't try to ping {last_message.guild.member_count:,} people." - embed = Embed(title=random.choice(NEGATIVE_REPLIES), description=embed_text, colour=Colours.soft_red) - await last_message.channel.send(embed=embed) - - return ( - "pinged the everyone role", - (last_message.author,), - relevant_messages, - ) - return None diff --git a/config-default.yml b/config-default.yml index e9324c62f..c1eef713f 100644 --- a/config-default.yml +++ b/config-default.yml @@ -389,10 +389,6 @@ anti_spam: interval: 10 max: 3 - everyone_ping: - interval: 10 - max: 0 - reddit: subreddits: diff --git a/tests/bot/rules/test_everyone_ping.py b/tests/bot/rules/test_everyone_ping.py deleted file mode 100644 index 3ecc43cdc..000000000 --- a/tests/bot/rules/test_everyone_ping.py +++ /dev/null @@ -1,102 +0,0 @@ -from typing import Iterable - -from bot.rules import everyone_ping -from tests.bot.rules import DisallowedCase, RuleTest -from tests.helpers import MockGuild, MockMessage - -NUM_GUILD_MEMBERS = 100 - - -def make_msg(author: str, message: str) -> MockMessage: - """Build a message with `message` as the content sent.""" - mocked_guild = MockGuild(member_count=NUM_GUILD_MEMBERS) - return MockMessage(author=author, content=message, guild=mocked_guild) - - -class EveryonePingRuleTest(RuleTest): - """Tests the `everyone_ping` antispam rule.""" - - def setUp(self): - self.apply = everyone_ping.apply - self.config = { - "max": 0, # Max allowed @everyone pings per user - "interval": 10, - } - - async def test_disallows_everyone_ping(self): - """Cases with an @everyone ping.""" - cases = ( - DisallowedCase( - [make_msg("bob", "@everyone")], - ("bob",), - 1 - ), - DisallowedCase( - [make_msg("bob", "Let me ping @everyone in the server.")], - ("bob",), - 1 - ), - DisallowedCase( - [make_msg("bob", "`codeblock message` and @everyone ping")], - ("bob",), - 1 - ), - DisallowedCase( - [make_msg("bob", "`sandwich` @everyone `ping between codeblocks`.")], - ("bob",), - 1 - ), - DisallowedCase( - [make_msg("bob", "This is a multiline\n@everyone\nping.")], - ("bob",), - 1 - ), - # Not actually valid code blocks - DisallowedCase( - [make_msg("bob", "`@everyone``")], - ("bob",), - 1 - ), - DisallowedCase( - [make_msg("bob", "`@everyone``````")], - ("bob",), - 1 - ), - DisallowedCase( - [make_msg("bob", "``@everyone``````")], - ("bob",), - 1 - ), - ) - - await self.run_disallowed(cases) - - async def test_allows_inline_codeblock_everyone_ping(self): - """Cases with an @everyone ping in an inline codeblock.""" - cases = ( - [make_msg("bob", "Codeblock has `@everyone` ping.")], - [make_msg("bob", "Multiple `codeblocks` including `@everyone` ping.")], - [make_msg("bob", "This is a valid ``inline @everyone` ping.")], - ) - - await self.run_allowed(cases) - - async def test_allows_multiline_codeblock_everyone_ping(self): - """Cases with an @everyone ping in a multiline codeblock.""" - cases = ( - [make_msg("bob", "```Multiline codeblock has\nan `@everyone` ping.```")], - [make_msg("bob", "``` `@everyone``` ` `")], - ) - - await self.run_allowed(cases) - - def relevant_messages(self, case: DisallowedCase) -> Iterable[MockMessage]: - last_message = case.recent_messages[0] - return tuple( - msg - for msg in case.recent_messages - if msg.author == last_message.author - ) - - def get_report(self, case: DisallowedCase) -> str: - return "pinged the everyone role" -- cgit v1.2.3 From 8e161a3ef7921c794e128df45302fb3a9e28bd2b Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sun, 6 Sep 2020 09:07:59 -0700 Subject: Implement role checks using has_any_role Use `has_any_role` to reduce redundancy. Because discord.py always makes a check's predicate a coroutine, the checks now have to be awaited. --- bot/cogs/information.py | 7 ++++--- bot/cogs/reminders.py | 8 ++++---- bot/cogs/verification.py | 5 +++-- bot/utils/checks.py | 49 ++++++++++++++++++++++++------------------------ 4 files changed, 35 insertions(+), 34 deletions(-) diff --git a/bot/cogs/information.py b/bot/cogs/information.py index abfbcb84e..5b132a469 100644 --- a/bot/cogs/information.py +++ b/bot/cogs/information.py @@ -15,11 +15,12 @@ from bot import constants from bot.bot import Bot from bot.decorators import in_whitelist from bot.pagination import LinePaginator -from bot.utils.checks import InWhitelistCheckFailure, cooldown_with_role_bypass, with_role_check +from bot.utils.checks import InWhitelistCheckFailure, cooldown_with_role_bypass, without_role_check from bot.utils.time import time_since log = logging.getLogger(__name__) + STATUS_EMOTES = { Status.offline: constants.Emojis.status_offline, Status.dnd: constants.Emojis.status_dnd, @@ -197,12 +198,12 @@ class Information(Cog): user = ctx.author # Do a role check if this is being executed on someone other than the caller - elif user != ctx.author and not with_role_check(ctx, *constants.MODERATION_ROLES): + elif user != ctx.author and await without_role_check(ctx, *constants.MODERATION_ROLES): await ctx.send("You may not use this command on users other than yourself.") return # Non-staff may only do this in #bot-commands - if not with_role_check(ctx, *constants.STAFF_ROLES): + if await without_role_check(ctx, *constants.STAFF_ROLES): if not ctx.channel.id == constants.Channels.bot_commands: raise InWhitelistCheckFailure(constants.Channels.bot_commands) diff --git a/bot/cogs/reminders.py b/bot/cogs/reminders.py index 08bce2153..d7357e3fa 100644 --- a/bot/cogs/reminders.py +++ b/bot/cogs/reminders.py @@ -117,9 +117,9 @@ class Reminders(Cog): If mentions aren't allowed, also return the type of mention(s) disallowed. """ - if without_role_check(ctx, *STAFF_ROLES): + if await without_role_check(ctx, *STAFF_ROLES): return False, "members/roles" - elif without_role_check(ctx, *MODERATION_ROLES): + elif await without_role_check(ctx, *MODERATION_ROLES): return all(isinstance(mention, discord.Member) for mention in mentions), "roles" else: return True, "" @@ -240,7 +240,7 @@ class Reminders(Cog): Expiration is parsed per: http://strftime.org/ """ # If the user is not staff, we need to verify whether or not to make a reminder at all. - if without_role_check(ctx, *STAFF_ROLES): + if await without_role_check(ctx, *STAFF_ROLES): # If they don't have permission to set a reminder in this channel if ctx.channel.id not in WHITELISTED_CHANNELS: @@ -431,7 +431,7 @@ class Reminders(Cog): The check passes when the user is an admin, or if they created the reminder. """ - if with_role_check(ctx, Roles.admins): + if await with_role_check(ctx, Roles.admins): return True api_response = await self.bot.api_client.get(f"bot/reminders/{reminder_id}") diff --git a/bot/cogs/verification.py b/bot/cogs/verification.py index ae156cf70..afbe1d3b8 100644 --- a/bot/cogs/verification.py +++ b/bot/cogs/verification.py @@ -178,9 +178,10 @@ class Verification(Cog): error.handled = True @staticmethod - def bot_check(ctx: Context) -> bool: + async def bot_check(ctx: Context) -> bool: """Block any command within the verification channel that is not !accept.""" - if ctx.channel.id == constants.Channels.verification and without_role_check(ctx, *constants.MODERATION_ROLES): + is_verification = ctx.channel.id == constants.Channels.verification + if is_verification and await without_role_check(ctx, *constants.MODERATION_ROLES): return ctx.command.name == "accept" else: return True diff --git a/bot/utils/checks.py b/bot/utils/checks.py index f0ef36302..c2e41efb3 100644 --- a/bot/utils/checks.py +++ b/bot/utils/checks.py @@ -1,6 +1,6 @@ import datetime import logging -from typing import Callable, Container, Iterable, Optional +from typing import Callable, Container, Iterable, Optional, Union from discord.ext.commands import ( BucketType, @@ -11,6 +11,8 @@ from discord.ext.commands import ( Context, Cooldown, CooldownMapping, + NoPrivateMessage, + has_any_role, ) from bot import constants @@ -89,35 +91,32 @@ def in_whitelist_check( return False -def with_role_check(ctx: Context, *role_ids: int) -> bool: - """Returns True if the user has any one of the roles in role_ids.""" - if not ctx.guild: # Return False in a DM - log.trace(f"{ctx.author} tried to use the '{ctx.command.name}'command from a DM. " - "This command is restricted by the with_role decorator. Rejecting request.") - return False +async def with_role_check(ctx: Context, *roles: Union[str, int]) -> bool: + """ + Returns True if the context's author has any of the specified roles. - for role in ctx.author.roles: - if role.id in role_ids: - log.trace(f"{ctx.author} has the '{role.name}' role, and passes the check.") - return True + `roles` are the names or IDs of the roles for which to check. + False is always returns if the context is outside a guild. + """ + try: + return await has_any_role(*roles).predicate(ctx) + except CheckFailure: + return False - log.trace(f"{ctx.author} does not have the required role to use " - f"the '{ctx.command.name}' command, so the request is rejected.") - return False +async def without_role_check(ctx: Context, *roles: Union[str, int]) -> bool: + """ + Returns True if the context's author doesn't have any of the specified roles. -def without_role_check(ctx: Context, *role_ids: int) -> bool: - """Returns True if the user does not have any of the roles in role_ids.""" - if not ctx.guild: # Return False in a DM - log.trace(f"{ctx.author} tried to use the '{ctx.command.name}' command from a DM. " - "This command is restricted by the without_role decorator. Rejecting request.") + `roles` are the names or IDs of the roles for which to check. + False is always returns if the context is outside a guild. + """ + try: + return not await has_any_role(*roles).predicate(ctx) + except NoPrivateMessage: return False - - author_roles = [role.id for role in ctx.author.roles] - check = all(role not in author_roles for role in role_ids) - log.trace(f"{ctx.author} tried to call the '{ctx.command.name}' command. " - f"The result of the without_role check was {check}.") - return check + except CheckFailure: + return True def cooldown_with_role_bypass(rate: int, per: float, type: BucketType = BucketType.default, *, -- cgit v1.2.3 From ab4eb39fcdde62cc9f558f9e41a5f48d9a587d38 Mon Sep 17 00:00:00 2001 From: kosayoda Date: Tue, 8 Sep 2020 12:04:20 +0800 Subject: Add everyone_ping filter. --- bot/cogs/filtering.py | 38 +++++++++++++++++++++++++++++++++++--- bot/constants.py | 2 ++ config-default.yml | 18 ++++++++++-------- 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/bot/cogs/filtering.py b/bot/cogs/filtering.py index 99b659bff..aa0cbad97 100644 --- a/bot/cogs/filtering.py +++ b/bot/cogs/filtering.py @@ -15,8 +15,8 @@ from bot.api import ResponseCodeError from bot.bot import Bot from bot.cogs.moderation import ModLog from bot.constants import ( - Channels, Colours, - Filter, Icons, URLs + Channels, Colours, Filter, + Guild, Icons, URLs ) from bot.utils.redis_cache import RedisCache from bot.utils.regex import INVITE_RE @@ -25,6 +25,12 @@ from bot.utils.scheduling import Scheduler log = logging.getLogger(__name__) # Regular expressions +CODE_BLOCK_RE = re.compile( + r"(?P``?)[^`]+?(?P=delim)(?!`+)" # Inline codeblock + r"|```(.+?)```", # Multiline codeblock + re.DOTALL | re.MULTILINE +) +EVERYONE_PING_RE = re.compile(rf"@everyone|<@&{Guild.id}>|@here") SPOILER_RE = re.compile(r"(\|\|.+?\|\|)", re.DOTALL) URL_RE = re.compile(r"(https?://[^\s]+)", flags=re.IGNORECASE) ZALGO_RE = re.compile(r"[\u0300-\u036F\u0489]") @@ -82,6 +88,19 @@ class Filtering(Cog): ), "schedule_deletion": False }, + "filter_everyone_ping": { + "enabled": Filter.filter_everyone_ping, + "function": self._has_everyone_ping, + "type": "filter", + "content_only": True, + "user_notification": Filter.notify_user_everyone_ping, + "notification_msg": ( + "Please don't try to ping `@everyone` or `@here`. " + f"Your message has been removed. {staff_mistake_str}" + ), + "schedule_deletion": False, + "ping_everyone": False + }, "watch_regex": { "enabled": Filter.watch_regex, "function": self._has_watch_regex_match, @@ -332,6 +351,9 @@ class Filtering(Cog): log.debug(message) + # Allow specific filters to override ping_everyone + ping_everyone = Filter.ping_everyone and _filter.get("ping_everyone", True) + # Send pretty mod log embed to mod-alerts await self.mod_log.send_log_message( icon_url=Icons.filtering, @@ -340,7 +362,7 @@ class Filtering(Cog): text=message, thumbnail=msg.author.avatar_url_as(static_format="png"), channel_id=Channels.mod_alerts, - ping_everyone=Filter.ping_everyone if not is_private else False, + ping_everyone=ping_everyone if not is_private else False, additional_embeds=additional_embeds, additional_embeds_msg=additional_embeds_msg ) @@ -528,6 +550,16 @@ class Filtering(Cog): return False return False + @staticmethod + async def _has_everyone_ping(text: str) -> bool: + """Determines if `msg` contains an @everyone or @here ping outside of a codeblock.""" + # First pass to avoid running re.sub on every message + if not EVERYONE_PING_RE.search(text): + return False + + content_without_codeblocks = CODE_BLOCK_RE.sub("", text) + return bool(EVERYONE_PING_RE.search(content_without_codeblocks)) + async def notify_member(self, filtered_member: Member, reason: str, channel: TextChannel) -> None: """ Notify filtered_member about a moderation action with the reason str. diff --git a/bot/constants.py b/bot/constants.py index 17fe34e95..70b36984f 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -217,6 +217,7 @@ class Filter(metaclass=YAMLGetter): filter_zalgo: bool filter_invites: bool filter_domains: bool + filter_everyone_ping: bool watch_regex: bool watch_rich_embeds: bool @@ -224,6 +225,7 @@ class Filter(metaclass=YAMLGetter): notify_user_zalgo: bool notify_user_invites: bool notify_user_domains: bool + notify_user_everyone_ping: bool ping_everyone: bool offensive_msg_delete_days: int diff --git a/config-default.yml b/config-default.yml index c1eef713f..cf9ce8798 100644 --- a/config-default.yml +++ b/config-default.yml @@ -273,17 +273,19 @@ guild: filter: # What do we filter? - filter_zalgo: false - filter_invites: true - filter_domains: true - watch_regex: true - watch_rich_embeds: true + filter_zalgo: false + filter_invites: true + filter_domains: true + filter_everyone_ping: true + watch_regex: true + watch_rich_embeds: true # Notify user on filter? # Notifications are not expected for "watchlist" type filters - notify_user_zalgo: false - notify_user_invites: true - notify_user_domains: false + notify_user_zalgo: false + notify_user_invites: true + notify_user_domains: false + notify_user_everyone_ping: true # Filter configuration ping_everyone: true -- cgit v1.2.3 From f7d436dfba90d1e8e92c2bf32440098aa266fbd3 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sun, 6 Sep 2020 09:11:17 -0700 Subject: Rename role checks and decorators Make their names more in line with `has_any_role` for consistency. --- bot/cogs/information.py | 6 +++--- bot/cogs/reminders.py | 10 +++++----- bot/cogs/verification.py | 8 ++++---- bot/decorators.py | 2 +- bot/utils/checks.py | 4 ++-- tests/bot/utils/test_checks.py | 36 ++++++++++++++++++------------------ 6 files changed, 33 insertions(+), 33 deletions(-) diff --git a/bot/cogs/information.py b/bot/cogs/information.py index 5b132a469..581b3a227 100644 --- a/bot/cogs/information.py +++ b/bot/cogs/information.py @@ -15,7 +15,7 @@ from bot import constants from bot.bot import Bot from bot.decorators import in_whitelist from bot.pagination import LinePaginator -from bot.utils.checks import InWhitelistCheckFailure, cooldown_with_role_bypass, without_role_check +from bot.utils.checks import InWhitelistCheckFailure, cooldown_with_role_bypass, has_no_roles_check from bot.utils.time import time_since log = logging.getLogger(__name__) @@ -198,12 +198,12 @@ class Information(Cog): user = ctx.author # Do a role check if this is being executed on someone other than the caller - elif user != ctx.author and await without_role_check(ctx, *constants.MODERATION_ROLES): + elif user != ctx.author and await has_no_roles_check(ctx, *constants.MODERATION_ROLES): await ctx.send("You may not use this command on users other than yourself.") return # Non-staff may only do this in #bot-commands - if await without_role_check(ctx, *constants.STAFF_ROLES): + if await has_no_roles_check(ctx, *constants.STAFF_ROLES): if not ctx.channel.id == constants.Channels.bot_commands: raise InWhitelistCheckFailure(constants.Channels.bot_commands) diff --git a/bot/cogs/reminders.py b/bot/cogs/reminders.py index d7357e3fa..6806f2889 100644 --- a/bot/cogs/reminders.py +++ b/bot/cogs/reminders.py @@ -15,7 +15,7 @@ from bot.bot import Bot from bot.constants import Guild, Icons, MODERATION_ROLES, POSITIVE_REPLIES, Roles, STAFF_ROLES from bot.converters import Duration from bot.pagination import LinePaginator -from bot.utils.checks import with_role_check, without_role_check +from bot.utils.checks import has_any_role_check, has_no_roles_check from bot.utils.messages import send_denial from bot.utils.scheduling import Scheduler from bot.utils.time import humanize_delta @@ -117,9 +117,9 @@ class Reminders(Cog): If mentions aren't allowed, also return the type of mention(s) disallowed. """ - if await without_role_check(ctx, *STAFF_ROLES): + if await has_no_roles_check(ctx, *STAFF_ROLES): return False, "members/roles" - elif await without_role_check(ctx, *MODERATION_ROLES): + elif await has_no_roles_check(ctx, *MODERATION_ROLES): return all(isinstance(mention, discord.Member) for mention in mentions), "roles" else: return True, "" @@ -240,7 +240,7 @@ class Reminders(Cog): Expiration is parsed per: http://strftime.org/ """ # If the user is not staff, we need to verify whether or not to make a reminder at all. - if await without_role_check(ctx, *STAFF_ROLES): + if await has_no_roles_check(ctx, *STAFF_ROLES): # If they don't have permission to set a reminder in this channel if ctx.channel.id not in WHITELISTED_CHANNELS: @@ -431,7 +431,7 @@ class Reminders(Cog): The check passes when the user is an admin, or if they created the reminder. """ - if await with_role_check(ctx, Roles.admins): + if await has_any_role_check(ctx, Roles.admins): return True api_response = await self.bot.api_client.get(f"bot/reminders/{reminder_id}") diff --git a/bot/cogs/verification.py b/bot/cogs/verification.py index afbe1d3b8..300c7f315 100644 --- a/bot/cogs/verification.py +++ b/bot/cogs/verification.py @@ -7,8 +7,8 @@ from discord.ext.commands import Cog, Context, command from bot import constants from bot.bot import Bot from bot.cogs.moderation import ModLog -from bot.decorators import in_whitelist, without_role -from bot.utils.checks import InWhitelistCheckFailure, without_role_check +from bot.decorators import has_no_roles, in_whitelist +from bot.utils.checks import InWhitelistCheckFailure, has_no_roles_check log = logging.getLogger(__name__) @@ -107,7 +107,7 @@ class Verification(Cog): await ctx.message.delete() @command(name='accept', aliases=('verify', 'verified', 'accepted'), hidden=True) - @without_role(constants.Roles.verified) + @has_no_roles(constants.Roles.verified) @in_whitelist(channels=(constants.Channels.verification,)) async def accept_command(self, ctx: Context, *_) -> None: # We don't actually care about the args """Accept our rules and gain access to the rest of the server.""" @@ -181,7 +181,7 @@ class Verification(Cog): async def bot_check(ctx: Context) -> bool: """Block any command within the verification channel that is not !accept.""" is_verification = ctx.channel.id == constants.Channels.verification - if is_verification and await without_role_check(ctx, *constants.MODERATION_ROLES): + if is_verification and await has_no_roles_check(ctx, *constants.MODERATION_ROLES): return ctx.command.name == "accept" else: return True diff --git a/bot/decorators.py b/bot/decorators.py index 56028ad8a..2518124da 100644 --- a/bot/decorators.py +++ b/bot/decorators.py @@ -44,7 +44,7 @@ def in_whitelist( return commands.check(predicate) -def without_role(*roles: Union[str, int]) -> Callable: +def has_no_roles(*roles: Union[str, int]) -> Callable: """ Returns True if the user does not have any of the roles specified. diff --git a/bot/utils/checks.py b/bot/utils/checks.py index c2e41efb3..460a937d8 100644 --- a/bot/utils/checks.py +++ b/bot/utils/checks.py @@ -91,7 +91,7 @@ def in_whitelist_check( return False -async def with_role_check(ctx: Context, *roles: Union[str, int]) -> bool: +async def has_any_role_check(ctx: Context, *roles: Union[str, int]) -> bool: """ Returns True if the context's author has any of the specified roles. @@ -104,7 +104,7 @@ async def with_role_check(ctx: Context, *roles: Union[str, int]) -> bool: return False -async def without_role_check(ctx: Context, *roles: Union[str, int]) -> bool: +async def has_no_roles_check(ctx: Context, *roles: Union[str, int]) -> bool: """ Returns True if the context's author doesn't have any of the specified roles. diff --git a/tests/bot/utils/test_checks.py b/tests/bot/utils/test_checks.py index de72e5748..dfee2cf91 100644 --- a/tests/bot/utils/test_checks.py +++ b/tests/bot/utils/test_checks.py @@ -12,37 +12,37 @@ class ChecksTests(unittest.TestCase): def setUp(self): self.ctx = MockContext() - def test_with_role_check_without_guild(self): - """`with_role_check` returns `False` if `Context.guild` is None.""" + def test_has_any_role_check_without_guild(self): + """`has_any_role_check` returns `False` if `Context.guild` is None.""" self.ctx.guild = None - self.assertFalse(checks.with_role_check(self.ctx)) + self.assertFalse(checks.has_any_role_check(self.ctx)) - def test_with_role_check_without_required_roles(self): - """`with_role_check` returns `False` if `Context.author` lacks the required role.""" + def test_has_any_role_check_without_required_roles(self): + """`has_any_role_check` returns `False` if `Context.author` lacks the required role.""" self.ctx.author.roles = [] - self.assertFalse(checks.with_role_check(self.ctx)) + self.assertFalse(checks.has_any_role_check(self.ctx)) - def test_with_role_check_with_guild_and_required_role(self): - """`with_role_check` returns `True` if `Context.author` has the required role.""" + def test_has_any_role_check_with_guild_and_required_role(self): + """`has_any_role_check` returns `True` if `Context.author` has the required role.""" self.ctx.author.roles.append(MockRole(id=10)) - self.assertTrue(checks.with_role_check(self.ctx, 10)) + self.assertTrue(checks.has_any_role_check(self.ctx, 10)) - def test_without_role_check_without_guild(self): - """`without_role_check` should return `False` when `Context.guild` is None.""" + def test_has_no_roles_check_without_guild(self): + """`has_no_roles_check` should return `False` when `Context.guild` is None.""" self.ctx.guild = None - self.assertFalse(checks.without_role_check(self.ctx)) + self.assertFalse(checks.has_no_roles_check(self.ctx)) - def test_without_role_check_returns_false_with_unwanted_role(self): - """`without_role_check` returns `False` if `Context.author` has unwanted role.""" + def test_has_no_roles_check_returns_false_with_unwanted_role(self): + """`has_no_roles_check` returns `False` if `Context.author` has unwanted role.""" role_id = 42 self.ctx.author.roles.append(MockRole(id=role_id)) - self.assertFalse(checks.without_role_check(self.ctx, role_id)) + self.assertFalse(checks.has_no_roles_check(self.ctx, role_id)) - def test_without_role_check_returns_true_without_unwanted_role(self): - """`without_role_check` returns `True` if `Context.author` does not have unwanted role.""" + def test_has_no_roles_check_returns_true_without_unwanted_role(self): + """`has_no_roles_check` returns `True` if `Context.author` does not have unwanted role.""" role_id = 42 self.ctx.author.roles.append(MockRole(id=role_id)) - self.assertTrue(checks.without_role_check(self.ctx, role_id + 10)) + self.assertTrue(checks.has_no_roles_check(self.ctx, role_id + 10)) def test_in_whitelist_check_correct_channel(self): """`in_whitelist_check` returns `True` if `Context.channel.id` is in the channel list.""" -- cgit v1.2.3 From 7741e9fd9054cb302e2d65afc8303db01c8eed7e Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sun, 6 Sep 2020 09:14:11 -0700 Subject: Fix tests for has_any_role_check --- tests/bot/utils/test_checks.py | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/tests/bot/utils/test_checks.py b/tests/bot/utils/test_checks.py index dfee2cf91..883465e0b 100644 --- a/tests/bot/utils/test_checks.py +++ b/tests/bot/utils/test_checks.py @@ -1,48 +1,50 @@ import unittest from unittest.mock import MagicMock +from discord import DMChannel + from bot.utils import checks from bot.utils.checks import InWhitelistCheckFailure from tests.helpers import MockContext, MockRole -class ChecksTests(unittest.TestCase): +class ChecksTests(unittest.IsolatedAsyncioTestCase): """Tests the check functions defined in `bot.checks`.""" def setUp(self): self.ctx = MockContext() - def test_has_any_role_check_without_guild(self): - """`has_any_role_check` returns `False` if `Context.guild` is None.""" - self.ctx.guild = None - self.assertFalse(checks.has_any_role_check(self.ctx)) + async def test_has_any_role_check_without_guild(self): + """`has_any_role_check` returns `False` for non-guild channels.""" + self.ctx.channel = MagicMock(DMChannel) + self.assertFalse(await checks.has_any_role_check(self.ctx)) - def test_has_any_role_check_without_required_roles(self): + async def test_has_any_role_check_without_required_roles(self): """`has_any_role_check` returns `False` if `Context.author` lacks the required role.""" self.ctx.author.roles = [] - self.assertFalse(checks.has_any_role_check(self.ctx)) + self.assertFalse(await checks.has_any_role_check(self.ctx)) - def test_has_any_role_check_with_guild_and_required_role(self): + async def test_has_any_role_check_with_guild_and_required_role(self): """`has_any_role_check` returns `True` if `Context.author` has the required role.""" self.ctx.author.roles.append(MockRole(id=10)) - self.assertTrue(checks.has_any_role_check(self.ctx, 10)) + self.assertTrue(await checks.has_any_role_check(self.ctx, 10)) - def test_has_no_roles_check_without_guild(self): + async def test_has_no_roles_check_without_guild(self): """`has_no_roles_check` should return `False` when `Context.guild` is None.""" - self.ctx.guild = None - self.assertFalse(checks.has_no_roles_check(self.ctx)) + self.ctx.channel = MagicMock(DMChannel) + self.assertFalse(await checks.has_no_roles_check(self.ctx)) - def test_has_no_roles_check_returns_false_with_unwanted_role(self): + async def test_has_no_roles_check_returns_false_with_unwanted_role(self): """`has_no_roles_check` returns `False` if `Context.author` has unwanted role.""" role_id = 42 self.ctx.author.roles.append(MockRole(id=role_id)) - self.assertFalse(checks.has_no_roles_check(self.ctx, role_id)) + self.assertFalse(await checks.has_no_roles_check(self.ctx, role_id)) - def test_has_no_roles_check_returns_true_without_unwanted_role(self): + async def test_has_no_roles_check_returns_true_without_unwanted_role(self): """`has_no_roles_check` returns `True` if `Context.author` does not have unwanted role.""" role_id = 42 self.ctx.author.roles.append(MockRole(id=role_id)) - self.assertTrue(checks.has_no_roles_check(self.ctx, role_id + 10)) + self.assertTrue(await checks.has_no_roles_check(self.ctx, role_id + 10)) def test_in_whitelist_check_correct_channel(self): """`in_whitelist_check` returns `True` if `Context.channel.id` is in the channel list.""" -- cgit v1.2.3 From 39b6e25ef5b50bfcd16ec1843b4abb72bf125a35 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 8 Sep 2020 10:32:22 -0700 Subject: Fix cog_check tests --- tests/bot/cogs/moderation/test_silence.py | 10 ++++++---- tests/bot/cogs/test_slowmode.py | 10 ++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/tests/bot/cogs/moderation/test_silence.py b/tests/bot/cogs/moderation/test_silence.py index ab3d0742a..5a664b1f8 100644 --- a/tests/bot/cogs/moderation/test_silence.py +++ b/tests/bot/cogs/moderation/test_silence.py @@ -253,9 +253,11 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): self.cog.cog_unload() asyncio_mock.create_task.assert_not_called() - @mock.patch("bot.cogs.moderation.silence.with_role_check") + @mock.patch("discord.ext.commands.has_any_role") @mock.patch("bot.cogs.moderation.silence.MODERATION_ROLES", new=(1, 2, 3)) - def test_cog_check(self, role_check): + async def test_cog_check(self, role_check): """Role check is called with `MODERATION_ROLES`""" - self.cog.cog_check(self.ctx) - role_check.assert_called_once_with(self.ctx, *(1, 2, 3)) + role_check.return_value.predicate = mock.AsyncMock() + await self.cog.cog_check(self.ctx) + role_check.assert_called_once_with(*(1, 2, 3)) + role_check.return_value.predicate.assert_awaited_once_with(self.ctx) diff --git a/tests/bot/cogs/test_slowmode.py b/tests/bot/cogs/test_slowmode.py index f442814c8..5e6d6a26a 100644 --- a/tests/bot/cogs/test_slowmode.py +++ b/tests/bot/cogs/test_slowmode.py @@ -103,9 +103,11 @@ class SlowmodeTests(unittest.IsolatedAsyncioTestCase): f'{Emojis.check_mark} The slowmode delay for #meta has been reset to 0 seconds.' ) - @mock.patch("bot.cogs.moderation.slowmode.with_role_check") + @mock.patch("bot.cogs.moderation.slowmode.has_any_role") @mock.patch("bot.cogs.moderation.slowmode.MODERATION_ROLES", new=(1, 2, 3)) - def test_cog_check(self, role_check): + async def test_cog_check(self, role_check): """Role check is called with `MODERATION_ROLES`""" - self.cog.cog_check(self.ctx) - role_check.assert_called_once_with(self.ctx, *(1, 2, 3)) + role_check.return_value.predicate = mock.AsyncMock() + await self.cog.cog_check(self.ctx) + role_check.assert_called_once_with(*(1, 2, 3)) + role_check.return_value.predicate.assert_awaited_once_with(self.ctx) -- cgit v1.2.3 From c705d6d19506873ab6e21a7301963510833b2b07 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sun, 13 Sep 2020 08:44:49 +0300 Subject: Shorten infraction text when any other field than reason is too long --- bot/cogs/moderation/utils.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bot/cogs/moderation/utils.py b/bot/cogs/moderation/utils.py index 95820404a..4a3c14391 100644 --- a/bot/cogs/moderation/utils.py +++ b/bot/cogs/moderation/utils.py @@ -160,6 +160,10 @@ async def notify_infraction( reason=textwrap.shorten(reason, 1000, placeholder="...") if reason else "No reason provided." ) + # For case when other fields than reason is too long and this reach limit, then force-shorten string + if len(text) > 2048: + text = f"{text[:2045]}..." + embed = discord.Embed( description=text, colour=Colours.soft_red -- cgit v1.2.3 From fc058490d62fa373e6ebb01392c511027401e72f Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff Date: Sat, 19 Sep 2020 21:45:54 +0200 Subject: Use async-rediscache package for our redis caches I've migrated our redis caches over to the async-rediscache package that we've recently released (https://git.pydis.com/async-rediscache). The main functionality remains the same, although the package handles some things, like getting the active session, differently internally. The main changes you'll note for our bot are: - We create a RedisSession instance and ensure that it connects before we even create a Bot instance in `__main__.py`. - We are now actually using a connection pool instead of a single connection. - Our Bot subclass now has a new required kwarg: `redis_session`. - Bool values are now properly converted to and from typestrings. In addition, I've made sure that our MockBot passes a MagicMock for the new `redis_session` kwarg when creating a Bot instance for the spec_set. Signed-off-by: Sebastiaan Zeeff --- Pipfile | 2 +- Pipfile.lock | 243 ++++++++++++++++++++++++---------------------- bot/__main__.py | 20 ++++ bot/bot.py | 49 ++-------- bot/cogs/dm_relay.py | 2 +- bot/cogs/filtering.py | 4 +- bot/cogs/help_channels.py | 2 +- bot/cogs/verification.py | 2 +- tests/helpers.py | 6 +- 9 files changed, 167 insertions(+), 163 deletions(-) diff --git a/Pipfile b/Pipfile index 6fff2223e..8ab5d230e 100644 --- a/Pipfile +++ b/Pipfile @@ -8,12 +8,12 @@ aio-pika = "~=6.1" aiodns = "~=2.0" aiohttp = "~=3.5" aioredis = "~=1.3.1" +"async-rediscache[fakeredis]" = "~=0.1.2" beautifulsoup4 = "~=4.9" colorama = {version = "~=0.4.3",sys_platform = "== 'win32'"} coloredlogs = "~=14.0" deepdiff = "~=4.0" discord.py = "~=1.4.0" -fakeredis = "~=1.4" feedparser = "~=5.2" fuzzywuzzy = "~=0.17" lxml = "~=4.4" diff --git a/Pipfile.lock b/Pipfile.lock index 50ddd478c..97436413d 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "1905fd7eb15074ddbf04f2177b6cdd65edc4c74cb5fcbf4e6ca08ef649ba8a3c" + "sha256": "63eec3557c8bfd42191cb5a7525d6e298471c16863adff485d917e08b72cd787" }, "pipfile-spec": 6, "requires": { @@ -18,11 +18,11 @@ "default": { "aio-pika": { "hashes": [ - "sha256:c4cbbeb85b3c7bf81bc127371846cd949e6231717ce1e6ac7ee1dd5ede21f866", - "sha256:ec7fef24f588d90314873463ab4f2c3debce0bd8830e49e3786586be96bc2e8e" + "sha256:4a20d4d941e1f113a950ea529a90bd9159c8d7aafaa1c71e9c707c8c2b526ea6", + "sha256:7bf3f183df1eb348d007210a0c1a3c5c755f1b3def1a9a395e93f30b91da1daf" ], "index": "pypi", - "version": "==6.6.1" + "version": "==6.7.0" }, "aiodns": { "hashes": [ @@ -73,6 +73,18 @@ ], "version": "==0.7.12" }, + "async-rediscache": { + "extras": [ + "fakeredis" + ], + "hashes": [ + "sha256:407aed1aad97bf22f690eca5369806d22eefc8ca104a52c1f1bd47dd6db45fc2", + "sha256:563aaff79ec611a92a0ad78e39ff159e3a4b4cf0bea41e061de5f3701a17d50c" + ], + "index": "pypi", + "markers": "python_version ~= '3.7'", + "version": "==0.1.2" + }, "async-timeout": { "hashes": [ "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", @@ -83,11 +95,11 @@ }, "attrs": { "hashes": [ - "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", - "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" + "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594", + "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==19.3.0" + "version": "==20.2.0" }, "babel": { "hashes": [ @@ -115,36 +127,36 @@ }, "cffi": { "hashes": [ - "sha256:267adcf6e68d77ba154334a3e4fc921b8e63cbb38ca00d33d40655d4228502bc", - "sha256:26f33e8f6a70c255767e3c3f957ccafc7f1f706b966e110b855bfe944511f1f9", - "sha256:3cd2c044517f38d1b577f05927fb9729d3396f1d44d0c659a445599e79519792", - "sha256:4a03416915b82b81af5502459a8a9dd62a3c299b295dcdf470877cb948d655f2", - "sha256:4ce1e995aeecf7cc32380bc11598bfdfa017d592259d5da00fc7ded11e61d022", - "sha256:4f53e4128c81ca3212ff4cf097c797ab44646a40b42ec02a891155cd7a2ba4d8", - "sha256:4fa72a52a906425416f41738728268072d5acfd48cbe7796af07a923236bcf96", - "sha256:66dd45eb9530e3dde8f7c009f84568bc7cac489b93d04ac86e3111fb46e470c2", - "sha256:6923d077d9ae9e8bacbdb1c07ae78405a9306c8fd1af13bfa06ca891095eb995", - "sha256:833401b15de1bb92791d7b6fb353d4af60dc688eaa521bd97203dcd2d124a7c1", - "sha256:8416ed88ddc057bab0526d4e4e9f3660f614ac2394b5e019a628cdfff3733849", - "sha256:892daa86384994fdf4856cb43c93f40cbe80f7f95bb5da94971b39c7f54b3a9c", - "sha256:98be759efdb5e5fa161e46d404f4e0ce388e72fbf7d9baf010aff16689e22abe", - "sha256:a6d28e7f14ecf3b2ad67c4f106841218c8ab12a0683b1528534a6c87d2307af3", - "sha256:b1d6ebc891607e71fd9da71688fcf332a6630b7f5b7f5549e6e631821c0e5d90", - "sha256:b2a2b0d276a136146e012154baefaea2758ef1f56ae9f4e01c612b0831e0bd2f", - "sha256:b87dfa9f10a470eee7f24234a37d1d5f51e5f5fa9eeffda7c282e2b8f5162eb1", - "sha256:bac0d6f7728a9cc3c1e06d4fcbac12aaa70e9379b3025b27ec1226f0e2d404cf", - "sha256:c991112622baee0ae4d55c008380c32ecfd0ad417bcd0417ba432e6ba7328caa", - "sha256:cda422d54ee7905bfc53ee6915ab68fe7b230cacf581110df4272ee10462aadc", - "sha256:d3148b6ba3923c5850ea197a91a42683f946dba7e8eb82dfa211ab7e708de939", - "sha256:d6033b4ffa34ef70f0b8086fd4c3df4bf801fee485a8a7d4519399818351aa8e", - "sha256:ddff0b2bd7edcc8c82d1adde6dbbf5e60d57ce985402541cd2985c27f7bec2a0", - "sha256:e23cb7f1d8e0f93addf0cae3c5b6f00324cccb4a7949ee558d7b6ca973ab8ae9", - "sha256:effd2ba52cee4ceff1a77f20d2a9f9bf8d50353c854a282b8760ac15b9833168", - "sha256:f90c2267101010de42f7273c94a1f026e56cbc043f9330acd8a80e64300aba33", - "sha256:f960375e9823ae6a07072ff7f8a85954e5a6434f97869f50d0e41649a1c8144f", - "sha256:fcf32bf76dc25e30ed793145a57426064520890d7c02866eb93d3e4abe516948" - ], - "version": "==1.14.1" + "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" }, "chardet": { "hashes": [ @@ -188,11 +200,11 @@ }, "discord.py": { "hashes": [ - "sha256:2b1846bfa382b54f4eace8e437a9f59f185388c5b08749ac0e1bbd98e05bfde5", - "sha256:f3db9531fccc391f51de65cfa46133106a9ba12ff2927aca6c14bffd3b7f17b5" + "sha256:98ea3096a3585c9c379209926f530808f5fcf4930928d8cfb579d2562d119570", + "sha256:f9decb3bfa94613d922376288617e6a6f969260923643e2897f4540c34793442" ], "markers": "python_full_version >= '3.5.3'", - "version": "==1.4.0" + "version": "==1.4.1" }, "docutils": { "hashes": [ @@ -204,11 +216,10 @@ }, "fakeredis": { "hashes": [ - "sha256:790c85ad0f3b2967aba1f51767021bc59760fcb612159584be018ea7384f7fd2", - "sha256:fdfe06f277092d022c271fcaefdc1f0c8d9bfa8cb15374cae41d66a20bd96d2b" + "sha256:7ea0866ba5edb40fe2e9b1722535df0c7e6b91d518aa5f50d96c2fff3ea7f4c2", + "sha256:aad8836ffe0319ffbba66dcf872ac6e7e32d1f19790e31296ba58445efb0a5c7" ], - "index": "pypi", - "version": "==1.4.2" + "version": "==1.4.3" }, "feedparser": { "hashes": [ @@ -350,10 +361,11 @@ }, "markdownify": { "hashes": [ - "sha256:28ce67d1888e4908faaab7b04d2193cda70ea4f902f156a21d0aaea55e63e0a1" + "sha256:30be8340724e706c9e811c27fe8c1542cf74a15b46827924fff5c54b40dd9b0d", + "sha256:a69588194fd76634f0139d6801b820fd652dc5eeba9530e90d323dfdc0155252" ], "index": "pypi", - "version": "==0.4.1" + "version": "==0.5.3" }, "markupsafe": { "hashes": [ @@ -396,11 +408,11 @@ }, "more-itertools": { "hashes": [ - "sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5", - "sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2" + "sha256:6f83822ae94818eae2612063a5101a7311e68ae8002005b5e05f03fd74a86a20", + "sha256:9b30f12df9393f0d28af9210ff8efe48d10c94f73e5daf886f10c4b0b0b4f03c" ], "index": "pypi", - "version": "==8.4.0" + "version": "==8.5.0" }, "multidict": { "hashes": [ @@ -491,11 +503,11 @@ }, "pygments": { "hashes": [ - "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44", - "sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324" + "sha256:307543fe65c0947b126e83dd5a61bd8acbd84abec11f43caebaf5534cbc17998", + "sha256:926c3f319eda178d1bd90851e4317e6d8cdb5e292a3386aac9bd75eca29cf9c7" ], "markers": "python_version >= '3.5'", - "version": "==2.6.1" + "version": "==2.7.1" }, "pyparsing": { "hashes": [ @@ -555,11 +567,11 @@ }, "sentry-sdk": { "hashes": [ - "sha256:21b17d6aa064c0fb703a7c00f77cf6c9c497cf2f83345c28892980a5e742d116", - "sha256:4fc97114c77d005467b9b1a29f042e2bc01923cb683b0ef0bbda46e79fa12532" + "sha256:1a086486ff9da15791f294f6e9915eb3747d161ef64dee2d038a4d0b4a369b24", + "sha256:45486deb031cea6bbb25a540d7adb4dd48cd8a1cc31e6a5ce9fb4f792a572e9a" ], "index": "pypi", - "version": "==0.16.3" + "version": "==0.17.6" }, "six": { "hashes": [ @@ -697,11 +709,11 @@ }, "attrs": { "hashes": [ - "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", - "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" + "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594", + "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==19.3.0" + "version": "==20.2.0" }, "cfgv": { "hashes": [ @@ -713,43 +725,43 @@ }, "coverage": { "hashes": [ - "sha256:098a703d913be6fbd146a8c50cc76513d726b022d170e5e98dc56d958fd592fb", - "sha256:16042dc7f8e632e0dcd5206a5095ebd18cb1d005f4c89694f7f8aafd96dd43a3", - "sha256:1adb6be0dcef0cf9434619d3b892772fdb48e793300f9d762e480e043bd8e716", - "sha256:27ca5a2bc04d68f0776f2cdcb8bbd508bbe430a7bf9c02315cd05fb1d86d0034", - "sha256:28f42dc5172ebdc32622a2c3f7ead1b836cdbf253569ae5673f499e35db0bac3", - "sha256:2fcc8b58953d74d199a1a4d633df8146f0ac36c4e720b4a1997e9b6327af43a8", - "sha256:304fbe451698373dc6653772c72c5d5e883a4aadaf20343592a7abb2e643dae0", - "sha256:30bc103587e0d3df9e52cd9da1dd915265a22fad0b72afe54daf840c984b564f", - "sha256:40f70f81be4d34f8d491e55936904db5c527b0711b2a46513641a5729783c2e4", - "sha256:4186fc95c9febeab5681bc3248553d5ec8c2999b8424d4fc3a39c9cba5796962", - "sha256:46794c815e56f1431c66d81943fa90721bb858375fb36e5903697d5eef88627d", - "sha256:4869ab1c1ed33953bb2433ce7b894a28d724b7aa76c19b11e2878034a4e4680b", - "sha256:4f6428b55d2916a69f8d6453e48a505c07b2245653b0aa9f0dee38785939f5e4", - "sha256:52f185ffd3291196dc1aae506b42e178a592b0b60a8610b108e6ad892cfc1bb3", - "sha256:538f2fd5eb64366f37c97fdb3077d665fa946d2b6d95447622292f38407f9258", - "sha256:64c4f340338c68c463f1b56e3f2f0423f7b17ba6c3febae80b81f0e093077f59", - "sha256:675192fca634f0df69af3493a48224f211f8db4e84452b08d5fcebb9167adb01", - "sha256:700997b77cfab016533b3e7dbc03b71d33ee4df1d79f2463a318ca0263fc29dd", - "sha256:8505e614c983834239f865da2dd336dcf9d72776b951d5dfa5ac36b987726e1b", - "sha256:962c44070c281d86398aeb8f64e1bf37816a4dfc6f4c0f114756b14fc575621d", - "sha256:9e536783a5acee79a9b308be97d3952b662748c4037b6a24cbb339dc7ed8eb89", - "sha256:9ea749fd447ce7fb1ac71f7616371f04054d969d412d37611716721931e36efd", - "sha256:a34cb28e0747ea15e82d13e14de606747e9e484fb28d63c999483f5d5188e89b", - "sha256:a3ee9c793ffefe2944d3a2bd928a0e436cd0ac2d9e3723152d6fd5398838ce7d", - "sha256:aab75d99f3f2874733946a7648ce87a50019eb90baef931698f96b76b6769a46", - "sha256:b1ed2bdb27b4c9fc87058a1cb751c4df8752002143ed393899edb82b131e0546", - "sha256:b360d8fd88d2bad01cb953d81fd2edd4be539df7bfec41e8753fe9f4456a5082", - "sha256:b8f58c7db64d8f27078cbf2a4391af6aa4e4767cc08b37555c4ae064b8558d9b", - "sha256:c1bbb628ed5192124889b51204de27c575b3ffc05a5a91307e7640eff1d48da4", - "sha256:c2ff24df02a125b7b346c4c9078c8936da06964cc2d276292c357d64378158f8", - "sha256:c890728a93fffd0407d7d37c1e6083ff3f9f211c83b4316fae3778417eab9811", - "sha256:c96472b8ca5dc135fb0aa62f79b033f02aa434fb03a8b190600a5ae4102df1fd", - "sha256:ce7866f29d3025b5b34c2e944e66ebef0d92e4a4f2463f7266daa03a1332a651", - "sha256:e26c993bd4b220429d4ec8c1468eca445a4064a61c74ca08da7429af9bc53bb0" - ], - "index": "pypi", - "version": "==5.2.1" + "sha256:0203acd33d2298e19b57451ebb0bed0ab0c602e5cf5a818591b4918b1f97d516", + "sha256:0f313707cdecd5cd3e217fc68c78a960b616604b559e9ea60cc16795c4304259", + "sha256:1c6703094c81fa55b816f5ae542c6ffc625fec769f22b053adb42ad712d086c9", + "sha256:1d44bb3a652fed01f1f2c10d5477956116e9b391320c94d36c6bf13b088a1097", + "sha256:280baa8ec489c4f542f8940f9c4c2181f0306a8ee1a54eceba071a449fb870a0", + "sha256:29a6272fec10623fcbe158fdf9abc7a5fa032048ac1d8631f14b50fbfc10d17f", + "sha256:2b31f46bf7b31e6aa690d4c7a3d51bb262438c6dcb0d528adde446531d0d3bb7", + "sha256:2d43af2be93ffbad25dd959899b5b809618a496926146ce98ee0b23683f8c51c", + "sha256:381ead10b9b9af5f64646cd27107fb27b614ee7040bb1226f9c07ba96625cbb5", + "sha256:47a11bdbd8ada9b7ee628596f9d97fbd3851bd9999d398e9436bd67376dbece7", + "sha256:4d6a42744139a7fa5b46a264874a781e8694bb32f1d76d8137b68138686f1729", + "sha256:50691e744714856f03a86df3e2bff847c2acede4c191f9a1da38f088df342978", + "sha256:530cc8aaf11cc2ac7430f3614b04645662ef20c348dce4167c22d99bec3480e9", + "sha256:582ddfbe712025448206a5bc45855d16c2e491c2dd102ee9a2841418ac1c629f", + "sha256:63808c30b41f3bbf65e29f7280bf793c79f54fb807057de7e5238ffc7cc4d7b9", + "sha256:71b69bd716698fa62cd97137d6f2fdf49f534decb23a2c6fc80813e8b7be6822", + "sha256:7858847f2d84bf6e64c7f66498e851c54de8ea06a6f96a32a1d192d846734418", + "sha256:78e93cc3571fd928a39c0b26767c986188a4118edc67bc0695bc7a284da22e82", + "sha256:7f43286f13d91a34fadf61ae252a51a130223c52bfefb50310d5b2deb062cf0f", + "sha256:86e9f8cd4b0cdd57b4ae71a9c186717daa4c5a99f3238a8723f416256e0b064d", + "sha256:8f264ba2701b8c9f815b272ad568d555ef98dfe1576802ab3149c3629a9f2221", + "sha256:9342dd70a1e151684727c9c91ea003b2fb33523bf19385d4554f7897ca0141d4", + "sha256:9361de40701666b034c59ad9e317bae95c973b9ff92513dd0eced11c6adf2e21", + "sha256:9669179786254a2e7e57f0ecf224e978471491d660aaca833f845b72a2df3709", + "sha256:aac1ba0a253e17889550ddb1b60a2063f7474155465577caa2a3b131224cfd54", + "sha256:aef72eae10b5e3116bac6957de1df4d75909fc76d1499a53fb6387434b6bcd8d", + "sha256:bd3166bb3b111e76a4f8e2980fa1addf2920a4ca9b2b8ca36a3bc3dedc618270", + "sha256:c1b78fb9700fc961f53386ad2fd86d87091e06ede5d118b8a50dea285a071c24", + "sha256:c3888a051226e676e383de03bf49eb633cd39fc829516e5334e69b8d81aae751", + "sha256:c5f17ad25d2c1286436761b462e22b5020d83316f8e8fcb5deb2b3151f8f1d3a", + "sha256:c851b35fc078389bc16b915a0a7c1d5923e12e2c5aeec58c52f4aa8085ac8237", + "sha256:cb7df71de0af56000115eafd000b867d1261f786b5eebd88a0ca6360cccfaca7", + "sha256:cedb2f9e1f990918ea061f28a0f0077a07702e3819602d3507e2ff98c8d20636", + "sha256:e8caf961e1b1a945db76f1b5fa9c91498d15f545ac0ababbe575cfab185d3bd8" + ], + "index": "pypi", + "version": "==5.3" }, "distlib": { "hashes": [ @@ -775,11 +787,11 @@ }, "flake8-annotations": { "hashes": [ - "sha256:7816a5d8f65ffdf37b8e21e5b17e0fd1e492aa92638573276de066e889a22b26", - "sha256:8d18db74a750dd97f40b483cc3ef80d07d03f687525bad8fd83365dcd3bfd414" + "sha256:09fe1aa3f40cb8fef632a0ab3614050a7584bb884b6134e70cf1fc9eeee642fa", + "sha256:5bda552f074fd6e34276c7761756fa07d824ffac91ce9c0a8555eb2bc5b92d7a" ], "index": "pypi", - "version": "==2.3.0" + "version": "==2.4.0" }, "flake8-bugbear": { "hashes": [ @@ -837,11 +849,11 @@ }, "identify": { "hashes": [ - "sha256:110ed090fec6bce1aabe3c72d9258a9de82207adeaa5a05cd75c635880312f9a", - "sha256:ccd88716b890ecbe10920659450a635d2d25de499b9a638525a48b48261d989b" + "sha256:c770074ae1f19e08aadbda1c886bc6d0cb55ffdc503a8c0fe8699af2fc9664ae", + "sha256:d02d004568c5a01261839a05e91705e3e9f5c57a3551648f9b3fb2b9c62c0f62" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.4.25" + "version": "==1.5.3" }, "mccabe": { "hashes": [ @@ -852,9 +864,10 @@ }, "nodeenv": { "hashes": [ - "sha256:4b0b77afa3ba9b54f4b6396e60b0c83f59eaeb2d63dc3cc7a70f7f4af96c82bc" + "sha256:5304d424c529c997bc888453aeaa6362d242b6b4631e90f3d4bf1b290f1c84a9", + "sha256:ab45090ae383b716c4ef89e690c41ff8c2b257b85b309f01f3654df3d084bd7c" ], - "version": "==1.4.0" + "version": "==1.5.0" }, "pep8-naming": { "hashes": [ @@ -866,11 +879,11 @@ }, "pre-commit": { "hashes": [ - "sha256:1657663fdd63a321a4a739915d7d03baedd555b25054449090f97bb0cb30a915", - "sha256:e8b1315c585052e729ab7e99dcca5698266bedce9067d21dc909c23e3ceed626" + "sha256:810aef2a2ba4f31eed1941fc270e72696a1ad5590b9751839c90807d0fff6b9a", + "sha256:c54fd3e574565fe128ecc5e7d2f91279772ddb03f8729645fa812fe809084a70" ], "index": "pypi", - "version": "==2.6.0" + "version": "==2.7.1" }, "pycodestyle": { "hashes": [ @@ -882,11 +895,11 @@ }, "pydocstyle": { "hashes": [ - "sha256:da7831660b7355307b32778c4a0dbfb137d89254ef31a2b2978f50fc0b4d7586", - "sha256:f4f5d210610c2d153fae39093d44224c17429e2ad7da12a8b419aba5c2f614b5" + "sha256:19b86fa8617ed916776a11cd8bc0197e5b9856d5433b777f51a3defe13075325", + "sha256:aca749e190a01726a4fb472dd4ef23b5c9da7b9205c0a7857c06533de13fd678" ], "markers": "python_version >= '3.5'", - "version": "==5.0.2" + "version": "==5.1.1" }, "pyflakes": { "hashes": [ @@ -937,19 +950,19 @@ }, "unittest-xml-reporting": { "hashes": [ - "sha256:74eaf7739a7957a74f52b8187c5616f61157372189bef0a32ba5c30bbc00e58a", - "sha256:e09b8ae70cce9904cdd331f53bf929150962869a5324ab7ff3dd6c8b87e01f7d" + "sha256:7bf515ea8cb244255a25100cd29db611a73f8d3d0aaf672ed3266307e14cc1ca", + "sha256:984cebba69e889401bfe3adb9088ca376b3a1f923f0590d005126c1bffd1a695" ], "index": "pypi", - "version": "==3.0.2" + "version": "==3.0.4" }, "virtualenv": { "hashes": [ - "sha256:7b54fd606a1b85f83de49ad8d80dbec08e983a2d2f96685045b262ebc7481ee5", - "sha256:8cd7b2a4850b003a11be2fc213e206419efab41115cc14bca20e69654f2ac08e" + "sha256:43add625c53c596d38f971a465553f6318decc39d98512bc100fa1b1e839c8dc", + "sha256:e0305af10299a7fb0d69393d8f04cb2965dda9351140d11ac8db4e5e3970451b" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.0.30" + "version": "==20.0.31" } } } diff --git a/bot/__main__.py b/bot/__main__.py index fe2cf90e6..ee627be0a 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -1,7 +1,9 @@ +import asyncio import logging import discord import sentry_sdk +from async_rediscache import RedisSession from discord.ext.commands import when_mentioned_or from sentry_sdk.integrations.aiohttp import AioHttpIntegration from sentry_sdk.integrations.logging import LoggingIntegration @@ -24,8 +26,26 @@ sentry_sdk.init( ] ) +# Create the redis session instance. +redis_session = RedisSession( + address=(constants.Redis.host, constants.Redis.port), + password=constants.Redis.password, + minsize=1, + maxsize=20, + use_fakeredis=constants.Redis.use_fakeredis, +) + +# Connect redis session to ensure it's connected before we try to access Redis +# from somewhere within the bot. We create the event loop in the same way +# discord.py normally does and pass it to the bot's __init__. +loop = asyncio.get_event_loop() +loop.run_until_complete(redis_session.connect()) + + allowed_roles = [discord.Object(id_) for id_ in constants.MODERATION_ROLES] bot = Bot( + redis_session=redis_session, + loop=loop, command_prefix=when_mentioned_or(constants.Bot.prefix), activity=discord.Game(name="Commands: !help"), case_insensitive=True, diff --git a/bot/bot.py b/bot/bot.py index d25074fd9..b2e5237fe 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -6,9 +6,8 @@ from collections import defaultdict from typing import Dict, Optional import aiohttp -import aioredis import discord -import fakeredis.aioredis +from async_rediscache import RedisSession from discord.ext import commands from sentry_sdk import push_scope @@ -21,7 +20,7 @@ log = logging.getLogger('bot') class Bot(commands.Bot): """A subclass of `discord.ext.commands.Bot` with an aiohttp session and an API client.""" - def __init__(self, *args, **kwargs): + def __init__(self, *args, redis_session: RedisSession, **kwargs): if "connector" in kwargs: warnings.warn( "If login() is called (or the bot is started), the connector will be overwritten " @@ -31,9 +30,7 @@ class Bot(commands.Bot): super().__init__(*args, **kwargs) self.http_session: Optional[aiohttp.ClientSession] = None - self.redis_session: Optional[aioredis.Redis] = None - self.redis_ready = asyncio.Event() - self.redis_closed = False + self.redis_session = redis_session self.api_client = api.APIClient(loop=self.loop) self.filter_list_cache = defaultdict(dict) @@ -58,30 +55,6 @@ class Bot(commands.Bot): for item in full_cache: self.insert_item_into_filter_list_cache(item) - async def _create_redis_session(self) -> None: - """ - Create the Redis connection pool, and then open the redis event gate. - - If constants.Redis.use_fakeredis is True, we'll set up a fake redis pool instead - of attempting to communicate with a real Redis server. This is useful because it - means contributors don't necessarily need to get Redis running locally just - to run the bot. - - The fakeredis cache won't have persistence across restarts, but that - usually won't matter for local bot testing. - """ - if constants.Redis.use_fakeredis: - log.info("Using fakeredis instead of communicating with a real Redis server.") - self.redis_session = await fakeredis.aioredis.create_redis_pool() - else: - self.redis_session = await aioredis.create_redis_pool( - address=(constants.Redis.host, constants.Redis.port), - password=constants.Redis.password, - ) - - self.redis_closed = False - self.redis_ready.set() - def _recreate(self) -> None: """Re-create the connector, aiohttp session, the APIClient and the Redis session.""" # Use asyncio for DNS resolution instead of threads so threads aren't spammed. @@ -94,13 +67,10 @@ class Bot(commands.Bot): "The previous connector was not closed; it will remain open and be overwritten" ) - if self.redis_session and not self.redis_session.closed: - log.warning( - "The previous redis pool was not closed; it will remain open and be overwritten" - ) - - # Create the redis session - self.loop.create_task(self._create_redis_session()) + if self.redis_session.closed: + # If the RedisSession was somehow closed, we try to reconnect it + # here. Normally, this shouldn't happen. + self.loop.create_task(self.redis_session.connect()) # Use AF_INET as its socket family to prevent HTTPS related problems both locally # and in production. @@ -180,10 +150,7 @@ class Bot(commands.Bot): self.stats._transport.close() if self.redis_session: - self.redis_closed = True - self.redis_session.close() - self.redis_ready.clear() - await self.redis_session.wait_closed() + await self.redis_session.close() def insert_item_into_filter_list_cache(self, item: Dict[str, str]) -> None: """Add an item to the bots filter_list_cache.""" diff --git a/bot/cogs/dm_relay.py b/bot/cogs/dm_relay.py index 0d8f340b4..368d6a5d9 100644 --- a/bot/cogs/dm_relay.py +++ b/bot/cogs/dm_relay.py @@ -2,6 +2,7 @@ import logging from typing import Optional import discord +from async_rediscache import RedisCache from discord import Color from discord.ext import commands from discord.ext.commands import Cog @@ -9,7 +10,6 @@ from discord.ext.commands import Cog from bot import constants from bot.bot import Bot from bot.converters import UserMentionOrID -from bot.utils import RedisCache from bot.utils.checks import in_whitelist_check, with_role_check from bot.utils.messages import send_attachments from bot.utils.webhooks import send_webhook diff --git a/bot/cogs/filtering.py b/bot/cogs/filtering.py index 99b659bff..0950ace60 100644 --- a/bot/cogs/filtering.py +++ b/bot/cogs/filtering.py @@ -6,6 +6,7 @@ from typing import List, Mapping, Optional, Tuple, Union import dateutil import discord.errors +from async_rediscache import RedisCache from dateutil.relativedelta import relativedelta from discord import Colour, HTTPException, Member, Message, NotFound, TextChannel from discord.ext.commands import Cog @@ -16,9 +17,8 @@ from bot.bot import Bot from bot.cogs.moderation import ModLog from bot.constants import ( Channels, Colours, - Filter, Icons, URLs + Filter, Icons, URLs, ) -from bot.utils.redis_cache import RedisCache from bot.utils.regex import INVITE_RE from bot.utils.scheduling import Scheduler diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 0f9cac89e..d4feb636c 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -9,11 +9,11 @@ from pathlib import Path import discord import discord.abc +from async_rediscache import RedisCache from discord.ext import commands from bot import constants from bot.bot import Bot -from bot.utils import RedisCache from bot.utils.checks import with_role_check from bot.utils.scheduling import Scheduler diff --git a/bot/cogs/verification.py b/bot/cogs/verification.py index 9ae92a228..859cc26e1 100644 --- a/bot/cogs/verification.py +++ b/bot/cogs/verification.py @@ -5,6 +5,7 @@ from contextlib import suppress from datetime import datetime, timedelta import discord +from async_rediscache import RedisCache from discord.ext import tasks from discord.ext.commands import Cog, Context, command, group from discord.utils import snowflake_time @@ -14,7 +15,6 @@ from bot.bot import Bot from bot.cogs.moderation import ModLog from bot.decorators import in_whitelist, with_role, without_role from bot.utils.checks import InWhitelistCheckFailure, without_role_check -from bot.utils.redis_cache import RedisCache log = logging.getLogger(__name__) diff --git a/tests/helpers.py b/tests/helpers.py index facc4e1af..e47fdf28f 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -308,7 +308,11 @@ class MockBot(CustomMockMixin, unittest.mock.MagicMock): Instances of this class will follow the specifications of `discord.ext.commands.Bot` instances. For more information, see the `MockGuild` docstring. """ - spec_set = Bot(command_prefix=unittest.mock.MagicMock(), loop=_get_mock_loop()) + spec_set = Bot( + command_prefix=unittest.mock.MagicMock(), + loop=_get_mock_loop(), + redis_session=unittest.mock.MagicMock(), + ) additional_spec_asyncs = ("wait_for", "redis_ready") def __init__(self, **kwargs) -> None: -- cgit v1.2.3 From de4a8d960f9f845467efb470e17a0e9685df1bdc Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff Date: Sat, 19 Sep 2020 21:48:41 +0200 Subject: Remove vestigial RedisCache class definition As we're now using the `async-rediscache` package, it's no longer necessary to keep the `RedisCache` defined in `bot.utils.redis_cache` around. I've removed the file containing it and the tests written for it. Signed-off-by: Sebastiaan Zeeff --- bot/utils/__init__.py | 3 +- bot/utils/redis_cache.py | 414 ------------------------------------ tests/bot/utils/test_redis_cache.py | 265 ----------------------- 3 files changed, 1 insertion(+), 681 deletions(-) delete mode 100644 bot/utils/redis_cache.py delete mode 100644 tests/bot/utils/test_redis_cache.py diff --git a/bot/utils/__init__.py b/bot/utils/__init__.py index 3e93fcb06..60170a88f 100644 --- a/bot/utils/__init__.py +++ b/bot/utils/__init__.py @@ -1,5 +1,4 @@ from bot.utils.helpers import CogABCMeta, find_nth_occurrence, pad_base64 -from bot.utils.redis_cache import RedisCache from bot.utils.services import send_to_paste_service -__all__ = ['RedisCache', 'CogABCMeta', 'find_nth_occurrence', 'pad_base64', 'send_to_paste_service'] +__all__ = ['CogABCMeta', 'find_nth_occurrence', 'pad_base64', 'send_to_paste_service'] diff --git a/bot/utils/redis_cache.py b/bot/utils/redis_cache.py deleted file mode 100644 index 52b689b49..000000000 --- a/bot/utils/redis_cache.py +++ /dev/null @@ -1,414 +0,0 @@ -from __future__ import annotations - -import asyncio -import logging -from functools import partialmethod -from typing import Any, Dict, ItemsView, Optional, Tuple, Union - -from bot.bot import Bot - -log = logging.getLogger(__name__) - -# Type aliases -RedisKeyType = Union[str, int] -RedisValueType = Union[str, int, float, bool] -RedisKeyOrValue = Union[RedisKeyType, RedisValueType] - -# Prefix tuples -_PrefixTuple = Tuple[Tuple[str, Any], ...] -_VALUE_PREFIXES = ( - ("f|", float), - ("i|", int), - ("s|", str), - ("b|", bool), -) -_KEY_PREFIXES = ( - ("i|", int), - ("s|", str), -) - - -class NoBotInstanceError(RuntimeError): - """Raised when RedisCache is created without an available bot instance on the owner class.""" - - -class NoNamespaceError(RuntimeError): - """Raised when RedisCache has no namespace, for example if it is not assigned to a class attribute.""" - - -class NoParentInstanceError(RuntimeError): - """Raised when the parent instance is available, for example if called by accessing the parent class directly.""" - - -class RedisCache: - """ - A simplified interface for a Redis connection. - - We implement several convenient methods that are fairly similar to have a dict - behaves, and should be familiar to Python users. The biggest difference is that - all the public methods in this class are coroutines, and must be awaited. - - Because of limitations in Redis, this cache will only accept strings and integers for keys, - and strings, integers, floats and booleans for values. - - Please note that this class MUST be created as a class attribute, and that that class - must also contain an attribute with an instance of our Bot. See `__get__` and `__set_name__` - for more information about how this works. - - Simple example for how to use this: - - class SomeCog(Cog): - # To initialize a valid RedisCache, just add it as a class attribute here. - # Do not add it to the __init__ method or anywhere else, it MUST be a class - # attribute. Do not pass any parameters. - cache = RedisCache() - - async def my_method(self): - - # Now we're ready to use the RedisCache. - # One thing to note here is that this will not work unless - # we access self.cache through an _instance_ of this class. - # - # For example, attempting to use SomeCog.cache will _not_ work, - # you _must_ instantiate the class first and use that instance. - # - # Now we can store some stuff in the cache just by doing this. - # This data will persist through restarts! - await self.cache.set("key", "value") - - # To get the data, simply do this. - value = await self.cache.get("key") - - # Other methods work more or less like a dictionary. - # Checking if something is in the cache - await self.cache.contains("key") - - # iterating the cache - async for key, value in self.cache.items(): - print(value) - - # We can even iterate in a comprehension! - consumed = [value async for key, value in self.cache.items()] - """ - - _namespaces = [] - - def __init__(self) -> None: - """Initialize the RedisCache.""" - self._namespace = None - self.bot = None - self._increment_lock = None - - def _set_namespace(self, namespace: str) -> None: - """Try to set the namespace, but do not permit collisions.""" - log.trace(f"RedisCache setting namespace to {namespace}") - self._namespaces.append(namespace) - self._namespace = namespace - - @staticmethod - def _to_typestring(key_or_value: RedisKeyOrValue, prefixes: _PrefixTuple) -> str: - """Turn a valid Redis type into a typestring.""" - for prefix, _type in prefixes: - # Convert bools into integers before storing them. - if type(key_or_value) is bool: - bool_int = int(key_or_value) - return f"{prefix}{bool_int}" - - # isinstance is a bad idea here, because isintance(False, int) == True. - if type(key_or_value) is _type: - return f"{prefix}{key_or_value}" - - raise TypeError(f"RedisCache._to_typestring only supports the following: {prefixes}.") - - @staticmethod - def _from_typestring(key_or_value: Union[bytes, str], prefixes: _PrefixTuple) -> RedisKeyOrValue: - """Deserialize a typestring into a valid Redis type.""" - # Stuff that comes out of Redis will be bytestrings, so let's decode those. - if isinstance(key_or_value, bytes): - key_or_value = key_or_value.decode('utf-8') - - # Now we convert our unicode string back into the type it originally was. - for prefix, _type in prefixes: - if key_or_value.startswith(prefix): - - # For booleans, we need special handling because bool("False") is True. - if prefix == "b|": - value = key_or_value[len(prefix):] - return bool(int(value)) - - # Otherwise we can just convert normally. - return _type(key_or_value[len(prefix):]) - raise TypeError(f"RedisCache._from_typestring only supports the following: {prefixes}.") - - # Add some nice partials to call our generic typestring converters. - # These are basically methods that will fill in some of the parameters for you, so that - # any call to _key_to_typestring will be like calling _to_typestring with the two parameters - # at `prefixes` and `types_string` pre-filled. - # - # See https://docs.python.org/3/library/functools.html#functools.partialmethod - _key_to_typestring = partialmethod(_to_typestring, prefixes=_KEY_PREFIXES) - _value_to_typestring = partialmethod(_to_typestring, prefixes=_VALUE_PREFIXES) - _key_from_typestring = partialmethod(_from_typestring, prefixes=_KEY_PREFIXES) - _value_from_typestring = partialmethod(_from_typestring, prefixes=_VALUE_PREFIXES) - - def _dict_from_typestring(self, dictionary: Dict) -> Dict: - """Turns all contents of a dict into valid Redis types.""" - return {self._key_from_typestring(key): self._value_from_typestring(value) for key, value in dictionary.items()} - - def _dict_to_typestring(self, dictionary: Dict) -> Dict: - """Turns all contents of a dict into typestrings.""" - return {self._key_to_typestring(key): self._value_to_typestring(value) for key, value in dictionary.items()} - - async def _validate_cache(self) -> None: - """Validate that the RedisCache is ready to be used.""" - if self._namespace is None: - error_message = ( - "Critical error: RedisCache has no namespace. " - "This object must be initialized as a class attribute." - ) - log.error(error_message) - raise NoNamespaceError(error_message) - - if self.bot is None: - error_message = ( - "Critical error: RedisCache has no `Bot` instance. " - "This happens when the class RedisCache was created in doesn't " - "have a Bot instance. Please make sure that you're instantiating " - "the RedisCache inside a class that has a Bot instance attribute." - ) - log.error(error_message) - raise NoBotInstanceError(error_message) - - if not self.bot.redis_closed: - await self.bot.redis_ready.wait() - - def __set_name__(self, owner: Any, attribute_name: str) -> None: - """ - Set the namespace to Class.attribute_name. - - Called automatically when this class is constructed inside a class as an attribute. - - This class MUST be created as a class attribute in a class, otherwise it will raise - exceptions whenever a method is used. This is because it uses this method to create - a namespace like `MyCog.my_class_attribute` which is used as a hash name when we store - stuff in Redis, to prevent collisions. - """ - self._set_namespace(f"{owner.__name__}.{attribute_name}") - - def __get__(self, instance: RedisCache, owner: Any) -> RedisCache: - """ - This is called if the RedisCache is a class attribute, and is accessed. - - The class this object is instantiated in must contain an attribute with an - instance of Bot. This is because Bot contains our redis_session, which is - the mechanism by which we will communicate with the Redis server. - - Any attempt to use RedisCache in a class that does not have a Bot instance - will fail. It is mostly intended to be used inside of a Cog, although theoretically - it should work in any class that has a Bot instance. - """ - if self.bot: - return self - - if self._namespace is None: - error_message = "RedisCache must be a class attribute." - log.error(error_message) - raise NoNamespaceError(error_message) - - if instance is None: - error_message = ( - "You must access the RedisCache instance through the cog instance " - "before accessing it using the cog's class object." - ) - log.error(error_message) - raise NoParentInstanceError(error_message) - - for attribute in vars(instance).values(): - if isinstance(attribute, Bot): - self.bot = attribute - return self - else: - error_message = ( - "Critical error: RedisCache has no `Bot` instance. " - "This happens when the class RedisCache was created in doesn't " - "have a Bot instance. Please make sure that you're instantiating " - "the RedisCache inside a class that has a Bot instance attribute." - ) - log.error(error_message) - raise NoBotInstanceError(error_message) - - def __repr__(self) -> str: - """Return a beautiful representation of this object instance.""" - return f"RedisCache(namespace={self._namespace!r})" - - async def set(self, key: RedisKeyType, value: RedisValueType) -> None: - """Store an item in the Redis cache.""" - await self._validate_cache() - - # Convert to a typestring and then set it - key = self._key_to_typestring(key) - value = self._value_to_typestring(value) - - log.trace(f"Setting {key} to {value}.") - await self.bot.redis_session.hset(self._namespace, key, value) - - async def get(self, key: RedisKeyType, default: Optional[RedisValueType] = None) -> Optional[RedisValueType]: - """Get an item from the Redis cache.""" - await self._validate_cache() - key = self._key_to_typestring(key) - - log.trace(f"Attempting to retrieve {key}.") - value = await self.bot.redis_session.hget(self._namespace, key) - - if value is None: - log.trace(f"Value not found, returning default value {default}") - return default - else: - value = self._value_from_typestring(value) - log.trace(f"Value found, returning value {value}") - return value - - async def delete(self, key: RedisKeyType) -> None: - """ - Delete an item from the Redis cache. - - If we try to delete a key that does not exist, it will simply be ignored. - - See https://redis.io/commands/hdel for more info on how this works. - """ - await self._validate_cache() - key = self._key_to_typestring(key) - - log.trace(f"Attempting to delete {key}.") - return await self.bot.redis_session.hdel(self._namespace, key) - - async def contains(self, key: RedisKeyType) -> bool: - """ - Check if a key exists in the Redis cache. - - Return True if the key exists, otherwise False. - """ - await self._validate_cache() - key = self._key_to_typestring(key) - exists = await self.bot.redis_session.hexists(self._namespace, key) - - log.trace(f"Testing if {key} exists in the RedisCache - Result is {exists}") - return exists - - async def items(self) -> ItemsView: - """ - Fetch all the key/value pairs in the cache. - - Returns a normal ItemsView, like you would get from dict.items(). - - Keep in mind that these items are just a _copy_ of the data in the - RedisCache - any changes you make to them will not be reflected - into the RedisCache itself. If you want to change these, you need - to make a .set call. - - Example: - items = await my_cache.items() - for key, value in items: - # Iterate like a normal dictionary - """ - await self._validate_cache() - items = self._dict_from_typestring( - await self.bot.redis_session.hgetall(self._namespace) - ).items() - - log.trace(f"Retrieving all key/value pairs from cache, total of {len(items)} items.") - return items - - async def length(self) -> int: - """Return the number of items in the Redis cache.""" - await self._validate_cache() - number_of_items = await self.bot.redis_session.hlen(self._namespace) - log.trace(f"Returning length. Result is {number_of_items}.") - return number_of_items - - async def to_dict(self) -> Dict: - """Convert to dict and return.""" - return {key: value for key, value in await self.items()} - - async def clear(self) -> None: - """Deletes the entire hash from the Redis cache.""" - await self._validate_cache() - log.trace("Clearing the cache of all key/value pairs.") - await self.bot.redis_session.delete(self._namespace) - - async def pop(self, key: RedisKeyType, default: Optional[RedisValueType] = None) -> RedisValueType: - """Get the item, remove it from the cache, and provide a default if not found.""" - log.trace(f"Attempting to pop {key}.") - value = await self.get(key, default) - - log.trace( - f"Attempting to delete item with key '{key}' from the cache. " - "If this key doesn't exist, nothing will happen." - ) - await self.delete(key) - - return value - - async def update(self, items: Dict[RedisKeyType, RedisValueType]) -> None: - """ - Update the Redis cache with multiple values. - - This works exactly like dict.update from a normal dictionary. You pass - a dictionary with one or more key/value pairs into this method. If the keys - do not exist in the RedisCache, they are created. If they do exist, the values - are updated with the new ones from `items`. - - Please note that keys and the values in the `items` dictionary - must consist of valid RedisKeyTypes and RedisValueTypes. - """ - await self._validate_cache() - log.trace(f"Updating the cache with the following items:\n{items}") - await self.bot.redis_session.hmset_dict(self._namespace, self._dict_to_typestring(items)) - - async def increment(self, key: RedisKeyType, amount: Optional[int, float] = 1) -> None: - """ - Increment the value by `amount`. - - This works for both floats and ints, but will raise a TypeError - if you try to do it for any other type of value. - - This also supports negative amounts, although it would provide better - readability to use .decrement() for that. - """ - log.trace(f"Attempting to increment/decrement the value with the key {key} by {amount}.") - - # We initialize the lock here, because we need to ensure we get it - # running on the same loop as the calling coroutine. - # - # If we initialized the lock in the __init__, the loop that the coroutine this method - # would be called from might not exist yet, and so the lock would be on a different - # loop, which would raise RuntimeErrors. - if self._increment_lock is None: - self._increment_lock = asyncio.Lock() - - # Since this has several API calls, we need a lock to prevent race conditions - async with self._increment_lock: - value = await self.get(key) - - # Can't increment a non-existing value - if value is None: - error_message = "The provided key does not exist!" - log.error(error_message) - raise KeyError(error_message) - - # If it does exist, and it's an int or a float, increment and set it. - if isinstance(value, int) or isinstance(value, float): - value += amount - await self.set(key, value) - else: - error_message = "You may only increment or decrement values that are integers or floats." - log.error(error_message) - raise TypeError(error_message) - - async def decrement(self, key: RedisKeyType, amount: Optional[int, float] = 1) -> None: - """ - Decrement the value by `amount`. - - Basically just does the opposite of .increment. - """ - await self.increment(key, -amount) diff --git a/tests/bot/utils/test_redis_cache.py b/tests/bot/utils/test_redis_cache.py deleted file mode 100644 index a2f0fe55d..000000000 --- a/tests/bot/utils/test_redis_cache.py +++ /dev/null @@ -1,265 +0,0 @@ -import asyncio -import unittest - -import fakeredis.aioredis - -from bot.utils import RedisCache -from bot.utils.redis_cache import NoBotInstanceError, NoNamespaceError, NoParentInstanceError -from tests import helpers - - -class RedisCacheTests(unittest.IsolatedAsyncioTestCase): - """Tests the RedisCache class from utils.redis_dict.py.""" - - async def asyncSetUp(self): # noqa: N802 - """Sets up the objects that only have to be initialized once.""" - self.bot = helpers.MockBot() - self.bot.redis_session = await fakeredis.aioredis.create_redis_pool() - - # Okay, so this is necessary so that we can create a clean new - # class for every test method, and we want that because it will - # ensure we get a fresh loop, which is necessary for test_increment_lock - # to be able to pass. - class DummyCog: - """A dummy cog, for dummies.""" - - redis = RedisCache() - - def __init__(self, bot: helpers.MockBot): - self.bot = bot - - self.cog = DummyCog(self.bot) - - await self.cog.redis.clear() - - def test_class_attribute_namespace(self): - """Test that RedisDict creates a namespace automatically for class attributes.""" - self.assertEqual(self.cog.redis._namespace, "DummyCog.redis") - - async def test_class_attribute_required(self): - """Test that errors are raised when not assigned as a class attribute.""" - bad_cache = RedisCache() - self.assertIs(bad_cache._namespace, None) - - with self.assertRaises(RuntimeError): - await bad_cache.set("test", "me_up_deadman") - - async def test_set_get_item(self): - """Test that users can set and get items from the RedisDict.""" - test_cases = ( - ('favorite_fruit', 'melon'), - ('favorite_number', 86), - ('favorite_fraction', 86.54), - ('favorite_boolean', False), - ('other_boolean', True), - ) - - # Test that we can get and set different types. - for test in test_cases: - await self.cog.redis.set(*test) - self.assertEqual(await self.cog.redis.get(test[0]), test[1]) - - # Test that .get allows a default value - self.assertEqual(await self.cog.redis.get('favorite_nothing', "bearclaw"), "bearclaw") - - async def test_set_item_type(self): - """Test that .set rejects keys and values that are not permitted.""" - fruits = ["lemon", "melon", "apple"] - - with self.assertRaises(TypeError): - await self.cog.redis.set(fruits, "nice") - - with self.assertRaises(TypeError): - await self.cog.redis.set(4.23, "nice") - - async def test_delete_item(self): - """Test that .delete allows us to delete stuff from the RedisCache.""" - # Add an item and verify that it gets added - await self.cog.redis.set("internet", "firetruck") - self.assertEqual(await self.cog.redis.get("internet"), "firetruck") - - # Delete that item and verify that it gets deleted - await self.cog.redis.delete("internet") - self.assertIs(await self.cog.redis.get("internet"), None) - - async def test_contains(self): - """Test that we can check membership with .contains.""" - await self.cog.redis.set('favorite_country', "Burkina Faso") - - self.assertIs(await self.cog.redis.contains('favorite_country'), True) - self.assertIs(await self.cog.redis.contains('favorite_dentist'), False) - - async def test_items(self): - """Test that the RedisDict can be iterated.""" - # Set up our test cases in the Redis cache - test_cases = [ - ('favorite_turtle', 'Donatello'), - ('second_favorite_turtle', 'Leonardo'), - ('third_favorite_turtle', 'Raphael'), - ] - for key, value in test_cases: - await self.cog.redis.set(key, value) - - # Consume the AsyncIterator into a regular list, easier to compare that way. - redis_items = [item for item in await self.cog.redis.items()] - - # These sequences are probably in the same order now, but probably - # isn't good enough for tests. Let's not rely on .hgetall always - # returning things in sequence, and just sort both lists to be safe. - redis_items = sorted(redis_items) - test_cases = sorted(test_cases) - - # If these are equal now, everything works fine. - self.assertSequenceEqual(test_cases, redis_items) - - async def test_length(self): - """Test that we can get the correct .length from the RedisDict.""" - await self.cog.redis.set('one', 1) - await self.cog.redis.set('two', 2) - await self.cog.redis.set('three', 3) - self.assertEqual(await self.cog.redis.length(), 3) - - await self.cog.redis.set('four', 4) - self.assertEqual(await self.cog.redis.length(), 4) - - async def test_to_dict(self): - """Test that the .to_dict method returns a workable dictionary copy.""" - copy = await self.cog.redis.to_dict() - local_copy = {key: value for key, value in await self.cog.redis.items()} - self.assertIs(type(copy), dict) - self.assertDictEqual(copy, local_copy) - - async def test_clear(self): - """Test that the .clear method removes the entire hash.""" - await self.cog.redis.set('teddy', 'with me') - await self.cog.redis.set('in my dreams', 'you have a weird hat') - self.assertEqual(await self.cog.redis.length(), 2) - - await self.cog.redis.clear() - self.assertEqual(await self.cog.redis.length(), 0) - - async def test_pop(self): - """Test that we can .pop an item from the RedisDict.""" - await self.cog.redis.set('john', 'was afraid') - - self.assertEqual(await self.cog.redis.pop('john'), 'was afraid') - self.assertEqual(await self.cog.redis.pop('pete', 'breakneck'), 'breakneck') - self.assertEqual(await self.cog.redis.length(), 0) - - async def test_update(self): - """Test that we can .update the RedisDict with multiple items.""" - await self.cog.redis.set("reckfried", "lona") - await self.cog.redis.set("bel air", "prince") - await self.cog.redis.update({ - "reckfried": "jona", - "mega": "hungry, though", - }) - - result = { - "reckfried": "jona", - "bel air": "prince", - "mega": "hungry, though", - } - self.assertDictEqual(await self.cog.redis.to_dict(), result) - - def test_typestring_conversion(self): - """Test the typestring-related helper functions.""" - conversion_tests = ( - (12, "i|12"), - (12.4, "f|12.4"), - ("cowabunga", "s|cowabunga"), - ) - - # Test conversion to typestring - for _input, expected in conversion_tests: - self.assertEqual(self.cog.redis._value_to_typestring(_input), expected) - - # Test conversion from typestrings - for _input, expected in conversion_tests: - self.assertEqual(self.cog.redis._value_from_typestring(expected), _input) - - # Test that exceptions are raised on invalid input - with self.assertRaises(TypeError): - self.cog.redis._value_to_typestring(["internet"]) - self.cog.redis._value_from_typestring("o|firedog") - - async def test_increment_decrement(self): - """Test .increment and .decrement methods.""" - await self.cog.redis.set("entropic", 5) - await self.cog.redis.set("disentropic", 12.5) - - # Test default increment - await self.cog.redis.increment("entropic") - self.assertEqual(await self.cog.redis.get("entropic"), 6) - - # Test default decrement - await self.cog.redis.decrement("entropic") - self.assertEqual(await self.cog.redis.get("entropic"), 5) - - # Test float increment with float - await self.cog.redis.increment("disentropic", 2.0) - self.assertEqual(await self.cog.redis.get("disentropic"), 14.5) - - # Test float increment with int - await self.cog.redis.increment("disentropic", 2) - self.assertEqual(await self.cog.redis.get("disentropic"), 16.5) - - # Test negative increments, because why not. - await self.cog.redis.increment("entropic", -5) - self.assertEqual(await self.cog.redis.get("entropic"), 0) - - # Negative decrements? Sure. - await self.cog.redis.decrement("entropic", -5) - self.assertEqual(await self.cog.redis.get("entropic"), 5) - - # What about if we use a negative float to decrement an int? - # This should convert the type into a float. - await self.cog.redis.decrement("entropic", -2.5) - self.assertEqual(await self.cog.redis.get("entropic"), 7.5) - - # Let's test that they raise the right errors - with self.assertRaises(KeyError): - await self.cog.redis.increment("doesn't_exist!") - - await self.cog.redis.set("stringthing", "stringthing") - with self.assertRaises(TypeError): - await self.cog.redis.increment("stringthing") - - async def test_increment_lock(self): - """Test that we can't produce a race condition in .increment.""" - await self.cog.redis.set("test_key", 0) - tasks = [] - - # Increment this a lot in different tasks - for _ in range(100): - task = asyncio.create_task( - self.cog.redis.increment("test_key", 1) - ) - tasks.append(task) - await asyncio.gather(*tasks) - - # Confirm that the value has been incremented the exact right number of times. - value = await self.cog.redis.get("test_key") - self.assertEqual(value, 100) - - async def test_exceptions_raised(self): - """Testing that the various RuntimeErrors are reachable.""" - class MyCog: - cache = RedisCache() - - def __init__(self): - self.other_cache = RedisCache() - - cog = MyCog() - - # Raises "No Bot instance" - with self.assertRaises(NoBotInstanceError): - await cog.cache.get("john") - - # Raises "RedisCache has no namespace" - with self.assertRaises(NoNamespaceError): - await cog.other_cache.get("was") - - # Raises "You must access the RedisCache instance through the cog instance" - with self.assertRaises(NoParentInstanceError): - await MyCog.cache.get("afraid") -- cgit v1.2.3 From 9bb3a4880205e9b24bb839de60ca2f5a26689067 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff Date: Sat, 19 Sep 2020 22:01:36 +0200 Subject: Use global namespace `bot` for our RedisSession As we're now planning on using Redis in multiple applications, it's important to minimize the risk of namespace collisions between different applications. The `async-rediscache` packages allows us to set a global namespace on an application level. I've chosen "bot" as the namespace for this application, which means all individual namespaces will automatically be prefixed by `bot.` whenever they are accessed. Signed-off-by: Sebastiaan Zeeff --- bot/__main__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bot/__main__.py b/bot/__main__.py index ee627be0a..fb0021d5d 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -33,6 +33,7 @@ redis_session = RedisSession( minsize=1, maxsize=20, use_fakeredis=constants.Redis.use_fakeredis, + global_namespace="bot", ) # Connect redis session to ensure it's connected before we try to access Redis -- cgit v1.2.3 From 409f0b5e5a0f71a0858c91ffde46275cf755f067 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff Date: Sun, 20 Sep 2020 11:41:48 +0200 Subject: Determine eligible duckpond emojis dynamically Instead of maintaining a list of duckpond IDs manually, it's a much better idea to detect ducky emojis dynamically using the new emoji name grouping we've introduced: All emojis that start with `ducky_` will now be counted as a duckpond ducky. The unicode duck emoji obviously still counts in addition to custom emojis with the `ducky_` prefix. Signed-off-by: Sebastiaan Zeeff --- bot/cogs/duck_pond.py | 54 +++++++++++++++++++++++++-------------------------- bot/constants.py | 15 -------------- config-default.yml | 28 -------------------------- 3 files changed, 27 insertions(+), 70 deletions(-) diff --git a/bot/cogs/duck_pond.py b/bot/cogs/duck_pond.py index 7021069fa..e1aceb482 100644 --- a/bot/cogs/duck_pond.py +++ b/bot/cogs/duck_pond.py @@ -49,32 +49,32 @@ class DuckPond(Cog): return True return False + @staticmethod + def _is_duck_emoji(emoji: Union[str, discord.PartialEmoji, discord.Emoji]) -> bool: + """Check if the emoji is a valid duck emoji.""" + if isinstance(emoji, str): + return emoji == "🦆" + else: + return hasattr(emoji, "name") and emoji.name.startswith("ducky_") + async def count_ducks(self, message: Message) -> int: """ Count the number of ducks in the reactions of a specific message. Only counts ducks added by staff members. """ - duck_count = 0 - duck_reactors = [] + duck_reactors = set() + # iterate over all reactions for reaction in message.reactions: - async for user in reaction.users(): - - # Is the user a staff member and not already counted as reactor? - if not self.is_staff(user) or user.id in duck_reactors: - continue - - # Is the emoji a duck? - if hasattr(reaction.emoji, "id"): - if reaction.emoji.id in constants.DuckPond.custom_emojis: - duck_count += 1 - duck_reactors.append(user.id) - elif isinstance(reaction.emoji, str): - if reaction.emoji == "🦆": - duck_count += 1 - duck_reactors.append(user.id) - return duck_count + # check if the current reaction is a duck + if not self._is_duck_emoji(reaction.emoji): + continue + + # update the set of reactors with all staff reactors + duck_reactors |= {user.id async for user in reaction.users() if self.is_staff(user)} + + return len(duck_reactors) async def relay_message(self, message: Message) -> None: """Relays the message's content and attachments to the duck pond channel.""" @@ -105,16 +105,16 @@ class DuckPond(Cog): await message.add_reaction("✅") - @staticmethod - def _payload_has_duckpond_emoji(payload: RawReactionActionEvent) -> bool: + def _payload_has_duckpond_emoji(self, emoji: discord.PartialEmoji) -> bool: """Test if the RawReactionActionEvent payload contains a duckpond emoji.""" - if payload.emoji.is_custom_emoji(): - if payload.emoji.id in constants.DuckPond.custom_emojis: - return True - elif payload.emoji.name == "🦆": - return True + if emoji.is_unicode_emoji(): + # For unicode PartialEmojis, the `name` attribute is just the string + # representation of the emoji. This is what the helper method + # expects, as unicode emojis show up as just a `str` instance when + # inspecting the reactions attached to a message. + emoji = emoji.name - return False + return self._is_duck_emoji(emoji) @Cog.listener() async def on_raw_reaction_add(self, payload: RawReactionActionEvent) -> None: @@ -126,7 +126,7 @@ class DuckPond(Cog): send the message off to the duck pond. """ # Is the emoji in the reaction a duck? - if not self._payload_has_duckpond_emoji(payload): + if not self._payload_has_duckpond_emoji(payload.emoji): return channel = discord.utils.get(self.bot.get_all_channels(), id=payload.channel_id) diff --git a/bot/constants.py b/bot/constants.py index 17f14fec0..f087fd96f 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -252,7 +252,6 @@ class DuckPond(metaclass=YAMLGetter): section = "duck_pond" threshold: int - custom_emojis: List[int] class Emojis(metaclass=YAMLGetter): @@ -292,20 +291,6 @@ class Emojis(metaclass=YAMLGetter): cross_mark: str check_mark: str - ducky_yellow: int - ducky_blurple: int - ducky_regal: int - ducky_camo: int - ducky_ninja: int - ducky_devil: int - ducky_tube: int - ducky_hunt: int - ducky_wizard: int - ducky_party: int - ducky_angel: int - ducky_maul: int - ducky_santa: int - upvotes: str comments: str user: str diff --git a/config-default.yml b/config-default.yml index 58651f548..8d13b2d11 100644 --- a/config-default.yml +++ b/config-default.yml @@ -62,20 +62,6 @@ style: cross_mark: "\u274C" check_mark: "\u2705" - ducky_yellow: &DUCKY_YELLOW 574951975574175744 - ducky_blurple: &DUCKY_BLURPLE 574951975310065675 - ducky_regal: &DUCKY_REGAL 637883439185395712 - ducky_camo: &DUCKY_CAMO 637914731566596096 - ducky_ninja: &DUCKY_NINJA 637923502535606293 - ducky_devil: &DUCKY_DEVIL 637925314982576139 - ducky_tube: &DUCKY_TUBE 637881368008851456 - ducky_hunt: &DUCKY_HUNT 639355090909528084 - ducky_wizard: &DUCKY_WIZARD 639355996954689536 - ducky_party: &DUCKY_PARTY 639468753440210977 - ducky_angel: &DUCKY_ANGEL 640121935610511361 - ducky_maul: &DUCKY_MAUL 640137724958867467 - ducky_santa: &DUCKY_SANTA 655360331002019870 - # emotes used for #reddit upvotes: "<:reddit_upvotes:755845219890757644>" comments: "<:reddit_comments:755845255001014384>" @@ -468,20 +454,6 @@ sync: duck_pond: threshold: 5 - custom_emojis: - - *DUCKY_YELLOW - - *DUCKY_BLURPLE - - *DUCKY_CAMO - - *DUCKY_DEVIL - - *DUCKY_NINJA - - *DUCKY_REGAL - - *DUCKY_TUBE - - *DUCKY_HUNT - - *DUCKY_WIZARD - - *DUCKY_PARTY - - *DUCKY_ANGEL - - *DUCKY_MAUL - - *DUCKY_SANTA python_news: mail_lists: -- cgit v1.2.3 From 3fea32523d84ee5e2ba0e68710191ffb61220d58 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff Date: Sun, 20 Sep 2020 12:03:04 +0200 Subject: Ignore non-staff messages for our duckpond Some of our members have expressed concern that their messages would be "ducked" by staff members and relayed to the staff-only duckpond. Since duckpond is supposed to be a funny, staff-only affair, I've made duckpond ignore messages from non-staff members. Signed-off-by: Sebastiaan Zeeff --- bot/cogs/duck_pond.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bot/cogs/duck_pond.py b/bot/cogs/duck_pond.py index e1aceb482..66e862ab2 100644 --- a/bot/cogs/duck_pond.py +++ b/bot/cogs/duck_pond.py @@ -133,7 +133,11 @@ class DuckPond(Cog): message = await channel.fetch_message(payload.message_id) member = discord.utils.get(message.guild.members, id=payload.user_id) - # Is the member a human and a staff member? + # Was the message sent by a human staff member? + if not self.is_staff(message.author) or message.author.bot: + return + + # Is the reactor a human staff member? if not self.is_staff(member) or member.bot: return -- cgit v1.2.3 From 9410194c058675428c4ed99f403e84cb65c18a71 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff Date: Sun, 20 Sep 2020 12:04:56 +0200 Subject: Add channel blacklist for duckpond As announcements already get a lot of exposure and have a high risk of getting "ducked", duckpond will now ignore those channels and never relay those announcements to our duckpond. Signed-off-by: Sebastiaan Zeeff --- bot/cogs/duck_pond.py | 4 ++++ bot/constants.py | 8 ++++++++ config-default.yml | 30 ++++++++++++++++++++++++++---- 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/bot/cogs/duck_pond.py b/bot/cogs/duck_pond.py index 66e862ab2..84c0c4265 100644 --- a/bot/cogs/duck_pond.py +++ b/bot/cogs/duck_pond.py @@ -125,6 +125,10 @@ class DuckPond(Cog): amount of ducks specified in the config under duck_pond/threshold, it will send the message off to the duck pond. """ + # Was this reaction issued in a blacklisted channel? + if payload.channel_id in constants.DuckPond.channel_blacklist: + return + # Is the emoji in the reaction a duck? if not self._payload_has_duckpond_emoji(payload.emoji): return diff --git a/bot/constants.py b/bot/constants.py index f087fd96f..aec0ffce3 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -252,6 +252,7 @@ class DuckPond(metaclass=YAMLGetter): section = "duck_pond" threshold: int + channel_blacklist: List[int] class Emojis(metaclass=YAMLGetter): @@ -380,12 +381,14 @@ class Channels(metaclass=YAMLGetter): section = "guild" subsection = "channels" + admin_announcements: int admin_spam: int admins: int announcements: int attachment_log: int big_brother_logs: int bot_commands: int + change_log: int cooldown: int defcon: int dev_contrib: int @@ -397,9 +400,11 @@ class Channels(metaclass=YAMLGetter): how_to_get_help: int incidents: int incidents_archive: int + mailing_lists: int message_log: int meta: int mod_alerts: int + mod_announcements: int mod_log: int mod_spam: int mods: int @@ -408,7 +413,10 @@ class Channels(metaclass=YAMLGetter): off_topic_2: int organisation: int python_discussion: int + python_events: int + python_news: int reddit: int + staff_announcements: int talent_pool: int user_event_announcements: int user_log: int diff --git a/config-default.yml b/config-default.yml index 8d13b2d11..4c1c7e483 100644 --- a/config-default.yml +++ b/config-default.yml @@ -130,9 +130,14 @@ guild: modmail: 714494672835444826 channels: - announcements: 354619224620138496 - user_event_announcements: &USER_EVENT_A 592000283102674944 - python_news: &PYNEWS_CHANNEL 704372456592506880 + # Public announcement and news channels + change_log: &CHANGE_LOG 748238795236704388 + announcements: &ANNOUNCEMENTS 354619224620138496 + python_news: &PYNEWS_CHANNEL 704372456592506880 + python_events: &PYEVENTS_CHANNEL 729674110270963822 + mailing_lists: &MAILING_LISTS 704372456592506880 + reddit: &REDDIT_CHANNEL 458224812528238616 + user_event_announcements: &USER_EVENT_A 592000283102674944 # Development dev_contrib: &DEV_CONTRIB 635950537262759947 @@ -163,7 +168,6 @@ guild: # Special bot_commands: &BOT_CMD 267659945086812160 esoteric: 470884583684964352 - reddit: 458224812528238616 verification: 352442727016693763 # Staff @@ -178,6 +182,12 @@ guild: mod_spam: &MOD_SPAM 620607373828030464 organisation: &ORGANISATION 551789653284356126 staff_lounge: &STAFF_LOUNGE 464905259261755392 + duck_pond: &DUCK_POND 637820308341915648 + + # Staff announcement channels + staff_announcements: &STAFF_ANNOUNCEMENTS 464033278631084042 + mod_announcements: &MOD_ANNOUNCEMENTS 372115205867700225 + admin_announcements: &ADMIN_ANNOUNCEMENTS 749736155569848370 # Voice admins_voice: &ADMINS_VOICE 500734494840717332 @@ -454,6 +464,18 @@ sync: duck_pond: threshold: 5 + channel_blacklist: + - *ANNOUNCEMENTS + - *PYNEWS_CHANNEL + - *PYEVENTS_CHANNEL + - *MAILING_LISTS + - *REDDIT_CHANNEL + - *USER_EVENT_A + - *DUCK_POND + - *CHANGE_LOG + - *STAFF_ANNOUNCEMENTS + - *MOD_ANNOUNCEMENTS + - *ADMIN_ANNOUNCEMENTS python_news: mail_lists: -- cgit v1.2.3 From d68d6d2858b8df74c48c00c0af23de24aa5022dc Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff Date: Sun, 20 Sep 2020 12:17:30 +0200 Subject: Fix relay race condition in duckpond using a lock Our duckpond suffered from a race condition: If multiple raw reaction events were received in quick succession and a message had enough ducks to take it over the duckpond threshold, the message would be relayed multiple times. The reason this happened is because the green checkmark emoji that stops a message from being relayed multiple times is only added after the message has been relayed. This means that multiple event triggers can make it past the green checkmark check before any of them has a chance to add a green checkmark. The solution was to create a relay lock that needs to be acquired before checking for the presence of a green checkmark and is only released after adding a green checkmark. This prevents multiple events from making it past the sentinel check. As our Cogs are potentially initialized before the event loop is created, the lock is load lazily when needed. Signed-off-by: Sebastiaan Zeeff --- bot/cogs/duck_pond.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/bot/cogs/duck_pond.py b/bot/cogs/duck_pond.py index 84c0c4265..12f4cb7b8 100644 --- a/bot/cogs/duck_pond.py +++ b/bot/cogs/duck_pond.py @@ -1,3 +1,4 @@ +import asyncio import logging from typing import Union @@ -21,6 +22,7 @@ class DuckPond(Cog): self.webhook_id = constants.Webhooks.duck_pond self.webhook = None self.bot.loop.create_task(self.fetch_webhook()) + self.relay_lock = None async def fetch_webhook(self) -> None: """Fetches the webhook object, so we can post to it.""" @@ -103,8 +105,6 @@ class DuckPond(Cog): except discord.HTTPException: log.exception("Failed to send an attachment to the webhook") - await message.add_reaction("✅") - def _payload_has_duckpond_emoji(self, emoji: discord.PartialEmoji) -> bool: """Test if the RawReactionActionEvent payload contains a duckpond emoji.""" if emoji.is_unicode_emoji(): @@ -145,16 +145,26 @@ class DuckPond(Cog): if not self.is_staff(member) or member.bot: return - # Does the message already have a green checkmark? - if await self.has_green_checkmark(message): - return - # Time to count our ducks! duck_count = await self.count_ducks(message) # If we've got more than the required amount of ducks, send the message to the duck_pond. if duck_count >= constants.DuckPond.threshold: - await self.relay_message(message) + if self.relay_lock is None: + # Lazily load the lock to ensure it's created within the + # appropriate event loop. + self.relay_lock = asyncio.Lock() + + async with self.relay_lock: + # check if the message has a checkmark after acquiring the lock + if await self.has_green_checkmark(message): + return + + # relay the message + await self.relay_message(message) + + # add a green checkmark to indicate that the message was relayed + await message.add_reaction("✅") @Cog.listener() async def on_raw_reaction_remove(self, payload: RawReactionActionEvent) -> None: -- cgit v1.2.3 From 0c5c472362d2891853bb82b225aa86da74d597e2 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff Date: Sun, 20 Sep 2020 12:43:00 +0200 Subject: Remove unit tests for duck pond I've removed the unit tests for duckpond in concordance with the new policy for writing unit tests for the bot The tests were unnecessarily complicated, difficult to maintain, and slowed development. Signed-off-by: Sebastiaan Zeeff --- tests/bot/cogs/test_duck_pond.py | 548 --------------------------------------- 1 file changed, 548 deletions(-) delete mode 100644 tests/bot/cogs/test_duck_pond.py diff --git a/tests/bot/cogs/test_duck_pond.py b/tests/bot/cogs/test_duck_pond.py deleted file mode 100644 index cfe10aebf..000000000 --- a/tests/bot/cogs/test_duck_pond.py +++ /dev/null @@ -1,548 +0,0 @@ -import asyncio -import logging -import typing -import unittest -from unittest.mock import AsyncMock, MagicMock, patch - -import discord - -from bot import constants -from bot.cogs import duck_pond -from tests import base -from tests import helpers - -MODULE_PATH = "bot.cogs.duck_pond" - - -class DuckPondTests(base.LoggingTestsMixin, unittest.IsolatedAsyncioTestCase): - """Tests for DuckPond functionality.""" - - @classmethod - def setUpClass(cls): - """Sets up the objects that only have to be initialized once.""" - cls.nonstaff_member = helpers.MockMember(name="Non-staffer") - - cls.staff_role = helpers.MockRole(name="Staff role", id=constants.STAFF_ROLES[0]) - cls.staff_member = helpers.MockMember(name="staffer", roles=[cls.staff_role]) - - cls.checkmark_emoji = "\N{White Heavy Check Mark}" - cls.thumbs_up_emoji = "\N{Thumbs Up Sign}" - cls.unicode_duck_emoji = "\N{Duck}" - cls.duck_pond_emoji = helpers.MockPartialEmoji(id=constants.DuckPond.custom_emojis[0]) - cls.non_duck_custom_emoji = helpers.MockPartialEmoji(id=123) - - def setUp(self): - """Sets up the objects that need to be refreshed before each test.""" - self.bot = helpers.MockBot(user=helpers.MockMember(id=46692)) - self.cog = duck_pond.DuckPond(bot=self.bot) - - def test_duck_pond_correctly_initializes(self): - """`__init__ should set `bot` and `webhook_id` attributes and schedule `fetch_webhook`.""" - bot = helpers.MockBot() - cog = MagicMock() - - duck_pond.DuckPond.__init__(cog, bot) - - self.assertEqual(cog.bot, bot) - self.assertEqual(cog.webhook_id, constants.Webhooks.duck_pond) - bot.loop.create_task.assert_called_once_with(cog.fetch_webhook()) - - def test_fetch_webhook_succeeds_without_connectivity_issues(self): - """The `fetch_webhook` method waits until `READY` event and sets the `webhook` attribute.""" - self.bot.fetch_webhook.return_value = "dummy webhook" - self.cog.webhook_id = 1 - - asyncio.run(self.cog.fetch_webhook()) - - self.bot.wait_until_guild_available.assert_called_once() - self.bot.fetch_webhook.assert_called_once_with(1) - self.assertEqual(self.cog.webhook, "dummy webhook") - - def test_fetch_webhook_logs_when_unable_to_fetch_webhook(self): - """The `fetch_webhook` method should log an exception when it fails to fetch the webhook.""" - self.bot.fetch_webhook.side_effect = discord.HTTPException(response=MagicMock(), message="Not found.") - self.cog.webhook_id = 1 - - log = logging.getLogger('bot.cogs.duck_pond') - with self.assertLogs(logger=log, level=logging.ERROR) as log_watcher: - asyncio.run(self.cog.fetch_webhook()) - - self.bot.wait_until_guild_available.assert_called_once() - self.bot.fetch_webhook.assert_called_once_with(1) - - self.assertEqual(len(log_watcher.records), 1) - - record = log_watcher.records[0] - self.assertEqual(record.levelno, logging.ERROR) - - def test_is_staff_returns_correct_values_based_on_instance_passed(self): - """The `is_staff` method should return correct values based on the instance passed.""" - test_cases = ( - (helpers.MockUser(name="User instance"), False), - (helpers.MockMember(name="Member instance without staff role"), False), - (helpers.MockMember(name="Member instance with staff role", roles=[self.staff_role]), True) - ) - - for user, expected_return in test_cases: - actual_return = self.cog.is_staff(user) - with self.subTest(user_type=user.name, expected_return=expected_return, actual_return=actual_return): - self.assertEqual(expected_return, actual_return) - - async def test_has_green_checkmark_correctly_detects_presence_of_green_checkmark_emoji(self): - """The `has_green_checkmark` method should only return `True` if one is present.""" - test_cases = ( - ( - "No reactions", helpers.MockMessage(), False - ), - ( - "No green check mark reactions", - helpers.MockMessage(reactions=[ - helpers.MockReaction(emoji=self.unicode_duck_emoji, users=[self.bot.user]), - helpers.MockReaction(emoji=self.thumbs_up_emoji, users=[self.bot.user]) - ]), - False - ), - ( - "Green check mark reaction, but not from our bot", - helpers.MockMessage(reactions=[ - helpers.MockReaction(emoji=self.unicode_duck_emoji, users=[self.bot.user]), - helpers.MockReaction(emoji=self.checkmark_emoji, users=[self.staff_member]) - ]), - False - ), - ( - "Green check mark reaction, with one from the bot", - helpers.MockMessage(reactions=[ - helpers.MockReaction(emoji=self.unicode_duck_emoji, users=[self.bot.user]), - helpers.MockReaction(emoji=self.checkmark_emoji, users=[self.staff_member, self.bot.user]) - ]), - True - ) - ) - - for description, message, expected_return in test_cases: - actual_return = await self.cog.has_green_checkmark(message) - with self.subTest( - test_case=description, - expected_return=expected_return, - actual_return=actual_return - ): - self.assertEqual(expected_return, actual_return) - - def _get_reaction( - self, - emoji: typing.Union[str, helpers.MockEmoji], - staff: int = 0, - nonstaff: int = 0 - ) -> helpers.MockReaction: - staffers = [helpers.MockMember(roles=[self.staff_role]) for _ in range(staff)] - nonstaffers = [helpers.MockMember() for _ in range(nonstaff)] - return helpers.MockReaction(emoji=emoji, users=staffers + nonstaffers) - - async def test_count_ducks_correctly_counts_the_number_of_eligible_duck_emojis(self): - """The `count_ducks` method should return the number of unique staffers who gave a duck.""" - test_cases = ( - # Simple test cases - # A message without reactions should return 0 - ( - "No reactions", - helpers.MockMessage(), - 0 - ), - # A message with a non-duck reaction from a non-staffer should return 0 - ( - "Non-duck reaction from non-staffer", - helpers.MockMessage(reactions=[self._get_reaction(emoji=self.thumbs_up_emoji, nonstaff=1)]), - 0 - ), - # A message with a non-duck reaction from a staffer should return 0 - ( - "Non-duck reaction from staffer", - helpers.MockMessage(reactions=[self._get_reaction(emoji=self.non_duck_custom_emoji, staff=1)]), - 0 - ), - # A message with a non-duck reaction from a non-staffer and staffer should return 0 - ( - "Non-duck reaction from staffer + non-staffer", - helpers.MockMessage(reactions=[self._get_reaction(emoji=self.thumbs_up_emoji, staff=1, nonstaff=1)]), - 0 - ), - # A message with a unicode duck reaction from a non-staffer should return 0 - ( - "Unicode Duck Reaction from non-staffer", - helpers.MockMessage(reactions=[self._get_reaction(emoji=self.unicode_duck_emoji, nonstaff=1)]), - 0 - ), - # A message with a unicode duck reaction from a staffer should return 1 - ( - "Unicode Duck Reaction from staffer", - helpers.MockMessage(reactions=[self._get_reaction(emoji=self.unicode_duck_emoji, staff=1)]), - 1 - ), - # A message with a unicode duck reaction from a non-staffer and staffer should return 1 - ( - "Unicode Duck Reaction from staffer + non-staffer", - helpers.MockMessage(reactions=[self._get_reaction(emoji=self.unicode_duck_emoji, staff=1, nonstaff=1)]), - 1 - ), - # A message with a duckpond duck reaction from a non-staffer should return 0 - ( - "Duckpond Duck Reaction from non-staffer", - helpers.MockMessage(reactions=[self._get_reaction(emoji=self.duck_pond_emoji, nonstaff=1)]), - 0 - ), - # A message with a duckpond duck reaction from a staffer should return 1 - ( - "Duckpond Duck Reaction from staffer", - helpers.MockMessage(reactions=[self._get_reaction(emoji=self.duck_pond_emoji, staff=1)]), - 1 - ), - # A message with a duckpond duck reaction from a non-staffer and staffer should return 1 - ( - "Duckpond Duck Reaction from staffer + non-staffer", - helpers.MockMessage(reactions=[self._get_reaction(emoji=self.duck_pond_emoji, staff=1, nonstaff=1)]), - 1 - ), - - # Complex test cases - # A message with duckpond duck reactions from 3 staffers and 2 non-staffers returns 3 - ( - "Duckpond Duck Reaction from 3 staffers + 2 non-staffers", - helpers.MockMessage(reactions=[self._get_reaction(emoji=self.duck_pond_emoji, staff=3, nonstaff=2)]), - 3 - ), - # A staffer with multiple duck reactions only counts once - ( - "Two different duck reactions from the same staffer", - helpers.MockMessage( - reactions=[ - helpers.MockReaction(emoji=self.duck_pond_emoji, users=[self.staff_member]), - helpers.MockReaction(emoji=self.unicode_duck_emoji, users=[self.staff_member]), - ] - ), - 1 - ), - # A non-string emoji does not count (to test the `isinstance(reaction.emoji, str)` elif) - ( - "Reaction with non-Emoji/str emoij from 3 staffers + 2 non-staffers", - helpers.MockMessage(reactions=[self._get_reaction(emoji=100, staff=3, nonstaff=2)]), - 0 - ), - # We correctly sum when multiple reactions are provided. - ( - "Duckpond Duck Reaction from 3 staffers + 2 non-staffers", - helpers.MockMessage( - reactions=[ - self._get_reaction(emoji=self.duck_pond_emoji, staff=3, nonstaff=2), - self._get_reaction(emoji=self.unicode_duck_emoji, staff=4, nonstaff=9), - ] - ), - 3 + 4 - ), - ) - - for description, message, expected_count in test_cases: - actual_count = await self.cog.count_ducks(message) - with self.subTest(test_case=description, expected_count=expected_count, actual_count=actual_count): - self.assertEqual(expected_count, actual_count) - - async def test_relay_message_correctly_relays_content_and_attachments(self): - """The `relay_message` method should correctly relay message content and attachments.""" - send_webhook_path = f"{MODULE_PATH}.send_webhook" - send_attachments_path = f"{MODULE_PATH}.send_attachments" - author = MagicMock( - display_name="x", - avatar_url="https://" - ) - - self.cog.webhook = helpers.MockAsyncWebhook() - - test_values = ( - (helpers.MockMessage(author=author, clean_content="", attachments=[]), False, False), - (helpers.MockMessage(author=author, clean_content="message", attachments=[]), True, False), - (helpers.MockMessage(author=author, clean_content="", attachments=["attachment"]), False, True), - (helpers.MockMessage(author=author, clean_content="message", attachments=["attachment"]), True, True), - ) - - for message, expect_webhook_call, expect_attachment_call in test_values: - with patch(send_webhook_path, new_callable=AsyncMock) as send_webhook: - with patch(send_attachments_path, new_callable=AsyncMock) as send_attachments: - with self.subTest(clean_content=message.clean_content, attachments=message.attachments): - await self.cog.relay_message(message) - - self.assertEqual(expect_webhook_call, send_webhook.called) - self.assertEqual(expect_attachment_call, send_attachments.called) - - message.add_reaction.assert_called_once_with(self.checkmark_emoji) - - @patch(f"{MODULE_PATH}.send_attachments", new_callable=AsyncMock) - async def test_relay_message_handles_irretrievable_attachment_exceptions(self, send_attachments): - """The `relay_message` method should handle irretrievable attachments.""" - message = helpers.MockMessage(clean_content="message", attachments=["attachment"]) - side_effects = (discord.errors.Forbidden(MagicMock(), ""), discord.errors.NotFound(MagicMock(), "")) - - self.cog.webhook = helpers.MockAsyncWebhook() - log = logging.getLogger("bot.cogs.duck_pond") - - for side_effect in side_effects: # pragma: no cover - send_attachments.side_effect = side_effect - with patch(f"{MODULE_PATH}.send_webhook", new_callable=AsyncMock) as send_webhook: - with self.subTest(side_effect=type(side_effect).__name__): - with self.assertNotLogs(logger=log, level=logging.ERROR): - await self.cog.relay_message(message) - - self.assertEqual(send_webhook.call_count, 2) - - @patch(f"{MODULE_PATH}.send_webhook", new_callable=AsyncMock) - @patch(f"{MODULE_PATH}.send_attachments", new_callable=AsyncMock) - async def test_relay_message_handles_attachment_http_error(self, send_attachments, send_webhook): - """The `relay_message` method should handle irretrievable attachments.""" - message = helpers.MockMessage(clean_content="message", attachments=["attachment"]) - - self.cog.webhook = helpers.MockAsyncWebhook() - log = logging.getLogger("bot.cogs.duck_pond") - - side_effect = discord.HTTPException(MagicMock(), "") - send_attachments.side_effect = side_effect - with self.subTest(side_effect=type(side_effect).__name__): - with self.assertLogs(logger=log, level=logging.ERROR) as log_watcher: - await self.cog.relay_message(message) - - send_webhook.assert_called_once_with( - webhook=self.cog.webhook, - content=message.clean_content, - username=message.author.display_name, - avatar_url=message.author.avatar_url - ) - - self.assertEqual(len(log_watcher.records), 1) - - record = log_watcher.records[0] - self.assertEqual(record.levelno, logging.ERROR) - - def _mock_payload(self, label: str, is_custom_emoji: bool, id_: int, emoji_name: str): - """Creates a mock `on_raw_reaction_add` payload with the specified emoji data.""" - payload = MagicMock(name=label) - payload.emoji.is_custom_emoji.return_value = is_custom_emoji - payload.emoji.id = id_ - payload.emoji.name = emoji_name - return payload - - async def test_payload_has_duckpond_emoji_correctly_detects_relevant_emojis(self): - """The `on_raw_reaction_add` event handler should ignore irrelevant emojis.""" - test_values = ( - # Custom Emojis - ( - self._mock_payload( - label="Custom Duckpond Emoji", - is_custom_emoji=True, - id_=constants.DuckPond.custom_emojis[0], - emoji_name="" - ), - True - ), - ( - self._mock_payload( - label="Custom Non-Duckpond Emoji", - is_custom_emoji=True, - id_=123, - emoji_name="" - ), - False - ), - # Unicode Emojis - ( - self._mock_payload( - label="Unicode Duck Emoji", - is_custom_emoji=False, - id_=1, - emoji_name=self.unicode_duck_emoji - ), - True - ), - ( - self._mock_payload( - label="Unicode Non-Duck Emoji", - is_custom_emoji=False, - id_=1, - emoji_name=self.thumbs_up_emoji - ), - False - ), - ) - - for payload, expected_return in test_values: - actual_return = self.cog._payload_has_duckpond_emoji(payload) - with self.subTest(case=payload._mock_name, expected_return=expected_return, actual_return=actual_return): - self.assertEqual(expected_return, actual_return) - - @patch(f"{MODULE_PATH}.discord.utils.get") - @patch(f"{MODULE_PATH}.DuckPond._payload_has_duckpond_emoji", new=MagicMock(return_value=False)) - def test_on_raw_reaction_add_returns_early_with_payload_without_duck_emoji(self, utils_get): - """The `on_raw_reaction_add` method should return early if the payload does not contain a duck emoji.""" - self.assertIsNone(asyncio.run(self.cog.on_raw_reaction_add(payload=MagicMock()))) - - # Ensure we've returned before making an unnecessary API call in the lines of code after the emoji check - utils_get.assert_not_called() - - def _raw_reaction_mocks(self, channel_id, message_id, user_id): - """Sets up mocks for tests of the `on_raw_reaction_add` event listener.""" - channel = helpers.MockTextChannel(id=channel_id) - self.bot.get_all_channels.return_value = (channel,) - - message = helpers.MockMessage(id=message_id) - - channel.fetch_message.return_value = message - - member = helpers.MockMember(id=user_id, roles=[self.staff_role]) - message.guild.members = (member,) - - payload = MagicMock(channel_id=channel_id, message_id=message_id, user_id=user_id) - - return channel, message, member, payload - - async def test_on_raw_reaction_add_returns_for_bot_and_non_staff_members(self): - """The `on_raw_reaction_add` event handler should return for bot users or non-staff members.""" - channel_id = 1234 - message_id = 2345 - user_id = 3456 - - channel, message, _, payload = self._raw_reaction_mocks(channel_id, message_id, user_id) - - test_cases = ( - ("non-staff member", helpers.MockMember(id=user_id)), - ("bot staff member", helpers.MockMember(id=user_id, roles=[self.staff_role], bot=True)), - ) - - payload.emoji = self.duck_pond_emoji - - for description, member in test_cases: - message.guild.members = (member, ) - with self.subTest(test_case=description), patch(f"{MODULE_PATH}.DuckPond.has_green_checkmark") as checkmark: - checkmark.side_effect = AssertionError( - "Expected method to return before calling `self.has_green_checkmark`." - ) - self.assertIsNone(await self.cog.on_raw_reaction_add(payload)) - - # Check that we did make it past the payload checks - channel.fetch_message.assert_called_once() - channel.fetch_message.reset_mock() - - @patch(f"{MODULE_PATH}.DuckPond.is_staff") - @patch(f"{MODULE_PATH}.DuckPond.count_ducks", new_callable=AsyncMock) - def test_on_raw_reaction_add_returns_on_message_with_green_checkmark_placed_by_bot(self, count_ducks, is_staff): - """The `on_raw_reaction_add` event should return when the message has a green check mark placed by the bot.""" - channel_id = 31415926535 - message_id = 27182818284 - user_id = 16180339887 - - channel, message, member, payload = self._raw_reaction_mocks(channel_id, message_id, user_id) - - payload.emoji = helpers.MockPartialEmoji(name=self.unicode_duck_emoji) - payload.emoji.is_custom_emoji.return_value = False - - message.reactions = [helpers.MockReaction(emoji=self.checkmark_emoji, users=[self.bot.user])] - - is_staff.return_value = True - count_ducks.side_effect = AssertionError("Expected method to return before calling `self.count_ducks`") - - self.assertIsNone(asyncio.run(self.cog.on_raw_reaction_add(payload))) - - # Assert that we've made it past `self.is_staff` - is_staff.assert_called_once() - - async def test_on_raw_reaction_add_does_not_relay_below_duck_threshold(self): - """The `on_raw_reaction_add` listener should not relay messages or attachments below the duck threshold.""" - test_cases = ( - (constants.DuckPond.threshold - 1, False), - (constants.DuckPond.threshold, True), - (constants.DuckPond.threshold + 1, True), - ) - - channel, message, member, payload = self._raw_reaction_mocks(channel_id=3, message_id=4, user_id=5) - - payload.emoji = self.duck_pond_emoji - - for duck_count, should_relay in test_cases: - with patch(f"{MODULE_PATH}.DuckPond.relay_message", new_callable=AsyncMock) as relay_message: - with patch(f"{MODULE_PATH}.DuckPond.count_ducks", new_callable=AsyncMock) as count_ducks: - count_ducks.return_value = duck_count - with self.subTest(duck_count=duck_count, should_relay=should_relay): - await self.cog.on_raw_reaction_add(payload) - - # Confirm that we've made it past counting - count_ducks.assert_called_once() - - # Did we relay a message? - has_relayed = relay_message.called - self.assertEqual(has_relayed, should_relay) - - if should_relay: - relay_message.assert_called_once_with(message) - - async def test_on_raw_reaction_remove_prevents_removal_of_green_checkmark_depending_on_the_duck_count(self): - """The `on_raw_reaction_remove` listener prevents removal of the check mark on messages with enough ducks.""" - checkmark = helpers.MockPartialEmoji(name=self.checkmark_emoji) - - message = helpers.MockMessage(id=1234) - - channel = helpers.MockTextChannel(id=98765) - channel.fetch_message.return_value = message - - self.bot.get_all_channels.return_value = (channel, ) - - payload = MagicMock(channel_id=channel.id, message_id=message.id, emoji=checkmark) - - test_cases = ( - (constants.DuckPond.threshold - 1, False), - (constants.DuckPond.threshold, True), - (constants.DuckPond.threshold + 1, True), - ) - for duck_count, should_re_add_checkmark in test_cases: - with patch(f"{MODULE_PATH}.DuckPond.count_ducks", new_callable=AsyncMock) as count_ducks: - count_ducks.return_value = duck_count - with self.subTest(duck_count=duck_count, should_re_add_checkmark=should_re_add_checkmark): - await self.cog.on_raw_reaction_remove(payload) - - # Check if we fetched the message - channel.fetch_message.assert_called_once_with(message.id) - - # Check if we actually counted the number of ducks - count_ducks.assert_called_once_with(message) - - has_re_added_checkmark = message.add_reaction.called - self.assertEqual(should_re_add_checkmark, has_re_added_checkmark) - - if should_re_add_checkmark: - message.add_reaction.assert_called_once_with(self.checkmark_emoji) - message.add_reaction.reset_mock() - - # reset mocks - channel.fetch_message.reset_mock() - message.reset_mock() - - def test_on_raw_reaction_remove_ignores_removal_of_non_checkmark_reactions(self): - """The `on_raw_reaction_remove` listener should ignore the removal of non-check mark emojis.""" - channel = helpers.MockTextChannel(id=98765) - - channel.fetch_message.side_effect = AssertionError( - "Expected method to return before calling `channel.fetch_message`" - ) - - self.bot.get_all_channels.return_value = (channel, ) - - payload = MagicMock(emoji=helpers.MockPartialEmoji(name=self.thumbs_up_emoji), channel_id=channel.id) - - self.assertIsNone(asyncio.run(self.cog.on_raw_reaction_remove(payload))) - - channel.fetch_message.assert_not_called() - - -class DuckPondSetupTests(unittest.TestCase): - """Tests setup of the `DuckPond` cog.""" - - def test_setup(self): - """Setup of the extension should call add_cog.""" - bot = helpers.MockBot() - duck_pond.setup(bot) - bot.add_cog.assert_called_once() -- cgit v1.2.3 From 28bfbe65b3fe113948a027970eb7cb22f5b467b9 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff Date: Sun, 20 Sep 2020 13:27:23 +0200 Subject: Use helper for duckpond's locked relay feature I've created a helper for duckpond's relay feature to allow me to use it separately from the command we're planning to add to the Cog. I opted not to include the lock in the original relay method to separate the logic more clearly. Signed-off-by: Sebastiaan Zeeff --- bot/cogs/duck_pond.py | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/bot/cogs/duck_pond.py b/bot/cogs/duck_pond.py index 12f4cb7b8..6156c3238 100644 --- a/bot/cogs/duck_pond.py +++ b/bot/cogs/duck_pond.py @@ -105,6 +105,25 @@ class DuckPond(Cog): except discord.HTTPException: log.exception("Failed to send an attachment to the webhook") + async def locked_relay(self, message: discord.Message) -> bool: + """Relay a message after obtaining the relay lock.""" + if self.relay_lock is None: + # Lazily load the lock to ensure it's created within the + # appropriate event loop. + self.relay_lock = asyncio.Lock() + + async with self.relay_lock: + # check if the message has a checkmark after acquiring the lock + if await self.has_green_checkmark(message): + return False + + # relay the message + await self.relay_message(message) + + # add a green checkmark to indicate that the message was relayed + await message.add_reaction("✅") + return True + def _payload_has_duckpond_emoji(self, emoji: discord.PartialEmoji) -> bool: """Test if the RawReactionActionEvent payload contains a duckpond emoji.""" if emoji.is_unicode_emoji(): @@ -150,21 +169,7 @@ class DuckPond(Cog): # If we've got more than the required amount of ducks, send the message to the duck_pond. if duck_count >= constants.DuckPond.threshold: - if self.relay_lock is None: - # Lazily load the lock to ensure it's created within the - # appropriate event loop. - self.relay_lock = asyncio.Lock() - - async with self.relay_lock: - # check if the message has a checkmark after acquiring the lock - if await self.has_green_checkmark(message): - return - - # relay the message - await self.relay_message(message) - - # add a green checkmark to indicate that the message was relayed - await message.add_reaction("✅") + await self.locked_relay(message) @Cog.listener() async def on_raw_reaction_remove(self, payload: RawReactionActionEvent) -> None: -- cgit v1.2.3 From ef31ddad7a736ccea5662cd1e192d3663f587bd7 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff Date: Sun, 20 Sep 2020 13:29:00 +0200 Subject: Add command to relay a message to duckpond This commit adds a command that allows admins to manually relay a message to the duckpond, regardless of duck counts and the checks done in the reaction event handler. Signed-off-by: Sebastiaan Zeeff --- bot/cogs/duck_pond.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/bot/cogs/duck_pond.py b/bot/cogs/duck_pond.py index 6156c3238..2758de8ab 100644 --- a/bot/cogs/duck_pond.py +++ b/bot/cogs/duck_pond.py @@ -4,10 +4,11 @@ from typing import Union import discord from discord import Color, Embed, Member, Message, RawReactionActionEvent, User, errors -from discord.ext.commands import Cog +from discord.ext.commands import Cog, Context, command from bot import constants from bot.bot import Bot +from bot.decorators import with_role from bot.utils.messages import send_attachments from bot.utils.webhooks import send_webhook @@ -183,6 +184,15 @@ class DuckPond(Cog): if duck_count >= constants.DuckPond.threshold: await message.add_reaction("✅") + @command(name="duckify", aliases=("duckpond", "pondify")) + @with_role(constants.Roles.admins) + async def duckify(self, ctx: Context, message: discord.Message) -> None: + """Relay a message to the duckpond, no ducks required!""" + if await self.locked_relay(message): + await ctx.message.add_reaction("🦆") + else: + await ctx.message.add_reaction("❌") + def setup(bot: Bot) -> None: """Load the DuckPond cog.""" -- cgit v1.2.3 From 588213c6209b8df143634f916ff866bea4a9cec3 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff Date: Sun, 20 Sep 2020 20:26:25 +0200 Subject: Lower duckpond threshold to increase activity There's not a lot of activity in our duckpond at the moment. To activate our duckies, I've decreased the duckpond threshold to 4. This means that a message will now be relayed once it's been ducked four times. Let's get all of our ducks in a row. Signed-off-by: Sebastiaan Zeeff --- config-default.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config-default.yml b/config-default.yml index 4c1c7e483..fe15e5a87 100644 --- a/config-default.yml +++ b/config-default.yml @@ -463,7 +463,7 @@ sync: max_diff: 10 duck_pond: - threshold: 5 + threshold: 4 channel_blacklist: - *ANNOUNCEMENTS - *PYNEWS_CHANNEL -- cgit v1.2.3 From ecb132f9044b90f880d1d66ea19c99eb338b338e Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sun, 20 Sep 2020 21:47:50 +0300 Subject: Remove special shortening from reason --- bot/cogs/moderation/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/cogs/moderation/utils.py b/bot/cogs/moderation/utils.py index 4a3c14391..4dba8e812 100644 --- a/bot/cogs/moderation/utils.py +++ b/bot/cogs/moderation/utils.py @@ -157,7 +157,7 @@ async def notify_infraction( text = INFRACTION_DESCRIPTION_TEMPLATE.format( type=infr_type.capitalize(), expires=expires_at or "N/A", - reason=textwrap.shorten(reason, 1000, placeholder="...") if reason else "No reason provided." + reason=reason or "No reason provided." ) # For case when other fields than reason is too long and this reach limit, then force-shorten string -- cgit v1.2.3 From 5212cc6d00355e3376454c181d8f825cb2547ff1 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sun, 20 Sep 2020 21:59:18 +0300 Subject: Remove useless textwrap import --- bot/cogs/moderation/utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bot/cogs/moderation/utils.py b/bot/cogs/moderation/utils.py index 4dba8e812..5ad7838f0 100644 --- a/bot/cogs/moderation/utils.py +++ b/bot/cogs/moderation/utils.py @@ -1,5 +1,4 @@ import logging -import textwrap import typing as t from datetime import datetime -- cgit v1.2.3 From c22561d2f527666def2e201e655f5ac767d95212 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sun, 20 Sep 2020 22:08:16 +0300 Subject: Try to fix location from where post infraction test get ID --- tests/bot/cogs/moderation/test_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index c9a4e4040..02a18bbca 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -306,7 +306,7 @@ class TestPostInfraction(unittest.IsolatedAsyncioTestCase): """Should return response from POST request if there are no errors.""" now = datetime.now() payload = { - "actor": self.ctx.message.author.id, + "actor": self.ctx.author.id, "hidden": True, "reason": "Test reason", "type": "ban", @@ -344,7 +344,7 @@ class TestPostInfraction(unittest.IsolatedAsyncioTestCase): async def test_first_fail_second_success_user_post_infraction(self, post_user_mock): """Should post the user if they don't exist, POST infraction again, and return the response if successful.""" payload = { - "actor": self.ctx.message.author.id, + "actor": self.ctx.author.id, "hidden": False, "reason": "Test reason", "type": "mute", -- cgit v1.2.3 From a8b1c72d379d187b6266b6b38f9e85e594f39b11 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sun, 20 Sep 2020 22:22:37 +0300 Subject: Apply recent changes of notify infraction to test --- tests/bot/cogs/moderation/test_utils.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py index 02a18bbca..5f649e136 100644 --- a/tests/bot/cogs/moderation/test_utils.py +++ b/tests/bot/cogs/moderation/test_utils.py @@ -1,4 +1,3 @@ -import textwrap import unittest from collections import namedtuple from datetime import datetime @@ -211,8 +210,8 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): description=utils.INFRACTION_DESCRIPTION_TEMPLATE.format( type="Mute", expires="N/A", - reason=textwrap.shorten("foo bar" * 4000, 1000, placeholder="...") - ), + reason="foo bar" * 4000 + )[:2045] + "...", colour=Colours.soft_red, url=utils.RULES_URL ).set_author( -- cgit v1.2.3 From e68fad590415479f7b53545bf942d9f3b25ad1d3 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Mon, 21 Sep 2020 20:41:47 +0300 Subject: Fix end of file of mod utils tests --- tests/bot/exts/moderation/infraction/test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/bot/exts/moderation/infraction/test_utils.py b/tests/bot/exts/moderation/infraction/test_utils.py index 674993862..5f649e136 100644 --- a/tests/bot/exts/moderation/infraction/test_utils.py +++ b/tests/bot/exts/moderation/infraction/test_utils.py @@ -356,4 +356,4 @@ class TestPostInfraction(unittest.IsolatedAsyncioTestCase): actual = await utils.post_infraction(self.ctx, self.user, "mute", "Test reason") self.assertEqual(actual, "foo") self.bot.api_client.post.assert_has_awaits([call("bot/infractions", json=payload)] * 2) - post_user_mock.assert_awaited_once_with(self.ctx, self.user) \ No newline at end of file + post_user_mock.assert_awaited_once_with(self.ctx, self.user) -- cgit v1.2.3 From cebee6c45f54fab1ab965cc0c764d5f478fc4cdd Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Mon, 21 Sep 2020 20:52:02 +0300 Subject: Fix import path of mod utils --- tests/bot/exts/moderation/infraction/test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/bot/exts/moderation/infraction/test_utils.py b/tests/bot/exts/moderation/infraction/test_utils.py index 5f649e136..412f4398e 100644 --- a/tests/bot/exts/moderation/infraction/test_utils.py +++ b/tests/bot/exts/moderation/infraction/test_utils.py @@ -6,7 +6,7 @@ from unittest.mock import AsyncMock, MagicMock, call, patch from discord import Embed, Forbidden, HTTPException, NotFound from bot.api import ResponseCodeError -from bot.cogs.moderation import utils +from bot.exts.moderation.infraction import _utils as utils from bot.constants import Colours, Icons from tests.helpers import MockBot, MockContext, MockMember, MockUser -- cgit v1.2.3 From 82b6af9ef458cea71704c2aff0d2ef10e8b623be Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Mon, 21 Sep 2020 20:59:00 +0300 Subject: Fix import order of mod utils tests --- tests/bot/exts/moderation/infraction/test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/bot/exts/moderation/infraction/test_utils.py b/tests/bot/exts/moderation/infraction/test_utils.py index 412f4398e..fbbe112de 100644 --- a/tests/bot/exts/moderation/infraction/test_utils.py +++ b/tests/bot/exts/moderation/infraction/test_utils.py @@ -6,8 +6,8 @@ from unittest.mock import AsyncMock, MagicMock, call, patch from discord import Embed, Forbidden, HTTPException, NotFound from bot.api import ResponseCodeError -from bot.exts.moderation.infraction import _utils as utils from bot.constants import Colours, Icons +from bot.exts.moderation.infraction import _utils as utils from tests.helpers import MockBot, MockContext, MockMember, MockUser -- cgit v1.2.3 From 1051877d921eb9542ae845e3463aa2eadb5257a8 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Mon, 21 Sep 2020 20:32:15 +0200 Subject: Upload output with codeblock escapes to pastebin The output can't be sent to discord, but it won't affect anything in the paste service and can safely be uploaded to it. --- bot/exts/utils/snekbox.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bot/exts/utils/snekbox.py b/bot/exts/utils/snekbox.py index 03bf454ac..b3baffba2 100644 --- a/bot/exts/utils/snekbox.py +++ b/bot/exts/utils/snekbox.py @@ -150,6 +150,7 @@ class Snekbox(Cog): output = output.replace(" Date: Mon, 21 Sep 2020 20:41:46 +0200 Subject: Accommodate new upload behaviour in tests --- tests/bot/exts/utils/test_snekbox.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/bot/exts/utils/test_snekbox.py b/tests/bot/exts/utils/test_snekbox.py index c272a4756..40b2202aa 100644 --- a/tests/bot/exts/utils/test_snekbox.py +++ b/tests/bot/exts/utils/test_snekbox.py @@ -117,12 +117,12 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase): (' Date: Mon, 21 Sep 2020 21:45:27 +0300 Subject: Fix mod utils tests patch locations --- tests/bot/exts/moderation/infraction/test_utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/bot/exts/moderation/infraction/test_utils.py b/tests/bot/exts/moderation/infraction/test_utils.py index fbbe112de..5b62463e0 100644 --- a/tests/bot/exts/moderation/infraction/test_utils.py +++ b/tests/bot/exts/moderation/infraction/test_utils.py @@ -123,7 +123,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): else: self.ctx.send.assert_not_awaited() - @patch("bot.cogs.moderation.utils.send_private_embed") + @patch("bot.exts.moderation.infraction._utils.send_private_embed") async def test_notify_infraction(self, send_private_embed_mock): """ Should send an embed of a certain format as a DM and return `True` if DM successful. @@ -238,7 +238,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): send_private_embed_mock.assert_awaited_once_with(case["args"][0], embed) - @patch("bot.cogs.moderation.utils.send_private_embed") + @patch("bot.exts.moderation.infraction._utils.send_private_embed") async def test_notify_pardon(self, send_private_embed_mock): """Should send an embed of a certain format as a DM and return `True` if DM successful.""" test_case = namedtuple("test_case", ["args", "icon", "send_result"]) @@ -330,7 +330,7 @@ class TestPostInfraction(unittest.IsolatedAsyncioTestCase): self.assertTrue("500" in self.ctx.send.call_args[0][0]) - @patch("bot.cogs.moderation.utils.post_user", return_value=None) + @patch("bot.exts.moderation.infraction._utils.post_user", return_value=None) async def test_user_not_found_none_post_infraction(self, post_user_mock): """Should abort and return `None` when a new user fails to be posted.""" self.bot.api_client.post.side_effect = ResponseCodeError(MagicMock(status=400), {"user": "foo"}) @@ -339,7 +339,7 @@ class TestPostInfraction(unittest.IsolatedAsyncioTestCase): self.assertIsNone(actual) post_user_mock.assert_awaited_once_with(self.ctx, self.user) - @patch("bot.cogs.moderation.utils.post_user", return_value="bar") + @patch("bot.exts.moderation.infraction._utils.post_user", return_value="bar") async def test_first_fail_second_success_user_post_infraction(self, post_user_mock): """Should post the user if they don't exist, POST infraction again, and return the response if successful.""" payload = { -- cgit v1.2.3 From 90c3c3bddd17a7b5ec5907041cc401f8685a502b Mon Sep 17 00:00:00 2001 From: Xithrius Date: Mon, 21 Sep 2020 17:29:10 -0700 Subject: Updated dependencies to include aioping. --- Pipfile | 1 + Pipfile.lock | 246 ++++++++++++++++++++++++++++++++--------------------------- 2 files changed, 133 insertions(+), 114 deletions(-) diff --git a/Pipfile b/Pipfile index 6fff2223e..0c8ba1380 100644 --- a/Pipfile +++ b/Pipfile @@ -7,6 +7,7 @@ name = "pypi" aio-pika = "~=6.1" aiodns = "~=2.0" aiohttp = "~=3.5" +aioping = "~=0.3.1" aioredis = "~=1.3.1" beautifulsoup4 = "~=4.9" colorama = {version = "~=0.4.3",sys_platform = "== 'win32'"} diff --git a/Pipfile.lock b/Pipfile.lock index 50ddd478c..1f8ff4830 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "1905fd7eb15074ddbf04f2177b6cdd65edc4c74cb5fcbf4e6ca08ef649ba8a3c" + "sha256": "0dfb214d61424e266f95666076d13e959dd9ebc049e94d5d676d00aa5438de70" }, "pipfile-spec": 6, "requires": { @@ -18,11 +18,11 @@ "default": { "aio-pika": { "hashes": [ - "sha256:c4cbbeb85b3c7bf81bc127371846cd949e6231717ce1e6ac7ee1dd5ede21f866", - "sha256:ec7fef24f588d90314873463ab4f2c3debce0bd8830e49e3786586be96bc2e8e" + "sha256:4a20d4d941e1f113a950ea529a90bd9159c8d7aafaa1c71e9c707c8c2b526ea6", + "sha256:7bf3f183df1eb348d007210a0c1a3c5c755f1b3def1a9a395e93f30b91da1daf" ], "index": "pypi", - "version": "==6.6.1" + "version": "==6.7.0" }, "aiodns": { "hashes": [ @@ -50,6 +50,14 @@ "index": "pypi", "version": "==3.6.2" }, + "aioping": { + "hashes": [ + "sha256:8900ef2f5a589ba0c12aaa9c2d586f5371820d468d21b374ddb47ef5fc8f297c", + "sha256:f983d86acab3a04c322731ce88d42c55d04d2842565fc8532fe10c838abfd275" + ], + "index": "pypi", + "version": "==0.3.1" + }, "aioredis": { "hashes": [ "sha256:15f8af30b044c771aee6787e5ec24694c048184c7b9e54c3b60c750a4b93273a", @@ -83,11 +91,11 @@ }, "attrs": { "hashes": [ - "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", - "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" + "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594", + "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==19.3.0" + "version": "==20.2.0" }, "babel": { "hashes": [ @@ -115,36 +123,44 @@ }, "cffi": { "hashes": [ - "sha256:267adcf6e68d77ba154334a3e4fc921b8e63cbb38ca00d33d40655d4228502bc", - "sha256:26f33e8f6a70c255767e3c3f957ccafc7f1f706b966e110b855bfe944511f1f9", - "sha256:3cd2c044517f38d1b577f05927fb9729d3396f1d44d0c659a445599e79519792", - "sha256:4a03416915b82b81af5502459a8a9dd62a3c299b295dcdf470877cb948d655f2", - "sha256:4ce1e995aeecf7cc32380bc11598bfdfa017d592259d5da00fc7ded11e61d022", - "sha256:4f53e4128c81ca3212ff4cf097c797ab44646a40b42ec02a891155cd7a2ba4d8", - "sha256:4fa72a52a906425416f41738728268072d5acfd48cbe7796af07a923236bcf96", - "sha256:66dd45eb9530e3dde8f7c009f84568bc7cac489b93d04ac86e3111fb46e470c2", - "sha256:6923d077d9ae9e8bacbdb1c07ae78405a9306c8fd1af13bfa06ca891095eb995", - "sha256:833401b15de1bb92791d7b6fb353d4af60dc688eaa521bd97203dcd2d124a7c1", - "sha256:8416ed88ddc057bab0526d4e4e9f3660f614ac2394b5e019a628cdfff3733849", - "sha256:892daa86384994fdf4856cb43c93f40cbe80f7f95bb5da94971b39c7f54b3a9c", - "sha256:98be759efdb5e5fa161e46d404f4e0ce388e72fbf7d9baf010aff16689e22abe", - "sha256:a6d28e7f14ecf3b2ad67c4f106841218c8ab12a0683b1528534a6c87d2307af3", - "sha256:b1d6ebc891607e71fd9da71688fcf332a6630b7f5b7f5549e6e631821c0e5d90", - "sha256:b2a2b0d276a136146e012154baefaea2758ef1f56ae9f4e01c612b0831e0bd2f", - "sha256:b87dfa9f10a470eee7f24234a37d1d5f51e5f5fa9eeffda7c282e2b8f5162eb1", - "sha256:bac0d6f7728a9cc3c1e06d4fcbac12aaa70e9379b3025b27ec1226f0e2d404cf", - "sha256:c991112622baee0ae4d55c008380c32ecfd0ad417bcd0417ba432e6ba7328caa", - "sha256:cda422d54ee7905bfc53ee6915ab68fe7b230cacf581110df4272ee10462aadc", - "sha256:d3148b6ba3923c5850ea197a91a42683f946dba7e8eb82dfa211ab7e708de939", - "sha256:d6033b4ffa34ef70f0b8086fd4c3df4bf801fee485a8a7d4519399818351aa8e", - "sha256:ddff0b2bd7edcc8c82d1adde6dbbf5e60d57ce985402541cd2985c27f7bec2a0", - "sha256:e23cb7f1d8e0f93addf0cae3c5b6f00324cccb4a7949ee558d7b6ca973ab8ae9", - "sha256:effd2ba52cee4ceff1a77f20d2a9f9bf8d50353c854a282b8760ac15b9833168", - "sha256:f90c2267101010de42f7273c94a1f026e56cbc043f9330acd8a80e64300aba33", - "sha256:f960375e9823ae6a07072ff7f8a85954e5a6434f97869f50d0e41649a1c8144f", - "sha256:fcf32bf76dc25e30ed793145a57426064520890d7c02866eb93d3e4abe516948" - ], - "version": "==1.14.1" + "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": [ @@ -188,11 +204,11 @@ }, "discord.py": { "hashes": [ - "sha256:2b1846bfa382b54f4eace8e437a9f59f185388c5b08749ac0e1bbd98e05bfde5", - "sha256:f3db9531fccc391f51de65cfa46133106a9ba12ff2927aca6c14bffd3b7f17b5" + "sha256:98ea3096a3585c9c379209926f530808f5fcf4930928d8cfb579d2562d119570", + "sha256:f9decb3bfa94613d922376288617e6a6f969260923643e2897f4540c34793442" ], "markers": "python_full_version >= '3.5.3'", - "version": "==1.4.0" + "version": "==1.4.1" }, "docutils": { "hashes": [ @@ -204,11 +220,11 @@ }, "fakeredis": { "hashes": [ - "sha256:790c85ad0f3b2967aba1f51767021bc59760fcb612159584be018ea7384f7fd2", - "sha256:fdfe06f277092d022c271fcaefdc1f0c8d9bfa8cb15374cae41d66a20bd96d2b" + "sha256:7ea0866ba5edb40fe2e9b1722535df0c7e6b91d518aa5f50d96c2fff3ea7f4c2", + "sha256:aad8836ffe0319ffbba66dcf872ac6e7e32d1f19790e31296ba58445efb0a5c7" ], "index": "pypi", - "version": "==1.4.2" + "version": "==1.4.3" }, "feedparser": { "hashes": [ @@ -350,10 +366,11 @@ }, "markdownify": { "hashes": [ - "sha256:28ce67d1888e4908faaab7b04d2193cda70ea4f902f156a21d0aaea55e63e0a1" + "sha256:30be8340724e706c9e811c27fe8c1542cf74a15b46827924fff5c54b40dd9b0d", + "sha256:a69588194fd76634f0139d6801b820fd652dc5eeba9530e90d323dfdc0155252" ], "index": "pypi", - "version": "==0.4.1" + "version": "==0.5.3" }, "markupsafe": { "hashes": [ @@ -396,11 +413,11 @@ }, "more-itertools": { "hashes": [ - "sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5", - "sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2" + "sha256:6f83822ae94818eae2612063a5101a7311e68ae8002005b5e05f03fd74a86a20", + "sha256:9b30f12df9393f0d28af9210ff8efe48d10c94f73e5daf886f10c4b0b0b4f03c" ], "index": "pypi", - "version": "==8.4.0" + "version": "==8.5.0" }, "multidict": { "hashes": [ @@ -491,11 +508,11 @@ }, "pygments": { "hashes": [ - "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44", - "sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324" + "sha256:307543fe65c0947b126e83dd5a61bd8acbd84abec11f43caebaf5534cbc17998", + "sha256:926c3f319eda178d1bd90851e4317e6d8cdb5e292a3386aac9bd75eca29cf9c7" ], "markers": "python_version >= '3.5'", - "version": "==2.6.1" + "version": "==2.7.1" }, "pyparsing": { "hashes": [ @@ -555,11 +572,11 @@ }, "sentry-sdk": { "hashes": [ - "sha256:21b17d6aa064c0fb703a7c00f77cf6c9c497cf2f83345c28892980a5e742d116", - "sha256:4fc97114c77d005467b9b1a29f042e2bc01923cb683b0ef0bbda46e79fa12532" + "sha256:1a086486ff9da15791f294f6e9915eb3747d161ef64dee2d038a4d0b4a369b24", + "sha256:45486deb031cea6bbb25a540d7adb4dd48cd8a1cc31e6a5ce9fb4f792a572e9a" ], "index": "pypi", - "version": "==0.16.3" + "version": "==0.17.6" }, "six": { "hashes": [ @@ -697,11 +714,11 @@ }, "attrs": { "hashes": [ - "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", - "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" + "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594", + "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==19.3.0" + "version": "==20.2.0" }, "cfgv": { "hashes": [ @@ -713,43 +730,43 @@ }, "coverage": { "hashes": [ - "sha256:098a703d913be6fbd146a8c50cc76513d726b022d170e5e98dc56d958fd592fb", - "sha256:16042dc7f8e632e0dcd5206a5095ebd18cb1d005f4c89694f7f8aafd96dd43a3", - "sha256:1adb6be0dcef0cf9434619d3b892772fdb48e793300f9d762e480e043bd8e716", - "sha256:27ca5a2bc04d68f0776f2cdcb8bbd508bbe430a7bf9c02315cd05fb1d86d0034", - "sha256:28f42dc5172ebdc32622a2c3f7ead1b836cdbf253569ae5673f499e35db0bac3", - "sha256:2fcc8b58953d74d199a1a4d633df8146f0ac36c4e720b4a1997e9b6327af43a8", - "sha256:304fbe451698373dc6653772c72c5d5e883a4aadaf20343592a7abb2e643dae0", - "sha256:30bc103587e0d3df9e52cd9da1dd915265a22fad0b72afe54daf840c984b564f", - "sha256:40f70f81be4d34f8d491e55936904db5c527b0711b2a46513641a5729783c2e4", - "sha256:4186fc95c9febeab5681bc3248553d5ec8c2999b8424d4fc3a39c9cba5796962", - "sha256:46794c815e56f1431c66d81943fa90721bb858375fb36e5903697d5eef88627d", - "sha256:4869ab1c1ed33953bb2433ce7b894a28d724b7aa76c19b11e2878034a4e4680b", - "sha256:4f6428b55d2916a69f8d6453e48a505c07b2245653b0aa9f0dee38785939f5e4", - "sha256:52f185ffd3291196dc1aae506b42e178a592b0b60a8610b108e6ad892cfc1bb3", - "sha256:538f2fd5eb64366f37c97fdb3077d665fa946d2b6d95447622292f38407f9258", - "sha256:64c4f340338c68c463f1b56e3f2f0423f7b17ba6c3febae80b81f0e093077f59", - "sha256:675192fca634f0df69af3493a48224f211f8db4e84452b08d5fcebb9167adb01", - "sha256:700997b77cfab016533b3e7dbc03b71d33ee4df1d79f2463a318ca0263fc29dd", - "sha256:8505e614c983834239f865da2dd336dcf9d72776b951d5dfa5ac36b987726e1b", - "sha256:962c44070c281d86398aeb8f64e1bf37816a4dfc6f4c0f114756b14fc575621d", - "sha256:9e536783a5acee79a9b308be97d3952b662748c4037b6a24cbb339dc7ed8eb89", - "sha256:9ea749fd447ce7fb1ac71f7616371f04054d969d412d37611716721931e36efd", - "sha256:a34cb28e0747ea15e82d13e14de606747e9e484fb28d63c999483f5d5188e89b", - "sha256:a3ee9c793ffefe2944d3a2bd928a0e436cd0ac2d9e3723152d6fd5398838ce7d", - "sha256:aab75d99f3f2874733946a7648ce87a50019eb90baef931698f96b76b6769a46", - "sha256:b1ed2bdb27b4c9fc87058a1cb751c4df8752002143ed393899edb82b131e0546", - "sha256:b360d8fd88d2bad01cb953d81fd2edd4be539df7bfec41e8753fe9f4456a5082", - "sha256:b8f58c7db64d8f27078cbf2a4391af6aa4e4767cc08b37555c4ae064b8558d9b", - "sha256:c1bbb628ed5192124889b51204de27c575b3ffc05a5a91307e7640eff1d48da4", - "sha256:c2ff24df02a125b7b346c4c9078c8936da06964cc2d276292c357d64378158f8", - "sha256:c890728a93fffd0407d7d37c1e6083ff3f9f211c83b4316fae3778417eab9811", - "sha256:c96472b8ca5dc135fb0aa62f79b033f02aa434fb03a8b190600a5ae4102df1fd", - "sha256:ce7866f29d3025b5b34c2e944e66ebef0d92e4a4f2463f7266daa03a1332a651", - "sha256:e26c993bd4b220429d4ec8c1468eca445a4064a61c74ca08da7429af9bc53bb0" - ], - "index": "pypi", - "version": "==5.2.1" + "sha256:0203acd33d2298e19b57451ebb0bed0ab0c602e5cf5a818591b4918b1f97d516", + "sha256:0f313707cdecd5cd3e217fc68c78a960b616604b559e9ea60cc16795c4304259", + "sha256:1c6703094c81fa55b816f5ae542c6ffc625fec769f22b053adb42ad712d086c9", + "sha256:1d44bb3a652fed01f1f2c10d5477956116e9b391320c94d36c6bf13b088a1097", + "sha256:280baa8ec489c4f542f8940f9c4c2181f0306a8ee1a54eceba071a449fb870a0", + "sha256:29a6272fec10623fcbe158fdf9abc7a5fa032048ac1d8631f14b50fbfc10d17f", + "sha256:2b31f46bf7b31e6aa690d4c7a3d51bb262438c6dcb0d528adde446531d0d3bb7", + "sha256:2d43af2be93ffbad25dd959899b5b809618a496926146ce98ee0b23683f8c51c", + "sha256:381ead10b9b9af5f64646cd27107fb27b614ee7040bb1226f9c07ba96625cbb5", + "sha256:47a11bdbd8ada9b7ee628596f9d97fbd3851bd9999d398e9436bd67376dbece7", + "sha256:4d6a42744139a7fa5b46a264874a781e8694bb32f1d76d8137b68138686f1729", + "sha256:50691e744714856f03a86df3e2bff847c2acede4c191f9a1da38f088df342978", + "sha256:530cc8aaf11cc2ac7430f3614b04645662ef20c348dce4167c22d99bec3480e9", + "sha256:582ddfbe712025448206a5bc45855d16c2e491c2dd102ee9a2841418ac1c629f", + "sha256:63808c30b41f3bbf65e29f7280bf793c79f54fb807057de7e5238ffc7cc4d7b9", + "sha256:71b69bd716698fa62cd97137d6f2fdf49f534decb23a2c6fc80813e8b7be6822", + "sha256:7858847f2d84bf6e64c7f66498e851c54de8ea06a6f96a32a1d192d846734418", + "sha256:78e93cc3571fd928a39c0b26767c986188a4118edc67bc0695bc7a284da22e82", + "sha256:7f43286f13d91a34fadf61ae252a51a130223c52bfefb50310d5b2deb062cf0f", + "sha256:86e9f8cd4b0cdd57b4ae71a9c186717daa4c5a99f3238a8723f416256e0b064d", + "sha256:8f264ba2701b8c9f815b272ad568d555ef98dfe1576802ab3149c3629a9f2221", + "sha256:9342dd70a1e151684727c9c91ea003b2fb33523bf19385d4554f7897ca0141d4", + "sha256:9361de40701666b034c59ad9e317bae95c973b9ff92513dd0eced11c6adf2e21", + "sha256:9669179786254a2e7e57f0ecf224e978471491d660aaca833f845b72a2df3709", + "sha256:aac1ba0a253e17889550ddb1b60a2063f7474155465577caa2a3b131224cfd54", + "sha256:aef72eae10b5e3116bac6957de1df4d75909fc76d1499a53fb6387434b6bcd8d", + "sha256:bd3166bb3b111e76a4f8e2980fa1addf2920a4ca9b2b8ca36a3bc3dedc618270", + "sha256:c1b78fb9700fc961f53386ad2fd86d87091e06ede5d118b8a50dea285a071c24", + "sha256:c3888a051226e676e383de03bf49eb633cd39fc829516e5334e69b8d81aae751", + "sha256:c5f17ad25d2c1286436761b462e22b5020d83316f8e8fcb5deb2b3151f8f1d3a", + "sha256:c851b35fc078389bc16b915a0a7c1d5923e12e2c5aeec58c52f4aa8085ac8237", + "sha256:cb7df71de0af56000115eafd000b867d1261f786b5eebd88a0ca6360cccfaca7", + "sha256:cedb2f9e1f990918ea061f28a0f0077a07702e3819602d3507e2ff98c8d20636", + "sha256:e8caf961e1b1a945db76f1b5fa9c91498d15f545ac0ababbe575cfab185d3bd8" + ], + "index": "pypi", + "version": "==5.3" }, "distlib": { "hashes": [ @@ -775,11 +792,11 @@ }, "flake8-annotations": { "hashes": [ - "sha256:7816a5d8f65ffdf37b8e21e5b17e0fd1e492aa92638573276de066e889a22b26", - "sha256:8d18db74a750dd97f40b483cc3ef80d07d03f687525bad8fd83365dcd3bfd414" + "sha256:09fe1aa3f40cb8fef632a0ab3614050a7584bb884b6134e70cf1fc9eeee642fa", + "sha256:5bda552f074fd6e34276c7761756fa07d824ffac91ce9c0a8555eb2bc5b92d7a" ], "index": "pypi", - "version": "==2.3.0" + "version": "==2.4.0" }, "flake8-bugbear": { "hashes": [ @@ -837,11 +854,11 @@ }, "identify": { "hashes": [ - "sha256:110ed090fec6bce1aabe3c72d9258a9de82207adeaa5a05cd75c635880312f9a", - "sha256:ccd88716b890ecbe10920659450a635d2d25de499b9a638525a48b48261d989b" + "sha256:d7da7de6825568daa4449858ce328ecc0e1ada2554d972a6f4f90e736aaf499a", + "sha256:e4db4796b3b0cf4f9cb921da51430abffff2d4ba7d7c521184ed5252bd90d461" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.4.25" + "version": "==1.5.4" }, "mccabe": { "hashes": [ @@ -852,9 +869,10 @@ }, "nodeenv": { "hashes": [ - "sha256:4b0b77afa3ba9b54f4b6396e60b0c83f59eaeb2d63dc3cc7a70f7f4af96c82bc" + "sha256:5304d424c529c997bc888453aeaa6362d242b6b4631e90f3d4bf1b290f1c84a9", + "sha256:ab45090ae383b716c4ef89e690c41ff8c2b257b85b309f01f3654df3d084bd7c" ], - "version": "==1.4.0" + "version": "==1.5.0" }, "pep8-naming": { "hashes": [ @@ -866,11 +884,11 @@ }, "pre-commit": { "hashes": [ - "sha256:1657663fdd63a321a4a739915d7d03baedd555b25054449090f97bb0cb30a915", - "sha256:e8b1315c585052e729ab7e99dcca5698266bedce9067d21dc909c23e3ceed626" + "sha256:810aef2a2ba4f31eed1941fc270e72696a1ad5590b9751839c90807d0fff6b9a", + "sha256:c54fd3e574565fe128ecc5e7d2f91279772ddb03f8729645fa812fe809084a70" ], "index": "pypi", - "version": "==2.6.0" + "version": "==2.7.1" }, "pycodestyle": { "hashes": [ @@ -882,11 +900,11 @@ }, "pydocstyle": { "hashes": [ - "sha256:da7831660b7355307b32778c4a0dbfb137d89254ef31a2b2978f50fc0b4d7586", - "sha256:f4f5d210610c2d153fae39093d44224c17429e2ad7da12a8b419aba5c2f614b5" + "sha256:19b86fa8617ed916776a11cd8bc0197e5b9856d5433b777f51a3defe13075325", + "sha256:aca749e190a01726a4fb472dd4ef23b5c9da7b9205c0a7857c06533de13fd678" ], "markers": "python_version >= '3.5'", - "version": "==5.0.2" + "version": "==5.1.1" }, "pyflakes": { "hashes": [ @@ -937,19 +955,19 @@ }, "unittest-xml-reporting": { "hashes": [ - "sha256:74eaf7739a7957a74f52b8187c5616f61157372189bef0a32ba5c30bbc00e58a", - "sha256:e09b8ae70cce9904cdd331f53bf929150962869a5324ab7ff3dd6c8b87e01f7d" + "sha256:7bf515ea8cb244255a25100cd29db611a73f8d3d0aaf672ed3266307e14cc1ca", + "sha256:984cebba69e889401bfe3adb9088ca376b3a1f923f0590d005126c1bffd1a695" ], "index": "pypi", - "version": "==3.0.2" + "version": "==3.0.4" }, "virtualenv": { "hashes": [ - "sha256:7b54fd606a1b85f83de49ad8d80dbec08e983a2d2f96685045b262ebc7481ee5", - "sha256:8cd7b2a4850b003a11be2fc213e206419efab41115cc14bca20e69654f2ac08e" + "sha256:43add625c53c596d38f971a465553f6318decc39d98512bc100fa1b1e839c8dc", + "sha256:e0305af10299a7fb0d69393d8f04cb2965dda9351140d11ac8db4e5e3970451b" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.0.30" + "version": "==20.0.31" } } } -- cgit v1.2.3 From 9853de0e3c83f3a95d5741d1e86e0266a08b4ea6 Mon Sep 17 00:00:00 2001 From: Xithrius Date: Mon, 21 Sep 2020 17:33:53 -0700 Subject: Created the Latency cog to measure ping in milliseconds. --- bot/exts/utils/ping.py | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 bot/exts/utils/ping.py diff --git a/bot/exts/utils/ping.py b/bot/exts/utils/ping.py new file mode 100644 index 000000000..14b26506a --- /dev/null +++ b/bot/exts/utils/ping.py @@ -0,0 +1,57 @@ +import socket +from datetime import datetime + +import aioping +from discord import Embed +from discord.ext import commands + +from bot.bot import Bot +from bot.constants import Emojis, URLs + +DESCRIPTIONS = ( + "Time to receive command information", + "Python Discord website latency", + "Discord API latency" +) +ROUND_LATENCY = 3 + + +class Latency(commands.Cog): + """Getting the latency between the bot and websites""" + + def __init__(self, bot: Bot) -> None: + self.bot = bot + + @commands.command() + async def ping(self, ctx: commands.Context) -> None: + """ + Gets different measures of latency within the bot. + + Returns bot, Python Discord Site, Discord Protocol latency. + """ + # datetime.datetime objects do not have the "milliseconds" attribute. + # It must be converted to microseconds before converting to milliseconds. + bot_ping = (datetime.utcnow() - ctx.message.created_at).total_seconds() / 1000 + bot_ping = f"{round(bot_ping, ROUND_LATENCY)} ms" + + try: + delay = await aioping.ping(URLs.site, family=socket.AddressFamily.AF_INET) * 1000 + site_ping = f"{round(delay, ROUND_LATENCY)} ms" + + except TimeoutError: + site_ping = f"{Emojis.cross_mark} Connection timed out." + + # Discord Protocol latency return value is in seconds, must be multiplied by 1000 to get milliseconds. + discord_ping = f"{round(self.bot.latency * 1000, ROUND_LATENCY)} ms" + + embed = Embed(title="Pong!") + + for desc, latency in zip(DESCRIPTIONS, [bot_ping, site_ping, discord_ping]): + embed.add_field(name=desc, value=latency, inline=False) + + await ctx.send(embed=embed) + + +def setup(bot: Bot) -> None: + """Load the Latency cog.""" + bot.add_cog(Latency(bot)) -- cgit v1.2.3 From 65f8fb3cca197d19220492a31688093531b065cb Mon Sep 17 00:00:00 2001 From: Xithrius Date: Mon, 21 Sep 2020 17:34:15 -0700 Subject: Added period to docstring. --- bot/exts/utils/ping.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/utils/ping.py b/bot/exts/utils/ping.py index 14b26506a..c07a21bb7 100644 --- a/bot/exts/utils/ping.py +++ b/bot/exts/utils/ping.py @@ -17,7 +17,7 @@ ROUND_LATENCY = 3 class Latency(commands.Cog): - """Getting the latency between the bot and websites""" + """Getting the latency between the bot and websites.""" def __init__(self, bot: Bot) -> None: self.bot = bot -- cgit v1.2.3 From 427a2dc03d81de4aea0dce5e270226b1ec940b00 Mon Sep 17 00:00:00 2001 From: Xithrius Date: Mon, 21 Sep 2020 17:46:07 -0700 Subject: Description renamed to avoid verbosity. --- bot/exts/utils/ping.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/utils/ping.py b/bot/exts/utils/ping.py index c07a21bb7..f2faa11ca 100644 --- a/bot/exts/utils/ping.py +++ b/bot/exts/utils/ping.py @@ -9,7 +9,7 @@ from bot.bot import Bot from bot.constants import Emojis, URLs DESCRIPTIONS = ( - "Time to receive command information", + "Command processing time", "Python Discord website latency", "Discord API latency" ) -- cgit v1.2.3 From 00f4e909070adb7916ff25cf8c772c404a50d329 Mon Sep 17 00:00:00 2001 From: Den4200 Date: Mon, 21 Sep 2020 20:54:56 -0400 Subject: Use `has_any_role` decorator instead of old `with_role` decorator --- bot/exts/fun/duck_pond.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/fun/duck_pond.py b/bot/exts/fun/duck_pond.py index 2758de8ab..6c2d22b9c 100644 --- a/bot/exts/fun/duck_pond.py +++ b/bot/exts/fun/duck_pond.py @@ -8,7 +8,7 @@ from discord.ext.commands import Cog, Context, command from bot import constants from bot.bot import Bot -from bot.decorators import with_role +from bot.utils.checks import has_any_role from bot.utils.messages import send_attachments from bot.utils.webhooks import send_webhook @@ -185,7 +185,7 @@ class DuckPond(Cog): await message.add_reaction("✅") @command(name="duckify", aliases=("duckpond", "pondify")) - @with_role(constants.Roles.admins) + @has_any_role(constants.Roles.admins) async def duckify(self, ctx: Context, message: discord.Message) -> None: """Relay a message to the duckpond, no ducks required!""" if await self.locked_relay(message): -- cgit v1.2.3 From b8747d69568feff3900cd4ffda2755d9f101c65f Mon Sep 17 00:00:00 2001 From: Xithrius <15021300+Xithrius@users.noreply.github.com> Date: Mon, 21 Sep 2020 19:20:26 -0700 Subject: Comment updated from "microseconds" to "seconds" Co-authored-by: Dennis Pham --- bot/exts/utils/ping.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/utils/ping.py b/bot/exts/utils/ping.py index f2faa11ca..f26817159 100644 --- a/bot/exts/utils/ping.py +++ b/bot/exts/utils/ping.py @@ -30,7 +30,7 @@ class Latency(commands.Cog): Returns bot, Python Discord Site, Discord Protocol latency. """ # datetime.datetime objects do not have the "milliseconds" attribute. - # It must be converted to microseconds before converting to milliseconds. + # It must be converted to seconds before converting to milliseconds. bot_ping = (datetime.utcnow() - ctx.message.created_at).total_seconds() / 1000 bot_ping = f"{round(bot_ping, ROUND_LATENCY)} ms" -- cgit v1.2.3 From 912bfe00dc8edbb3795dc93485a8a871ccf9997c Mon Sep 17 00:00:00 2001 From: Xithrius <15021300+Xithrius@users.noreply.github.com> Date: Mon, 21 Sep 2020 19:21:36 -0700 Subject: Replacing the round function with a format specifier. Co-authored-by: Dennis Pham --- bot/exts/utils/ping.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/utils/ping.py b/bot/exts/utils/ping.py index f26817159..ea67a7b16 100644 --- a/bot/exts/utils/ping.py +++ b/bot/exts/utils/ping.py @@ -32,7 +32,7 @@ class Latency(commands.Cog): # datetime.datetime objects do not have the "milliseconds" attribute. # It must be converted to seconds before converting to milliseconds. bot_ping = (datetime.utcnow() - ctx.message.created_at).total_seconds() / 1000 - bot_ping = f"{round(bot_ping, ROUND_LATENCY)} ms" + bot_ping = f"{bot_ping:.{ROUND_LATENCY}f} ms" try: delay = await aioping.ping(URLs.site, family=socket.AddressFamily.AF_INET) * 1000 -- cgit v1.2.3 From 7ce1c29402e4cc5aff5cc3ebe81f5477877fdb52 Mon Sep 17 00:00:00 2001 From: Xithrius <15021300+Xithrius@users.noreply.github.com> Date: Mon, 21 Sep 2020 19:22:06 -0700 Subject: Replacing the round function with a format specifier. Co-authored-by: Dennis Pham --- bot/exts/utils/ping.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/utils/ping.py b/bot/exts/utils/ping.py index ea67a7b16..608b6c22c 100644 --- a/bot/exts/utils/ping.py +++ b/bot/exts/utils/ping.py @@ -36,7 +36,7 @@ class Latency(commands.Cog): try: delay = await aioping.ping(URLs.site, family=socket.AddressFamily.AF_INET) * 1000 - site_ping = f"{round(delay, ROUND_LATENCY)} ms" + site_ping = f"{delay:.{ROUND_LATENCY}f} ms" except TimeoutError: site_ping = f"{Emojis.cross_mark} Connection timed out." -- cgit v1.2.3 From 0fee151b4c0ec96ed49913ef5254eaf86d92cbcb Mon Sep 17 00:00:00 2001 From: Xithrius <15021300+Xithrius@users.noreply.github.com> Date: Mon, 21 Sep 2020 19:22:25 -0700 Subject: Replacing the round function with a format specifier. Co-authored-by: Dennis Pham --- bot/exts/utils/ping.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/utils/ping.py b/bot/exts/utils/ping.py index 608b6c22c..e19a2c099 100644 --- a/bot/exts/utils/ping.py +++ b/bot/exts/utils/ping.py @@ -42,7 +42,7 @@ class Latency(commands.Cog): site_ping = f"{Emojis.cross_mark} Connection timed out." # Discord Protocol latency return value is in seconds, must be multiplied by 1000 to get milliseconds. - discord_ping = f"{round(self.bot.latency * 1000, ROUND_LATENCY)} ms" + discord_ping = f"{self.bot.latency * 1000:.{ROUND_LATENCY}f} ms" embed = Embed(title="Pong!") -- cgit v1.2.3 From 6d9b68a0fe38fba669be242c111f5e3baac31516 Mon Sep 17 00:00:00 2001 From: Xithrius Date: Tue, 22 Sep 2020 10:40:44 -0700 Subject: Whitelisted the bot_commands channel and all staff for other channels. --- bot/exts/utils/ping.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bot/exts/utils/ping.py b/bot/exts/utils/ping.py index e19a2c099..a9ca3dbeb 100644 --- a/bot/exts/utils/ping.py +++ b/bot/exts/utils/ping.py @@ -6,7 +6,8 @@ from discord import Embed from discord.ext import commands from bot.bot import Bot -from bot.constants import Emojis, URLs +from bot.constants import Channels, Emojis, STAFF_ROLES, URLs +from bot.decorators import in_whitelist DESCRIPTIONS = ( "Command processing time", @@ -23,6 +24,7 @@ class Latency(commands.Cog): self.bot = bot @commands.command() + @in_whitelist(channels=(Channels.bot_commands,), roles=STAFF_ROLES) async def ping(self, ctx: commands.Context) -> None: """ Gets different measures of latency within the bot. -- cgit v1.2.3 From 675eb2fd1294766904be24758fe56cc54190a47e Mon Sep 17 00:00:00 2001 From: Xithrius Date: Tue, 22 Sep 2020 10:53:35 -0700 Subject: Updated dependencies to match with master and include aioping. --- Pipfile | 2 +- Pipfile.lock | 21 ++++++++++++++++----- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/Pipfile b/Pipfile index 0c8ba1380..e6f84d911 100644 --- a/Pipfile +++ b/Pipfile @@ -9,12 +9,12 @@ aiodns = "~=2.0" aiohttp = "~=3.5" aioping = "~=0.3.1" aioredis = "~=1.3.1" +"async-rediscache[fakeredis]" = "~=0.1.2" beautifulsoup4 = "~=4.9" colorama = {version = "~=0.4.3",sys_platform = "== 'win32'"} coloredlogs = "~=14.0" deepdiff = "~=4.0" discord.py = "~=1.4.0" -fakeredis = "~=1.4" feedparser = "~=5.2" fuzzywuzzy = "~=0.17" lxml = "~=4.4" diff --git a/Pipfile.lock b/Pipfile.lock index 1f8ff4830..acda79f11 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "0dfb214d61424e266f95666076d13e959dd9ebc049e94d5d676d00aa5438de70" + "sha256": "644012a1c3fa3e3a30f8b8f8e672c468dfaa155d9e43d26e2be8713c8dc5ebb3" }, "pipfile-spec": 6, "requires": { @@ -81,6 +81,18 @@ ], "version": "==0.7.12" }, + "async-rediscache": { + "extras": [ + "fakeredis" + ], + "hashes": [ + "sha256:407aed1aad97bf22f690eca5369806d22eefc8ca104a52c1f1bd47dd6db45fc2", + "sha256:563aaff79ec611a92a0ad78e39ff159e3a4b4cf0bea41e061de5f3701a17d50c" + ], + "index": "pypi", + "markers": "python_version ~= '3.7'", + "version": "==0.1.2" + }, "async-timeout": { "hashes": [ "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", @@ -223,7 +235,6 @@ "sha256:7ea0866ba5edb40fe2e9b1722535df0c7e6b91d518aa5f50d96c2fff3ea7f4c2", "sha256:aad8836ffe0319ffbba66dcf872ac6e7e32d1f19790e31296ba58445efb0a5c7" ], - "index": "pypi", "version": "==1.4.3" }, "feedparser": { @@ -572,11 +583,11 @@ }, "sentry-sdk": { "hashes": [ - "sha256:1a086486ff9da15791f294f6e9915eb3747d161ef64dee2d038a4d0b4a369b24", - "sha256:45486deb031cea6bbb25a540d7adb4dd48cd8a1cc31e6a5ce9fb4f792a572e9a" + "sha256:96a0e494b243a81065ec7ab73457d16719fb955ed9e469c8e4577ba737bc836e", + "sha256:a698993f3abbe06e88e8a3c8b61c8a79c12f62e503f1a23eda30c3921f0525a9" ], "index": "pypi", - "version": "==0.17.6" + "version": "==0.17.7" }, "six": { "hashes": [ -- cgit v1.2.3 From 24e58e302f1e3eaa7518183b9747c78e4a7f8f3d Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 22 Sep 2020 15:07:00 -0700 Subject: Fix type annotation for expanded infractions The `_utils.Infraction` alias does not cover nested data structures. Therefore, it's inappropriate for expanded infraction API responses. --- bot/exts/moderation/infraction/management.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bot/exts/moderation/infraction/management.py b/bot/exts/moderation/infraction/management.py index 622262c9b..0f3ea4bb1 100644 --- a/bot/exts/moderation/infraction/management.py +++ b/bot/exts/moderation/infraction/management.py @@ -11,7 +11,6 @@ from discord.utils import escape_markdown from bot import constants from bot.bot import Bot from bot.converters import Expiry, Snowflake, UserMention, allowed_strings, proxy_user -from bot.exts.moderation.infraction import _utils from bot.exts.moderation.infraction.infractions import Infractions from bot.exts.moderation.modlog import ModLog from bot.pagination import LinePaginator @@ -217,7 +216,7 @@ class ModManagement(commands.Cog): self, ctx: Context, embed: discord.Embed, - infractions: t.Iterable[_utils.Infraction] + infractions: t.Iterable[t.Dict[str, t.Any]] ) -> None: """Send a paginated embed of infractions for the specified user.""" if not infractions: -- cgit v1.2.3 From 3c8ecbdad6a6e6888b3f110a8eb6b875150740b8 Mon Sep 17 00:00:00 2001 From: Mark Date: Sat, 19 Sep 2020 11:53:20 -0700 Subject: Filtering: add missing space to log msg --- bot/exts/filters/filtering.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/filters/filtering.py b/bot/exts/filters/filtering.py index 80ec67641..92cdfb8f5 100644 --- a/bot/exts/filters/filtering.py +++ b/bot/exts/filters/filtering.py @@ -339,7 +339,7 @@ class Filtering(Cog): # Allow specific filters to override ping_everyone ping_everyone = Filter.ping_everyone and _filter.get("ping_everyone", True) - eval_msg = "using !eval" if is_eval else "" + eval_msg = "using !eval " if is_eval else "" message = ( f"The {filter_name} {_filter['type']} was triggered by {format_user(msg.author)} " f"{channel_str} {eval_msg}with [the following message]({msg.jump_url}):\n\n" -- cgit v1.2.3 From 5d3505fe3415882879a74ee8138b418bbb96df9a Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 22 Sep 2020 16:47:50 -0700 Subject: Fix future date check in snowflake converter --- bot/converters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/converters.py b/bot/converters.py index 4cfd663ba..2e118d476 100644 --- a/bot/converters.py +++ b/bot/converters.py @@ -209,7 +209,7 @@ class Snowflake(IDConverter): if time < DISCORD_EPOCH_DT: raise BadArgument(f"{error}: timestamp is before the Discord epoch.") - elif (datetime.utcnow() - time).days >= 1: + elif (datetime.utcnow() - time).days < -1: raise BadArgument(f"{error}: timestamp is too far into the future.") return snowflake -- cgit v1.2.3 From 38a6ad5d026dd002bc5744e1d7380cab8328e4dc Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 22 Sep 2020 16:49:41 -0700 Subject: Fix AttributeError for infraction user searches via the group --- 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 0f3ea4bb1..d448b22b2 100644 --- a/bot/exts/moderation/infraction/management.py +++ b/bot/exts/moderation/infraction/management.py @@ -179,7 +179,7 @@ class ModManagement(commands.Cog): async def infraction_search_group(self, ctx: Context, query: t.Union[UserMention, Snowflake, str]) -> None: """Searches for infractions in the database.""" if isinstance(query, int): - await ctx.invoke(self.search_user, query) + await ctx.invoke(self.search_user, discord.Object(query)) else: await ctx.invoke(self.search_reason, query) -- cgit v1.2.3 From 11397ec9b96cb63743347782a0690997059024c0 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 22 Sep 2020 17:07:56 -0700 Subject: Avoid using discord.Object's repr as the username for infraction search --- bot/exts/moderation/infraction/management.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/bot/exts/moderation/infraction/management.py b/bot/exts/moderation/infraction/management.py index d448b22b2..de4fb4175 100644 --- a/bot/exts/moderation/infraction/management.py +++ b/bot/exts/moderation/infraction/management.py @@ -190,6 +190,13 @@ class ModManagement(commands.Cog): 'bot/infractions/expanded', params={'user__id': str(user.id)} ) + + user = self.bot.get_user(user.id) + if not user and infraction_list: + # Use the user data retrieved from the DB for the username. + user = infraction_list[0] + user = escape_markdown(user["name"]) + f"#{user['discriminator']:04}" + embed = discord.Embed( title=f"Infractions for {user} ({len(infraction_list)} total)", colour=discord.Colour.orange() -- cgit v1.2.3 From 1ae971423ae5fcf3f8a68bcacbf1591c2ccf0aac Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 22 Sep 2020 20:44:38 -0700 Subject: Clean: fix mention in mod log message Fixes BOT-99 --- bot/exts/utils/clean.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/utils/clean.py b/bot/exts/utils/clean.py index 5a5ee9a81..bf25cb4c2 100644 --- a/bot/exts/utils/clean.py +++ b/bot/exts/utils/clean.py @@ -179,7 +179,7 @@ class Clean(Cog): message = ( f"**{len(message_ids)}** messages deleted in {target_channels} by " - f"{ctx.author.name.mention}\n\n" + f"{ctx.author.mention}\n\n" f"A log of the deleted messages can be found [here]({log_url})." ) -- cgit v1.2.3 From 4e49b9ffd7a359b8b6139e5cc6fc30c9e631b77d Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Wed, 23 Sep 2020 15:12:33 +0100 Subject: Update format_user to remove username and add ID --- bot/utils/messages.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/bot/utils/messages.py b/bot/utils/messages.py index 74956ed24..9cc0d8a34 100644 --- a/bot/utils/messages.py +++ b/bot/utils/messages.py @@ -9,7 +9,6 @@ from typing import List, Optional, Sequence, Union import discord from discord.errors import HTTPException from discord.ext.commands import Context -from discord.utils import escape_markdown from bot.constants import Emojis, NEGATIVE_REPLIES @@ -142,6 +141,5 @@ async def send_denial(ctx: Context, reason: str) -> None: def format_user(user: discord.abc.User) -> str: - """Return a string for `user` which has their mention and name#discriminator.""" - name = escape_markdown(str(user)) - return f"{user.mention} ({name})" + """Return a string for `user` which has their mention and ID.""" + return f"{user.mention} (`{user.id}`)" -- cgit v1.2.3 From bcca56c726d30f2c9e0cd762e9e65aebda2521d0 Mon Sep 17 00:00:00 2001 From: kwzrd Date: Wed, 23 Sep 2020 17:09:58 +0200 Subject: Verification: reduce request dispatch log level Avoid information duplication in production logs. --- bot/exts/moderation/verification.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/moderation/verification.py b/bot/exts/moderation/verification.py index 210c7a1af..6bbe81701 100644 --- a/bot/exts/moderation/verification.py +++ b/bot/exts/moderation/verification.py @@ -286,7 +286,7 @@ class Verification(Cog): Returns the amount of successful requests. Failed requests are logged at info level. """ - log.info(f"Sending {len(members)} requests") + log.trace(f"Sending {len(members)} requests") n_success, bad_statuses = 0, set() for progress, member in enumerate(members, start=1): -- cgit v1.2.3 From 5038aea67d41f579914dec2cf93042468dc2d3cf Mon Sep 17 00:00:00 2001 From: kwzrd Date: Wed, 23 Sep 2020 17:10:09 +0200 Subject: Incidents: bump archive log to INFO level --- bot/exts/moderation/incidents.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/moderation/incidents.py b/bot/exts/moderation/incidents.py index e49913552..31be48a43 100644 --- a/bot/exts/moderation/incidents.py +++ b/bot/exts/moderation/incidents.py @@ -237,7 +237,7 @@ class Incidents(Cog): not all information was relayed, return False. This signals that the original message is not safe to be deleted, as we will lose some information. """ - log.debug(f"Archiving incident: {incident.id} (outcome: {outcome}, actioned by: {actioned_by})") + log.info(f"Archiving incident: {incident.id} (outcome: {outcome}, actioned by: {actioned_by})") embed, attachment_file = await make_embed(incident, outcome, actioned_by) try: -- cgit v1.2.3 From 77205149613e25623ee646de977e5d5d0cd16e11 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Wed, 23 Sep 2020 09:35:18 -0700 Subject: Fix use of expanded infraction response for username Fixes BOT-9A --- 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 de4fb4175..856a4e1a2 100644 --- a/bot/exts/moderation/infraction/management.py +++ b/bot/exts/moderation/infraction/management.py @@ -194,7 +194,7 @@ class ModManagement(commands.Cog): user = self.bot.get_user(user.id) if not user and infraction_list: # Use the user data retrieved from the DB for the username. - user = infraction_list[0] + user = infraction_list[0]["user"] user = escape_markdown(user["name"]) + f"#{user['discriminator']:04}" embed = discord.Embed( -- cgit v1.2.3