diff options
| author | 2020-02-05 11:46:16 -0500 | |
|---|---|---|
| committer | 2020-02-05 11:46:16 -0500 | |
| commit | 3e8f41c120a2f8bb1df8b2ea3f59c1a6d655a1f4 (patch) | |
| tree | 8b96cc2b0b140e60d54ad1a00ea7af619b2f2891 | |
| parent | Merge branch 'master' into remove-prometheus (diff) | |
| parent | Merge pull request #739 from python-discord/resources-move (diff) | |
Merge branch 'master' into remove-prometheus
Diffstat (limited to '')
| -rw-r--r-- | bot/cogs/antispam.py | 33 | ||||
| -rw-r--r-- | bot/cogs/moderation/modlog.py | 22 | ||||
| -rw-r--r-- | bot/cogs/site.py | 4 | ||||
| -rw-r--r-- | bot/constants.py | 1 | ||||
| -rw-r--r-- | bot/utils/messages.py | 49 | ||||
| -rw-r--r-- | config-default.yml | 3 | 
6 files changed, 71 insertions, 41 deletions
| diff --git a/bot/cogs/antispam.py b/bot/cogs/antispam.py index f454061a6..f67ef6f05 100644 --- a/bot/cogs/antispam.py +++ b/bot/cogs/antispam.py @@ -19,6 +19,7 @@ from bot.constants import (      STAFF_ROLES,  )  from bot.converters import Duration +from bot.utils.messages import send_attachments  log = logging.getLogger(__name__) @@ -45,8 +46,9 @@ class DeletionContext:      members: Dict[int, Member] = field(default_factory=dict)      rules: Set[str] = field(default_factory=set)      messages: Dict[int, Message] = field(default_factory=dict) +    attachments: List[List[str]] = field(default_factory=list) -    def add(self, rule_name: str, members: Iterable[Member], messages: Iterable[Message]) -> None: +    async def add(self, rule_name: str, members: Iterable[Member], messages: Iterable[Message]) -> None:          """Adds new rule violation events to the deletion context."""          self.rules.add(rule_name) @@ -58,6 +60,11 @@ class DeletionContext:              if message.id not in self.messages:                  self.messages[message.id] = message +                # Re-upload attachments +                destination = message.guild.get_channel(Channels.attachment_log) +                urls = await send_attachments(message, destination, link_large=False) +                self.attachments.append(urls) +      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()) @@ -70,7 +77,7 @@ class DeletionContext:          # For multiple messages or those with excessive newlines, use the logs API          if len(self.messages) > 1 or 'newlines' in self.rules: -            url = await modlog.upload_log(self.messages.values(), actor_id) +            url = await modlog.upload_log(self.messages.values(), actor_id, self.attachments)              mod_alert_message += f"A complete log of the offending messages can be found [here]({url})"          else:              mod_alert_message += "Message:\n" @@ -98,7 +105,7 @@ class DeletionContext:  class AntiSpam(Cog):      """Cog that controls our anti-spam measures.""" -    def __init__(self, bot: Bot, validation_errors: bool) -> None: +    def __init__(self, bot: Bot, validation_errors: Dict[str, str]) -> None:          self.bot = bot          self.validation_errors = validation_errors          role_id = AntiSpamConfig.punishment['role_id'] @@ -106,7 +113,6 @@ class AntiSpam(Cog):          self.expiration_date_converter = Duration()          self.message_deletion_queue = dict() -        self.queue_consumption_tasks = dict()          self.bot.loop.create_task(self.alert_on_validation_error()) @@ -180,15 +186,14 @@ class AntiSpam(Cog):                  full_reason = f"`{rule_name}` rule: {reason}"                  # If there's no spam event going on for this channel, start a new Message Deletion Context -                if message.channel.id not in self.message_deletion_queue: -                    log.trace(f"Creating queue for channel `{message.channel.id}`") -                    self.message_deletion_queue[message.channel.id] = DeletionContext(channel=message.channel) -                    self.queue_consumption_tasks = self.bot.loop.create_task( -                        self._process_deletion_context(message.channel.id) -                    ) +                channel = message.channel +                if channel.id not in self.message_deletion_queue: +                    log.trace(f"Creating queue for channel `{channel.id}`") +                    self.message_deletion_queue[message.channel.id] = DeletionContext(channel) +                    self.bot.loop.create_task(self._process_deletion_context(message.channel.id))                  # Add the relevant of this trigger to the Deletion Context -                self.message_deletion_queue[message.channel.id].add( +                await self.message_deletion_queue[message.channel.id].add(                      rule_name=rule_name,                      members=members,                      messages=relevant_messages @@ -202,7 +207,7 @@ class AntiSpam(Cog):                          self.punish(message, member, full_reason)                      ) -                await self.maybe_delete_messages(message.channel, relevant_messages) +                await self.maybe_delete_messages(channel, relevant_messages)                  break      async def punish(self, msg: Message, member: Member, reason: str) -> None: @@ -255,10 +260,10 @@ class AntiSpam(Cog):          await deletion_context.upload_messages(self.bot.user.id, self.mod_log) -def validate_config(rules: Mapping = AntiSpamConfig.rules) -> Dict[str, str]: +def validate_config(rules_: Mapping = AntiSpamConfig.rules) -> Dict[str, str]:      """Validates the antispam configs."""      validation_errors = {} -    for name, config in rules.items(): +    for name, config in rules_.items():          if name not in RULE_FUNCTION_MAPPING:              log.error(                  f"Unrecognized antispam rule `{name}`. " diff --git a/bot/cogs/moderation/modlog.py b/bot/cogs/moderation/modlog.py index c78eb24a7..e8ae0dbe6 100644 --- a/bot/cogs/moderation/modlog.py +++ b/bot/cogs/moderation/modlog.py @@ -4,6 +4,7 @@ import itertools  import logging  import typing as t  from datetime import datetime +from itertools import zip_longest  import discord  from dateutil.relativedelta import relativedelta @@ -42,14 +43,16 @@ class ModLog(Cog, name="ModLog"):          self._cached_deletes = []          self._cached_edits = [] -    async def upload_log(self, messages: t.List[discord.Message], actor_id: int) -> str: -        """ -        Uploads the log data to the database via an API endpoint for uploading logs. - -        Used in several mod log embeds. +    async def upload_log( +        self, +        messages: t.Iterable[discord.Message], +        actor_id: int, +        attachments: t.Iterable[t.List[str]] = None +    ) -> str: +        """Upload message logs to the database and return a URL to a page for viewing the logs.""" +        if attachments is None: +            attachments = [] -        Returns a URL that can be used to view the log. -        """          response = await self.bot.api_client.post(              'bot/deleted-messages',              json={ @@ -61,9 +64,10 @@ class ModLog(Cog, name="ModLog"):                          'author': message.author.id,                          'channel_id': message.channel.id,                          'content': message.content, -                        'embeds': [embed.to_dict() for embed in message.embeds] +                        'embeds': [embed.to_dict() for embed in message.embeds], +                        'attachments': attachment,                      } -                    for message in messages +                    for message, attachment in zip_longest(messages, attachments)                  ]              }          ) diff --git a/bot/cogs/site.py b/bot/cogs/site.py index 2ea8c7a2e..853e29568 100644 --- a/bot/cogs/site.py +++ b/bot/cogs/site.py @@ -59,7 +59,7 @@ class Site(Cog):      @site_group.command(name="tools")      async def site_tools(self, ctx: Context) -> None:          """Info about the site's Tools page.""" -        tools_url = f"{PAGES_URL}/tools" +        tools_url = f"{PAGES_URL}/resources/tools"          embed = Embed(title="Tools")          embed.set_footer(text=f"{tools_url}") @@ -74,7 +74,7 @@ class Site(Cog):      @site_group.command(name="help")      async def site_help(self, ctx: Context) -> None:          """Info about the site's Getting Help page.""" -        url = f"{PAGES_URL}/asking-good-questions" +        url = f"{PAGES_URL}/resources/guides/asking-good-questions"          embed = Embed(title="Asking Good Questions")          embed.set_footer(text=url) diff --git a/bot/constants.py b/bot/constants.py index 25c7856ba..629985bdf 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -359,6 +359,7 @@ class Channels(metaclass=YAMLGetter):      admins: int      admin_spam: int      announcements: int +    attachment_log: int      big_brother_logs: int      bot: int      checkpoint_test: int diff --git a/bot/utils/messages.py b/bot/utils/messages.py index 549b33ca6..c4e2753e0 100644 --- a/bot/utils/messages.py +++ b/bot/utils/messages.py @@ -1,7 +1,8 @@  import asyncio  import contextlib +import logging  from io import BytesIO -from typing import Optional, Sequence, Union +from typing import List, Optional, Sequence, Union  from discord import Client, Embed, File, Member, Message, Reaction, TextChannel, Webhook  from discord.abc import Snowflake @@ -9,7 +10,7 @@ from discord.errors import HTTPException  from bot.constants import Emojis -MAX_SIZE = 1024 * 1024 * 8  # 8 Mebibytes +log = logging.getLogger(__name__)  async def wait_for_deletion( @@ -51,42 +52,58 @@ async def wait_for_deletion(          await message.delete() -async def send_attachments(message: Message, destination: Union[TextChannel, Webhook]) -> None: +async def send_attachments( +    message: Message, +    destination: Union[TextChannel, Webhook], +    link_large: bool = True +) -> List[str]:      """ -    Re-uploads each attachment in a message to the given channel or webhook. +    Re-upload the message's attachments to the destination and return a list of their new URLs. -    Each attachment is sent as a separate message to more easily comply with the 8 MiB request size limit. -    If attachments are too large, they are instead grouped into a single embed which links to them. +    Each attachment is sent as a separate message to more easily comply with the request/file size +    limit. If link_large is True, attachments which are too large are instead grouped into a single +    embed which links to them.      """      large = [] +    urls = []      for attachment in message.attachments: +        failure_msg = ( +            f"Failed to re-upload attachment {attachment.filename} from message {message.id}" +        ) +          try: -            # This should avoid most files that are too large, but some may get through hence the try-catch.              # Allow 512 bytes of leeway for the rest of the request. -            if attachment.size <= MAX_SIZE - 512: +            # This should avoid most files that are too large, +            # but some may get through hence the try-catch. +            if attachment.size <= destination.guild.filesize_limit - 512:                  with BytesIO() as file: -                    await attachment.save(file) +                    await attachment.save(file, use_cached=True)                      attachment_file = File(file, filename=attachment.filename)                      if isinstance(destination, TextChannel): -                        await destination.send(file=attachment_file) +                        msg = await destination.send(file=attachment_file) +                        urls.append(msg.attachments[0].url)                      else:                          await destination.send(                              file=attachment_file,                              username=message.author.display_name,                              avatar_url=message.author.avatar_url                          ) -            else: +            elif link_large:                  large.append(attachment) +            else: +                log.warning(f"{failure_msg} because it's too large.")          except HTTPException as e: -            if e.status == 413: +            if link_large and e.status == 413:                  large.append(attachment)              else: -                raise +                log.warning(f"{failure_msg} with status {e.status}.") -    if large: -        embed = Embed(description=f"\n".join(f"[{attachment.filename}]({attachment.url})" for attachment in large)) +    if link_large and large: +        desc = f"\n".join(f"[{attachment.filename}]({attachment.url})" for attachment in large) +        embed = Embed(description=desc)          embed.set_footer(text="Attachments exceed upload size limit.") +          if isinstance(destination, TextChannel):              await destination.send(embed=embed)          else: @@ -95,3 +112,5 @@ async def send_attachments(message: Message, destination: Union[TextChannel, Web                  username=message.author.display_name,                  avatar_url=message.author.avatar_url              ) + +    return urls diff --git a/config-default.yml b/config-default.yml index 1a8aaedae..c113d3330 100644 --- a/config-default.yml +++ b/config-default.yml @@ -115,6 +115,7 @@ guild:          admin_spam:        &ADMIN_SPAM    563594791770914816          admins_voice:      &ADMINS_VOICE  500734494840717332          announcements:                    354619224620138496 +        attachment_log:    &ATTCH_LOG     649243850006855680          big_brother_logs:  &BBLOGS        468507907357409333          bot:                              267659945086812160          checkpoint_test:                  422077681434099723 @@ -152,7 +153,7 @@ guild:          voice_log:                        640292421988646961      staff_channels: [*ADMINS, *ADMIN_SPAM, *MOD_SPAM, *MODS, *HELPERS, *ORGANISATION, *DEFCON] -    ignored: [*ADMINS, *MESSAGE_LOG, *MODLOG, *ADMINS_VOICE, *STAFF_VOICE] +    ignored: [*ADMINS, *MESSAGE_LOG, *MODLOG, *ADMINS_VOICE, *STAFF_VOICE, *ATTCH_LOG]      roles:          admin:             &ADMIN_ROLE      267628507062992896 | 
