feat: adding attachments to snippets. - #3421
Conversation
This adds the ability to add attachments to snippets
|
I have began a review which will result in a "changes requested" verdict. I will need additional time to complete a full review. I hope to have it completed by sometime around 12PM tomorrow, Eastern. |
There was a problem hiding this comment.
Pull request overview
This PR adds support for storing, managing, and sending file attachments associated with snippets, including persisting snippet attachments in MongoDB GridFS and relaying them through the existing thread send pipeline.
Changes:
- Persist snippet attachments in MongoDB GridFS with upload/download/delete APIs and a configurable max attachment size.
- Extend snippet commands (
snippet add/edit/remove) to accept/manage an optional attachment and display attachment presence in snippet views. - Attach downloaded snippet files to outgoing thread messages (including embedding snippet images via
attachment://).
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| core/thread.py | Adds files_to_upload pipeline and special handling for embedded snippet images when sending thread messages. |
| core/config.py | Introduces snippet_attachment_max_size config key and conversion handling for MB-based values. |
| core/config_help.json | Documents the new snippet_attachment_max_size config option. |
| core/clients.py | Adds GridFS bucket + upload/download/delete methods for snippet attachments. |
| cogs/modmail.py | Updates snippet CRUD commands to support attachments (validation, confirmation, GridFS persistence). |
| bot.py | Downloads snippet attachments at invocation time and wraps them to flow through existing attachment sending logic. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Suppressed comments (4)
core/thread.py:2020
extincludes forwarded attachments (added just above), but the loop only iterates overmessage.attachments, so forwarded attachments appended toextare never classified intoimages/attachmentsand won't be included in the outgoing embed/log.
for i, a in enumerate(message.attachments):
attachment = ext[i]
if getattr(a, "is_snippet_attachment", False):
bot.py:1413
- This replaces the original message attachments with only the snippet attachment. If a user invokes a snippet while also attaching files, those files will be silently dropped.
if attachment is not None:
snippet_message = copy.copy(message)
snippet_message.attachments = [attachment]
ctx.message = snippet_message
bot.py:1386
- This overwrites any attachments present on the original alias-invoking message when a snippet attachment exists, so user-provided attachments would be dropped instead of being sent along with the snippet attachment.
This issue also appears on line 1410 of the same file.
attachment = await self._download_snippet_attachment(snippet_data)
if attachment is not None:
context_message.attachments = [attachment]
else:
cogs/modmail.py:363
- The help text hard-codes a 10 MB limit, but the actual limit is configurable via
snippet_attachment_max_size(default 10). This can mislead users if the config is changed.
You can also attach a file (max 10 MB) to include with the snippet.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Suppressed comments (2)
cogs/modmail.py:399
confirm_msgis only defined inside theif ctx.message.attachments:block, but it’s referenced later unconditionally (if confirm_msg:). When adding a text-only snippet (no attachment), this will raise an UnboundLocalError.
# Handle optional attachment
file_id = None
attachment_info = None
if ctx.message.attachments:
cogs/modmail.py:363
- The docstring hard-codes “max 10 MB”, but the actual limit is configurable via
snippet_attachment_max_size(default 10). This will become inaccurate if the config is changed.
You can also attach a file (max 10 MB) to include with the snippet.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.
Suppressed comments (2)
cogs/modmail.py:399
confirm_msgis only defined inside theif ctx.message.attachments:block, but it is referenced later (if confirm_msg:). When creating a snippet without an attachment, this will raiseUnboundLocalErrorat runtime.
# Handle optional attachment
file_id = None
attachment_info = None
if ctx.message.attachments:
cogs/modmail.py:363
- The help text hard-codes a 10 MB maximum, but the actual limit is configurable via
snippet_attachment_max_size(default 10). This makes the command help inaccurate when the config is changed.
You can also attach a file (max 10 MB) to include with the snippet.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.
Suppressed comments (4)
bot.py:1385
- This overwrites any attachments that were present on the original message when an alias expands to a snippet with an attachment. Since downstream sending code is already written to handle mixes of snippet and non-snippet attachments, consider appending the snippet attachment instead of replacing the list.
context_message.attachments = [attachment]
bot.py:1412
- This overwrites any attachments on the original message when invoking a snippet that has an attachment, which can unexpectedly drop user-provided files. Consider appending the snippet attachment to the existing attachments list instead.
snippet_message.attachments = [attachment]
bot.py:1393
command_invocation_textalready gets prefixed withinvoked_prefixwhen constructingStringView(invoked_prefix + command_invocation_text). Includinginvoked_prefixinsidecommand_invocation_textduplicates the prefix (e.g.!!!!freply ...) and can prevent the alias-expanded context from parsing the intended command.
command_invocation_text = f"{invoked_prefix}{command} {snippet_text}"
cogs/modmail.py:363
- The docstring hard-codes a 10 MB maximum, but the actual limit is configurable via
snippet_attachment_max_size(and may not be 10). Update the wording so it stays accurate when the config changes.
You can also attach a file (max 10 MB) to include with the snippet.
StephenDaDev
left a comment
There was a problem hiding this comment.
tested working, no serious issues found
sebkuip
left a comment
There was a problem hiding this comment.
Review for now. Did not check all, and is purely a code review. Have to do some testing later myself and look further into the code.
| snippet_text = self._get_snippet_text(snippet_data) | ||
| has_attachment = self._has_snippet_attachment(snippet_data) | ||
|
|
||
| description = snippet_text if snippet_text else "(No text content)" |
There was a problem hiding this comment.
If a snippet is only an attachment, having it show up as (No text content) This snippet has an attachment seems weird. I would put it as something like this is an attachment-only snippet
|
|
||
| display_value = return_or_truncate(snippet_text, 350) if snippet_text else "(No text)" | ||
| if has_attachment: | ||
| display_value = "📎 " + display_value |
There was a problem hiding this comment.
Maybe make the embed have a footer text explaining that the paperclip emoji means the embed has an attachment?
|
|
||
| description = snippet_text if snippet_text else "(No text content)" | ||
| if has_attachment: | ||
| description += "\n\n📎 *This snippet has an attachment.*" |
There was a problem hiding this comment.
I'd say show the attachment when you request the snippet. In the list it's fine to leave out but if you just pull up that snippet it should show.
| val = truncate(escape_code_block(snippet_text), 2048 - 7) if snippet_text else "(No text content)" | ||
| description = f"```\n{val}```" | ||
| if has_attachment: | ||
| description += "\n\n📎 *This snippet has an attachment.*" |
There was a problem hiding this comment.
Similar to the regular version of this command, show the attachment so the staff member can see what it sends.
| file_id = None | ||
| attachment_info = None | ||
| if ctx.message.attachments: | ||
| attachment = ctx.message.attachments[0] |
There was a problem hiding this comment.
Don't silently ignore multiple attachments. Either allow it, or give the user an error that only a single attachment is allowed.
| return await ctx.send(embed=embed) | ||
|
|
||
| # Require at least text or attachment | ||
| if not value and not file_id: |
There was a problem hiding this comment.
This if statement acts as a guard, and should be at the top of the method. Probably change it to if not value and len(message.attachments) == 0:
| self.bot.snippets.pop(name) | ||
| await self.bot.config.update() | ||
| if file_id and not await self.bot.api.delete_snippet_attachment(file_id): | ||
| logger.warning("Failed to delete snippet attachment for %s.", name) |
There was a problem hiding this comment.
This should either inform the user in chat, or fail the command entirely so you don't end up with tons of data on gridFS when it errors a few times.
| ``` | ||
|
|
||
| Attach a new file to replace the existing attachment. | ||
| Provide text without attachment to keep the existing attachment. |
There was a problem hiding this comment.
I feel like an edit should be able to remove the attachment as well. Isn't it better that providing no attachment deletes it, instead of having the user needing to delete the snippet if they want the attachment gone?
|
|
||
| def process_help_msg(self, help_: str): | ||
| return help_.format(prefix=self.context.clean_prefix) if help_ else "No help message." | ||
| return help_.replace("{prefix}", self.context.clean_prefix) if help_ else "No help message." |
There was a problem hiding this comment.
Why is this randomly edited? This is out of scope for the PR
| elif key in self.megabytes: | ||
| if not isinstance(value, int): | ||
| try: | ||
| value = int(value) |
There was a problem hiding this comment.
Might also want to guard against negative values. Also this causes rounding when a comma value is entered, is that intentional?


This adds the ability to add attachments to snippets.