add danger zone menu
This commit is contained in:
parent
8740d5db45
commit
7762922d4c
4 changed files with 345 additions and 15 deletions
28
README.md
28
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 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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## 🛠️ Getting Started
|
## Getting Started
|
||||||
|
|
||||||
### 1. Prerequisites
|
### 1. Prerequisites
|
||||||
Make sure you have Python installed on your computer.
|
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:
|
Open your terminal and run:
|
||||||
```bash
|
```bash
|
||||||
pip install -r requirements.txt
|
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!
|
### 4. Run it!
|
||||||
Start the tool by running:
|
Start the tool by running:
|
||||||
```bash
|
```bash
|
||||||
python fluxer-reaper.py
|
python fluxer-reaper.py
|
||||||
```
|
```
|
||||||
|
|
||||||
## ✨ Features
|
|
||||||
|
## Features
|
||||||
* **Clone Server Structure**: Automatically creates all your Discord categories and channels in Fluxer.
|
* **Clone Server Structure**: Automatically creates all your Discord categories and channels in Fluxer.
|
||||||
* **Copy Roles**: Copies your roles and their basic permissions.
|
* **Copy Roles**: Copies your roles and their basic permissions.
|
||||||
* **Copy Emojis & Stickers**: Copies your custom emojis and stickers over.
|
* **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.
|
* **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.
|
* **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).
|
|
||||||
|
|
|
||||||
|
|
@ -293,3 +293,25 @@ class MigrationEngine:
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self.is_running = False
|
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)
|
||||||
|
|
|
||||||
|
|
@ -256,6 +256,146 @@ class FluxerWriter:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Failed to update community metadata: {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):
|
async def close(self):
|
||||||
"""Cleanly close connection and stop bot task."""
|
"""Cleanly close connection and stop bot task."""
|
||||||
if self.bot:
|
if self.bot:
|
||||||
|
|
|
||||||
170
src/ui/app.py
170
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]"
|
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(f"(6) Configuration {val_status}")
|
||||||
|
console.print("(7) [bold red]⚠ Danger Zone[/bold red]")
|
||||||
|
|
||||||
console.print("(Q) Exit")
|
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":
|
if choice == "1":
|
||||||
await self.clone_server_template()
|
await self.clone_server_template()
|
||||||
|
|
@ -129,6 +130,8 @@ class MigrationCLI:
|
||||||
await self.migrate_message_history()
|
await self.migrate_message_history()
|
||||||
elif choice == "6":
|
elif choice == "6":
|
||||||
await self.edit_configuration()
|
await self.edit_configuration()
|
||||||
|
elif choice == "7":
|
||||||
|
await self.danger_zone()
|
||||||
elif choice == "Q":
|
elif choice == "Q":
|
||||||
console.print("[yellow]Exiting tool...[/yellow]")
|
console.print("[yellow]Exiting tool...[/yellow]")
|
||||||
await self.engine.close_connections()
|
await self.engine.close_connections()
|
||||||
|
|
@ -557,6 +560,171 @@ class MigrationCLI:
|
||||||
self.engine.is_running = False
|
self.engine.is_running = False
|
||||||
await self.engine.close_connections()
|
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():
|
async def run_cli():
|
||||||
cli = MigrationCLI()
|
cli = MigrationCLI()
|
||||||
await cli.run()
|
await cli.run()
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue