diff options
| author | 2018-12-30 15:05:16 +0100 | |
|---|---|---|
| committer | 2018-12-30 15:05:16 +0100 | |
| commit | c4ffaed9813c389b107710dc05c83e3dd062a245 (patch) | |
| tree | b4c77a9f856be2dd72bdc4f7fd05ec47a5ab02ba | |
| parent | Add missing awaits (diff) | |
| parent | Merge pull request #223 from python-discord/mod-dm-status (diff) | |
Merge branch 'master' into defcon-channel-title
| -rw-r--r-- | bot/cogs/filtering.py | 14 | ||||
| -rw-r--r-- | bot/cogs/moderation.py | 133 | ||||
| -rw-r--r-- | bot/cogs/modlog.py | 12 | ||||
| -rw-r--r-- | bot/cogs/snekbox.py | 26 | ||||
| -rw-r--r-- | bot/cogs/utils.py | 63 | ||||
| -rw-r--r-- | bot/constants.py | 2 | ||||
| -rw-r--r-- | bot/decorators.py | 44 | ||||
| -rw-r--r-- | config-default.yml | 2 | 
8 files changed, 215 insertions, 81 deletions
| diff --git a/bot/cogs/filtering.py b/bot/cogs/filtering.py index b6ce501fc..0ba1e49c5 100644 --- a/bot/cogs/filtering.py +++ b/bot/cogs/filtering.py @@ -238,15 +238,13 @@ class Filtering:                  f"{URLs.discord_invite_api}/{invite}"              )              response = await response.json() -            if response.get("guild") is None: -                # If we have a valid invite which is not a guild invite -                # it might be a DM channel invite -                if response.get("channel") is not None: -                    # We don't have whitelisted Group DMs so we can -                    # go ahead and return a positive for any group DM -                    return True +            guild = response.get("guild") +            if guild is None: +                # We don't have whitelisted Group DMs so we can +                # go ahead and return a positive for any group DM +                return True -            guild_id = int(response.get("guild").get("id")) +            guild_id = int(guild.get("id"))              if guild_id not in Filter.guild_invite_whitelist:                  return True diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py index 6e958b912..ac08d3dd4 100644 --- a/bot/cogs/moderation.py +++ b/bot/cogs/moderation.py @@ -82,7 +82,7 @@ class Moderation(Scheduler):          :param reason: The reason for the warning.          """ -        await self.notify_infraction( +        notified = await self.notify_infraction(              user=user,              infr_type="Warning",              reason=reason @@ -92,12 +92,29 @@ class Moderation(Scheduler):          if response_object is None:              return +        dm_result = ":incoming_envelope: " if notified else "" +        action = f"{dm_result}:ok_hand: warned {user.mention}" +          if reason is None: -            result_message = f":ok_hand: warned {user.mention}." +            await ctx.send(f"{action}.")          else: -            result_message = f":ok_hand: warned {user.mention} ({reason})." +            await ctx.send(f"{action} ({reason}).") -        await ctx.send(result_message) +        if not notified: +            await self.log_notify_failure(user, ctx.author, "warning") + +        # Send a message to the mod log +        await self.mod_log.send_log_message( +            icon_url=Icons.user_warn, +            colour=Colour(Colours.soft_red), +            title="Member warned", +            thumbnail=user.avatar_url_as(static_format="png"), +            text=textwrap.dedent(f""" +                Member: {user.mention} (`{user.id}`) +                Actor: {ctx.message.author} +                Reason: {reason} +            """) +        )      @with_role(*MODERATION_ROLES)      @command(name="kick") @@ -108,7 +125,7 @@ class Moderation(Scheduler):          :param reason: The reason for the kick.          """ -        await self.notify_infraction( +        notified = await self.notify_infraction(              user=user,              infr_type="Kick",              reason=reason @@ -121,12 +138,16 @@ class Moderation(Scheduler):          self.mod_log.ignore(Event.member_remove, user.id)          await user.kick(reason=reason) +        dm_result = ":incoming_envelope: " if notified else "" +        action = f"{dm_result}:ok_hand: kicked {user.mention}" +          if reason is None: -            result_message = f":ok_hand: kicked {user.mention}." +            await ctx.send(f"{action}.")          else: -            result_message = f":ok_hand: kicked {user.mention} ({reason})." +            await ctx.send(f"{action} ({reason}).") -        await ctx.send(result_message) +        if not notified: +            await self.log_notify_failure(user, ctx.author, "kick")          # Send a log message to the mod log          await self.mod_log.send_log_message( @@ -150,7 +171,7 @@ class Moderation(Scheduler):          :param reason: The reason for the ban.          """ -        await self.notify_infraction( +        notified = await self.notify_infraction(              user=user,              infr_type="Ban",              duration="Permanent", @@ -165,12 +186,16 @@ class Moderation(Scheduler):          self.mod_log.ignore(Event.member_remove, user.id)          await ctx.guild.ban(user, reason=reason, delete_message_days=0) +        dm_result = ":incoming_envelope: " if notified else "" +        action = f"{dm_result}:ok_hand: permanently banned {user.mention}" +          if reason is None: -            result_message = f":ok_hand: permanently banned {user.mention}." +            await ctx.send(f"{action}.")          else: -            result_message = f":ok_hand: permanently banned {user.mention} ({reason})." +            await ctx.send(f"{action} ({reason}).") -        await ctx.send(result_message) +        if not notified: +            await self.log_notify_failure(user, ctx.author, "ban")          # Send a log message to the mod log          await self.mod_log.send_log_message( @@ -194,7 +219,7 @@ class Moderation(Scheduler):          :param reason: The reason for the mute.          """ -        await self.notify_infraction( +        notified = await self.notify_infraction(              user=user,              infr_type="Mute",              duration="Permanent", @@ -209,12 +234,16 @@ class Moderation(Scheduler):          self.mod_log.ignore(Event.member_update, user.id)          await user.add_roles(self._muted_role, reason=reason) +        dm_result = ":incoming_envelope: " if notified else "" +        action = f"{dm_result}:ok_hand: permanently muted {user.mention}" +          if reason is None: -            result_message = f":ok_hand: permanently muted {user.mention}." +            await ctx.send(f"{action}.")          else: -            result_message = f":ok_hand: permanently muted {user.mention} ({reason})." +            await ctx.send(f"{action} ({reason}).") -        await ctx.send(result_message) +        if not notified: +            await self.log_notify_failure(user, ctx.author, "mute")          # Send a log message to the mod log          await self.mod_log.send_log_message( @@ -242,7 +271,7 @@ class Moderation(Scheduler):          :param reason: The reason for the temporary mute.          """ -        await self.notify_infraction( +        notified = await self.notify_infraction(              user=user,              infr_type="Mute",              duration=duration, @@ -262,12 +291,16 @@ class Moderation(Scheduler):          loop = asyncio.get_event_loop()          self.schedule_task(loop, infraction_object["id"], infraction_object) +        dm_result = ":incoming_envelope: " if notified else "" +        action = f"{dm_result}:ok_hand: muted {user.mention} until {infraction_expiration}" +          if reason is None: -            result_message = f":ok_hand: muted {user.mention} until {infraction_expiration}." +            await ctx.send(f"{action}.")          else: -            result_message = f":ok_hand: muted {user.mention} until {infraction_expiration} ({reason})." +            await ctx.send(f"{action} ({reason}).") -        await ctx.send(result_message) +        if not notified: +            await self.log_notify_failure(user, ctx.author, "mute")          # Send a log message to the mod log          await self.mod_log.send_log_message( @@ -294,7 +327,7 @@ class Moderation(Scheduler):          :param reason: The reason for the temporary ban.          """ -        await self.notify_infraction( +        notified = await self.notify_infraction(              user=user,              infr_type="Ban",              duration=duration, @@ -316,12 +349,16 @@ class Moderation(Scheduler):          loop = asyncio.get_event_loop()          self.schedule_task(loop, infraction_object["id"], infraction_object) +        dm_result = ":incoming_envelope: " if notified else "" +        action = f"{dm_result}:ok_hand: banned {user.mention} until {infraction_expiration}" +          if reason is None: -            result_message = f":ok_hand: banned {user.mention} until {infraction_expiration}." +            await ctx.send(f"{action}.")          else: -            result_message = f":ok_hand: banned {user.mention} until {infraction_expiration} ({reason})." +            await ctx.send(f"{action} ({reason}).") -        await ctx.send(result_message) +        if not notified: +            await self.log_notify_failure(user, ctx.author, "ban")          # Send a log message to the mod log          await self.mod_log.send_log_message( @@ -361,6 +398,19 @@ class Moderation(Scheduler):          await ctx.send(result_message) +        # Send a message to the mod log +        await self.mod_log.send_log_message( +            icon_url=Icons.user_warn, +            colour=Colour(Colours.soft_red), +            title="Member shadow warned", +            thumbnail=user.avatar_url_as(static_format="png"), +            text=textwrap.dedent(f""" +                Member: {user.mention} (`{user.id}`) +                Actor: {ctx.message.author} +                Reason: {reason} +            """) +        ) +      @with_role(*MODERATION_ROLES)      @command(name="shadow_kick", hidden=True, aliases=['shadowkick', 'skick'])      async def shadow_kick(self, ctx: Context, user: Member, *, reason: str = None): @@ -603,7 +653,18 @@ class Moderation(Scheduler):              if infraction_object["expires_at"] is not None:                  self.cancel_expiration(infraction_object["id"]) -            await ctx.send(f":ok_hand: Un-muted {user.mention}.") +            notified = await self.notify_pardon( +                user=user, +                title="You have been unmuted.", +                content="You may now send messages in the server.", +                icon_url=Icons.user_unmute +            ) + +            dm_result = ":incoming_envelope: " if notified else "" +            await ctx.send(f"{dm_result}:ok_hand: Un-muted {user.mention}.") + +            if not notified: +                await self.log_notify_failure(user, ctx.author, "unmute")              # Send a log message to the mod log              await self.mod_log.send_log_message( @@ -617,13 +678,6 @@ class Moderation(Scheduler):                      Intended expiry: {infraction_object['expires_at']}                  """)              ) - -            await self.notify_pardon( -                user=user, -                title="You have been unmuted.", -                content="You may now send messages in the server.", -                icon_url=Icons.user_unmute -            )          except Exception:              log.exception("There was an error removing an infraction.")              await ctx.send(":x: There was an error removing the infraction.") @@ -1093,7 +1147,7 @@ class Moderation(Scheduler):          embed.title = f"Please review our rules over at {RULES_URL}"          embed.url = RULES_URL -        await self.send_private_embed(user, embed) +        return await self.send_private_embed(user, embed)      async def notify_pardon(              self, user: Union[User, Member], title: str, content: str, icon_url: str = Icons.user_verified @@ -1114,7 +1168,7 @@ class Moderation(Scheduler):          embed.set_author(name=title, icon_url=icon_url) -        await self.send_private_embed(user, embed) +        return await self.send_private_embed(user, embed)      async def send_private_embed(self, user: Union[User, Member], embed: Embed):          """ @@ -1129,11 +1183,22 @@ class Moderation(Scheduler):          try:              await user.send(embed=embed) +            return True          except (HTTPException, Forbidden):              log.debug(                  f"Infraction-related information could not be sent to user {user} ({user.id}). "                  "They've probably just disabled private messages."              ) +            return False + +    async def log_notify_failure(self, target: str, actor: Member, infraction_type: str): +        await self.mod_log.send_log_message( +            icon_url=Icons.token_removed, +            content=actor.mention, +            colour=Colour(Colours.soft_red), +            title="Notification Failed", +            text=f"Direct message was unable to be sent.\nUser: {target.mention}\nType: {infraction_type}" +        )      # endregion diff --git a/bot/cogs/modlog.py b/bot/cogs/modlog.py index 1d1546d5b..905f114c1 100644 --- a/bot/cogs/modlog.py +++ b/bot/cogs/modlog.py @@ -104,8 +104,9 @@ class ModLog:                  self._ignored[event].append(item)      async def send_log_message( -            self, icon_url: Optional[str], colour: Colour, title: Optional[str], text: str, thumbnail: str = None, -            channel_id: int = Channels.modlog, ping_everyone: bool = False, files: List[File] = None +            self, icon_url: Optional[str], colour: Colour, title: Optional[str], text: str, +            thumbnail: str = None, channel_id: int = Channels.modlog, ping_everyone: bool = False, +            files: List[File] = None, content: str = None      ):          embed = Embed(description=text) @@ -118,10 +119,11 @@ class ModLog:          if thumbnail is not None:              embed.set_thumbnail(url=thumbnail) -        content = None -          if ping_everyone: -            content = "@everyone" +            if content: +                content = f"@everyone\n{content}" +            else: +                content = "@everyone"          await self.bot.get_channel(channel_id).send(content=content, embed=embed, files=files) diff --git a/bot/cogs/snekbox.py b/bot/cogs/snekbox.py index 1b51da843..cb0454249 100644 --- a/bot/cogs/snekbox.py +++ b/bot/cogs/snekbox.py @@ -6,12 +6,12 @@ import textwrap  from discord import Colour, Embed  from discord.ext.commands import ( -    Bot, CommandError, Context, MissingPermissions, -    NoPrivateMessage, check, command, guild_only +    Bot, CommandError, Context, NoPrivateMessage, command, guild_only  )  from bot.cogs.rmq import RMQ  from bot.constants import Channels, ERROR_REPLIES, NEGATIVE_REPLIES, Roles, URLs +from bot.decorators import InChannelCheckFailure, in_channel  from bot.utils.messages import wait_for_deletion @@ -51,22 +51,8 @@ RAW_CODE_REGEX = re.compile(      r"\s*$",                                # any trailing whitespace until the end of the string      re.DOTALL                               # "." also matches newlines  ) -BYPASS_ROLES = (Roles.owner, Roles.admin, Roles.moderator, Roles.helpers) -WHITELISTED_CHANNELS = (Channels.bot,) -WHITELISTED_CHANNELS_STRING = ', '.join(f"<#{channel_id}>" for channel_id in WHITELISTED_CHANNELS) - - -async def channel_is_whitelisted_or_author_can_bypass(ctx: Context): -    """ -    Checks that the author is either helper or above -    or the channel is a whitelisted channel. -    """ -    if ctx.channel.id in WHITELISTED_CHANNELS: -        return True -    if any(r.id in BYPASS_ROLES for r in ctx.author.roles): -        return True -    raise MissingPermissions("You are not allowed to do that here.") +BYPASS_ROLES = (Roles.owner, Roles.admin, Roles.moderator, Roles.helpers)  class Snekbox: @@ -84,7 +70,7 @@ class Snekbox:      @command(name='eval', aliases=('e',))      @guild_only() -    @check(channel_is_whitelisted_or_author_can_bypass) +    @in_channel(Channels.bot, bypass_roles=BYPASS_ROLES)      async def eval_command(self, ctx: Context, *, code: str = None):          """          Run some code. get the result back. We've done our best to make this safe, but do let us know if you @@ -205,9 +191,9 @@ class Snekbox:              embed.description = "You're not allowed to use this command in private messages."              await ctx.send(embed=embed) -        elif isinstance(error, MissingPermissions): +        elif isinstance(error, InChannelCheckFailure):              embed.title = random.choice(NEGATIVE_REPLIES) -            embed.description = f"Sorry, but you may only use this command within {WHITELISTED_CHANNELS_STRING}." +            embed.description = str(error)              await ctx.send(embed=embed)          else: diff --git a/bot/cogs/utils.py b/bot/cogs/utils.py index b101b8816..65c729414 100644 --- a/bot/cogs/utils.py +++ b/bot/cogs/utils.py @@ -1,16 +1,20 @@  import logging +import random +import re +import unicodedata  from email.parser import HeaderParser  from io import StringIO -  from discord import Colour, Embed  from discord.ext.commands import AutoShardedBot, Context, command -from bot.constants import Roles -from bot.decorators import with_role +from bot.constants import Channels, NEGATIVE_REPLIES, Roles +from bot.decorators import InChannelCheckFailure, in_channel  log = logging.getLogger(__name__) +BYPASS_ROLES = (Roles.owner, Roles.admin, Roles.moderator, Roles.helpers) +  class Utils:      """ @@ -24,7 +28,6 @@ class Utils:          self.base_github_pep_url = "https://raw.githubusercontent.com/python/peps/master/pep-"      @command(name='pep', aliases=('get_pep', 'p')) -    @with_role(Roles.verified)      async def pep_command(self, ctx: Context, pep_number: str):          """          Fetches information about a PEP and sends it to the channel. @@ -87,6 +90,58 @@ class Utils:          await ctx.message.channel.send(embed=pep_embed) +    @command() +    @in_channel(Channels.bot, bypass_roles=BYPASS_ROLES) +    async def charinfo(self, ctx, *, characters: str): +        """ +        Shows you information on up to 25 unicode characters. +        """ + +        match = re.match(r"<(a?):(\w+):(\d+)>", characters) +        if match: +            embed = Embed( +                title="Non-Character Detected", +                description=( +                    "Only unicode characters can be processed, but a custom Discord emoji " +                    "was found. Please remove it and try again." +                ) +            ) +            embed.colour = Colour.red() +            return await ctx.send(embed=embed) + +        if len(characters) > 25: +            embed = Embed(title=f"Too many characters ({len(characters)}/25)") +            embed.colour = Colour.red() +            return await ctx.send(embed=embed) + +        def get_info(char): +            digit = f"{ord(char):x}" +            if len(digit) <= 4: +                u_code = f"\\u{digit:>04}" +            else: +                u_code = f"\\U{digit:>08}" +            url = f"https://www.compart.com/en/unicode/U+{digit:>04}" +            name = f"[{unicodedata.name(char, '')}]({url})" +            info = f"`{u_code.ljust(10)}`: {name} - {char}" +            return info, u_code + +        charlist, rawlist = zip(*(get_info(c) for c in characters)) + +        embed = Embed(description="\n".join(charlist)) +        embed.set_author(name="Character Info") + +        if len(characters) > 1: +            embed.add_field(name='Raw', value=f"`{''.join(rawlist)}`", inline=False) + +        await ctx.send(embed=embed) + +    async def __error(self, ctx, error): +        embed = Embed(colour=Colour.red()) +        if isinstance(error, InChannelCheckFailure): +            embed.title = random.choice(NEGATIVE_REPLIES) +            embed.description = str(error) +            await ctx.send(embed=embed) +  def setup(bot):      bot.add_cog(Utils(bot)) diff --git a/bot/constants.py b/bot/constants.py index b4eca7e1d..05d2abf81 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -292,6 +292,8 @@ class Icons(metaclass=YAMLGetter):      user_unmute: str      user_verified: str +    user_warn: str +      pencil: str      remind_blurple: str diff --git a/bot/decorators.py b/bot/decorators.py index fe974cbd3..87877ecbf 100644 --- a/bot/decorators.py +++ b/bot/decorators.py @@ -1,18 +1,51 @@  import logging  import random +import typing  from asyncio import Lock  from functools import wraps  from weakref import WeakValueDictionary  from discord import Colour, Embed  from discord.ext import commands -from discord.ext.commands import Context +from discord.ext.commands import CheckFailure, Context  from bot.constants import ERROR_REPLIES  log = logging.getLogger(__name__) +class InChannelCheckFailure(CheckFailure): +    pass + + +def in_channel(*channels: int, bypass_roles: typing.Container[int] = None): +    """ +    Checks that the message is in a whitelisted channel or optionally has a bypass role. +    """ +    def predicate(ctx: Context): +        if ctx.channel.id in channels: +            log.debug(f"{ctx.author} tried to call the '{ctx.command.name}' command. " +                      f"The command was used in a whitelisted channel.") +            return True + +        if bypass_roles: +            if any(r.id in bypass_roles for r in ctx.author.roles): +                log.debug(f"{ctx.author} tried to call the '{ctx.command.name}' command. " +                          f"The command was not used in a whitelisted channel, " +                          f"but the author had a role to bypass the in_channel check.") +                return True + +        log.debug(f"{ctx.author} tried to call the '{ctx.command.name}' command. " +                  f"The in_channel check failed.") + +        channels_str = ', '.join(f"<#{c_id}>" for c_id in channels) +        raise InChannelCheckFailure( +            f"Sorry, but you may only use this command within {channels_str}." +        ) + +    return commands.check(predicate) + +  def with_role(*role_ids: int):      async def predicate(ctx: Context):          if not ctx.guild:  # Return False in a DM @@ -46,15 +79,6 @@ def without_role(*role_ids: int):      return commands.check(predicate) -def in_channel(channel_id): -    async def predicate(ctx: Context): -        check = ctx.channel.id == channel_id -        log.debug(f"{ctx.author} tried to call the '{ctx.command.name}' command. " -                  f"The result of the in_channel check was {check}.") -        return check -    return commands.check(predicate) - -  def locked():      """      Allows the user to only run one instance of the decorated command at a time. diff --git a/config-default.yml b/config-default.yml index 6d301048f..7a5960987 100644 --- a/config-default.yml +++ b/config-default.yml @@ -72,6 +72,8 @@ style:          user_unmute:   "https://cdn.discordapp.com/emojis/472472639206719508.png"          user_verified: "https://cdn.discordapp.com/emojis/470326274519334936.png" +        user_warn: "https://cdn.discordapp.com/emojis/470326274238447633.png" +          pencil: "https://cdn.discordapp.com/emojis/470326272401211415.png"          remind_blurple: "https://cdn.discordapp.com/emojis/477907609215827968.png" | 
