diff --git a/README.md b/README.md index 2ce3ac9..419adf3 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,40 @@ -# Fluxer Reaper 🚀 +# Fluxer Reaper Fluxer Reaper is a simple tool to help you move your Discord server content over to a Fluxer community. It handles channels, roles, emojis, and even your message history. ![Fluxer Reaper](fluxer-reaper.jpg) -## 🛠️ Getting Started +## Getting Started ### 1. Prerequisites Make sure you have Python installed on your computer. -### 2. Install the tool +### 2. Download or Clone +Clone the repository using git: +```bash +git clone https://github.com/rambros3d/fluxer-reaper.git +cd fluxer-reaper +``` +Alternatively, you can download the project as a ZIP file and extract its contents. + +### 3. Install the tool Open your terminal and run: ```bash pip install -r requirements.txt ``` -### 3. Setup your config -Open the `config.yaml` file and fill in your details: -* **Discord Bot Token**: Your bot's token from the Discord Developer Portal. -* **Discord Server ID**: The ID of the server you want to copy. -* **Fluxer Bot Token**: Your Fluxer bot token. -* **Fluxer Community ID**: The ID of the Fluxer community where you want to move everything. - ### 4. Run it! Start the tool by running: ```bash python fluxer-reaper.py ``` -## ✨ Features + +## Features * **Clone Server Structure**: Automatically creates all your Discord categories and channels in Fluxer. * **Copy Roles**: Copies your roles and their basic permissions. * **Copy Emojis & Stickers**: Copies your custom emojis and stickers over. * **Message History**: Select a specific channel and migrate messages starting from the oldest or a custom point. * **Server Identity**: Syncs your server name, icon, and banner. +* **Danger Zone**: Option to wipe existing channels, roles, and content in the fluxer community. -## ⚠️ Important Tips -* **Token Validation**: The tool will double-check your tokens every time you start it or change settings. -* **Safety**: You will always be asked for confirmation before any major action (like deleting or creating many items). diff --git a/src/core/engine.py b/src/core/engine.py index 34297cc..d34682f 100644 --- a/src/core/engine.py +++ b/src/core/engine.py @@ -293,3 +293,25 @@ class MigrationEngine: def stop(self): self.is_running = False + + # ──────────────── DANGER ZONE ──────────────── + + async def danger_remove_logo_and_banner(self) -> None: + """Removes the community logo and banner image.""" + await self.fluxer_writer.remove_community_logo_and_banner() + + async def danger_delete_all_channels(self, progress_callback=None) -> int: + """Deletes every channel and category in the Fluxer community.""" + return await self.fluxer_writer.delete_all_channels(progress_callback=progress_callback) + + async def danger_reset_channel_permissions(self, progress_callback=None) -> int: + """Resets all permission overwrites on every channel and category.""" + return await self.fluxer_writer.reset_channel_permissions(progress_callback=progress_callback) + + async def danger_delete_all_roles(self, progress_callback=None) -> int: + """Deletes all deletable roles (skips managed/bot roles and @everyone).""" + return await self.fluxer_writer.delete_all_roles(progress_callback=progress_callback) + + async def danger_delete_all_emojis_and_stickers(self, progress_callback=None) -> int: + """Deletes all custom emojis and stickers from the Fluxer community.""" + return await self.fluxer_writer.delete_all_emojis_and_stickers(progress_callback=progress_callback) diff --git a/src/fluxer_bot/writer.py b/src/fluxer_bot/writer.py index 11dc8b5..c64d5e4 100644 --- a/src/fluxer_bot/writer.py +++ b/src/fluxer_bot/writer.py @@ -256,6 +256,146 @@ class FluxerWriter: except Exception as e: print(f"Failed to update community metadata: {e}") + async def remove_community_logo_and_banner(self) -> None: + """ + Removes the community logo (icon) and banner by setting them to None. + """ + assert self.client is not None + try: + await self.client.modify_guild( + guild_id=self.community_id, + icon=None, + banner=None + ) + except Exception as e: + print(f"Failed to remove community logo/banner: {e}") + + async def delete_all_channels(self, progress_callback=None) -> int: + """ + Deletes all channels and categories in the Fluxer community. + Returns the count of deleted channels. + """ + assert self.client is not None + channels = await self.client.get_guild_channels(self.community_id) + total = len(channels) + deleted = 0 + # Delete non-category channels first, then categories + sorted_channels = sorted(channels, key=lambda c: 0 if c.get("type") == 4 else -1) + for ch in sorted_channels: + try: + await self.client.delete_channel(ch["id"]) + deleted += 1 + if progress_callback: + await progress_callback(ch.get("name", "Unknown"), deleted, total) + except Exception as e: + print(f"Failed to delete channel {ch.get('name')}: {e}") + return deleted + + async def reset_channel_permissions(self, progress_callback=None) -> int: + """ + Resets all permission overwrites on every channel and category. + Returns the count of channels processed. + """ + assert self.client is not None + channels = await self.client.get_guild_channels(self.community_id) + total = len(channels) + processed = 0 + for ch in channels: + try: + # Fetch existing overwrites and delete each one + overwrites = ch.get("permission_overwrites", []) + for ow in overwrites: + try: + await self.client.delete_channel_permission(ch["id"], ow["id"]) + except Exception: + pass + processed += 1 + if progress_callback: + await progress_callback(ch.get("name", "Unknown"), processed, total) + except Exception as e: + print(f"Failed to reset permissions for channel {ch.get('name')}: {e}") + return processed + + async def delete_all_roles(self, progress_callback=None) -> int: + """ + Deletes all non-managed, non-default roles in the Fluxer community, + while safely skipping the bot's own managed role. + Returns the count of deleted roles. + """ + assert self.client is not None + + # Fetch the bot's user ID so we can skip its managed role + bot_user_id = None + try: + if self.bot and self.bot.user: + bot_user_id = str(self.bot.user.id) + else: + me = await self.client.get_current_user() + if me: + bot_user_id = str(me.get("id")) + except Exception: + pass + + roles = await self.client.get_guild_roles(self.community_id) + deletable = [] + for r in roles: + # Skip @everyone (position 0) and managed roles (e.g. bot roles) + if r.get("managed") or r.get("name") == "@everyone": + continue + deletable.append(r) + + total = len(deletable) + deleted = 0 + for role in deletable: + try: + await self.client.delete_guild_role(self.community_id, role["id"]) + deleted += 1 + if progress_callback: + await progress_callback(role.get("name", "Unknown"), deleted, total) + except Exception as e: + print(f"Failed to delete role {role.get('name')}: {e}") + return deleted + + async def delete_all_emojis_and_stickers(self, progress_callback=None) -> int: + """ + Deletes all custom emojis and stickers in the Fluxer community. + Returns the total count of deleted items. + """ + assert self.client is not None + deleted = 0 + + # Delete emojis + try: + emojis = await self.client.get_guild_emojis(self.community_id) + emoji_total = len(emojis) + for idx, emoji in enumerate(emojis): + try: + await self.client.delete_guild_emoji(self.community_id, emoji["id"]) + deleted += 1 + if progress_callback: + await progress_callback(emoji.get("name", "Unknown"), "Emoji", deleted, emoji_total) + except Exception as e: + print(f"Failed to delete emoji {emoji.get('name')}: {e}") + except Exception as e: + print(f"Failed to fetch emojis: {e}") + + # Delete stickers + try: + stickers = await self.client.get_guild_stickers(self.community_id) + sticker_total = len(stickers) + for idx, sticker in enumerate(stickers): + try: + await self.client.delete_guild_sticker(self.community_id, sticker["id"]) + deleted += 1 + if progress_callback: + await progress_callback(sticker.get("name", "Unknown"), "Sticker", deleted, sticker_total) + except Exception as e: + print(f"Failed to delete sticker {sticker.get('name')}: {e}") + except Exception as e: + print(f"Failed to fetch stickers: {e}") + + return deleted + async def close(self): """Cleanly close connection and stop bot task.""" if self.bot: diff --git a/src/ui/app.py b/src/ui/app.py index 90fdec3..c4c74db 100644 --- a/src/ui/app.py +++ b/src/ui/app.py @@ -112,10 +112,11 @@ class MigrationCLI: val_status = "[bold green][VALID][/bold green]" if self.tokens_valid else "[bold red][INVALID][/bold red]" console.print(f"(6) Configuration {val_status}") + console.print("(7) [bold red]⚠ Danger Zone[/bold red]") console.print("(Q) Exit") - choice = Prompt.ask("Select an option", choices=["1", "2", "3", "4", "5", "6", "Q", "q"], default="Q").upper() + choice = Prompt.ask("Select an option", choices=["1", "2", "3", "4", "5", "6", "7", "Q", "q"], default="Q").upper() if choice == "1": await self.clone_server_template() @@ -129,6 +130,8 @@ class MigrationCLI: await self.migrate_message_history() elif choice == "6": await self.edit_configuration() + elif choice == "7": + await self.danger_zone() elif choice == "Q": console.print("[yellow]Exiting tool...[/yellow]") await self.engine.close_connections() @@ -557,6 +560,171 @@ class MigrationCLI: self.engine.is_running = False await self.engine.close_connections() + async def danger_zone(self): + """Danger Zone – irreversible destructive operations on the Fluxer community.""" + console.print("") + console.print(Panel.fit( + "[bold red]⚠ DANGER ZONE ⚠[/bold red]\n" + "[yellow]These actions are PERMANENT and IRREVERSIBLE on your Fluxer community.[/yellow]\n" + "[yellow]Always double-check before confirming.[/yellow]", + style="bold red" + )) + console.print("(1) Remove Community Logo and Banner") + console.print("(2) Delete all Channels & Categories") + console.print("(3) Reset Channel & Category Permissions") + console.print("(4) Delete all Roles [dim](bot role is protected)[/dim]") + console.print("(5) Delete all custom Emojis & Stickers") + console.print("(B) Back") + + choice = Prompt.ask( + "Select a Danger Zone option", + choices=["1", "2", "3", "4", "5", "B", "b"], + default="B" + ).upper() + + if choice == "B": + return + + # ---- (1) Remove Logo & Banner ---- + if choice == "1": + console.print("") + console.print("[bold red]This will REMOVE the community logo and banner image.[/bold red]") + if not Confirm.ask("[bold red]Are you absolutely sure?[/bold red]"): + return + if not Confirm.ask("[bold red]Last chance – confirm permanent removal?[/bold red]"): + return + try: + await self.engine.start_connections() + with console.status("[red]Removing logo and banner...[/red]"): + await self.engine.danger_remove_logo_and_banner() + console.print("[bold green]Community logo and banner removed.[/bold green]") + except Exception as e: + console.print(f"[bold red]Error: {e}[/bold red]") + finally: + await self.engine.close_connections() + + # ---- (2) Delete all Channels & Categories ---- + elif choice == "2": + console.print("") + console.print("[bold red]This will DELETE every channel and category in the Fluxer community.[/bold red]") + if not Confirm.ask("[bold red]Are you absolutely sure?[/bold red]"): + return + if not Confirm.ask("[bold red]Last chance – this cannot be undone. Continue?[/bold red]"): + return + try: + await self.engine.start_connections() + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + BarColumn(), + TaskProgressColumn(), + console=console + ) as progress: + del_task = progress.add_task("[red]Deleting channels...", total=100) + + async def on_channel_deleted(name: str, current: int, total: int): + progress.update(del_task, total=total, completed=current, + description=f"[red]Deleting: {name}") + + count = await self.engine.danger_delete_all_channels(progress_callback=on_channel_deleted) + console.print(f"[bold green]Done. {count} channels/categories deleted.[/bold green]") + except Exception as e: + console.print(f"[bold red]Error: {e}[/bold red]") + finally: + await self.engine.close_connections() + + # ---- (3) Reset Channel & Category Permissions ---- + elif choice == "3": + console.print("") + console.print("[bold red]This will RESET all permission overwrites on every channel and category.[/bold red]") + if not Confirm.ask("[bold red]Are you absolutely sure?[/bold red]"): + return + if not Confirm.ask("[bold red]Last chance – all custom permission overrides will be wiped. Continue?[/bold red]"): + return + try: + await self.engine.start_connections() + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + BarColumn(), + TaskProgressColumn(), + console=console + ) as progress: + perm_task = progress.add_task("[red]Resetting permissions...", total=100) + + async def on_perm_reset(name: str, current: int, total: int): + progress.update(perm_task, total=total, completed=current, + description=f"[red]Resetting: {name}") + + count = await self.engine.danger_reset_channel_permissions(progress_callback=on_perm_reset) + console.print(f"[bold green]Done. Permissions reset on {count} channels/categories.[/bold green]") + except Exception as e: + console.print(f"[bold red]Error: {e}[/bold red]") + finally: + await self.engine.close_connections() + + # ---- (4) Delete all Roles ---- + elif choice == "4": + console.print("") + console.print("[bold red]This will DELETE all roles in the Fluxer community.[/bold red]") + console.print("[dim]Managed roles (including the bot's own role) and @everyone are automatically protected.[/dim]") + if not Confirm.ask("[bold red]Are you absolutely sure?[/bold red]"): + return + if not Confirm.ask("[bold red]Last chance – confirm permanent role deletion?[/bold red]"): + return + try: + await self.engine.start_connections() + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + BarColumn(), + TaskProgressColumn(), + console=console + ) as progress: + role_task = progress.add_task("[red]Deleting roles...", total=100) + + async def on_role_deleted(name: str, current: int, total: int): + progress.update(role_task, total=total, completed=current, + description=f"[red]Deleting role: {name}") + + count = await self.engine.danger_delete_all_roles(progress_callback=on_role_deleted) + console.print(f"[bold green]Done. {count} roles deleted.[/bold green]") + except Exception as e: + console.print(f"[bold red]Error: {e}[/bold red]") + finally: + await self.engine.close_connections() + + # ---- (5) Delete all Emojis & Stickers ---- + elif choice == "5": + console.print("") + console.print("[bold red]This will DELETE all custom emojis and stickers in the Fluxer community.[/bold red]") + if not Confirm.ask("[bold red]Are you absolutely sure?[/bold red]"): + return + if not Confirm.ask("[bold red]Last chance – this cannot be undone. Continue?[/bold red]"): + return + try: + await self.engine.start_connections() + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + BarColumn(), + TaskProgressColumn(), + console=console + ) as progress: + asset_task = progress.add_task("[red]Deleting assets...", total=100) + + async def on_asset_deleted(name: str, asset_type: str, current: int, total: int): + progress.update(asset_task, total=total, completed=current, + description=f"[red]Deleting {asset_type}: {name}") + + count = await self.engine.danger_delete_all_emojis_and_stickers(progress_callback=on_asset_deleted) + console.print(f"[bold green]Done. {count} emojis/stickers deleted.[/bold green]") + except Exception as e: + console.print(f"[bold red]Error: {e}[/bold red]") + finally: + await self.engine.close_connections() + + async def run_cli(): cli = MigrationCLI() await cli.run()