2026-01-13 16:03:54 -05:00
import discord
import os
import random
import asyncio
import json
import logging
2026-02-10 20:11:22 -05:00
from logging.handlers import RotatingFileHandler
from collections import Counter
2026-01-13 16:03:54 -05:00
from datetime import datetime , timedelta
from pathlib import Path
2026-02-10 20:11:22 -05:00
from functools import wraps , partial
2026-01-13 16:03:54 -05:00
from dotenv import load_dotenv
from discord import app_commands
from discord.ext import commands , tasks
from itertools import cycle
from mcrcon import MCRcon
2026-02-10 20:11:22 -05:00
import aiohttp
2026-01-13 16:03:54 -05:00
Path ( "logs" ) . mkdir ( exist_ok = True )
2026-02-10 20:49:55 -05:00
2026-02-10 20:11:22 -05:00
def setup_logging ():
"""Configure logging with rotation"""
logger = logging . getLogger ( 'discord_bot' )
logger . setLevel ( logging . INFO )
file_handler = RotatingFileHandler (
'logs/discord.log' ,
maxBytes = 10 * 1024 * 1024 ,
backupCount = 5
)
file_handler . setFormatter (
logging . Formatter ( ' %(asctime)s - %(levelname)s - %(message)s ' )
)
stream_handler = logging . StreamHandler ()
stream_handler . setFormatter (
logging . Formatter ( ' %(asctime)s - %(levelname)s - %(message)s ' )
)
logger . addHandler ( file_handler )
logger . addHandler ( stream_handler )
return logger
2026-02-10 20:49:55 -05:00
2026-02-10 20:11:22 -05:00
logger = setup_logging ()
2026-01-13 16:03:54 -05:00
load_dotenv ()
2026-02-10 20:49:55 -05:00
# Configuration
2026-01-13 16:03:54 -05:00
GUILD_ID = int ( os . getenv ( 'GUILD_ID' , '605864889927467181' ))
AUDIT_CHANNEL_ID = int ( os . getenv ( 'AUDIT_CHANNEL_ID' , '1340861451392520233' ))
ADMIN_ROLE_ID = int ( os . getenv ( 'ADMIN_ROLE_ID' , '605867042104541194' ))
MINECRAFT_ROLE_ID = int ( os . getenv ( 'MINECRAFT_ROLE_ID' , '821163520942145556' ))
2026-01-13 16:44:20 -05:00
HYTALE_ROLE_ID = int ( os . getenv ( 'HYTALE_ROLE_ID' , '1460750779848589548' ))
2026-01-13 16:03:54 -05:00
COOL_KIDS_ROLE_ID = int ( os . getenv ( 'COOL_KIDS_ROLE_ID' , '788968178117902347' ))
OWNER_ID = int ( os . getenv ( 'OWNER_ID' , '238728085342519296' ))
REACTION_MESSAGE_ID = int ( os . getenv ( 'REACTION_MESSAGE_ID' , '744047519696420914' ))
MINECRAFT_RCON_HOST = os . getenv ( 'MINECRAFT_RCON_HOST' , '10.10.10.67' )
MINECRAFT_RCON_PASSWORD = os . getenv ( 'MINECRAFT_RCON_PASSWORD' , '' )
2026-02-10 20:11:22 -05:00
PELICAN_API_KEY = os . getenv ( 'PELICAN_API_KEY' , '' )
STATUS_UPDATE_INTERVAL = int ( os . getenv ( 'STATUS_UPDATE_INTERVAL' , '300' ))
2026-02-10 20:41:16 -05:00
OLLAMA_URL = os . getenv ( 'OLLAMA_HOST' , 'http://10.10.10.157:11434' )
2026-02-10 20:11:22 -05:00
2026-02-10 20:49:55 -05:00
# Constants
2026-02-10 20:11:22 -05:00
MIN_USERNAME_LENGTH = 3
MAX_USERNAME_LENGTH = 16
MAX_INPUT_LENGTH = 500
MAX_DICE_SIDES = 100
MAX_DICE_COUNT = 20
COOLDOWN_MINUTES = 2
RCON_TIMEOUT = 5.0
USERNAME_CACHE_TTL_MINUTES = 60
AUDIT_BATCH_SIZE = 5
AUDIT_FLUSH_INTERVAL = 30
2026-01-13 16:03:54 -05:00
EMOJI_ROLE_MAP = {
"Overwatch" : "(Overwatch)" ,
"Minecraft" : "(Minecraft)" ,
"LeagueOfLegends" : "(League Of Legends)" ,
"ClashRoyale" : "(Clash Royale)" ,
"CSGO" : "(CSGO)" ,
"CivilizationVI" : "(Civilization VI)" ,
"Python" : "Computer Nerd" ,
2026-01-13 16:30:09 -05:00
"computer" : "Computer Nerd" ,
2026-01-13 16:03:54 -05:00
"Valorant" : "(Valorant)" ,
"Ark" : "(Ark Survival Evolved)" ,
"AmongUs" : "(Among Us)" ,
"RainbowSixSiege" : "(Rainbow Six Siege)" ,
"Phasmophobia" : "(Phasmophobia)" ,
"StardewValley" : "(Stardew Valley)" ,
"Tarkov" : "(Tarkov)" ,
"LethalCompany" : "(Lethal Company)" ,
"BTD" : "(Balloons Tower Defense)" ,
"HellDivers" : "(Hell Divers)" ,
"ABI" : "(Arena Breakout Infinite)" ,
"UnoReverse" : "(Uno)" ,
"Hytale" : "(Hytale)"
}
2026-02-10 20:11:22 -05:00
class ConfigValidator :
REQUIRED = [ 'DISCORD_TOKEN' ]
OPTIONAL = {
'MINECRAFT_RCON_PASSWORD' : 'Minecraft commands' ,
'PELICAN_API_KEY' : 'Pelican integration' ,
2026-02-10 20:41:16 -05:00
'OLLAMA_HOST' : '/ask command'
2026-02-10 20:11:22 -05:00
}
@classmethod
def validate ( cls ):
errors = []
warnings = []
for var in cls . REQUIRED :
if not os . getenv ( var ):
errors . append ( f "Missing required: { var } " )
for var , feature in cls . OPTIONAL . items ():
if not os . getenv ( var ):
warnings . append ( f "Missing { var } - { feature } may use defaults" )
return errors , warnings
class UsernameCache :
def __init__ ( self , ttl_minutes : int = 60 ):
self . cache = {}
self . ttl = timedelta ( minutes = ttl_minutes )
async def is_valid ( self , username : str , session : aiohttp . ClientSession ) -> bool :
if username in self . cache :
is_valid , timestamp = self . cache [ username ]
if datetime . now () - timestamp < self . ttl :
return is_valid
url = f 'https://api.mojang.com/users/profiles/minecraft/ { username } '
try :
async with session . get ( url ) as response :
is_valid = response . status == 200
self . cache [ username ] = ( is_valid , datetime . now ())
return is_valid
except Exception as e :
logger . error ( f "Error validating username: { e } " )
return False
username_cache = UsernameCache ( ttl_minutes = USERNAME_CACHE_TTL_MINUTES )
class MediaManager :
def __init__ ( self ):
self . _cache = {}
def get_random ( self , category : str ) -> str | None :
if category not in self . _cache :
patterns = [ f "media/ { category } .gif" , f "media/ { category } 1.gif" ,
f "media/ { category } 2.gif" , f "media/ { category } 3.gif" ]
self . _cache [ category ] = [ p for p in patterns if Path ( p ) . exists ()]
files = self . _cache [ category ]
return random . choice ( files ) if files else None
media_manager = MediaManager ()
class AuditLogger :
def __init__ ( self , channel_id : int , batch_size : int = AUDIT_BATCH_SIZE ,
flush_interval : int = AUDIT_FLUSH_INTERVAL ):
self . channel_id = channel_id
self . batch_size = batch_size
self . flush_interval = flush_interval
self . queue = []
self . last_flush = datetime . now ()
async def log ( self , message : str , color : int = 0x980000 ):
self . queue . append (( message , color , datetime . now ()))
if len ( self . queue ) >= self . batch_size or \
2026-02-10 20:49:55 -05:00
( datetime . now () - self . last_flush ) . total_seconds () >= self . flush_interval :
2026-02-10 20:11:22 -05:00
await self . flush ()
async def flush ( self ):
if not self . queue :
return
channel = client . get_channel ( self . channel_id )
if not channel :
return
2026-02-10 20:49:55 -05:00
# Process in batches of 10 (Discord embed field limit)
while self . queue :
batch = self . queue [: 10 ]
self . queue = self . queue [ 10 :]
2026-02-10 20:11:22 -05:00
2026-02-10 20:49:55 -05:00
embed = discord . Embed ( color = 0x980000 , timestamp = datetime . now ())
for msg , _ , timestamp in batch :
embed . add_field ( name = timestamp . strftime ( "%H:%M:%S" ), value = msg [: 1024 ], inline = False )
try :
await channel . send ( embed = embed )
except Exception as e :
logger . error ( f "Failed to send audit log: { e } " , exc_info = True )
break
2026-02-10 20:11:22 -05:00
self . last_flush = datetime . now ()
audit_logger = AuditLogger ( AUDIT_CHANNEL_ID )
class MetricsCollector :
def __init__ ( self ):
self . command_counts = Counter ()
self . error_counts = Counter ()
self . start_time = datetime . now ()
def record_command ( self , command_name : str ):
self . command_counts [ command_name ] += 1
def record_error ( self , command_name : str ):
self . error_counts [ command_name ] += 1
def get_stats ( self ) -> dict :
uptime = datetime . now () - self . start_time
return {
"uptime_seconds" : uptime . total_seconds (),
"commands_executed" : sum ( self . command_counts . values ()),
"top_commands" : self . command_counts . most_common ( 5 ),
"error_count" : sum ( self . error_counts . values ())
}
metrics = MetricsCollector ()
def sanitize_input ( text : str , max_length : int = MAX_INPUT_LENGTH ) -> str :
text = text . strip ()[: max_length ]
text = '' . join ( char for char in text if char . isprintable ())
return text
def handle_command_errors ( func ):
@wraps ( func )
async def wrapper ( interaction : discord . Interaction , * args , ** kwargs ):
try :
metrics . record_command ( func . __name__ )
return await func ( interaction , * args , ** kwargs )
except Exception as e :
metrics . record_error ( func . __name__ )
logger . error ( f "Error in { func . __name__ } : { e } " , exc_info = True )
error_msg = "An unexpected error occurred. Please try again later."
if not interaction . response . is_done ():
await interaction . response . send_message ( error_msg , ephemeral = True )
else :
await interaction . followup . send ( error_msg , ephemeral = True )
return wrapper
def execute_rcon ( host : str , password : str , command : str ):
with MCRcon ( host , password , timeout = 3 ) as mcr :
return mcr . command ( command )
async def rcon_command ( host : str , password : str , command : str , timeout : float = RCON_TIMEOUT ):
try :
loop = asyncio . get_event_loop ()
func = partial ( execute_rcon , host , password , command )
return await asyncio . wait_for ( loop . run_in_executor ( None , func ), timeout = timeout )
except asyncio . TimeoutError :
raise Exception ( "RCON timeout - server may be offline" )
except Exception as e :
raise Exception ( f "RCON error: { str ( e ) } " )
async def send_interaction_media (
interaction : discord . Interaction ,
member : discord . Member ,
action : str ,
media_prefix : str
):
messages = {
'kill' : f "You killed { member } " ,
'punch' : f "You punched { member } " ,
'hug' : f " { member } has been squeezed tightly!" ,
'revive' : f " { member } has been brought back to life"
}
message = messages [ action ]
media_file = media_manager . get_random ( media_prefix )
if media_file :
await interaction . response . send_message (
message , file = discord . File ( media_file )
)
else :
await interaction . response . send_message ( message )
whitelisted_usernames = set ()
WHITELIST_FILE = Path ( "data/whitelisted_usernames.json" )
def load_whitelisted_usernames ():
global whitelisted_usernames
try :
if WHITELIST_FILE . exists ():
with open ( WHITELIST_FILE ) as f :
whitelisted_usernames = set ( json . load ( f ))
except Exception as e :
logger . error ( f "Error loading whitelisted usernames: { e } " )
def save_whitelisted_username ( username : str ):
whitelisted_usernames . add ( username )
try :
WHITELIST_FILE . parent . mkdir ( parents = True , exist_ok = True )
with open ( WHITELIST_FILE , 'w' ) as f :
json . dump ( list ( whitelisted_usernames ), f )
except Exception as e :
logger . error ( f "Error saving whitelisted username: { e } " )
async def minecraft_username_autocomplete (
interaction : discord . Interaction ,
current : str
) -> list :
return [
app_commands . Choice ( name = username , value = username )
for username in sorted ( whitelisted_usernames )
if current . lower () in username . lower ()
][: 25 ]
2026-01-13 16:03:54 -05:00
class CustomBot ( commands . Bot ):
def __init__ ( self ):
intents = discord . Intents . all ()
intents . message_content = True
super () . __init__ (
command_prefix = "." ,
intents = intents
)
self . status_cycle = cycle ([
"The Lotus Guild is boomin" ,
"lotusguild.org" ,
"Ranked Minesweeper"
])
self . remove_command ( "help" )
2026-02-10 20:11:22 -05:00
self . http_session : aiohttp . ClientSession = None
2026-01-13 16:03:54 -05:00
async def setup_hook ( self ):
2026-02-10 20:11:22 -05:00
self . http_session = aiohttp . ClientSession ()
if not MINECRAFT_RCON_PASSWORD :
logger . warning ( "MINECRAFT_RCON_PASSWORD not set - /minecraft command will fail" )
if not PELICAN_API_KEY :
logger . warning ( "PELICAN_API_KEY not set - Pelican features disabled" )
if not OLLAMA_URL :
2026-02-10 20:41:16 -05:00
logger . warning ( "OLLAMA_HOST not set - /ask command will fail" )
2026-02-10 20:11:22 -05:00
load_whitelisted_usernames ()
2026-01-13 16:03:54 -05:00
logger . info ( "Syncing commands..." )
await self . tree . sync ()
logger . info ( "Commands synced!" )
2026-02-10 20:11:22 -05:00
async def close ( self ):
if self . http_session :
await self . http_session . close ()
await audit_logger . flush ()
await super () . close ()
2026-01-13 16:03:54 -05:00
@tasks.loop ( seconds = STATUS_UPDATE_INTERVAL )
async def change_status ( self ):
try :
await self . change_presence ( activity = discord . Game ( next ( self . status_cycle )))
except Exception as e :
logger . error ( f "Error changing status: { e } " , exc_info = True )
@change_status.before_loop
async def before_change_status ( self ):
await self . wait_until_ready ()
async def on_ready ( self ):
logger . info ( f "Bot logged in as: { self . user . name } " )
logger . info ( f "Registered commands: { [ cmd . name for cmd in self . tree . get_commands ()] } " )
self . change_status . start ()
2026-02-10 20:11:22 -05:00
self . flush_audit_logs . start ()
@tasks.loop ( seconds = AUDIT_FLUSH_INTERVAL )
async def flush_audit_logs ( self ):
await audit_logger . flush ()
@flush_audit_logs.before_loop
async def before_flush_audit_logs ( self ):
await self . wait_until_ready ()
2026-01-13 16:03:54 -05:00
client = CustomBot ()
def get_role_from_emoji ( emoji_name : str ) -> str :
return EMOJI_ROLE_MAP . get ( emoji_name , emoji_name )
def has_role_check ( role_id : int ):
async def predicate ( interaction : discord . Interaction ) -> bool :
return any ( role . id == role_id for role in interaction . user . roles )
return app_commands . check ( predicate )
2026-02-10 20:49:55 -05:00
# ==================== EVENTS ====================
2026-01-13 16:03:54 -05:00
@client.event
async def on_message ( message ):
try :
2026-02-10 20:11:22 -05:00
if message . content . startswith (( '.' , '/' )):
logger . info ( f "Command in { message . channel } : { message . author } - { message . content [: 50 ] } " )
2026-02-01 11:50:34 -05:00
if message . author . id == 331862422996647938 and message . content . startswith ( '.' ):
await message . channel . send ( "Whatever conversation is happening right now, Jared is right" )
2026-01-13 16:03:54 -05:00
except Exception as e :
logger . error ( f "Error in on_message: { e } " , exc_info = True )
@client.event
async def on_member_join ( member ):
try :
logger . info ( f "New member joined: { member . name } (ID: { member . id } )" )
2026-02-10 20:11:22 -05:00
await audit_logger . log ( f "Member Joined \n User: { member . mention } \n Account Created: { member . created_at } " )
2026-01-13 16:03:54 -05:00
except Exception as e :
logger . error ( f "Error in on_member_join: { e } " , exc_info = True )
@client.event
async def on_member_remove ( member ):
try :
logger . info ( f "Member left: { member . name } (ID: { member . id } )" )
2026-02-10 20:11:22 -05:00
await audit_logger . log ( f "Member Left \n User: { member . name } ( { member . id } )" )
2026-01-13 16:03:54 -05:00
except Exception as e :
logger . error ( f "Error in on_member_remove: { e } " , exc_info = True )
@client.event
async def on_message_delete ( message ):
try :
2026-02-10 20:11:22 -05:00
logger . warning ( f "Message deleted in { message . channel } : { message . author } " )
await audit_logger . log ( f "Message Deleted \n Channel: { message . channel } \n User: { message . author } \n Content: { message . content } " )
2026-01-13 16:03:54 -05:00
except Exception as e :
logger . error ( f "Error in on_message_delete: { e } " , exc_info = True )
@client.event
async def on_voice_state_update ( member , before , after ):
try :
if before . channel != after . channel :
if after . channel :
2026-02-10 20:11:22 -05:00
await audit_logger . log ( f "Voice Join \n User: { member . mention } \n Channel: { after . channel . name } " )
2026-01-13 16:03:54 -05:00
else :
2026-02-10 20:11:22 -05:00
await audit_logger . log ( f "Voice Leave \n User: { member . mention } \n Channel: { before . channel . name } " )
2026-01-13 16:03:54 -05:00
except Exception as e :
logger . error ( f "Error in on_voice_state_update: { e } " , exc_info = True )
@client.event
async def on_integration_create ( integration ):
try :
2026-02-10 20:11:22 -05:00
await audit_logger . log ( f "New Integration Added \n Name: { integration . name } \n Type: { integration . type } " )
2026-01-13 16:03:54 -05:00
except Exception as e :
logger . error ( f "Error in on_integration_create: { e } " , exc_info = True )
@client.event
async def on_scheduled_event_create ( event ):
try :
2026-02-10 20:11:22 -05:00
await audit_logger . log ( f "Event Created \n Name: { event . name } \n Start Time: { event . start_time } " )
2026-01-13 16:03:54 -05:00
except Exception as e :
logger . error ( f "Error in on_scheduled_event_create: { e } " , exc_info = True )
@client.event
async def on_scheduled_event_update ( before , after ):
try :
2026-02-10 20:11:22 -05:00
await audit_logger . log ( f "Event Updated \n Name: { after . name } \n Changes Made" )
2026-01-13 16:03:54 -05:00
except Exception as e :
logger . error ( f "Error in on_scheduled_event_update: { e } " , exc_info = True )
@client.event
async def on_member_update ( before , after ):
try :
if before . timed_out_until != after . timed_out_until :
2026-02-10 20:11:22 -05:00
await audit_logger . log ( f "Member Timeout \n User: { after . mention } \n Until: { after . timed_out_until } " )
2026-01-13 16:03:54 -05:00
if before . premium_since != after . premium_since :
2026-02-10 20:11:22 -05:00
await audit_logger . log ( f "Server Boost \n User: { after . mention } boosted the server!" )
2026-01-13 16:03:54 -05:00
if before . nick != after . nick :
2026-02-10 20:11:22 -05:00
await audit_logger . log ( f "Nickname Change \n User: { before . mention } \n Before: { before . nick } \n After: { after . nick } " )
2026-01-13 16:03:54 -05:00
if before . roles != after . roles :
logger . info ( f "Role change for { before . name } : { before . roles } -> { after . roles } " )
2026-02-10 20:11:22 -05:00
await audit_logger . log (
f "Role Update \n User: { before . mention } \n "
2026-01-13 16:03:54 -05:00
f "Before: { ', ' . join ([ r . name for r in before . roles ]) } \n "
f "After: { ', ' . join ([ r . name for r in after . roles ]) } "
)
except Exception as e :
logger . error ( f "Error in on_member_update: { e } " , exc_info = True )
@client.event
async def on_member_ban ( guild , user ):
try :
2026-02-10 20:11:22 -05:00
await audit_logger . log ( f "Member Banned \n User: { user . name } " )
2026-01-13 16:03:54 -05:00
except Exception as e :
logger . error ( f "Error in on_member_ban: { e } " , exc_info = True )
@client.event
async def on_guild_role_update ( before , after ):
try :
if before . permissions != after . permissions :
2026-02-10 20:11:22 -05:00
await audit_logger . log ( f "Role Permissions Updated \n Role: { after . name } " )
2026-01-13 16:03:54 -05:00
except Exception as e :
logger . error ( f "Error in on_guild_role_update: { e } " , exc_info = True )
@client.event
async def on_guild_emoji_create ( emoji ):
try :
2026-02-10 20:11:22 -05:00
await audit_logger . log ( f "New Emoji Added \n Name: { emoji . name } " )
2026-01-13 16:03:54 -05:00
except Exception as e :
logger . error ( f "Error in on_guild_emoji_create: { e } " , exc_info = True )
@client.event
async def on_guild_sticker_create ( sticker ):
try :
2026-02-10 20:11:22 -05:00
await audit_logger . log ( f "New Sticker Added \n Name: { sticker . name } " )
2026-01-13 16:03:54 -05:00
except Exception as e :
logger . error ( f "Error in on_guild_sticker_create: { e } " , exc_info = True )
@client.event
async def on_guild_role_create ( role ):
try :
2026-02-10 20:11:22 -05:00
await audit_logger . log ( f "Role Created \n Name: { role . name } \n Color: { role . color } " )
2026-01-13 16:03:54 -05:00
except Exception as e :
logger . error ( f "Error in on_guild_role_create: { e } " , exc_info = True )
@client.event
async def on_guild_role_delete ( role ):
try :
2026-02-10 20:11:22 -05:00
await audit_logger . log ( f "Role Deleted \n Name: { role . name } " )
2026-01-13 16:03:54 -05:00
except Exception as e :
logger . error ( f "Error in on_guild_role_delete: { e } " , exc_info = True )
@client.event
async def on_message_edit ( before , after ):
try :
if before . content == after . content :
return
2026-02-10 20:11:22 -05:00
logger . info ( f "Message edited in { before . channel } " )
2026-01-13 16:03:54 -05:00
2026-02-10 20:11:22 -05:00
await audit_logger . log (
f "Message Edited \n "
2026-01-13 16:03:54 -05:00
f "Channel: { before . channel . mention } \n "
f "User: { before . author . mention } \n "
f "Before: { before . content } \n "
f "After: { after . content } "
)
except Exception as e :
logger . error ( f "Error in on_message_edit: { e } " , exc_info = True )
@client.event
async def on_guild_channel_create ( channel ):
try :
logger . info ( f "Channel created: { channel . name } " )
except Exception as e :
logger . error ( f "Error in on_guild_channel_create: { e } " , exc_info = True )
@client.event
async def on_guild_channel_delete ( channel ):
try :
logger . info ( f "Channel deleted: { channel . name } " )
except Exception as e :
logger . error ( f "Error in on_guild_channel_delete: { e } " , exc_info = True )
@client.event
async def on_raw_reaction_add ( payload ):
try :
if payload . message_id != REACTION_MESSAGE_ID :
return
guild = client . get_guild ( payload . guild_id )
if not guild :
return
member = await guild . fetch_member ( payload . user_id )
if not member :
logger . warning ( f "Member not found: { payload . user_id } " )
return
role_name = get_role_from_emoji ( payload . emoji . name )
role = discord . utils . get ( guild . roles , name = role_name )
if role :
await member . add_roles ( role )
try :
member_role = discord . utils . get ( guild . roles , name = 'Member' )
if member_role :
await member . add_roles ( member_role )
except Exception as e :
logger . error ( f "Failed to add Member role: { e } " )
logger . info ( f "Role { role_name } added to { member . name } " )
else :
logger . warning ( f "Role not found: { role_name } " )
except Exception as e :
logger . error ( f "Error in on_raw_reaction_add: { e } " , exc_info = True )
@client.event
async def on_raw_reaction_remove ( payload ):
try :
if payload . message_id != REACTION_MESSAGE_ID :
return
guild = client . get_guild ( payload . guild_id )
if not guild :
return
member = await guild . fetch_member ( payload . user_id )
if not member :
logger . warning ( f "Member not found: { payload . user_id } " )
return
role_name = get_role_from_emoji ( payload . emoji . name )
role = discord . utils . get ( guild . roles , name = role_name )
if role :
await member . remove_roles ( role )
logger . info ( f "Role { role_name } removed from { member . name } " )
else :
logger . warning ( f "Role not found: { role_name } " )
except Exception as e :
logger . error ( f "Error in on_raw_reaction_remove: { e } " , exc_info = True )
2026-02-10 20:11:22 -05:00
# ==================== SLASH COMMANDS ====================
2026-01-13 16:03:54 -05:00
@client.tree.command ( name = "help" , description = "Shows all available commands" )
2026-02-10 20:11:22 -05:00
@handle_command_errors
2026-01-13 16:03:54 -05:00
async def help_command ( interaction : discord . Interaction ):
2026-02-10 20:11:22 -05:00
embed = discord . Embed (
color = discord . Color . from_rgb ( 152 , 0 , 0 ),
title = "Command List" ,
description = "Describes what commands do and how to use them."
)
embed . set_author ( name = "Help Page (Lotus Bot)" , icon_url = "https://lotusguild.org/Lotus.png" )
embed . set_image ( url = "https://lotusguild.org/favicon.ico" )
embed . set_thumbnail ( url = "https://lotusguild.org/favicon.ico" )
embed . add_field ( name = "/help" , value = "Shows this help message with all available commands" , inline = False )
embed . add_field ( name = "/ping" , value = "Shows the bot's response time in milliseconds" , inline = False )
embed . add_field ( name = "/8ball" , value = "Ask the magic 8-ball a question and receive an answer" , inline = False )
embed . add_field ( name = "/fortune" , value = "Get your fortune cookie message" , inline = False )
embed . add_field ( name = "/flip" , value = "Flip a coin and get heads or tails" , inline = False )
embed . add_field ( name = "/roll" , value = "Roll dice (format: NdS, example: 2d6)" , inline = False )
embed . add_field ( name = "/random" , value = "Generate a random number between specified range" , inline = False )
embed . add_field ( name = "/rps" , value = "Play Rock Paper Scissors against the bot" , inline = False )
embed . add_field ( name = "/poll" , value = "Create a simple yes/no poll" , inline = False )
2026-02-10 20:41:16 -05:00
embed . add_field ( name = "/trivia" , value = "Play a trivia game with random questions" , inline = False )
2026-02-10 20:11:22 -05:00
embed . add_field ( name = "/agent" , value = "Get a random Valorant agent with their role" , inline = False )
embed . add_field ( name = "/champion" , value = "Get a random League of Legends champion with their lane" , inline = False )
embed . add_field ( name = "/minecraft" , value = "Whitelists a player on the Minecraft server and shows server info" , inline = False )
embed . add_field ( name = "/hytale" , value = "Request whitelist on the Hytale server" , inline = False )
embed . add_field ( name = "/problem" , value = "Express your opinion about Canada geese" , inline = False )
embed . add_field ( name = "/kill" , value = "Kill another user with a random animation" , inline = False )
embed . add_field ( name = "/punch" , value = "Punch another user with a random animation" , inline = False )
embed . add_field ( name = "/hug" , value = "Hug another user with a random animation" , inline = False )
embed . add_field ( name = "/revive" , value = "Revive another user with a random animation" , inline = False )
if discord . utils . get ( interaction . user . roles , id = ADMIN_ROLE_ID ):
embed . add_field ( name = "Admin Commands" , value = "---------------" , inline = False )
embed . add_field ( name = "/clear" , value = "Clears specified number of messages from the channel" , inline = False )
embed . add_field ( name = "/lockdown" , value = "Lock a channel to prevent messages" , inline = False )
embed . add_field ( name = "/unlock" , value = "Unlock a previously locked channel" , inline = False )
2026-02-10 20:41:16 -05:00
embed . add_field ( name = "/health" , value = "Check bot system health and service status" , inline = False )
2026-02-10 20:11:22 -05:00
embed . set_footer ( text = "Made by https://lotusguild.org" )
await interaction . response . send_message ( embed = embed )
2026-01-13 16:03:54 -05:00
@client.tree.command ( name = "ping" , description = "Check the bot's latency" )
2026-02-10 20:11:22 -05:00
@handle_command_errors
2026-01-13 16:03:54 -05:00
async def ping ( interaction : discord . Interaction ):
2026-02-10 20:11:22 -05:00
await interaction . response . send_message ( f "Pong! { round ( client . latency * 1000 ) } ms" )
2026-01-13 16:03:54 -05:00
@client.tree.command ( name = "problem" , description = "Express your opinion about Canada geese" )
2026-02-10 20:11:22 -05:00
@handle_command_errors
2026-01-13 16:03:54 -05:00
async def problem ( interaction : discord . Interaction ):
2026-02-10 20:11:22 -05:00
media_file = media_manager . get_random ( "canadagoose" )
msg = "If you got a problem with canada gooses then you got a problem with me and I suggest you let that one marinate."
if media_file :
await interaction . response . send_message ( msg , file = discord . File ( media_file ))
else :
await interaction . response . send_message ( msg )
2026-01-13 16:03:54 -05:00
@client.tree.command ( name = "clear" , description = "Clear messages from the channel" )
@app_commands.describe ( amount = "Number of messages to delete" )
@has_role_check ( ADMIN_ROLE_ID )
2026-02-10 20:11:22 -05:00
@handle_command_errors
2026-01-13 16:03:54 -05:00
async def clear ( interaction : discord . Interaction , amount : int = 5 ):
2026-02-10 20:11:22 -05:00
await interaction . response . defer ( ephemeral = True )
deleted = await interaction . channel . purge ( limit = amount )
await interaction . followup . send ( f "Successfully cleared { len ( deleted ) } messages!" , ephemeral = True )
2026-01-13 16:03:54 -05:00
@client.tree.command ( name = "lockdown" , description = "Lock a channel" )
@has_role_check ( ADMIN_ROLE_ID )
2026-02-10 20:11:22 -05:00
@handle_command_errors
2026-01-13 16:03:54 -05:00
async def lockdown ( interaction : discord . Interaction , channel : discord . TextChannel = None ):
2026-02-10 20:11:22 -05:00
channel = channel or interaction . channel
await channel . set_permissions ( interaction . guild . default_role , send_messages = False )
await audit_logger . log ( f "Channel Locked \n Channel: { channel . name } " )
await interaction . response . send_message ( f "Locked { channel . mention } " )
2026-01-13 16:03:54 -05:00
@client.tree.command ( name = "unlock" , description = "Unlock a channel" )
@has_role_check ( ADMIN_ROLE_ID )
2026-02-10 20:11:22 -05:00
@handle_command_errors
2026-01-13 16:03:54 -05:00
async def unlock ( interaction : discord . Interaction , channel : discord . TextChannel = None ):
2026-02-10 20:11:22 -05:00
channel = channel or interaction . channel
await channel . set_permissions ( interaction . guild . default_role , send_messages = True )
await audit_logger . log ( f "Channel Unlocked \n Channel: { channel . name } " )
await interaction . response . send_message ( f "Unlocked { channel . mention } " )
2026-01-13 16:03:54 -05:00
@client.tree.command ( name = "minecraft" , description = "Whitelist a player on the Minecraft server" )
@app_commands.describe ( minecraft_username = "The Minecraft username to whitelist" )
2026-02-10 20:11:22 -05:00
@app_commands.autocomplete ( minecraft_username = minecraft_username_autocomplete )
2026-01-13 16:03:54 -05:00
@has_role_check ( MINECRAFT_ROLE_ID )
2026-02-10 20:11:22 -05:00
@handle_command_errors
2026-01-13 16:03:54 -05:00
async def minecraft ( interaction : discord . Interaction , minecraft_username : str ):
2026-02-10 20:11:22 -05:00
if not await username_cache . is_valid ( minecraft_username , client . http_session ):
await interaction . response . send_message ( "Invalid MC Username" , ephemeral = True )
return
if not MINECRAFT_RCON_PASSWORD :
logger . error ( "MINECRAFT_RCON_PASSWORD not configured" )
await interaction . response . send_message (
"Server configuration error. Please contact an administrator." ,
ephemeral = True
2026-01-13 16:03:54 -05:00
)
2026-02-10 20:11:22 -05:00
return
2026-01-13 16:03:54 -05:00
2026-02-10 20:11:22 -05:00
await interaction . response . defer ()
2026-01-13 16:30:09 -05:00
2026-02-10 20:11:22 -05:00
try :
response = await rcon_command ( MINECRAFT_RCON_HOST , MINECRAFT_RCON_PASSWORD ,
f "whitelist add { minecraft_username } " )
logger . info ( f "RCON response: { response } " )
except Exception as e :
logger . error ( f "RCON error: { e } " , exc_info = True )
await interaction . followup . send (
"An error occurred while whitelisting the player (jared will fix just let him know)."
)
return
save_whitelisted_username ( minecraft_username )
minecraft_embed = discord . Embed (
color = discord . Color . from_rgb ( 152 , 0 , 0 ),
title = "Minecraft"
)
minecraft_embed . set_author (
name = "(Lotus Bot)" ,
icon_url = "https://photos.lotusguild.org/api/assets/3c4eb2da-0d06-407f-bdb7-c9e4cf795f0a/thumbnail?key=4aoZxX5-FHE3m_Ywwz1uGo3iNW53kmFztxfUw91PdOgphPNxayLFicNuxPvit1OYTpY&size=preview&c=jUqDBQAWF9iId3J %2F yAeIcIAICEd4d3BzSA%3D%3D"
)
minecraft_embed . set_image (
url = "https://photos.lotusguild.org/api/assets/3c4eb2da-0d06-407f-bdb7-c9e4cf795f0a/thumbnail?key=4aoZxX5-FHE3m_Ywwz1uGo3iNW53kmFztxfUw91PdOgphPNxayLFicNuxPvit1OYTpY&size=preview&c=jUqDBQAWF9iId3J %2F yAeIcIAICEd4d3BzSA%3D%3D"
)
minecraft_embed . set_thumbnail (
url = "https://photos.lotusguild.org/api/assets/3c4eb2da-0d06-407f-bdb7-c9e4cf795f0a/thumbnail?key=4aoZxX5-FHE3m_Ywwz1uGo3iNW53kmFztxfUw91PdOgphPNxayLFicNuxPvit1OYTpY&size=preview&c=jUqDBQAWF9iId3J %2F yAeIcIAICEd4d3BzSA%3D%3D"
)
minecraft_embed . add_field ( name = "You" , value = "have been whitelisted on the SMP" , inline = False )
minecraft_embed . add_field ( name = "Server Address" , value = "minecraft.lotusguild.org" , inline = False )
minecraft_embed . add_field ( name = "Version" , value = "1.21.11" , inline = False )
minecraft_embed . set_footer ( text = "Thanks for using Lotus Minecraft Server!" )
await interaction . followup . send ( embed = minecraft_embed )
2026-01-13 16:30:09 -05:00
@client.tree.command ( name = "hytale" , description = "Request whitelist on the Hytale server" )
@app_commands.describe ( hytale_username = "Your Hytale username" )
2026-01-13 16:44:20 -05:00
@has_role_check ( HYTALE_ROLE_ID )
2026-02-10 20:11:22 -05:00
@handle_command_errors
2026-01-13 16:30:09 -05:00
async def hytale ( interaction : discord . Interaction , hytale_username : str ):
2026-02-10 20:11:22 -05:00
await interaction . response . defer ()
if not hytale_username . replace ( '_' , '' ) . replace ( '-' , '' ) . isalnum ():
await interaction . followup . send ( "Invalid username. Use only letters, numbers, _ and -" , ephemeral = True )
return
if not ( MIN_USERNAME_LENGTH <= len ( hytale_username ) <= MAX_USERNAME_LENGTH ):
await interaction . followup . send (
f "Username must be { MIN_USERNAME_LENGTH } - { MAX_USERNAME_LENGTH } characters" ,
ephemeral = True
2026-01-13 16:30:09 -05:00
)
2026-02-10 20:11:22 -05:00
return
2026-01-13 16:30:09 -05:00
2026-02-10 20:11:22 -05:00
await audit_logger . log (
f "Hytale Whitelist Request \n User: { interaction . user . mention } \n Username: ` { hytale_username } ` \n Action: Run `/whitelist add { hytale_username } `" ,
color = 0x00ff00
)
embed = discord . Embed ( color = discord . Color . from_rgb ( 152 , 0 , 0 ), title = "Hytale" )
embed . set_author ( name = "(Lotus Bot)" , icon_url = "https://photos.lotusguild.org/api/assets/3c4eb2da-0d06-407f-bdb7-c9e4cf795f0a/thumbnail?key=wd3-Z4zFdrR6WBUfXLnBN8RgeT9tivgQwT6iDN3T0AaBIOfyIuYrbEszABB8OvUplFM&size=preview&c=jUqDBQAWF9iId3J %2F yAeIcIAICEd4d3BzSA%3D%3D" )
embed . add_field ( name = "Request Submitted" , value = f "Whitelist request for ` { hytale_username } ` sent!" , inline = False )
embed . add_field ( name = "Server Address" , value = "hytale.lotusguild.org" , inline = False )
embed . add_field ( name = "Status" , value = "An admin will whitelist you shortly!" , inline = False )
embed . set_footer ( text = "Welcome to Lotus Hytale Server!" )
await interaction . followup . send ( embed = embed )
logger . info ( f "Hytale whitelist request: { hytale_username } by { interaction . user . name } " )
2026-01-13 16:03:54 -05:00
@client.tree.command ( name = "ask" , description = "Ask a question to Lotus LLM" )
@app_commands.describe ( question = "Your question for the AI" )
2026-02-10 20:11:22 -05:00
@app_commands.checks.cooldown ( 1 , COOLDOWN_MINUTES * 60 , key = lambda i : i . user . id )
2026-01-13 16:03:54 -05:00
@has_role_check ( COOL_KIDS_ROLE_ID )
2026-02-10 20:11:22 -05:00
@handle_command_errors
2026-01-13 16:03:54 -05:00
async def ask ( interaction : discord . Interaction , question : str ):
2026-02-10 20:11:22 -05:00
question = sanitize_input ( question )
if not question :
await interaction . response . send_message ( "Please provide a valid question." , ephemeral = True )
return
await interaction . response . defer ()
model = "lotusllmben" if interaction . user . id == 460640040096104459 else "lotusllm"
logger . info ( f "Sending question to Ollama: { question } " )
async with client . http_session . post (
f " { OLLAMA_URL } /api/generate" ,
json = {
"model" : model ,
"prompt" : question ,
"stream" : True
}
) as response :
full_response = ""
async for line in response . content :
try :
chunk = json . loads ( line )
if "response" in chunk :
full_response += chunk [ "response" ]
except json . JSONDecodeError as e :
logger . error ( f "Failed to parse JSON: { e } " )
embed = discord . Embed (
title = "Lotus LLM" ,
description = full_response if full_response else "No response received from server" ,
color = discord . Color . from_rgb ( 152 , 0 , 0 )
)
embed . add_field ( name = "Question" , value = question , inline = False )
embed . set_footer ( text = f "Asked by { interaction . user . display_name } " )
await interaction . followup . send ( embed = embed )
2026-01-13 16:03:54 -05:00
@client.tree.command ( name = "8ball" , description = "Ask the magic 8-ball a question" )
@app_commands.describe ( question = "What would you like to ask the 8ball?" )
2026-02-10 20:11:22 -05:00
@handle_command_errors
2026-01-13 16:03:54 -05:00
async def eight_ball ( interaction : discord . Interaction , question : str ):
2026-02-10 20:11:22 -05:00
possible_responses = [
"It is certain" , "Without a doubt" , "You may rely on it" ,
"Yes definitely" , "It is decidedly so" , "As I see it, yes" ,
"Most likely" , "Yes sir!" , "Hell yeah my dude" , "100 % e asily" ,
"Reply hazy try again" , "Ask again later" , "Better not tell you now" ,
"Cannot predict now" , "Concentrate and ask again" , "Idk bro" ,
"Don't count on it" , "My reply is no" , "My sources say no" ,
"Outlook not so good" , "Very doubtful" , "Hell no" , "Prolly not"
]
2026-01-13 16:03:54 -05:00
2026-02-10 20:11:22 -05:00
embed = discord . Embed (
color = discord . Color . from_rgb ( 152 , 0 , 0 ),
title = "Magic 8-Ball"
)
embed . add_field ( name = "Question" , value = question , inline = False )
embed . add_field ( name = "Answer" , value = random . choice ( possible_responses ), inline = False )
embed . set_footer ( text = f "Asked by { interaction . user . display_name } " )
await interaction . response . send_message ( embed = embed )
2026-01-13 16:03:54 -05:00
@client.tree.command ( name = "fortune" , description = "Get your fortune cookie message" )
2026-02-10 20:11:22 -05:00
@handle_command_errors
2026-01-13 16:03:54 -05:00
async def fortune ( interaction : discord . Interaction ):
2026-02-10 20:11:22 -05:00
possible_responses = [
"If you eat something & nobody sees you eat it, it has no calories" ,
"Your pet is plotting world domination" ,
"Error 404: Fortune not found. Try again after system reboot" ,
"The fortune you seek is in another cookie" ,
"A journey of a thousand miles begins with ordering delivery" ,
"You will find great fortune... in between your couch cushions" ,
"A true friend is someone who tells you when your stream is muted" ,
"Your next competitive match will be legendary" ,
"The cake is still a lie" ,
"Press Alt+F4 for instant success" ,
"You will not encounter any campers today" ,
"Your tank will have a healer" ,
"No one will steal your pentakill" ,
"Your random teammate will have a mic" ,
"You will find diamonds on your first dig" ,
"The boss will drop the rare loot" ,
"Your speedrun will be WR pace" ,
"No lag spikes in your next match" ,
"Your gaming chair will grant you powers" ,
"The RNG gods will bless you" ,
"You will not get third partied" ,
"Your squad will actually stick together" ,
"The enemy team will forfeit at 15" ,
"Your aim will be crispy today" ,
"You will escape the backrooms" ,
"The imposter will not sus you" ,
"Your Minecraft bed will remain unbroken" ,
"You will get Play of the Game" ,
"Your next meme will go viral" ,
"Someone is talking about you in their Discord server" ,
"Your FBI agent thinks you're hilarious" ,
"Your next TikTok will hit the FYP, if the government doesn't ban it first" ,
"Someone will actually read your Twitter thread" ,
"Your DMs will be blessed with quality memes today" ,
"Touch grass (respectfully)" ,
"The algorithm will be in your favor today" ,
"Your next Spotify shuffle will hit different" ,
"Someone saved your Instagram post" ,
"Your Reddit comment will get gold" ,
"POV: You're about to go viral" ,
"Main character energy detected" ,
"No cap, you're gonna have a great day fr fr" ,
"Your rizz levels are increasing" ,
"You will not get ratio'd today" ,
"Someone will actually use your custom emoji" ,
"Your next selfie will be iconic" ,
"Buy a dolphin - your life will have a porpoise" ,
"Stop procrastinating - starting tomorrow" ,
"Catch fire with enthusiasm - people will come for miles to watch you burn" ,
"Your code will compile on the first try today" ,
"A semicolon will save your day" ,
"The bug you've been hunting is just a typo" ,
"Your next Git commit will be perfect" ,
"You will find the solution on the first StackOverflow link" ,
"Your Docker container will build without errors" ,
"The cloud is just someone else's computer" ,
"Your backup strategy will soon prove its worth" ,
"A mechanical keyboard is in your future" ,
"You will finally understand regex... maybe" ,
"Your CSS will align perfectly on the first try" ,
"Someone will star your GitHub repo today" ,
"Your Linux installation will not break after updates" ,
"You will remember to push your changes before shutdown" ,
"Your code comments will actually make sense in 6 months" ,
"The missing curly brace is on line 247" ,
"Have you tried turning it off and on again?" ,
"Your next pull request will be merged without comments" ,
"Your keyboard RGB will sync perfectly today" ,
"You will find that memory leak" ,
"Your next algorithm will have O(1) complexity" ,
"The force quit was strong with this one" ,
"Ctrl+S will save you today" ,
"Your next Python script will need no debugging" ,
"Your next API call will return 200 OK"
]
2026-01-13 16:03:54 -05:00
2026-02-10 20:11:22 -05:00
embed = discord . Embed (
color = discord . Color . from_rgb ( 152 , 0 , 0 ),
title = "Fortune Cookie"
)
embed . add_field ( name = "Your Fortune" , value = random . choice ( possible_responses ), inline = False )
embed . set_footer ( text = f "Cracked open by { interaction . user . display_name } " )
await interaction . response . send_message ( embed = embed )
2026-01-13 16:03:54 -05:00
@client.tree.command ( name = "flip" , description = "Flip a coin" )
2026-02-10 20:11:22 -05:00
@handle_command_errors
2026-01-13 16:03:54 -05:00
async def flip ( interaction : discord . Interaction ):
2026-02-10 20:11:22 -05:00
result = random . choice ([ "Heads" , "Tails" ])
embed = discord . Embed ( title = "Coin Flip" , color = discord . Color . from_rgb ( 152 , 0 , 0 ))
embed . add_field ( name = "Result" , value = result )
await interaction . response . send_message ( embed = embed )
2026-01-13 16:03:54 -05:00
@client.tree.command ( name = "roll" , description = "Roll dice (e.g. 2d6)" )
@app_commands.describe ( dice = "Format: NdS (N=number of dice, S=sides)" )
2026-02-10 20:11:22 -05:00
@handle_command_errors
2026-01-13 16:03:54 -05:00
async def roll ( interaction : discord . Interaction , dice : str = "1d6" ):
try :
num , sides = map ( int , dice . lower () . split ( 'd' ))
2026-02-10 20:11:22 -05:00
except ValueError :
2026-01-13 16:03:54 -05:00
await interaction . response . send_message ( "Please use format: NdS (example: 2d6)" , ephemeral = True )
2026-02-10 20:11:22 -05:00
return
if num < 1 or num > MAX_DICE_COUNT :
await interaction . response . send_message ( f "Number of dice must be 1- { MAX_DICE_COUNT } " , ephemeral = True )
return
if sides < 2 or sides > MAX_DICE_SIDES :
await interaction . response . send_message ( f "Sides must be 2- { MAX_DICE_SIDES } " , ephemeral = True )
return
results = [ random . randint ( 1 , sides ) for _ in range ( num )]
embed = discord . Embed ( title = "Dice Roll" , color = discord . Color . from_rgb ( 152 , 0 , 0 ))
embed . add_field ( name = "Results" , value = f "Rolls: { results } \n Total: { sum ( results ) } " )
await interaction . response . send_message ( embed = embed )
2026-01-13 16:03:54 -05:00
@client.tree.command ( name = "random" , description = "Generate a random number" )
@app_commands.describe ( min = "Minimum number" , max = "Maximum number" )
2026-02-10 20:11:22 -05:00
@handle_command_errors
2026-01-13 16:03:54 -05:00
async def random_number ( interaction : discord . Interaction , min : int = 1 , max : int = 100 ):
2026-02-10 20:11:22 -05:00
result = random . randint ( min , max )
embed = discord . Embed ( title = "Random Number" , color = discord . Color . from_rgb ( 152 , 0 , 0 ))
embed . add_field ( name = "Result" , value = str ( result ))
await interaction . response . send_message ( embed = embed )
2026-01-13 16:03:54 -05:00
@client.tree.command ( name = "rps" , description = "Play Rock Paper Scissors" )
@app_commands.describe ( choice = "Your choice: rock, paper, or scissors" )
2026-02-10 20:11:22 -05:00
@handle_command_errors
2026-01-13 16:03:54 -05:00
async def rps ( interaction : discord . Interaction , choice : str ):
2026-02-10 20:11:22 -05:00
choices = [ "rock" , "paper" , "scissors" ]
bot_choice = random . choice ( choices )
choice = choice . lower ()
2026-01-13 16:03:54 -05:00
2026-02-10 20:11:22 -05:00
if choice not in choices :
await interaction . response . send_message ( "Please choose rock, paper, or scissors!" , ephemeral = True )
return
2026-01-13 16:03:54 -05:00
2026-02-10 20:11:22 -05:00
embed = discord . Embed ( title = "Rock Paper Scissors" , color = discord . Color . from_rgb ( 152 , 0 , 0 ))
embed . add_field ( name = "Your Choice" , value = choice . capitalize ())
embed . add_field ( name = "Bot's Choice" , value = bot_choice . capitalize ())
2026-01-13 16:03:54 -05:00
2026-02-10 20:11:22 -05:00
if choice == bot_choice :
result = "It's a tie!"
elif ( choice == "rock" and bot_choice == "scissors" ) or \
( choice == "paper" and bot_choice == "rock" ) or \
( choice == "scissors" and bot_choice == "paper" ):
result = "You win!"
else :
result = "Bot wins!"
2026-01-13 16:03:54 -05:00
2026-02-10 20:11:22 -05:00
embed . add_field ( name = "Result" , value = result , inline = False )
await interaction . response . send_message ( embed = embed )
2026-01-13 16:03:54 -05:00
@client.tree.command ( name = "poll" , description = "Create a simple yes/no poll" )
@app_commands.describe ( question = "The poll question" )
2026-02-10 20:11:22 -05:00
@handle_command_errors
2026-01-13 16:03:54 -05:00
async def poll ( interaction : discord . Interaction , question : str ):
2026-02-10 20:11:22 -05:00
embed = discord . Embed ( title = "Poll" , description = question , color = discord . Color . from_rgb ( 152 , 0 , 0 ))
embed . set_footer ( text = f "Created by { interaction . user . display_name } " )
await interaction . response . send_message ( embed = embed )
message = await interaction . original_response ()
2026-02-10 20:49:55 -05:00
await message . add_reaction ( " \U0001f44d " )
await message . add_reaction ( " \U0001f44e " )
2026-01-13 16:03:54 -05:00
@client.tree.command ( name = "kill" , description = "Kill another user" )
@app_commands.describe ( member = "The user to kill" )
2026-02-10 20:11:22 -05:00
@handle_command_errors
2026-01-13 16:03:54 -05:00
async def kill ( interaction : discord . Interaction , member : discord . Member ):
2026-02-10 20:11:22 -05:00
await send_interaction_media ( interaction , member , 'kill' , 'kill' )
2026-01-13 16:03:54 -05:00
@client.tree.command ( name = "punch" , description = "Punch another user" )
@app_commands.describe ( member = "The user to punch" )
2026-02-10 20:11:22 -05:00
@handle_command_errors
2026-01-13 16:03:54 -05:00
async def punch ( interaction : discord . Interaction , member : discord . Member ):
2026-02-10 20:11:22 -05:00
await send_interaction_media ( interaction , member , 'punch' , 'punch' )
2026-01-13 16:03:54 -05:00
@client.tree.command ( name = "hug" , description = "Hug another user" )
@app_commands.describe ( member = "The user to hug" )
2026-02-10 20:11:22 -05:00
@handle_command_errors
2026-01-13 16:03:54 -05:00
async def hug ( interaction : discord . Interaction , member : discord . Member ):
2026-02-10 20:11:22 -05:00
await send_interaction_media ( interaction , member , 'hug' , 'hug' )
2026-01-13 16:03:54 -05:00
@client.tree.command ( name = "revive" , description = "Revive another user" )
@app_commands.describe ( member = "The user to revive" )
2026-02-10 20:11:22 -05:00
@handle_command_errors
2026-01-13 16:03:54 -05:00
async def revive ( interaction : discord . Interaction , member : discord . Member ):
2026-02-10 20:11:22 -05:00
await send_interaction_media ( interaction , member , 'revive' , 'revive' )
2026-01-13 16:03:54 -05:00
@client.tree.command ( name = "agent" , description = "Get a random Valorant agent" )
2026-02-10 20:11:22 -05:00
@handle_command_errors
2026-01-13 16:03:54 -05:00
async def agent ( interaction : discord . Interaction ):
2026-02-10 20:11:22 -05:00
agents = {
2026-02-10 20:41:16 -05:00
"Duelists" : [ "Jett" , "Phoenix" , "Raze" , "Reyna" , "Yoru" , "Neon" , "Iso" , "Waylay" ],
"Controllers" : [ "Brimstone" , "Viper" , "Omen" , "Astra" , "Harbor" , "Clove" ],
"Initiators" : [ "Sova" , "Breach" , "Skye" , "KAY/O" , "Fade" , "Gekko" , "Tejo" ],
"Sentinels" : [ "Killjoy" , "Cypher" , "Sage" , "Chamber" , "Deadlock" , "Vyse" , "Veto" ]
2026-02-10 20:11:22 -05:00
}
2026-01-13 16:03:54 -05:00
2026-02-10 20:11:22 -05:00
category = random . choice ( list ( agents . keys ()))
selected_agent = random . choice ( agents [ category ])
2026-01-13 16:03:54 -05:00
2026-02-10 20:11:22 -05:00
embed = discord . Embed (
title = "Valorant Agent Picker" ,
color = discord . Color . from_rgb ( 152 , 0 , 0 )
)
embed . add_field ( name = "Selected Agent" , value = selected_agent , inline = False )
embed . add_field ( name = "Role" , value = category , inline = False )
embed . set_footer ( text = f "Selected for { interaction . user . display_name } " )
2026-01-13 16:03:54 -05:00
2026-02-10 20:11:22 -05:00
await interaction . response . send_message ( embed = embed )
2026-01-13 16:03:54 -05:00
@client.tree.command ( name = "champion" , description = "Get a random League of Legends champion" )
2026-02-10 20:11:22 -05:00
@handle_command_errors
2026-01-13 16:03:54 -05:00
async def champion ( interaction : discord . Interaction ):
2026-02-10 20:11:22 -05:00
champions = {
2026-02-10 20:41:16 -05:00
"Top" : [
"Aatrox" , "Ambessa" , "Aurora" , "Camille" , "Cho'Gath" , "Darius" ,
"Dr. Mundo" , "Fiora" , "Gangplank" , "Garen" , "Gnar" , "Gragas" ,
"Gwen" , "Illaoi" , "Irelia" , "Jax" , "Jayce" , "K'Sante" , "Kennen" ,
"Kled" , "Malphite" , "Mordekaiser" , "Nasus" , "Olaf" , "Ornn" ,
"Poppy" , "Quinn" , "Renekton" , "Riven" , "Rumble" , "Sett" , "Shen" ,
"Singed" , "Sion" , "Teemo" , "Trundle" , "Tryndamere" , "Urgot" ,
"Vladimir" , "Volibear" , "Wukong" , "Yone" , "Yorick"
],
"Jungle" : [
"Amumu" , "Bel'Veth" , "Briar" , "Diana" , "Ekko" , "Elise" ,
"Evelynn" , "Fiddlesticks" , "Graves" , "Hecarim" , "Ivern" ,
"Jarvan IV" , "Kayn" , "Kha'Zix" , "Kindred" , "Lee Sin" , "Lillia" ,
"Maokai" , "Master Yi" , "Nidalee" , "Nocturne" , "Nunu" , "Olaf" ,
"Rek'Sai" , "Rengar" , "Sejuani" , "Shaco" , "Skarner" , "Taliyah" ,
"Udyr" , "Vi" , "Viego" , "Warwick" , "Xin Zhao" , "Zac"
],
"Mid" : [
"Ahri" , "Akali" , "Akshan" , "Annie" , "Aurelion Sol" , "Azir" ,
"Cassiopeia" , "Corki" , "Ekko" , "Fizz" , "Galio" , "Heimerdinger" ,
"Hwei" , "Irelia" , "Katarina" , "LeBlanc" , "Lissandra" , "Lux" ,
"Malzahar" , "Mel" , "Naafiri" , "Neeko" , "Orianna" , "Qiyana" ,
"Ryze" , "Sylas" , "Syndra" , "Talon" , "Twisted Fate" , "Veigar" ,
"Vex" , "Viktor" , "Vladimir" , "Xerath" , "Yasuo" , "Yone" , "Zed" ,
"Zoe"
],
"Bot" : [
"Aphelios" , "Ashe" , "Caitlyn" , "Draven" , "Ezreal" , "Jhin" ,
"Jinx" , "Kai'Sa" , "Kalista" , "Kog'Maw" , "Lucian" ,
"Miss Fortune" , "Nilah" , "Samira" , "Sivir" , "Smolder" ,
"Tristana" , "Twitch" , "Varus" , "Vayne" , "Xayah" , "Zeri"
],
"Support" : [
"Alistar" , "Bard" , "Blitzcrank" , "Brand" , "Braum" , "Janna" ,
"Karma" , "Leona" , "Lulu" , "Lux" , "Milio" , "Morgana" , "Nami" ,
"Nautilus" , "Pyke" , "Rakan" , "Rell" , "Renata Glasc" , "Senna" ,
"Seraphine" , "Sona" , "Soraka" , "Swain" , "Taric" , "Thresh" ,
"Yuumi" , "Zilean" , "Zyra"
]
2026-02-10 20:11:22 -05:00
}
2026-01-13 16:03:54 -05:00
2026-02-10 20:11:22 -05:00
lane = random . choice ( list ( champions . keys ()))
selected_champion = random . choice ( champions [ lane ])
2026-01-13 16:03:54 -05:00
2026-02-10 20:11:22 -05:00
embed = discord . Embed (
title = "League Champion Picker" ,
color = discord . Color . from_rgb ( 152 , 0 , 0 )
)
embed . add_field ( name = "Selected Champion" , value = selected_champion , inline = False )
embed . add_field ( name = "Lane" , value = lane , inline = False )
embed . set_footer ( text = f "Selected for { interaction . user . display_name } " )
2026-01-13 16:03:54 -05:00
2026-02-10 20:11:22 -05:00
await interaction . response . send_message ( embed = embed )
2026-01-13 16:03:54 -05:00
@client.tree.command ( name = "trivia" , description = "Play a trivia game" )
2026-02-10 20:11:22 -05:00
@handle_command_errors
2026-01-13 16:03:54 -05:00
async def trivia ( interaction : discord . Interaction ):
2026-02-10 20:41:16 -05:00
questions = [
{ "q" : "What year was the original Super Mario Bros. released?" , "options" : [ "1983" , "1985" , "1987" , "1990" ], "answer" : 1 },
{ "q" : "Which game features the quote 'The cake is a lie'?" , "options" : [ "Half-Life 2" , "Portal" , "BioShock" , "Minecraft" ], "answer" : 1 },
{ "q" : "What is the max level in League of Legends?" , "options" : [ "16" , "18" , "20" , "25" ], "answer" : 1 },
{ "q" : "Which Valorant agent has the codename 'Deadeye'?" , "options" : [ "Jett" , "Sova" , "Chamber" , "Cypher" ], "answer" : 2 },
{ "q" : "How many Ender Dragon eggs can exist in a vanilla Minecraft world?" , "options" : [ "1" , "2" , "Unlimited" , "0" ], "answer" : 0 },
{ "q" : "What was the first battle royale game to hit mainstream popularity?" , "options" : [ "Fortnite" , "PUBG" , "H1Z1" , "Apex Legends" ], "answer" : 2 },
{ "q" : "In Minecraft, what is the rarest ore?" , "options" : [ "Diamond" , "Emerald" , "Ancient Debris" , "Lapis Lazuli" ], "answer" : 1 },
{ "q" : "What is the name of the main character in The Legend of Zelda?" , "options" : [ "Zelda" , "Link" , "Ganondorf" , "Epona" ], "answer" : 1 },
{ "q" : "Which game has the most registered players of all time?" , "options" : [ "Fortnite" , "Minecraft" , "League of Legends" , "Roblox" ], "answer" : 1 },
{ "q" : "What type of animal is Sonic?" , "options" : [ "Fox" , "Hedgehog" , "Rabbit" , "Echidna" ], "answer" : 1 },
{ "q" : "In Among Us, what is the maximum number of impostors?" , "options" : [ "1" , "2" , "3" , "4" ], "answer" : 2 },
{ "q" : "What does GG stand for in gaming?" , "options" : [ "Get Good" , "Good Game" , "Go Go" , "Great Going" ], "answer" : 1 },
{ "q" : "Which company developed Valorant?" , "options" : [ "Blizzard" , "Valve" , "Riot Games" , "Epic Games" ], "answer" : 2 },
{ "q" : "What is the highest rank in Valorant?" , "options" : [ "Immortal" , "Diamond" , "Radiant" , "Challenger" ], "answer" : 2 },
{ "q" : "In League of Legends, what is Baron Nashor an anagram of?" , "options" : [ "Baron Roshan" , "Roshan" , "Nashor Baron" , "Nash Robot" ], "answer" : 1 },
{ "q" : "What does HTTP stand for?" , "options" : [ "HyperText Transfer Protocol" , "High Tech Transfer Program" , "HyperText Transmission Process" , "Home Tool Transfer Protocol" ], "answer" : 0 },
{ "q" : "What year was Discord founded?" , "options" : [ "2013" , "2015" , "2017" , "2019" ], "answer" : 1 },
{ "q" : "What programming language has a logo that is a snake?" , "options" : [ "Java" , "Ruby" , "Python" , "Go" ], "answer" : 2 },
{ "q" : "How many bits are in a byte?" , "options" : [ "4" , "8" , "16" , "32" ], "answer" : 1 },
{ "q" : "What does 'RGB' stand for?" , "options" : [ "Really Good Build" , "Red Green Blue" , "Red Gold Black" , "Rapid Gaming Boost" ], "answer" : 1 },
{ "q" : "What is the most subscribed YouTube channel?" , "options" : [ "PewDiePie" , "MrBeast" , "T-Series" , "Cocomelon" ], "answer" : 1 },
{ "q" : "What does 'AFK' stand for?" , "options" : [ "A Free Kill" , "Away From Keyboard" , "Always Fun Killing" , "Another Fake Knockdown" ], "answer" : 1 },
{ "q" : "What animal is the Linux mascot?" , "options" : [ "Fox" , "Penguin" , "Cat" , "Dog" ], "answer" : 1 },
{ "q" : "What does 'NPC' stand for?" , "options" : [ "Non-Player Character" , "New Player Content" , "Normal Playing Conditions" , "Never Played Competitively" ], "answer" : 0 },
{ "q" : "In what year was the first iPhone released?" , "options" : [ "2005" , "2006" , "2007" , "2008" ], "answer" : 2 },
]
question = random . choice ( questions )
labels = [ "A" , "B" , "C" , "D" ]
2026-02-10 20:11:22 -05:00
class TriviaView ( discord . ui . View ):
2026-02-10 20:41:16 -05:00
def __init__ ( self ):
super () . __init__ ( timeout = 30 )
self . answered_users = set ()
async def handle_answer ( self , interaction : discord . Interaction , selected : int ):
if interaction . user . id in self . answered_users :
await interaction . response . send_message ( "You already answered!" , ephemeral = True )
return
self . answered_users . add ( interaction . user . id )
if selected == question [ "answer" ]:
await interaction . response . send_message (
f "Correct! The answer was ** { labels [ question [ 'answer' ]] } . { question [ 'options' ][ question [ 'answer' ]] } **" ,
ephemeral = True
)
else :
await interaction . response . send_message (
f "Wrong! The correct answer was ** { labels [ question [ 'answer' ]] } . { question [ 'options' ][ question [ 'answer' ]] } **" ,
ephemeral = True
)
2026-02-10 20:11:22 -05:00
@discord.ui.button ( label = "A" , style = discord . ButtonStyle . primary )
async def answer_a ( self , interaction : discord . Interaction , button : discord . ui . Button ):
2026-02-10 20:41:16 -05:00
await self . handle_answer ( interaction , 0 )
@discord.ui.button ( label = "B" , style = discord . ButtonStyle . primary )
async def answer_b ( self , interaction : discord . Interaction , button : discord . ui . Button ):
await self . handle_answer ( interaction , 1 )
@discord.ui.button ( label = "C" , style = discord . ButtonStyle . primary )
async def answer_c ( self , interaction : discord . Interaction , button : discord . ui . Button ):
await self . handle_answer ( interaction , 2 )
@discord.ui.button ( label = "D" , style = discord . ButtonStyle . primary )
async def answer_d ( self , interaction : discord . Interaction , button : discord . ui . Button ):
await self . handle_answer ( interaction , 3 )
2026-02-10 20:11:22 -05:00
2026-02-10 20:41:16 -05:00
embed = discord . Embed ( title = "Trivia Time!" , color = discord . Color . from_rgb ( 152 , 0 , 0 ))
embed . description = question [ "q" ]
for i , option in enumerate ( question [ "options" ]):
embed . add_field ( name = f " { labels [ i ] } " , value = option , inline = True )
embed . set_footer ( text = "You have 30 seconds to answer!" )
await interaction . response . send_message ( embed = embed , view = TriviaView ())
2026-01-13 16:03:54 -05:00
@client.tree.command ( name = "userinfo" , description = "Get information about a user" )
@app_commands.describe ( member = "The user to get information about" )
2026-02-10 20:11:22 -05:00
@handle_command_errors
2026-01-13 16:03:54 -05:00
async def userinfo ( interaction : discord . Interaction , member : discord . Member ):
2026-02-10 20:11:22 -05:00
embed = discord . Embed ( title = "User Information" , color = discord . Color . from_rgb ( 152 , 0 , 0 ))
embed . add_field ( name = "Joined Server" , value = member . joined_at . strftime ( "%Y-%m- %d " ))
embed . add_field ( name = "Account Created" , value = member . created_at . strftime ( "%Y-%m- %d " ))
embed . add_field ( name = "Roles" , value = ", " . join ([ role . name for role in member . roles [ 1 :]]))
await interaction . response . send_message ( embed = embed )
2026-02-10 20:41:16 -05:00
@client.tree.command ( name = "health" , description = "Check bot system status" )
2026-02-10 20:11:22 -05:00
@has_role_check ( ADMIN_ROLE_ID )
@handle_command_errors
2026-02-10 20:41:16 -05:00
async def health ( interaction : discord . Interaction ):
2026-02-10 20:11:22 -05:00
stats = metrics . get_stats ()
uptime_hours = stats [ "uptime_seconds" ] / 3600
embed = discord . Embed ( title = "Bot Status" , color = discord . Color . green ())
embed . add_field ( name = "Latency" , value = f " { round ( client . latency * 1000 ) } ms" )
embed . add_field ( name = "Guilds" , value = str ( len ( client . guilds )))
embed . add_field ( name = "Users" , value = str ( len ( client . users )))
embed . add_field ( name = "Uptime" , value = f " { uptime_hours : .1f } h" )
embed . add_field ( name = "Commands Run" , value = str ( stats [ "commands_executed" ]))
embed . add_field ( name = "Errors" , value = str ( stats [ "error_count" ]))
2026-01-13 16:03:54 -05:00
2026-02-10 20:11:22 -05:00
if stats [ "top_commands" ]:
top_cmds = " \n " . join ([ f "` { name } `: { count } " for name , count in stats [ "top_commands" ]])
embed . add_field ( name = "Top Commands" , value = top_cmds , inline = False )
2026-01-13 16:03:54 -05:00
2026-02-10 20:11:22 -05:00
checks = {
"RCON" : bool ( MINECRAFT_RCON_PASSWORD ),
"Ollama" : bool ( OLLAMA_URL ),
"Pelican" : bool ( PELICAN_API_KEY )
}
status_text = " \n " . join ([ f " { 'OK' if available else 'N/A' } { service } " for service , available in checks . items ()])
embed . add_field ( name = "Services" , value = status_text , inline = False )
await interaction . response . send_message ( embed = embed , ephemeral = True )
2026-01-13 16:03:54 -05:00
@client.tree.error
async def on_app_command_error ( interaction : discord . Interaction , error : app_commands . AppCommandError ):
try :
if isinstance ( error , app_commands . CheckFailure ):
await interaction . response . send_message (
"You don't have permission to use this command!" ,
ephemeral = True
)
logger . warning ( f "Permission denied for { interaction . user } on command { interaction . command . name } " )
elif isinstance ( error , app_commands . CommandOnCooldown ):
await interaction . response . send_message (
2026-02-10 20:11:22 -05:00
f "This command is on cooldown. Try again in { error . retry_after : .0f } seconds." ,
2026-01-13 16:03:54 -05:00
ephemeral = True
)
else :
await interaction . response . send_message (
"An error occurred while executing this command." ,
ephemeral = True
)
logger . error ( f "App command error: { str ( error ) } " , exc_info = True )
except Exception as e :
logger . error ( f "Error in on_app_command_error: { e } " , exc_info = True )
if __name__ == "__main__" :
2026-02-10 20:11:22 -05:00
errors , warnings = ConfigValidator . validate ()
if errors :
for error in errors :
logger . error ( error )
2026-01-13 16:03:54 -05:00
exit ( 1 )
2026-02-10 20:11:22 -05:00
for warning in warnings :
logger . warning ( warning )
token = os . getenv ( 'DISCORD_TOKEN' )
2026-01-13 16:03:54 -05:00
logger . info ( "Starting bot..." )
client . run ( token )