Compare commits
1 Commits
ea91a40053
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 715d778080 |
145
bot.py
145
bot.py
@@ -7,27 +7,23 @@ import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
from collections import Counter
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import time as datetime_time
|
||||
from pathlib import Path
|
||||
from functools import wraps, partial
|
||||
from dotenv import load_dotenv
|
||||
from discord import app_commands
|
||||
from discord.ext import commands, tasks
|
||||
from discord.utils import get
|
||||
from itertools import cycle
|
||||
from mcrcon import MCRcon
|
||||
import aiohttp
|
||||
|
||||
# Create logs directory if it doesn't exist
|
||||
Path("logs").mkdir(exist_ok=True)
|
||||
|
||||
# --- Issue #21: Improve Logging Configuration ---
|
||||
|
||||
def setup_logging():
|
||||
"""Configure logging with rotation"""
|
||||
logger = logging.getLogger('discord_bot')
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
# Rotating file handler (10MB max, keep 5 backups)
|
||||
file_handler = RotatingFileHandler(
|
||||
'logs/discord.log',
|
||||
maxBytes=10*1024*1024,
|
||||
@@ -46,14 +42,14 @@ def setup_logging():
|
||||
logger.addHandler(stream_handler)
|
||||
return logger
|
||||
|
||||
|
||||
logger = setup_logging()
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# Configuration - Environment variables with defaults
|
||||
# Configuration
|
||||
GUILD_ID = int(os.getenv('GUILD_ID', '605864889927467181'))
|
||||
AUDIT_CHANNEL_ID = int(os.getenv('AUDIT_CHANNEL_ID', '1340861451392520233'))
|
||||
ANNOUNCEMENT_CHANNEL_ID = int(os.getenv('ANNOUNCEMENT_CHANNEL_ID', '605864889940050088'))
|
||||
ADMIN_ROLE_ID = int(os.getenv('ADMIN_ROLE_ID', '605867042104541194'))
|
||||
MINECRAFT_ROLE_ID = int(os.getenv('MINECRAFT_ROLE_ID', '821163520942145556'))
|
||||
HYTALE_ROLE_ID = int(os.getenv('HYTALE_ROLE_ID', '1460750779848589548'))
|
||||
@@ -62,15 +58,11 @@ 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', '')
|
||||
PELICAN_URL = os.getenv('PELICAN_URL', 'http://10.10.10.67')
|
||||
PELICAN_API_KEY = os.getenv('PELICAN_API_KEY', '')
|
||||
HYTALE_SERVER_UUID = os.getenv('HYTALE_SERVER_UUID', '7a656836-c3f3-491e-ac55-66affe435e72')
|
||||
STATUS_UPDATE_INTERVAL = int(os.getenv('STATUS_UPDATE_INTERVAL', '300'))
|
||||
|
||||
# --- Issue #7: Hardcoded URLs - Use env vars for internal services ---
|
||||
OLLAMA_URL = os.getenv('OLLAMA_HOST', 'http://10.10.10.157:11434')
|
||||
|
||||
# --- Issue #11: Magic Numbers - Extract constants ---
|
||||
# Constants
|
||||
MIN_USERNAME_LENGTH = 3
|
||||
MAX_USERNAME_LENGTH = 16
|
||||
MAX_INPUT_LENGTH = 500
|
||||
@@ -82,7 +74,6 @@ USERNAME_CACHE_TTL_MINUTES = 60
|
||||
AUDIT_BATCH_SIZE = 5
|
||||
AUDIT_FLUSH_INTERVAL = 30
|
||||
|
||||
# Emoji to role mapping for reaction roles
|
||||
EMOJI_ROLE_MAP = {
|
||||
"Overwatch": "(Overwatch)",
|
||||
"Minecraft": "(Minecraft)",
|
||||
@@ -108,7 +99,6 @@ EMOJI_ROLE_MAP = {
|
||||
}
|
||||
|
||||
|
||||
# --- Issue #17: Configuration Validator ---
|
||||
class ConfigValidator:
|
||||
REQUIRED = ['DISCORD_TOKEN']
|
||||
OPTIONAL = {
|
||||
@@ -119,7 +109,6 @@ class ConfigValidator:
|
||||
|
||||
@classmethod
|
||||
def validate(cls):
|
||||
"""Validate configuration on startup"""
|
||||
errors = []
|
||||
warnings = []
|
||||
|
||||
@@ -134,31 +123,6 @@ class ConfigValidator:
|
||||
return errors, warnings
|
||||
|
||||
|
||||
# --- Issue #2: Memory Leak in Cooldown System ---
|
||||
class CooldownManager:
|
||||
def __init__(self, minutes: int):
|
||||
self.cooldowns = {}
|
||||
self.duration = timedelta(minutes=minutes)
|
||||
|
||||
def check(self, user_id: int) -> tuple:
|
||||
"""Returns (can_use, remaining_minutes)"""
|
||||
current_time = datetime.now()
|
||||
if user_id in self.cooldowns:
|
||||
time_diff = current_time - self.cooldowns[user_id]
|
||||
if time_diff < self.duration:
|
||||
remaining = self.duration.total_seconds() - time_diff.total_seconds()
|
||||
return False, remaining / 60
|
||||
return True, 0
|
||||
|
||||
def update(self, user_id: int):
|
||||
self.cooldowns[user_id] = datetime.now()
|
||||
# Cleanup old entries (>24 hours)
|
||||
cutoff = datetime.now() - timedelta(hours=24)
|
||||
self.cooldowns = {k: v for k, v in self.cooldowns.items() if v > cutoff}
|
||||
|
||||
|
||||
|
||||
# --- Issue #12: Cache Username Validations ---
|
||||
class UsernameCache:
|
||||
def __init__(self, ttl_minutes: int = 60):
|
||||
self.cache = {}
|
||||
@@ -184,13 +148,11 @@ class UsernameCache:
|
||||
username_cache = UsernameCache(ttl_minutes=USERNAME_CACHE_TTL_MINUTES)
|
||||
|
||||
|
||||
# --- Issue #13: Lazy Load Media Files ---
|
||||
class MediaManager:
|
||||
def __init__(self):
|
||||
self._cache = {}
|
||||
|
||||
def get_random(self, category: str) -> str | None:
|
||||
"""Get random media file from category"""
|
||||
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"]
|
||||
@@ -203,7 +165,6 @@ class MediaManager:
|
||||
media_manager = MediaManager()
|
||||
|
||||
|
||||
# --- Issue #14: Batch Audit Logs ---
|
||||
class AuditLogger:
|
||||
def __init__(self, channel_id: int, batch_size: int = AUDIT_BATCH_SIZE,
|
||||
flush_interval: int = AUDIT_FLUSH_INTERVAL):
|
||||
@@ -216,7 +177,7 @@ class AuditLogger:
|
||||
async def log(self, message: str, color: int = 0x980000):
|
||||
self.queue.append((message, color, datetime.now()))
|
||||
if len(self.queue) >= self.batch_size or \
|
||||
(datetime.now() - self.last_flush).seconds >= self.flush_interval:
|
||||
(datetime.now() - self.last_flush).total_seconds() >= self.flush_interval:
|
||||
await self.flush()
|
||||
|
||||
async def flush(self):
|
||||
@@ -227,23 +188,27 @@ class AuditLogger:
|
||||
if not channel:
|
||||
return
|
||||
|
||||
embed = discord.Embed(color=0x980000, timestamp=datetime.now())
|
||||
for msg, _, timestamp in self.queue[:10]:
|
||||
embed.add_field(name=timestamp.strftime("%H:%M:%S"), value=msg[:1024], inline=False)
|
||||
# Process in batches of 10 (Discord embed field limit)
|
||||
while self.queue:
|
||||
batch = self.queue[:10]
|
||||
self.queue = self.queue[10:]
|
||||
|
||||
try:
|
||||
await channel.send(embed=embed)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send audit log: {e}", exc_info=True)
|
||||
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
|
||||
|
||||
self.queue.clear()
|
||||
self.last_flush = datetime.now()
|
||||
|
||||
|
||||
audit_logger = AuditLogger(AUDIT_CHANNEL_ID)
|
||||
|
||||
|
||||
# --- Issue #20: Metrics Collection ---
|
||||
class MetricsCollector:
|
||||
def __init__(self):
|
||||
self.command_counts = Counter()
|
||||
@@ -269,15 +234,12 @@ class MetricsCollector:
|
||||
metrics = MetricsCollector()
|
||||
|
||||
|
||||
# --- Issue #8: Input Sanitization ---
|
||||
def sanitize_input(text: str, max_length: int = MAX_INPUT_LENGTH) -> str:
|
||||
"""Sanitize user input"""
|
||||
text = text.strip()[:max_length]
|
||||
text = ''.join(char for char in text if char.isprintable())
|
||||
return text
|
||||
|
||||
|
||||
# --- Issue #9: Inconsistent Error Handling ---
|
||||
def handle_command_errors(func):
|
||||
@wraps(func)
|
||||
async def wrapper(interaction: discord.Interaction, *args, **kwargs):
|
||||
@@ -295,15 +257,12 @@ def handle_command_errors(func):
|
||||
return wrapper
|
||||
|
||||
|
||||
# --- Issue #5: Race Condition in RCON ---
|
||||
def execute_rcon(host: str, password: str, command: str):
|
||||
"""Execute RCON command synchronously (for use with executor)"""
|
||||
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):
|
||||
"""Execute RCON command with timeout in executor"""
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
func = partial(execute_rcon, host, password, command)
|
||||
@@ -314,14 +273,12 @@ async def rcon_command(host: str, password: str, command: str, timeout: float =
|
||||
raise Exception(f"RCON error: {str(e)}")
|
||||
|
||||
|
||||
# --- Issue #10: Duplicate Code for Media Files ---
|
||||
async def send_interaction_media(
|
||||
interaction: discord.Interaction,
|
||||
member: discord.Member,
|
||||
action: str,
|
||||
media_prefix: str
|
||||
):
|
||||
"""Generic function for interaction commands"""
|
||||
messages = {
|
||||
'kill': f"You killed {member}",
|
||||
'punch': f"You punched {member}",
|
||||
@@ -339,13 +296,11 @@ async def send_interaction_media(
|
||||
await interaction.response.send_message(message)
|
||||
|
||||
|
||||
# --- Issue #15: Track whitelisted usernames for autocomplete ---
|
||||
whitelisted_usernames = set()
|
||||
WHITELIST_FILE = Path("data/whitelisted_usernames.json")
|
||||
|
||||
|
||||
def load_whitelisted_usernames():
|
||||
"""Load previously whitelisted usernames from file"""
|
||||
global whitelisted_usernames
|
||||
try:
|
||||
if WHITELIST_FILE.exists():
|
||||
@@ -356,7 +311,6 @@ def load_whitelisted_usernames():
|
||||
|
||||
|
||||
def save_whitelisted_username(username: str):
|
||||
"""Save a whitelisted username to file"""
|
||||
whitelisted_usernames.add(username)
|
||||
try:
|
||||
WHITELIST_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
@@ -370,7 +324,6 @@ async def minecraft_username_autocomplete(
|
||||
interaction: discord.Interaction,
|
||||
current: str
|
||||
) -> list:
|
||||
"""Suggest previously whitelisted usernames"""
|
||||
return [
|
||||
app_commands.Choice(name=username, value=username)
|
||||
for username in sorted(whitelisted_usernames)
|
||||
@@ -378,7 +331,6 @@ async def minecraft_username_autocomplete(
|
||||
][:25]
|
||||
|
||||
|
||||
# --- Issue #4: Unclosed aiohttp Sessions ---
|
||||
class CustomBot(commands.Bot):
|
||||
def __init__(self):
|
||||
intents = discord.Intents.all()
|
||||
@@ -398,7 +350,6 @@ class CustomBot(commands.Bot):
|
||||
async def setup_hook(self):
|
||||
self.http_session = aiohttp.ClientSession()
|
||||
|
||||
# --- Issue #3: Validate configs on startup ---
|
||||
if not MINECRAFT_RCON_PASSWORD:
|
||||
logger.warning("MINECRAFT_RCON_PASSWORD not set - /minecraft command will fail")
|
||||
if not PELICAN_API_KEY:
|
||||
@@ -437,7 +388,6 @@ class CustomBot(commands.Bot):
|
||||
|
||||
@tasks.loop(seconds=AUDIT_FLUSH_INTERVAL)
|
||||
async def flush_audit_logs(self):
|
||||
"""Periodically flush batched audit logs"""
|
||||
await audit_logger.flush()
|
||||
|
||||
@flush_audit_logs.before_loop
|
||||
@@ -448,33 +398,24 @@ class CustomBot(commands.Bot):
|
||||
client = CustomBot()
|
||||
|
||||
|
||||
def is_owner():
|
||||
async def predicate(ctx):
|
||||
return ctx.author.id == OWNER_ID
|
||||
return commands.check(predicate)
|
||||
|
||||
|
||||
def get_role_from_emoji(emoji_name: str) -> str:
|
||||
"""Helper function to map emoji names to role names."""
|
||||
return EMOJI_ROLE_MAP.get(emoji_name, emoji_name)
|
||||
|
||||
|
||||
def has_role_check(role_id: int):
|
||||
"""Check if user has a specific role (for slash commands)."""
|
||||
async def predicate(interaction: discord.Interaction) -> bool:
|
||||
return any(role.id == role_id for role in interaction.user.roles)
|
||||
return app_commands.check(predicate)
|
||||
|
||||
|
||||
# --- Issue #6: Only log commands, not all message content ---
|
||||
# ==================== EVENTS ====================
|
||||
|
||||
@client.event
|
||||
async def on_message(message):
|
||||
try:
|
||||
# Only log commands or bot-prefix messages, not all content
|
||||
if message.content.startswith(('.', '/')):
|
||||
logger.info(f"Command in {message.channel}: {message.author} - {message.content[:50]}")
|
||||
|
||||
# Special response for user 331862422996647938 when message starts with a period
|
||||
if message.author.id == 331862422996647938 and message.content.startswith('.'):
|
||||
await message.channel.send("Whatever conversation is happening right now, Jared is right")
|
||||
except Exception as e:
|
||||
@@ -546,7 +487,6 @@ async def on_scheduled_event_update(before, after):
|
||||
|
||||
@client.event
|
||||
async def on_member_update(before, after):
|
||||
"""Unified handler for all member update events."""
|
||||
try:
|
||||
if before.timed_out_until != after.timed_out_until:
|
||||
await audit_logger.log(f"Member Timeout\nUser: {after.mention}\nUntil: {after.timed_out_until}")
|
||||
@@ -654,7 +594,6 @@ async def on_guild_channel_delete(channel):
|
||||
|
||||
@client.event
|
||||
async def on_raw_reaction_add(payload):
|
||||
"""Handle reaction role additions."""
|
||||
try:
|
||||
if payload.message_id != REACTION_MESSAGE_ID:
|
||||
return
|
||||
@@ -688,7 +627,6 @@ async def on_raw_reaction_add(payload):
|
||||
|
||||
@client.event
|
||||
async def on_raw_reaction_remove(payload):
|
||||
"""Handle reaction role removals."""
|
||||
try:
|
||||
if payload.message_id != REACTION_MESSAGE_ID:
|
||||
return
|
||||
@@ -729,11 +667,9 @@ async def help_command(interaction: discord.Interaction):
|
||||
embed.set_image(url="https://lotusguild.org/favicon.ico")
|
||||
embed.set_thumbnail(url="https://lotusguild.org/favicon.ico")
|
||||
|
||||
# Basic Commands
|
||||
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)
|
||||
|
||||
# Fun Commands
|
||||
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)
|
||||
@@ -743,20 +679,17 @@ async def help_command(interaction: discord.Interaction):
|
||||
embed.add_field(name="/poll", value="Create a simple yes/no poll", inline=False)
|
||||
embed.add_field(name="/trivia", value="Play a trivia game with random questions", inline=False)
|
||||
|
||||
# Game Commands
|
||||
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)
|
||||
|
||||
# Interaction Commands
|
||||
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)
|
||||
|
||||
# Admin Commands (Only visible to admins)
|
||||
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)
|
||||
@@ -815,14 +748,12 @@ async def unlock(interaction: discord.Interaction, channel: discord.TextChannel
|
||||
await interaction.response.send_message(f"Unlocked {channel.mention}")
|
||||
|
||||
|
||||
# --- Issue #15: Autocomplete for minecraft usernames ---
|
||||
@client.tree.command(name="minecraft", description="Whitelist a player on the Minecraft server")
|
||||
@app_commands.describe(minecraft_username="The Minecraft username to whitelist")
|
||||
@app_commands.autocomplete(minecraft_username=minecraft_username_autocomplete)
|
||||
@has_role_check(MINECRAFT_ROLE_ID)
|
||||
@handle_command_errors
|
||||
async def minecraft(interaction: discord.Interaction, minecraft_username: str):
|
||||
# Validate username using cached checker (Issue #12)
|
||||
if not await username_cache.is_valid(minecraft_username, client.http_session):
|
||||
await interaction.response.send_message("Invalid MC Username", ephemeral=True)
|
||||
return
|
||||
@@ -837,7 +768,6 @@ async def minecraft(interaction: discord.Interaction, minecraft_username: str):
|
||||
|
||||
await interaction.response.defer()
|
||||
|
||||
# --- Issue #5: Async RCON with timeout ---
|
||||
try:
|
||||
response = await rcon_command(MINECRAFT_RCON_HOST, MINECRAFT_RCON_PASSWORD,
|
||||
f"whitelist add {minecraft_username}")
|
||||
@@ -849,7 +779,6 @@ async def minecraft(interaction: discord.Interaction, minecraft_username: str):
|
||||
)
|
||||
return
|
||||
|
||||
# Issue #15: Track whitelisted username
|
||||
save_whitelisted_username(minecraft_username)
|
||||
|
||||
minecraft_embed = discord.Embed(
|
||||
@@ -878,10 +807,8 @@ async def minecraft(interaction: discord.Interaction, minecraft_username: str):
|
||||
@has_role_check(HYTALE_ROLE_ID)
|
||||
@handle_command_errors
|
||||
async def hytale(interaction: discord.Interaction, hytale_username: str):
|
||||
"""Request whitelist on the Hytale server"""
|
||||
await interaction.response.defer()
|
||||
|
||||
# --- Issue #11: Use constants instead of magic numbers ---
|
||||
if not hytale_username.replace('_', '').replace('-', '').isalnum():
|
||||
await interaction.followup.send("Invalid username. Use only letters, numbers, _ and -", ephemeral=True)
|
||||
return
|
||||
@@ -907,14 +834,12 @@ async def hytale(interaction: discord.Interaction, hytale_username: str):
|
||||
logger.info(f"Hytale whitelist request: {hytale_username} by {interaction.user.name}")
|
||||
|
||||
|
||||
# --- Issue #19: Rate Limiting for AI ---
|
||||
@client.tree.command(name="ask", description="Ask a question to Lotus LLM")
|
||||
@app_commands.describe(question="Your question for the AI")
|
||||
@app_commands.checks.cooldown(1, COOLDOWN_MINUTES * 60, key=lambda i: i.user.id)
|
||||
@has_role_check(COOL_KIDS_ROLE_ID)
|
||||
@handle_command_errors
|
||||
async def ask(interaction: discord.Interaction, question: str):
|
||||
# --- Issue #8: Sanitize input ---
|
||||
question = sanitize_input(question)
|
||||
if not question:
|
||||
await interaction.response.send_message("Please provide a valid question.", ephemeral=True)
|
||||
@@ -922,7 +847,6 @@ async def ask(interaction: discord.Interaction, question: str):
|
||||
|
||||
await interaction.response.defer()
|
||||
|
||||
# Select model based on user ID
|
||||
model = "lotusllmben" if interaction.user.id == 460640040096104459 else "lotusllm"
|
||||
logger.info(f"Sending question to Ollama: {question}")
|
||||
|
||||
@@ -958,14 +882,11 @@ async def ask(interaction: discord.Interaction, question: str):
|
||||
@handle_command_errors
|
||||
async def eight_ball(interaction: discord.Interaction, question: str):
|
||||
possible_responses = [
|
||||
# Positive answers
|
||||
"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% easily",
|
||||
# Neutral answers
|
||||
"Reply hazy try again", "Ask again later", "Better not tell you now",
|
||||
"Cannot predict now", "Concentrate and ask again", "Idk bro",
|
||||
# Negative answers
|
||||
"Don't count on it", "My reply is no", "My sources say no",
|
||||
"Outlook not so good", "Very doubtful", "Hell no", "Prolly not"
|
||||
]
|
||||
@@ -985,18 +906,13 @@ async def eight_ball(interaction: discord.Interaction, question: str):
|
||||
@handle_command_errors
|
||||
async def fortune(interaction: discord.Interaction):
|
||||
possible_responses = [
|
||||
# Humorous fortunes
|
||||
"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",
|
||||
|
||||
# Classic fortunes with a twist
|
||||
"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",
|
||||
|
||||
# Gaming-themed fortunes
|
||||
"Your next competitive match will be legendary",
|
||||
"The cake is still a lie",
|
||||
"Press Alt+F4 for instant success",
|
||||
@@ -1018,8 +934,6 @@ async def fortune(interaction: discord.Interaction):
|
||||
"The imposter will not sus you",
|
||||
"Your Minecraft bed will remain unbroken",
|
||||
"You will get Play of the Game",
|
||||
|
||||
# Internet culture fortunes
|
||||
"Your next meme will go viral",
|
||||
"Someone is talking about you in their Discord server",
|
||||
"Your FBI agent thinks you're hilarious",
|
||||
@@ -1038,13 +952,9 @@ async def fortune(interaction: discord.Interaction):
|
||||
"You will not get ratio'd today",
|
||||
"Someone will actually use your custom emoji",
|
||||
"Your next selfie will be iconic",
|
||||
|
||||
# Original favorites
|
||||
"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",
|
||||
|
||||
# Tech & computer nerd fortunes
|
||||
"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",
|
||||
@@ -1101,7 +1011,6 @@ async def roll(interaction: discord.Interaction, dice: str = "1d6"):
|
||||
await interaction.response.send_message("Please use format: NdS (example: 2d6)", ephemeral=True)
|
||||
return
|
||||
|
||||
# --- Issue #11: Validate against constants ---
|
||||
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
|
||||
@@ -1162,11 +1071,10 @@ async def poll(interaction: discord.Interaction, question: str):
|
||||
embed.set_footer(text=f"Created by {interaction.user.display_name}")
|
||||
await interaction.response.send_message(embed=embed)
|
||||
message = await interaction.original_response()
|
||||
await message.add_reaction("👍")
|
||||
await message.add_reaction("👎")
|
||||
await message.add_reaction("\U0001f44d")
|
||||
await message.add_reaction("\U0001f44e")
|
||||
|
||||
|
||||
# --- Issue #10: DRY media commands ---
|
||||
@client.tree.command(name="kill", description="Kill another user")
|
||||
@app_commands.describe(member="The user to kill")
|
||||
@handle_command_errors
|
||||
@@ -1282,7 +1190,6 @@ async def champion(interaction: discord.Interaction):
|
||||
@handle_command_errors
|
||||
async def trivia(interaction: discord.Interaction):
|
||||
questions = [
|
||||
# Gaming
|
||||
{"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},
|
||||
@@ -1298,7 +1205,6 @@ async def trivia(interaction: discord.Interaction):
|
||||
{"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},
|
||||
# General/Internet culture
|
||||
{"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},
|
||||
@@ -1370,18 +1276,15 @@ async def userinfo(interaction: discord.Interaction, member: discord.Member):
|
||||
await interaction.response.send_message(embed=embed)
|
||||
|
||||
|
||||
# --- Issue #16: Health Check Command ---
|
||||
@client.tree.command(name="health", description="Check bot system status")
|
||||
@has_role_check(ADMIN_ROLE_ID)
|
||||
@handle_command_errors
|
||||
async def health(interaction: discord.Interaction):
|
||||
"""Show bot health metrics"""
|
||||
stats = metrics.get_stats()
|
||||
uptime_hours = stats["uptime_seconds"] / 3600
|
||||
|
||||
embed = discord.Embed(title="Bot Status", color=discord.Color.green())
|
||||
|
||||
# System info
|
||||
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)))
|
||||
@@ -1389,12 +1292,10 @@ async def health(interaction: discord.Interaction):
|
||||
embed.add_field(name="Commands Run", value=str(stats["commands_executed"]))
|
||||
embed.add_field(name="Errors", value=str(stats["error_count"]))
|
||||
|
||||
# Top commands
|
||||
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)
|
||||
|
||||
# Service checks
|
||||
checks = {
|
||||
"RCON": bool(MINECRAFT_RCON_PASSWORD),
|
||||
"Ollama": bool(OLLAMA_URL),
|
||||
@@ -1409,7 +1310,6 @@ async def health(interaction: discord.Interaction):
|
||||
|
||||
@client.tree.error
|
||||
async def on_app_command_error(interaction: discord.Interaction, error: app_commands.AppCommandError):
|
||||
"""Handle slash command errors."""
|
||||
try:
|
||||
if isinstance(error, app_commands.CheckFailure):
|
||||
await interaction.response.send_message(
|
||||
@@ -1432,7 +1332,6 @@ async def on_app_command_error(interaction: discord.Interaction, error: app_comm
|
||||
logger.error(f"Error in on_app_command_error: {e}", exc_info=True)
|
||||
|
||||
|
||||
# --- Issue #17: Validate config on startup ---
|
||||
if __name__ == "__main__":
|
||||
errors, warnings = ConfigValidator.validate()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user