import discord from discord import app_commands from discord.ext import commands import pymongo from bson.objectid import ObjectId import logging from config import GAME_SERVER_PATH, ADMIN_ROLE_ID, MUSIC_FOLDER, DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET, DISCORD_REDIRECT_URI, DISCORD_BOT_TOKEN, DISCORD_GUILD_ID, ACCOUNT_LINKED_ROLE_ID, DISCORD_WEBHOOK_URL import yt_dlp import os import subprocess import re import psutil import time # Set up logging logging.basicConfig(level=logging.DEBUG, format='%(asctime)s:%(levelname)s:%(name)s: %(message)s') logger = logging.getLogger('resonance_rumble_bot') # MongoDB setup client = pymongo.MongoClient('mongodb://localhost:27017/') db = client['resonance_rumble'] classes_collection = db['classes'] weapons_collection = db['weapons'] skins_collection = db['skins'] users_collection = db['users'] achievements_collection = db['achievements'] intents = discord.Intents.all() intents.message_content = True #v2 intents.members = True bot = commands.Bot(command_prefix='!', intents=intents) LINKED_ROLE_NAME = "Account-linked" @bot.event async def on_ready(): print(f'{bot.user} has connected to Discord!') try: synced = await bot.tree.sync() print(f"Synced {len(synced)} command(s)") except Exception as e: print(f"Error syncing commands: {e}") @bot.event async def on_member_join(member): logger.info(f"New member joined: {member.name} (ID: {member.id})") role = discord.utils.get(member.guild.roles, name="Member") if role: try: await member.add_roles(role) logger.info(f"Successfully added Member role to {member.name}") except discord.Forbidden: logger.error(f"Failed to add Member role to {member.name}: Bot doesn't have permission") except discord.HTTPException as e: logger.error(f"Failed to add Member role to {member.name}: {str(e)}") else: logger.error(f"'Member' role not found in the server") # Send a welcome DM to the new member try: welcome_message = ( f"Welcome to the Resonance Rumble Discord server, {member.name}!\n\n" "We're excited to have you join our community. Here are a few things to get you started:\n" "• Come play the game here https://resonancerumble.com\n" "• Introduce yourself in the #introductions channel.\n" "• If you need any help, feel free to ask in the #help channel.\n\n" "Don't forget to link your Discord account in the game to unlock an exclusive class!\n" "Have fun and happy rumbling!" ) await member.send(welcome_message) logger.info(f"Sent welcome DM to {member.name}") except discord.Forbidden: logger.error(f"Failed to send welcome DM to {member.name}: User has DMs disabled") except discord.HTTPException as e: logger.error(f"Failed to send welcome DM to {member.name}: {str(e)}") async def class_autocomplete(interaction: discord.Interaction, current: str) -> list[app_commands.Choice[str]]: classes = classes_collection.find({}, {"name": 1}) return [ app_commands.Choice(name=class_doc["name"], value=class_doc["name"]) for class_doc in classes if current.lower() in class_doc["name"].lower() ][:25] # Discord limits to 25 choices @bot.tree.command(name="class_info", description="Get information about a game class") @app_commands.autocomplete(class_name=class_autocomplete) async def class_info(interaction: discord.Interaction, class_name: str): class_data = classes_collection.find_one({"name": class_name}) if class_data: embed = discord.Embed(title=f"{class_name} Class Info", color=0x00ff00) embed.add_field(name="Health", value=class_data['base_attributes']['health'], inline=True) embed.add_field(name="Speed", value=class_data['base_attributes']['speed'], inline=True) embed.add_field(name="Damage Multiplier", value=class_data['base_attributes']['damage_multiplier'], inline=True) embed.add_field(name="Weapon Limit", value=class_data['weapon_limit'], inline=True) await interaction.response.send_message(embed=embed) else: await interaction.response.send_message(f"Class '{class_name}' not found.", ephemeral=True) async def weapon_autocomplete(interaction: discord.Interaction, current: str) -> list[app_commands.Choice[str]]: weapons = weapons_collection.find({}, {"name": 1}) return [ app_commands.Choice(name=weapon["name"], value=weapon["name"]) for weapon in weapons if current.lower() in weapon["name"].lower() ][:25] @bot.tree.command(name="weapon_info", description="Get information about a weapon") @app_commands.autocomplete(weapon_name=weapon_autocomplete) async def weapon_info(interaction: discord.Interaction, weapon_name: str): weapon_data = weapons_collection.find_one({"name": weapon_name}) if weapon_data: embed = discord.Embed(title=f"{weapon_name} Weapon Info", color=0x0000ff) embed.add_field(name="Type", value=weapon_data['weapon_type'], inline=True) embed.add_field(name="Damage", value=weapon_data['base_attributes']['damage'], inline=True) embed.add_field(name="Fire Rate", value=weapon_data['base_attributes']['fire_rate'], inline=True) embed.add_field(name="Bullet Speed", value=weapon_data['base_attributes']['bullet_speed'], inline=True) await interaction.response.send_message(embed=embed) else: await interaction.response.send_message(f"Weapon '{weapon_name}' not found.", ephemeral=True) async def skin_autocomplete(interaction: discord.Interaction, current: str) -> list[app_commands.Choice[str]]: skins = skins_collection.find({}, {"name": 1}) return [ app_commands.Choice(name=skin["name"], value=skin["name"]) for skin in skins if current.lower() in skin["name"].lower() ][:25] @bot.tree.command(name="skin_info", description="Get information about a skin") @app_commands.autocomplete(skin_name=skin_autocomplete) async def skin_info(interaction: discord.Interaction, skin_name: str): skin_data = skins_collection.find_one({"name": skin_name}) if skin_data: embed = discord.Embed(title=f"{skin_name} Skin Info", color=0xff00ff) embed.add_field(name="Type", value=skin_data['type'], inline=True) embed.add_field(name="Rarity", value=skin_data['rarity'], inline=True) embed.add_field(name="Effect", value=skin_data['effect'], inline=True) await interaction.response.send_message(embed=embed) else: await interaction.response.send_message(f"Skin '{skin_name}' not found.", ephemeral=True) @bot.tree.command(name="leaderboard", description="Show the top players") @app_commands.describe(category="The category for the leaderboard") @app_commands.choices(category=[ app_commands.Choice(name="Level", value="level"), app_commands.Choice(name="Synth Coins", value="synth_coins"), app_commands.Choice(name="Experience", value="experience") ]) async def leaderboard(interaction: discord.Interaction, category: app_commands.Choice[str]): top_players = list(db.users.find().sort(category.value, -1).limit(10)) embed = discord.Embed(title=f"Top 10 Players - {category.name}", color=0xffa500) for i, player in enumerate(top_players, 1): embed.add_field(name=f"{i}. {player['username']}", value=f"{category.name}: {player.get(category.value, 0)}", inline=False) await interaction.response.send_message(embed=embed) @bot.tree.command(name="check_permissions", description="Check bot's permissions") @commands.has_permissions(administrator=True) async def check_permissions(interaction: discord.Interaction): bot_member = interaction.guild.get_member(bot.user.id) permissions = bot_member.guild_permissions embed = discord.Embed(title="Bot Permissions", color=0x00ff00) crucial_perms = ["manage_roles", "view_channel", "send_messages", "embed_links"] for perm, value in permissions: if perm in crucial_perms: embed.add_field(name=perm.replace('_', ' ').title(), value=str(value), inline=False) await interaction.response.send_message(embed=embed) @bot.tree.command(name="link_status", description="Check your Resonance Rumble account link status") async def link_status(interaction: discord.Interaction): user = users_collection.find_one({'discord_id': str(interaction.user.id)}) if user: await add_linked_role(interaction.user) await interaction.response.send_message(f"Your Discord account is linked to the Resonance Rumble account: {user['username']}") else: await interaction.response.send_message("Your Discord account is not linked to any Resonance Rumble account. Use the in-game menu to link your account.") async def add_linked_role(member): guild = member.guild role = discord.utils.get(guild.roles, name=LINKED_ROLE_NAME) if role is None: # Create the role if it doesn't exist role = await guild.create_role(name=LINKED_ROLE_NAME, color=discord.Color.blue()) if role not in member.roles: try: await member.add_roles(role) print(f"Added {LINKED_ROLE_NAME} role to {member.name}") except discord.Forbidden: print(f"Bot doesn't have permission to add roles to {member.name}") except discord.HTTPException as e: print(f"Failed to add role to {member.name}: {str(e)}") # You might want to add a command to manually sync roles @bot.tree.command(name="sync_linked_role", description="Sync the Account-linked role for all linked users") @commands.has_permissions(administrator=True) async def sync_linked_role(interaction: discord.Interaction): await interaction.response.defer() guild = interaction.guild linked_users = users_collection.find({'discord_id': {'$exists': True}}) count = 0 for user in linked_users: member = guild.get_member(int(user['discord_id'])) if member: await add_linked_role(member) count += 1 await interaction.followup.send(f"Synced roles for {count} linked users.") @bot.tree.command(name="downloadsong", description="Download a YouTube video as MP3") @app_commands.describe(url="YouTube video URL") async def download_song(interaction: discord.Interaction, url: str): if not interaction.user.get_role(ADMIN_ROLE_ID): await interaction.response.send_message("You don't have permission to use this command.", ephemeral=True) return await interaction.response.defer() ydl_opts = { 'format': 'bestaudio/best', 'postprocessors': [{ 'key': 'FFmpegExtractAudio', 'preferredcodec': 'mp3', 'preferredquality': '192', }], 'outtmpl': os.path.join(MUSIC_FOLDER, '%(title)s.%(ext)s'), } try: with yt_dlp.YoutubeDL(ydl_opts) as ydl: info = ydl.extract_info(url, download=False) title = info['title'] artist = info.get('artist', 'Unknown') # Format the filename formatted_title = re.sub(r'[^\w\-]', '', title.title().replace(' ', '')) formatted_artist = re.sub(r'[^\w\-]', '', artist.title().replace(' ', '')) filename = f"{formatted_artist}-{formatted_title}.mp3" ydl_opts['outtmpl'] = os.path.join(MUSIC_FOLDER, filename) ydl.download([url]) await interaction.followup.send(f"Successfully downloaded: {filename}") except Exception as e: await interaction.followup.send(f"An error occurred: {str(e)}") @bot.tree.command(name="restartserver", description="Restart or start the game server") async def restart_server(interaction: discord.Interaction): if not interaction.user.get_role(ADMIN_ROLE_ID): await interaction.response.send_message("You don't have permission to use this command.", ephemeral=True) return await interaction.response.defer() server_was_running = False try: # Find and kill the existing game_server.py process for proc in psutil.process_iter(['pid', 'name', 'cmdline']): if 'python' in proc.info['name'].lower() and any('game_server.py' in cmd.lower() for cmd in proc.info['cmdline']): server_was_running = True proc.terminate() proc.wait(timeout=10) # Wait for the process to terminate break if server_was_running: # Wait a moment to ensure the port is freed time.sleep(2) # Start the process subprocess.Popen(["py", GAME_SERVER_PATH], shell=True) if server_was_running: await interaction.followup.send("Game server has been restarted successfully.") else: await interaction.followup.send("Game server has been started successfully.") except psutil.NoSuchProcess: await interaction.followup.send("Failed to restart the game server. The process disappeared unexpectedly. Attempting to start a new instance.") subprocess.Popen(["python", GAME_SERVER_PATH], shell=True) except psutil.AccessDenied: await interaction.followup.send("Failed to restart the game server. Access denied when trying to terminate the process. Attempting to start a new instance.") subprocess.Popen(["python", GAME_SERVER_PATH], shell=True) except psutil.TimeoutExpired: await interaction.followup.send("Failed to restart the game server. Timeout occurred while waiting for the process to terminate. Attempting to start a new instance.") subprocess.Popen(["python", GAME_SERVER_PATH], shell=True) except Exception as e: await interaction.followup.send(f"An error occurred while managing the server: {str(e)}") bot.run(DISCORD_BOT_TOKEN)