diff options
Diffstat (limited to '')
| -rw-r--r-- | bot/cogs/antispam.py | 4 | ||||
| -rw-r--r-- | bot/cogs/clean.py | 3 | ||||
| -rw-r--r-- | bot/cogs/defcon.py | 3 | ||||
| -rw-r--r-- | bot/cogs/filtering.py | 125 | ||||
| -rw-r--r-- | bot/cogs/moderation/infractions.py | 3 | ||||
| -rw-r--r-- | bot/cogs/moderation/management.py | 49 | ||||
| -rw-r--r-- | bot/cogs/moderation/modlog.py | 34 | ||||
| -rw-r--r-- | bot/cogs/moderation/scheduler.py | 51 | ||||
| -rw-r--r-- | bot/cogs/moderation/superstarify.py | 12 | ||||
| -rw-r--r-- | bot/cogs/token_remover.py | 6 | ||||
| -rw-r--r-- | bot/cogs/verification.py | 3 | ||||
| -rw-r--r-- | bot/cogs/webhook_remover.py | 5 | ||||
| -rw-r--r-- | bot/utils/messages.py | 34 | ||||
| -rw-r--r-- | tests/bot/cogs/test_token_remover.py | 3 | 
14 files changed, 148 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 29aac812f..67d4e6010 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.scheduling import Scheduler @@ -46,6 +47,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 +58,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): @@ -181,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)}"              ) @@ -221,35 +231,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 @@ -312,43 +295,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}" - -                        message_content, additional_embeds, additional_embeds_msg = self._add_stats( -                            filter_name, match, msg.content -                        ) +                        stats = self._add_stats(filter_name, match, msg.content) +                        await self._send_log(filter_name, _filter["type"], msg, stats) -                        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": @@ -385,7 +376,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..b4c69acc2 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']})" +          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..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, diff --git a/bot/cogs/moderation/scheduler.py b/bot/cogs/moderation/scheduler.py index 601e238c9..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,48 +236,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": @@ -352,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,          } 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..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, | 
