diff --git a/src/core/engine.py b/src/core/engine.py index f8f3c5f..a7166c3 100644 --- a/src/core/engine.py +++ b/src/core/engine.py @@ -37,10 +37,13 @@ class MigrationEngine: "discord_bot_name": d_valid.get("bot_name"), "discord_server": d_valid.get("server", False), "discord_server_name": d_valid.get("server_name"), + "discord_intents": d_valid.get("intents", {}), + "discord_permissions": d_valid.get("permissions", {}), "fluxer_token": f_valid.get("token", False), "fluxer_bot_name": f_valid.get("bot_name"), "fluxer_community": f_valid.get("community", False), - "fluxer_community_name": f_valid.get("community_name") + "fluxer_community_name": f_valid.get("community_name"), + "fluxer_permissions": f_valid.get("permissions", {}) } except Exception as e: logger.error(f"Validation failed with exception: {e}") diff --git a/src/discord_bot/reader.py b/src/discord_bot/reader.py index 06be458..cf12ba0 100644 --- a/src/discord_bot/reader.py +++ b/src/discord_bot/reader.py @@ -26,8 +26,15 @@ class DiscordReader: self.guild = await self.client.fetch_guild(self.server_id) async def validate(self) -> Dict[str, Any]: - """Validates the token and server ID.""" - results = {"token": False, "server": False, "bot_name": None, "server_name": None} + """Validates the token, server ID, intents, and permissions.""" + results = { + "token": False, + "server": False, + "bot_name": None, + "server_name": None, + "intents": {"message_content": False}, + "permissions": {"view_channel": False, "read_message_history": False} + } temp_client = self._create_client() try: await temp_client.login(self.token) @@ -39,6 +46,20 @@ class DiscordReader: if guild is not None: results["server"] = True results["server_name"] = guild.name + + # Check intents + results["intents"]["message_content"] = temp_client.intents.message_content + + # Check permissions + # We need to fetch the member to check permissions + try: + member = await guild.fetch_member(temp_client.user.id) + perms = member.guild_permissions + results["permissions"]["view_channel"] = perms.view_channel + results["permissions"]["read_message_history"] = perms.read_message_history + except Exception: + # Fallback if member fetch fails, though it shouldn't for the bot itself + pass except Exception: pass finally: diff --git a/src/fluxer_bot/writer.py b/src/fluxer_bot/writer.py index b967447..eefd42e 100644 --- a/src/fluxer_bot/writer.py +++ b/src/fluxer_bot/writer.py @@ -68,7 +68,7 @@ class FluxerWriter: return self.bot._http if self.bot else None async def validate(self) -> dict: - """Validates the token and community ID.""" + """Validates the token, community ID, and permissions.""" if not self.bot or not self._ready_event.is_set(): await self.start() @@ -76,23 +76,74 @@ class FluxerWriter: is_community_valid = False bot_name = None community_name = None + permissions = { + "manage_channels": False, + "manage_messages": False, + "manage_roles": False, + "manage_emojis_stickers": False, + "manage_webhooks": False + } + try: # Check token by fetching me + me_id = None if self.bot and self.bot.user: is_token_valid = True bot_name = self.bot.user.username + me_id = self.bot.user.id else: - # Fallback if on_ready didn't fire yet but we want to check token me = await self.client.get_current_user() if me: is_token_valid = True bot_name = me.get("username") + me_id = int(me["id"]) # Check community - guild = await self.client.get_guild(self.community_id) - if guild: + guild_data = await self.client.get_guild(self.community_id) + if guild_data: is_community_valid = True - community_name = guild.get("name") + community_name = guild_data.get("name") + + if me_id: + try: + # Fetch member to get roles + member_data = await self.client.get_guild_member(self.community_id, me_id) + member_role_ids = [int(r) for r in member_data.get("roles", [])] + + # Fetch all roles to get their permissions + all_roles_data = await self.client.get_guild_roles(self.community_id) + + # Calculate total permissions + # In Discord/Fluxer, permissions are additive + total_perms = 0 + for r_data in all_roles_data: + r_id = int(r_data["id"]) + # Add @everyone permissions (role ID same as guild ID) + if r_id == int(self.community_id) or r_id in member_role_ids: + total_perms |= int(r_data.get("permissions", 0)) + + # Debugging + # print(f"DEBUG: me_id={me_id}, roles={member_role_ids}, total_perms={total_perms}") + + # Bitmask Mapping (Discord standard) + # Administrator: 1 << 3 + # Manage Channels: 1 << 4 + # Manage Messages: 1 << 13 + # Manage Roles: 1 << 28 + # Manage Webhooks: 1 << 29 + # Manage Emojis/Stickers: 1 << 30 + is_admin = bool(total_perms & (1 << 3)) + + permissions["manage_channels"] = is_admin or bool(total_perms & (1 << 4)) + permissions["manage_messages"] = is_admin or bool(total_perms & (1 << 13)) + permissions["manage_roles"] = is_admin or bool(total_perms & (1 << 28)) + permissions["manage_webhooks"] = is_admin or bool(total_perms & (1 << 29)) + permissions["manage_emojis_stickers"] = is_admin or bool(total_perms & (1 << 30)) + + if is_admin: + logger.info(f"Fluxer bot {bot_name} has Administrator permission.") + except Exception as e: + logger.error(f"Failed to calculate Fluxer permissions: {e}") except Exception: pass @@ -100,7 +151,8 @@ class FluxerWriter: "token": is_token_valid, "community": is_community_valid, "bot_name": bot_name, - "community_name": community_name + "community_name": community_name, + "permissions": permissions } async def create_channel(self, name: str, topic: str = "", type: int = 0, parent_id: Optional[str] = None) -> str: diff --git a/src/ui/app.py b/src/ui/app.py index 2dab1af..b12638c 100644 --- a/src/ui/app.py +++ b/src/ui/app.py @@ -68,8 +68,10 @@ class MigrationCLI: self.validation_results = { "discord_token": False, "discord_bot_name": None, "discord_server": False, "discord_server_name": None, + "discord_intents": {}, "discord_permissions": {}, "fluxer_token": False, "fluxer_bot_name": None, - "fluxer_community": False, "fluxer_community_name": None + "fluxer_community": False, "fluxer_community_name": None, + "fluxer_permissions": {} } self.tokens_valid = False @@ -97,6 +99,11 @@ class MigrationCLI: console.print("[bold red]Discord Token validation failed (Invalid Token).[/bold red]") elif not res.get("server"): console.print("[bold red]Discord Server ID validation failed (Invalid/Inaccessible ID).[/bold red]") + else: + # Store intents & permissions for later UI display + self.validation_results["discord_intents"] = res.get("intents", {}) + self.validation_results["discord_permissions"] = res.get("permissions", {}) + except Exception as e: console.print(f"[bold red]Discord validation failed with error: {e}[/bold red]") else: @@ -115,13 +122,37 @@ class MigrationCLI: console.print("[bold red]Fluxer Token validation failed (Invalid Token).[/bold red]") elif not res.get("community"): console.print("[bold red]Fluxer Community ID validation failed (Invalid/Inaccessible ID).[/bold red]") + else: + # Store permissions + self.validation_results["fluxer_permissions"] = res.get("permissions", {}) + except Exception as e: 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]") fluxer_task.cancel() - self.tokens_valid = all(self.validation_results.values()) + # Only tokens and server/community existence are strictly required for 'tokens_valid' + self.tokens_valid = ( + self.validation_results.get("discord_token") and + self.validation_results.get("discord_server") and + self.validation_results.get("fluxer_token") and + self.validation_results.get("fluxer_community") + ) + + # Check if all permissions are actually granted + self.permissions_complete = True + if self.tokens_valid: + # Discord + d_intents = self.validation_results.get("discord_intents", {}) + d_perms = self.validation_results.get("discord_permissions", {}) + if not all([d_intents.get("message_content"), d_perms.get("view_channel"), d_perms.get("read_message_history")]): + self.permissions_complete = False + + # Fluxer + f_perms = self.validation_results.get("fluxer_permissions", {}) + if not all(f_perms.values()) if f_perms else True: + self.permissions_complete = False except Exception as e: console.print(f"[bold red]Validation system failure: {e}[/bold red]") @@ -151,7 +182,13 @@ class MigrationCLI: console.print("(4) Sync Server Name, Logo and Banner") console.print("(5) Migrate message history") - val_status = "[bold green][VALID][/bold green]" if self.tokens_valid else "[bold red][INVALID][/bold red]" + if not self.tokens_valid: + val_status = "[bold red][INVALID][/bold red]" + elif not self.permissions_complete: + val_status = "[bold yellow][PERMISSION MISSING][/bold yellow]" + else: + val_status = "[bold green][VALID][/bold green]" + console.print(f"(6) Configuration {val_status}") console.print("(7) [bold red]⚠ Danger Zone[/bold red]") @@ -180,6 +217,34 @@ class MigrationCLI: async def edit_configuration(self): await self.validate_config() + + # Display Required Permissions FIRST + def fmt(name, val): + if val is None: return f"[dim]{name}[/dim]" # Unknown + return f"[green]{name}[/green]" if val else f"[red]{name} [MISSING][/red]" + + d_intents = self.validation_results.get("discord_intents", {}) + d_perms = self.validation_results.get("discord_permissions", {}) + f_perms = self.validation_results.get("fluxer_permissions", {}) + + perm_table = Table(show_header=True, header_style="bold magenta", box=None, padding=(0, 2)) + perm_table.add_column("Bot Type") + perm_table.add_column("Required Permissions & Intents") + + perm_table.add_row( + "[bold #5865F2]Discord Bot[/bold #5865F2]", + f"• [bold]Intents:[/bold] Server Members, {fmt('Message Content', d_intents.get('message_content'))}\n" + f"• [bold]Permissions:[/bold] {fmt('View Channels', d_perms.get('view_channel'))}, {fmt('Read Message History', d_perms.get('read_message_history'))}" + ) + perm_table.add_row( + "[bold #4641D9]Fluxer Bot[/bold #4641D9]", + f"• [bold]Permissions:[/bold] {fmt('Manage Channels', f_perms.get('manage_channels'))}, {fmt('Manage Messages', f_perms.get('manage_messages'))},\n" + f" {fmt('Manage Roles', f_perms.get('manage_roles'))}, {fmt('Manage Emojis/Stickers', f_perms.get('manage_emojis_stickers'))}, {fmt('Manage Webhooks', f_perms.get('manage_webhooks'))}" + ) + + console.print("\n") # Add spacing before panel + console.print(Panel(perm_table, title="[bold]Required Bot Permissions[/bold]", expand=False, border_style="dim")) + console.print("\n[bold]Configuration Status:[/bold]") def get_status_str(is_valid, name=None): @@ -192,8 +257,8 @@ class MigrationCLI: console.print(f"Fluxer Bot Token {get_status_str(self.validation_results.get('fluxer_token', False), self.validation_results.get('fluxer_bot_name'))}") console.print(f"Discord Server ID {get_status_str(self.validation_results.get('discord_server', False), self.validation_results.get('discord_server_name'))}") console.print(f"Fluxer Community ID {get_status_str(self.validation_results.get('fluxer_community', False), self.validation_results.get('fluxer_community_name'))}") - - if not Confirm.ask("Edit now?"): + + if not Confirm.ask("\nEdit now?"): return console.print("\n[bold]Configuration Editor[/bold] (leave blank to keep current)")