diff --git a/src/core/audit.py b/src/core/audit.py new file mode 100644 index 0000000..bf69584 --- /dev/null +++ b/src/core/audit.py @@ -0,0 +1,57 @@ +import logging +from src.core.base import MigrationContext + +logger = logging.getLogger(__name__) + +async def log_audit_event(context: MigrationContext, title: str, description: str) -> None: + """ + 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. + """ + # 1. Initialize channel if not tracked + if not context.state.audit_log_channel: + logger.info("Audit log channel not found in state. Checking Fluxer community...") + try: + # Check if it already exists in the community but isn't in state + channels = await context.fluxer_writer.get_channels() + channel_id = None + + for ch in channels: + name = str(ch.get("name", "")).lower() + if name in ["fluxer-reaper", "fluxer_reaper"]: + channel_id = str(ch.get("id")) + logger.info(f"Found existing audit channel: {channel_id}") + break + + if not channel_id: + logger.info("Audit log channel not found. Creating #fluxer-reaper.") + # Create channel + channel_id = await context.fluxer_writer.create_channel( + name="fluxer-reaper", + topic="Fluxer Reaper - Migration audit logs.", + type=0 + ) + + # Immediately lock down 'View Channel' (1024) for @everyone (community_id) + await context.fluxer_writer.set_channel_permission( + channel_id=channel_id, + overwrite_id=context.config.fluxer_community_id, # @everyone matches community ID + allow=0, + deny=1024, + is_role=True + ) + + # Save permanently + context.state.audit_log_channel = channel_id + context.state.save_state() + + except Exception as e: + logger.error(f"Failed to setup audit log channel: {e}") + return + + # 2. Format and send the message natively through FluxerBot (avoiding impersonation webhook for admin logs) + content = f"**[{title}]**\n{description}" + try: + await context.fluxer_writer.send_marker(context.state.audit_log_channel, content) + except Exception as e: + logger.error(f"Failed to send audit log event: {e}") diff --git a/src/core/fluxer_writer.py b/src/core/fluxer_writer.py index 3b2723f..732172d 100644 --- a/src/core/fluxer_writer.py +++ b/src/core/fluxer_writer.py @@ -435,6 +435,12 @@ class FluxerWriter: # Delete non-category channels first, then categories sorted_channels = sorted(channels, key=lambda c: 0 if c.get("type") == 4 else -1) for ch in sorted_channels: + name = str(ch.get("name", "")).lower() + if name in ["fluxer-reaper", "fluxer_reaper"]: + logger.info(f"Danger Zone: Skipping deletion of audit channel {name}") + total -= 1 + continue + try: await self.client.delete_channel(ch["id"]) deleted += 1 @@ -454,6 +460,12 @@ class FluxerWriter: total = len(channels) processed = 0 for ch in channels: + name = str(ch.get("name", "")).lower() + if name in ["fluxer-reaper", "fluxer_reaper"]: + logger.info(f"Danger Zone: Skipping permission reset for audit channel {name}") + total -= 1 + continue + try: # Fetch existing overwrites and delete each one overwrites = ch.get("permission_overwrites", []) diff --git a/src/core/state.py b/src/core/state.py index 7273d60..fbc13c7 100644 --- a/src/core/state.py +++ b/src/core/state.py @@ -16,6 +16,9 @@ class MigrationState: self.emoji_map: Dict[str, str] = {} self.sticker_map: Dict[str, str] = {} + # audit log tracking + self.audit_log_channel: str | None = None + # message tracking self.message_map: Dict[str, str] = {} self.last_message_timestamps: Dict[str, str] = {} @@ -35,6 +38,7 @@ class MigrationState: self.role_map = data.get("roles", {}) self.emoji_map = data.get("emojis", {}) self.sticker_map = data.get("stickers", {}) + self.audit_log_channel = data.get("audit_log_channel") # Check for legacy messages in state.json if "messages" in data or "last_message_timestamps" in data: @@ -79,7 +83,8 @@ class MigrationState: "categories": self.category_map, "roles": self.role_map, "emojis": self.emoji_map, - "stickers": self.sticker_map + "stickers": self.sticker_map, + "audit_log_channel": self.audit_log_channel } with open(self.state_file, "w", encoding="utf-8") as f: json.dump(data, f, indent=4) diff --git a/src/ui/app.py b/src/ui/app.py index 245b3da..95b1bcf 100644 --- a/src/ui/app.py +++ b/src/ui/app.py @@ -15,6 +15,7 @@ from src.core.emoji_stickers import sync_assets_state, migrate_emojis from src.core.server_metadata import sync_server_metadata from src.core.migrate_message import analyze_migration, migrate_messages from src.core.danger_zone import danger_remove_logo_and_banner, danger_delete_all_channels, danger_reset_channel_permissions, danger_delete_all_roles, danger_delete_all_emojis_and_stickers +from src.core.audit import log_audit_event class RateLimitHandler(logging.Handler): """Intersects library logs to print clean rate limit messages.""" @@ -417,6 +418,7 @@ class MigrationCLI: await migrate_channels(self.engine, progress_callback=update_progress, force=force) 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.") except Exception as e: console.print(f"[bold red]Error during channel clone: {str(e)}[/bold red]") @@ -501,6 +503,7 @@ class MigrationCLI: await migrate_roles(self.engine, progress_callback=update_progress, force=force) 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.") except Exception as e: console.print(f"[bold red]Error during role migration: {str(e)}[/bold red]") @@ -547,6 +550,7 @@ class MigrationCLI: await sync_permissions(self.engine, progress_callback=update_progress) console.print("[bold green]Permission synchronization complete![/bold green]") + await log_audit_event(self.engine, "Permissions Synced", "Successfully synchronized channel and category permission overrides.") except Exception as e: console.print(f"[bold red]Error during permission sync: {str(e)}[/bold red]") @@ -656,6 +660,7 @@ class MigrationCLI: ) console.print("[bold green]Migration complete![/bold green]") + await log_audit_event(self.engine, "Emojis & Stickers Cloned", "Successfully synchronized emojis and stickers from Discord.") except Exception as e: console.print(f"[bold red]Error during emoji migration: {str(e)}[/bold red]") @@ -715,6 +720,7 @@ class MigrationCLI: await sync_server_metadata(self.engine, progress_callback, components=components) console.print("[bold green]Server metadata sync finished![/bold green]") + await log_audit_event(self.engine, "Server Metadata Synced", "Successfully synchronized the community icon, banner, and name.") except Exception as e: console.print(f"[bold red]Error during metadata sync: {str(e)}[/bold red]") finally: @@ -962,6 +968,7 @@ class MigrationCLI: ) console.print(f"\n[bold green]Success! {count} messages migrated to {target_channel.get('name')}.[/bold green]") + await log_audit_event(self.engine, "Message History Migrated", f"Successfully migrated {count} messages to #{target_channel.get('name')}.") except Exception as e: console.print(f"[bold red]Migration encountered an error: {str(e)}[/bold red]") @@ -1020,6 +1027,7 @@ class MigrationCLI: count = await danger_delete_all_channels(self.engine, progress_callback=on_channel_deleted) 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.") console.print("[bold green]Done.[/bold green]") except Exception as e: console.print(f"[bold red]Error: {e}[/bold red]") @@ -1052,6 +1060,7 @@ class MigrationCLI: count = await danger_reset_channel_permissions(self.engine, progress_callback=on_perm_reset) console.print(f"[bold green]Permissions reset on {count} channels/categories.[/bold green]") + await log_audit_event(self.engine, "Danger Zone: Permissions Wiped", f"Administrators reset permissions on {count} channels and categories.") console.print("[bold green]Done.[/bold green]") except Exception as e: console.print(f"[bold red]Error: {e}[/bold red]") @@ -1085,6 +1094,7 @@ class MigrationCLI: count = await danger_delete_all_roles(self.engine, progress_callback=on_role_deleted) 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.") console.print("[bold green]Done.[/bold green]") except Exception as e: console.print(f"[bold red]Error: {e}[/bold red]") @@ -1118,6 +1128,7 @@ class MigrationCLI: 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('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.") console.print("[bold green]Done.[/bold green]") except Exception as e: console.print(f"[bold red]Error: {e}[/bold red]")