diff --git a/src/fluxer/migrate_message.py b/src/fluxer/migrate_message.py index fa1d817..b765c64 100644 --- a/src/fluxer/migrate_message.py +++ b/src/fluxer/migrate_message.py @@ -102,7 +102,9 @@ async def migrate_messages( target_channel_id: str, after_message_id: int | None = None, progress_callback: Callable[[Dict[str, Any]], Awaitable[None]] | None = None, - thread_id: str | None = None + thread_id: str | None = None, + parent_target_id: str | None = None, + thread_name: str | None = None ) -> Dict[str, Any]: """Migrate messages for a specific channel and returns detailed statistics.""" stats = { @@ -128,11 +130,7 @@ async def migrate_messages( # Skip system messages like "pinned a message", etc. - # We treat thread_starter_message (type 21) as our thread marker. - if msg.type == context.discord_reader.MESSAGE_TYPE_THREAD_STARTER: - channel_name = msg.channel.name if msg.channel else "Unknown Thread" - content = f"> <<< THREAD: **{channel_name}** >>>" - elif msg.type not in [context.discord_reader.MESSAGE_TYPE_DEFAULT, context.discord_reader.MESSAGE_TYPE_REPLY]: + if msg.type not in [context.discord_reader.MESSAGE_TYPE_DEFAULT, context.discord_reader.MESSAGE_TYPE_REPLY, context.discord_reader.MESSAGE_TYPE_THREAD_STARTER]: # If we are skipping the parent, we STILL need to check for a thread! if hasattr(msg, 'thread') and msg.thread: thread = msg.thread @@ -146,7 +144,9 @@ async def migrate_messages( context=context, source_channel_id=thread.id, target_channel_id=target_channel_id, - thread_id=str(thread.id) + thread_id=str(thread.id), + parent_target_id=None, + thread_name=thread.name ) stats["messages"] += thread_stats["messages"] stats["attachments"] += thread_stats["attachments"] @@ -211,6 +211,14 @@ async def migrate_messages( else: logger.debug(f"Reply target Discord ID {msg.reference.message_id} not found in current session map.") + # If this is the FIRST thread message and we have a parent_target_id, force it as reply to the starter + if not reply_to_fluxer_id and parent_target_id and stats["messages"] == 0: + reply_to_fluxer_id = parent_target_id + + # Prepend thread marker to the first message of the thread + if thread_name and stats["messages"] == 0: + content = f"> <<< THREAD: **{thread_name}** >>>\n{content}" + avatar_url = str(msg.author.display_avatar.url) if msg.author.display_avatar.url else None if avatar_url and not avatar_url.startswith("http"): avatar_url = None @@ -262,7 +270,9 @@ async def migrate_messages( context=context, source_channel_id=thread.id, target_channel_id=target_channel_id, - thread_id=str(thread.id) + thread_id=str(thread.id), + parent_target_id=fluxer_msg_id, + thread_name=thread.name ) stats["messages"] += thread_stats["messages"] stats["attachments"] += thread_stats["attachments"] diff --git a/src/fluxer/writer.py b/src/fluxer/writer.py index 097a208..3a8cc4c 100644 --- a/src/fluxer/writer.py +++ b/src/fluxer/writer.py @@ -299,7 +299,7 @@ class FluxerWriter: print(err_msg) return None - async def send_marker(self, channel_id: str, content: str, files: list[dict] | None = None) -> Optional[str]: + async def send_marker(self, channel_id: str, content: str, files: list[dict] | None = None, reply_to_message_id: Optional[str] = None) -> Optional[str]: """ Sends a simple marker message (e.g., thread start/end) using the bot directly. """ @@ -308,12 +308,17 @@ class FluxerWriter: fluxer_files = None if files: fluxer_files = [File(io.BytesIO(f["data"]), filename=f["filename"]) for f in files] - + + message_reference = None + if reply_to_message_id: + message_reference = {"message_id": str(reply_to_message_id), "channel_id": str(channel_id)} + try: msg_data = await self.client.send_message( channel_id=channel_id, content=content, - files=fluxer_files + files=fluxer_files, + message_reference=message_reference ) return str(msg_data["id"]) if msg_data else None except Exception as e: diff --git a/src/stoat/migrate_message.py b/src/stoat/migrate_message.py index 2c3c734..746e110 100644 --- a/src/stoat/migrate_message.py +++ b/src/stoat/migrate_message.py @@ -107,7 +107,9 @@ async def migrate_messages( target_channel_id: str, after_message_id: int | None = None, progress_callback: Callable[[Dict[str, Any]], Awaitable[None]] | None = None, - thread_id: str | None = None + thread_id: str | None = None, + parent_target_id: str | None = None, + thread_name: str | None = None ) -> Dict[str, Any]: """Migrate messages for a specific channel using Stoat masquerade for author impersonation.""" stats = { @@ -133,14 +135,8 @@ async def migrate_messages( # Skip system messages like "pinned a message", etc. - # We treat thread_starter_message (type 21) as our thread marker. content = "" # Initialize content - if msg.type == context.discord_reader.MESSAGE_TYPE_THREAD_STARTER: - channel_name = msg.channel.name if msg.channel else "Unknown Thread" - content = f"> <<< THREAD: **{channel_name}** >>>" - # If it's a thread starter and we already processed the thread at the top, - # we might be double-posting. But we want it as a marker. - elif msg.type not in [context.discord_reader.MESSAGE_TYPE_DEFAULT, context.discord_reader.MESSAGE_TYPE_REPLY]: + if msg.type not in [context.discord_reader.MESSAGE_TYPE_DEFAULT, context.discord_reader.MESSAGE_TYPE_REPLY, context.discord_reader.MESSAGE_TYPE_THREAD_STARTER]: # If we are skipping the parent, we STILL need to check for a thread! if hasattr(msg, 'thread') and msg.thread: thread = msg.thread @@ -149,12 +145,16 @@ async def migrate_messages( # Track thread entry stats["threads"] += 1 + pass + # Migrate thread messages recursively thread_stats = await migrate_messages( context=context, source_channel_id=thread.id, target_channel_id=target_channel_id, - thread_id=str(thread.id) + thread_id=str(thread.id), + parent_target_id=None, + thread_name=thread.name ) stats["messages"] += thread_stats["messages"] stats["attachments"] += thread_stats["attachments"] @@ -213,6 +213,14 @@ async def migrate_messages( logger.debug(f"Detected reply to Discord ID {msg.reference.message_id} -> Stoat ID {reply_to_stoat_id}") else: logger.debug(f"Reply target Discord ID {msg.reference.message_id} not found in current session map.") + + # If this is the FIRST thread message and we have a parent_target_id, force it as reply to the starter + if not reply_to_stoat_id and parent_target_id and stats["messages"] == 0: + reply_to_stoat_id = parent_target_id + + # Prepend thread marker to the first message of the thread + if thread_name and stats["messages"] == 0: + content = f"> <<< THREAD: **{thread_name}** >>>\n{content}" avatar_url = str(msg.author.display_avatar.url) if msg.author.display_avatar.url else None if avatar_url and not avatar_url.startswith("http"): @@ -260,12 +268,16 @@ async def migrate_messages( # Track thread entry stats["threads"] += 1 + pass + # Migrate thread messages recursively thread_stats = await migrate_messages( context=context, source_channel_id=thread.id, target_channel_id=target_channel_id, - thread_id=str(thread.id) + thread_id=str(thread.id), + parent_target_id=stoat_msg_id, + thread_name=thread.name ) stats["messages"] += thread_stats["messages"] stats["attachments"] += thread_stats["attachments"] diff --git a/src/stoat/writer.py b/src/stoat/writer.py index 53ab0c8..b2013d0 100644 --- a/src/stoat/writer.py +++ b/src/stoat/writer.py @@ -279,7 +279,7 @@ class StoatWriter: logger.error(f"Failed to send Stoat message to {channel_id}: {e}") raise # Let caller handle (migration loop will stop for permission errors) - async def send_marker(self, channel_id: str, content: str, files: Optional[List[Dict[str, Any]]] = None) -> Optional[str]: + async def send_marker(self, channel_id: str, content: str, files: Optional[List[Dict[str, Any]]] = None, reply_to_message_id: Optional[str] = None) -> Optional[str]: try: channel = await self.client.fetch_channel(channel_id) attachments = None @@ -287,7 +287,16 @@ class StoatWriter: attachments = [] for f in files: attachments.append((f["filename"], f["data"])) - msg = await channel.send(content=content, attachments=attachments) + + replies = None + if reply_to_message_id: + replies = [stoat.Reply(id=reply_to_message_id, mention=False)] + + msg = await channel.send( + content=content, + attachments=attachments, + replies=replies + ) return str(msg.id) except Exception as e: logger.error(f"Failed to send Stoat marker to {channel_id}: {e}")