preserve reply structure
This commit is contained in:
parent
7762922d4c
commit
7442f3c0a0
5 changed files with 210 additions and 110 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -20,6 +20,7 @@ wheels/
|
|||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
temp.txt
|
||||
|
||||
# Virtual Environment
|
||||
venv/
|
||||
|
|
|
|||
|
|
@ -55,10 +55,18 @@ class MigrationEngine:
|
|||
await self.discord_reader.start()
|
||||
await self.fluxer_writer.start()
|
||||
|
||||
async def start_fluxer_only(self):
|
||||
"""Starts only the Fluxer writer (used for Danger Zone operations that don't need Discord)."""
|
||||
await self.fluxer_writer.start()
|
||||
|
||||
async def close_connections(self):
|
||||
await self.discord_reader.close()
|
||||
await self.fluxer_writer.close()
|
||||
|
||||
async def close_fluxer_only(self):
|
||||
"""Closes only the Fluxer writer. Pair with start_fluxer_only()."""
|
||||
await self.fluxer_writer.close()
|
||||
|
||||
async def sync_server_metadata(self, progress_callback: Callable[[str, str], Awaitable[None]], components: List[str] = ["name", "icon", "banner"]):
|
||||
"""Syncs the server name, logo and banner."""
|
||||
metadata = await self.discord_reader.get_server_metadata()
|
||||
|
|
@ -193,15 +201,24 @@ class MigrationEngine:
|
|||
logger.error(f"Failed to download attachment {att.filename}: {e}")
|
||||
|
||||
try:
|
||||
await self.fluxer_writer.send_message(
|
||||
# Check if this message is a reply
|
||||
reply_to_fluxer_id = None
|
||||
if msg.reference and msg.reference.message_id:
|
||||
reply_to_fluxer_id = self.state.get_fluxer_message_id(str(msg.reference.message_id))
|
||||
|
||||
fluxer_msg_id = await self.fluxer_writer.send_message(
|
||||
channel_id=target_channel_id,
|
||||
author_name=msg.author.name,
|
||||
author_name=msg.author.display_name,
|
||||
author_avatar_url=str(msg.author.display_avatar.url),
|
||||
content=msg.content,
|
||||
timestamp=msg.created_at.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
files=files if files else None
|
||||
files=files if files else None,
|
||||
reply_to_message_id=reply_to_fluxer_id
|
||||
)
|
||||
|
||||
if fluxer_msg_id:
|
||||
self.state.set_message_mapping(str(msg.id), fluxer_msg_id)
|
||||
|
||||
self.state.update_last_message_timestamp(str(source_channel_id), str(msg.created_at))
|
||||
message_count += 1
|
||||
if progress_callback:
|
||||
|
|
@ -236,8 +253,12 @@ class MigrationEngine:
|
|||
if progress_callback: await progress_callback(role.name, idx + 1, total)
|
||||
await asyncio.sleep(self.config.migration.rate_limit_delay_seconds)
|
||||
|
||||
async def migrate_emojis(self, progress_callback: Callable[[str, str, int, int], Awaitable[None]] | None = None, types_to_include: List[str] = ["Emoji", "Sticker"]):
|
||||
"""Copies custom emojis and stickers."""
|
||||
async def migrate_emojis(self, progress_callback: Callable[[str, str, int, int], Awaitable[None]] | None = None, types_to_include: List[str] = ["Emoji", "Sticker"], force: bool = False):
|
||||
"""Copies custom emojis and stickers.
|
||||
|
||||
Args:
|
||||
force: If True, skip state cache and re-copy even if already migrated.
|
||||
"""
|
||||
objs = []
|
||||
if "Emoji" in types_to_include:
|
||||
emojis = await self.discord_reader.get_emojis()
|
||||
|
|
@ -252,7 +273,7 @@ class MigrationEngine:
|
|||
if not self.is_running: break
|
||||
|
||||
state_key = f"{obj_type.lower()}_{obj.id}"
|
||||
fluxer_id = self.state.get_fluxer_channel_id(state_key)
|
||||
fluxer_id = None if force else self.state.get_fluxer_channel_id(state_key)
|
||||
if not fluxer_id:
|
||||
try:
|
||||
if obj_type == "Emoji":
|
||||
|
|
@ -296,9 +317,9 @@ class MigrationEngine:
|
|||
|
||||
# ──────────────── 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_remove_logo_and_banner(self) -> dict:
|
||||
"""Removes the community logo and banner image. Returns per-field status."""
|
||||
return 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."""
|
||||
|
|
@ -312,6 +333,7 @@ class MigrationEngine:
|
|||
"""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."""
|
||||
async def danger_delete_all_emojis_and_stickers(self, progress_callback=None) -> dict:
|
||||
"""Deletes all custom emojis and stickers. Returns {"emojis": int, "stickers": int}."""
|
||||
return await self.fluxer_writer.delete_all_emojis_and_stickers(progress_callback=progress_callback)
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ class MigrationState:
|
|||
self.channel_map: Dict[str, str] = {}
|
||||
self.role_map: Dict[str, str] = {}
|
||||
self.user_map: Dict[str, str] = {}
|
||||
self.message_map: Dict[str, str] = {}
|
||||
|
||||
# tracking last message timestamp per channel to resume
|
||||
self.last_message_timestamps: Dict[str, str] = {}
|
||||
|
|
@ -24,6 +25,7 @@ class MigrationState:
|
|||
self.channel_map = data.get("channels", {})
|
||||
self.role_map = data.get("roles", {})
|
||||
self.user_map = data.get("users", {})
|
||||
self.message_map = data.get("messages", {})
|
||||
self.last_message_timestamps = data.get("last_message_timestamps", {})
|
||||
|
||||
def save(self):
|
||||
|
|
@ -31,6 +33,7 @@ class MigrationState:
|
|||
"channels": self.channel_map,
|
||||
"roles": self.role_map,
|
||||
"users": self.user_map,
|
||||
"messages": self.message_map,
|
||||
"last_message_timestamps": self.last_message_timestamps
|
||||
}
|
||||
with open(self.state_file, "w", encoding="utf-8") as f:
|
||||
|
|
@ -42,6 +45,13 @@ class MigrationState:
|
|||
|
||||
def get_fluxer_channel_id(self, discord_id: str) -> str | None:
|
||||
return self.channel_map.get(str(discord_id))
|
||||
|
||||
def set_message_mapping(self, discord_id: str, fluxer_id: str):
|
||||
self.message_map[str(discord_id)] = str(fluxer_id)
|
||||
self.save()
|
||||
|
||||
def get_fluxer_message_id(self, discord_id: str) -> str | None:
|
||||
return self.message_map.get(str(discord_id))
|
||||
|
||||
def update_last_message_timestamp(self, channel_id: str, timestamp: str):
|
||||
self.last_message_timestamps[str(channel_id)] = timestamp
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import asyncio
|
||||
import logging
|
||||
from typing import Optional, List, Dict, Any
|
||||
from fluxer import Bot, Webhook
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class FluxerWriter:
|
||||
def __init__(self, token: str, community_id: str):
|
||||
self.token = token
|
||||
|
|
@ -121,10 +124,11 @@ class FluxerWriter:
|
|||
assert self.client is not None
|
||||
return await self.client.get_guild_channels(self.community_id)
|
||||
|
||||
async def send_message(self, channel_id: str, author_name: str, content: str, timestamp: str, author_avatar_url: Optional[str] = None, files: Optional[List[Dict[str, Any]]] = None) -> None:
|
||||
async def send_message(self, channel_id: str, author_name: str, content: str, timestamp: str, author_avatar_url: Optional[str] = None, files: Optional[List[Dict[str, Any]]] = None, reply_to_message_id: Optional[str] = None) -> Optional[str]:
|
||||
"""
|
||||
Sends a message to the target channel.
|
||||
Uses a webhook to mimic the original author if possible.
|
||||
Returns the ID of the sent message if available.
|
||||
"""
|
||||
assert self.client is not None
|
||||
|
||||
|
|
@ -138,38 +142,45 @@ class FluxerWriter:
|
|||
# Use webhook for avatar/username spoofing
|
||||
webhook = await self._get_or_create_webhook(channel_id)
|
||||
|
||||
# Use webhook for avatar/username spoofing
|
||||
webhook = await self._get_or_create_webhook(channel_id)
|
||||
|
||||
# Prepare content with timestamp (crucial for migration)
|
||||
# We still add the timestamp to the message body so it's searchable and preserved
|
||||
prefix = f"**[{timestamp}]**:\n"
|
||||
# Prepare content with subtext timestamp
|
||||
# -# is Fluxer/Discord's subtext markdown: small, muted grey text
|
||||
prefix = f"-# {timestamp}\n"
|
||||
final_content = prefix + content if content else prefix
|
||||
|
||||
try:
|
||||
# Current limitation: fluxer.py execute_webhook doesn't support 'files' yet.
|
||||
# So if we have files, we MUST use the bot's direct send method.
|
||||
if webhook and not files:
|
||||
await webhook.send(
|
||||
# Current limitation: fluxer.py execute_webhook doesn't support 'files' or 'message_reference' yet.
|
||||
# So if we have files OR a reply, we MUST use the bot's direct send method.
|
||||
if webhook and not files and not reply_to_message_id:
|
||||
msg = await webhook.send(
|
||||
content=final_content,
|
||||
username=f"{author_name} (via Discord)",
|
||||
avatar_url=author_avatar_url
|
||||
avatar_url=author_avatar_url,
|
||||
wait=True
|
||||
)
|
||||
return str(msg.id) if msg else None
|
||||
else:
|
||||
# Use bot direct message (supports files)
|
||||
# Use bot direct message (supports files and message_reference)
|
||||
# We add the author name to the prefix since bot name won't match
|
||||
bot_prefix = f"**[{timestamp}] {author_name}**:\n"
|
||||
bot_prefix = f"-# {timestamp} · {author_name}\n"
|
||||
bot_content = bot_prefix + content if content else bot_prefix
|
||||
await self.client.send_message(
|
||||
|
||||
message_reference = None
|
||||
if reply_to_message_id:
|
||||
message_reference = {"message_id": str(reply_to_message_id), "channel_id": str(channel_id)}
|
||||
|
||||
msg_data = await self.client.send_message(
|
||||
channel_id=channel_id,
|
||||
content=bot_content,
|
||||
files=files
|
||||
files=files,
|
||||
message_reference=message_reference
|
||||
)
|
||||
return str(msg_data["id"]) if msg_data else None
|
||||
except Exception as e:
|
||||
err_msg = f"Failed to copy message: {e}"
|
||||
if hasattr(e, 'errors') and e.errors:
|
||||
err_msg += f" - Details: {e.errors}"
|
||||
print(err_msg)
|
||||
return None
|
||||
|
||||
|
||||
async def create_role(self, name: str, color: int, hoist: bool, mentionable: bool) -> str:
|
||||
|
|
@ -206,7 +217,7 @@ class FluxerWriter:
|
|||
)
|
||||
return str(emoji["id"])
|
||||
except Exception as e:
|
||||
print(f"Failed to copy emoji {name}: {e}")
|
||||
logger.error(f"Failed to copy emoji '{name}': {e}", exc_info=True)
|
||||
return ""
|
||||
|
||||
async def create_sticker(self, name: str, image_bytes: bytes) -> str:
|
||||
|
|
@ -223,7 +234,7 @@ class FluxerWriter:
|
|||
)
|
||||
return str(sticker["id"])
|
||||
except Exception as e:
|
||||
print(f"Failed to copy sticker {name}: {e}")
|
||||
logger.error(f"Failed to copy sticker '{name}': {e}", exc_info=True)
|
||||
return ""
|
||||
|
||||
async def update_guild_metadata(self, name: Optional[str] = None, icon: Optional[bytes] = None, banner: Optional[bytes] = None) -> None:
|
||||
|
|
@ -256,19 +267,50 @@ class FluxerWriter:
|
|||
except Exception as e:
|
||||
print(f"Failed to update community metadata: {e}")
|
||||
|
||||
async def remove_community_logo_and_banner(self) -> None:
|
||||
async def remove_community_logo_and_banner(self) -> dict:
|
||||
"""
|
||||
Removes the community logo (icon) and banner by setting them to None.
|
||||
Removes the community logo (icon) and banner.
|
||||
Fetches the current guild state first so it can report whether each
|
||||
field was actually set (REMOVED) or already empty (SKIP).
|
||||
|
||||
Correct API calls per Fluxer contract:
|
||||
await http.modify_guild(guild_id, icon=None)
|
||||
await http.modify_guild(guild_id, banner=None)
|
||||
|
||||
Returns:
|
||||
{"icon": "REMOVED"|"SKIP", "banner": "REMOVED"|"SKIP"}
|
||||
"""
|
||||
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}")
|
||||
|
||||
# 1. Check current state
|
||||
guild = await self.client.get_guild(self.community_id)
|
||||
has_icon = bool(guild.get("icon"))
|
||||
has_banner = bool(guild.get("banner"))
|
||||
|
||||
# 2. Remove icon if set
|
||||
if has_icon:
|
||||
try:
|
||||
await self.client.modify_guild(
|
||||
guild_id=self.community_id,
|
||||
icon=None
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Failed to remove community icon: {e}")
|
||||
|
||||
# 3. Remove banner if set
|
||||
if has_banner:
|
||||
try:
|
||||
await self.client.modify_guild(
|
||||
guild_id=self.community_id,
|
||||
banner=None
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Failed to remove community banner: {e}")
|
||||
|
||||
return {
|
||||
"icon": "REMOVED" if has_icon else "SKIP",
|
||||
"banner": "REMOVED" if has_banner else "SKIP",
|
||||
}
|
||||
|
||||
async def delete_all_channels(self, progress_callback=None) -> int:
|
||||
"""
|
||||
|
|
@ -356,24 +398,25 @@ class FluxerWriter:
|
|||
print(f"Failed to delete role {role.get('name')}: {e}")
|
||||
return deleted
|
||||
|
||||
async def delete_all_emojis_and_stickers(self, progress_callback=None) -> int:
|
||||
async def delete_all_emojis_and_stickers(self, progress_callback=None) -> dict:
|
||||
"""
|
||||
Deletes all custom emojis and stickers in the Fluxer community.
|
||||
Returns the total count of deleted items.
|
||||
Returns {"emojis": int, "stickers": int} with independent counts.
|
||||
"""
|
||||
assert self.client is not None
|
||||
deleted = 0
|
||||
emoji_deleted = 0
|
||||
sticker_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):
|
||||
for emoji in emojis:
|
||||
try:
|
||||
await self.client.delete_guild_emoji(self.community_id, emoji["id"])
|
||||
deleted += 1
|
||||
emoji_deleted += 1
|
||||
if progress_callback:
|
||||
await progress_callback(emoji.get("name", "Unknown"), "Emoji", deleted, emoji_total)
|
||||
await progress_callback(emoji.get("name", "Unknown"), "Emoji", emoji_deleted, emoji_total)
|
||||
except Exception as e:
|
||||
print(f"Failed to delete emoji {emoji.get('name')}: {e}")
|
||||
except Exception as e:
|
||||
|
|
@ -383,18 +426,19 @@ class FluxerWriter:
|
|||
try:
|
||||
stickers = await self.client.get_guild_stickers(self.community_id)
|
||||
sticker_total = len(stickers)
|
||||
for idx, sticker in enumerate(stickers):
|
||||
for sticker in stickers:
|
||||
try:
|
||||
await self.client.delete_guild_sticker(self.community_id, sticker["id"])
|
||||
deleted += 1
|
||||
sticker_deleted += 1
|
||||
if progress_callback:
|
||||
await progress_callback(sticker.get("name", "Unknown"), "Sticker", deleted, sticker_total)
|
||||
await progress_callback(sticker.get("name", "Unknown"), "Sticker", 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
|
||||
return {"emojis": emoji_deleted, "stickers": sticker_deleted}
|
||||
|
||||
|
||||
async def close(self):
|
||||
"""Cleanly close connection and stop bot task."""
|
||||
|
|
|
|||
145
src/ui/app.py
145
src/ui/app.py
|
|
@ -348,22 +348,36 @@ class MigrationCLI:
|
|||
await self.engine.start_connections()
|
||||
emojis = await self.engine.discord_reader.get_emojis()
|
||||
stickers = await self.engine.discord_reader.get_stickers()
|
||||
|
||||
|
||||
console.print(f"\n[bold]Custom emojis found: {len(emojis)}[/bold]")
|
||||
for e in emojis:
|
||||
console.print(f" - Emoji: {e.name}")
|
||||
|
||||
already = self.engine.state.get_fluxer_channel_id(f"emoji_{e.id}")
|
||||
tag = " [dim](already copied)[/dim]" if already else ""
|
||||
console.print(f" - Emoji: {e.name}{tag}")
|
||||
|
||||
console.print(f"[bold]Custom stickers found: {len(stickers)}[/bold]")
|
||||
for s in stickers:
|
||||
console.print(f" - Sticker: {s.name}")
|
||||
|
||||
already = self.engine.state.get_fluxer_channel_id(f"sticker_{s.id}")
|
||||
tag = " [dim](already copied)[/dim]" if already else ""
|
||||
console.print(f" - Sticker: {s.name}{tag}")
|
||||
|
||||
# Warn if everything is already in the state cache
|
||||
all_ids = ([f"emoji_{e.id}" for e in emojis] +
|
||||
[f"sticker_{s.id}" for s in stickers])
|
||||
cached_count = sum(
|
||||
1 for k in all_ids if self.engine.state.get_fluxer_channel_id(k)
|
||||
)
|
||||
if cached_count > 0:
|
||||
console.print(f"\n[yellow]\u26a0 {cached_count}/{len(all_ids)} item(s) marked as already copied in state.json.[/yellow]")
|
||||
console.print("[yellow] If the target community was reset, choose Force Re-copy.[/yellow]")
|
||||
|
||||
console.print("\n(1) Copy Emojis only")
|
||||
console.print("(2) Copy Stickers only")
|
||||
console.print("(3) Copy Emojis and Stickers")
|
||||
console.print("(B) Back")
|
||||
|
||||
|
||||
choice = Prompt.ask("Select an option", choices=["1", "2", "3", "B", "b"], default="B").upper()
|
||||
|
||||
|
||||
if choice == "B":
|
||||
return
|
||||
|
||||
|
|
@ -375,6 +389,23 @@ class MigrationCLI:
|
|||
elif choice == "3":
|
||||
types_to_include = ["Emoji", "Sticker"]
|
||||
|
||||
# Ask about force re-copy only if there are cached items in scope
|
||||
in_scope_keys = []
|
||||
if "Emoji" in types_to_include:
|
||||
in_scope_keys += [f"emoji_{e.id}" for e in emojis]
|
||||
if "Sticker" in types_to_include:
|
||||
in_scope_keys += [f"sticker_{s.id}" for s in stickers]
|
||||
cached_in_scope = sum(
|
||||
1 for k in in_scope_keys if self.engine.state.get_fluxer_channel_id(k)
|
||||
)
|
||||
|
||||
force = False
|
||||
if cached_in_scope > 0:
|
||||
force = Confirm.ask(
|
||||
f"[yellow]{cached_in_scope} item(s) already in state cache. Force re-copy anyway?[/yellow]",
|
||||
default=False
|
||||
)
|
||||
|
||||
console.print("\n[bold green]Starting Migration...[/bold green]")
|
||||
with Progress(
|
||||
SpinnerColumn(),
|
||||
|
|
@ -383,21 +414,26 @@ class MigrationCLI:
|
|||
TaskProgressColumn(),
|
||||
console=console
|
||||
) as progress:
|
||||
|
||||
|
||||
emoji_task = progress.add_task("[cyan]Copying Assets...", total=100)
|
||||
|
||||
|
||||
async def update_progress(item_name: str, item_type: str, current: int, total: int):
|
||||
progress.update(emoji_task, total=total, completed=current, description=f"[cyan]Copying {item_type}: {item_name}")
|
||||
|
||||
self.engine.is_running = True
|
||||
await self.engine.migrate_emojis(progress_callback=update_progress, types_to_include=types_to_include)
|
||||
|
||||
await self.engine.migrate_emojis(
|
||||
progress_callback=update_progress,
|
||||
types_to_include=types_to_include,
|
||||
force=force
|
||||
)
|
||||
|
||||
console.print("[bold green]Migration complete![/bold green]")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
console.print(f"[bold red]Error during emoji migration: {str(e)}[/bold red]")
|
||||
finally:
|
||||
await self.engine.close_connections()
|
||||
|
||||
self.engine.is_running = False
|
||||
|
||||
async def sync_server_metadata(self):
|
||||
|
|
@ -564,55 +600,36 @@ class MigrationCLI:
|
|||
"""Danger Zone – irreversible destructive operations on the Fluxer community."""
|
||||
console.print("")
|
||||
console.print(Panel.fit(
|
||||
"[bold red]⚠ DANGER ZONE ⚠[/bold red]\n"
|
||||
"[bold red]\u26a0 DANGER ZONE \u26a0[/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("(1) Delete all Channels & Categories")
|
||||
console.print("(2) Reset Channel & Category Permissions")
|
||||
console.print("(3) Delete all Roles [dim](bot role is protected)[/dim]")
|
||||
console.print("(4) 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"],
|
||||
choices=["1", "2", "3", "4", "B", "b"],
|
||||
default="B"
|
||||
).upper()
|
||||
|
||||
if choice == "B":
|
||||
return
|
||||
|
||||
# ---- (1) Remove Logo & Banner ----
|
||||
# ---- (1) Delete all Channels & Categories ----
|
||||
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]"):
|
||||
if not Confirm.ask("[bold red]Last chance \u2013 this cannot be undone. Continue?[/bold red]"):
|
||||
return
|
||||
try:
|
||||
await self.engine.start_connections()
|
||||
await self.engine.start_fluxer_only()
|
||||
with Progress(
|
||||
SpinnerColumn(),
|
||||
TextColumn("[progress.description]{task.description}"),
|
||||
|
|
@ -627,22 +644,23 @@ class MigrationCLI:
|
|||
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]")
|
||||
console.print(f"[bold green]{count} channels/categories deleted.[/bold green]")
|
||||
console.print("[bold green]Done.[/bold green]")
|
||||
except Exception as e:
|
||||
console.print(f"[bold red]Error: {e}[/bold red]")
|
||||
finally:
|
||||
await self.engine.close_connections()
|
||||
await self.engine.close_fluxer_only()
|
||||
|
||||
# ---- (3) Reset Channel & Category Permissions ----
|
||||
elif choice == "3":
|
||||
# ---- (2) Reset Channel & Category Permissions ----
|
||||
elif choice == "2":
|
||||
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]"):
|
||||
if not Confirm.ask("[bold red]Last chance \u2013 all custom permission overrides will be wiped. Continue?[/bold red]"):
|
||||
return
|
||||
try:
|
||||
await self.engine.start_connections()
|
||||
await self.engine.start_fluxer_only()
|
||||
with Progress(
|
||||
SpinnerColumn(),
|
||||
TextColumn("[progress.description]{task.description}"),
|
||||
|
|
@ -657,23 +675,24 @@ class MigrationCLI:
|
|||
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]")
|
||||
console.print(f"[bold green]Permissions reset on {count} channels/categories.[/bold green]")
|
||||
console.print("[bold green]Done.[/bold green]")
|
||||
except Exception as e:
|
||||
console.print(f"[bold red]Error: {e}[/bold red]")
|
||||
finally:
|
||||
await self.engine.close_connections()
|
||||
await self.engine.close_fluxer_only()
|
||||
|
||||
# ---- (4) Delete all Roles ----
|
||||
elif choice == "4":
|
||||
# ---- (3) Delete all Roles ----
|
||||
elif choice == "3":
|
||||
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]"):
|
||||
if not Confirm.ask("[bold red]Last chance \u2013 confirm permanent role deletion?[/bold red]"):
|
||||
return
|
||||
try:
|
||||
await self.engine.start_connections()
|
||||
await self.engine.start_fluxer_only()
|
||||
with Progress(
|
||||
SpinnerColumn(),
|
||||
TextColumn("[progress.description]{task.description}"),
|
||||
|
|
@ -688,22 +707,23 @@ class MigrationCLI:
|
|||
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]")
|
||||
console.print(f"[bold green]{count} roles deleted.[/bold green]")
|
||||
console.print("[bold green]Done.[/bold green]")
|
||||
except Exception as e:
|
||||
console.print(f"[bold red]Error: {e}[/bold red]")
|
||||
finally:
|
||||
await self.engine.close_connections()
|
||||
await self.engine.close_fluxer_only()
|
||||
|
||||
# ---- (5) Delete all Emojis & Stickers ----
|
||||
elif choice == "5":
|
||||
# ---- (4) Delete all Emojis & Stickers ----
|
||||
elif choice == "4":
|
||||
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]"):
|
||||
if not Confirm.ask("[bold red]Last chance \u2013 this cannot be undone. Continue?[/bold red]"):
|
||||
return
|
||||
try:
|
||||
await self.engine.start_connections()
|
||||
await self.engine.start_fluxer_only()
|
||||
with Progress(
|
||||
SpinnerColumn(),
|
||||
TextColumn("[progress.description]{task.description}"),
|
||||
|
|
@ -717,12 +737,15 @@ class MigrationCLI:
|
|||
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]")
|
||||
counts = await self.engine.danger_delete_all_emojis_and_stickers(progress_callback=on_asset_deleted)
|
||||
console.print(f"[bold green]{counts.get('emojis', 0)} emojis deleted.[/bold green]")
|
||||
console.print(f"[bold green]{counts.get('stickers', 0)} stickers deleted.[/bold green]")
|
||||
console.print("[bold green]Done.[/bold green]")
|
||||
except Exception as e:
|
||||
console.print(f"[bold red]Error: {e}[/bold red]")
|
||||
finally:
|
||||
await self.engine.close_connections()
|
||||
await self.engine.close_fluxer_only()
|
||||
|
||||
|
||||
|
||||
async def run_cli():
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue