@@ -7,8 +7,7 @@ from dotenv import load_dotenv
import aiomysql
import aiomysql
import json
import json
from datetime import datetime
from datetime import datetime
from typing import Optional , List , Dict , Tuple
from typing import Optional , List , Dict
import yt_dlp
# Load environment variables
# Load environment variables
load_dotenv ( )
load_dotenv ( )
@@ -79,7 +78,6 @@ intents = discord.Intents.default()
intents . message_content = True
intents . message_content = True
intents . guilds = True
intents . guilds = True
intents . members = True
intents . members = True
intents . voice_states = True
bot = commands . Bot ( command_prefix = ' ! ' , intents = intents )
bot = commands . Bot ( command_prefix = ' ! ' , intents = intents )
@@ -93,12 +91,6 @@ async def on_ready():
# Initialize database
# Initialize database
await init_database ( )
await init_database ( )
# Preload opus if available (voice)
try :
if not discord . opus . is_loaded ( ) :
discord . opus . load_opus ( ' libopus ' )
except Exception :
pass
# Set bot status
# Set bot status
await bot . change_presence (
await bot . change_presence (
@@ -113,248 +105,6 @@ async def on_ready():
except Exception as e :
except Exception as e :
print ( f ' Failed to sync commands: { e } ' )
print ( f ' Failed to sync commands: { e } ' )
# =========================
# YouTube/Voice Player
# =========================
COOKIE_FILE = os . getenv ( ' YT_COOKIES_PATH ' , ' /app/cookie.txt ' )
class Song :
def __init__ ( self , title : str , url : str , webpage_url : str , duration : Optional [ int ] ) :
self . title = title
self . url = url
self . webpage_url = webpage_url
self . duration = duration
class GuildMusic :
def __init__ ( self ) :
self . queue : asyncio . Queue [ Song ] = asyncio . Queue ( )
self . current : Optional [ Song ] = None
self . play_next = asyncio . Event ( )
self . player_task : Optional [ asyncio . Task ] = None
music_states : Dict [ int , GuildMusic ] = { }
def get_guild_music ( guild_id : int ) - > GuildMusic :
st = music_states . get ( guild_id )
if not st :
st = GuildMusic ( )
music_states [ guild_id ] = st
return st
def ytdlp_opts ( ) - > dict :
opts = {
' format ' : ' bestaudio/best ' ,
' noplaylist ' : True ,
' quiet ' : True ,
' no_warnings ' : True ,
' default_search ' : ' auto ' ,
' nocheckcertificate ' : True ,
' source_address ' : ' 0.0.0.0 ' ,
}
try :
if os . path . exists ( COOKIE_FILE ) :
opts [ ' cookiefile ' ] = COOKIE_FILE
except Exception :
pass
return opts
async def yt_extract ( query : str ) - > Song :
loop = asyncio . get_event_loop ( )
def _extract ( ) - > Tuple [ str , str , str , Optional [ int ] ] :
with yt_dlp . YoutubeDL ( ytdlp_opts ( ) ) as ydl :
info = ydl . extract_info ( query , download = False )
if ' entries ' in info :
info = info [ ' entries ' ] [ 0 ]
title = info . get ( ' title ' )
url = info . get ( ' url ' )
webpage_url = info . get ( ' webpage_url ' ) or info . get ( ' original_url ' ) or query
duration = info . get ( ' duration ' )
return title , url , webpage_url , duration
title , url , webpage_url , duration = await loop . run_in_executor ( None , _extract )
return Song ( title , url , webpage_url , duration )
FFMPEG_BEFORE_OPTS = ' -reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5 '
FFMPEG_OPTS = ' -vn '
async def ensure_voice ( ctx : commands . Context ) - > Optional [ discord . VoiceClient ] :
if not ctx . author or not getattr ( ctx . author , ' voice ' , None ) or not ctx . author . voice :
await ctx . reply ( " ❌ You need to be in a voice channel. " )
return None
channel = ctx . author . voice . channel
if not channel :
await ctx . reply ( " ❌ Can ' t find your voice channel. " )
return None
if ctx . voice_client is None :
try :
return await channel . connect ( timeout = 30 , reconnect = True , self_deaf = True )
except Exception as e :
await ctx . reply ( f " ❌ Failed to join: { e } " )
return None
elif ctx . voice_client . channel != channel :
try :
await ctx . voice_client . move_to ( channel )
except Exception as e :
await ctx . reply ( f " ❌ Failed to move: { e } " )
return None
return ctx . voice_client
async def start_player_loop ( ctx : commands . Context ) :
guild = ctx . guild
if not guild :
return
state = get_guild_music ( guild . id )
if state . player_task and not state . player_task . done ( ) :
return
async def runner ( ) :
while True :
state . play_next . clear ( )
try :
song = await asyncio . wait_for ( state . queue . get ( ) , timeout = 300 )
except asyncio . TimeoutError :
# idle for 5 minutes, disconnect
if ctx . voice_client and ctx . voice_client . is_connected ( ) :
await ctx . voice_client . disconnect ( )
break
state . current = song
source = discord . FFmpegPCMAudio ( song . url , before_options = FFMPEG_BEFORE_OPTS , options = FFMPEG_OPTS )
ctx . voice_client . play ( source , after = lambda e : state . play_next . set ( ) )
await state . play_next . wait ( )
state . current = None
state . player_task = asyncio . create_task ( runner ( ) )
@bot.hybrid_command ( name = ' join ' , description = ' Join your voice channel ' )
async def cmd_join ( ctx ) :
vc = await ensure_voice ( ctx )
if vc :
await ctx . reply ( f " ✅ Joined { vc . channel . mention } " )
@bot.hybrid_command ( name = ' leave ' , description = ' Leave the voice channel ' )
async def cmd_leave ( ctx ) :
if ctx . voice_client and ctx . voice_client . is_connected ( ) :
await ctx . voice_client . disconnect ( )
await ctx . reply ( " 👋 Disconnected. " )
else :
await ctx . reply ( " ℹ ️ Not connected." )
@bot.hybrid_command ( name = ' play ' , description = ' Play a YouTube URL or search term ' )
async def cmd_play ( ctx , * , query : str ) :
vc = await ensure_voice ( ctx )
if not vc :
return
try :
song = await yt_extract ( query )
state = get_guild_music ( ctx . guild . id )
await state . queue . put ( song )
await ctx . reply ( f " ▶️ Queued: ** { song . title } ** " )
await start_player_loop ( ctx )
except Exception as e :
await ctx . reply ( f " ❌ Failed to add: { e } " )
@bot.hybrid_command ( name = ' skip ' , description = ' Skip the current track ' )
async def cmd_skip ( ctx ) :
if ctx . voice_client and ctx . voice_client . is_playing ( ) :
ctx . voice_client . stop ( )
await ctx . reply ( " ⏭️ Skipped. " )
else :
await ctx . reply ( " ℹ ️ Nothing is playing." )
@bot.hybrid_command ( name = ' stop ' , description = ' Stop playback and clear the queue ' )
async def cmd_stop ( ctx ) :
state = get_guild_music ( ctx . guild . id )
# Clear queue
try :
while True :
state . queue . get_nowait ( )
except asyncio . QueueEmpty :
pass
if ctx . voice_client and ctx . voice_client . is_playing ( ) :
ctx . voice_client . stop ( )
await ctx . reply ( " ⏹️ Stopped and cleared queue. " )
@bot.hybrid_command ( name = ' pause ' , description = ' Pause playback ' )
async def cmd_pause ( ctx ) :
if ctx . voice_client and ctx . voice_client . is_playing ( ) :
ctx . voice_client . pause ( )
await ctx . reply ( " ⏸️ Paused. " )
else :
await ctx . reply ( " ℹ ️ Nothing is playing." )
@bot.hybrid_command ( name = ' resume ' , description = ' Resume playback ' )
async def cmd_resume ( ctx ) :
if ctx . voice_client and ctx . voice_client . is_paused ( ) :
ctx . voice_client . resume ( )
await ctx . reply ( " ▶️ Resumed. " )
else :
await ctx . reply ( " ℹ ️ Nothing is paused." )
@bot.hybrid_command ( name = ' np ' , description = ' Show the currently playing track ' )
async def cmd_nowplaying ( ctx ) :
state = get_guild_music ( ctx . guild . id )
if state . current :
dur = f " ( { state . current . duration / / 60 } : { state . current . duration % 60 : 02d } ) " if state . current . duration else " "
await ctx . reply ( f " 🎶 Now playing: ** { state . current . title } ** { dur } \n { state . current . webpage_url } " )
else :
await ctx . reply ( " ℹ ️ Nothing is playing." )
@bot.hybrid_command ( name = ' queue ' , description = ' Show queued tracks ' )
async def cmd_queue ( ctx ) :
state = get_guild_music ( ctx . guild . id )
if state . queue . empty ( ) :
await ctx . reply ( " 🗒️ Queue is empty. " )
return
# Snapshot queue without draining
items : List [ Song ] = [ ]
try :
while True :
items . append ( state . queue . get_nowait ( ) )
except asyncio . QueueEmpty :
pass
# Put them back
for s in items :
await state . queue . put ( s )
desc = " \n " . join ( [ f " { i + 1 } . { s . title } " for i , s in enumerate ( items [ : 10 ] ) ] )
more = state . queue . qsize ( ) - len ( items [ : 10 ] )
if more > 0 :
desc + = f " \n ...and { more } more "
embed = discord . Embed ( title = " 🎼 Queue " , description = desc , color = discord . Color . purple ( ) )
await ctx . reply ( embed = embed )
@bot.hybrid_command ( name = ' voicediag ' , description = ' Diagnose voice playback environment ' )
async def cmd_voicediag ( ctx ) :
details = [ ]
details . append ( f " discord.py: { discord . __version__ } " )
# PyNaCl check
try :
import nacl
details . append ( f " PyNaCl: { getattr ( nacl , ' __version__ ' , ' present ' ) } " )
except Exception as e :
details . append ( f " PyNaCl: missing ( { e } ) " )
# Opus check
try :
loaded = discord . opus . is_loaded ( )
details . append ( f " Opus loaded: { loaded } " )
except Exception as e :
details . append ( f " Opus check failed: { e } " )
# FFmpeg check
ff = " unknown "
try :
proc = await asyncio . create_subprocess_exec ( ' ffmpeg ' , ' -version ' , stdout = asyncio . subprocess . PIPE , stderr = asyncio . subprocess . STDOUT )
out , _ = await proc . communicate ( )
ff = out . decode ( errors = ' ignore ' ) . splitlines ( ) [ 0 ] [ : 120 ]
except FileNotFoundError :
ff = " not found "
except Exception as e :
ff = f " error: { e } "
details . append ( f " ffmpeg: { ff } " )
# Cookie file
details . append ( f " cookie.txt present: { os . path . exists ( COOKIE_FILE ) } at { COOKIE_FILE } " )
embed = discord . Embed ( title = ' Voice Diagnostics ' , description = ' \n ' . join ( details ) , color = discord . Color . dark_grey ( ) )
await ctx . reply ( embed = embed )
@bot.event
@bot.event
async def on_guild_join ( guild ) :
async def on_guild_join ( guild ) :
""" Event triggered when the bot joins a server """
""" Event triggered when the bot joins a server """
@@ -714,11 +464,22 @@ async def init_database():
status VARCHAR(50) DEFAULT ' setup ' ,
status VARCHAR(50) DEFAULT ' setup ' ,
players JSON NOT NULL,
players JSON NOT NULL,
winner_team VARCHAR(255),
winner_team VARCHAR(255),
notification_message_id BIGINT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
finished_at TIMESTAMP NULL
finished_at TIMESTAMP NULL
)
)
''' )
''' )
# Add notification_message_id column if it doesn't exist (for existing databases)
try :
await cursor . execute ( '''
ALTER TABLE games
ADD COLUMN notification_message_id BIGINT NULL
''' )
except :
# Column already exists, ignore error
pass
# Create game_results table (MySQL syntax)
# Create game_results table (MySQL syntax)
await cursor . execute ( '''
await cursor . execute ( '''
CREATE TABLE IF NOT EXISTS game_results (
CREATE TABLE IF NOT EXISTS game_results (
@@ -872,6 +633,88 @@ async def reload_bot(ctx):
)
)
await ctx . send ( embed = embed )
await ctx . send ( embed = embed )
# Game notification channel
GAME_NOTIFICATION_CHANNEL_ID = 1432368177685332030
async def create_game_notification_embed ( game_data : Dict , players_data : List [ Dict ] ) - > discord . Embed :
""" Create an embed for game notifications """
embed = discord . Embed (
title = f " 🎮 { game_data [ ' game_name ' ] } " ,
description = f " **Type:** { game_data [ ' game_type ' ] . title ( ) } \n **Status:** Setup Phase " ,
color = discord . Color . blue ( )
)
if not players_data :
embed . add_field ( name = " Players " , value = " No players yet " , inline = False )
return embed
# Group players by team
teams = { }
for p in players_data :
team = p [ ' team_name ' ]
if team not in teams :
teams [ team ] = [ ]
teams [ team ] . append ( p )
# Add team fields
for team_name , members in teams . items ( ) :
team_emoji = " 🎖️ " # Simple fallback since we don't have ctx here
avg_elo = sum ( m [ ' current_elo ' ] for m in members ) / len ( members ) if members else 0
field_name = f " { team_emoji } { team_name } (avg { avg_elo : .0f } ) "
lines = [ ]
for m in sorted ( members , key = lambda mm : ( - mm . get ( ' t_level ' , 2 ) , mm [ ' username ' ] ) ) :
t_level = m . get ( ' t_level ' , 2 )
t_emoji = { 1 : " 🔹 " , 2 : " 🔸 " , 3 : " 🔺 " } . get ( t_level , " 🔹 " )
country = m . get ( ' country ' )
country_text = f " [ { country } ] " if country else " "
lines . append ( f " { t_emoji } { m [ ' username ' ] } { country_text } ( { m [ ' current_elo ' ] } ) " )
embed . add_field ( name = field_name , value = " \n " . join ( lines ) , inline = True )
embed . set_footer ( text = f " Players: { len ( players_data ) } | Teams: { len ( teams ) } " )
return embed
async def update_game_notification ( game_data : Dict , players_data : List [ Dict ] ) :
""" Update or create game notification in the notification channel """
try :
channel = bot . get_channel ( GAME_NOTIFICATION_CHANNEL_ID )
if not channel :
return
embed = await create_game_notification_embed ( game_data , players_data )
if game_data . get ( ' notification_message_id ' ) :
# Try to edit existing message
try :
message = await channel . fetch_message ( game_data [ ' notification_message_id ' ] )
await message . edit ( embed = embed )
except ( discord . NotFound , discord . HTTPException ) :
# Message not found, create new one
message = await channel . send ( embed = embed )
# Update DB with new message ID
async with db_pool . acquire ( ) as conn :
async with conn . cursor ( ) as cursor :
await cursor . execute (
" UPDATE games SET notification_message_id = %s WHERE id = %s " ,
( message . id , game_data [ ' id ' ] )
)
else :
# Create new message
message = await channel . send ( embed = embed )
# Store message ID in database
async with db_pool . acquire ( ) as conn :
async with conn . cursor ( ) as cursor :
await cursor . execute (
" UPDATE games SET notification_message_id = %s WHERE id = %s " ,
( message . id , game_data [ ' id ' ] )
)
except Exception as e :
logging . warning ( f " Failed to update game notification: { e } " )
# HOI4 ELO Commands
# HOI4 ELO Commands
@bot.hybrid_command ( name = ' hoi4create ' , description = ' Create a new HOI4 game ' )
@bot.hybrid_command ( name = ' hoi4create ' , description = ' Create a new HOI4 game ' )
async def hoi4create ( ctx , game_type : str , game_name : str ) :
async def hoi4create ( ctx , game_type : str , game_name : str ) :
@@ -899,6 +742,13 @@ async def hoi4create(ctx, game_type: str, game_name: str):
" INSERT INTO games (game_name, game_type, status, players) VALUES ( %s , %s , ' setup ' , %s ) " ,
" INSERT INTO games (game_name, game_type, status, players) VALUES ( %s , %s , ' setup ' , %s ) " ,
( game_name , game_type . lower ( ) , ' [] ' )
( game_name , game_type . lower ( ) , ' [] ' )
)
)
# Get the created game data for notification
await cursor . execute (
" SELECT * FROM games WHERE game_name = %s AND game_type = %s ORDER BY id DESC LIMIT 1 " ,
( game_name , game_type . lower ( ) )
)
game_data = await cursor . fetchone ( )
embed = discord . Embed (
embed = discord . Embed (
title = " 🎮 Game Created " ,
title = " 🎮 Game Created " ,
@@ -912,16 +762,131 @@ async def hoi4create(ctx, game_type: str, game_name: str):
await ctx . send ( embed = embed )
await ctx . send ( embed = embed )
# Create notification in notification channel
if game_data :
await update_game_notification ( dict ( game_data ) , [ ] )
except Exception as e :
except Exception as e :
await ctx . send ( f " ❌ Error creating game: { str ( e ) } " )
await ctx . send ( f " ❌ Error creating game: { str ( e ) } " )
@bot.hybrid_command ( name = ' hoi4delete ' , description = ' Delete a game lobby ' )
async def hoi4delete ( ctx , game_name : str ) :
""" Delete a game lobby that is in setup phase """
try :
async with db_pool . acquire ( ) as conn :
async with conn . cursor ( aiomysql . DictCursor ) as cursor :
# Get the game
await cursor . execute (
" SELECT * FROM games WHERE game_name = %s AND status = ' setup ' " ,
( game_name , )
)
game = await cursor . fetchone ( )
if not game :
await ctx . send ( f " ❌ No active game found with name ' { game_name } ' in setup phase! " )
return
# Delete the game
await cursor . execute (
" DELETE FROM games WHERE id = %s " ,
( game [ ' id ' ] , )
)
embed = discord . Embed (
title = " 🗑️ Game Deleted " ,
description = f " Game ' { game_name } ' has been deleted! " ,
color = discord . Color . red ( )
)
embed . add_field ( name = " Game Name " , value = game_name , inline = True )
embed . add_field ( name = " Type " , value = game [ ' game_type ' ] . title ( ) , inline = True )
embed . set_footer ( text = f " Deleted by { ctx . author } " )
await ctx . send ( embed = embed )
except Exception as e :
await ctx . send ( f " ❌ Error deleting game: { str ( e ) } " )
@bot.hybrid_command ( name = ' hoi4remove ' , description = ' Remove a player from an existing game ' )
async def hoi4remove ( ctx , game_name : str , user : discord . Member ) :
""" Remove a player from an existing game """
try :
async with db_pool . acquire ( ) as conn :
async with conn . cursor ( aiomysql . DictCursor ) as cursor :
# Get the game
await cursor . execute (
" SELECT * FROM games WHERE game_name = %s AND status = ' setup ' " ,
( game_name , )
)
game = await cursor . fetchone ( )
if not game :
await ctx . send ( f " ❌ No game found with name ' { game_name } ' in setup phase! " )
return
# Parse existing players
players = json . loads ( game [ ' players ' ] ) if game [ ' players ' ] else [ ]
# Find and remove the player
player_found = False
new_players = [ ]
for p in players :
if p [ ' discord_id ' ] == user . id :
player_found = True
# Skip this player (don't add to new_players)
else :
new_players . append ( p )
if not player_found :
await ctx . send ( f " ❌ { user . display_name } is not in this game! " )
return
# Update game with new player list
await cursor . execute (
" UPDATE games SET players = %s WHERE id = %s " ,
( json . dumps ( new_players ) , game [ ' id ' ] )
)
embed = discord . Embed (
title = " ✅ Player Removed " ,
description = f " { user . display_name } has been removed from ' { game_name } ' ! " ,
color = discord . Color . orange ( )
)
embed . add_field ( name = " Player " , value = user . display_name , inline = True )
embed . add_field ( name = " Game " , value = game_name , inline = True )
embed . add_field ( name = " Players Left " , value = len ( new_players ) , inline = True )
embed . set_footer ( text = f " Removed by { ctx . author } " )
await ctx . send ( embed = embed )
# Update notification in notification channel
await update_game_notification ( dict ( game ) , new_players )
except Exception as e :
await ctx . send ( f " ❌ Error removing player: { str ( e ) } " )
@bot.hybrid_command ( name = ' hoi4setup ' , description = ' Add a player to an existing game ' )
@bot.hybrid_command ( name = ' hoi4setup ' , description = ' Add a player to an existing game ' )
async def hoi4setup ( ctx , game_name : str , user : discord . Member , team_name : str , t_level : int , country : Optional [ str ] = None ) :
async def hoi4setup ( ctx , game_name : str , user : discord . Member , team_name : str , t_level : int , country : Optional [ str ] = None , modifier : Optional [ str ] = None ) :
""" Add a player to an existing game """
""" Add a player to an existing game. Use modifier= ' --force ' to bypass MP ban. """
if t_level not in [ 1 , 2 , 3 ] :
if t_level not in [ 1 , 2 , 3 ] :
await ctx . send ( " ❌ T-Level must be 1, 2, or 3 " )
await ctx . send ( " ❌ T-Level must be 1, 2, or 3 " )
return
return
# Check for MP ban role (unless --force is used)
MP_BAN_ROLE_ID = 1432368177052127353
if modifier != " --force " :
mp_ban_role = discord . utils . get ( user . roles , id = MP_BAN_ROLE_ID )
if mp_ban_role :
embed = discord . Embed (
title = " 🚫 Player Banned " ,
description = f " { user . display_name } is currently banned from multiplayer games! " ,
color = discord . Color . red ( )
)
embed . add_field ( name = " Banned Player " , value = user . mention , inline = True )
embed . add_field ( name = " Ban Role " , value = mp_ban_role . name , inline = True )
embed . set_footer ( text = " Contact an administrator if this is an error " )
await ctx . send ( embed = embed )
return
try :
try :
async with db_pool . acquire ( ) as conn :
async with db_pool . acquire ( ) as conn :
async with conn . cursor ( aiomysql . DictCursor ) as cursor :
async with conn . cursor ( aiomysql . DictCursor ) as cursor :
@@ -983,6 +948,9 @@ async def hoi4setup(ctx, game_name: str, user: discord.Member, team_name: str, t
await ctx . send ( embed = embed )
await ctx . send ( embed = embed )
# Update notification in notification channel
await update_game_notification ( dict ( game ) , players )
except Exception as e :
except Exception as e :
await ctx . send ( f " ❌ Error adding player: { str ( e ) } " )
await ctx . send ( f " ❌ Error adding player: { str ( e ) } " )
@@ -1178,6 +1146,34 @@ async def hoi4end(ctx, game_name: str, winner_team: str):
await ctx . send ( embed = embed )
await ctx . send ( embed = embed )
# Post final game result to notification channel
try :
channel = bot . get_channel ( GAME_NOTIFICATION_CHANNEL_ID )
if channel :
final_embed = discord . Embed (
title = f " 🏁 Game Finished: { game_name } " ,
description = f " **Result:** { ' Draw ' if is_draw else f ' { winner_team } Victory ' } " ,
color = discord . Color . gold ( ) if not is_draw else discord . Color . orange ( )
)
final_embed . add_field ( name = " Game Type " , value = game [ ' game_type ' ] . title ( ) , inline = True )
final_embed . add_field ( name = " Players " , value = len ( players ) , inline = True )
final_embed . add_field ( name = " Teams " , value = len ( teams ) , inline = True )
# Add team results
for team , team_changes in teams_results . items ( ) :
avg_change = sum ( c [ ' elo_change ' ] for c in team_changes ) / len ( team_changes )
emoji = " 🏆 " if team == winner_team and not is_draw else " 🤝 " if is_draw else " 💔 "
final_embed . add_field (
name = f " { emoji } { team } " ,
value = f " { len ( team_changes ) } players \n Avg ELO change: { avg_change : +.1f } " ,
inline = True
)
await channel . send ( embed = final_embed )
except Exception as e :
logging . warning ( f " Failed to post final game notification: { e } " )
except Exception as e :
except Exception as e :
await ctx . send ( f " ❌ Error ending game: { str ( e ) } " )
await ctx . send ( f " ❌ Error ending game: { str ( e ) } " )
@@ -1598,14 +1594,14 @@ async def hoi4leaderboard(ctx, game_type: Optional[str] = "standard", limit: Opt
async def on_command_error ( ctx , error ) :
async def on_command_error ( ctx , error ) :
""" Handles command errors """
""" Handles command errors """
if isinstance ( error , commands . CheckFailure ) :
if isinstance ( error , commands . CheckFailure ) :
await _safe_send ( ctx , " ❌ You don ' t have permission to use this command! " )
await ctx . send ( " ❌ You don ' t have permission to use this command! " )
elif isinstance ( error , commands . CommandNotFound ) :
elif isinstance ( error , commands . CommandNotFound ) :
# Silently ignore command not found errors
# Silently ignore command not found errors
pass
pass
elif isinstance ( error , commands . MissingRequiredArgument ) :
elif isinstance ( error , commands . MissingRequiredArgument ) :
await _safe_send ( ctx , f " ❌ Missing arguments! Command: ` { ctx . command } ` " )
await ctx . send ( f " ❌ Missing arguments! Command: ` { ctx . command } ` " )
elif isinstance ( error , commands . BadArgument ) :
elif isinstance ( error , commands . BadArgument ) :
await _safe_send ( ctx , " ❌ Invalid argument! " )
await ctx . send ( " ❌ Invalid argument! " )
else :
else :
# Log detailed error information
# Log detailed error information
print ( f " ❌ Unknown error in command ' { ctx . command } ' : { type ( error ) . __name__ } : { error } " )
print ( f " ❌ Unknown error in command ' { ctx . command } ' : { type ( error ) . __name__ } : { error } " )
@@ -1614,34 +1610,9 @@ async def on_command_error(ctx, error):
# Send detailed error to user if owner
# Send detailed error to user if owner
if ctx . author . id == OWNER_ID :
if ctx . author . id == OWNER_ID :
await _safe_send ( ctx , f " ❌ **Error Details (Owner only):** \n ```python \n { type ( error ) . __name__ } : { str ( error ) [ : 1800 ] } \n ``` " )
await ctx . send ( f " ❌ **Error Details (Owner only):** \n ```python \n { type ( error ) . __name__ } : { str ( error ) [ : 1800 ] } \n ``` " )
else :
else :
await _safe_send ( ctx , " ❌ An unknown error occurred! " )
await ctx . send ( " ❌ An unknown error occurred! " )
async def _safe_send ( ctx : commands . Context , content : str = None , * * kwargs ) :
""" Send a message safely for both message and slash contexts, even if the interaction timed out. """
try :
if getattr ( ctx , ' interaction ' , None ) :
# If we have an interaction, try normal response first
interaction = ctx . interaction
if not interaction . response . is_done ( ) :
await interaction . response . send_message ( content = content , * * kwargs )
return
# Otherwise use followup
await interaction . followup . send ( content = content , * * kwargs )
return
# Fallback to classic send
await ctx . send ( content = content , * * kwargs )
except discord . NotFound :
# Interaction unknown/expired, try channel.send
try :
channel = getattr ( ctx , ' channel ' , None )
if channel :
await channel . send ( content = content , * * kwargs )
except Exception :
pass
except Exception :
pass
async def main ( ) :
async def main ( ) :
""" Main function to start the bot """
""" Main function to start the bot """