diff --git a/RFT.py b/RFT.py index f779d99..bf6888c 100644 --- a/RFT.py +++ b/RFT.py @@ -1,43 +1,644 @@ +""" +DISCORD BOT - Personal Best Tracker for Multiple Bosses +====================================================== + +COMMANDS DOCUMENTATION: +===================== + +📊 PERSONAL BEST COMMANDS: +-------------------------- +!pbhydra → Show your Hydra PB + screenshot +!pbhydra → Show username's Hydra PB + screenshot +!pbhydra → Submit new Hydra PB (requires screenshot attachment) + +!pbchimera → Show your Chimera PB + screenshot +!pbchimera → Show username's Chimera PB + screenshot +!pbchimera → Submit new Chimera PB (requires screenshot attachment) + +!pbcvc → Show your CvC PB + screenshot +!pbcvc → Show username's CvC PB + screenshot +!pbcvc → Submit new CvC PB (requires screenshot attachment) + +🏆 LEADERBOARD COMMANDS (GLOBAL): +-------------------------------- +!top10hydra → Top 10 Hydra records (all clans) +!top10chimera → Top 10 Chimera records (all clans) +!top10cvc → Top 10 CvC records (all clans) + +⭐ LEADERBOARD COMMANDS (RTF CLAN): +---------------------------------- +!rtfhydra → Top 10 Hydra records (RTF clan only) +!rtfchimera → Top 10 Chimera records (RTF clan only) +!rtfcvc → Top 10 CvC records (RTF clan only) + +🔥 LEADERBOARD COMMANDS (RTFC CLAN): +----------------------------------- +!rtfchydra → Top 10 Hydra records (RTFC clan only) +!rtfcchimera → Top 10 Chimera records (RTFC clan only) +!rtfccvc → Top 10 CvC records (RTFC clan only) + +⚡ LEADERBOARD COMMANDS (RTFR CLAN): +----------------------------------- +!rtfrhydra → Top 10 Hydra records (RTFR clan only) +!rtfrchimera → Top 10 Chimera records (RTFR clan only) +!rtfrcvc → Top 10 CvC records (RTFR clan only) + +📈 STATS COMMANDS: +----------------- +!mystats → Show all your PBs across all bosses +!mystats → Show all PBs for specified user + +💡 USAGE EXAMPLES: +----------------- +!pbhydra [RTF]Alice → Shows Alice's Hydra PB and screenshot +!pbchimera 850000 → Submit 850k damage (with screenshot attached) +!rtfhydra → Shows RTF clan's top 10 Hydra records +!mystats [RTFC]Bob → Shows Bob's complete PB overview + +🏛️ CLAN SYSTEM: +-------------- +Clans are auto-detected from username prefixes: +- RTF members: [RTF]Username or RTFUsername +- RTFC members: [RTFC]Username or RTFCUsername +- RTFR members: [RTFR]Username or RTFRUsername + +🔧 REQUIREMENTS: +--------------- +- Screenshot must be attached when submitting new PBs +- Only works in authorized channel +- Supported image formats: PNG, JPG, JPEG, GIF, WEBP +""" + import discord from discord.ext import commands -import pandas as pd +import sqlite3 +import os +import aiohttp +from datetime import datetime intents = discord.Intents.default() +intents.message_content = True bot = commands.Bot(command_prefix="!", intents=intents) -# Replace this with your authorized channel ID -AUTHORIZED_CHANNEL_ID = # TODO: input channel ID here +# Configuration +AUTHORIZED_CHANNEL_ID = 0 # TODO: input channel ID here +SCREENSHOTS_BASE_PATH = "/share/Container/discord-bot/screenshots" +DATABASE_PATH = "/share/Container/discord-bot/bot_data.db" + +# Configuration des clans +CLAN_CONFIG = { + 'RTF': { + 'name': 'RTF', + 'emoji': '⭐', + 'color': 0x00ff00 + }, + 'RTFC': { + 'name': 'RTFC', + 'emoji': '🔥', + 'color': 0xff4500 + }, + 'RTFR': { + 'name': 'RTFR', + 'emoji': '⚡', + 'color': 0x1e90ff + } +} +BOSS_CONFIG = { + 'hydra': { + 'name': 'Hydra', + 'emoji': '🔥', + 'color': 0xff6b35 + }, + 'chimera': { + 'name': 'Chimera', + 'emoji': '⚡', + 'color': 0x9932cc + }, + 'cvc': { + 'name': 'Clan vs Clan', + 'emoji': '⚔️', + 'color': 0xff0000 + } +} + +class DatabaseManager: + def __init__(self, db_path=DATABASE_PATH): + self.db_path = db_path + self.init_database() + + def init_database(self): + """Initialise la base de données""" + os.makedirs(os.path.dirname(self.db_path), exist_ok=True) + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + cursor.execute(''' + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + discord_username TEXT UNIQUE, + pb_hydra INTEGER DEFAULT 0, + pb_hydra_screenshot TEXT, + pb_hydra_date TIMESTAMP, + pb_chimera INTEGER DEFAULT 0, + pb_chimera_screenshot TEXT, + pb_chimera_date TIMESTAMP, + pb_cvc INTEGER DEFAULT 0, + pb_cvc_screenshot TEXT, + pb_cvc_date TIMESTAMP, + total_attempts INTEGER DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # Table pour l'historique global + cursor.execute(''' + CREATE TABLE IF NOT EXISTS pb_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT, + boss_type TEXT, + damage INTEGER, + screenshot_filename TEXT, + date TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + + conn.commit() + conn.close() + + def get_user_pb(self, username, boss_type): + """Récupère le PB d'un utilisateur pour un boss spécifique""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + cursor.execute( + f"SELECT pb_{boss_type}, pb_{boss_type}_screenshot, pb_{boss_type}_date FROM users WHERE discord_username = ?", + (username.lower(),) + ) + result = cursor.fetchone() + conn.close() + + return result if result else (0, None, None) + + def update_user_pb(self, username, boss_type, damage, screenshot_filename): + """Met à jour le PB d'un utilisateur pour un boss spécifique""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + # Créer l'utilisateur s'il n'existe pas, sinon mettre à jour + cursor.execute(f''' + INSERT INTO users (discord_username, pb_{boss_type}, pb_{boss_type}_screenshot, pb_{boss_type}_date, total_attempts) + VALUES (?, ?, ?, CURRENT_TIMESTAMP, 1) + ON CONFLICT(discord_username) + DO UPDATE SET + pb_{boss_type} = ?, + pb_{boss_type}_screenshot = ?, + pb_{boss_type}_date = CURRENT_TIMESTAMP, + total_attempts = total_attempts + 1 + ''', (username.lower(), damage, screenshot_filename, damage, screenshot_filename)) + + # Ajouter à l'historique + cursor.execute(''' + INSERT INTO pb_history (username, boss_type, damage, screenshot_filename) + VALUES (?, ?, ?, ?) + ''', (username.lower(), boss_type, damage, screenshot_filename)) + + conn.commit() + conn.close() + + def get_leaderboard(self, boss_type, limit=10, clan=None): + """Récupère le classement pour un boss spécifique, optionnellement filtré par clan""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + if clan: + cursor.execute(f''' + SELECT discord_username, pb_{boss_type}, pb_{boss_type}_date + FROM users + WHERE pb_{boss_type} > 0 AND ( + discord_username LIKE '[{clan}]%' OR + discord_username LIKE '{clan}%' + ) + ORDER BY pb_{boss_type} DESC + LIMIT ? + ''', (limit,)) + else: + cursor.execute(f''' + SELECT discord_username, pb_{boss_type}, pb_{boss_type}_date + FROM users + WHERE pb_{boss_type} > 0 + ORDER BY pb_{boss_type} DESC + LIMIT ? + ''', (limit,)) + + results = cursor.fetchall() + conn.close() + return results + + def get_user_all_pbs(self, username): + """Récupère tous les PB d'un utilisateur""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + cursor.execute(''' + SELECT pb_hydra, pb_hydra_date, pb_chimera, pb_chimera_date, pb_cvc, pb_cvc_date + FROM users WHERE discord_username = ? + ''', (username.lower(),)) + + result = cursor.fetchone() + conn.close() + return result + +class ScreenshotManager: + def __init__(self, base_path=SCREENSHOTS_BASE_PATH): + self.base_path = base_path + # Créer les dossiers pour chaque boss + for boss_type in BOSS_CONFIG.keys(): + os.makedirs(os.path.join(base_path, boss_type), exist_ok=True) + + async def save_screenshot(self, attachment, username, damage, boss_type): + """Sauvegarde la screenshot localement""" + try: + timestamp = int(datetime.now().timestamp()) + file_extension = attachment.filename.split('.')[-1].lower() + filename = f"{username.lower()}_{damage}_{timestamp}.{file_extension}" + boss_path = os.path.join(self.base_path, boss_type) + filepath = os.path.join(boss_path, filename) + + async with aiohttp.ClientSession() as session: + async with session.get(attachment.url) as resp: + if resp.status == 200: + with open(filepath, 'wb') as f: + f.write(await resp.read()) + return filename + return None + + except Exception as e: + print(f"Erreur sauvegarde screenshot: {e}") + return None + + def get_screenshot_path(self, filename, boss_type): + """Retourne le chemin complet de la screenshot""" + if filename: + return os.path.join(self.base_path, boss_type, filename) + return None + +# Fonctions utilitaires +def get_user_clan(username): + """Détermine le clan d'un utilisateur basé sur son pseudo""" + username_upper = username.upper() + for clan_tag in ['[RTF]', '[RTFC]', '[RTFR]', 'RTF', 'RTFC', 'RTFR']: + if username_upper.startswith(clan_tag): + return clan_tag.replace('[', '').replace(']', '') + return None + +def format_datetime(date_str): + """Formate une date en format AM/PM""" + if not date_str: + return None + try: + dt = datetime.fromisoformat(date_str) + return dt.strftime("%m/%d/%Y at %I:%M %p") + except: + return None + +def format_date_only(date_str): + """Formate une date sans l'heure""" + if not date_str: + return None + try: + dt = datetime.fromisoformat(date_str) + return dt.strftime("%m/%d/%Y") + except: + return None + +# Initialisation des managers +db_manager = DatabaseManager() +screenshot_manager = ScreenshotManager() @bot.event async def on_ready(): print(f"Bot connected as {bot.user}") -def read_google_sheet(url): - file_id = url.split("/d/")[1].split("/")[0] - csv_url = f"https://docs.google.com/spreadsheets/d/{file_id}/export?format=csv" - df = pd.read_csv(csv_url) - return df - -@bot.command() -async def siege(ctx): - # If not in the authorized channel, silently ignore the command +async def handle_pb_command(ctx, boss_type, target_user=None, damage=None): + """Fonction générique pour gérer toutes les commandes PB""" if ctx.channel.id != AUTHORIZED_CHANNEL_ID: return - + + boss_info = BOSS_CONFIG[boss_type] + try: - df = read_google_sheet("") # TODO: set public sheet url - - user_pseudo = ctx.author.name.lower() - first_column = df.columns[0] - - matching_row = df[df[first_column].str.lower() == user_pseudo] - - if matching_row.empty: - await ctx.send("No data found for your username.") - else: - info = matching_row.iloc[0].to_dict() - response = "\n".join([f"**{col}**: {val}" for col, val in info.items() if col != first_column]) - await ctx.send(f"Data for **{ctx.author.name}**:\n{response}") - + # Cas 1: !pb{boss} username (afficher le PB d'un autre utilisateur) + if target_user and damage is None and not target_user.isdigit(): + pb_data = db_manager.get_user_pb(target_user, boss_type) + pb_damage, screenshot_filename, pb_date = pb_data + + if pb_damage == 0: + await ctx.send(f"❌ **{target_user}** has no {boss_info['name']} PB recorded yet.") + return + + embed = discord.Embed( + title=f"{boss_info['emoji']} {target_user}'s {boss_info['name']} PB", + description=f"**{pb_damage:,} damage**", + color=boss_info['color'] + ) + if pb_date: + formatted_date = format_datetime(pb_date) + if formatted_date: + embed.add_field(name="📅 Record Date", value=formatted_date, inline=False) + + # Envoyer la screenshot si elle existe + if screenshot_filename: + screenshot_path = screenshot_manager.get_screenshot_path(screenshot_filename, boss_type) + if screenshot_path and os.path.exists(screenshot_path): + file = discord.File(screenshot_path, filename=f"{target_user}_{boss_type}_pb.png") + embed.set_image(url=f"attachment://{target_user}_{boss_type}_pb.png") + await ctx.send(embed=embed, file=file) + return + + await ctx.send(embed=embed) + return + + # Cas 2: !pb{boss} 1500000 (soumettre un nouveau PB) + if target_user and target_user.isdigit(): + damage = int(target_user) # Le premier argument est en fait le damage + + if not ctx.message.attachments: + await ctx.send("❌ Please attach a screenshot to validate your PB!") + return + + attachment = ctx.message.attachments[0] + if not any(attachment.filename.lower().endswith(ext) for ext in ['.png', '.jpg', '.jpeg', '.gif', '.webp']): + await ctx.send("❌ Please attach a valid image file!") + return + + username = ctx.author.name + current_pb, _, _ = db_manager.get_user_pb(username, boss_type) + + if damage > current_pb: + # Sauvegarder la screenshot + screenshot_filename = await screenshot_manager.save_screenshot(attachment, username, damage, boss_type) + + if screenshot_filename: + # Mettre à jour la base + db_manager.update_user_pb(username, boss_type, damage, screenshot_filename) + + improvement = damage - current_pb if current_pb > 0 else damage + embed = discord.Embed( + title=f"🎉 NEW {boss_info['name'].upper()} PB! 🎉", + description=f"**{username}** just hit **{damage:,} damage** on {boss_info['name']}!", + color=0x00ff00 + ) + embed.add_field(name="📈 Improvement", value=f"+{improvement:,} damage", inline=True) + embed.set_image(url=attachment.url) + + await ctx.send(embed=embed) + else: + await ctx.send("❌ Failed to save screenshot. Please try again.") + else: + embed = discord.Embed( + title="💪 Nice attempt!", + description=f"Your damage: **{damage:,}**\nCurrent PB: **{current_pb:,}**", + color=0xffa500 + ) + embed.add_field( + name="Keep going!", + value=f"You need **{current_pb - damage + 1:,}** more damage for a new PB!", + inline=False + ) + await ctx.send(embed=embed) + return + + # Cas 3: !pb{boss} (afficher son propre PB) + username = ctx.author.name + pb_data = db_manager.get_user_pb(username, boss_type) + pb_damage, screenshot_filename, pb_date = pb_data + + if pb_damage == 0: + embed = discord.Embed( + title=f"{boss_info['emoji']} Your {boss_info['name']} PB", + description="**No record yet**", + color=0x666666 + ) + embed.add_field( + name="💡 Get started!", + value=f"Use `!pb{boss_type} ` with a screenshot to set your first record!", + inline=False + ) + await ctx.send(embed=embed) + return + + embed = discord.Embed( + title=f"{boss_info['emoji']} {username}'s {boss_info['name']} PB", + description=f"**{pb_damage:,} damage**", + color=boss_info['color'] + ) + if pb_date: + formatted_date = format_datetime(pb_date) + if formatted_date: + embed.add_field(name="📅 Record Date", value=formatted_date, inline=False) + + # Envoyer la screenshot si elle existe + if screenshot_filename: + screenshot_path = screenshot_manager.get_screenshot_path(screenshot_filename, boss_type) + if screenshot_path and os.path.exists(screenshot_path): + file = discord.File(screenshot_path, filename=f"{username}_{boss_type}_pb.png") + embed.set_image(url=f"attachment://{username}_{boss_type}_pb.png") + await ctx.send(embed=embed, file=file) + return + + await ctx.send(embed=embed) + + except ValueError: + await ctx.send(f"❌ Please provide a valid damage number!\nExample: `!pb{boss_type} 1500000`") except Exception as e: - await ctx.send(f"Error: {e}") + await ctx.send(f"❌ Error: {e}") + +# Commandes pour chaque boss +@bot.command() +async def pbhydra(ctx, target_user: str = None, damage: int = None): + """Commande !pbhydra""" + await handle_pb_command(ctx, 'hydra', target_user, damage) + +@bot.command() +async def pbchimera(ctx, target_user: str = None, damage: int = None): + """Commande !pbchimera""" + await handle_pb_command(ctx, 'chimera', target_user, damage) + +@bot.command() +async def pbcvc(ctx, target_user: str = None, damage: int = None): + """Commande !pbcvc""" + await handle_pb_command(ctx, 'cvc', target_user, damage) + +# Commandes de classement global +@bot.command() +async def top10hydra(ctx): + """Affiche le top 10 des PB Hydra (tous clans)""" + await show_leaderboard(ctx, 'hydra') + +@bot.command() +async def top10chimera(ctx): + """Affiche le top 10 des PB Chimera (tous clans)""" + await show_leaderboard(ctx, 'chimera') + +@bot.command() +async def top10cvc(ctx): + """Affiche le top 10 des PB CvC (tous clans)""" + await show_leaderboard(ctx, 'cvc') + +# Commandes de classement par clan - RTF +@bot.command() +async def rtfhydra(ctx): + """Affiche le top 10 Hydra du clan RTF""" + await show_leaderboard(ctx, 'hydra', 'RTF') + +@bot.command() +async def rtfchimera(ctx): + """Affiche le top 10 Chimera du clan RTF""" + await show_leaderboard(ctx, 'chimera', 'RTF') + +@bot.command() +async def rtfcvc(ctx): + """Affiche le top 10 CvC du clan RTF""" + await show_leaderboard(ctx, 'cvc', 'RTF') + +# Commandes de classement par clan - RTFC +@bot.command() +async def rtfchydra(ctx): + """Affiche le top 10 Hydra du clan RTFC""" + await show_leaderboard(ctx, 'hydra', 'RTFC') + +@bot.command() +async def rtfcchimera(ctx): + """Affiche le top 10 Chimera du clan RTFC""" + await show_leaderboard(ctx, 'chimera', 'RTFC') + +@bot.command() +async def rtfccvc(ctx): + """Affiche le top 10 CvC du clan RTFC""" + await show_leaderboard(ctx, 'cvc', 'RTFC') + +# Commandes de classement par clan - RTFR +@bot.command() +async def rtfrhydra(ctx): + """Affiche le top 10 Hydra du clan RTFR""" + await show_leaderboard(ctx, 'hydra', 'RTFR') + +@bot.command() +async def rtfrchimera(ctx): + """Affiche le top 10 Chimera du clan RTFR""" + await show_leaderboard(ctx, 'chimera', 'RTFR') + +@bot.command() +async def rtfrcvc(ctx): + """Affiche le top 10 CvC du clan RTFR""" + await show_leaderboard(ctx, 'cvc', 'RTFR') + +async def show_leaderboard(ctx, boss_type, clan=None): + """Fonction générique pour afficher les classements""" + if ctx.channel.id != AUTHORIZED_CHANNEL_ID: + return + + try: + boss_info = BOSS_CONFIG[boss_type] + leaderboard = db_manager.get_leaderboard(boss_type, 10, clan) + + if not leaderboard: + clan_text = f" for clan {clan}" if clan else "" + await ctx.send(f"❌ No {boss_info['name']} records found{clan_text} yet!") + return + + # Titre avec clan si spécifié + title = f"🏆 {boss_info['name']} Leaderboard - Top 10" + if clan: + clan_info = CLAN_CONFIG.get(clan, {'name': clan, 'emoji': '🏛️'}) + title = f"{clan_info['emoji']} {clan_info['name']} - {boss_info['name']} Top 10" + + embed = discord.Embed( + title=title, + color=boss_info['color'] if not clan else CLAN_CONFIG.get(clan, {'color': boss_info['color']})['color'] + ) + + medals = ["🥇", "🥈", "🥉"] + ["🏅"] * 7 + + for i, (username, damage, date) in enumerate(leaderboard): + date_text = "" + if date: + formatted_date = format_date_only(date) + if formatted_date: + date_text = f" • {formatted_date}" + + # Afficher le clan dans le nom si pas de filtre par clan + display_name = username + if not clan: + user_clan = get_user_clan(username) + if user_clan: + clan_emoji = CLAN_CONFIG.get(user_clan, {'emoji': '🏛️'})['emoji'] + display_name = f"{clan_emoji} {username}" + + embed.add_field( + name=f"{medals[i]} #{i+1} {display_name}", + value=f"**{damage:,}** damage{date_text}", + inline=False + ) + + await ctx.send(embed=embed) + + except Exception as e: + await ctx.send(f"❌ Error: {e}") + +@bot.command() +async def mystats(ctx, target_user: str = None): + """Affiche tous les PB d'un utilisateur""" + if ctx.channel.id != AUTHORIZED_CHANNEL_ID: + return + + try: + username = target_user if target_user else ctx.author.name + user_data = db_manager.get_user_all_pbs(username) + + if not user_data: + await ctx.send(f"❌ No data found for **{username}**.") + return + + hydra_pb, hydra_date, chimera_pb, chimera_date, cvc_pb, cvc_date = user_data + + embed = discord.Embed( + title=f"📊 {username}'s Complete Stats", + color=0x00bfff + ) + + # Hydra + hydra_text = f"**{hydra_pb:,} damage**" if hydra_pb > 0 else "No record" + if hydra_pb > 0 and hydra_date: + formatted_date = format_date_only(hydra_date) + if formatted_date: + hydra_text += f"\n📅 {formatted_date}" + embed.add_field(name="🔥 Hydra PB", value=hydra_text, inline=True) + + # Chimera + chimera_text = f"**{chimera_pb:,} damage**" if chimera_pb > 0 else "No record" + if chimera_pb > 0 and chimera_date: + formatted_date = format_date_only(chimera_date) + if formatted_date: + chimera_text += f"\n📅 {formatted_date}" + embed.add_field(name="⚡ Chimera PB", value=chimera_text, inline=True) + + # CvC + cvc_text = f"**{cvc_pb:,} damage**" if cvc_pb > 0 else "No record" + if cvc_pb > 0 and cvc_date: + formatted_date = format_date_only(cvc_date) + if formatted_date: + cvc_text += f"\n📅 {formatted_date}" + embed.add_field(name="⚔️ CvC PB", value=cvc_text, inline=True) + + # Total des PB + total_damage = (hydra_pb or 0) + (chimera_pb or 0) + (cvc_pb or 0) + embed.add_field(name="💯 Total Combined", value=f"**{total_damage:,} damage**", inline=False) + + await ctx.send(embed=embed) + + except Exception as e: + await ctx.send(f"❌ Error: {e}") + +# TODO: Add your bot token here +# bot.run("YOUR_DISCORD_TOKEN")