From df897e57ea0dbbaeb3e2a07a1e443c5b2df34c36 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Mon, 30 Nov 2020 22:26:23 +0300 Subject: Adds Member Checks Before Changing Voice Adds a check that checks if the user object is an instance of guild member, before performing guild operations. Adds tests. Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- bot/exts/moderation/infraction/infractions.py | 12 ++++++++++-- tests/bot/exts/moderation/infraction/test_infractions.py | 15 ++++++++++++++- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/bot/exts/moderation/infraction/infractions.py b/bot/exts/moderation/infraction/infractions.py index 18e937e87..78c326c47 100644 --- a/bot/exts/moderation/infraction/infractions.py +++ b/bot/exts/moderation/infraction/infractions.py @@ -257,6 +257,10 @@ class Infractions(InfractionScheduler, commands.Cog): self.mod_log.ignore(Event.member_update, user.id) async def action() -> None: + # Skip members that left the server + if not isinstance(user, Member): + return + await user.add_roles(self._muted_role, reason=reason) log.trace(f"Attempting to kick {user} from voice because they've been muted.") @@ -351,9 +355,13 @@ class Infractions(InfractionScheduler, commands.Cog): if reason: reason = textwrap.shorten(reason, width=512, placeholder="...") - await user.move_to(None, reason="Disconnected from voice to apply voiceban.") + action = None + + # Skip members that left the server + if isinstance(user, Member): + await user.move_to(None, reason="Disconnected from voice to apply voiceban.") + action = user.remove_roles(self._voice_verified_role, reason=reason) - action = user.remove_roles(self._voice_verified_role, reason=reason) await self.apply_infraction(ctx, infraction, user, action) # endregion diff --git a/tests/bot/exts/moderation/infraction/test_infractions.py b/tests/bot/exts/moderation/infraction/test_infractions.py index bf557a484..4ba5a4feb 100644 --- a/tests/bot/exts/moderation/infraction/test_infractions.py +++ b/tests/bot/exts/moderation/infraction/test_infractions.py @@ -4,7 +4,7 @@ from unittest.mock import AsyncMock, MagicMock, Mock, patch from bot.constants import Event from bot.exts.moderation.infraction.infractions import Infractions -from tests.helpers import MockBot, MockContext, MockGuild, MockMember, MockRole +from tests.helpers import MockBot, MockContext, MockGuild, MockMember, MockRole, MockUser class TruncationTests(unittest.IsolatedAsyncioTestCase): @@ -164,6 +164,19 @@ class VoiceBanTests(unittest.IsolatedAsyncioTestCase): ) self.cog.apply_infraction.assert_awaited_once_with(self.ctx, {"foo": "bar"}, self.user, "my_return_value") + @patch("bot.exts.moderation.infraction._utils.get_active_infraction", return_value=None) + @patch("bot.exts.moderation.infraction._utils.post_infraction") + @patch("bot.exts.moderation.infraction.infractions.Infractions.apply_infraction") + async def test_voice_ban_user_left_guild(self, apply_infraction_mock, post_infraction_mock, _): + """Should voice ban user that left the guild without throwing an error.""" + infraction = {"foo": "bar"} + post_infraction_mock.return_value = {"foo": "bar"} + + user = MockUser() + await self.cog.voiceban(self.cog, self.ctx, user, reason=None) + post_infraction_mock.assert_called_once_with(self.ctx, user, "voice_ban", None, active=True) + apply_infraction_mock.assert_called_once_with(self.ctx, infraction, user, None) + async def test_voice_unban_user_not_found(self): """Should include info to return dict when user was not found from guild.""" self.guild.get_member.return_value = None -- cgit v1.2.3 From 37bfd7569cdeb0900ee131c22f67aed3f78692ba Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Wed, 20 Jan 2021 20:19:47 +0300 Subject: Cleans Up Tests --- tests/bot/exts/moderation/infraction/test_infractions.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/bot/exts/moderation/infraction/test_infractions.py b/tests/bot/exts/moderation/infraction/test_infractions.py index 4ba5a4feb..13efee054 100644 --- a/tests/bot/exts/moderation/infraction/test_infractions.py +++ b/tests/bot/exts/moderation/infraction/test_infractions.py @@ -3,8 +3,9 @@ import unittest from unittest.mock import AsyncMock, MagicMock, Mock, patch from bot.constants import Event +from bot.exts.moderation.infraction import _utils from bot.exts.moderation.infraction.infractions import Infractions -from tests.helpers import MockBot, MockContext, MockGuild, MockMember, MockRole, MockUser +from tests.helpers import MockBot, MockContext, MockGuild, MockMember, MockRole, MockUser, autospec class TruncationTests(unittest.IsolatedAsyncioTestCase): @@ -164,9 +165,8 @@ class VoiceBanTests(unittest.IsolatedAsyncioTestCase): ) self.cog.apply_infraction.assert_awaited_once_with(self.ctx, {"foo": "bar"}, self.user, "my_return_value") - @patch("bot.exts.moderation.infraction._utils.get_active_infraction", return_value=None) - @patch("bot.exts.moderation.infraction._utils.post_infraction") - @patch("bot.exts.moderation.infraction.infractions.Infractions.apply_infraction") + @autospec(_utils, "post_infraction", "get_active_infraction", return_value=None) + @autospec(Infractions, "apply_infraction") async def test_voice_ban_user_left_guild(self, apply_infraction_mock, post_infraction_mock, _): """Should voice ban user that left the guild without throwing an error.""" infraction = {"foo": "bar"} @@ -175,7 +175,7 @@ class VoiceBanTests(unittest.IsolatedAsyncioTestCase): user = MockUser() await self.cog.voiceban(self.cog, self.ctx, user, reason=None) post_infraction_mock.assert_called_once_with(self.ctx, user, "voice_ban", None, active=True) - apply_infraction_mock.assert_called_once_with(self.ctx, infraction, user, None) + apply_infraction_mock.assert_called_once_with(self.cog, self.ctx, infraction, user, None) async def test_voice_unban_user_not_found(self): """Should include info to return dict when user was not found from guild.""" -- cgit v1.2.3 From 06c5eba63f735237b44f2b1da9944bc7cddd2177 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Thu, 21 Jan 2021 01:26:41 +0300 Subject: Restructures Voice Ban Action Updates the voice ban action so the infraction pardoning is always run, and so all operations are handled in the _scheduler. Updates tests. --- bot/exts/moderation/infraction/infractions.py | 11 ++++--- .../exts/moderation/infraction/test_infractions.py | 37 +++++++++++++++------- 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/bot/exts/moderation/infraction/infractions.py b/bot/exts/moderation/infraction/infractions.py index 78c326c47..be4327bb0 100644 --- a/bot/exts/moderation/infraction/infractions.py +++ b/bot/exts/moderation/infraction/infractions.py @@ -355,14 +355,15 @@ class Infractions(InfractionScheduler, commands.Cog): if reason: reason = textwrap.shorten(reason, width=512, placeholder="...") - action = None + async def action() -> None: + # Skip members that left the server + if not isinstance(user, Member): + return - # Skip members that left the server - if isinstance(user, Member): await user.move_to(None, reason="Disconnected from voice to apply voiceban.") - action = user.remove_roles(self._voice_verified_role, reason=reason) + await user.remove_roles(self._voice_verified_role, reason=reason) - await self.apply_infraction(ctx, infraction, user, action) + await self.apply_infraction(ctx, infraction, user, action()) # endregion # region: Base pardon functions diff --git a/tests/bot/exts/moderation/infraction/test_infractions.py b/tests/bot/exts/moderation/infraction/test_infractions.py index 13efee054..86c2617ea 100644 --- a/tests/bot/exts/moderation/infraction/test_infractions.py +++ b/tests/bot/exts/moderation/infraction/test_infractions.py @@ -1,6 +1,7 @@ +import inspect import textwrap import unittest -from unittest.mock import AsyncMock, MagicMock, Mock, patch +from unittest.mock import ANY, AsyncMock, MagicMock, Mock, patch from bot.constants import Event from bot.exts.moderation.infraction import _utils @@ -133,20 +134,29 @@ class VoiceBanTests(unittest.IsolatedAsyncioTestCase): self.assertIsNone(await self.cog.apply_voice_ban(self.ctx, self.user, "foobar")) self.cog.mod_log.ignore.assert_called_once_with(Event.member_update, self.user.id) + async def action_tester(self, action, reason: str) -> None: + """Helper method to test voice ban action.""" + self.assertTrue(inspect.iscoroutine(action)) + await action + + self.user.move_to.assert_called_once_with(None, reason=ANY) + self.user.remove_roles.assert_called_once_with(self.cog._voice_verified_role, reason=reason) + @patch("bot.exts.moderation.infraction.infractions._utils.post_infraction") @patch("bot.exts.moderation.infraction.infractions._utils.get_active_infraction") async def test_voice_ban_apply_infraction(self, get_active_infraction, post_infraction_mock): """Should ignore Voice Verified role removing.""" self.cog.mod_log.ignore = MagicMock() self.cog.apply_infraction = AsyncMock() - self.user.remove_roles = MagicMock(return_value="my_return_value") get_active_infraction.return_value = None post_infraction_mock.return_value = {"foo": "bar"} - self.assertIsNone(await self.cog.apply_voice_ban(self.ctx, self.user, "foobar")) - self.user.remove_roles.assert_called_once_with(self.cog._voice_verified_role, reason="foobar") - self.cog.apply_infraction.assert_awaited_once_with(self.ctx, {"foo": "bar"}, self.user, "my_return_value") + reason = "foobar" + self.assertIsNone(await self.cog.apply_voice_ban(self.ctx, self.user, reason)) + self.cog.apply_infraction.assert_awaited_once_with(self.ctx, {"foo": "bar"}, self.user, ANY) + + await self.action_tester(self.cog.apply_infraction.call_args[0][-1], reason) @patch("bot.exts.moderation.infraction.infractions._utils.post_infraction") @patch("bot.exts.moderation.infraction.infractions._utils.get_active_infraction") @@ -154,16 +164,16 @@ class VoiceBanTests(unittest.IsolatedAsyncioTestCase): """Should truncate reason for voice ban.""" self.cog.mod_log.ignore = MagicMock() self.cog.apply_infraction = AsyncMock() - self.user.remove_roles = MagicMock(return_value="my_return_value") get_active_infraction.return_value = None post_infraction_mock.return_value = {"foo": "bar"} self.assertIsNone(await self.cog.apply_voice_ban(self.ctx, self.user, "foobar" * 3000)) - self.user.remove_roles.assert_called_once_with( - self.cog._voice_verified_role, reason=textwrap.shorten("foobar" * 3000, 512, placeholder="...") - ) - self.cog.apply_infraction.assert_awaited_once_with(self.ctx, {"foo": "bar"}, self.user, "my_return_value") + self.cog.apply_infraction.assert_awaited_once_with(self.ctx, {"foo": "bar"}, self.user, ANY) + + # Test action + action = self.cog.apply_infraction.call_args[0][-1] + await self.action_tester(action, textwrap.shorten("foobar" * 3000, 512, placeholder="...")) @autospec(_utils, "post_infraction", "get_active_infraction", return_value=None) @autospec(Infractions, "apply_infraction") @@ -175,7 +185,12 @@ class VoiceBanTests(unittest.IsolatedAsyncioTestCase): user = MockUser() await self.cog.voiceban(self.cog, self.ctx, user, reason=None) post_infraction_mock.assert_called_once_with(self.ctx, user, "voice_ban", None, active=True) - apply_infraction_mock.assert_called_once_with(self.cog, self.ctx, infraction, user, None) + apply_infraction_mock.assert_called_once_with(self.cog, self.ctx, infraction, user, ANY) + + # Test action + action = self.cog.apply_infraction.call_args[0][-1] + self.assertTrue(inspect.iscoroutine(action)) + await action async def test_voice_unban_user_not_found(self): """Should include info to return dict when user was not found from guild.""" -- cgit v1.2.3 From a41b932436b8b5c07c0f5ea3bd7ee886fa01e88e Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Thu, 21 Jan 2021 02:25:46 +0300 Subject: Updates Apply Infraction Docstring --- bot/exts/moderation/infraction/_scheduler.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bot/exts/moderation/infraction/_scheduler.py b/bot/exts/moderation/infraction/_scheduler.py index 242b2d30f..a73f2e8da 100644 --- a/bot/exts/moderation/infraction/_scheduler.py +++ b/bot/exts/moderation/infraction/_scheduler.py @@ -102,6 +102,7 @@ class InfractionScheduler: """ Apply an infraction to the user, log the infraction, and optionally notify the user. + `action_coro`, if not provided, will result in the infraction not getting scheduled for deletion. `user_reason`, if provided, will be sent to the user in place of the infraction reason. `additional_info` will be attached to the text field in the mod-log embed. -- cgit v1.2.3 From 576146af6a01b387c6b0abec1309169aa77a5bf2 Mon Sep 17 00:00:00 2001 From: PH-KDX <50588793+PH-KDX@users.noreply.github.com> Date: Fri, 22 Jan 2021 16:23:39 +0100 Subject: Create voice.md This tag would provide info for users which are not voice-verified, so that they can easily be directed toward the appropriate channel. --- bot/resources/tags/voice.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 bot/resources/tags/voice.md diff --git a/bot/resources/tags/voice.md b/bot/resources/tags/voice.md new file mode 100644 index 000000000..3d88b0c71 --- /dev/null +++ b/bot/resources/tags/voice.md @@ -0,0 +1,3 @@ +**Voice verification** + +Can’t talk in voice chat? Check out <#764802555427029012> to get access. The criteria for verifying are specified there. -- cgit v1.2.3 From eb45e9895a0bf1b294bbdca9b154ee433942989a Mon Sep 17 00:00:00 2001 From: PH-KDX <50588793+PH-KDX@users.noreply.github.com> Date: Sat, 23 Jan 2021 19:46:09 +0100 Subject: Rename voice.md to voice-verification.md --- bot/resources/tags/voice-verification.md | 3 +++ bot/resources/tags/voice.md | 3 --- 2 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 bot/resources/tags/voice-verification.md delete mode 100644 bot/resources/tags/voice.md diff --git a/bot/resources/tags/voice-verification.md b/bot/resources/tags/voice-verification.md new file mode 100644 index 000000000..3d88b0c71 --- /dev/null +++ b/bot/resources/tags/voice-verification.md @@ -0,0 +1,3 @@ +**Voice verification** + +Can’t talk in voice chat? Check out <#764802555427029012> to get access. The criteria for verifying are specified there. diff --git a/bot/resources/tags/voice.md b/bot/resources/tags/voice.md deleted file mode 100644 index 3d88b0c71..000000000 --- a/bot/resources/tags/voice.md +++ /dev/null @@ -1,3 +0,0 @@ -**Voice verification** - -Can’t talk in voice chat? Check out <#764802555427029012> to get access. The criteria for verifying are specified there. -- cgit v1.2.3 From 8130b34fe0d9c59b75294844b9f05942a4e6b5ad Mon Sep 17 00:00:00 2001 From: Chris Date: Sat, 30 Jan 2021 22:24:36 +0000 Subject: Protect against overflows caused by large expirations --- bot/converters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/converters.py b/bot/converters.py index d0a9731d6..0d9a519df 100644 --- a/bot/converters.py +++ b/bot/converters.py @@ -350,7 +350,7 @@ class Duration(DurationDelta): try: return now + delta - except ValueError: + except (ValueError, OverflowError): raise BadArgument(f"`{duration}` results in a datetime outside the supported range.") -- cgit v1.2.3 From 7432f580300c58321d3a37695779026fe5e51f8c Mon Sep 17 00:00:00 2001 From: wookie184 Date: Tue, 2 Feb 2021 14:16:17 +0000 Subject: Add tag on float imprecision Adds a tag on float imprecision --- bot/resources/tags/floats.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 bot/resources/tags/floats.md diff --git a/bot/resources/tags/floats.md b/bot/resources/tags/floats.md new file mode 100644 index 000000000..4b0651930 --- /dev/null +++ b/bot/resources/tags/floats.md @@ -0,0 +1,19 @@ +**Floating Point Arithmetic** +You may have noticed that when doing arithmetic with floats in Python you sometimes get strange results, like this: +```python +>>> 0.1 + 0.2 +0.30000000000000004 +``` +**Why this happens** +Internally your computer stores floats as as binary fractions. Many decimal values cannot be stored as exact binary fractions, which means an approximation has to be used. + +**How you can avoid this** +If you require an exact decimal representation, you can use the [decimal](https://docs.python.org/3/library/decimal.html) or [fractions](https://docs.python.org/3/library/fractions.html) module. Here is an example using the decimal module: +```python +>>> from decimal import Decimal +>>> Decimal('0.1') + Decimal('0.2') +Decimal('0.3') +``` +Note that we enter in the number we want as a string so we don't pass on the imprecision from the float. + +For more details on why this happens check out this [page in the python docs](https://docs.python.org/3/tutorial/floatingpoint.html) or this [Computerphile video](https://www.youtube.com/watch/PZRI1IfStY0). -- cgit v1.2.3 From 9c1e5fd22e71f919bcbb7e2215430a0b4d518310 Mon Sep 17 00:00:00 2001 From: wookie184 Date: Wed, 3 Feb 2021 10:00:42 +0000 Subject: Mention math.isclose and add an example --- bot/resources/tags/floats.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/bot/resources/tags/floats.md b/bot/resources/tags/floats.md index 4b0651930..7129b91bb 100644 --- a/bot/resources/tags/floats.md +++ b/bot/resources/tags/floats.md @@ -8,12 +8,13 @@ You may have noticed that when doing arithmetic with floats in Python you someti Internally your computer stores floats as as binary fractions. Many decimal values cannot be stored as exact binary fractions, which means an approximation has to be used. **How you can avoid this** -If you require an exact decimal representation, you can use the [decimal](https://docs.python.org/3/library/decimal.html) or [fractions](https://docs.python.org/3/library/fractions.html) module. Here is an example using the decimal module: + You can use [math.isclose](https://docs.python.org/3/library/math.html#math.isclose) to check if two floats are close, or to get an exact decimal representation, you can use the [decimal](https://docs.python.org/3/library/decimal.html) or [fractions](https://docs.python.org/3/library/fractions.html) module. Here are some examples: ```python ->>> from decimal import Decimal ->>> Decimal('0.1') + Decimal('0.2') +>>> math.isclose(0.1 + 0.2, 0.3) +True +>>> decimal.Decimal('0.1') + decimal.Decimal('0.2') Decimal('0.3') ``` -Note that we enter in the number we want as a string so we don't pass on the imprecision from the float. +Note that with `decimal.Decimal` we enter the number we want as a string so we don't pass on the imprecision from the float. For more details on why this happens check out this [page in the python docs](https://docs.python.org/3/tutorial/floatingpoint.html) or this [Computerphile video](https://www.youtube.com/watch/PZRI1IfStY0). -- cgit v1.2.3 From 555c1a8a4543f051b950c72a4b89805db988ca76 Mon Sep 17 00:00:00 2001 From: Senjan21 <53477086+Senjan21@users.noreply.github.com> Date: Fri, 5 Feb 2021 12:29:27 +0100 Subject: Add jumplink to deleted messages --- bot/exts/moderation/modlog.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bot/exts/moderation/modlog.py b/bot/exts/moderation/modlog.py index e4b119f41..2dae9d268 100644 --- a/bot/exts/moderation/modlog.py +++ b/bot/exts/moderation/modlog.py @@ -546,6 +546,7 @@ class ModLog(Cog, name="ModLog"): f"**Author:** {format_user(author)}\n" f"**Channel:** {channel.category}/#{channel.name} (`{channel.id}`)\n" f"**Message ID:** `{message.id}`\n" + f"[Jump to message]({message.jump_url})\n" "\n" ) else: @@ -553,6 +554,7 @@ class ModLog(Cog, name="ModLog"): f"**Author:** {format_user(author)}\n" f"**Channel:** #{channel.name} (`{channel.id}`)\n" f"**Message ID:** `{message.id}`\n" + f"[Jump to message]({message.jump_url})\n" "\n" ) -- cgit v1.2.3