From 88f00f7d33cbc8b5049aa9dc6e34b9ebad7b37b6 Mon Sep 17 00:00:00 2001 From: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> Date: Fri, 5 Dec 2025 21:06:43 +0100 Subject: [PATCH 1/7] fix: show forwarded message in logviewer. Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> --- core/clients.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/core/clients.py b/core/clients.py index 90f09b3b48..3826f55001 100644 --- a/core/clients.py +++ b/core/clients.py @@ -661,9 +661,18 @@ async def append_log( channel_id: str = "", type_: str = "thread_message", ) -> dict: + from core.utils import extract_forwarded_content + channel_id = str(channel_id) or str(message.channel.id) message_id = str(message_id) or str(message.id) + content = message.content or "" + if forwarded := extract_forwarded_content(message): + if content: + content += "\n" + forwarded + else: + content = forwarded + data = { "timestamp": str(message.created_at), "message_id": message_id, @@ -674,7 +683,7 @@ async def append_log( "avatar_url": message.author.display_avatar.url if message.author.display_avatar else None, "mod": not isinstance(message.channel, DMChannel), }, - "content": message.content, + "content": content, "type": type_, "attachments": [ { From 09069ff1bbc3f51ac0cb48efdd2274c1048377b0 Mon Sep 17 00:00:00 2001 From: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> Date: Fri, 5 Dec 2025 21:07:04 +0100 Subject: [PATCH 2/7] fix: show forwarded message in logviewer. Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> --- core/thread.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/core/thread.py b/core/thread.py index bf77180f8c..2c065bf8d7 100644 --- a/core/thread.py +++ b/core/thread.py @@ -205,7 +205,10 @@ async def snooze(self, moderator=None, command_used=None, snooze_for=None): "messages": [ { "author_id": m.author.id, - "content": m.content, + "content": ( + (m.content or "") + + (("\n" + extract_forwarded_content(m)) if extract_forwarded_content(m) else "") + ).strip(), "attachments": [a.url for a in m.attachments], "embeds": [e.to_dict() for e in m.embeds], "created_at": m.created_at.isoformat(), @@ -224,16 +227,12 @@ async def snooze(self, moderator=None, command_used=None, snooze_for=None): "author_name": ( getattr(m.embeds[0].author, "name", "").split(" (")[0] if m.embeds and m.embeds[0].author and m.author == self.bot.user - else getattr(m.author, "name", None) - if m.author != self.bot.user - else None + else getattr(m.author, "name", None) if m.author != self.bot.user else None ), "author_avatar": ( getattr(m.embeds[0].author, "icon_url", None) if m.embeds and m.embeds[0].author and m.author == self.bot.user - else m.author.display_avatar.url - if m.author != self.bot.user - else None + else m.author.display_avatar.url if m.author != self.bot.user else None ), } async for m in channel.history(limit=None, oldest_first=True) @@ -259,6 +258,9 @@ async def snooze(self, moderator=None, command_used=None, snooze_for=None): logging.info(f"[SNOOZE] DB update result: {result.modified_count}") + # Dispatch thread_snoozed event for plugins + self.bot.dispatch("thread_snoozed", self, moderator, snooze_for) + behavior = behavior_pre if behavior == "move": # Move the channel to the snoozed category (if configured) and optionally apply a prefix @@ -751,6 +753,9 @@ async def _ensure_genesis(force: bool = False): # Mark unsnooze as complete self._unsnoozing = False + # Dispatch thread_unsnoozed event for plugins + self.bot.dispatch("thread_unsnoozed", self) + # Process queued commands await self._process_command_queue() From b0e03b05bba84ce74e3bad4a6990ee7b9e4f9b8f Mon Sep 17 00:00:00 2001 From: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> Date: Fri, 5 Dec 2025 21:07:21 +0100 Subject: [PATCH 3/7] fix: show forwarded message in logviewer. Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> --- core/utils.py | 76 +++++++++++++++++++++++++++++---------------------- 1 file changed, 43 insertions(+), 33 deletions(-) diff --git a/core/utils.py b/core/utils.py index dc8737ead6..9d3fc160ac 100644 --- a/core/utils.py +++ b/core/utils.py @@ -658,39 +658,49 @@ def extract_forwarded_content(message) -> typing.Optional[str]: try: # Handle multi-forward (message_snapshots) - if hasattr(message, "flags") and getattr(message.flags, "has_snapshot", False): - if hasattr(message, "message_snapshots") and message.message_snapshots: - forwarded_parts = [] - for snap in message.message_snapshots: - author = getattr(snap, "author", None) - author_name = getattr(author, "name", "Unknown") if author else "Unknown" - snap_content = getattr(snap, "content", "") - - if snap_content: - # Truncate very long messages to prevent spam - if len(snap_content) > 500: - snap_content = snap_content[:497] + "..." - forwarded_parts.append(f"**{author_name}:** {snap_content}") - elif getattr(snap, "embeds", None): - for embed in snap.embeds: - if hasattr(embed, "description") and embed.description: - embed_desc = embed.description - if len(embed_desc) > 300: - embed_desc = embed_desc[:297] + "..." - forwarded_parts.append(f"**{author_name}:** {embed_desc}") - break - elif getattr(snap, "attachments", None): - attachment_info = ", ".join( - [getattr(a, "filename", "Unknown") for a in snap.attachments[:3]] - ) - if len(snap.attachments) > 3: - attachment_info += f" (+{len(snap.attachments) - 3} more)" - forwarded_parts.append(f"**{author_name}:** [Attachments: {attachment_info}]") - else: - forwarded_parts.append(f"**{author_name}:** [No content]") - - if forwarded_parts: - return "\n".join(forwarded_parts) + # Check directly for snapshots as flags.has_snapshot can be unreliable in some versions + if getattr(message, "message_snapshots", None): + forwarded_parts = [] + for snap in message.message_snapshots: + author = getattr(snap, "author", None) + # If author is missing, we can try to rely on the container message context or just omit. + # Since we can't reliably get the original author from snapshot in this state, we focus on content. + + snap_content = getattr(snap, "content", "") + + formatted_part = "📨 **Forwarded Message**\n" + + if snap_content: + if len(snap_content) > 500: + snap_content = snap_content[:497] + "..." + formatted_part += "\n".join([f"{line}" for line in snap_content.splitlines()]) + "\n" + + if getattr(snap, "embeds", None): + for embed in snap.embeds: + if hasattr(embed, "description") and embed.description: + embed_desc = embed.description + if len(embed_desc) > 300: + embed_desc = embed_desc[:297] + "..." + formatted_part += ( + "\n".join([f"> {line}" for line in embed_desc.splitlines()]) + "\n" + ) + break # One embed preview is usually enough + + if getattr(snap, "attachments", None): + attachment_info = ", ".join( + [getattr(a, "filename", "Unknown") for a in snap.attachments[:3]] + ) + if len(snap.attachments) > 3: + attachment_info += f" (+{len(snap.attachments) - 3} more)" + formatted_part += f"[Attachments: {attachment_info}]\n" + + # Add source link to the container message since snapshot doesn't have its own public link + formatted_part += f"\n**Source:** {message.jump_url}" + + forwarded_parts.append(formatted_part) + + if forwarded_parts: + return "\n".join(forwarded_parts) # Handle single-message forward elif getattr(message, "type", None) == getattr(discord.MessageType, "forward", None): From ddab052224bc0fd030fabd776c30b82c666e1945 Mon Sep 17 00:00:00 2001 From: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> Date: Fri, 5 Dec 2025 21:13:03 +0100 Subject: [PATCH 4/7] black formatting Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> --- core/thread.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/core/thread.py b/core/thread.py index 2c065bf8d7..672eefc2b4 100644 --- a/core/thread.py +++ b/core/thread.py @@ -227,12 +227,16 @@ async def snooze(self, moderator=None, command_used=None, snooze_for=None): "author_name": ( getattr(m.embeds[0].author, "name", "").split(" (")[0] if m.embeds and m.embeds[0].author and m.author == self.bot.user - else getattr(m.author, "name", None) if m.author != self.bot.user else None + else getattr(m.author, "name", None) + if m.author != self.bot.user + else None ), "author_avatar": ( getattr(m.embeds[0].author, "icon_url", None) if m.embeds and m.embeds[0].author and m.author == self.bot.user - else m.author.display_avatar.url if m.author != self.bot.user else None + else m.author.display_avatar.url + if m.author != self.bot.user + else None ), } async for m in channel.history(limit=None, oldest_first=True) From 7bb8e677817e48c805510accbf815eccea90e281 Mon Sep 17 00:00:00 2001 From: lorenzo132 Date: Thu, 18 Dec 2025 18:23:43 +0100 Subject: [PATCH 5/7] remove jump_url I believe it is a safer aproach to remove the jumpurl from logviewer, since it shows in the threadchannel already. --- core/utils.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/core/utils.py b/core/utils.py index 9d3fc160ac..d1f1260c22 100644 --- a/core/utils.py +++ b/core/utils.py @@ -693,10 +693,6 @@ def extract_forwarded_content(message) -> typing.Optional[str]: if len(snap.attachments) > 3: attachment_info += f" (+{len(snap.attachments) - 3} more)" formatted_part += f"[Attachments: {attachment_info}]\n" - - # Add source link to the container message since snapshot doesn't have its own public link - formatted_part += f"\n**Source:** {message.jump_url}" - forwarded_parts.append(formatted_part) if forwarded_parts: From b57010154482b4ebd3c5bfb6a0bd1ad29cd694d8 Mon Sep 17 00:00:00 2001 From: lorenzo132 Date: Thu, 18 Dec 2025 20:29:23 +0100 Subject: [PATCH 6/7] feat: adds attachment support to forwarded content. --- bot.py | 2 ++ core/thread.py | 6 +++++ core/utils.py | 63 ++++++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 61 insertions(+), 10 deletions(-) diff --git a/bot.py b/bot.py index 9f3de008a1..bdcfb976e1 100644 --- a/bot.py +++ b/bot.py @@ -1018,6 +1018,8 @@ def __init__(self, original_message, forwarded_content): self.author = original_message.author self.content = forwarded_content self.attachments = [] + for snap in getattr(original_message, "message_snapshots", []): + self.attachments.extend(getattr(snap, "attachments", [])) self.stickers = [] self.created_at = original_message.created_at self.embeds = [] diff --git a/core/thread.py b/core/thread.py index 640d72e266..0889cd4f2f 100644 --- a/core/thread.py +++ b/core/thread.py @@ -35,6 +35,7 @@ ConfirmThreadCreationView, DummyParam, extract_forwarded_content, + extract_forwarded_attachments, ) logger = getLogger(__name__) @@ -1950,6 +1951,11 @@ async def send( embed.color = 0x5865F2 # Discord blurple for system messages ext = [(a.url, a.filename, False) for a in message.attachments] + + # Add forwarded message attachments + forwarded_attachments = extract_forwarded_attachments(message) + for url, filename in forwarded_attachments: + ext.append((url, filename, False)) images = [] attachments = [] diff --git a/core/utils.py b/core/utils.py index d1f1260c22..2b544eefdb 100644 --- a/core/utils.py +++ b/core/utils.py @@ -46,6 +46,7 @@ "ConfirmThreadCreationView", "DummyParam", "extract_forwarded_content", + "extract_forwarded_attachments", ] @@ -640,6 +641,36 @@ def __init__(self): self.value = None +def extract_forwarded_attachments(message) -> typing.List[typing.Tuple[str, str]]: + """ + Extract attachment URLs from forwarded messages. + + Parameters + ---------- + message : discord.Message + The message to extract attachments from. + + Returns + ------- + List[Tuple[str, str]] + List of (url, filename) tuples for attachments. + """ + import discord + attachments = [] + try: + if getattr(message, "message_snapshots", None): + for snap in message.message_snapshots: + if getattr(snap, "attachments", None): + for a in snap.attachments: + url = getattr(a, "url", None) + filename = getattr(a, "filename", "Unknown") + if url: + attachments.append((url.split('?')[0], filename)) + except Exception: + pass + return attachments + + def extract_forwarded_content(message) -> typing.Optional[str]: """ Extract forwarded message content from Discord forwarded messages. @@ -687,12 +718,18 @@ def extract_forwarded_content(message) -> typing.Optional[str]: break # One embed preview is usually enough if getattr(snap, "attachments", None): - attachment_info = ", ".join( - [getattr(a, "filename", "Unknown") for a in snap.attachments[:3]] - ) + attachment_links = [] + for a in snap.attachments[:3]: + filename = getattr(a, "filename", "Unknown") + url = getattr(a, "url", None) + if url: + url = url.split('?')[0] + attachment_links.append(f"[{filename}]({url})") + else: + attachment_links.append(filename) if len(snap.attachments) > 3: - attachment_info += f" (+{len(snap.attachments) - 3} more)" - formatted_part += f"[Attachments: {attachment_info}]\n" + attachment_links.append(f"(+{len(snap.attachments) - 3} more)") + formatted_part += f"📎 {', '.join(attachment_links)}\n" forwarded_parts.append(formatted_part) if forwarded_parts: @@ -725,12 +762,18 @@ def extract_forwarded_content(message) -> typing.Optional[str]: embed_desc = embed_desc[:297] + "..." return f"**{ref_author_name}:** {embed_desc}" elif getattr(ref_msg, "attachments", None): - attachment_info = ", ".join( - [getattr(a, "filename", "Unknown") for a in ref_msg.attachments[:3]] - ) + attachment_links = [] + for a in ref_msg.attachments[:3]: + filename = getattr(a, "filename", "Unknown") + url = getattr(a, "url", None) + if url: + url = url.split('?')[0] + attachment_links.append(f"[{filename}]({url})") + else: + attachment_links.append(filename) if len(ref_msg.attachments) > 3: - attachment_info += f" (+{len(ref_msg.attachments) - 3} more)" - return f"**{ref_author_name}:** [Attachments: {attachment_info}]" + attachment_links.append(f"(+{len(ref_msg.attachments) - 3} more)") + return f"**{ref_author_name}:** 📎 {', '.join(attachment_links)}" except Exception as e: # Log and continue; failing to extract a reference preview shouldn't break flow logger.debug("Failed to extract reference preview: %s", e) From 4cf543a671233424a68b6069cd855e02010e5c95 Mon Sep 17 00:00:00 2001 From: lorenzo132 Date: Thu, 18 Dec 2025 20:31:27 +0100 Subject: [PATCH 7/7] black formatting --- core/thread.py | 2 +- core/utils.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/core/thread.py b/core/thread.py index 0889cd4f2f..7d0c9ed651 100644 --- a/core/thread.py +++ b/core/thread.py @@ -1951,7 +1951,7 @@ async def send( embed.color = 0x5865F2 # Discord blurple for system messages ext = [(a.url, a.filename, False) for a in message.attachments] - + # Add forwarded message attachments forwarded_attachments = extract_forwarded_attachments(message) for url, filename in forwarded_attachments: diff --git a/core/utils.py b/core/utils.py index 2b544eefdb..5079917740 100644 --- a/core/utils.py +++ b/core/utils.py @@ -656,6 +656,7 @@ def extract_forwarded_attachments(message) -> typing.List[typing.Tuple[str, str] List of (url, filename) tuples for attachments. """ import discord + attachments = [] try: if getattr(message, "message_snapshots", None): @@ -665,7 +666,7 @@ def extract_forwarded_attachments(message) -> typing.List[typing.Tuple[str, str] url = getattr(a, "url", None) filename = getattr(a, "filename", "Unknown") if url: - attachments.append((url.split('?')[0], filename)) + attachments.append((url.split("?")[0], filename)) except Exception: pass return attachments @@ -723,7 +724,7 @@ def extract_forwarded_content(message) -> typing.Optional[str]: filename = getattr(a, "filename", "Unknown") url = getattr(a, "url", None) if url: - url = url.split('?')[0] + url = url.split("?")[0] attachment_links.append(f"[{filename}]({url})") else: attachment_links.append(filename) @@ -767,7 +768,7 @@ def extract_forwarded_content(message) -> typing.Optional[str]: filename = getattr(a, "filename", "Unknown") url = getattr(a, "url", None) if url: - url = url.split('?')[0] + url = url.split("?")[0] attachment_links.append(f"[{filename}]({url})") else: attachment_links.append(filename)