#!/usr/bin/env python3 """ RedFlag Discord Management Bot Interactive Discord server management with secure configuration """ import discord import asyncio import logging import sys from typing import Optional, Dict, List from discord.ext import commands from discord import app_commands from env_manager import discord_env # Configure logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) logger = logging.getLogger(__name__) class DiscordManager: """Interactive Discord server manager""" def __init__(self): self.bot_token = discord_env.get_required('DISCORD_BOT_TOKEN') self.server_id = int(discord_env.get_required('DISCORD_SERVER_ID')) self.application_id = discord_env.get_required('DISCORD_APPLICATION_ID') self.public_key = discord_env.get_required('DISCORD_PUBLIC_KEY') # Bot setup with required intents intents = discord.Intents.default() intents.guilds = True intents.message_content = True # Initialize bot self.bot = commands.Bot( command_prefix='!', intents=intents, help_command=None # We'll create custom help ) self.setup_events() self.setup_commands() def setup_events(self): """Setup bot event handlers""" @self.bot.event async def on_ready(): logger.info(f'✅ Bot logged in as {self.bot.user}') logger.info(f'Serving server: {self.bot.user.name} (ID: {self.bot.user.id})') # Sync commands to guild specifically (more reliable) guild = self.bot.get_guild(self.server_id) # Sync commands globally await self.bot.tree.sync() logger.info('✅ Commands synced globally') # Get server info guild = self.bot.get_guild(self.server_id) if guild: logger.info(f'✅ Connected to server: {guild.name}') await self.print_server_status(guild) else: logger.error('❌ Could not find server!') @self.bot.event async def on_command_error(ctx, error): """Handle command errors""" logger.error(f'Command error in {ctx.command}: {error}') if isinstance(error, commands.MissingRequiredArgument): await ctx.send(f'❌ Missing required argument: `{error.param}`') elif isinstance(error, commands.BadArgument): await ctx.send(f'❌ Invalid argument: {error}') else: await ctx.send(f'❌ An error occurred: {error}') @self.bot.event async def on_interaction_error(interaction, error): """Handle interaction errors""" logger.error(f'Interaction error: {error}') if interaction.response.is_done(): await interaction.followup.send(f'❌ An error occurred: {error}', ephemeral=True) else: await interaction.response.send_message(f'❌ An error occurred: {error}', ephemeral=True) def setup_commands(self): """Setup slash commands""" # Server management commands @self.bot.tree.command(name="status", description="Show current Discord server status") async def cmd_status(interaction: discord.Interaction): await self.cmd_status(interaction) @self.bot.tree.command(name="create-channels", description="Create standard RedFlag channels") async def cmd_create_channels(interaction: discord.Interaction): await self.cmd_create_channels(interaction) @self.bot.tree.command(name="send-message", description="Send a message to a channel") @app_commands.describe(channel="Channel to send message to", message="Message to send") async def cmd_send_message(interaction: discord.Interaction, channel: str, message: str): await self.cmd_send_message(interaction, channel, message) @self.bot.tree.command(name="list-channels", description="List all channels and categories") async def cmd_list_channels(interaction: discord.Interaction): await self.cmd_list_channels(interaction) @self.bot.tree.command(name="create-category", description="Create a channel category") @app_commands.describe(name="Category name") async def cmd_create_category(interaction: discord.Interaction, name: str): await self.cmd_create_category(interaction, name) @self.bot.tree.command(name="create-test-channel", description="Create one simple test channel") async def cmd_create_test_channel(interaction: discord.Interaction): await self.cmd_create_test_channel(interaction) @self.bot.tree.command(name="create-roles", description="Create RedFlag community roles") async def cmd_create_roles(interaction: discord.Interaction): await self.cmd_create_roles(interaction) @self.bot.tree.command(name="role-menu", description="Show interactive role assignment menu") async def cmd_role_menu(interaction: discord.Interaction): await self.cmd_role_menu(interaction) @self.bot.tree.command(name="assign-lead-dev", description="Assign RedFlag Lead Dev role *(Admin only)*") async def cmd_assign_lead_dev(interaction: discord.Interaction, user: discord.Member): await self.cmd_assign_lead_dev(interaction, user) @self.bot.tree.command(name="setup-welcome", description="Setup welcome channel with message and role selector *(Admin only)*") async def cmd_setup_welcome(interaction: discord.Interaction): await self.cmd_setup_welcome(interaction) @self.bot.tree.command(name="create-version-channels", description="Create version-related channels *(Admin only)*") async def cmd_create_version_channels(interaction: discord.Interaction): await self.cmd_create_version_channels(interaction) @self.bot.tree.command(name="sync-commands", description="Force sync commands *(Admin only)*") async def cmd_sync_commands(interaction: discord.Interaction): await self.cmd_sync_commands(interaction) @self.bot.tree.command(name="create-redflag-channels", description="Create RedFlag homelab management channels") async def cmd_create_redflag_channels(interaction: discord.Interaction): await self.cmd_create_redflag_channels(interaction) @self.bot.tree.command(name="test", description="Test command") async def cmd_test(interaction: discord.Interaction): await interaction.response.send_message("✅ Test command works!", ephemeral=True) @self.bot.tree.command(name="create-welcome-banner", description="Create a welcome banner in a channel") @app_commands.describe(channel="Channel to create banner in") async def cmd_create_welcome_banner(interaction: discord.Interaction, channel: discord.TextChannel): await self.cmd_create_welcome_banner(interaction, channel) @self.bot.tree.command(name="list-commands", description="List all available bot commands") async def cmd_list_commands_debug(interaction: discord.Interaction): await self.cmd_list_commands_debug(interaction) @self.bot.tree.command(name="help", description="Show available commands") async def cmd_help(interaction: discord.Interaction): await self.cmd_help(interaction) async def cmd_status(self, interaction: discord.Interaction): """Show server status""" guild = self.bot.get_guild(self.server_id) if not guild: await interaction.response.send_message("❌ Could not find server!", ephemeral=True) return embed = discord.Embed( title="📊 RedFlag Discord Server Status", color=discord.Color.blue(), description=f"Server: **{guild.name}**" ) embed.add_field(name="👥 Members", value=str(guild.member_count), inline=True) embed.add_field(name="💬 Channels", value=str(len(guild.channels)), inline=True) embed.add_field(name="🎭 Roles", value=str(len(guild.roles)), inline=True) embed.add_field(name="📅 Created", value=guild.created_at.strftime("%Y-%m-%d"), inline=True) embed.add_field(name="👑 Owner", value=f"<@{guild.owner_id}>", inline=True) embed.add_field(name="🚀 Boost Level", value=str(guild.premium_tier), inline=True) await interaction.response.send_message(embed=embed) async def cmd_create_channels(self, interaction: discord.Interaction): """Create standard RedFlag channels""" guild = self.bot.get_guild(self.server_id) if not guild: await interaction.response.send_message("❌ Could not find server!", ephemeral=True) return await interaction.response.defer(ephemeral=True) results = [] # Create categories first try: # Community category community_cat = await guild.create_category_channel("🌍 Community") discord_env.update_category_ids("community", str(community_cat.id)) results.append("✅ Community category") # Development category dev_cat = await guild.create_category_channel("💻 Development") discord_env.update_category_ids("development", str(dev_cat.id)) results.append("✅ Development category") # Security category security_cat = await guild.create_category_channel("🔒 Security") discord_env.update_category_ids("security", str(security_cat.id)) results.append("✅ Security category") except Exception as e: logger.error(f"Error creating categories: {e}") results.append(f"❌ Categories: {e}") # Create channels (with small delays to avoid rate limits) await asyncio.sleep(1) # Community channels try: general = await guild.create_text_channel( "general", category=discord.Object(id=int(discord_env.get('COMMUNITY_CATEGORY_ID', 0))), reason="Community general discussion" ) discord_env.update_channel_ids("general", str(general.id)) results.append("✅ #general") announcements = await guild.create_text_channel( "announcements", category=discord.Object(id=int(discord_env.get('COMMUNITY_CATEGORY_ID', 0))), reason="Project announcements" ) discord_env.update_channel_ids("announcements", str(announcements.id)) results.append("✅ #announcements") except Exception as e: logger.error(f"Error creating community channels: {e}") results.append(f"❌ Community channels: {e}") await asyncio.sleep(1) # Security channels try: security_alerts = await guild.create_text_channel( "security-alerts", category=discord.Object(id=int(discord_env.get('SECURITY_CATEGORY_ID', 0))), reason="Security alerts and notifications" ) discord_env.update_channel_ids("security-alerts", str(security_alerts.id)) results.append("✅ #security-alerts") except Exception as e: logger.error(f"Error creating security channels: {e}") results.append(f"❌ Security channels: {e}") await asyncio.sleep(1) # Development channels try: dev_chat = await guild.create_text_channel( "dev-chat", category=discord.Object(id=int(discord_env.get('DEVELOPMENT_CATEGORY_ID', 0))), reason="Development discussions" ) discord_env.update_channel_ids("dev-chat", str(dev_chat.id)) results.append("✅ #dev-chat") bug_reports = await guild.create_text_channel( "bug-reports", category=discord.Object(id=int(discord_env.get('DEVELOPMENT_CATEGORY_ID', 0))), reason="Bug reports and issues" ) discord_env.update_channel_ids("bug-reports", str(bug_reports.id)) results.append("✅ #bug-reports") except Exception as e: logger.error(f"Error creating development channels: {e}") results.append(f"❌ Development channels: {e}") # Send results embed = discord.Embed( title="🔧 Channel Creation Results", color=discord.Color.green() if "❌" not in str(results) else discord.Color.red(), description="\n".join(results) ) await interaction.followup.send(embed=embed, ephemeral=True) async def cmd_send_message(self, interaction: discord.Interaction, channel: str, message: str): """Send a message to a specific channel""" guild = self.bot.get_guild(self.server_id) if not guild: await interaction.response.send_message("❌ Could not find server!", ephemeral=True) return # Find channel by name target_channel = discord.utils.get(guild.text_channels, name=channel.lower()) if not target_channel: await interaction.response.send_message(f"❌ Channel '{channel}' not found!", ephemeral=True) return try: await target_channel.send(message) await interaction.response.send_message( f"✅ Message sent to #{channel}!", ephemeral=True ) except Exception as e: logger.error(f"Error sending message: {e}") await interaction.response.send_message( f"❌ Failed to send message: {e}", ephemeral=True ) async def cmd_list_channels(self, interaction: discord.Interaction): """List all channels and categories""" guild = self.bot.get_guild(self.server_id) if not guild: await interaction.response.send_message("❌ Could not find server!", ephemeral=True) return embed = discord.Embed( title="📋 Server Channels", color=discord.Color.blue() ) # List categories categories = [c for c in guild.categories if c.name] if categories: category_text = "\n".join([f"**{c.name}** (ID: {c.id})" for c in categories]) embed.add_field(name="📂 Categories", value=category_text or "None", inline=False) # List text channels text_channels = [c for c in guild.text_channels if c.category] if text_channels: channel_text = "\n".join([f"#{c.name} (ID: {c.id})" for c in text_channels]) embed.add_field(name="💬 Text Channels", value=channel_text or "None", inline=False) # List voice channels voice_channels = [c for c in guild.voice_channels if c.category] if voice_channels: voice_text = "\n".join([f"🎤 {c.name} (ID: {c.id})" for c in voice_channels]) embed.add_field(name="🎤 Voice Channels", value=voice_text or "None", inline=False) await interaction.response.send_message(embed=embed, ephemeral=True) async def cmd_create_category(self, interaction: discord.Interaction, name: str): """Create a new category""" guild = self.bot.get_guild(self.server_id) if not guild: await interaction.response.send_message("❌ Could not find server!", ephemeral=True) return try: category = await guild.create_category_channel(name) await interaction.response.send_message( f"✅ Created category: **{name}** (ID: {category.id})", ephemeral=True ) except Exception as e: logger.error(f"Error creating category: {e}") await interaction.response.send_message( f"❌ Failed to create category: {e}", ephemeral=True ) async def cmd_create_test_channel(self, interaction: discord.Interaction): """Create one simple test channel""" guild = self.bot.get_guild(self.server_id) if not guild: await interaction.response.send_message("❌ Could not find server!", ephemeral=True) return try: # Create a simple text channel test_channel = await guild.create_text_channel( "test-channel", reason="Testing bot channel creation" ) await interaction.response.send_message( f"✅ Created test channel: **#{test_channel.name}**", ephemeral=True ) except Exception as e: logger.error(f"Error creating test channel: {e}") await interaction.response.send_message( f"❌ Failed to create test channel: {e}", ephemeral=True ) async def cmd_create_redflag_channels(self, interaction: discord.Interaction): """Create RedFlag development/support Discord channels""" guild = self.bot.get_guild(self.server_id) if not guild: await interaction.response.send_message("❌ Could not find server!", ephemeral=True) return await interaction.response.defer(ephemeral=True) results = [] try: # Create categories for community Discord welcome_cat = await guild.create_category_channel("👋 Welcome & Info") results.append("✅ Welcome & Info category") support_cat = await guild.create_category_channel("💬 Support & Help") results.append("✅ Support & Help category") dev_cat = await guild.create_category_channel("🔧 Development") results.append("✅ Development category") community_cat = await guild.create_category_channel("🌍 Community") results.append("✅ Community category") await asyncio.sleep(1) # Welcome & Info channels rules = await guild.create_text_channel( "rules-and-info", category=welcome_cat, reason="Community rules and project information" ) results.append("✅ #rules-and-info") announcements = await guild.create_text_channel( "announcements", category=welcome_cat, reason="Project announcements and releases" ) results.append("✅ #announcements") await asyncio.sleep(1) # Support & Help channels general_support = await guild.create_text_channel( "general-support", category=support_cat, reason="General RedFlag support and questions" ) results.append("✅ #general-support") installation = await guild.create_text_channel( "installation-help", category=support_cat, reason="Help with RedFlag installation and setup" ) results.append("✅ #installation-help") bug_reports = await guild.create_text_channel( "bug-reports", category=support_cat, reason="Bug reports and troubleshooting" ) results.append("✅ #bug-reports") await asyncio.sleep(1) # Development channels general_dev = await guild.create_text_channel( "general-development", category=dev_cat, reason="General development discussions" ) results.append("✅ #general-development") feature_requests = await guild.create_text_channel( "feature-requests", category=dev_cat, reason="Feature requests and ideas" ) results.append("✅ #feature-requests") code_review = await guild.create_text_channel( "code-review", category=dev_cat, reason="Code review and development collaboration" ) results.append("✅ #code-review") await asyncio.sleep(1) # Community channels general_chat = await guild.create_text_channel( "general-chat", category=community_cat, reason="Off-topic community chat" ) results.append("✅ #general-chat") homelab = await guild.create_text_channel( "homelab-showcase", category=community_cat, reason="Share your homelab setups and RedFlag deployments" ) results.append("✅ #homelab-showcase") # Update .env with important channel IDs discord_env.update_channel_ids("announcements", str(announcements.id)) discord_env.update_channel_ids("general-support", str(general_support.id)) discord_env.update_channel_ids("bug-reports", str(bug_reports.id)) discord_env.update_channel_ids("general-development", str(general_dev.id)) except Exception as e: logger.error(f"Error creating RedFlag community channels: {e}") results.append(f"❌ Error: {e}") embed = discord.Embed( title="🏠 RedFlag Community Discord Setup", color=discord.Color.green() if "❌" not in str(results) else discord.Color.red(), description="Created RedFlag development/support community channels:\n\n" + "\n".join(results) ) await interaction.followup.send(embed=embed, ephemeral=True) async def cmd_create_roles(self, interaction: discord.Interaction): """Create RedFlag community roles""" guild = self.bot.get_guild(self.server_id) if not guild: await interaction.response.send_message("❌ Could not find server!", ephemeral=True) return # Only allow administrators to create roles if not interaction.user.guild_permissions.administrator: await interaction.response.send_message("❌ Only administrators can create roles!", ephemeral=True) return await interaction.response.defer(ephemeral=True) results = [] # Define RedFlag roles redflag_roles = { "🚩 RedFlag Lead Dev": discord.Color.red(), "🛠 Backend Dev": discord.Color.blue(), "🎨 Frontend Dev": discord.Color.green(), "🔍 QA Tester": discord.Color.orange(), "💬 Community Helper": discord.Color.purple(), "👤 User": discord.Color.greyple(), "👀 Lurker": discord.Color.dark_grey(), } for role_name, role_color in redflag_roles.items(): try: # Check if role already exists existing_role = discord.utils.get(guild.roles, name=role_name) if existing_role: results.append(f"⚠️ {role_name} already exists") continue # Create the role role = await guild.create_role( name=role_name, color=role_color, reason="RedFlag community role creation", mentionable=True ) results.append(f"✅ Created {role_name}") # Store role ID in .env for future reference safe_name = role_name.replace("🚩 ", "").replace("🛠 ", "").replace("🎨 ", "").replace("🔍 ", "").replace("💬 ", "").replace("👤 ", "").replace("👀 ", "").lower().replace(" ", "_") discord_env._config[f"ROLE_{safe_name.upper()}_ID"] = str(role.id) except Exception as e: logger.error(f"Error creating role {role_name}: {e}") results.append(f"❌ Failed to create {role_name}: {e}") embed = discord.Embed( title="🎭 Role Creation Results", color=discord.Color.green() if "❌" not in str(results) else discord.Color.red(), description="\n".join(results) ) await interaction.followup.send(embed=embed, ephemeral=True) async def cmd_role_menu(self, interaction: discord.Interaction): """Show interactive role assignment menu""" guild = self.bot.get_guild(self.server_id) if not guild: await interaction.response.send_message("❌ Could not find server!", ephemeral=True) return # Create the view with role buttons view = discord.ui.View(timeout=180) # 3 minutes timeout # Available roles for self-assignment (excluding Lead Dev) available_roles = [ ("🛠 Backend Dev", discord.Color.blue()), ("🎨 Frontend Dev", discord.Color.green()), ("🔍 QA Tester", discord.Color.orange()), ("💬 Community Helper", discord.Color.purple()), ("👤 User", discord.Color.greyple()), ("👀 Lurker", discord.Color.dark_grey()), ] # Create buttons for each role for role_name, role_color in available_roles: button = discord.ui.Button( label=role_name.replace("🛠 ", "").replace("🎨 ", "").replace("🔍 ", "").replace("💬 ", "").replace("👤 ", "").replace("👀 ", ""), emoji=role_name.split()[0], # Get the emoji style=discord.ButtonStyle.secondary ) async def button_callback(interaction: discord.Interaction, current_role_name=role_name): await self.handle_role_assignment(interaction, current_role_name) button.callback = button_callback view.add_item(button) embed = discord.Embed( title="🎭 Choose Your RedFlag Role", description="Click a button below to assign yourself a role. You can change your role anytime!", color=discord.Color.blue() ) embed.add_field( name="🚩 RedFlag Lead Dev", value="This role is assigned by administrators only", inline=False ) embed.set_footer(text="You can only have one role at a time. Click again to change roles.") await interaction.response.send_message(embed=embed, view=view, ephemeral=True) async def cmd_assign_lead_dev(self, interaction: discord.Interaction, user: discord.Member): """Assign RedFlag Lead Dev role (admin only)""" guild = self.bot.get_guild(self.server_id) if not guild: await interaction.response.send_message("❌ Could not find server!", ephemeral=True) return # Only allow administrators to assign Lead Dev role if not interaction.user.guild_permissions.administrator: await interaction.response.send_message("❌ Only administrators can assign the Lead Dev role!", ephemeral=True) return await interaction.response.defer(ephemeral=True) # Find the Lead Dev role lead_role = discord.utils.get(guild.roles, name="🚩 RedFlag Lead Dev") if not lead_role: await interaction.followup.send("❌ Lead Dev role not found! Please create roles first.", ephemeral=True) return try: # Remove existing RedFlag roles from the user redflag_role_prefixes = ["🚩 ", "🛠 ", "🎨 ", "🔍 ", "💬 ", "👤 ", "👀 "] current_roles = [role for role in user.roles if any(role.name.startswith(prefix) for prefix in redflag_role_prefixes)] if current_roles: await user.remove_roles(*current_roles, reason="Assigned Lead Dev role") # Assign Lead Dev role await user.add_roles(lead_role, reason="Assigned by admin") await interaction.followup.send(f"✅ Assigned **🚩 RedFlag Lead Dev** to {user.mention}", ephemeral=True) except Exception as e: logger.error(f"Error assigning Lead Dev role: {e}") await interaction.followup.send(f"❌ Failed to assign role: {e}", ephemeral=True) async def cmd_setup_welcome(self, interaction: discord.Interaction): """Setup welcome channel with message and role selector""" guild = self.bot.get_guild(self.server_id) if not guild: await interaction.response.send_message("❌ Could not find server!", ephemeral=True) return # Only allow administrators if not interaction.user.guild_permissions.administrator: await interaction.response.send_message("❌ Only administrators can setup the welcome channel!", ephemeral=True) return await interaction.response.defer(ephemeral=True) results = [] try: # Find the general channel (try multiple names) general_channel = None possible_names = ["general", "🏠127.0.0.1", "🏠localhost", "welcome", "welcome-and-info"] for name in possible_names: general_channel = discord.utils.get(guild.text_channels, name=name) if general_channel: break if not general_channel: # If no specific channel found, just use the first text channel (any category) logger.info(f"Using first available text channel: {guild.text_channels[0].name}") general_channel = guild.text_channels[0] logger.info(f"Selected channel: {general_channel.name} (Category: {general_channel.category.name if general_channel.category else 'No category'})") if not general_channel: await interaction.followup.send("❌ Could not find any text channel to use!", ephemeral=True) return # Rename the channel to localhost with house emoji await general_channel.edit(name="🏠localhost", reason="Setup welcome channel") results.append("✅ Renamed general to 🏠localhost") # Create welcome message with role selector welcome_embed = discord.Embed( title="🏠 Welcome to RedFlag", description="**Self-hosted update management for homelabs**", color=discord.Color.blue() ) welcome_embed.add_field( name="⚠️ ALPHA SOFTWARE", value="This is experimental software in active development. Features may be broken, bugs are expected, and breaking changes happen frequently. Use at your own risk, preferably on test systems only.", inline=False ) welcome_embed.add_field( name="🤝 Community & Support", value="""**Discord Maintenance:** Full disclosure - Discord community management isn't my strongest area. If we grow over 100 users, I'll be looking to vet a moderator to help keep things organized. **Response Times:** I *should* get alerts and will try to respond timely, but this place is a community for us all to grow and share in. **Community Guidelines:** Small requests that are slightly off-topic are totally fine. We're building a community around homelabs, update management, and practical solutions - not a corporate support channel.""", inline=False ) welcome_embed.add_field( name="🚀 Get Started", value="1. **Choose Your Role** below - This helps us know how you're using RedFlag\n2. **Introduce Yourself** in #general-chat\n3. **Share Your Setup** in #homelab-showcase\n4. **Ask Questions** in #general-support", inline=False ) welcome_embed.set_footer(text="RedFlag - Simple, Honest, Homelab-first") welcome_embed.set_thumbnail(url=guild.icon.url if guild.icon else None) # Create role selector view view = discord.ui.View(timeout=None) # Persistent view # Available roles for self-assignment available_roles = [ ("🛠 Backend Dev", discord.Color.blue()), ("🎨 Frontend Dev", discord.Color.green()), ("🔍 QA Tester", discord.Color.orange()), ("💬 Community Helper", discord.Color.purple()), ("👤 User", discord.Color.greyple()), ("👀 Lurker", discord.Color.dark_grey()), ] # Create buttons for each role for role_name, role_color in available_roles: button = discord.ui.Button( label=role_name.replace("🛠 ", "").replace("🎨 ", "").replace("🔍 ", "").replace("💬 ", "").replace("👤 ", "").replace("👀 ", ""), emoji=role_name.split()[0], style=discord.ButtonStyle.secondary, custom_id=f"role_select_{role_name.replace(' ', '_').replace('🛠', '').replace('🎨', '').replace('🔍', '').replace('💬', '').replace('👤', '').replace('👀', '')}" ) async def button_callback(interaction: discord.Interaction, current_role_name=role_name): await self.handle_role_assignment(interaction, current_role_name) button.callback = button_callback view.add_item(button) # Set channel topic with important info topic = "🏠 Welcome! Use /role-menu to choose your role. RedFlag: Self-hosted update management for homelabs. ALPHA SOFTWARE - expect bugs!" await general_channel.edit(topic=topic, reason="Set welcome channel topic") # Send the welcome message await general_channel.send(embed=welcome_embed, view=view) results.append("✅ Posted welcome message with role selector and channel topic") except Exception as e: logger.error(f"Error setting up welcome channel: {e}") results.append(f"❌ Error: {e}") embed = discord.Embed( title="🏠 Welcome Channel Setup Complete", color=discord.Color.green() if "❌" not in str(results) else discord.Color.red(), description="\n".join(results) ) await interaction.followup.send(embed=embed, ephemeral=True) async def cmd_create_version_channels(self, interaction: discord.Interaction): """Create version-related channels""" guild = self.bot.get_guild(self.server_id) if not guild: await interaction.response.send_message("❌ Could not find server!", ephemeral=True) return # Only allow administrators if not interaction.user.guild_permissions.administrator: await interaction.response.send_message("❌ Only administrators can create version channels!", ephemeral=True) return await interaction.response.defer(ephemeral=True) results = [] try: # Create version category version_cat = await guild.create_category_channel("📦 Version Management") results.append("✅ Version Management category") await asyncio.sleep(1) # Main version channel main_version = await guild.create_text_channel( "🎯main", category=version_cat, reason="Main stable version discussion" ) results.append("✅ #main (stable version)") # Tagged versions channel tagged_versions = await guild.create_text_channel( "🏷️tagged", category=version_cat, reason="Tagged release versions discussion" ) results.append("✅ #tagged (release versions)") # Unstable dev channel unstable_dev = await guild.create_text_channel( "🔮unstable-developer", category=version_cat, reason="Unstable developer branch discussion" ) results.append("✅ #unstable-developer (dev branch)") # Update .env with channel IDs discord_env.update_channel_ids("main_version", str(main_version.id)) discord_env.update_channel_ids("tagged_versions", str(tagged_versions.id)) discord_env.update_channel_ids("unstable_dev", str(unstable_dev.id)) except Exception as e: logger.error(f"Error creating version channels: {e}") results.append(f"❌ Error: {e}") embed = discord.Embed( title="📦 Version Channels Created", color=discord.Color.green() if "❌" not in str(results) else discord.Color.red(), description="Created version management channels:\n\n" + "\n".join(results) ) await interaction.followup.send(embed=embed, ephemeral=True) async def cmd_sync_commands(self, interaction: discord.Interaction): """Force sync commands""" # Only allow administrators if not interaction.user.guild_permissions.administrator: await interaction.response.send_message("❌ Only administrators can sync commands!", ephemeral=True) return await interaction.response.defer(ephemeral=True) try: # Sync commands globally synced = await self.bot.tree.sync() await interaction.followup.send(f"✅ Synced {len(synced)} commands globally!", ephemeral=True) logger.info(f"Manually synced {len(synced)} commands") except Exception as e: logger.error(f"Error syncing commands: {e}") await interaction.followup.send(f"❌ Failed to sync commands: {e}", ephemeral=True) async def handle_role_assignment(self, interaction: discord.Interaction, role_name: str): """Handle role assignment from button click""" guild = self.bot.get_guild(self.server_id) if not guild: await interaction.response.send_message("❌ Could not find server!", ephemeral=True) return # Find the role target_role = discord.utils.get(guild.roles, name=role_name) if not target_role: await interaction.response.send_message("❌ Role not found! Please ask an admin to create roles first.", ephemeral=True) return # Get all RedFlag roles (for removal) redflag_role_prefixes = ["🚩 ", "🛠 ", "🎨 ", "🔍 ", "💬 ", "👤 ", "👀 "] current_roles = [role for role in interaction.user.roles if any(role.name.startswith(prefix) for prefix in redflag_role_prefixes)] try: # Remove existing RedFlag roles if current_roles: await interaction.user.remove_roles(*current_roles, reason="Role change via bot") # Add new role await interaction.user.add_roles(target_role, reason="Self-assigned via bot") # Update the original message to show success await interaction.response.edit_message( content=f"✅ Successfully assigned role: **{role_name}**", view=None # Remove buttons after selection ) except Exception as e: logger.error(f"Error assigning role {role_name}: {e}") await interaction.response.send_message(f"❌ Failed to assign role: {e}", ephemeral=True) async def cmd_create_welcome_banner(self, interaction: discord.Interaction, channel: discord.TextChannel): """Create a welcome banner in a channel""" try: # Check if user has admin permissions if not interaction.user.guild_permissions.administrator: await interaction.response.send_message("❌ This command requires Administrator permissions.", ephemeral=True) return await interaction.response.defer() # Create simple welcome embed embed = discord.Embed( title="🏠 RedFlag", description="Self-hosted update management for homelabs", color=discord.Color.red() ) embed.add_field( name="Links", value="[GitHub](https://github.com/Fimeg/RedFlag) • [Issues](https://github.com/Fimeg/RedFlag/issues)", inline=False ) embed.set_thumbnail(url="https://raw.githubusercontent.com/Fimeg/RedFlag/main/website/public/favicon.svg") # Send and pin the welcome message message = await channel.send(embed=embed) await message.pin() await interaction.followup.send(f"✅ Created welcome banner in #{channel.name}!", ephemeral=True) logger.info(f"Created welcome banner in #{channel.name}") except Exception as e: logger.error(f"Error converting announcement channel: {e}") await interaction.followup.send(f"❌ Error converting channel: {e}", ephemeral=True) async def cmd_list_commands_debug(self, interaction: discord.Interaction): """List all registered commands for debugging""" try: commands = self.bot.tree.get_commands(guild=discord.Object(id=self.server_id)) command_list = [] for cmd in commands: if hasattr(cmd, 'name') and hasattr(cmd, 'description'): command_list.append(f"**/{cmd.name}** - {cmd.description}") embed = discord.Embed( title="🔍 Registered Commands Debug", description=f"Found {len(command_list)} commands:", color=discord.Color.gold() ) if command_list: embed.add_field(name="Available Commands", value="\n".join(command_list), inline=False) else: embed.description = "No commands found!" await interaction.response.send_message(embed=embed, ephemeral=True) except Exception as e: await interaction.response.send_message(f"❌ Error listing commands: {e}", ephemeral=True) async def cmd_help(self, interaction: discord.Interaction): """Show help information""" embed = discord.Embed( title="🤖 RedFlag Discord Bot Help", description="Interactive Discord server management commands", color=discord.Color.blue() ) commands_info = [ ("`/status`", "📊 Show server status"), ("`/create-channels`", "🔧 Create standard channels"), ("`/create-redflag-channels`", "🏠 Create RedFlag community channels"), ("`/create-roles`", "🎭 Create RedFlag community roles *(Admin only)*"), ("`/setup-welcome`", "🏠 Setup welcome channel with role selector *(Admin only)*"), ("`/create-version-channels`", "📦 Create version management channels *(Admin only)*"), ("`/role-menu`", "🎮 Show interactive role assignment menu"), ("`/assign-lead-dev`", "🚩 Assign Lead Dev role *(Admin only)*"), ("`/list-channels`", "📋 List all channels"), ("`/send-message`", "💬 Send message to channel"), ("`/create-category`", "📂 Create new category"), ("`/create-test-channel`", "🧪 Create one test channel"), ("`/help`", "❓ Show this help"), ] for cmd, desc in commands_info: embed.add_field(name=cmd, value=desc, inline=False) embed.add_field( name="🛡️ Security Features", value="✅ Secure configuration management\n✅ Token protection\n✅ Rate limiting", inline=False ) embed.add_field( name="🔄 Live Updates", value="Configuration changes are saved instantly to .env file", inline=False ) embed.set_footer(text="RedFlag Security Discord Management v1.0") await interaction.response.send_message(embed=embed, ephemeral=True) async def print_server_status(self, guild): """Print detailed server status to console""" print(f"\n{'='*60}") print(f"🚀 REDFLAG DISCORD SERVER STATUS") print(f"{'='*60}") print(f"Server Name: {guild.name}") print(f"Server ID: {guild.id}") print(f"Members: {guild.member_count}") print(f"Channels: {len(guild.channels)}") print(f"Roles: {len(guild.roles)}") print(f"Owner: <@{guild.owner_id}>") print(f"Created: {guild.created_at}") print(f"Boost Level: {guild.premium_tier}") print(f"{'='*60}\n") async def run(self): """Start the bot""" try: # Connect to Discord await self.bot.start(self.bot_token) except discord.errors.LoginFailure: logger.error("❌ Invalid Discord bot token!") logger.error("Please check your DISCORD_BOT_TOKEN in .env file") sys.exit(1) except Exception as e: logger.error(f"❌ Failed to start bot: {e}") sys.exit(1) def main(): """Main entry point""" print("🚀 Starting RedFlag Discord Management Bot...") # Check configuration if not discord_env.is_configured(): print("❌ Discord not configured!") print("Please:") print("1. Copy .env.example to .env") print("2. Fill in your Discord bot token and server ID") print("3. Run this script again") sys.exit(1) # Create and run bot bot = DiscordManager() asyncio.run(bot.run()) if __name__ == "__main__": main()