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/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": [ { diff --git a/core/thread.py b/core/thread.py index 09263b197d..7d0c9ed651 100644 --- a/core/thread.py +++ b/core/thread.py @@ -35,6 +35,7 @@ ConfirmThreadCreationView, DummyParam, extract_forwarded_content, + extract_forwarded_attachments, ) logger = getLogger(__name__) @@ -205,7 +206,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(), @@ -1948,6 +1952,11 @@ async def send( 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 = [] for attachment in ext: diff --git a/core/utils.py b/core/utils.py index dc8737ead6..5079917740 100644 --- a/core/utils.py +++ b/core/utils.py @@ -46,6 +46,7 @@ "ConfirmThreadCreationView", "DummyParam", "extract_forwarded_content", + "extract_forwarded_attachments", ] @@ -640,6 +641,37 @@ 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. @@ -658,39 +690,51 @@ 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_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_links.append(f"(+{len(snap.attachments) - 3} more)") + formatted_part += f"📎 {', '.join(attachment_links)}\n" + 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): @@ -719,12 +763,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)