diff --git a/src/core/backup_reader.py b/src/core/backup_reader.py index 1fb50b4..c7a2697 100644 --- a/src/core/backup_reader.py +++ b/src/core/backup_reader.py @@ -691,7 +691,6 @@ class BackupReader: def __init__(self, backup_path: str | Path): self.backup_path = Path(backup_path) self.guild: BackupGuild | None = None - self.role_map: Dict[int, str] = {} # Internal caches populated by start() self._categories: List[BackupCategory] = [] @@ -724,7 +723,6 @@ class BackupReader: if roles_file.exists(): roles_data = json.loads(roles_file.read_text(encoding="utf-8")) self._roles = [BackupRole(r) for r in roles_data] - self.role_map = {r.id: r.name for r in self._roles} logger.info(f"[Backup] Loaded {len(self._roles)} roles") # 3. Structure -> categories + channels diff --git a/src/core/discord_reader.py b/src/core/discord_reader.py index 1dff7d0..3789f52 100644 --- a/src/core/discord_reader.py +++ b/src/core/discord_reader.py @@ -61,14 +61,6 @@ class DiscordReader: self.guild: discord.Guild | None = None self.client: discord.Client | None = None - self.role_map: Dict[int, str] = {} - self.channel_name_map: Dict[int, str] = {} - # Session-level caches to avoid redundant fetch calls - self._roles_cache: list[discord.Role] | None = None - self._channels_cache: list[discord.abc.GuildChannel] | None = None - self._categories_cache: list[discord.CategoryChannel] | None = None - self._emojis_cache: list[discord.Emoji] | None = None - self._stickers_cache: list[discord.GuildSticker] | None = None def _create_client(self): intents = discord.Intents.default() @@ -86,6 +78,7 @@ class DiscordReader: await self.client.login(self.token) # Use fetch methods specifically to bypass dependency on gateway cache + # fetch_guild initializes the guild object needed for subsequent API calls try: logger.info(f"Fetching guild {self.server_id}...") self.guild = await self.client.fetch_guild(self.server_id) @@ -93,32 +86,9 @@ class DiscordReader: except discord.Forbidden: logger.error(f"403 Forbidden: Missing Access to fetch guild {self.server_id}.") raise - - # Pre-fetch roles via API - Handle Forbidden gracefully - try: - roles = await self.guild.fetch_roles() - self.role_map = {r.id: r.name for r in roles} - self._roles_cache = [r for r in roles if not r.is_default()] - except discord.Forbidden: - logger.warning("403 Forbidden: Missing Access to fetch roles. Continuing without role mapping.") - self.role_map = {} except Exception as e: - logger.error(f"Failed to fetch roles: {e}") - self.role_map = {} - - # Pre-fetch channels via API - try: - channels = await self.guild.fetch_channels() - self.channel_name_map = {c.id: c.name for c in channels} - self._channels_cache = [c for c in channels if not isinstance(c, discord.CategoryChannel)] - self._categories_cache = [c for c in channels if isinstance(c, discord.CategoryChannel)] - logger.debug(f"Pre-fetched {len(self.channel_name_map)} channels") - except discord.Forbidden: - logger.warning("403 Forbidden: Missing Access to fetch channels. Continuing without channel name mapping.") - self.channel_name_map = {} - except Exception as e: - logger.error(f"Failed to fetch channels: {e}") - self.channel_name_map = {} + logger.error(f"Failed to fetch guild {self.server_id}: {e}") + raise async def validate(self) -> Dict[str, Any]: """Validates the token, server ID, intents, and permissions.""" @@ -180,40 +150,27 @@ class DiscordReader: async def get_categories(self): if not self.guild: return [] - if self._categories_cache is not None: - return self._categories_cache categories = await self.guild.fetch_channels() - self._categories_cache = [c for c in categories if isinstance(c, discord.CategoryChannel)] - return self._categories_cache + return [c for c in categories if isinstance(c, discord.CategoryChannel)] async def get_roles(self): """Returns all roles in the server (excluding @everyone).""" if not self.guild: return [] - if self._roles_cache is not None: - return self._roles_cache roles = await self.guild.fetch_roles() - # Filter out default @everyone role which cannot typically be created - self._roles_cache = [r for r in roles if not r.is_default()] - return self._roles_cache + return [r for r in roles if not r.is_default()] async def get_emojis(self): """Returns all custom emojis in the server.""" if not self.guild: return [] - if self._emojis_cache is not None: - return self._emojis_cache - self._emojis_cache = await self.guild.fetch_emojis() - return self._emojis_cache + return await self.guild.fetch_emojis() async def get_stickers(self): """Returns all custom stickers in the server.""" if not self.guild: return [] - if self._stickers_cache is not None: - return self._stickers_cache - self._stickers_cache = await self.guild.fetch_stickers() - return self._stickers_cache + return await self.guild.fetch_stickers() async def get_members(self): """Returns all members in the server.""" @@ -230,11 +187,9 @@ class DiscordReader: if not self.guild: return [] - if self._channels_cache is None: - channels = await self.guild.fetch_channels() - self._channels_cache = [c for c in channels if not isinstance(c, discord.CategoryChannel)] + channels = await self.guild.fetch_channels() + all_channels = [c for c in channels if not isinstance(c, discord.CategoryChannel)] - all_channels = self._channels_cache if category_id: all_channels = [c for c in all_channels if c.category_id == category_id] return all_channels @@ -284,11 +239,6 @@ class DiscordReader: client = self.client self.client = None # Atomic clear self.guild = None - self._roles_cache = None - self._channels_cache = None - self._categories_cache = None - self._emojis_cache = None - self._stickers_cache = None if client: try: await client.close() diff --git a/src/fluxer/migrate_message.py b/src/fluxer/migrate_message.py index 307b16f..8631413 100644 --- a/src/fluxer/migrate_message.py +++ b/src/fluxer/migrate_message.py @@ -8,7 +8,7 @@ from src.core.utils import resolve_discord_links logger = logging.getLogger(__name__) -def clean_mentions(content: str, guild, user_mentions=None, role_mentions=None, role_map=None, emoji_map=None, channel_map=None, discord_channel_map=None, state=None, target_server_id=None) -> str: +def clean_mentions(content: str, guild, user_mentions=None, role_mentions=None, emoji_map=None, channel_map=None, state=None, target_server_id=None) -> str: if not content or not guild: return content @@ -47,10 +47,6 @@ def clean_mentions(content: str, guild, user_mentions=None, role_mentions=None, if role and role.name: return f"`@{role.name}`" - # 4. Try provided role map - if role_map and rid in role_map: - return f"`@{role_map[rid]}`" - return match.group(0) def replace_channel(match): @@ -61,13 +57,8 @@ def clean_mentions(content: str, guild, user_mentions=None, role_mentions=None, return f"<#{channel_map[str(cid)]}>" # 2. Fallback to name in backticks - # Try metadata map first (robust) - if discord_channel_map and cid in discord_channel_map: - return f"`#{discord_channel_map[cid]}`" - - # Try cache channel = guild.get_channel(cid) - return f"`{channel.name}`" if channel else f"<#{cid}>" + return f"`#{channel.name}`" if channel else f"<#{cid}>" def replace_emoji(match): animated = match.group(1) == "a" @@ -196,10 +187,8 @@ async def migrate_messages( msg.guild, msg.mentions, msg.role_mentions, - context.discord_reader.role_map, context.state.emoji_map, context.state.channel_map, - context.discord_reader.channel_name_map, state=context.state, target_server_id=context.fluxer_writer.community_id ) @@ -229,10 +218,8 @@ async def migrate_messages( msg.guild, snapshot.mentions if hasattr(snapshot, 'mentions') else None, snapshot.role_mentions if hasattr(snapshot, 'role_mentions') else None, - context.discord_reader.role_map, context.state.emoji_map, context.state.channel_map, - context.discord_reader.channel_name_map, state=context.state, target_server_id=context.fluxer_writer.community_id ) diff --git a/src/stoat/migrate_message.py b/src/stoat/migrate_message.py index 7cd4217..2294697 100644 --- a/src/stoat/migrate_message.py +++ b/src/stoat/migrate_message.py @@ -8,7 +8,7 @@ from src.core.utils import resolve_discord_links logger = logging.getLogger(__name__) -def clean_mentions(content: str, guild, user_mentions=None, role_mentions=None, role_map=None, emoji_map=None, channel_map=None, discord_channel_map=None, state=None, target_server_id=None) -> str: +def clean_mentions(content: str, guild, user_mentions=None, role_mentions=None, emoji_map=None, channel_map=None, state=None, target_server_id=None) -> str: if not content or not guild: return content @@ -47,10 +47,6 @@ def clean_mentions(content: str, guild, user_mentions=None, role_mentions=None, if role and role.name: return f"`@{role.name}`" - # 4. Try provided role map - if role_map and rid in role_map: - return f"`@{role_map[rid]}`" - return match.group(0) def replace_channel(match): @@ -61,13 +57,8 @@ def clean_mentions(content: str, guild, user_mentions=None, role_mentions=None, return f"<#{channel_map[str(cid)]}>" # 2. Fallback to name in backticks - # Try metadata map first (robust) - if discord_channel_map and cid in discord_channel_map: - return f"`#{discord_channel_map[cid]}`" - - # Try cache channel = guild.get_channel(cid) - return f"`{channel.name}`" if channel else f"<#{cid}>" + return f"`#{channel.name}`" if channel else f"<#{cid}>" def replace_emoji(match): animated = match.group(1) == "a" @@ -204,10 +195,8 @@ async def migrate_messages( msg.guild, msg.mentions, msg.role_mentions, - context.discord_reader.role_map, context.state.emoji_map, context.state.channel_map, - context.discord_reader.channel_name_map, state=context.state, target_server_id=context.stoat_writer.community_id ) @@ -233,10 +222,8 @@ async def migrate_messages( msg.guild, snapshot.mentions if hasattr(snapshot, 'mentions') else None, snapshot.role_mentions if hasattr(snapshot, 'role_mentions') else None, - context.discord_reader.role_map, context.state.emoji_map, context.state.channel_map, - context.discord_reader.channel_name_map, state=context.state, target_server_id=context.stoat_writer.community_id ) diff --git a/src/stoat/writer.py b/src/stoat/writer.py index f5e42b6..bfa1918 100644 --- a/src/stoat/writer.py +++ b/src/stoat/writer.py @@ -337,7 +337,13 @@ class StoatWriter: # Stoat requires SendableEmbed objects, not raw dicts stoat_embeds = [] if embeds: - for e in embeds: + for raw_embed in embeds: + # Normalize embed to dict (handles both live discord.Embed and backup dicts) + if hasattr(raw_embed, "to_dict"): + e = raw_embed.to_dict() + else: + e = raw_embed + # Convert integer color to hex string if present color = None if e.get("color"):