Removed backup files and unused legacy scanner function. All code verified as unreferenced.
1046 lines
45 KiB
Python
Executable File
1046 lines
45 KiB
Python
Executable File
#!/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() |