aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--bot/cogs/antispam.py4
-rw-r--r--bot/cogs/clean.py3
-rw-r--r--bot/cogs/defcon.py3
-rw-r--r--bot/cogs/filtering.py124
-rw-r--r--bot/cogs/moderation/infractions.py3
-rw-r--r--bot/cogs/moderation/management.py49
-rw-r--r--bot/cogs/moderation/modlog.py35
-rw-r--r--bot/cogs/moderation/scheduler.py51
-rw-r--r--bot/cogs/moderation/superstarify.py12
-rw-r--r--bot/cogs/token_remover.py6
-rw-r--r--bot/cogs/verification.py3
-rw-r--r--bot/cogs/webhook_remover.py5
-rw-r--r--bot/utils/messages.py34
-rw-r--r--tests/bot/cogs/test_token_remover.py4
14 files changed, 149 insertions, 187 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/filtering.py b/bot/cogs/filtering.py
index 93cc1c655..0205e7a98 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
@@ -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.regex import INVITE_RE
from bot.utils.scheduling import Scheduler
@@ -32,6 +33,16 @@ ZALGO_RE = re.compile(r"[\u0300-\u036F\u0489]")
DAYS_BETWEEN_ALERTS = 3
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):
"""Filtering out invites, blacklisting domains, and warning us of certain regular expressions."""
@@ -174,8 +185,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)}"
)
@@ -214,35 +225,8 @@ 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
- )
+ 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
@@ -305,43 +289,51 @@ 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}"
+ stats = self._add_stats(filter_name, match, msg.content)
+ await self._send_log(filter_name, _filter["type"], msg, stats)
- message_content, additional_embeds, additional_embeds_msg = self._add_stats(
- filter_name, match, msg.content
- )
-
- 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}"
- )
+ break # We don't want multiple filters to trigger
- 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 if not is_private else False,
- additional_embeds=additional_embeds,
- additional_embeds_msg=additional_embeds_msg
- )
+ async def _send_log(
+ self,
+ filter_name: str,
+ filter_type: str,
+ msg: discord.Message,
+ stats: Stats,
+ *,
+ is_eval: bool = False,
+ ) -> None:
+ """Send a mod log for a triggered filter."""
+ if msg.channel.type is discord.ChannelType.private:
+ channel_str = "via DM"
+ ping_everyone = False
+ else:
+ channel_str = f"in {msg.channel.mention}"
+ ping_everyone = Filter.ping_everyone
+
+ 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"
+ f"{stats.message_content}"
+ )
- break # We don't want multiple filters to trigger
+ 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=ping_everyone,
+ 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":
@@ -378,7 +370,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:
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..56a601cb7 100644
--- a/bot/cogs/moderation/management.py
+++ b/bot/cogs/moderation/management.py
@@ -6,12 +6,13 @@ 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
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 +155,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 +168,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}
""")
)
@@ -191,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(
@@ -204,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(
@@ -241,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']:04})"
+
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 "==============="}
diff --git a/bot/cogs/moderation/modlog.py b/bot/cogs/moderation/modlog.py
index 0a63f57b8..5d3055796 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,12 +403,10 @@ class ModLog(Cog, name="ModLog"):
if member.guild.id != GuildConstant.id:
return
- member_str = escape_markdown(str(member))
- message = f"{member_str} (`{member.id}`)"
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}"
@@ -430,10 +428,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 +445,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 +507,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 +540,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 +635,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 +666,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 +718,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 +730,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 +808,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,
diff --git a/bot/cogs/moderation/scheduler.py b/bot/cogs/moderation/scheduler.py
index 75028d851..0d54075f0 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))
@@ -197,8 +196,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,
@@ -241,48 +240,12 @@ 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_}"
- # 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":
@@ -356,7 +319,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,
}
diff --git a/bot/cogs/moderation/superstarify.py b/bot/cogs/moderation/superstarify.py
index 867de815a..b23588b1c 100644
--- a/bot/cogs/moderation/superstarify.py
+++ b/bot/cogs/moderation/superstarify.py
@@ -7,11 +7,13 @@ 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
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
@@ -138,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"])
@@ -148,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,
@@ -181,8 +185,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 +225,7 @@ class Superstarify(InfractionScheduler, Cog):
)
return {
- "Member": f"{user.mention}(`{user.id}`)",
+ "Member": format_user(user),
"DM": "Sent" if notified else "**Failed**"
}
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 5812da87c..d87664e85 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?://)?discord(?:app)?\.com/api/webhooks/\d+/)\S+/?", re.IGNORECASE)
@@ -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)
diff --git a/bot/utils/messages.py b/bot/utils/messages.py
index 670289941..31825d4a7 100644
--- a/bot/utils/messages.py
+++ b/bot/utils/messages.py
@@ -6,10 +6,10 @@ 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
+from discord.utils import escape_markdown
from bot.constants import Emojis, NEGATIVE_REPLIES
@@ -17,12 +17,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 +42,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 +56,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 +81,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 +104,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,9 +138,15 @@ 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
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})"
diff --git a/tests/bot/cogs/test_token_remover.py b/tests/bot/cogs/test_token_remover.py
index 3349caa73..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,8 +241,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=format_user(self.msg.author),
channel=self.msg.channel.mention,
user_id=token.user_id,
timestamp=token.timestamp,