send more info in audit logs

This commit is contained in:
rambros 2026-02-23 17:40:44 +05:30
parent 4f557dc20c
commit f5b9133947
8 changed files with 203 additions and 47 deletions

View file

@ -3,14 +3,27 @@ from src.core.base import MigrationContext
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
async def log_audit_event(context: MigrationContext, title: str, description: str) -> None: async def log_audit_event(context: MigrationContext, title: str, description: str, files: list[dict] | None = None) -> None:
""" """
Logs an event by sending a summary to the `#fluxer-reaper` audit channel. Logs an event by sending a summary to the `#fluxer-reaper` audit channel.
If the channel does not exist, it will dynamically create it and hide it from @everyone. If the channel does not exist, it will dynamically create it and hide it from @everyone.
""" """
# 1. Initialize channel if not tracked # 1. Initialize or Validate channel
channel_id = context.state.audit_log_channel
if channel_id:
# Verify it still exists in Fluxer to avoid 404 errors
try:
fluxer_channels = await context.fluxer_writer.get_channels()
channel_exists = any(str(ch.get("id")) == str(channel_id) for ch in fluxer_channels)
if not channel_exists:
logger.info(f"Audit channel {channel_id} no longer exists in Fluxer. Resetting.")
context.state.audit_log_channel = None
except Exception as e:
logger.warning(f"Failed to verify audit channel existence: {e}")
if not context.state.audit_log_channel: if not context.state.audit_log_channel:
logger.info("Audit log channel not found in state. Checking Fluxer community...") logger.info("Audit log channel not found or invalid in state. Checking Fluxer community...")
try: try:
# Check if it already exists in the community but isn't in state # Check if it already exists in the community but isn't in state
channels = await context.fluxer_writer.get_channels() channels = await context.fluxer_writer.get_channels()
@ -52,6 +65,6 @@ async def log_audit_event(context: MigrationContext, title: str, description: st
# 2. Format and send the message natively through FluxerBot (avoiding impersonation webhook for admin logs) # 2. Format and send the message natively through FluxerBot (avoiding impersonation webhook for admin logs)
content = f"**[{title}]**\n{description}" content = f"**[{title}]**\n{description}"
try: try:
await context.fluxer_writer.send_marker(context.state.audit_log_channel, content) await context.fluxer_writer.send_marker(context.state.audit_log_channel, content, files=files)
except Exception as e: except Exception as e:
logger.error(f"Failed to send audit log event: {e}") logger.error(f"Failed to send audit log event: {e}")

View file

@ -52,7 +52,7 @@ async def sync_channel_state(context: MigrationContext):
logger.info(f"Channel sync: {updates} mapped, {removals} stale mappings removed") logger.info(f"Channel sync: {updates} mapped, {removals} stale mappings removed")
async def migrate_channels(context: MigrationContext, progress_callback: Callable[[str, str, int, int], Awaitable[None]] | None = None, force: bool = False): async def migrate_channels(context: MigrationContext, progress_callback: Callable[[str, str, int, int], Awaitable[None]] | None = None, force: bool = False) -> dict:
"""Clones categories and text channels. """Clones categories and text channels.
Args: Args:
@ -62,6 +62,14 @@ async def migrate_channels(context: MigrationContext, progress_callback: Callabl
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()
cloned_info = {
"categories_created": [],
"channels_created": [],
"channels_synced": [],
"structure": {} # category_name -> [channel_names]
}
cat_name_map = {str(cat.id): cat.name for cat in categories}
# 1. Identify categories to create # 1. Identify categories to create
missing_categories = [cat for cat in categories if force or not context.state.get_fluxer_category_id(str(cat.id))] missing_categories = [cat for cat in categories if force or not context.state.get_fluxer_category_id(str(cat.id))]
missing_category_ids = {str(cat.id) for cat in missing_categories} missing_category_ids = {str(cat.id) for cat in missing_categories}
@ -90,7 +98,7 @@ async def migrate_channels(context: MigrationContext, progress_callback: Callabl
current_idx = 0 current_idx = 0
if total == 0: if total == 0:
return return cloned_info
# Migrate Categories first # Migrate Categories first
for cat in missing_categories: for cat in missing_categories:
@ -99,6 +107,9 @@ async def migrate_channels(context: MigrationContext, progress_callback: Callabl
state_key = str(cat.id) state_key = str(cat.id)
fluxer_id = await context.fluxer_writer.create_channel(cat.name, type=4) fluxer_id = await context.fluxer_writer.create_channel(cat.name, type=4)
context.state.set_category_mapping(state_key, fluxer_id) context.state.set_category_mapping(state_key, fluxer_id)
cloned_info["categories_created"].append(cat.name)
if cat.name not in cloned_info["structure"]:
cloned_info["structure"][cat.name] = []
current_idx += 1 current_idx += 1
if progress_callback: await progress_callback(f"Cat: {cat.name}", "Copying", current_idx, total) if progress_callback: await progress_callback(f"Cat: {cat.name}", "Copying", current_idx, total)
@ -126,6 +137,12 @@ async def migrate_channels(context: MigrationContext, progress_callback: Callabl
slowmode_delay=slowmode slowmode_delay=slowmode
) )
context.state.set_channel_mapping(state_key, fluxer_id) context.state.set_channel_mapping(state_key, fluxer_id)
cloned_info["channels_created"].append(channel.name)
parent_name = cat_name_map.get(str(channel.category_id), "No Category") if channel.category_id else "No Category"
if parent_name not in cloned_info["structure"]:
cloned_info["structure"][parent_name] = []
cloned_info["structure"][parent_name].append(channel.name)
# Sync again immediately because some properties (like slowmode) are ignored on creation # Sync again immediately because some properties (like slowmode) are ignored on creation
await context.fluxer_writer.modify_channel( await context.fluxer_writer.modify_channel(
@ -161,6 +178,10 @@ async def migrate_channels(context: MigrationContext, progress_callback: Callabl
slowmode_delay=slowmode slowmode_delay=slowmode
) )
cloned_info["channels_synced"].append(channel.name)
current_idx += 1 current_idx += 1
if progress_callback: await progress_callback(channel.name, "Syncing", current_idx, total) if progress_callback: await progress_callback(channel.name, "Syncing", current_idx, total)
await asyncio.sleep(context.config.migration.rate_limit_delay_seconds) await asyncio.sleep(context.config.migration.rate_limit_delay_seconds)
return cloned_info

View file

@ -55,11 +55,14 @@ async def sync_assets_state(context: MigrationContext):
logger.info(f"Asset sync: {updates} mapped, {removals} stale mappings removed") logger.info(f"Asset sync: {updates} mapped, {removals} stale mappings removed")
async def migrate_emojis(context: MigrationContext, progress_callback: Callable[[str, str, int, int], Awaitable[None]] | None = None, types_to_include: List[str] = ["Emoji", "Sticker"], force: bool = False): async def migrate_emojis(context: MigrationContext, progress_callback: Callable[[str, str, int, int], Awaitable[None]] | None = None, types_to_include: List[str] = ["Emoji", "Sticker"], force: bool = False) -> dict[str, dict[str, str]]:
"""Copies custom emojis and stickers. """Copies custom emojis and stickers.
Args: Args:
force: If True, skip state cache and re-copy even if already migrated. force: If True, skip state cache and re-copy even if already migrated.
Returns:
A dictionary containing dicts of cloned asset names to their new IDs by type: {"Emoji": {"name": "id", ...}, "Sticker": {"name": "id", ...}}
""" """
objs = [] objs = []
if "Emoji" in types_to_include: if "Emoji" in types_to_include:
@ -75,9 +78,10 @@ async def migrate_emojis(context: MigrationContext, progress_callback: Callable[
)] )]
total = len(objs) total = len(objs)
cloned_assets: dict[str, dict[str, str]] = {"Emoji": {}, "Sticker": {}}
if total == 0: if total == 0:
return 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: break
@ -91,6 +95,7 @@ async def migrate_emojis(context: MigrationContext, progress_callback: Callable[
) )
if fluxer_id: if fluxer_id:
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
else: else:
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(
@ -99,8 +104,11 @@ async def migrate_emojis(context: MigrationContext, progress_callback: Callable[
) )
if fluxer_id: if fluxer_id:
context.state.set_sticker_mapping(str(obj.id), fluxer_id) context.state.set_sticker_mapping(str(obj.id), fluxer_id)
cloned_assets["Sticker"][obj.name] = fluxer_id
except Exception as e: except Exception as e:
logger.error(f"Error downloading/uploading {obj_type.lower()} {obj.name}: {e}") logger.error(f"Error downloading/uploading {obj_type.lower()} {obj.name}: {e}")
if progress_callback: await progress_callback(obj.name, obj_type, idx + 1, total) if progress_callback: await progress_callback(obj.name, obj_type, idx + 1, total)
await asyncio.sleep(context.config.migration.rate_limit_delay_seconds) await asyncio.sleep(context.config.migration.rate_limit_delay_seconds)
return cloned_assets

View file

@ -279,7 +279,7 @@ class FluxerWriter:
print(err_msg) print(err_msg)
return None return None
async def send_marker(self, channel_id: str, content: str) -> Optional[str]: async def send_marker(self, channel_id: str, content: str, files: list[dict] | None = None) -> Optional[str]:
""" """
Sends a simple marker message (e.g., thread start/end) using the bot directly. Sends a simple marker message (e.g., thread start/end) using the bot directly.
""" """
@ -287,7 +287,8 @@ class FluxerWriter:
try: try:
msg_data = await self.client.send_message( msg_data = await self.client.send_message(
channel_id=channel_id, channel_id=channel_id,
content=content content=content,
files=files
) )
return str(msg_data["id"]) if msg_data else None return str(msg_data["id"]) if msg_data else None
except Exception as e: except Exception as e:

View file

@ -37,7 +37,7 @@ async def sync_roles_state(context: MigrationContext):
logger.info(f"Role sync: {updates} mapped, {removals} stale mappings removed") logger.info(f"Role sync: {updates} mapped, {removals} stale mappings removed")
async def sync_permissions(context: MigrationContext, progress_callback: Callable[[str, int, int], Awaitable[None]] | None = None): 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."""
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()
@ -46,11 +46,17 @@ async def sync_permissions(context: MigrationContext, progress_callback: Callabl
categories = [c for c in categories if context.state.get_fluxer_category_id(str(c.id))] categories = [c for c in categories if context.state.get_fluxer_category_id(str(c.id))]
channels = [c for c in channels if context.state.get_fluxer_channel_id(str(c.id))] channels = [c for c in channels if context.state.get_fluxer_channel_id(str(c.id))]
synced_info = {
"categories_synced": [],
"channels_synced": [],
"structure": {} # category_name -> [channel_names]
}
total = len(categories) + len(channels) total = len(categories) + len(channels)
current_idx = 0 current_idx = 0
if total == 0: if total == 0:
return return synced_info
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."""
@ -75,6 +81,10 @@ async def sync_permissions(context: MigrationContext, progress_callback: Callabl
is_role=True is_role=True
) )
# Dictionary to map category names to their synced channels
# Categorize items as we sync them
cat_name_map = {str(cat.id): cat.name for cat in (await context.discord_reader.get_categories())}
# Sync Category Permissions (Role Overwrites) # Sync Category Permissions (Role Overwrites)
for cat in categories: for cat in categories:
if not context.is_running: break if not context.is_running: break
@ -82,6 +92,9 @@ async def sync_permissions(context: MigrationContext, progress_callback: Callabl
if fluxer_id: if fluxer_id:
try: try:
await _sync_overwrites(cat, fluxer_id) await _sync_overwrites(cat, fluxer_id)
synced_info["categories_synced"].append(cat.name)
if cat.name not in synced_info["structure"]:
synced_info["structure"][cat.name] = []
except Exception as e: except Exception as e:
logger.error(f"Failed syncing permissions for category {cat.name}: {e}") logger.error(f"Failed syncing permissions for category {cat.name}: {e}")
@ -95,24 +108,33 @@ async def sync_permissions(context: MigrationContext, progress_callback: Callabl
if fluxer_id: if fluxer_id:
try: try:
await _sync_overwrites(channel, fluxer_id) await _sync_overwrites(channel, fluxer_id)
synced_info["channels_synced"].append(channel.name)
parent_name = cat_name_map.get(str(channel.category_id), "No Category") if channel.category_id else "No Category"
if parent_name not in synced_info["structure"]:
synced_info["structure"][parent_name] = []
synced_info["structure"][parent_name].append(channel.name)
except Exception as e: except Exception as e:
logger.error(f"Failed syncing permissions for channel {channel.name}: {e}") logger.error(f"Failed syncing permissions for channel {channel.name}: {e}")
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)
return synced_info
async def migrate_roles(context: MigrationContext, progress_callback: Callable[[str, int, int], Awaitable[None]] | None = None, force: bool = False):
"""Copies roles and their baseline permissions.""" async def migrate_roles(context: MigrationContext, progress_callback: Callable[[str, int, int], Awaitable[None]] | None = None, force: bool = False) -> list[str]:
"""Copies roles and their baseline permissions. Returns a list of cloned role names."""
roles = await context.discord_reader.get_roles() roles = await context.discord_reader.get_roles()
if not force: if not force:
roles = [r for r in roles if not context.state.get_fluxer_role_id(str(r.id))] roles = [r for r in roles if not context.state.get_fluxer_role_id(str(r.id))]
total = len(roles) total = len(roles)
cloned_role_names = []
if total == 0: if total == 0:
return 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: break
@ -125,6 +147,9 @@ async def migrate_roles(context: MigrationContext, progress_callback: Callable[[
) )
if fluxer_id: if fluxer_id:
context.state.set_role_mapping(str(role.id), fluxer_id) context.state.set_role_mapping(str(role.id), fluxer_id)
cloned_role_names.append(role.name)
if progress_callback: await progress_callback(role.name, idx + 1, total) if progress_callback: await progress_callback(role.name, idx + 1, total)
await asyncio.sleep(context.config.migration.rate_limit_delay_seconds) await asyncio.sleep(context.config.migration.rate_limit_delay_seconds)
return cloned_role_names

View file

@ -5,15 +5,17 @@ from src.core.base import MigrationContext
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
async def sync_server_metadata(context: MigrationContext, progress_callback: Callable[[str, str], Awaitable[None]], components: List[str] = ["name", "icon", "banner"]): async def sync_server_metadata(context: MigrationContext, progress_callback: Callable[[str, str], Awaitable[None]], components: List[str] = ["name", "icon", "banner"]) -> dict:
"""Syncs the server name, logo and banner.""" """Syncs the server name, logo and banner. Returns a dict of cloned attributes."""
metadata = await context.discord_reader.get_server_metadata() metadata = await context.discord_reader.get_server_metadata()
cloned_data = {}
# 1. Sync Name # 1. Sync Name
if "name" in components: if "name" in components:
try: try:
name = metadata.get("name") name = metadata.get("name")
await context.fluxer_writer.update_guild_metadata(name=name) await context.fluxer_writer.update_guild_metadata(name=name)
cloned_data["name"] = name
await progress_callback("Server Name", "DONE") await progress_callback("Server Name", "DONE")
except Exception: except Exception:
await progress_callback("Server Name", "ERROR") await progress_callback("Server Name", "ERROR")
@ -27,6 +29,7 @@ async def sync_server_metadata(context: MigrationContext, progress_callback: Cal
if icon_bytes: if icon_bytes:
await context.fluxer_writer.update_guild_metadata(icon=icon_bytes) await context.fluxer_writer.update_guild_metadata(icon=icon_bytes)
cloned_data["icon"] = icon_bytes
await progress_callback("Server Icon", "DONE") await progress_callback("Server Icon", "DONE")
else: else:
await progress_callback("Server Icon", "SKIP") await progress_callback("Server Icon", "SKIP")
@ -42,8 +45,11 @@ async def sync_server_metadata(context: MigrationContext, progress_callback: Cal
if banner_bytes: if banner_bytes:
await context.fluxer_writer.update_guild_metadata(banner=banner_bytes) await context.fluxer_writer.update_guild_metadata(banner=banner_bytes)
cloned_data["banner"] = banner_bytes
await progress_callback("Server Banner", "DONE") await progress_callback("Server Banner", "DONE")
else: else:
await progress_callback("Server Banner", "SKIP") await progress_callback("Server Banner", "SKIP")
except Exception: except Exception:
await progress_callback("Server Banner", "ERROR") await progress_callback("Server Banner", "ERROR")
return cloned_data

View file

@ -21,8 +21,8 @@ class MigrationState:
# message tracking # message tracking
self.message_map: Dict[str, str] = {} self.message_map: Dict[str, str] = {}
self.last_message_timestamps: Dict[str, str] = {}
self.last_message_ids: Dict[str, str] = {} self.last_message_ids: Dict[str, str] = {}
self.last_message_timestamps: Dict[str, str] = {}
self.load() self.load()
@ -44,8 +44,8 @@ class MigrationState:
# Check for legacy messages in state.json # Check for legacy messages in state.json
if "messages" in data or "last_message_timestamps" in data: if "messages" in data or "last_message_timestamps" in data:
self.message_map = data.get("messages", {}) self.message_map = data.get("messages", {})
self.last_message_timestamps = data.get("last_message_timestamps", {})
self.last_message_ids = data.get("last_message_ids", {}) self.last_message_ids = data.get("last_message_ids", {})
self.last_message_timestamps = data.get("last_message_timestamps", {})
migrated_messages = True # We found legacy data, we should write it out to messages.json later migrated_messages = True # We found legacy data, we should write it out to messages.json later
# Legacy Migration: Move role_, emoji_, sticker_ from channel_map to dedicated maps # Legacy Migration: Move role_, emoji_, sticker_ from channel_map to dedicated maps
@ -69,8 +69,8 @@ class MigrationState:
with open(self.messages_file, "r", encoding="utf-8") as f: with open(self.messages_file, "r", encoding="utf-8") as f:
msg_data = json.load(f) msg_data = json.load(f)
self.message_map = msg_data.get("messages", {}) self.message_map = msg_data.get("messages", {})
self.last_message_timestamps = msg_data.get("last_message_timestamps", {})
self.last_message_ids = msg_data.get("last_message_ids", {}) self.last_message_ids = msg_data.get("last_message_ids", {})
self.last_message_timestamps = msg_data.get("last_message_timestamps", {})
migrated_messages = False # No need to force a migrating save since it already exists migrated_messages = False # No need to force a migrating save since it already exists
# 3. Save if we migrated any legacy data to separate maps/files # 3. Save if we migrated any legacy data to separate maps/files
@ -95,8 +95,8 @@ class MigrationState:
def save_messages(self): def save_messages(self):
"""Saves only the message tracking data.""" """Saves only the message tracking data."""
data = { data = {
"last_message_timestamps": self.last_message_timestamps,
"last_message_ids": self.last_message_ids, "last_message_ids": self.last_message_ids,
"last_message_timestamps": self.last_message_timestamps,
"messages": self.message_map "messages": self.message_map
} }
with open(self.messages_file, "w", encoding="utf-8") as f: with open(self.messages_file, "w", encoding="utf-8") as f:
@ -201,6 +201,6 @@ class MigrationState:
def clear_message_history(self): def clear_message_history(self):
"""Clears all message mappings and timestamps.""" """Clears all message mappings and timestamps."""
self.message_map.clear() self.message_map.clear()
self.last_message_timestamps.clear()
self.last_message_ids.clear() self.last_message_ids.clear()
self.last_message_timestamps.clear()
self.save_messages() self.save_messages()

View file

@ -415,10 +415,26 @@ class MigrationCLI:
progress.update(channel_task, total=total, completed=current, description=f"[{color}]{status} Channel: {item_name}") progress.update(channel_task, total=total, completed=current, description=f"[{color}]{status} Channel: {item_name}")
self.engine.is_running = True self.engine.is_running = True
await migrate_channels(self.engine, progress_callback=update_progress, force=force) cloned_info = await migrate_channels(self.engine, progress_callback=update_progress, force=force)
console.print("[bold green]Server Template cloned![/bold green]") console.print("[bold green]Server Template cloned![/bold green]")
await log_audit_event(self.engine, "Server Template Cloned", "Successfully cloned channels and categories from Discord.")
if cloned_info and cloned_info.get("structure"):
lines = ["Successfully cloned channels and categories from Discord:"]
# Sort categories but keep "No Category" at the bottom if it exists
cats = sorted(cloned_info["structure"].keys(), key=lambda x: (x == "No Category", x))
for cat_name in cats:
channel_names = cloned_info["structure"][cat_name]
# Only print if the category itself was created OR it has channels created under it
if cat_name in cloned_info["categories_created"] or channel_names:
lines.append(f"- **{cat_name}**")
for ch_name in sorted(channel_names):
lines.append(f" - {ch_name}")
audit_desc = "\n".join(lines)
await log_audit_event(self.engine, "Server Template Cloned", audit_desc)
else:
await log_audit_event(self.engine, "Server Template Cloned", "No new channels or categories were cloned.")
except Exception as e: except Exception as e:
console.print(f"[bold red]Error during channel clone: {str(e)}[/bold red]") console.print(f"[bold red]Error during channel clone: {str(e)}[/bold red]")
@ -500,10 +516,14 @@ class MigrationCLI:
progress.update(role_task, total=total, completed=current, description=f"[cyan]Syncing Role: {item_name}") progress.update(role_task, total=total, completed=current, description=f"[cyan]Syncing Role: {item_name}")
self.engine.is_running = True self.engine.is_running = True
await migrate_roles(self.engine, progress_callback=update_progress, force=force) cloned_roles = await migrate_roles(self.engine, progress_callback=update_progress, force=force)
console.print("[bold green]Role migration complete![/bold green]") console.print("[bold green]Role migration complete![/bold green]")
await log_audit_event(self.engine, "Roles Cloned", "Successfully cloned roles and baseline role permissions from Discord.") if cloned_roles:
audit_desc = "Successfully cloned these roles and their baseline permissions:\n" + "\n".join(f"- {r}" for r in cloned_roles)
await log_audit_event(self.engine, "Roles Cloned", audit_desc)
else:
await log_audit_event(self.engine, "Roles Cloned", "No new roles were cloned.")
except Exception as e: except Exception as e:
console.print(f"[bold red]Error during role migration: {str(e)}[/bold red]") console.print(f"[bold red]Error during role migration: {str(e)}[/bold red]")
@ -547,10 +567,26 @@ class MigrationCLI:
progress.update(perm_task, total=total, completed=current, description=f"[cyan]Syncing: {item_name}") progress.update(perm_task, total=total, completed=current, description=f"[cyan]Syncing: {item_name}")
self.engine.is_running = True self.engine.is_running = True
await sync_permissions(self.engine, progress_callback=update_progress) synced_info = await sync_permissions(self.engine, progress_callback=update_progress)
console.print("[bold green]Permission synchronization complete![/bold green]") console.print("[bold green]Permission synchronization complete![/bold green]")
await log_audit_event(self.engine, "Permissions Synced", "Successfully synchronized channel and category permission overrides.")
if synced_info and synced_info.get("structure"):
lines = ["Successfully synchronized channel and category permission overrides:"]
# Sort categories but keep "No Category" at the bottom
cats = sorted(synced_info["structure"].keys(), key=lambda x: (x == "No Category", x))
for cat_name in cats:
channel_names = synced_info["structure"][cat_name]
# For permissions, we show everything that was successfully synced
if cat_name in synced_info["categories_synced"] or channel_names:
lines.append(f"- **{cat_name}**")
for ch_name in sorted(channel_names):
lines.append(f" - {ch_name}")
audit_desc = "\n".join(lines)
await log_audit_event(self.engine, "Permissions Synced", audit_desc)
else:
await log_audit_event(self.engine, "Permissions Synced", "No permissions were synchronized.")
except Exception as e: except Exception as e:
console.print(f"[bold red]Error during permission sync: {str(e)}[/bold red]") console.print(f"[bold red]Error during permission sync: {str(e)}[/bold red]")
@ -652,7 +688,7 @@ class MigrationCLI:
progress.update(emoji_task, total=total, completed=current, description=f"[cyan]Copying {item_type}: {item_name}") progress.update(emoji_task, total=total, completed=current, description=f"[cyan]Copying {item_type}: {item_name}")
self.engine.is_running = True self.engine.is_running = True
await migrate_emojis( cloned_assets = await migrate_emojis(
self.engine, self.engine,
progress_callback=update_progress, progress_callback=update_progress,
types_to_include=types_to_include, types_to_include=types_to_include,
@ -660,7 +696,22 @@ class MigrationCLI:
) )
console.print("[bold green]Migration complete![/bold green]") console.print("[bold green]Migration complete![/bold green]")
await log_audit_event(self.engine, "Emojis & Stickers Cloned", "Successfully synchronized emojis and stickers from Discord.") if cloned_assets and (cloned_assets.get("Emoji") or cloned_assets.get("Sticker")):
lines = []
if cloned_assets.get("Emoji"):
lines.append("Successfully cloned these emojis:")
for name, e_id in cloned_assets["Emoji"].items():
lines.append(f"- {name} <:{name}:{e_id}>")
if cloned_assets.get("Sticker"):
if lines: lines.append("")
lines.append("Successfully cloned these stickers:")
for name, s_id in cloned_assets["Sticker"].items():
lines.append(f"- {name}")
audit_desc = "\n".join(lines)
await log_audit_event(self.engine, "Emojis & Stickers Cloned", audit_desc)
else:
await log_audit_event(self.engine, "Emojis & Stickers Cloned", "No new emojis or stickers were cloned.")
except Exception as e: except Exception as e:
console.print(f"[bold red]Error during emoji migration: {str(e)}[/bold red]") console.print(f"[bold red]Error during emoji migration: {str(e)}[/bold red]")
@ -718,9 +769,36 @@ class MigrationCLI:
color = "green" if status == "DONE" else "red" if status == "ERROR" else "yellow" color = "green" if status == "DONE" else "red" if status == "ERROR" else "yellow"
console.print(f"{item} [[bold {color}]{status}[/bold {color}]]") console.print(f"{item} [[bold {color}]{status}[/bold {color}]]")
await sync_server_metadata(self.engine, progress_callback, components=components) cloned_data = await sync_server_metadata(self.engine, progress_callback, components=components)
console.print("[bold green]Server metadata sync finished![/bold green]") console.print("[bold green]Server profile sync finished![/bold green]")
await log_audit_event(self.engine, "Server Metadata Synced", "Successfully synchronized the community icon, banner, and name.")
audit_lines = ["Successfully synchronized Community profile:"]
if "name" in cloned_data:
audit_lines.append(f"- **Name**: {cloned_data['name']}")
if "icon" in cloned_data:
audit_lines.append(f"- **Icon**")
if "banner" in cloned_data:
audit_lines.append(f"- **Banner**")
files = []
if "icon" in cloned_data:
icon_ext = "gif" if cloned_data["icon"].startswith(b"GIF") else "png"
files.append({
"filename": f"icon.{icon_ext}",
"data": cloned_data["icon"]
})
if "banner" in cloned_data:
banner_ext = "gif" if cloned_data["banner"].startswith(b"GIF") else "png"
files.append({
"filename": f"banner.{banner_ext}",
"data": cloned_data["banner"]
})
audit_desc = "\n".join(audit_lines)
if cloned_data:
await log_audit_event(self.engine, "Server Profile Synced", audit_desc, files=files)
else:
await log_audit_event(self.engine, "Server Profile Synced", "No profile information was synchronized.")
except Exception as e: except Exception as e:
console.print(f"[bold red]Error during metadata sync: {str(e)}[/bold red]") console.print(f"[bold red]Error during metadata sync: {str(e)}[/bold red]")
finally: finally:
@ -1019,17 +1097,21 @@ class MigrationCLI:
console.print(f"\n[bold green]Success! {result_stats['messages']} messages migrated to {target_channel.get('name')}.[/bold green]") console.print(f"\n[bold green]Success! {result_stats['messages']} messages migrated to {target_channel.get('name')}.[/bold green]")
audit_text = ( lines = [f"Successfully migrated messages from Discord [cyan]#{source_channel.name}[/cyan] to [blue]#{target_channel.get('name')}[/blue]:"]
f"**Message info:**\n" lines.append(f"\n**Stats:**")
f"first message: {result_stats['first_message_url'] or 'N/A'}\n" lines.append(f"- {result_stats['messages']} messages")
f"last message: {result_stats['last_message_url'] or 'N/A'}\n" lines.append(f"- {result_stats['attachments']} attachments")
f"**Stats:**\n" lines.append(f"- {result_stats['threads']} threads")
f"{stats['messages']} messages\n"
f"{stats['attachments']} attachments\n" if result_stats.get('first_message_url') or result_stats.get('last_message_url'):
f"{stats['threads']} threads\n" lines.append(f"\n**Message Info:**")
f"Successfully migrated to <#{target_channel.get('id')}>\n" if result_stats.get('first_message_url'):
) lines.append(f"- First message: <{result_stats['first_message_url']}>")
await log_audit_event(self.engine, "Message History Migrated", audit_text) if result_stats.get('last_message_url'):
lines.append(f"- Last message: <{result_stats['last_message_url']}>")
audit_desc = "\n".join(lines)
await log_audit_event(self.engine, "Message History Migrated", audit_desc)
except Exception as e: except Exception as e:
console.print(f"[bold red]Migration encountered an error: {str(e)}[/bold red]") console.print(f"[bold red]Migration encountered an error: {str(e)}[/bold red]")
@ -1088,7 +1170,7 @@ class MigrationCLI:
count = await danger_delete_all_channels(self.engine, progress_callback=on_channel_deleted) count = await danger_delete_all_channels(self.engine, progress_callback=on_channel_deleted)
console.print(f"[bold green]{count} channels/categories deleted.[/bold green]") console.print(f"[bold green]{count} channels/categories deleted.[/bold green]")
await log_audit_event(self.engine, "Danger Zone: Channels Wiped", f"Administrators deleted {count} channels and categories from the community.") await log_audit_event(self.engine, "Danger Zone: Channels Wiped", f"Deleted {count} channels and categories from the community.")
console.print("[bold green]Done.[/bold green]") console.print("[bold green]Done.[/bold green]")
except Exception as e: except Exception as e:
console.print(f"[bold red]Error: {e}[/bold red]") console.print(f"[bold red]Error: {e}[/bold red]")
@ -1155,7 +1237,7 @@ class MigrationCLI:
count = await danger_delete_all_roles(self.engine, progress_callback=on_role_deleted) count = await danger_delete_all_roles(self.engine, progress_callback=on_role_deleted)
console.print(f"[bold green]{count} roles deleted.[/bold green]") console.print(f"[bold green]{count} roles deleted.[/bold green]")
await log_audit_event(self.engine, "Danger Zone: Roles Wiped", f"Administrators deleted {count} roles from the community.") await log_audit_event(self.engine, "Danger Zone: Roles Wiped", f"Deleted {count} roles from the community.")
console.print("[bold green]Done.[/bold green]") console.print("[bold green]Done.[/bold green]")
except Exception as e: except Exception as e:
console.print(f"[bold red]Error: {e}[/bold red]") console.print(f"[bold red]Error: {e}[/bold red]")
@ -1189,7 +1271,7 @@ class MigrationCLI:
counts = await danger_delete_all_emojis_and_stickers(self.engine, progress_callback=on_asset_deleted) counts = await danger_delete_all_emojis_and_stickers(self.engine, 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('emojis', 0)} emojis deleted.[/bold green]")
console.print(f"[bold green]{counts.get('stickers', 0)} stickers deleted.[/bold green]") console.print(f"[bold green]{counts.get('stickers', 0)} stickers deleted.[/bold green]")
await log_audit_event(self.engine, "Danger Zone: Assets Wiped", f"Administrators deleted {counts.get('emojis', 0)} emojis and {counts.get('stickers', 0)} stickers.") await log_audit_event(self.engine, "Danger Zone: Assets Wiped", f"Deleted {counts.get('emojis', 0)} emojis and {counts.get('stickers', 0)} stickers.")
console.print("[bold green]Done.[/bold green]") console.print("[bold green]Done.[/bold green]")
except Exception as e: except Exception as e:
console.print(f"[bold red]Error: {e}[/bold red]") console.print(f"[bold red]Error: {e}[/bold red]")