diff --git a/src/core/engine.py b/src/core/engine.py index a7166c3..fe8a66e 100644 --- a/src/core/engine.py +++ b/src/core/engine.py @@ -158,6 +158,36 @@ class MigrationEngine: if updates > 0 or removals > 0: logger.info(f"Channel sync: {updates} mapped, {removals} stale mappings removed") + async def sync_roles_state(self): + """ + Scans Fluxer for roles matching Discord names and updates state.json mappings. + """ + discord_roles = await self.discord_reader.get_roles() + fluxer_roles = await self.fluxer_writer.client.get_guild_roles(self.config.fluxer_community_id) + + # Build name -> id maps and ID sets for Fluxer for fast lookup + fluxer_role_map = {r.get("name"): str(r.get("id")) for r in fluxer_roles if r.get("name")} + fluxer_role_ids = {str(r.get("id")) for r in fluxer_roles} + + updates = 0 + removals = 0 + + # Verify and Sync Roles + for role in discord_roles: + discord_id = str(role.id) + fluxer_id = self.state.get_fluxer_role_id(discord_id) + + if fluxer_id: + if fluxer_id not in fluxer_role_ids: + self.state.remove_role_mapping(discord_id) + removals += 1 + elif role.name in fluxer_role_map: + self.state.set_role_mapping(discord_id, fluxer_role_map[role.name]) + updates += 1 + + if updates > 0 or removals > 0: + logger.info(f"Role sync: {updates} mapped, {removals} stale mappings removed") + async def sync_assets_state(self): """ Scans Fluxer for emojis and stickers matching Discord names and updates state.json mappings. @@ -310,12 +340,38 @@ class MigrationEngine: if total == 0: return + async def _sync_overwrites(discord_item, fluxer_id): + """Helper to sync role overwrites for a given channel or category.""" + for target, overwrite in discord_item.overwrites.items(): + if type(target).__name__ == "Role": + discord_role_id = str(target.id) + # Handle @everyone role special case + if discord_role_id == self.config.discord_server_id: + fluxer_role_id = self.config.fluxer_community_id + else: + fluxer_role_id = self.state.get_fluxer_role_id(discord_role_id) + + if not fluxer_role_id: + continue + + allow_val, deny_val = overwrite.pair() + await self.fluxer_writer.set_channel_permission( + channel_id=fluxer_id, + overwrite_id=fluxer_role_id, + allow=allow_val.value, + deny=deny_val.value, + is_role=True + ) + # Sync Category Permissions (Role Overwrites) for cat in categories: if not self.is_running: break - fluxer_id = self.state.get_fluxer_channel_id(str(cat.id)) - # In a real implementation, we would diff discord perms - # and apply them to fluxer_id using client methods. + fluxer_id = self.state.get_fluxer_category_id(str(cat.id)) + if fluxer_id: + try: + await _sync_overwrites(cat, fluxer_id) + except Exception as e: + logger.error(f"Failed syncing permissions for category {cat.name}: {e}") current_idx += 1 if progress_callback: await progress_callback(f"Cat: {cat.name}", current_idx, total) @@ -324,7 +380,11 @@ class MigrationEngine: for channel in channels: if not self.is_running: break fluxer_id = self.state.get_fluxer_channel_id(str(channel.id)) - # apply perms + if fluxer_id: + try: + await _sync_overwrites(channel, fluxer_id) + except Exception as e: + logger.error(f"Failed syncing permissions for channel {channel.name}: {e}") current_idx += 1 if progress_callback: await progress_callback(channel.name, current_idx, total) diff --git a/src/core/state.py b/src/core/state.py index ae0e7ee..6c6c49b 100644 --- a/src/core/state.py +++ b/src/core/state.py @@ -107,6 +107,10 @@ class MigrationState: def get_fluxer_role_id(self, discord_id: str) -> str | None: return self.role_map.get(str(discord_id)) + def remove_role_mapping(self, discord_id: str): + self.role_map.pop(str(discord_id), None) + self.save() + def set_emoji_mapping(self, discord_id: str, fluxer_id: str): self.emoji_map[str(discord_id)] = str(fluxer_id) self.save() diff --git a/src/fluxer_bot/writer.py b/src/fluxer_bot/writer.py index eefd42e..8ed8515 100644 --- a/src/fluxer_bot/writer.py +++ b/src/fluxer_bot/writer.py @@ -436,9 +436,16 @@ class FluxerWriter: overwrites = ch.get("permission_overwrites", []) for ow in overwrites: try: - await self.client.delete_channel_permission(ch["id"], ow["id"]) - except Exception: - pass + await self.client.request( + self.client._route( + "DELETE", + "/channels/{channel_id}/permissions/{overwrite_id}", + channel_id=ch["id"], + overwrite_id=ow["id"] + ) + ) + except Exception as e: + logger.error(f"Failed to delete overwrite {ow['id']} for channel {ch['id']}: {e}") processed += 1 if progress_callback: await progress_callback(ch.get("name", "Unknown"), processed, total) @@ -446,6 +453,21 @@ class FluxerWriter: print(f"Failed to reset permissions for channel {ch.get('name')}: {e}") return processed + async def set_channel_permission(self, channel_id: str, overwrite_id: str, allow: int, deny: int, is_role: bool = True): + """Sets a permission overwrite for a channel or category.""" + assert self.client is not None + try: + await self.client.edit_channel_permissions( + channel_id=int(channel_id), + overwrite_id=int(overwrite_id), + allow=allow, + deny=deny, + type=0 if is_role else 1 + ) + except Exception as e: + logger.error(f"Failed to set permission on channel {channel_id} for overwrite {overwrite_id}: {e}") + + async def delete_all_roles(self, progress_callback=None) -> int: """ Deletes all non-managed, non-default roles in the Fluxer community, diff --git a/src/ui/app.py b/src/ui/app.py index b12638c..8736fd7 100644 --- a/src/ui/app.py +++ b/src/ui/app.py @@ -71,7 +71,8 @@ class MigrationCLI: "discord_intents": {}, "discord_permissions": {}, "fluxer_token": False, "fluxer_bot_name": None, "fluxer_community": False, "fluxer_community_name": None, - "fluxer_permissions": {} + "fluxer_permissions": {}, + "discord_timeout": False, "fluxer_timeout": False } self.tokens_valid = False @@ -108,6 +109,7 @@ class MigrationCLI: console.print(f"[bold red]Discord validation failed with error: {e}[/bold red]") else: console.print("[bold red]Discord bot token validation timed out after 10 seconds.[/bold red]") + self.validation_results["discord_timeout"] = True discord_task.cancel() # Process Fluxer Result @@ -130,6 +132,7 @@ class MigrationCLI: console.print(f"[bold red]Fluxer validation failed with error: {e}[/bold red]") else: console.print("[bold red]Fluxer bot token validation timed out after 10 seconds.[/bold red]") + self.validation_results["fluxer_timeout"] = True fluxer_task.cancel() # Only tokens and server/community existence are strictly required for 'tokens_valid' @@ -168,10 +171,10 @@ class MigrationCLI: console.print("") console.print(Panel.fit("Fluxer Reaper", style="bold blue")) d_name = self.validation_results.get("discord_server_name") - d_display = f"[bold green]\"{d_name}\"[/bold green]" if d_name else "[bold red]NOT SET UP[/bold red]" + d_display = f"[bold green]\"{d_name}\"[/bold green]" if d_name else ("[bold yellow]TIMEOUT ERROR[/bold yellow]" if self.validation_results.get("discord_timeout") else "[bold red]NOT SET UP[/bold red]") f_name = self.validation_results.get("fluxer_community_name") - f_display = f"[bold green]\"{f_name}\"[/bold green]" if f_name else "[bold red]NOT SET UP[/bold red]" + f_display = f"[bold green]\"{f_name}\"[/bold green]" if f_name else ("[bold yellow]TIMEOUT ERROR[/bold yellow]" if self.validation_results.get("fluxer_timeout") else "[bold red]NOT SET UP[/bold red]") console.print(f"[bold cyan]Discord Server:[/bold cyan] {d_display}") console.print(f"[bold #4641D9]Fluxer Community:[/bold #4641D9] {f_display}") @@ -190,7 +193,7 @@ class MigrationCLI: val_status = "[bold green][VALID][/bold green]" console.print(f"(6) Configuration {val_status}") - console.print("(7) [bold red]⚠ Danger Zone[/bold red]") + console.print("(7) [red]Danger Zone ⚠[/red]") console.print("(Q) Exit") @@ -466,6 +469,13 @@ class MigrationCLI: return elif sub_choice == "F": force = True + else: + if not Confirm.ask("Clone roles?", default=True): + await self.engine.close_connections() + return + if not Confirm.ask("Are you sure?", default=True): + await self.engine.close_connections() + return console.print("\n[bold green]Starting Role Migration...[/bold green]") with Progress(