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
23 changes: 17 additions & 6 deletions bot/exts/moderation/incidents.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import asyncio
import re
from datetime import datetime
from datetime import datetime, timezone
from enum import Enum
from typing import Optional

Expand All @@ -13,6 +13,7 @@
from bot.constants import Channels, Colours, Emojis, Guild, Roles, Webhooks
from bot.log import get_logger
from bot.utils.messages import format_user, sub_clyde
from bot.utils.time import TimestampFormats, discord_timestamp

log = get_logger(__name__)

Expand All @@ -25,9 +26,9 @@
CRAWL_SLEEP = 2

DISCORD_MESSAGE_LINK_RE = re.compile(
r"(https?:\/\/(?:(ptb|canary|www)\.)?discord(?:app)?\.com\/channels\/"
r"(https?://(?:(ptb|canary|www)\.)?discord(?:app)?\.com/channels/"
r"[0-9]{15,20}"
r"\/[0-9]{15,20}\/[0-9]{15,20})"
r"/[0-9]{15,20}/[0-9]{15,20})"
)


Expand Down Expand Up @@ -97,10 +98,20 @@ async def make_embed(incident: discord.Message, outcome: Signal, actioned_by: di
colour = Colours.soft_red
footer = f"Rejected by {actioned_by}"

reported_timestamp = discord_timestamp(incident.created_at)
relative_timestamp = discord_timestamp(incident.created_at, TimestampFormats.RELATIVE)
reported_on_msg = f"*Reported {reported_timestamp} ({relative_timestamp}).*"

# If the description will be too long (>4096 total characters), truncate the incident content
if len(incident.content) > (allowed_content_chars := 4096-len(reported_on_msg)-2): # -2 for the newlines
description = incident.content[:allowed_content_chars-3] + f"...\n\n{reported_on_msg}"
else:
description = incident.content + f"\n\n{reported_on_msg}"
Comment thread
wookie184 marked this conversation as resolved.

embed = discord.Embed(
description=incident.content,
timestamp=datetime.utcnow(),
description=description,
colour=colour,
timestamp=datetime.now(timezone.utc)
)
embed.set_footer(text=footer, icon_url=actioned_by.display_avatar.url)

Expand Down Expand Up @@ -381,7 +392,7 @@ async def archive(self, incident: discord.Message, outcome: Signal, actioned_by:
webhook = await self.bot.fetch_webhook(Webhooks.incidents_archive)
await webhook.send(
embed=embed,
username=sub_clyde(incident.author.name),
username=sub_clyde(incident.author.display_name),
avatar_url=incident.author.display_avatar.url,
file=attachment_file,
)
Expand Down
49 changes: 36 additions & 13 deletions tests/bot/exts/moderation/test_incidents.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import asyncio
import datetime
import enum
import logging
import typing as t
Expand All @@ -12,12 +13,15 @@
from bot.constants import Colours
from bot.exts.moderation import incidents
from bot.utils.messages import format_user
from bot.utils.time import TimestampFormats, discord_timestamp
from tests.base import RedisTestCase
from tests.helpers import (
MockAsyncWebhook, MockAttachment, MockBot, MockMember, MockMessage, MockReaction, MockRole, MockTextChannel,
MockUser
)

CURRENT_TIME = datetime.datetime(2022, 1, 1, tzinfo=datetime.timezone.utc)


class MockAsyncIterable:
"""
Expand Down Expand Up @@ -100,30 +104,45 @@ class TestMakeEmbed(unittest.IsolatedAsyncioTestCase):

async def test_make_embed_actioned(self):
"""Embed is coloured green and footer contains 'Actioned' when `outcome=Signal.ACTIONED`."""
embed, file = await incidents.make_embed(MockMessage(), incidents.Signal.ACTIONED, MockMember())
embed, file = await incidents.make_embed(
incident=MockMessage(created_at=CURRENT_TIME),
outcome=incidents.Signal.ACTIONED,
actioned_by=MockMember()
)

self.assertEqual(embed.colour.value, Colours.soft_green)
self.assertIn("Actioned", embed.footer.text)

async def test_make_embed_not_actioned(self):
"""Embed is coloured red and footer contains 'Rejected' when `outcome=Signal.NOT_ACTIONED`."""
embed, file = await incidents.make_embed(MockMessage(), incidents.Signal.NOT_ACTIONED, MockMember())
embed, file = await incidents.make_embed(
incident=MockMessage(created_at=CURRENT_TIME),
outcome=incidents.Signal.NOT_ACTIONED,
actioned_by=MockMember()
)

self.assertEqual(embed.colour.value, Colours.soft_red)
self.assertIn("Rejected", embed.footer.text)

async def test_make_embed_content(self):
"""Incident content appears as embed description."""
incident = MockMessage(content="this is an incident")
incident = MockMessage(content="this is an incident", created_at=CURRENT_TIME)

reported_timestamp = discord_timestamp(CURRENT_TIME)
relative_timestamp = discord_timestamp(CURRENT_TIME, TimestampFormats.RELATIVE)

embed, file = await incidents.make_embed(incident, incidents.Signal.ACTIONED, MockMember())

self.assertEqual(incident.content, embed.description)
self.assertEqual(
f"{incident.content}\n\n*Reported {reported_timestamp} ({relative_timestamp}).*",
embed.description
)

async def test_make_embed_with_attachment_succeeds(self):
"""Incident's attachment is downloaded and displayed in the embed's image field."""
file = MagicMock(discord.File, filename="bigbadjoe.jpg")
attachment = MockAttachment(filename="bigbadjoe.jpg")
incident = MockMessage(content="this is an incident", attachments=[attachment])
incident = MockMessage(content="this is an incident", attachments=[attachment], created_at=CURRENT_TIME)

# Patch `download_file` to return our `file`
with patch("bot.exts.moderation.incidents.download_file", AsyncMock(return_value=file)):
Expand All @@ -135,7 +154,7 @@ async def test_make_embed_with_attachment_succeeds(self):
async def test_make_embed_with_attachment_fails(self):
"""Incident's attachment fails to download, proxy url is linked instead."""
attachment = MockAttachment(proxy_url="discord.com/bigbadjoe.jpg")
incident = MockMessage(content="this is an incident", attachments=[attachment])
incident = MockMessage(content="this is an incident", attachments=[attachment], created_at=CURRENT_TIME)

# Patch `download_file` to return None as if the download failed
with patch("bot.exts.moderation.incidents.download_file", AsyncMock(return_value=None)):
Expand Down Expand Up @@ -349,7 +368,6 @@ async def test_crawl_incidents_add_signals_called(self):

class TestArchive(TestIncidents):
"""Tests for the `Incidents.archive` coroutine."""

async def test_archive_webhook_not_found(self):
"""
Method recovers and returns False when the webhook is not found.
Expand All @@ -359,7 +377,11 @@ async def test_archive_webhook_not_found(self):
"""
self.cog_instance.bot.fetch_webhook = AsyncMock(side_effect=mock_404)
self.assertFalse(
await self.cog_instance.archive(incident=MockMessage(), outcome=MagicMock(), actioned_by=MockMember())
await self.cog_instance.archive(
incident=MockMessage(created_at=CURRENT_TIME),
outcome=MagicMock(),
actioned_by=MockMember()
)
)

async def test_archive_relays_incident(self):
Expand All @@ -375,7 +397,7 @@ async def test_archive_relays_incident(self):
# Define our own `incident` to be archived
incident = MockMessage(
content="this is an incident",
author=MockUser(name="author_name", display_avatar=Mock(url="author_avatar")),
author=MockUser(display_name="author_name", display_avatar=Mock(url="author_avatar")),
id=123,
)
built_embed = MagicMock(discord.Embed, id=123) # We patch `make_embed` to return this
Expand Down Expand Up @@ -406,7 +428,7 @@ async def test_archive_clyde_username(self):
webhook = MockAsyncWebhook()
self.cog_instance.bot.fetch_webhook = AsyncMock(return_value=webhook)

message_from_clyde = MockMessage(author=MockUser(name="clyde the great"))
message_from_clyde = MockMessage(author=MockUser(display_name="clyde the great"), created_at=CURRENT_TIME)
await self.cog_instance.archive(message_from_clyde, MagicMock(incidents.Signal), MockMember())

self.assertNotIn("clyde", webhook.send.call_args.kwargs["username"])
Expand Down Expand Up @@ -505,12 +527,13 @@ async def test_process_event_no_delete_if_archive_fails(self):
async def test_process_event_confirmation_task_is_awaited(self):
"""Task given by `Incidents.make_confirmation_task` is awaited before method exits."""
mock_task = AsyncMock()
mock_member = MockMember(display_name="Bobby Johnson", roles=[MockRole(id=1)])

with patch("bot.exts.moderation.incidents.Incidents.make_confirmation_task", mock_task):
await self.cog_instance.process_event(
reaction=incidents.Signal.ACTIONED.value,
incident=MockMessage(id=123),
member=MockMember(roles=[MockRole(id=1)])
incident=MockMessage(author=mock_member, id=123, created_at=CURRENT_TIME),
member=mock_member
)

mock_task.assert_awaited()
Expand All @@ -529,7 +552,7 @@ async def test_process_event_confirmation_task_timeout_is_handled(self):
with patch("bot.exts.moderation.incidents.Incidents.make_confirmation_task", mock_task):
await self.cog_instance.process_event(
reaction=incidents.Signal.ACTIONED.value,
incident=MockMessage(id=123),
incident=MockMessage(id=123, created_at=CURRENT_TIME),
member=MockMember(roles=[MockRole(id=1)])
)
except asyncio.TimeoutError:
Expand Down