Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bot/exts/moderation/infraction/_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ async def apply_infraction(
"""
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.

Expand Down
15 changes: 12 additions & 3 deletions bot/exts/moderation/infraction/infractions.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,10 @@ async def apply_mute(self, ctx: Context, user: Member, reason: t.Optional[str],
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.")
Expand Down Expand Up @@ -351,10 +355,15 @@ async def apply_voice_ban(self, ctx: Context, user: UserSnowflake, reason: t.Opt
if reason:
reason = textwrap.shorten(reason, width=512, placeholder="...")

await user.move_to(None, reason="Disconnected from voice to apply voiceban.")
async def action() -> None:
# Skip members that left the server
if not isinstance(user, Member):
return

action = user.remove_roles(self._voice_verified_role, reason=reason)
await self.apply_infraction(ctx, infraction, user, action)
await user.move_to(None, reason="Disconnected from voice to apply voiceban.")
await user.remove_roles(self._voice_verified_role, reason=reason)

await self.apply_infraction(ctx, infraction, user, action())

# endregion
# region: Base pardon functions
Expand Down
50 changes: 39 additions & 11 deletions tests/bot/exts/moderation/infraction/test_infractions.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
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
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, autospec


class TruncationTests(unittest.IsolatedAsyncioTestCase):
Expand Down Expand Up @@ -132,37 +134,63 @@ async def test_voice_ban_mod_log_ignore(self, get_active_infraction, post_infrac
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")
async def test_voice_ban_truncate_reason(self, get_active_infraction, post_infraction_mock):
"""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")
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.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."""
Expand Down