fix back button behavior & improve logging info
This commit is contained in:
parent
c7adc61a4a
commit
eb029ac656
14 changed files with 283 additions and 242 deletions
|
|
@ -31,11 +31,15 @@ class MigrationContext:
|
||||||
messages_file: str | Path = ""
|
messages_file: str | Path = ""
|
||||||
|
|
||||||
if server_id != "unconfigured":
|
if server_id != "unconfigured":
|
||||||
|
logger.info(f"Probing for existing migration folder for Server ID: {server_id}")
|
||||||
for d in Path(".").iterdir():
|
for d in Path(".").iterdir():
|
||||||
if d.is_dir() and d.name.endswith(f"-{server_id}"):
|
if d.is_dir() and d.name.endswith(f"-{server_id}"):
|
||||||
|
logger.info(f"Targeting existing folder: {d.name}")
|
||||||
state_file = d / "state-migration.json"
|
state_file = d / "state-migration.json"
|
||||||
messages_file = d / "message-tracker.json"
|
messages_file = d / "message-tracker.json"
|
||||||
break
|
break
|
||||||
|
else:
|
||||||
|
logger.info(f"No existing folder found for {server_id}. A new one will be created upon first state change.")
|
||||||
|
|
||||||
self.state = MigrationState(
|
self.state = MigrationState(
|
||||||
state_file=state_file,
|
state_file=state_file,
|
||||||
|
|
|
||||||
|
|
@ -179,6 +179,7 @@ class DiscordReader:
|
||||||
channel = await self.get_channel(channel_id)
|
channel = await self.get_channel(channel_id)
|
||||||
if isinstance(channel, discord.TextChannel) or isinstance(channel, discord.Thread):
|
if isinstance(channel, discord.TextChannel) or isinstance(channel, discord.Thread):
|
||||||
after = discord.Object(id=after_id) if after_id else None
|
after = discord.Object(id=after_id) if after_id else None
|
||||||
|
logger.info(f"Fetching message history for {channel.name} ({channel.id}) oldest_first=True after={after_id}")
|
||||||
# To avoid exploding RAM, we yield items one by one
|
# To avoid exploding RAM, we yield items one by one
|
||||||
async for message in channel.history(limit=limit, oldest_first=True, after=after):
|
async for message in channel.history(limit=limit, oldest_first=True, after=after):
|
||||||
yield message
|
yield message
|
||||||
|
|
|
||||||
|
|
@ -45,27 +45,33 @@ class MigrationState:
|
||||||
|
|
||||||
# 2. Load separate messages file
|
# 2. Load separate messages file
|
||||||
if self.messages_file and self.messages_file.exists():
|
if self.messages_file and self.messages_file.exists():
|
||||||
with open(self.messages_file, "r", encoding="utf-8") as f:
|
logger.info(f"Loading messages from {self.messages_file.name}")
|
||||||
msg_data = json.load(f)
|
try:
|
||||||
|
with open(self.messages_file, "r", encoding="utf-8") as f:
|
||||||
|
msg_data = json.load(f)
|
||||||
|
|
||||||
# Check for new schema (nested under 'channels')
|
# Check for new schema (nested under 'channels')
|
||||||
if "channels" in msg_data:
|
if "channels" in msg_data:
|
||||||
self.channel_messages = msg_data.get("channels", {})
|
self.channel_messages = msg_data.get("channels", {})
|
||||||
else:
|
logger.debug(f"Loaded {len(self.channel_messages)} tracked channels.")
|
||||||
# Legacy schema detection & conversion to a default 'unknown_channel' just in case,
|
else:
|
||||||
# though new migrations shouldn't hit this based on previous removals.
|
logger.warning("Legacy schema or empty tracker detected in messages file.")
|
||||||
legacy_map = msg_data.get("messages", {})
|
# Legacy schema detection & conversion to a default 'unknown_channel' just in case,
|
||||||
legacy_ids = msg_data.get("last_message_ids", {})
|
# though new migrations shouldn't hit this based on previous removals.
|
||||||
legacy_times = msg_data.get("last_message_timestamps", {})
|
legacy_map = msg_data.get("messages", {})
|
||||||
|
legacy_ids = msg_data.get("last_message_ids", {})
|
||||||
|
legacy_times = msg_data.get("last_message_timestamps", {})
|
||||||
|
|
||||||
if legacy_map or legacy_ids or legacy_times:
|
if legacy_map or legacy_ids or legacy_times:
|
||||||
self.channel_messages = {
|
self.channel_messages = {
|
||||||
"legacy_migrated_channel": {
|
"legacy_migrated_channel": {
|
||||||
"message_map": legacy_map,
|
"message_map": legacy_map,
|
||||||
"last_message_id": list(legacy_ids.values())[-1] if legacy_ids else "",
|
"last_message_id": list(legacy_ids.values())[-1] if legacy_ids else "",
|
||||||
"last_message_timestamp": list(legacy_times.values())[-1] if legacy_times else ""
|
"last_message_timestamp": list(legacy_times.values())[-1] if legacy_times else ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to load messages file: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -73,6 +79,8 @@ class MigrationState:
|
||||||
"""Saves only the core server configuration (channels, roles, emojis)."""
|
"""Saves only the core server configuration (channels, roles, emojis)."""
|
||||||
if not self.state_file:
|
if not self.state_file:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
logger.debug(f"Saving state to {self.state_file.name}")
|
||||||
data = {
|
data = {
|
||||||
"channels": self.channel_map,
|
"channels": self.channel_map,
|
||||||
"categories": self.category_map,
|
"categories": self.category_map,
|
||||||
|
|
@ -81,18 +89,26 @@ class MigrationState:
|
||||||
"stickers": self.sticker_map,
|
"stickers": self.sticker_map,
|
||||||
"audit_log_channel": self.audit_log_channel
|
"audit_log_channel": self.audit_log_channel
|
||||||
}
|
}
|
||||||
with open(self.state_file, "w", encoding="utf-8") as f:
|
try:
|
||||||
json.dump(data, f, indent=4)
|
with open(self.state_file, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(data, f, indent=4)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to save state file: {e}")
|
||||||
|
|
||||||
def save_messages(self):
|
def save_messages(self):
|
||||||
"""Saves only the message tracking data."""
|
"""Saves only the message tracking data."""
|
||||||
if not self.messages_file:
|
if not self.messages_file:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
logger.debug(f"Saving messages to {self.messages_file.name}")
|
||||||
data = {
|
data = {
|
||||||
"channels": self.channel_messages
|
"channels": self.channel_messages
|
||||||
}
|
}
|
||||||
with open(self.messages_file, "w", encoding="utf-8") as f:
|
try:
|
||||||
json.dump(data, f, indent=4)
|
with open(self.messages_file, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(data, f, indent=4)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to save messages file: {e}")
|
||||||
|
|
||||||
# --- Type Specific Getters/Setters ---
|
# --- Type Specific Getters/Setters ---
|
||||||
|
|
||||||
|
|
@ -260,11 +276,13 @@ class MigrationState:
|
||||||
def set_folder(self, server_id: str, clean_name: str, base_dir: Path | str = ""):
|
def set_folder(self, server_id: str, clean_name: str, base_dir: Path | str = ""):
|
||||||
base = Path(base_dir) if base_dir else Path(".")
|
base = Path(base_dir) if base_dir else Path(".")
|
||||||
new_folder = base / f"{clean_name}-{server_id}"
|
new_folder = base / f"{clean_name}-{server_id}"
|
||||||
|
logger.info(f"Setting active migration folder: {new_folder}")
|
||||||
|
|
||||||
# If we have an existing folder that is different, rename it
|
# If we have an existing folder that is different, rename it
|
||||||
if self.state_file and self.state_file.parent.exists() and self.state_file.parent != new_folder:
|
if self.state_file and self.state_file.parent.exists() and self.state_file.parent != new_folder:
|
||||||
# Check if it's actually in a server-specific folder (not roots)
|
# Check if it's actually in a server-specific folder (not roots)
|
||||||
if self.state_file.parent.name.endswith(f"-{server_id}"):
|
if self.state_file.parent.name.endswith(f"-{server_id}"):
|
||||||
|
logger.info(f"Renaming active folder from {self.state_file.parent.name} to {new_folder.name}")
|
||||||
try:
|
try:
|
||||||
self.state_file.parent.rename(new_folder)
|
self.state_file.parent.rename(new_folder)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -275,4 +293,5 @@ class MigrationState:
|
||||||
self.state_file = new_folder / "state-migration.json"
|
self.state_file = new_folder / "state-migration.json"
|
||||||
self.messages_file = new_folder / "message-tracker.json"
|
self.messages_file = new_folder / "message-tracker.json"
|
||||||
|
|
||||||
|
logger.debug("Re-loading data from new folder location.")
|
||||||
self.load()
|
self.load()
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ async def sync_assets_state(context: MigrationContext):
|
||||||
"""
|
"""
|
||||||
Scans Fluxer for emojis and stickers matching Discord names and updates state file mappings.
|
Scans Fluxer for emojis and stickers matching Discord names and updates state file mappings.
|
||||||
"""
|
"""
|
||||||
|
logger.info("Synchronizing asset mappings (emojis/stickers) with Fluxer...")
|
||||||
discord_emojis = await context.discord_reader.get_emojis()
|
discord_emojis = await context.discord_reader.get_emojis()
|
||||||
discord_stickers = await context.discord_reader.get_stickers()
|
discord_stickers = await context.discord_reader.get_stickers()
|
||||||
|
|
||||||
|
|
@ -80,14 +81,16 @@ async def migrate_emojis(context: MigrationContext, progress_callback: Callable[
|
||||||
total = len(objs)
|
total = len(objs)
|
||||||
cloned_assets: dict[str, dict[str, str]] = {"Emoji": {}, "Sticker": {}}
|
cloned_assets: dict[str, dict[str, str]] = {"Emoji": {}, "Sticker": {}}
|
||||||
|
|
||||||
if total == 0:
|
logger.info(f"Migrating {total} assets to Fluxer (Types: {', '.join(types_to_include)})...")
|
||||||
return cloned_assets
|
|
||||||
|
|
||||||
for idx, (obj, obj_type) in enumerate(objs):
|
for idx, (obj, obj_type) in enumerate(objs):
|
||||||
if not context.is_running: break
|
if not context.is_running:
|
||||||
|
logger.warning("Asset migration interrupted.")
|
||||||
|
break
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if obj_type == "Emoji":
|
if obj_type == "Emoji":
|
||||||
|
logger.debug(f"Migrating emoji: {obj.name}")
|
||||||
img_data = await context.discord_reader.download_emoji(obj)
|
img_data = await context.discord_reader.download_emoji(obj)
|
||||||
fluxer_id = await context.fluxer_writer.create_emoji(
|
fluxer_id = await context.fluxer_writer.create_emoji(
|
||||||
name=obj.name,
|
name=obj.name,
|
||||||
|
|
@ -97,6 +100,7 @@ async def migrate_emojis(context: MigrationContext, progress_callback: Callable[
|
||||||
context.state.set_emoji_mapping(str(obj.id), fluxer_id)
|
context.state.set_emoji_mapping(str(obj.id), fluxer_id)
|
||||||
cloned_assets["Emoji"][obj.name] = fluxer_id
|
cloned_assets["Emoji"][obj.name] = fluxer_id
|
||||||
else:
|
else:
|
||||||
|
logger.debug(f"Migrating sticker: {obj.name}")
|
||||||
img_data = await context.discord_reader.download_sticker(obj)
|
img_data = await context.discord_reader.download_sticker(obj)
|
||||||
fluxer_id = await context.fluxer_writer.create_sticker(
|
fluxer_id = await context.fluxer_writer.create_sticker(
|
||||||
name=obj.name,
|
name=obj.name,
|
||||||
|
|
|
||||||
|
|
@ -99,16 +99,16 @@ async def analyze_migration(context: MigrationContext, source_channel_id: int, a
|
||||||
|
|
||||||
async def migrate_messages(context: MigrationContext, source_channel_id: int, target_channel_id: str, after_message_id: int | None = None, progress_callback: Callable[[Dict[str, Any]], Awaitable[None]] | None = None) -> Dict[str, Any]:
|
async def migrate_messages(context: MigrationContext, source_channel_id: int, target_channel_id: str, after_message_id: int | None = None, progress_callback: Callable[[Dict[str, Any]], Awaitable[None]] | None = None) -> Dict[str, Any]:
|
||||||
"""Migrate messages for a specific channel and returns detailed statistics."""
|
"""Migrate messages for a specific channel and returns detailed statistics."""
|
||||||
stats = {
|
stats = {"messages": 0, "threads": 0, "attachments": 0}
|
||||||
"messages": 0,
|
|
||||||
"attachments": 0,
|
logger.info(f"Starting message migration: Discord #{source_channel_id} -> Fluxer #{target_channel_id}")
|
||||||
"threads": 0,
|
if after_message_id:
|
||||||
"first_message_url": None,
|
logger.info(f"Resuming migration from after message ID: {after_message_id}")
|
||||||
"last_message_url": None
|
|
||||||
}
|
|
||||||
try:
|
try:
|
||||||
async for msg in context.discord_reader.fetch_message_history(source_channel_id, after_id=after_message_id):
|
async for msg in context.discord_reader.fetch_message_history(source_channel_id, after_id=after_message_id):
|
||||||
if not context.is_running:
|
if not context.is_running:
|
||||||
|
logger.warning("Migration interrupted by user (is_running=False)")
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -190,6 +190,10 @@ async def migrate_messages(context: MigrationContext, source_channel_id: int, ta
|
||||||
reply_to_fluxer_id = None
|
reply_to_fluxer_id = None
|
||||||
if msg.reference and msg.reference.message_id:
|
if msg.reference and msg.reference.message_id:
|
||||||
reply_to_fluxer_id = context.state.get_fluxer_message_id(target_channel_id, str(msg.reference.message_id))
|
reply_to_fluxer_id = context.state.get_fluxer_message_id(target_channel_id, str(msg.reference.message_id))
|
||||||
|
if reply_to_fluxer_id:
|
||||||
|
logger.debug(f"Detected reply to Discord ID {msg.reference.message_id} -> Fluxer ID {reply_to_fluxer_id}")
|
||||||
|
else:
|
||||||
|
logger.debug(f"Reply target Discord ID {msg.reference.message_id} not found in current session map.")
|
||||||
|
|
||||||
fluxer_msg_id = await context.fluxer_writer.send_message(
|
fluxer_msg_id = await context.fluxer_writer.send_message(
|
||||||
channel_id=target_channel_id,
|
channel_id=target_channel_id,
|
||||||
|
|
@ -210,6 +214,10 @@ async def migrate_messages(context: MigrationContext, source_channel_id: int, ta
|
||||||
stats["messages"] += 1
|
stats["messages"] += 1
|
||||||
context.state.increment_stats(target_channel_id, messages=1, files=len(files) if files else 0)
|
context.state.increment_stats(target_channel_id, messages=1, files=len(files) if files else 0)
|
||||||
|
|
||||||
|
# Periodic log
|
||||||
|
if stats["messages"] % 50 == 0:
|
||||||
|
logger.info(f"Progress: Migrated {stats['messages']}/{total_to_process} messages in this channel.")
|
||||||
|
|
||||||
# Check for associated thread (Normal case: parent message is migrated)
|
# Check for associated thread (Normal case: parent message is migrated)
|
||||||
if hasattr(msg, 'thread') and msg.thread:
|
if hasattr(msg, 'thread') and msg.thread:
|
||||||
thread = msg.thread
|
thread = msg.thread
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ async def sync_roles_state(context: MigrationContext):
|
||||||
"""
|
"""
|
||||||
Scans Fluxer for roles matching Discord names and updates state file mappings.
|
Scans Fluxer for roles matching Discord names and updates state file mappings.
|
||||||
"""
|
"""
|
||||||
|
logger.info("Synchronizing role mappings with Fluxer...")
|
||||||
discord_roles = await context.discord_reader.get_roles()
|
discord_roles = await context.discord_reader.get_roles()
|
||||||
fluxer_roles = await context.fluxer_writer.client.get_guild_roles(context.config.fluxer_community_id)
|
fluxer_roles = await context.fluxer_writer.client.get_guild_roles(context.config.fluxer_community_id)
|
||||||
|
|
||||||
|
|
@ -39,6 +40,7 @@ async def sync_roles_state(context: MigrationContext):
|
||||||
|
|
||||||
async def sync_permissions(context: MigrationContext, progress_callback: Callable[[str, int, int], Awaitable[None]] | None = None) -> dict:
|
async def sync_permissions(context: MigrationContext, progress_callback: Callable[[str, int, int], Awaitable[None]] | None = None) -> dict:
|
||||||
"""Syncs category and channel role overrides/permissions."""
|
"""Syncs category and channel role overrides/permissions."""
|
||||||
|
logger.info("Starting permissions synchronization...")
|
||||||
categories = await context.discord_reader.get_categories()
|
categories = await context.discord_reader.get_categories()
|
||||||
channels = await context.discord_reader.get_channels()
|
channels = await context.discord_reader.get_channels()
|
||||||
|
|
||||||
|
|
@ -56,8 +58,11 @@ async def sync_permissions(context: MigrationContext, progress_callback: Callabl
|
||||||
current_idx = 0
|
current_idx = 0
|
||||||
|
|
||||||
if total == 0:
|
if total == 0:
|
||||||
|
logger.info("No mapped categories or channels found for permission sync.")
|
||||||
return synced_info
|
return synced_info
|
||||||
|
|
||||||
|
logger.info(f"Syncing permissions for {len(categories)} categories and {len(channels)} channels.")
|
||||||
|
|
||||||
async def _sync_overwrites(discord_item, fluxer_id):
|
async def _sync_overwrites(discord_item, fluxer_id):
|
||||||
"""Helper to sync role overwrites for a given channel or category."""
|
"""Helper to sync role overwrites for a given channel or category."""
|
||||||
for target, overwrite in discord_item.overwrites.items():
|
for target, overwrite in discord_item.overwrites.items():
|
||||||
|
|
@ -120,6 +125,7 @@ async def sync_permissions(context: MigrationContext, progress_callback: Callabl
|
||||||
current_idx += 1
|
current_idx += 1
|
||||||
if progress_callback: await progress_callback(channel.name, current_idx, total)
|
if progress_callback: await progress_callback(channel.name, current_idx, total)
|
||||||
|
|
||||||
|
logger.info(f"Permissions sync complete: {len(synced_info['categories_synced'])} categories, {len(synced_info['channels_synced'])} channels.")
|
||||||
return synced_info
|
return synced_info
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -133,12 +139,14 @@ async def migrate_roles(context: MigrationContext, progress_callback: Callable[[
|
||||||
total = len(roles)
|
total = len(roles)
|
||||||
cloned_role_names = []
|
cloned_role_names = []
|
||||||
|
|
||||||
if total == 0:
|
logger.info(f"Migrating {total} roles to Fluxer...")
|
||||||
return cloned_role_names
|
|
||||||
|
|
||||||
for idx, role in enumerate(roles):
|
for idx, role in enumerate(roles):
|
||||||
if not context.is_running: break
|
if not context.is_running:
|
||||||
|
logger.warning("Role migration interrupted.")
|
||||||
|
break
|
||||||
|
|
||||||
|
logger.debug(f"Creating role: {role.name}")
|
||||||
fluxer_id = await context.fluxer_writer.create_role(
|
fluxer_id = await context.fluxer_writer.create_role(
|
||||||
name=role.name,
|
name=role.name,
|
||||||
color=role.color.value,
|
color=role.color.value,
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,12 @@ async def sync_server_metadata(context: MigrationContext, progress_callback: Cal
|
||||||
if "name" in components:
|
if "name" in components:
|
||||||
try:
|
try:
|
||||||
name = metadata.get("name")
|
name = metadata.get("name")
|
||||||
|
logger.info(f"Syncing server name: {name}")
|
||||||
await context.writer.update_guild_metadata(name=name)
|
await context.writer.update_guild_metadata(name=name)
|
||||||
cloned_data["name"] = name
|
cloned_data["name"] = name
|
||||||
await progress_callback("Server Name", "DONE")
|
await progress_callback("Server Name", "DONE")
|
||||||
except Exception:
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to sync server name: {e}")
|
||||||
await progress_callback("Server Name", "ERROR")
|
await progress_callback("Server Name", "ERROR")
|
||||||
|
|
||||||
# 2. Sync Icon
|
# 2. Sync Icon
|
||||||
|
|
@ -28,12 +30,15 @@ async def sync_server_metadata(context: MigrationContext, progress_callback: Cal
|
||||||
icon_bytes = await context.discord_reader.download_asset(context.discord_reader.guild.icon)
|
icon_bytes = await context.discord_reader.download_asset(context.discord_reader.guild.icon)
|
||||||
|
|
||||||
if icon_bytes:
|
if icon_bytes:
|
||||||
|
logger.info(f"Uploading server icon ({len(icon_bytes)} bytes)...")
|
||||||
await context.writer.update_guild_metadata(icon=icon_bytes)
|
await context.writer.update_guild_metadata(icon=icon_bytes)
|
||||||
cloned_data["icon"] = icon_bytes
|
cloned_data["icon"] = icon_bytes
|
||||||
await progress_callback("Server Icon", "DONE")
|
await progress_callback("Server Icon", "DONE")
|
||||||
else:
|
else:
|
||||||
|
logger.info("No server icon found to sync.")
|
||||||
await progress_callback("Server Icon", "SKIP")
|
await progress_callback("Server Icon", "SKIP")
|
||||||
except Exception:
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to sync server icon: {e}")
|
||||||
await progress_callback("Server Icon", "ERROR")
|
await progress_callback("Server Icon", "ERROR")
|
||||||
|
|
||||||
# 3. Sync Banner
|
# 3. Sync Banner
|
||||||
|
|
@ -44,12 +49,15 @@ async def sync_server_metadata(context: MigrationContext, progress_callback: Cal
|
||||||
banner_bytes = await context.discord_reader.download_asset(context.discord_reader.guild.banner)
|
banner_bytes = await context.discord_reader.download_asset(context.discord_reader.guild.banner)
|
||||||
|
|
||||||
if banner_bytes:
|
if banner_bytes:
|
||||||
|
logger.info(f"Uploading server banner ({len(banner_bytes)} bytes)...")
|
||||||
await context.writer.update_guild_metadata(banner=banner_bytes)
|
await context.writer.update_guild_metadata(banner=banner_bytes)
|
||||||
cloned_data["banner"] = banner_bytes
|
cloned_data["banner"] = banner_bytes
|
||||||
await progress_callback("Server Banner", "DONE")
|
await progress_callback("Server Banner", "DONE")
|
||||||
else:
|
else:
|
||||||
|
logger.info("No server banner found to sync.")
|
||||||
await progress_callback("Server Banner", "SKIP")
|
await progress_callback("Server Banner", "SKIP")
|
||||||
except Exception:
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to sync server banner: {e}")
|
||||||
await progress_callback("Server Banner", "ERROR")
|
await progress_callback("Server Banner", "ERROR")
|
||||||
|
|
||||||
return cloned_data
|
return cloned_data
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ async def sync_assets_state(context: MigrationContext):
|
||||||
"""
|
"""
|
||||||
Scans Stoat for emojis matching Discord names and updates state file mappings.
|
Scans Stoat for emojis matching Discord names and updates state file mappings.
|
||||||
"""
|
"""
|
||||||
|
logger.info("Synchronizing asset mappings (emojis) with Stoat...")
|
||||||
discord_emojis = await context.discord_reader.get_emojis()
|
discord_emojis = await context.discord_reader.get_emojis()
|
||||||
# Stickers not supported on Stoat based on current library investigation
|
# Stickers not supported on Stoat based on current library investigation
|
||||||
|
|
||||||
|
|
@ -74,11 +75,16 @@ async def migrate_emojis(context: MigrationContext, progress_callback: Callable[
|
||||||
if total == 0:
|
if total == 0:
|
||||||
return cloned_assets
|
return cloned_assets
|
||||||
|
|
||||||
|
logger.info(f"Migrating {total} assets to Stoat (Types: {', '.join(types_to_include)})...")
|
||||||
|
|
||||||
for idx, (obj, obj_type) in enumerate(objs):
|
for idx, (obj, obj_type) in enumerate(objs):
|
||||||
if not context.is_running: break
|
if not context.is_running:
|
||||||
|
logger.warning("Stoat asset migration interrupted.")
|
||||||
|
break
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if obj_type == "Emoji":
|
if obj_type == "Emoji":
|
||||||
|
logger.debug(f"Migrating emoji to Stoat: {obj.name}")
|
||||||
img_data = await context.discord_reader.download_emoji(obj)
|
img_data = await context.discord_reader.download_emoji(obj)
|
||||||
stoat_id = await context.writer.create_emoji(
|
stoat_id = await context.writer.create_emoji(
|
||||||
name=obj.name,
|
name=obj.name,
|
||||||
|
|
|
||||||
|
|
@ -98,16 +98,16 @@ async def analyze_migration(context: MigrationContext, source_channel_id: int, a
|
||||||
|
|
||||||
async def migrate_messages(context: MigrationContext, source_channel_id: int, target_channel_id: str, after_message_id: int | None = None, progress_callback: Callable[[Dict[str, Any]], Awaitable[None]] | None = None) -> Dict[str, Any]:
|
async def migrate_messages(context: MigrationContext, source_channel_id: int, target_channel_id: str, after_message_id: int | None = None, progress_callback: Callable[[Dict[str, Any]], Awaitable[None]] | None = None) -> Dict[str, Any]:
|
||||||
"""Migrate messages for a specific channel using Stoat masquerade for author impersonation."""
|
"""Migrate messages for a specific channel using Stoat masquerade for author impersonation."""
|
||||||
stats = {
|
stats = {"messages": 0, "threads": 0, "attachments": 0}
|
||||||
"messages": 0,
|
|
||||||
"attachments": 0,
|
logger.info(f"Starting message migration: Discord #{source_channel_id} -> Stoat #{target_channel_id}")
|
||||||
"threads": 0,
|
if after_message_id:
|
||||||
"first_message_url": None,
|
logger.info(f"Resuming migration from after message ID: {after_message_id}")
|
||||||
"last_message_url": None
|
|
||||||
}
|
|
||||||
try:
|
try:
|
||||||
async for msg in context.discord_reader.fetch_message_history(source_channel_id, after_id=after_message_id):
|
async for msg in context.discord_reader.fetch_message_history(source_channel_id, after_id=after_message_id):
|
||||||
if not context.is_running:
|
if not context.is_running:
|
||||||
|
logger.warning("Migration interrupted by user (is_running=False)")
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -187,6 +187,10 @@ async def migrate_messages(context: MigrationContext, source_channel_id: int, ta
|
||||||
reply_to_stoat_id = None
|
reply_to_stoat_id = None
|
||||||
if msg.reference and msg.reference.message_id:
|
if msg.reference and msg.reference.message_id:
|
||||||
reply_to_stoat_id = context.state.get_target_message_id(target_channel_id, str(msg.reference.message_id))
|
reply_to_stoat_id = context.state.get_target_message_id(target_channel_id, str(msg.reference.message_id))
|
||||||
|
if reply_to_stoat_id:
|
||||||
|
logger.debug(f"Detected reply to Discord ID {msg.reference.message_id} -> Stoat ID {reply_to_stoat_id}")
|
||||||
|
else:
|
||||||
|
logger.debug(f"Reply target Discord ID {msg.reference.message_id} not found in current session map.")
|
||||||
|
|
||||||
stoat_msg_id = await context.stoat_writer.send_message(
|
stoat_msg_id = await context.stoat_writer.send_message(
|
||||||
channel_id=target_channel_id,
|
channel_id=target_channel_id,
|
||||||
|
|
@ -207,6 +211,10 @@ async def migrate_messages(context: MigrationContext, source_channel_id: int, ta
|
||||||
stats["messages"] += 1
|
stats["messages"] += 1
|
||||||
context.state.increment_stats(target_channel_id, messages=1, files=len(files) if files else 0)
|
context.state.increment_stats(target_channel_id, messages=1, files=len(files) if files else 0)
|
||||||
|
|
||||||
|
# Periodic log
|
||||||
|
if stats["messages"] % 50 == 0:
|
||||||
|
logger.info(f"Progress: Migrated {stats['messages']} messages in this channel.")
|
||||||
|
|
||||||
# Check for associated thread (Normal case: parent message is migrated)
|
# Check for associated thread (Normal case: parent message is migrated)
|
||||||
if hasattr(msg, 'thread') and msg.thread:
|
if hasattr(msg, 'thread') and msg.thread:
|
||||||
thread = msg.thread
|
thread = msg.thread
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ async def sync_roles_state(context: MigrationContext):
|
||||||
"""
|
"""
|
||||||
Scans Stoat for roles matching Discord names and updates state file mappings.
|
Scans Stoat for roles matching Discord names and updates state file mappings.
|
||||||
"""
|
"""
|
||||||
|
logger.info("Synchronizing role mappings with Stoat...")
|
||||||
discord_roles = await context.discord_reader.get_roles()
|
discord_roles = await context.discord_reader.get_roles()
|
||||||
server = await context.stoat_writer._get_server()
|
server = await context.stoat_writer._get_server()
|
||||||
stoat_roles = list(server.roles.values())
|
stoat_roles = list(server.roles.values())
|
||||||
|
|
@ -41,6 +42,7 @@ async def sync_roles_state(context: MigrationContext):
|
||||||
|
|
||||||
async def sync_permissions(context: MigrationContext, progress_callback: Callable[[str, int, int], Awaitable[None]] | None = None) -> dict:
|
async def sync_permissions(context: MigrationContext, progress_callback: Callable[[str, int, int], Awaitable[None]] | None = None) -> dict:
|
||||||
"""Syncs category and channel role overrides/permissions."""
|
"""Syncs category and channel role overrides/permissions."""
|
||||||
|
logger.info("Starting permissions synchronization for Stoat...")
|
||||||
discord_categories = await context.discord_reader.get_categories()
|
discord_categories = await context.discord_reader.get_categories()
|
||||||
discord_channels = await context.discord_reader.get_channels()
|
discord_channels = await context.discord_reader.get_channels()
|
||||||
|
|
||||||
|
|
@ -58,8 +60,11 @@ async def sync_permissions(context: MigrationContext, progress_callback: Callabl
|
||||||
current_idx = 0
|
current_idx = 0
|
||||||
|
|
||||||
if total == 0:
|
if total == 0:
|
||||||
|
logger.info("No mapped categories or channels found for Stoat permission sync.")
|
||||||
return synced_info
|
return synced_info
|
||||||
|
|
||||||
|
logger.info(f"Syncing permissions for {len(mapped_categories)} categories and {len(mapped_channels)} channels to Stoat.")
|
||||||
|
|
||||||
cat_map = {cat.id: cat for cat in discord_categories}
|
cat_map = {cat.id: cat for cat in discord_categories}
|
||||||
cat_names = {cat.id: cat.name for cat in discord_categories}
|
cat_names = {cat.id: cat.name for cat in discord_categories}
|
||||||
|
|
||||||
|
|
@ -160,6 +165,7 @@ async def sync_permissions(context: MigrationContext, progress_callback: Callabl
|
||||||
await asyncio.sleep(context.config.migration.rate_limit_delay_seconds)
|
await asyncio.sleep(context.config.migration.rate_limit_delay_seconds)
|
||||||
|
|
||||||
|
|
||||||
|
logger.info(f"Stoat permissions sync complete: {len(synced_info['categories_synced'])} categories, {len(synced_info['channels_synced'])} channels.")
|
||||||
return synced_info
|
return synced_info
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -176,20 +182,17 @@ async def migrate_roles(context: MigrationContext, progress_callback: Callable[[
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to sync default permissions: {e}")
|
logger.error(f"Failed to sync default permissions: {e}")
|
||||||
|
|
||||||
roles = await context.discord_reader.get_roles()
|
|
||||||
|
|
||||||
if not force:
|
|
||||||
roles = [r for r in roles if not context.state.get_target_role_id(str(r.id))]
|
|
||||||
|
|
||||||
total = len(roles)
|
total = len(roles)
|
||||||
cloned_role_names = []
|
cloned_role_names = []
|
||||||
|
|
||||||
if total == 0:
|
logger.info(f"Migrating {total} roles to Stoat...")
|
||||||
return cloned_role_names
|
|
||||||
|
|
||||||
for idx, role in enumerate(roles):
|
for idx, role in enumerate(roles):
|
||||||
if not context.is_running: break
|
if not context.is_running:
|
||||||
|
logger.warning("Stoat role migration interrupted.")
|
||||||
|
break
|
||||||
|
|
||||||
|
logger.debug(f"Creating role in Stoat: {role.name}")
|
||||||
stoat_id = await context.stoat_writer.create_role(
|
stoat_id = await context.stoat_writer.create_role(
|
||||||
name=role.name,
|
name=role.name,
|
||||||
color=role.color.value,
|
color=role.color.value,
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,12 @@ async def sync_server_metadata(context: MigrationContext, progress_callback: Cal
|
||||||
if "name" in components:
|
if "name" in components:
|
||||||
try:
|
try:
|
||||||
name = metadata.get("name")
|
name = metadata.get("name")
|
||||||
|
logger.info(f"Syncing server name to Stoat: {name}")
|
||||||
await context.writer.update_guild_metadata(name=name)
|
await context.writer.update_guild_metadata(name=name)
|
||||||
cloned_data["name"] = name
|
cloned_data["name"] = name
|
||||||
await progress_callback("Server Name", "DONE")
|
await progress_callback("Server Name", "DONE")
|
||||||
except Exception:
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to sync server name to Stoat: {e}")
|
||||||
await progress_callback("Server Name", "ERROR")
|
await progress_callback("Server Name", "ERROR")
|
||||||
|
|
||||||
# 2. Sync Icon
|
# 2. Sync Icon
|
||||||
|
|
@ -28,12 +30,15 @@ async def sync_server_metadata(context: MigrationContext, progress_callback: Cal
|
||||||
icon_bytes = await context.discord_reader.download_asset(context.discord_reader.guild.icon)
|
icon_bytes = await context.discord_reader.download_asset(context.discord_reader.guild.icon)
|
||||||
|
|
||||||
if icon_bytes:
|
if icon_bytes:
|
||||||
|
logger.info(f"Uploading server icon to Stoat ({len(icon_bytes)} bytes)...")
|
||||||
await context.writer.update_guild_metadata(icon=icon_bytes)
|
await context.writer.update_guild_metadata(icon=icon_bytes)
|
||||||
cloned_data["icon"] = icon_bytes
|
cloned_data["icon"] = icon_bytes
|
||||||
await progress_callback("Server Icon", "DONE")
|
await progress_callback("Server Icon", "DONE")
|
||||||
else:
|
else:
|
||||||
|
logger.debug("No server icon found to sync to Stoat.")
|
||||||
await progress_callback("Server Icon", "SKIP")
|
await progress_callback("Server Icon", "SKIP")
|
||||||
except Exception:
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to sync server icon to Stoat: {e}")
|
||||||
await progress_callback("Server Icon", "ERROR")
|
await progress_callback("Server Icon", "ERROR")
|
||||||
|
|
||||||
# 3. Sync Banner
|
# 3. Sync Banner
|
||||||
|
|
@ -44,12 +49,15 @@ async def sync_server_metadata(context: MigrationContext, progress_callback: Cal
|
||||||
banner_bytes = await context.discord_reader.download_asset(context.discord_reader.guild.banner)
|
banner_bytes = await context.discord_reader.download_asset(context.discord_reader.guild.banner)
|
||||||
|
|
||||||
if banner_bytes:
|
if banner_bytes:
|
||||||
|
logger.info(f"Uploading server banner to Stoat ({len(banner_bytes)} bytes)...")
|
||||||
await context.writer.update_guild_metadata(banner=banner_bytes)
|
await context.writer.update_guild_metadata(banner=banner_bytes)
|
||||||
cloned_data["banner"] = banner_bytes
|
cloned_data["banner"] = banner_bytes
|
||||||
await progress_callback("Server Banner", "DONE")
|
await progress_callback("Server Banner", "DONE")
|
||||||
else:
|
else:
|
||||||
|
logger.debug("No server banner found to sync to Stoat.")
|
||||||
await progress_callback("Server Banner", "SKIP")
|
await progress_callback("Server Banner", "SKIP")
|
||||||
except Exception:
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to sync server banner to Stoat: {e}")
|
||||||
await progress_callback("Server Banner", "ERROR")
|
await progress_callback("Server Banner", "ERROR")
|
||||||
|
|
||||||
return cloned_data
|
return cloned_data
|
||||||
|
|
|
||||||
|
|
@ -63,12 +63,12 @@ class BackupPane(Container):
|
||||||
|
|
||||||
# ── validation ────────────────────────────────────────────────────────
|
# ── validation ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@work(exclusive=True, thread=True)
|
@work(exclusive=True)
|
||||||
async def _validate(self) -> None:
|
async def _validate(self) -> None:
|
||||||
fillers = ["DISCORD_BOT_TOKEN", "000000000000000000", "DISCORD_SERVER_ID", "", None]
|
fillers = ["DISCORD_BOT_TOKEN", "000000000000000000", "DISCORD_SERVER_ID", "", None]
|
||||||
d_token = self.config.discord_bot_token
|
d_token = self.config.discord_bot_token
|
||||||
if d_token in fillers or self.config.discord_server_id in fillers:
|
if d_token in fillers or self.config.discord_server_id in fillers:
|
||||||
self.app.call_from_thread(self._update_ui, "[red]NOT CONFIGURED[/red]", "", "", False)
|
self._update_ui("[red]NOT CONFIGURED[/red]", "", "", False)
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
res = await self.engine.discord_reader.validate()
|
res = await self.engine.discord_reader.validate()
|
||||||
|
|
@ -83,9 +83,9 @@ class BackupPane(Container):
|
||||||
if info:
|
if info:
|
||||||
backup_text = f"Last backup: [cyan]{info}[/cyan]"
|
backup_text = f"Last backup: [cyan]{info}[/cyan]"
|
||||||
|
|
||||||
self.app.call_from_thread(self._update_ui, s_text, b_text, backup_text, valid)
|
self._update_ui(s_text, b_text, backup_text, valid)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.app.call_from_thread(self._update_ui, f"[red]Error: {e}[/red]", "", "", False)
|
self._update_ui(f"[red]Error: {e}[/red]", "", "", False)
|
||||||
|
|
||||||
def _get_backup_info(self) -> str | None:
|
def _get_backup_info(self) -> str | None:
|
||||||
profile_file = Path(f"Reaper-{self.cfg_name}") / "server_profile.json"
|
profile_file = Path(f"Reaper-{self.cfg_name}") / "server_profile.json"
|
||||||
|
|
@ -121,49 +121,49 @@ class BackupPane(Container):
|
||||||
|
|
||||||
# ── workers ───────────────────────────────────────────────────────────
|
# ── workers ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@work(exclusive=True, thread=True)
|
@work(exclusive=True)
|
||||||
async def run_backup_profile(self) -> None:
|
async def run_backup_profile(self) -> None:
|
||||||
modal = ProgressScreen()
|
modal = ProgressScreen()
|
||||||
self.app.call_from_thread(self.app.push_screen, modal)
|
self.app.push_screen(modal)
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
self.app.call_from_thread(modal.phase_progress)
|
modal.phase_progress()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.app.call_from_thread(modal.set_status, "Starting readers...")
|
modal.set_status("Starting readers...")
|
||||||
await self.engine.discord_reader.start()
|
await self.engine.discord_reader.start()
|
||||||
await self.exporter.setup()
|
await self.exporter.setup()
|
||||||
|
|
||||||
self.app.call_from_thread(modal.write, "[yellow]Backing up server profile & skeleton...[/yellow]")
|
modal.write("[yellow]Backing up server profile & skeleton...[/yellow]")
|
||||||
await self.exporter.export_metadata()
|
await self.exporter.export_metadata()
|
||||||
await self.exporter.download_server_assets()
|
await self.exporter.download_server_assets()
|
||||||
|
|
||||||
self.app.call_from_thread(modal.write, "Exporting structure...")
|
modal.write("Exporting structure...")
|
||||||
_, cat_count, chan_count = await self.exporter.export_channels_structure()
|
_, cat_count, chan_count = await self.exporter.export_channels_structure()
|
||||||
|
|
||||||
self.app.call_from_thread(modal.write, "Exporting assets...")
|
modal.write("Exporting assets...")
|
||||||
e_count, s_count = await self.exporter.export_assets()
|
e_count, s_count = await self.exporter.export_assets()
|
||||||
|
|
||||||
self.app.call_from_thread(modal.write, f"[bold green]Server Profile backed up to: {self.exporter.export_path}[/bold green]")
|
modal.write(f"[bold green]Server Profile backed up to: {self.exporter.export_path}[/bold green]")
|
||||||
self.app.call_from_thread(modal.write, f"- {e_count} emojis, {s_count} stickers.")
|
modal.write(f"- {e_count} emojis, {s_count} stickers.")
|
||||||
self.app.call_from_thread(modal.phase_report, "Profile Backup")
|
modal.phase_report("Profile Backup")
|
||||||
|
|
||||||
except discord.Forbidden as e:
|
except discord.Forbidden as e:
|
||||||
self.app.call_from_thread(modal.write, f"[bold red]Backup failed: {e}[/bold red]")
|
modal.write(f"[bold red]Backup failed: {e}[/bold red]")
|
||||||
self.app.call_from_thread(modal.phase_report, "Profile Backup", "error")
|
modal.phase_report("Profile Backup", "error")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.app.call_from_thread(modal.write, f"[bold red]Error: {e}[/bold red]")
|
modal.write(f"[bold red]Error: {e}[/bold red]")
|
||||||
self.app.call_from_thread(modal.phase_report, "Profile Backup", "error")
|
modal.phase_report("Profile Backup", "error")
|
||||||
finally:
|
finally:
|
||||||
await self.engine.close_connections()
|
await self.engine.close_connections()
|
||||||
|
|
||||||
@work(exclusive=True, thread=True)
|
@work(exclusive=True)
|
||||||
async def run_backup_messages(self) -> None:
|
async def run_backup_messages(self) -> None:
|
||||||
modal_prog = ProgressScreen()
|
modal_prog = ProgressScreen()
|
||||||
self.app.call_from_thread(self.app.push_screen, modal_prog)
|
self.app.push_screen(modal_prog)
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.app.call_from_thread(modal_prog.set_status, "Fetching channels...")
|
modal_prog.set_status("Fetching channels...")
|
||||||
await self.engine.discord_reader.start()
|
await self.engine.discord_reader.start()
|
||||||
await self.exporter.setup()
|
await self.exporter.setup()
|
||||||
|
|
||||||
|
|
@ -178,8 +178,8 @@ class BackupPane(Container):
|
||||||
]
|
]
|
||||||
|
|
||||||
if not eligible_channels:
|
if not eligible_channels:
|
||||||
self.app.call_from_thread(modal_prog.write, "[yellow]No text/news channels found to backup.[/yellow]")
|
modal_prog.write("[yellow]No text/news channels found to backup.[/yellow]")
|
||||||
self.app.call_from_thread(modal_prog.allow_close)
|
modal_prog.allow_close()
|
||||||
return
|
return
|
||||||
|
|
||||||
any_found = False
|
any_found = False
|
||||||
|
|
@ -189,7 +189,7 @@ class BackupPane(Container):
|
||||||
any_found = True
|
any_found = True
|
||||||
backed_up_ids.add(chan.id)
|
backed_up_ids.add(chan.id)
|
||||||
|
|
||||||
self.app.call_from_thread(self.app.pop_screen)
|
self.app.pop_screen()
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
loop = asyncio.get_running_loop()
|
loop = asyncio.get_running_loop()
|
||||||
|
|
@ -199,8 +199,7 @@ class BackupPane(Container):
|
||||||
if not future.done():
|
if not future.done():
|
||||||
future.set_result(reply)
|
future.set_result(reply)
|
||||||
|
|
||||||
self.app.call_from_thread(
|
self.app.push_screen(
|
||||||
self.app.push_screen,
|
|
||||||
ChannelSelectScreen(eligible_channels, cat_map, backed_up_ids, any_found),
|
ChannelSelectScreen(eligible_channels, cat_map, backed_up_ids, any_found),
|
||||||
check_channels,
|
check_channels,
|
||||||
)
|
)
|
||||||
|
|
@ -214,69 +213,69 @@ class BackupPane(Container):
|
||||||
selected_channels = [c for c in eligible_channels if c.id in selected_ids]
|
selected_channels = [c for c in eligible_channels if c.id in selected_ids]
|
||||||
|
|
||||||
# Phase 2: Confirmation
|
# Phase 2: Confirmation
|
||||||
self.app.call_from_thread(self.app.push_screen, modal_prog)
|
self.app.push_screen(modal_prog)
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
msg = "Sync existing backups" if not force_overwrite else "Overwriting existing backups"
|
msg = "Sync existing backups" if not force_overwrite else "Overwriting existing backups"
|
||||||
self.app.call_from_thread(modal_prog.set_status, f"Awaiting Confirmation to backup [bold]{len(selected_channels)}[/bold] channels...")
|
modal_prog.set_status(f"Awaiting Confirmation to backup [bold]{len(selected_channels)}[/bold] channels...")
|
||||||
self.app.call_from_thread(modal_prog.show_info, f"[cyan]{msg}[/cyan]", f"Targets: {', '.join([c.name for c in selected_channels[:3]])}{'...' if len(selected_channels) > 3 else ''}")
|
modal_prog.show_info(f"[cyan]{msg}[/cyan]", f"Targets: {', '.join([c.name for c in selected_channels[:3]])}{'...' if len(selected_channels) > 3 else ''}")
|
||||||
|
|
||||||
choice = await modal_prog.phase_wait_confirm()
|
choice = await modal_prog.phase_wait_confirm()
|
||||||
if choice == "btn_back":
|
if choice == "btn_back":
|
||||||
self.app.call_from_thread(modal_prog.dismiss)
|
modal_prog.dismiss()
|
||||||
continue
|
continue
|
||||||
elif choice == "btn_main_menu":
|
elif choice == "btn_main_menu":
|
||||||
self.app.call_from_thread(modal_prog.dismiss)
|
modal_prog.dismiss()
|
||||||
self.app.call_from_thread(self.app.switch_screen, "config_selection")
|
self.app.switch_screen("config_selection")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Proceed to progress
|
# Proceed to progress
|
||||||
break
|
break
|
||||||
|
|
||||||
self.app.call_from_thread(modal_prog.phase_progress)
|
modal_prog.phase_progress()
|
||||||
|
|
||||||
total_chans = len(selected_channels)
|
total_chans = len(selected_channels)
|
||||||
self.app.call_from_thread(modal_prog.set_status, "Backing up messages...")
|
modal_prog.set_status("Backing up messages...")
|
||||||
self.app.call_from_thread(modal_prog.write, f"[yellow]Starting backup for {total_chans} channels...[/yellow]")
|
modal_prog.write(f"[yellow]Starting backup for {total_chans} channels...[/yellow]")
|
||||||
|
|
||||||
for chan in selected_channels:
|
for chan in selected_channels:
|
||||||
backup_exists = (self.exporter.export_path / "message_backup" / f"{chan.id}.json").exists()
|
backup_exists = (self.exporter.export_path / "message_backup" / f"{chan.id}.json").exists()
|
||||||
is_sync = backup_exists and not force_overwrite
|
is_sync = backup_exists and not force_overwrite
|
||||||
|
|
||||||
label = "Syncing Backup" if is_sync else "Backing up"
|
label = "Syncing Backup" if is_sync else "Backing up"
|
||||||
self.app.call_from_thread(modal_prog.write, f"[cyan]{label}: {chan.name}[/cyan]")
|
modal_prog.write(f"[cyan]{label}: {chan.name}[/cyan]")
|
||||||
|
|
||||||
async def update_msg_count(name, count):
|
async def update_msg_count(name, count):
|
||||||
self.app.call_from_thread(modal_prog.set_status, f"{name}: {count} messages")
|
modal_prog.set_status(f"{name}: {count} messages")
|
||||||
|
|
||||||
await self.exporter.export_channel_messages(chan.id, progress_callback=update_msg_count, force=force_overwrite)
|
await self.exporter.export_channel_messages(chan.id, progress_callback=update_msg_count, force=force_overwrite)
|
||||||
await self.exporter.export_threads(chan.id, progress_callback=update_msg_count, force=force_overwrite)
|
await self.exporter.export_threads(chan.id, progress_callback=update_msg_count, force=force_overwrite)
|
||||||
|
|
||||||
self.app.call_from_thread(modal_prog.write, f"[green]Completed: {chan.name}[/green]")
|
modal_prog.write(f"[green]Completed: {chan.name}[/green]")
|
||||||
|
|
||||||
await self.exporter.export_metadata()
|
await self.exporter.export_metadata()
|
||||||
self.app.call_from_thread(modal_prog.write, "[bold green]Message backup complete![/bold green]")
|
modal_prog.write("[bold green]Message backup complete![/bold green]")
|
||||||
self.app.call_from_thread(modal_prog.phase_report, "Message Backup")
|
modal_prog.phase_report("Message Backup")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.app.call_from_thread(modal_prog.write, f"[bold red]Message backup failed: {e}[/bold red]")
|
modal_prog.write(f"[bold red]Message backup failed: {e}[/bold red]")
|
||||||
self.app.call_from_thread(modal_prog.phase_report, "Message Backup", "error")
|
modal_prog.phase_report("Message Backup", "error")
|
||||||
finally:
|
finally:
|
||||||
await self.engine.close_connections()
|
await self.engine.close_connections()
|
||||||
|
|
||||||
@work(exclusive=True, thread=True)
|
@work(exclusive=True)
|
||||||
async def run_backup_sync(self) -> None:
|
async def run_backup_sync(self) -> None:
|
||||||
modal_prog = ProgressScreen()
|
modal_prog = ProgressScreen()
|
||||||
self.app.call_from_thread(self.app.push_screen, modal_prog)
|
self.app.push_screen(modal_prog)
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
self.app.call_from_thread(modal_prog.phase_progress)
|
modal_prog.phase_progress()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.app.call_from_thread(modal_prog.set_status, "Starting sync...")
|
modal_prog.set_status("Starting sync...")
|
||||||
await self.engine.discord_reader.start()
|
await self.engine.discord_reader.start()
|
||||||
await self.exporter.setup()
|
await self.exporter.setup()
|
||||||
|
|
||||||
self.app.call_from_thread(modal_prog.write, "Updating structure...")
|
modal_prog.write("Updating structure...")
|
||||||
await self.exporter.export_metadata()
|
await self.exporter.export_metadata()
|
||||||
await self.exporter.download_server_assets()
|
await self.exporter.download_server_assets()
|
||||||
await self.exporter.export_channels_structure()
|
await self.exporter.export_channels_structure()
|
||||||
|
|
@ -294,25 +293,25 @@ class BackupPane(Container):
|
||||||
]
|
]
|
||||||
|
|
||||||
if not selected_channels:
|
if not selected_channels:
|
||||||
self.app.call_from_thread(modal_prog.write, "[yellow]No existing backups found to sync.[/yellow]")
|
modal_prog.write("[yellow]No existing backups found to sync.[/yellow]")
|
||||||
else:
|
else:
|
||||||
self.app.call_from_thread(modal_prog.write, f"[yellow]Syncing {len(selected_channels)} channels...[/yellow]")
|
modal_prog.write(f"[yellow]Syncing {len(selected_channels)} channels...[/yellow]")
|
||||||
for chan in selected_channels:
|
for chan in selected_channels:
|
||||||
self.app.call_from_thread(modal_prog.write, f"[cyan]Syncing: {chan.name}[/cyan]")
|
modal_prog.write(f"[cyan]Syncing: {chan.name}[/cyan]")
|
||||||
|
|
||||||
async def update_msg_count(name, count):
|
async def update_msg_count(name, count):
|
||||||
self.app.call_from_thread(modal_prog.set_status, f"{name}: {count} messages")
|
modal_prog.set_status(f"{name}: {count} messages")
|
||||||
|
|
||||||
await self.exporter.export_channel_messages(chan.id, progress_callback=update_msg_count, force=False)
|
await self.exporter.export_channel_messages(chan.id, progress_callback=update_msg_count, force=False)
|
||||||
await self.exporter.export_threads(chan.id, progress_callback=update_msg_count, force=False)
|
await self.exporter.export_threads(chan.id, progress_callback=update_msg_count, force=False)
|
||||||
self.app.call_from_thread(modal_prog.write, f"[green]Synced: {chan.name}[/green]")
|
modal_prog.write(f"[green]Synced: {chan.name}[/green]")
|
||||||
|
|
||||||
await self.exporter.export_metadata()
|
await self.exporter.export_metadata()
|
||||||
self.app.call_from_thread(modal_prog.write, "[bold green]Sync operation complete![/bold green]")
|
modal_prog.write("[bold green]Sync operation complete![/bold green]")
|
||||||
self.app.call_from_thread(modal_prog.phase_report, "Backup Sync")
|
modal_prog.phase_report("Backup Sync")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.app.call_from_thread(modal_prog.write, f"[bold red]Sync failed: {e}[/bold red]")
|
modal_prog.write(f"[bold red]Sync failed: {e}[/bold red]")
|
||||||
self.app.call_from_thread(modal_prog.phase_report, "Backup Sync", "error")
|
modal_prog.phase_report("Backup Sync", "error")
|
||||||
finally:
|
finally:
|
||||||
await self.engine.close_connections()
|
await self.engine.close_connections()
|
||||||
|
|
|
||||||
|
|
@ -365,17 +365,20 @@ class OptionSelectModal(ModalScreen[list[str]]):
|
||||||
OptionSelectModal { align: center middle; }
|
OptionSelectModal { align: center middle; }
|
||||||
#opt_dialog {
|
#opt_dialog {
|
||||||
width: 60;
|
width: 60;
|
||||||
height: auto;
|
height: 80%;
|
||||||
border: solid green;
|
border: solid green;
|
||||||
background: $surface;
|
background: $surface;
|
||||||
padding: 1 2;
|
padding: 1 2;
|
||||||
}
|
}
|
||||||
#opt_title { text-style: bold; margin-bottom: 1; text-align: center; width: 100%; }
|
#opt_title { text-style: bold; margin-bottom: 1; text-align: center; width: 100%; }
|
||||||
#opt_scroll { margin-bottom: 1; border: solid $primary; max-height: 12; padding: 1; }
|
#opt_scroll { margin-bottom: 1; height: 1fr; overflow-y: auto; }
|
||||||
#opt_buttons { height: auto; }
|
#opt_buttons { height: auto; }
|
||||||
#opt_buttons Button { width: 1fr; margin: 0 1; }
|
#opt_buttons Button { width: 1fr; margin: 0 1; }
|
||||||
#opt_batch_buttons { height: auto; margin-bottom: 1; }
|
#opt_batch_buttons { height: auto; margin-bottom: 1; }
|
||||||
#opt_batch_buttons Button { width: 1fr; margin: 0 1; }
|
#opt_batch_buttons Button { width: 1fr; margin: 0 1; }
|
||||||
|
RadioButton { background: transparent; }
|
||||||
|
RadioButton:focus { background: $accent 20%; }
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, title: str, options: list[tuple[str, str]]):
|
def __init__(self, title: str, options: list[tuple[str, str]]):
|
||||||
|
|
@ -391,7 +394,7 @@ class OptionSelectModal(ModalScreen[list[str]]):
|
||||||
yield Button("Select All", id="btn_opt_all")
|
yield Button("Select All", id="btn_opt_all")
|
||||||
yield Button("Deselect All", id="btn_opt_none")
|
yield Button("Deselect All", id="btn_opt_none")
|
||||||
|
|
||||||
with VerticalScroll(id="opt_scroll"):
|
with Vertical(id="opt_scroll"):
|
||||||
for opt_id, label in self._options:
|
for opt_id, label in self._options:
|
||||||
yield RadioButton(label, id=f"opt_{opt_id}")
|
yield RadioButton(label, id=f"opt_{opt_id}")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -319,6 +319,7 @@ class ShuttlePane(Container):
|
||||||
choice = await modal.phase_wait_confirm()
|
choice = await modal.phase_wait_confirm()
|
||||||
if choice == "btn_back":
|
if choice == "btn_back":
|
||||||
modal.dismiss()
|
modal.dismiss()
|
||||||
|
self._open_clone_menu()
|
||||||
return
|
return
|
||||||
elif choice == "btn_main_menu":
|
elif choice == "btn_main_menu":
|
||||||
modal.dismiss()
|
modal.dismiss()
|
||||||
|
|
@ -358,6 +359,7 @@ class ShuttlePane(Container):
|
||||||
choice = await modal.phase_wait_confirm()
|
choice = await modal.phase_wait_confirm()
|
||||||
if choice == "btn_back":
|
if choice == "btn_back":
|
||||||
modal.dismiss()
|
modal.dismiss()
|
||||||
|
self._open_sync_menu()
|
||||||
return
|
return
|
||||||
elif choice == "btn_main_menu":
|
elif choice == "btn_main_menu":
|
||||||
modal.dismiss()
|
modal.dismiss()
|
||||||
|
|
@ -575,12 +577,14 @@ class ShuttlePane(Container):
|
||||||
async def update_scan(current_stats):
|
async def update_scan(current_stats):
|
||||||
modal.set_status(f"[cyan]Scanned {current_stats['messages']} items...")
|
modal.set_status(f"[cyan]Scanned {current_stats['messages']} items...")
|
||||||
|
|
||||||
|
logger.info(f"Analyzing message history for Discord #{source_channel.name}...")
|
||||||
stats_analysis = await migrate_mod.analyze_migration(
|
stats_analysis = await migrate_mod.analyze_migration(
|
||||||
self.engine,
|
self.engine,
|
||||||
source_channel_id=source_channel.id,
|
source_channel_id=source_channel.id,
|
||||||
after_message_id=int(last_migrated) if last_migrated else None,
|
after_message_id=int(last_migrated) if last_migrated else None,
|
||||||
progress_callback=update_scan,
|
progress_callback=update_scan,
|
||||||
)
|
)
|
||||||
|
logger.info(f"Analysis complete: {stats_analysis['messages']} new messages found.")
|
||||||
self.engine.is_running = False
|
self.engine.is_running = False
|
||||||
|
|
||||||
# Set initial total stats for the confirmation block
|
# Set initial total stats for the confirmation block
|
||||||
|
|
@ -599,6 +603,7 @@ class ShuttlePane(Container):
|
||||||
|
|
||||||
# Phase 2: Confirmation
|
# Phase 2: Confirmation
|
||||||
choice = await modal.phase_wait_confirm(show_continue=has_previous)
|
choice = await modal.phase_wait_confirm(show_continue=has_previous)
|
||||||
|
logger.info(f"User confirmation choice: {choice}")
|
||||||
if choice == "btn_back":
|
if choice == "btn_back":
|
||||||
modal.dismiss()
|
modal.dismiss()
|
||||||
continue # Return to channel picker
|
continue # Return to channel picker
|
||||||
|
|
@ -611,11 +616,15 @@ class ShuttlePane(Container):
|
||||||
|
|
||||||
after_id = None
|
after_id = None
|
||||||
if choice == "btn_continue" and last_migrated:
|
if choice == "btn_continue" and last_migrated:
|
||||||
|
logger.info("Proceeding with 'Continue Migration' (incremental sink).")
|
||||||
after_id = int(last_migrated)
|
after_id = int(last_migrated)
|
||||||
elif choice == "btn_start_id":
|
elif choice == "btn_start_id":
|
||||||
# Fallback to full for now since we don't have an ID input dialog yet
|
# Fallback to full for now since we don't have an ID input dialog yet
|
||||||
modal.write("[yellow]Custom Message ID start not fully implemented, starting from beginning.[/yellow]")
|
modal.write("[yellow]Custom Message ID start not fully implemented, starting from beginning.[/yellow]")
|
||||||
|
logger.info("Proceeding with 'Start from ID' (fallback to begin).")
|
||||||
after_id = None
|
after_id = None
|
||||||
|
else:
|
||||||
|
logger.info("Proceeding with 'Start from First' (clean sink).")
|
||||||
|
|
||||||
# If we are here, we are proceeding with migration
|
# If we are here, we are proceeding with migration
|
||||||
break
|
break
|
||||||
|
|
@ -629,6 +638,7 @@ class ShuttlePane(Container):
|
||||||
total_threads = stats_analysis["threads"]
|
total_threads = stats_analysis["threads"]
|
||||||
total_attachments = stats_analysis["attachments"]
|
total_attachments = stats_analysis["attachments"]
|
||||||
|
|
||||||
|
logger.info(f"Execution started for #{source_channel.name} -> {platform_name} @ {target_channel.get('name')}")
|
||||||
self.engine.is_running = True
|
self.engine.is_running = True
|
||||||
|
|
||||||
async def update_msg(current_stats):
|
async def update_msg(current_stats):
|
||||||
|
|
@ -684,164 +694,116 @@ class ShuttlePane(Container):
|
||||||
|
|
||||||
def _open_danger_menu(self):
|
def _open_danger_menu(self):
|
||||||
options = [
|
options = [
|
||||||
("dz_del_channels", "Delete ALL Channels & Categories", "error"),
|
("dz_del_channels", "Delete ALL Channels & Categories"),
|
||||||
("dz_reset_perms", "Reset ALL Channel Permissions", "error"),
|
("dz_reset_perms", "Reset ALL Channel Permissions"),
|
||||||
("dz_del_roles", "Delete ALL Roles", "error"),
|
("dz_del_roles", "Delete ALL Roles"),
|
||||||
("dz_del_assets", "Delete ALL Emojis & Stickers", "error"),
|
("dz_del_assets", "Delete ALL Emojis & Stickers"),
|
||||||
]
|
]
|
||||||
def on_result(choice):
|
def on_result(selections: list[str] | None):
|
||||||
if choice == "dz_del_channels":
|
if selections:
|
||||||
self._confirm_danger("Delete ALL channels and categories? This is IRREVERSIBLE.", self.run_dz_delete_channels)
|
self.run_batch_danger(selections)
|
||||||
elif choice == "dz_reset_perms":
|
self.app.push_screen(OptionSelectModal("⚠ DANGER ZONE ⚠", options), on_result)
|
||||||
self._confirm_danger("Reset ALL channel permissions? This is IRREVERSIBLE.", self.run_dz_reset_perms)
|
|
||||||
elif choice == "dz_del_roles":
|
|
||||||
self._confirm_danger("Delete ALL roles? This is IRREVERSIBLE.", self.run_dz_delete_roles)
|
|
||||||
elif choice == "dz_del_assets":
|
|
||||||
self._confirm_danger("Delete ALL emojis and stickers? This is IRREVERSIBLE.", self.run_dz_delete_assets)
|
|
||||||
self.app.push_screen(SubMenuModal("⚠ DANGER ZONE ⚠", options), on_result)
|
|
||||||
|
|
||||||
def _confirm_danger(self, message: str, callback):
|
@work(exclusive=True)
|
||||||
async def do_confirm():
|
async def run_batch_danger(self, selections: list[str]) -> None:
|
||||||
modal = ProgressScreen()
|
modal = ProgressScreen()
|
||||||
self.app.push_screen(modal)
|
self.app.push_screen(modal)
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
|
try:
|
||||||
modal.set_status(f"[bold red]DANGER:[/bold red] {message}")
|
modal.set_status(f"Awaiting Confirmation for {len(selections)} Destructive Operations...")
|
||||||
modal.write("[bold red]WARNING: THIS WILL DELETE DATA PERMANENTLY! MUST PROCEED TO CONTINUE.[/bold red]")
|
modal.write("[bold red]WARNING: THIS WILL DELETE DATA PERMANENTLY! MUST PROCEED TO CONTINUE.[/bold red]")
|
||||||
|
|
||||||
choice = await modal.phase_wait_confirm()
|
choice = await modal.phase_wait_confirm()
|
||||||
modal.dismiss()
|
if choice == "btn_back":
|
||||||
if choice == "btn_start_first":
|
modal.dismiss()
|
||||||
callback()
|
self._open_danger_menu()
|
||||||
|
return
|
||||||
elif choice == "btn_main_menu":
|
elif choice == "btn_main_menu":
|
||||||
|
modal.dismiss()
|
||||||
self.app.switch_screen("config_selection")
|
self.app.switch_screen("config_selection")
|
||||||
|
return
|
||||||
|
|
||||||
asyncio.create_task(do_confirm())
|
modal.cancel_callback = lambda: setattr(self.engine, "is_running", False)
|
||||||
|
modal.phase_progress()
|
||||||
|
self.engine.is_running = True
|
||||||
|
await self.engine.start_target_only()
|
||||||
|
|
||||||
@work(exclusive=True)
|
for sel in selections:
|
||||||
async def run_dz_delete_channels(self) -> None:
|
if not self.engine.is_running: break
|
||||||
|
if sel == "dz_del_channels":
|
||||||
|
await self._logic_dz_delete_channels(modal)
|
||||||
|
elif sel == "dz_reset_perms":
|
||||||
|
await self._logic_dz_reset_perms(modal)
|
||||||
|
elif sel == "dz_del_roles":
|
||||||
|
await self._logic_dz_delete_roles(modal)
|
||||||
|
elif sel == "dz_del_assets":
|
||||||
|
await self._logic_dz_delete_assets(modal)
|
||||||
|
|
||||||
|
modal.phase_report("Danger Zone Operations Complete")
|
||||||
|
modal.write("[bold green]All selected destructive operations finished.[/bold green]")
|
||||||
|
except Exception as e:
|
||||||
|
modal.write(f"[bold red]Error: {e}[/bold red]")
|
||||||
|
modal.phase_report("Danger Zone Batch", "error")
|
||||||
|
finally:
|
||||||
|
self.engine.is_running = False
|
||||||
|
await self.engine.close_target_only()
|
||||||
|
|
||||||
|
async def _logic_dz_delete_channels(self, modal: ProgressScreen) -> None:
|
||||||
if self.target_platform == "fluxer":
|
if self.target_platform == "fluxer":
|
||||||
from src.fluxer.danger_zone import danger_delete_all_channels
|
from src.fluxer.danger_zone import danger_delete_all_channels
|
||||||
else:
|
else:
|
||||||
from src.stoat.danger_zone import danger_delete_all_channels
|
from src.stoat.danger_zone import danger_delete_all_channels
|
||||||
|
|
||||||
modal = ProgressScreen()
|
modal.set_status("[red]Deleting channels...")
|
||||||
self.app.push_screen(modal)
|
async def on_deleted(name, current, total):
|
||||||
await asyncio.sleep(0.1)
|
modal.set_status(f"[red]Deleting: {name}")
|
||||||
modal.cancel_callback = lambda: setattr(self.engine, "is_running", False)
|
modal.set_progress(current, total)
|
||||||
modal.phase_progress()
|
|
||||||
|
|
||||||
try:
|
count = await danger_delete_all_channels(self.engine, progress_callback=on_deleted)
|
||||||
modal.set_status("[red]Deleting channels...")
|
modal.write(f"[bold green]Success! {count} channels/categories deleted.[/bold green]")
|
||||||
await self.engine.start_target_only()
|
await log_audit_event(self.engine, "Danger Zone: Channels Wiped", f"Deleted {count} channels and categories.")
|
||||||
|
|
||||||
async def on_deleted(name, current, total):
|
async def _logic_dz_reset_perms(self, modal: ProgressScreen) -> None:
|
||||||
modal.set_status(f"[red]Deleting: {name}")
|
|
||||||
modal.set_progress(current, total)
|
|
||||||
|
|
||||||
count = await danger_delete_all_channels(self.engine, progress_callback=on_deleted)
|
|
||||||
|
|
||||||
modal.phase_report("Danger Zone: Channels Wiped")
|
|
||||||
modal.write(f"[bold green]Success! {count} channels/categories deleted.[/bold green]")
|
|
||||||
await log_audit_event(self.engine, "Danger Zone: Channels Wiped", f"Deleted {count} channels and categories.")
|
|
||||||
except Exception as e:
|
|
||||||
modal.write(f"[bold red]Error: {e}[/bold red]")
|
|
||||||
modal.phase_report("Delete Channels", "error")
|
|
||||||
finally:
|
|
||||||
await self.engine.close_target_only()
|
|
||||||
|
|
||||||
@work(exclusive=True)
|
|
||||||
async def run_dz_reset_perms(self) -> None:
|
|
||||||
if self.target_platform == "fluxer":
|
if self.target_platform == "fluxer":
|
||||||
from src.fluxer.danger_zone import danger_reset_channel_permissions
|
from src.fluxer.danger_zone import danger_reset_channel_permissions
|
||||||
else:
|
else:
|
||||||
from src.stoat.danger_zone import danger_reset_channel_permissions
|
from src.stoat.danger_zone import danger_reset_channel_permissions
|
||||||
|
|
||||||
modal = ProgressScreen()
|
modal.set_status("[red]Resetting permissions...")
|
||||||
self.app.push_screen(modal)
|
async def on_reset(name, current, total):
|
||||||
await asyncio.sleep(0.1)
|
modal.set_status(f"[red]Resetting: {name}")
|
||||||
modal.cancel_callback = lambda: setattr(self.engine, "is_running", False)
|
modal.set_progress(current, total)
|
||||||
modal.phase_progress()
|
|
||||||
|
|
||||||
try:
|
count = await danger_reset_channel_permissions(self.engine, progress_callback=on_reset)
|
||||||
modal.set_status("[red]Resetting permissions...")
|
modal.write(f"[bold green]Success! Permissions reset on {count} items.[/bold green]")
|
||||||
await self.engine.start_target_only()
|
await log_audit_event(self.engine, "Danger Zone: Permissions Wiped", f"Reset permissions on {count} items.")
|
||||||
|
|
||||||
async def on_reset(name, current, total):
|
async def _logic_dz_delete_roles(self, modal: ProgressScreen) -> None:
|
||||||
modal.set_status(f"[red]Resetting: {name}")
|
|
||||||
modal.set_progress(current, total)
|
|
||||||
|
|
||||||
count = await danger_reset_channel_permissions(self.engine, progress_callback=on_reset)
|
|
||||||
|
|
||||||
modal.phase_report("Danger Zone: Permissions Wiped")
|
|
||||||
modal.write(f"[bold green]Success! Permissions reset on {count} items.[/bold green]")
|
|
||||||
await log_audit_event(self.engine, "Danger Zone: Permissions Wiped", f"Reset permissions on {count} items.")
|
|
||||||
except Exception as e:
|
|
||||||
modal.write(f"[bold red]Error: {e}[/bold red]")
|
|
||||||
modal.phase_report("Wipe Permissions", "error")
|
|
||||||
finally:
|
|
||||||
await self.engine.close_target_only()
|
|
||||||
|
|
||||||
@work(exclusive=True)
|
|
||||||
async def run_dz_delete_roles(self) -> None:
|
|
||||||
if self.target_platform == "fluxer":
|
if self.target_platform == "fluxer":
|
||||||
from src.fluxer.danger_zone import danger_delete_all_roles
|
from src.fluxer.danger_zone import danger_delete_all_roles
|
||||||
else:
|
else:
|
||||||
from src.stoat.danger_zone import danger_delete_all_roles
|
from src.stoat.danger_zone import danger_delete_all_roles
|
||||||
|
|
||||||
modal = ProgressScreen()
|
modal.set_status("[red]Deleting roles...")
|
||||||
self.app.push_screen(modal)
|
async def on_deleted(name, current, total):
|
||||||
await asyncio.sleep(0.1)
|
modal.set_status(f"[red]Deleting role: {name}")
|
||||||
modal.cancel_callback = lambda: setattr(self.engine, "is_running", False)
|
modal.set_progress(current, total)
|
||||||
modal.phase_progress()
|
|
||||||
|
|
||||||
try:
|
count = await danger_delete_all_roles(self.engine, progress_callback=on_deleted)
|
||||||
modal.set_status("[red]Deleting roles...")
|
modal.write(f"[bold green]Success! {count} roles deleted.[/bold green]")
|
||||||
await self.engine.start_target_only()
|
await log_audit_event(self.engine, "Danger Zone: Roles Wiped", f"Deleted {count} roles.")
|
||||||
|
|
||||||
async def on_deleted(name, current, total):
|
async def _logic_dz_delete_assets(self, modal: ProgressScreen) -> None:
|
||||||
modal.set_status(f"[red]Deleting role: {name}")
|
|
||||||
modal.set_progress(current, total)
|
|
||||||
|
|
||||||
count = await danger_delete_all_roles(self.engine, progress_callback=on_deleted)
|
|
||||||
|
|
||||||
modal.phase_report("Danger Zone: Roles Wiped")
|
|
||||||
modal.write(f"[bold green]Success! {count} roles deleted.[/bold green]")
|
|
||||||
await log_audit_event(self.engine, "Danger Zone: Roles Wiped", f"Deleted {count} roles.")
|
|
||||||
except Exception as e:
|
|
||||||
modal.write(f"[bold red]Error: {e}[/bold red]")
|
|
||||||
modal.phase_report("Wipe Roles", "error")
|
|
||||||
finally:
|
|
||||||
await self.engine.close_target_only()
|
|
||||||
|
|
||||||
@work(exclusive=True)
|
|
||||||
async def run_dz_delete_assets(self) -> None:
|
|
||||||
if self.target_platform == "fluxer":
|
if self.target_platform == "fluxer":
|
||||||
from src.fluxer.danger_zone import danger_delete_all_emojis_and_stickers
|
from src.fluxer.danger_zone import danger_delete_all_emojis_and_stickers
|
||||||
else:
|
else:
|
||||||
from src.stoat.danger_zone import danger_delete_all_emojis_and_stickers
|
from src.stoat.danger_zone import danger_delete_all_emojis_and_stickers
|
||||||
|
|
||||||
modal = ProgressScreen()
|
modal.set_status("[red]Deleting assets...")
|
||||||
self.app.push_screen(modal)
|
async def on_deleted(name, asset_type, current, total):
|
||||||
await asyncio.sleep(0.1)
|
modal.set_status(f"[red]Deleting {asset_type}: {name}")
|
||||||
modal.cancel_callback = lambda: setattr(self.engine, "is_running", False)
|
modal.set_progress(current, total)
|
||||||
modal.phase_progress()
|
|
||||||
|
|
||||||
try:
|
counts = await danger_delete_all_emojis_and_stickers(self.engine, progress_callback=on_deleted)
|
||||||
modal.set_status("[red]Deleting assets...")
|
modal.write(f"[bold green]Success! {counts.get('emojis', 0)} emojis, {counts.get('stickers', 0)} stickers deleted.[/bold green]")
|
||||||
await self.engine.start_target_only()
|
await log_audit_event(self.engine, "Danger Zone: Assets Wiped", f"Deleted {counts.get('emojis', 0)} emojis and {counts.get('stickers', 0)} stickers.")
|
||||||
|
|
||||||
async def on_deleted(name, asset_type, current, total):
|
|
||||||
modal.set_status(f"[red]Deleting {asset_type}: {name}")
|
|
||||||
modal.set_progress(current, total)
|
|
||||||
|
|
||||||
counts = await danger_delete_all_emojis_and_stickers(self.engine, progress_callback=on_deleted)
|
|
||||||
|
|
||||||
modal.phase_report("Danger Zone: Assets Wiped")
|
|
||||||
modal.write(f"[bold green]Success! {counts.get('emojis', 0)} emojis, {counts.get('stickers', 0)} stickers deleted.[/bold green]")
|
|
||||||
await log_audit_event(self.engine, "Danger Zone: Assets Wiped", f"Deleted {counts.get('emojis', 0)} emojis and {counts.get('stickers', 0)} stickers.")
|
|
||||||
except Exception as e:
|
|
||||||
modal.write(f"[bold red]Error: {e}[/bold red]")
|
|
||||||
modal.phase_report("Wipe Assets", "error")
|
|
||||||
finally:
|
|
||||||
await self.engine.close_target_only()
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue