__version__ = "dev-0.9.7" __all__ = ["Discordbot-chatai (Discord)"] __author__ = "SimolZimol" import discord import uuid import os, sys from openai import OpenAI from discord.ext import commands, tasks from discord.ui import Button, View import requests import asyncio import base64 import mysql.connector import mysql.connector.pooling import json import logging import time import random import hashlib from datetime import datetime, timedelta import concurrent.futures from gtts import gTTS import shutil from bs4 import BeautifulSoup from dotenv import load_dotenv import random import time import hashlib from urllib.parse import urlparse load_dotenv() DB_HOST = os.getenv("DB_HOST") DB_PORT = os.getenv("DB_PORT") DB_USER = os.getenv("DB_USER") DB_PASSWORD = os.getenv("DB_PASSWORD") DB_DATABASE = os.getenv("DB_DATABASE") OPENAI_BASE_URL = os.getenv("OPENAI_BASE_URL") OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") OWNER_ID = int(os.getenv("OWNER_ID")) GIVEAWAY_WEBSITE_URL = os.getenv("GIVEAWAY_WEBSITE_URL") features = { "askmultus": bool(int(os.getenv("ASKMULTUS_ENABLED", 0))), "vision": bool(int(os.getenv("VISION_ENABLED", 0))), "summarize": bool(int(os.getenv("SUMMARIZE_ENABLED", 0))) } giveaways = {} LOGS_DIR = "logs" if not os.path.exists(LOGS_DIR): os.makedirs(LOGS_DIR) logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S') logger = logging.getLogger("discord_bot") logger.setLevel(logging.INFO) log_file = os.path.join(LOGS_DIR, f"{datetime.now().strftime('%Y-%m-%d')}.log") if os.path.exists(log_file): try: timestamp = datetime.now().strftime('%Y-%m-%d_%H-%M-%S') renamed_log_file = os.path.join(LOGS_DIR, f"{datetime.now().strftime('%Y-%m-%d')}_{timestamp}.log") os.rename(log_file, renamed_log_file) except PermissionError: print(f"Unable to rename log file {log_file}. It may be in use by another process.") file_handler = logging.FileHandler(log_file) file_handler.setLevel(logging.INFO) file_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S') file_handler.setFormatter(file_formatter) logger.addHandler(file_handler) #to do: # permissions system, Filter, mysql for user data, fix vision, embeds, info cmd (Server Info, user info), logs, points redo, ticket system, levels, mc ranks integration, add image gen, reaction system, dm system, better ready, resource management, bot action (aka playing) # mysql = userid / permission / points / ban / askmultus-int / Filter-int / rank / chat-history / guild_id # 10 filter = acc under review = nicht ok = ban add timestamp = 2 bans = unendlicher ban #perms || 10 = Owner || 8 = Admin || 5 = Mod openai_instance = OpenAI(base_url=OPENAI_BASE_URL, api_key=OPENAI_API_KEY) TOKEN = os.getenv("DISCORD_TOKEN") intents = discord.Intents.default() intents.message_content = True intents.members = True intents.reactions = True python = sys.executable client = commands.Bot(command_prefix='-', intents=intents, owner_id = OWNER_ID) askmultus_queue = asyncio.Queue() loop = asyncio.get_event_loop() # Verbindung zur MySQL-Datenbank herstellen (OLD - now using connection pool) # db_connection = mysql.connector.connect( # host=DB_HOST, # port=DB_PORT, # user=DB_USER, # password=DB_PASSWORD, # database=DB_DATABASE # ) # Cursor erstellen (OLD - now using connection pool) # db_cursor = db_connection.cursor() def close_database_connection(connection): connection.close() def insert_user_data(user_id, guild_id, permission, points, ban, askmultus, filter_value, chat_history, xp=0, level=1, nickname="", profile_picture="", join_date=None, leave_date=None, ai_ban=0, mutes=0, warns=0): """Fügt neue Benutzerdaten in die Datenbank ein mit Connection Pool""" connection = None cursor = None try: connection = connect_to_database() cursor = connection.cursor() insert_query = """ INSERT INTO user_data (user_id, guild_id, permission, points, ban, askmultus, filter_value, rank, chat_history, xp, level, nickname, profile_picture, join_date, leave_date, ai_ban, mutes, warns) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) """ serialized_chat_history = json.dumps(chat_history) data = (user_id, guild_id, permission, points, ban, askmultus, filter_value, 0, serialized_chat_history, xp, level, nickname, profile_picture, join_date, leave_date, ai_ban, mutes, warns) cursor.execute(insert_query, data) connection.commit() logger.info("User data inserted successfully.") except Exception as e: logger.error(f"Error inserting user data: {e}") if connection: connection.rollback() raise e finally: if cursor: cursor.close() if connection: close_database_connection(connection) def update_user_data(user_id, guild_id, field, value): """Aktualisiert Benutzerdaten in der Datenbank mit Connection Pool""" connection = None cursor = None try: connection = connect_to_database() cursor = connection.cursor() update_query = f"UPDATE user_data SET {field} = %s WHERE user_id = %s AND guild_id = %s" # Überprüfen, ob das Feld 'chat_history' aktualisiert wird if field == 'chat_history': serialized_chat_history = json.dumps(value) cursor.execute(update_query, (serialized_chat_history, user_id, guild_id)) else: cursor.execute(update_query, (value, user_id, guild_id)) connection.commit() logger.debug(f"Successfully updated {field} for user {user_id} in guild {guild_id}") except mysql.connector.Error as err: logger.error(f"Database error: {err}") if connection: connection.rollback() raise err finally: if cursor: cursor.close() if connection: close_database_connection(connection) def connect_to_database(): connection = mysql.connector.connect( host=DB_HOST, port=DB_PORT, user=DB_USER, password=DB_PASSWORD, database=DB_DATABASE ) connection.autocommit = True # Automatisches Commit für stabilere Abfragen return connection def retry_query(func, *args, retries=3, delay=5): for _ in range(retries): try: return func(*args) except mysql.connector.Error as err: print(f"Retrying due to error: {err}") time.sleep(delay) raise RuntimeError("Max retries exceeded") # Removed get_database_cursor() - now using connection pool directly pool = mysql.connector.pooling.MySQLConnectionPool( pool_name="mypool", pool_size=30, # Erhöht von 10 auf 30 pool_reset_session=True, autocommit=True, host=DB_HOST, port=DB_PORT, user=DB_USER, password=DB_PASSWORD, database=DB_DATABASE ) def connect_to_database(): """Holt eine Verbindung aus dem Pool""" try: connection = pool.get_connection() return connection except mysql.connector.PoolError as e: logger.error(f"Pool error: {e}") raise e def close_database_connection(connection): """Gibt eine Verbindung an den Pool zurück""" if connection and connection.is_connected(): connection.close() async def create_user_data_with_member(user_id, guild_id, member=None): """Erstellt neue User-Daten mit korrekten Informationen vom Member-Objekt""" nickname = member.display_name if member else "" # Profilbild herunterladen und lokal speichern if member and member.display_avatar: discord_avatar_url = str(member.display_avatar.url) profile_picture = await download_and_save_profile_image(user_id, discord_avatar_url) else: profile_picture = "/static/default_profile.png" join_date = member.joined_at.date() if member and member.joined_at else None user_data = { "user_id": user_id, "guild_id": guild_id, "permission": 0, "points": 0, "ban": 0, "askmultus": 0, "filter_value": 0, "rank": 0, "chat_history": [], "asknotes_history": [], "xp": 0, "level": 1, "nickname": nickname, "ai_ban": 0, "mutes": 0, "warns": 0 } insert_user_data( user_data["user_id"], user_data["guild_id"], user_data["permission"], user_data["points"], user_data["ban"], user_data["askmultus"], user_data["filter_value"], user_data["chat_history"], user_data["xp"], user_data["level"], nickname, profile_picture, join_date ) logger.info(f"Created new user data for {nickname} (ID: {user_id}) with join_date: {join_date} and profile_picture: {profile_picture}") return user_data def load_user_data_from_mysql(user_id, guild_id): connection = None cursor = None try: connection = connect_to_database() cursor = connection.cursor() select_query = "SELECT * FROM user_data WHERE user_id = %s AND guild_id = %s" cursor.execute(select_query, (user_id, guild_id)) result = cursor.fetchone() if result: user_data = { "user_id": result[0], "guild_id": result[1], "permission": result[2], "points": int(result[3]), "ban": result[4], "askmultus": result[5], "filter_value": result[6], "rank": result[7], "chat_history": json.loads(result[8]) if result[8] else [], "asknotes_history": json.loads(result[9]) if result[9] else [], "xp": int(result[10]) if result[10] is not None else 0, "level": int(result[11]) if result[11] is not None else 1, "nickname": result[12], "ai_ban": int(result[15]) if len(result) > 15 and result[15] is not None else 0, "mutes": int(result[16]) if len(result) > 16 and result[16] is not None else 0, "warns": 0 # Will be calculated from user_warnings table } else: user_data = { "user_id": user_id, "guild_id": guild_id, "permission": 0, "points": 0, "ban": 0, "askmultus": 0, "filter_value": 0, "rank": 0, "chat_history": [], "asknotes_history": [], "xp": 0, "level": 1, "nickname": "", "ai_ban": 0, "mutes": 0, "warns": 0 } # Count warnings from user_warnings table warning_count_query = "SELECT COUNT(*) FROM user_warnings WHERE user_id = %s AND guild_id = %s" cursor.execute(warning_count_query, (user_id, guild_id)) warning_count = cursor.fetchone()[0] user_data["warns"] = warning_count return user_data except Exception as e: logger.error(f"Error loading user data from MySQL: {e}") return None finally: if cursor: cursor.close() if connection: close_database_connection(connection) cached_user_data = {} pending_deletion = {} async def cache_user_data(user_id, guild_id, data): cached_user_data[(user_id, guild_id)] = data # Setze die Daten nach 30 Sekunden auf die Löschliste if (user_id, guild_id) not in pending_deletion: pending_deletion[(user_id, guild_id)] = asyncio.get_event_loop().call_later(30, lambda: remove_user_data_from_cache(user_id, guild_id)) def remove_user_data_from_cache(user_id, guild_id): # Entferne den Benutzer aus dem Cache und der Löschliste if (user_id, guild_id) in cached_user_data: del cached_user_data[(user_id, guild_id)] if (user_id, guild_id) in pending_deletion: pending_deletion[(user_id, guild_id)].cancel() del pending_deletion[(user_id, guild_id)] async def load_user_data(user_id, guild_id, member=None): if (user_id, guild_id) in cached_user_data: return cached_user_data[(user_id, guild_id)] # Daten aus der Datenbank laden oder neu anlegen user_data = load_user_data_from_mysql(user_id, guild_id) # Wenn keine User-Daten existieren, erstelle neue mit Member-Informationen if not user_data or user_data.get("user_id") is None: user_data = await create_user_data_with_member(user_id, guild_id, member) asyncio.ensure_future(cache_user_data(user_id, guild_id, user_data)) return user_data def load_user_data_sync(user_id, guild_id): """Synchrone Version von load_user_data für bestehende Commands""" if (user_id, guild_id) in cached_user_data: return cached_user_data[(user_id, guild_id)] # Daten aus der Datenbank laden oder neu anlegen user_data = load_user_data_from_mysql(user_id, guild_id) # Wenn keine User-Daten existieren, erstelle neue mit Default-Werten if not user_data or user_data.get("user_id") is None: user_data = { "user_id": user_id, "guild_id": guild_id, "permission": 0, "points": 0, "ban": 0, "askmultus": 0, "filter_value": 0, "rank": 0, "chat_history": [], "asknotes_history": [], "xp": 0, "level": 1, "nickname": "", "ai_ban": 0, "mutes": 0, "warns": 0 } insert_user_data( user_data["user_id"], user_data["guild_id"], user_data["permission"], user_data["points"], user_data["ban"], user_data["askmultus"], user_data["filter_value"], user_data["chat_history"], user_data["xp"], user_data["level"], user_data["nickname"], "/static/default_profile.png" ) asyncio.ensure_future(cache_user_data(user_id, guild_id, user_data)) return user_data def get_global_permission(user_id): connection = None cursor = None try: connection = connect_to_database() cursor = connection.cursor() select_query = "SELECT global_permission FROM bot_data WHERE user_id = %s" cursor.execute(select_query, (user_id,)) result = cursor.fetchone() return result[0] if result else None except Exception as e: logger.error(f"Error getting global permission: {e}") return None finally: if cursor: cursor.close() if connection: close_database_connection(connection) def save_global_permission(user_id, permission_level): connection = None cursor = None try: connection = connect_to_database() cursor = connection.cursor() insert_query = """ INSERT INTO bot_data (user_id, global_permission) VALUES (%s, %s) ON DUPLICATE KEY UPDATE global_permission = %s """ cursor.execute(insert_query, (user_id, permission_level, permission_level)) connection.commit() logger.info(f"Successfully saved global permission for user {user_id}: {permission_level}") except Exception as e: logger.error(f"Error saving global permission: {e}") if connection: connection.rollback() raise e finally: if cursor: cursor.close() if connection: close_database_connection(connection) #----------------------------------------------------------------------------------------------------------- # Active Processes System - Robust system for storing and managing active processes def create_active_process(process_type, guild_id, channel_id=None, user_id=None, target_id=None, start_time=None, end_time=None, status="active", data=None, metadata=None): """Creates a new active process entry in the database""" connection = None cursor = None try: connection = connect_to_database() cursor = connection.cursor() process_uuid = uuid.uuid4() insert_query = """ INSERT INTO active_processes (uuid, process_type, guild_id, channel_id, user_id, target_id, start_time, end_time, status, data, metadata, created_at) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) """ current_time = datetime.now() start_time = start_time or current_time # Serialize data and metadata as JSON data_json = json.dumps(data) if data else None metadata_json = json.dumps(metadata) if metadata else None cursor.execute(insert_query, ( str(process_uuid), process_type, guild_id, channel_id, user_id, target_id, start_time, end_time, status, data_json, metadata_json, current_time )) connection.commit() logger.info(f"Created active process: {process_type} with UUID: {process_uuid}") return process_uuid except Exception as e: logger.error(f"Error creating active process: {e}") if connection: connection.rollback() raise e finally: if cursor: cursor.close() if connection: close_database_connection(connection) def get_active_processes(process_type=None, guild_id=None, status="active"): """Retrieves active processes from the database with optional filters""" connection = None cursor = None try: connection = connect_to_database() cursor = connection.cursor() query = "SELECT * FROM active_processes WHERE status = %s" params = [status] if process_type: query += " AND process_type = %s" params.append(process_type) if guild_id: query += " AND guild_id = %s" params.append(guild_id) query += " ORDER BY created_at ASC" cursor.execute(query, params) results = cursor.fetchall() processes = [] for row in results: process = { "uuid": row[0], "process_type": row[1], "guild_id": row[2], "channel_id": row[3], "user_id": row[4], "target_id": row[5], "start_time": row[6], "end_time": row[7], "status": row[8], "data": json.loads(row[9]) if row[9] else None, "metadata": json.loads(row[10]) if row[10] else None, "created_at": row[11], "updated_at": row[12] } processes.append(process) return processes except Exception as e: logger.error(f"Error retrieving active processes: {e}") return [] finally: if cursor: cursor.close() if connection: close_database_connection(connection) def update_process_status(process_uuid, status, data=None, metadata=None): """Updates the status and optionally data/metadata of an active process""" connection = None cursor = None try: connection = connect_to_database() cursor = connection.cursor() # Prepare update fields update_fields = ["status = %s", "updated_at = %s"] params = [status, datetime.now()] if data is not None: update_fields.append("data = %s") params.append(json.dumps(data)) if metadata is not None: update_fields.append("metadata = %s") params.append(json.dumps(metadata)) params.append(str(process_uuid)) update_query = f"UPDATE active_processes SET {', '.join(update_fields)} WHERE uuid = %s" cursor.execute(update_query, params) connection.commit() logger.info(f"Updated process {process_uuid} status to: {status}") return True except Exception as e: logger.error(f"Error updating process status: {e}") if connection: connection.rollback() return False finally: if cursor: cursor.close() if connection: close_database_connection(connection) def delete_process(process_uuid): """Deletes a process from the database""" connection = None cursor = None try: connection = connect_to_database() cursor = connection.cursor() delete_query = "DELETE FROM active_processes WHERE uuid = %s" cursor.execute(delete_query, (str(process_uuid),)) connection.commit() logger.info(f"Deleted process: {process_uuid}") return True except Exception as e: logger.error(f"Error deleting process: {e}") if connection: connection.rollback() return False finally: if cursor: cursor.close() if connection: close_database_connection(connection) def cleanup_expired_processes(): """Cleans up expired processes and marks them as completed""" connection = None cursor = None try: connection = connect_to_database() cursor = connection.cursor() current_time = datetime.now() # Find expired processes select_query = """ SELECT uuid, process_type, data FROM active_processes WHERE status = 'active' AND end_time IS NOT NULL AND end_time <= %s """ cursor.execute(select_query, (current_time,)) expired_processes = cursor.fetchall() # Update expired processes if expired_processes: update_query = """ UPDATE active_processes SET status = 'expired', updated_at = %s WHERE status = 'active' AND end_time IS NOT NULL AND end_time <= %s """ cursor.execute(update_query, (current_time, current_time)) connection.commit() logger.info(f"Marked {len(expired_processes)} processes as expired") return expired_processes except Exception as e: logger.error(f"Error cleaning up expired processes: {e}") return [] finally: if cursor: cursor.close() if connection: close_database_connection(connection) # Process Management Task @tasks.loop(minutes=1) async def process_manager(): """Main task that manages all active processes""" try: # Clean up expired processes first expired_processes = cleanup_expired_processes() # Handle expired processes for uuid_str, process_type, data_json in expired_processes: await handle_expired_process(uuid_str, process_type, json.loads(data_json) if data_json else {}) # Check for processes that need handling active_processes = get_active_processes(status="active") for process in active_processes: if process["end_time"] and datetime.now() >= process["end_time"]: await handle_process_completion(process) except Exception as e: logger.error(f"Error in process manager: {e}") async def handle_expired_process(process_uuid, process_type, data): """Handles different types of expired processes""" try: if process_type == "giveaway": await handle_expired_giveaway(process_uuid, data) elif process_type == "mute": await handle_expired_mute(process_uuid, data) elif process_type == "ban": await handle_expired_ban(process_uuid, data) # Add more process types as needed except Exception as e: logger.error(f"Error handling expired process {process_uuid}: {e}") async def handle_expired_giveaway(process_uuid, data): """Handles expired giveaway processes""" try: giveaway_id = data.get("giveaway_id") guild_id = data.get("guild_id") channel_id = data.get("channel_id") if not giveaway_id: logger.error(f"No giveaway_id found in process data for {process_uuid}") update_process_status(process_uuid, "failed") return # Try to get giveaway from memory first, then from database giveaway = None if giveaway_id in giveaways: giveaway = giveaways[giveaway_id] else: # Recreate giveaway object from database data try: guild = client.get_guild(guild_id) channel = guild.get_channel(channel_id) if guild else None if not guild or not channel: logger.error(f"Could not find guild {guild_id} or channel {channel_id} for giveaway {giveaway_id}") update_process_status(process_uuid, "failed") return # Create a minimal context object for the giveaway class MinimalContext: def __init__(self, channel): self.channel = channel self.guild = channel.guild async def send(self, *args, **kwargs): return await self.channel.send(*args, **kwargs) ctx = MinimalContext(channel) # Create giveaway object from stored data using the class method giveaway = Giveaway.from_process_data(ctx, data) # Restore participants from database stored_participants = data.get("participants", []) for participant_data in stored_participants: try: user = await client.fetch_user(participant_data["id"]) giveaway.participants.append(user) except Exception as e: logger.error(f"Could not fetch participant {participant_data}: {e}") logger.info(f"Recreated giveaway {giveaway_id} from database for completion with {len(giveaway.participants)} participants") except Exception as e: logger.error(f"Error recreating giveaway {giveaway_id}: {e}") update_process_status(process_uuid, "failed") return # Execute giveaway ending logic winners = giveaway.pick_winners() if winners: winner_mentions = ", ".join([winner.mention for winner in winners]) await giveaway.ctx.send(f"🎉 Congratulations to the winners of the giveaway '{giveaway.title}'! The winners are: {winner_mentions}") # Process winners for i, winner in enumerate(winners): try: if i < len(giveaway.winner_uuids): winner_uuid = giveaway.winner_uuids[i] assign_winner_to_uuid(winner_uuid, winner.id) await winner.send(f"🎁 Congratulations! You won the giveaway '{giveaway.title}'!\n" f"Please claim your prize using the following link: {GIVEAWAY_WEBSITE_URL}{giveaway.guild_id}/{winner_uuid}") except Exception as e: logger.error(f"Error processing winner {winner.name}: {e}") else: await giveaway.ctx.send(f"The giveaway '{giveaway.title}' has ended, but there were no participants.") # Clean up if giveaway_id in giveaways: del giveaways[giveaway_id] update_process_status(process_uuid, "completed") logger.info(f"Successfully completed expired giveaway {giveaway_id}") except Exception as e: logger.error(f"Error handling expired giveaway {process_uuid}: {e}") update_process_status(process_uuid, "failed") async def handle_expired_mute(process_uuid, data): """Handles expired mute processes""" try: guild_id = data.get("guild_id") user_id = data.get("user_id") mute_role_id = data.get("mute_role_id") channel_id = data.get("channel_id") reason = data.get("reason", "Automatic unmute - time expired") if not guild_id or not user_id: logger.error(f"Missing guild_id or user_id in mute process {process_uuid}") update_process_status(process_uuid, "failed") return # Get guild and member guild = client.get_guild(int(guild_id)) if not guild: logger.error(f"Guild {guild_id} not found for expired mute {process_uuid}") update_process_status(process_uuid, "failed") return member = guild.get_member(int(user_id)) if not member: logger.info(f"User {user_id} no longer in guild {guild_id}, marking mute as completed") update_process_status(process_uuid, "completed") return # Get mute role mute_role = None if mute_role_id: mute_role = guild.get_role(int(mute_role_id)) # If mute role not found, try to get it from guild settings if not mute_role: guild_settings = get_guild_settings(guild_id) if guild_settings and guild_settings.get("mute_role_id"): mute_role = guild.get_role(int(guild_settings["mute_role_id"])) # Remove mute role if user still has it if mute_role and mute_role in member.roles: await member.remove_roles(mute_role, reason="Automatic unmute - time expired") logger.info(f"Removed mute role from user {user_id} in guild {guild_id}") # Restore previous roles if they were saved try: restored_roles = await restore_user_roles(member, guild) if restored_roles: logger.info(f"Restored {len(restored_roles)} roles for user {user_id} in guild {guild_id}") except Exception as e: logger.warning(f"Could not restore roles for user {user_id}: {e}") # Send notification to channel if possible if channel_id: try: channel = guild.get_channel(int(channel_id)) if channel: embed = discord.Embed( title="🔊 User Automatically Unmuted", description=f"{member.mention} has been automatically unmuted.", color=0x00ff00, timestamp=datetime.now() ) embed.add_field(name="📝 Reason", value="Mute duration expired", inline=False) embed.set_footer(text=f"Process ID: {process_uuid}") await channel.send(embed=embed) except Exception as e: logger.warning(f"Could not send unmute notification: {e}") # Try to DM the user try: user = await client.fetch_user(int(user_id)) if user: dm_embed = discord.Embed( title="🔊 You have been unmuted", description=f"Your mute in **{guild.name}** has expired.", color=0x00ff00, timestamp=datetime.now() ) dm_embed.set_footer(text=f"Server: {guild.name}") await user.send(embed=dm_embed) except (discord.Forbidden, discord.NotFound): pass # User has DMs disabled or doesn't exist logger.info(f"Successfully unmuted user {user_id} in guild {guild_id}") update_process_status(process_uuid, "completed") except Exception as e: logger.error(f"Error handling expired mute {process_uuid}: {e}") update_process_status(process_uuid, "failed") async def handle_expired_ban(process_uuid, data): """Handles expired ban processes - placeholder for future implementation""" try: # TODO: Implement ban removal logic guild_id = data.get("guild_id") user_id = data.get("user_id") logger.info(f"Ban expired for user {user_id} in guild {guild_id}") update_process_status(process_uuid, "completed") except Exception as e: logger.error(f"Error handling expired ban {process_uuid}: {e}") async def handle_process_completion(process): """Generic handler for process completion""" try: await handle_expired_process(process["uuid"], process["process_type"], process["data"] or {}) except Exception as e: logger.error(f"Error in process completion handler: {e}") async def restore_active_processes_on_startup(): """Restores active processes when the bot starts up""" try: logger.info("Restoring active processes from database...") # Get all active processes processes = get_active_processes(status="active") restored_count = 0 for process in processes: try: if process["process_type"] == "giveaway": await restore_giveaway_process(process) restored_count += 1 elif process["process_type"] == "mute": await restore_mute_process(process) restored_count += 1 # Add more process types as needed except Exception as e: logger.error(f"Error restoring process {process['uuid']}: {e}") logger.info(f"Successfully restored {restored_count} active processes") except Exception as e: logger.error(f"Error restoring active processes: {e}") async def restore_giveaway_process(process): """Restores a giveaway process from the database""" try: data = process["data"] or {} giveaway_id = data.get("giveaway_id") if giveaway_id: # Check if giveaway is still valid and not expired if process["end_time"] and datetime.now() < process["end_time"]: # Create a minimal giveaway object for restoration # Note: This is a simplified restoration - full ctx recreation may not be possible logger.info(f"Restored giveaway process {giveaway_id} from database") # Start the process manager if it's not already running if not process_manager.is_running(): process_manager.start() else: # Mark as expired if past end time update_process_status(process["uuid"], "expired") except Exception as e: logger.error(f"Error restoring giveaway process: {e}") async def restore_mute_process(process): """Restores a mute process from the database""" try: logger.info(f"Restoring mute process {process['uuid']}") # Extract process data data = process.get("data", {}) user_id = data.get("user_id") or process.get("user_id") guild_id = data.get("guild_id") or process.get("guild_id") end_time = process.get("end_time") if not user_id or not guild_id or not end_time: logger.error(f"Missing required data for mute process {process['uuid']}") update_process_status(process["uuid"], "error") return # Get guild and user guild = client.get_guild(int(guild_id)) if not guild: logger.error(f"Guild {guild_id} not found for mute process {process['uuid']}") update_process_status(process["uuid"], "error") return member = guild.get_member(int(user_id)) if not member: logger.info(f"User {user_id} no longer in guild {guild_id}, marking mute process as completed") update_process_status(process["uuid"], "completed") return # Check if mute has already expired if datetime.now() >= end_time: logger.info(f"Mute process {process['uuid']} has already expired, executing unmute") await handle_expired_process(process["uuid"], "mute", data) else: # Schedule the unmute for when it's supposed to end remaining_time = (end_time - datetime.now()).total_seconds() logger.info(f"Scheduling mute process {process['uuid']} to complete in {remaining_time:.0f} seconds") # Check if the user is actually still muted (has mute role) mute_role_id = data.get("mute_role_id") if mute_role_id: mute_role = guild.get_role(int(mute_role_id)) if mute_role and mute_role in member.roles: logger.info(f"User {user_id} still has mute role, mute process {process['uuid']} is valid") else: logger.info(f"User {user_id} no longer has mute role, marking process as completed") update_process_status(process["uuid"], "completed") except Exception as e: logger.error(f"Error restoring mute process: {e}") update_process_status(process["uuid"], "error") #----------------------------------------------------------------------------------------------------------- async def update_all_users(batch_size=20, pause_duration=1): connection = connect_to_database() cursor = connection.cursor() cursor.execute("SELECT DISTINCT guild_id FROM guilds") guilds = cursor.fetchall() cursor.close() close_database_connection(connection) for guild_id_tuple in guilds: guild_id = guild_id_tuple[0] guild = client.get_guild(int(guild_id)) if guild: members = guild.members total_members = len(members) for i in range(0, total_members, batch_size): batch = members[i:i + batch_size] for member in batch: user_id = member.id user_data = load_user_data_sync(user_id, guild_id) # Daten aktualisieren nickname = member.display_name profile_picture = str(member.display_avatar.url) if member.display_avatar else None join_date = member.joined_at.date() if member.joined_at else None leave_date = None if member in guild.members else datetime.now().date() update_user_data(user_id, guild_id, "nickname", nickname) update_user_data(user_id, guild_id, "profile_picture", profile_picture) update_user_data(user_id, guild_id, "join_date", join_date) update_user_data(user_id, guild_id, "leave_date", leave_date) # Pause nach jeder Charge await asyncio.sleep(pause_duration) def save_giveaway_to_db(guild_id, platform, name, prize_uuid, game_key): connection = None cursor = None try: connection = connect_to_database() cursor = connection.cursor() insert_query = """ INSERT INTO giveaway_data (guild_id, uuid, platform, name, game_key) VALUES (%s, %s, %s, %s, %s) """ data = (guild_id, str(prize_uuid), platform, name, game_key) cursor.execute(insert_query, data) connection.commit() logger.info(f"Successfully saved giveaway to database: UUID={prize_uuid}") except Exception as e: logger.error(f"Error saving giveaway to database: {e}") if connection: connection.rollback() raise e finally: if cursor: cursor.close() if connection: close_database_connection(connection) def create_winner_slots_in_db(guild_id, platform, name, num_winners, game_key="PREDEFINED_GAME_KEY"): """Erstellt vorab Datenbank-Einträge für alle möglichen Gewinner mit eigenen UUIDs""" winner_uuids = [] for i in range(num_winners): connection = None cursor = None try: connection = connect_to_database() cursor = connection.cursor() winner_uuid = uuid.uuid4() insert_query = """ INSERT INTO giveaway_data (guild_id, uuid, platform, name, game_key, winner_dc_id) VALUES (%s, %s, %s, %s, %s, %s) """ # winner_dc_id ist zunächst NULL, wird später beim Gewinn gesetzt data = (guild_id, str(winner_uuid), platform, name, game_key, None) cursor.execute(insert_query, data) connection.commit() winner_uuids.append(winner_uuid) logger.info(f"Created winner slot {i+1}/{num_winners} with UUID: {winner_uuid}") except Exception as e: logger.error(f"Error creating winner slot {i+1}: {e}") if connection: connection.rollback() raise e finally: if cursor: cursor.close() if connection: close_database_connection(connection) return winner_uuids def save_winner_to_db(guild_id, platform, name, winner_dc_id, game_key="PREDEFINED_GAME_KEY"): """Erstellt einen eigenen Datenbankeintrag für jeden Gewinner mit eigener UUID""" connection = None cursor = None try: connection = connect_to_database() cursor = connection.cursor() winner_uuid = uuid.uuid4() insert_query = """ INSERT INTO giveaway_data (guild_id, uuid, platform, name, game_key, winner_dc_id) VALUES (%s, %s, %s, %s, %s, %s) """ data = (guild_id, str(winner_uuid), platform, name, game_key, winner_dc_id) cursor.execute(insert_query, data) connection.commit() logger.info(f"Successfully saved winner to database: UUID={winner_uuid}, winner_dc_id={winner_dc_id}") return winner_uuid except Exception as e: logger.error(f"Error saving winner to database: {e}") if connection: connection.rollback() raise e finally: if cursor: cursor.close() if connection: close_database_connection(connection) def assign_winner_to_uuid(winner_uuid, winner_dc_id): """Verknüpft eine bereits existierende UUID mit einem tatsächlichen Gewinner""" connection = None cursor = None try: connection = connect_to_database() cursor = connection.cursor() update_query = """ UPDATE giveaway_data SET winner_dc_id = %s WHERE uuid = %s """ data = (winner_dc_id, str(winner_uuid)) cursor.execute(update_query, data) connection.commit() logger.info(f"Successfully assigned winner {winner_dc_id} to UUID: {winner_uuid}") return True except Exception as e: logger.error(f"Error assigning winner to UUID: {e}") if connection: connection.rollback() raise e finally: if cursor: cursor.close() if connection: close_database_connection(connection) def update_winner_in_db(guild_id, prize_uuid, winner_dc_id): connection = None cursor = None try: connection = connect_to_database() cursor = connection.cursor() update_query = """ UPDATE giveaway_data SET winner_dc_id = %s WHERE uuid = %s AND guild_id = %s """ data = (winner_dc_id, str(prize_uuid), guild_id) cursor.execute(update_query, data) connection.commit() logger.info(f"Successfully updated winner in database: UUID={prize_uuid}, winner_dc_id={winner_dc_id}") except Exception as e: logger.error(f"Error updating winner in database: {e}") if connection: connection.rollback() raise e finally: if cursor: cursor.close() if connection: close_database_connection(connection) class Giveaway: def __init__(self, ctx, platform, prize, num_winners, title, subtitle, duration, end_time): self.ctx = ctx self.guild_id = ctx.guild.id # Speichern der guild_id self.platform = platform self.prize = prize self.num_winners = num_winners self.title = title self.subtitle = subtitle self.duration = duration self.end_time = end_time self.participants = [] self.prize_uuid = uuid.uuid4() # Generiert eine eindeutige UUID für das Gewinnspiel self.game_key = f"PREDEFINED_GAME_KEY" # Platzhalter für den tatsächlichen Game-Key # Erstelle nur die Gewinner-Einträge, KEINEN Haupt-Eintrag self.winner_uuids = create_winner_slots_in_db(self.guild_id, self.platform, self.title, self.num_winners, self.game_key) logger.info(f"Created giveaway '{self.title}' with {len(self.winner_uuids)} winner slots: {[str(uuid) for uuid in self.winner_uuids]}") # Create process entry in active_processes table self.process_uuid = None self.create_process_entry() @classmethod def from_process_data(cls, ctx, data): """Alternative constructor for restoring from process data""" giveaway = cls.__new__(cls) giveaway.ctx = ctx giveaway.guild_id = ctx.guild.id giveaway.platform = data.get("platform", "Unknown") giveaway.prize = data.get("prize", "Unknown Prize") giveaway.num_winners = data.get("num_winners", 1) giveaway.title = data.get("title", "Unknown Giveaway") giveaway.subtitle = data.get("subtitle", "") giveaway.duration = "restored" giveaway.end_time = datetime.now() # Already expired giveaway.participants = [] giveaway.prize_uuid = data.get("prize_uuid", str(uuid.uuid4())) giveaway.game_key = data.get("game_key", "PREDEFINED_GAME_KEY") giveaway.winner_uuids = data.get("winner_uuids", []) giveaway.process_uuid = None return giveaway def create_process_entry(self): """Creates an entry in the active_processes table for this giveaway""" try: giveaway_data = { "giveaway_id": len(giveaways) + 1, # Will be set properly when added to giveaways dict "guild_id": self.guild_id, "channel_id": self.ctx.channel.id, "platform": self.platform, "prize": self.prize, "num_winners": self.num_winners, "title": self.title, "subtitle": self.subtitle, "winner_uuids": [str(uuid) for uuid in self.winner_uuids], "prize_uuid": str(self.prize_uuid), "game_key": self.game_key, "participants": [] } giveaway_metadata = { "duration": self.duration, "author_id": self.ctx.author.id } self.process_uuid = create_active_process( process_type="giveaway", guild_id=self.guild_id, channel_id=self.ctx.channel.id, user_id=self.ctx.author.id, start_time=datetime.now(), end_time=self.end_time, status="active", data=giveaway_data, metadata=giveaway_metadata ) logger.info(f"Created process entry for giveaway '{self.title}' with process UUID: {self.process_uuid}") except Exception as e: logger.error(f"Error creating process entry for giveaway: {e}") def update_process_data(self, giveaway_id): """Updates the process data with the actual giveaway ID""" try: if self.process_uuid: current_processes = get_active_processes() for process in current_processes: if process["uuid"] == str(self.process_uuid): data = process["data"] or {} data["giveaway_id"] = giveaway_id update_process_status(self.process_uuid, "active", data=data) logger.info(f"Updated process data for giveaway {giveaway_id}") break except Exception as e: logger.error(f"Error updating process data: {e}") def add_participant(self, user): if user not in self.participants: self.participants.append(user) # Update process data with participant list and count try: if self.process_uuid: current_processes = get_active_processes() for process in current_processes: if process["uuid"] == str(self.process_uuid): data = process["data"] or {} data["participant_count"] = len(self.participants) data["participants"] = [{"id": p.id, "name": p.name} for p in self.participants] update_process_status(self.process_uuid, "active", data=data) break except Exception as e: logger.error(f"Error updating participant data: {e}") return True return False def is_finished(self): return datetime.now() >= self.end_time def pick_winners(self): available_participants = len(self.participants) winners_to_pick = min(self.num_winners, available_participants) logger.info(f"Picking winners: requested={self.num_winners}, available_participants={available_participants}, winners_to_pick={winners_to_pick}") if winners_to_pick == 0: return [] winners = random.sample(self.participants, winners_to_pick) logger.info(f"Selected {len(winners)} winners: {[winner.name for winner in winners]}") return winners def complete_giveaway(self): """Marks the giveaway process as completed""" try: if self.process_uuid: update_process_status(self.process_uuid, "completed") logger.info(f"Marked giveaway process {self.process_uuid} as completed") except Exception as e: logger.error(f"Error completing giveaway process: {e}") @client.hybrid_command() async def startgiveaway(ctx, platform: str, prize: str, num_winners: int, title: str, subtitle: str, duration: str): """Creates a new giveaway, only available for admins.""" guild_id = ctx.guild.id user_data = load_user_data_sync(ctx.author.id, guild_id) if user_data["permission"] < 5: await ctx.send("You don't have permission to create a giveaway.") return if duration.endswith("m"): minutes = int(duration[:-1]) end_time = datetime.now() + timedelta(minutes=minutes) elif duration.endswith("d"): days = int(duration[:-1]) end_time = datetime.now() + timedelta(days=days) else: await ctx.send("Invalid duration. Please use 'm' for minutes or 'd' for days.") return # Create new giveaway giveaway = Giveaway(ctx, platform, prize, num_winners, title, subtitle, duration, end_time) giveaway_id = len(giveaways) + 1 giveaways[giveaway_id] = giveaway # Update the process data with the actual giveaway ID giveaway.update_process_data(giveaway_id) button = Button(label="Participate", style=discord.ButtonStyle.green, custom_id=f"giveaway_{giveaway_id}") view = View() view.add_item(button) unix_end_time = int(time.mktime(end_time.timetuple())) embed = discord.Embed( title=title, description=f"{subtitle}\n\nPrize: {prize}\nPlatform: {platform}\nNumber of winners: {num_winners}\nEnds ", color=0x00ff00 ) embed.set_footer(text=f"Giveaway ends at {end_time.strftime('%Y-%m-%d %H:%M:%S')}") await ctx.send(embed=embed, view=view) # Start the process manager if it's not already running if not process_manager.is_running(): process_manager.start() # ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- live_chats = {} live_chat_queue = asyncio.Queue() def read_file(filename): try: with open(filename, "r", encoding="utf-8") as file: return file.read() except FileNotFoundError: return "Du bist ein hilfreicher Assistent, der Fragen beantwortet." def load_chat_history(channel_id): """Lädt die Chat-Historie für einen bestimmten Kanal.""" history_file = os.path.join(CACHE_DIR, f"chat_{channel_id}.json") if os.path.exists(history_file): with open(history_file, "r", encoding="utf-8") as file: return json.load(file) return [] def save_chat_history(channel_id, messages): """Speichert die Chat-Historie für einen bestimmten Kanal.""" history_file = os.path.join(CACHE_DIR, f"chat_{channel_id}.json") with open(history_file, "w", encoding="utf-8") as file: json.dump(messages, file, indent=4) @client.hybrid_command() async def startlivechat(ctx): """Starts the live chat in the current channel.""" channel_id = ctx.channel.id if channel_id in live_chats and live_chats[channel_id]["active"]: await ctx.send("Live chat is already active.") return # Lade oder initialisiere die Chat-Historie history = load_chat_history(channel_id) live_chats[channel_id] = {"messages": history, "active": True} await ctx.send("Live chat started. Messages will be processed.") @client.hybrid_command() async def stoplivechat(ctx): """Stops the live chat in the current channel.""" channel_id = ctx.channel.id if channel_id in live_chats: live_chats[channel_id]["active"] = False await ctx.send("Live chat has been stopped.") else: await ctx.send("No active live chat in this channel.") @client.event async def on_message(message): if message.author.bot: # Bots ignorieren return channel_id = message.channel.id if channel_id in live_chats and live_chats[channel_id]["active"]: # Alle benötigten Daten aus dem message-Objekt extrahieren msg_id = str(message.id) user_id = str(message.author.id) nickname = message.author.display_name content = message.content # Füge die Nachricht zur Warteschlange hinzu await live_chat_queue.put((message, msg_id, user_id, nickname, content)) await client.process_commands(message) async def process_live_chat_queue(): while True: loop = asyncio.get_running_loop() try: if not askmultus_queue.empty(): message, msg_id, user_id, nickname, content = await live_chat_queue.get() live_introduction = read_file("live_introduction.txt") live_background_data = read_file("live_background_data.txt") message_data = live_introduction + " background data:" + live_background_data chat_history = load_chat_history(message.channel.id) timestamp = int(time.time()) # Unix-Timestamp content = timestamp + "/" + msg_id + "/" + user_id + "/" + nickname + ":" + content chat_history.append({"role": "user", "content": f"{content}"}) messages = [ {"role": "system", "content": message_data}, *chat_history ] ai_answer = await loop.run_in_executor(executor, lambda: openai_instance.chat.completions.create( model="model", messages=messages, temperature=0.8, timeout=15, # Limit waiting time for response )) ai_message = ai_answer.choices[0].message.content chat_history.append({"role": "assistant", "content": ai_message}) save_chat_history(message.channel.id, chat_history) channel = message.channel if ai_message.strip() != "::null::": if channel: await channel.send(f"**AI:** {ai_message}") live_chat_queue.task_done() except asyncio.CancelledError: break except Exception as e: logger.error(f"Error processing live chat queue: {e}") await asyncio.sleep(5) # ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @client.hybrid_command() async def setlocalpermission(ctx, permission_level: int): """Allows an admin or higher to set their own local permission level.""" user_id = ctx.author.id guild_id = ctx.guild.id # Globale Berechtigungen abrufen global_perms = get_global_permission(user_id) # Wenn der Benutzer mindestens Admin ist, kann er die Berechtigungen setzen if global_perms is not None and global_perms >= 8: # Admin-Level ist 8 oder höher # Lokale Berechtigungen setzen update_user_data(user_id, guild_id, "permission", permission_level) await ctx.send(f"Your local permission level has been set to {permission_level}.") else: await ctx.send("You do not have permission to set local permissions.") # Old check_giveaway task removed - now handled by process_manager @client.event async def on_interaction(interaction): """Processes participation in a giveaway.""" # Nur Button-Interaktionen für Giveaways verarbeiten if interaction.type == discord.InteractionType.component and "custom_id" in interaction.data: if interaction.data["custom_id"].startswith("giveaway_"): giveaway_id = int(interaction.data["custom_id"].split("_")[1]) giveaway = giveaways.get(giveaway_id) if giveaway: if giveaway.is_finished(): await interaction.response.send_message("This giveaway has already ended.", ephemeral=True) else: added = giveaway.add_participant(interaction.user) if added: await interaction.response.send_message("You have successfully entered the giveaway!", ephemeral=True) else: await interaction.response.send_message("You're already participating in this giveaway.", ephemeral=True) # Slash Commands und andere Interaktionen werden automatisch vom Framework verarbeitet def read_introduction(): try: with open("introduction.txt", "r", encoding="utf-8") as file: introduction = file.read() return introduction except FileNotFoundError: return "" def read_askintroduction(): try: with open("asknotesintro.txt", "r", encoding="utf-8") as file: introduction = file.read() return introduction except FileNotFoundError: return "" def read_background_data(filename): try: with open(filename, "r", encoding="utf-8") as file: data = file.read() return data except FileNotFoundError: return "" def get_current_datetime(): return datetime.now().strftime("%Y-%m-%d %H:%M:%S") def calculate_xp_needed_for_level(level): """Calculates the XP needed for the next level.""" xp_need = 5 * (int(level) ** 2) + 50 * int(level) + 100 return int(xp_need) async def add_xp_to_user(user_id, guild_id, xp_gained, member=None): """Adds XP to a user and checks if they level up. Also updates user data.""" # Lade Benutzerdaten (XP, Level, etc.) - mit member-Objekt für neue User user_data = await load_user_data(user_id, guild_id, member) # Initialisiere XP, falls es None ist user_data["xp"] = user_data.get("xp", 0) # Füge die gewonnenen XP hinzu user_data["xp"] += xp_gained # Berechne die benötigten XP für das aktuelle Level level = user_data["level"] xp_needed = calculate_xp_needed_for_level(level) # Überprüfe, ob der Benutzer aufgestiegen ist level_up = False while user_data["xp"] >= xp_needed: # Reduziere die überschüssigen XP und erhöhe das Level user_data["xp"] -= xp_needed user_data["level"] += 1 level_up = True # Berechne die neuen XP-Anforderungen für das nächste Level xp_needed = calculate_xp_needed_for_level(user_data["level"]) # Aktualisiere Benutzerdaten wenn member-Objekt verfügbar ist if member: try: # Aktualisiere Nickname new_nickname = member.display_name if user_data.get("nickname") != new_nickname: try: update_user_data(user_id, guild_id, "nickname", new_nickname) user_data["nickname"] = new_nickname except Exception as e: logger.error(f"Failed to update nickname for user {user_id}: {e}") # Aktualisiere Profilbild - mit lokalem Download und Hash-Vergleich if member.display_avatar: try: discord_avatar_url = str(member.display_avatar.url) # Download und speichere das Profilbild lokal local_profile_path = await download_and_save_profile_image(user_id, discord_avatar_url) # Speichere den lokalen Pfad in der Datenbank statt der Discord URL if user_data.get("profile_picture") != local_profile_path: update_user_data(user_id, guild_id, "profile_picture", local_profile_path) user_data["profile_picture"] = local_profile_path except Exception as e: logger.error(f"Failed to update profile picture for user {user_id}: {e}") else: # Kein Profilbild vorhanden, nutze Default try: default_path = "/static/default_profile.png" if user_data.get("profile_picture") != default_path: update_user_data(user_id, guild_id, "profile_picture", default_path) user_data["profile_picture"] = default_path except Exception as e: logger.error(f"Failed to set default profile picture for user {user_id}: {e}") # Aktualisiere Join-Datum - IMMER wenn member.joined_at verfügbar ist if member.joined_at: try: join_date = member.joined_at.date() # Aktualisiere Join-Datum auch wenn es bereits existiert (für den Fall, dass es falsch war) update_user_data(user_id, guild_id, "join_date", join_date) user_data["join_date"] = join_date except Exception as e: logger.error(f"Failed to update join date for user {user_id}: {e}") logger.info(f"Updated user data for {member.display_name} (ID: {user_id}) - Nickname: {new_nickname}, Join Date: {join_date if member.joined_at else 'N/A'}") except Exception as e: logger.error(f"Error updating user data during XP gain: {e}") # Speichere die aktualisierten XP und Level in der Datenbank try: update_user_data(user_id, guild_id, "xp", user_data["xp"]) update_user_data(user_id, guild_id, "level", user_data["level"]) except Exception as e: logger.error(f"Failed to update XP/Level for user {user_id}: {e}") return level_up # Gibt zurück, ob ein Level-Up stattgefunden hat @client.hybrid_command() async def level(ctx, user: discord.User = None): """Shows the current level and XP of the user or another person.""" guild_id = ctx.guild.id # Wenn kein User angegeben wurde, zeige das eigene Level if user is None: target_user = ctx.author user_id = ctx.author.id else: target_user = user user_id = user.id # Lade die Benutzerdaten (XP und Level) aus der Datenbank user_data = load_user_data_sync(user_id, guild_id) # Berechne die für das nächste Level benötigten XP current_level = user_data["level"] current_xp = user_data["xp"] xp_needed = calculate_xp_needed_for_level(current_level) # Erstelle eine Antwort mit den aktuellen Level-Informationen embed = discord.Embed( title=f"Level Information for {target_user.display_name}", description=f"Level: {current_level}\nXP: {current_xp}/{xp_needed}", color=0x00ff00 ) # Füge das Profilbild des Benutzers hinzu embed.set_thumbnail(url=target_user.display_avatar.url) await ctx.send(embed=embed) @client.hybrid_command() async def leaderboard(ctx): """Shows the top users in the XP leaderboard.""" guild_id = ctx.guild.id connection = connect_to_database() cursor = connection.cursor() # Abfrage, um die Benutzer basierend auf der XP zu sortieren select_query = """ SELECT user_id, xp, level FROM user_data WHERE guild_id = %s ORDER BY level DESC, xp DESC LIMIT 10 """ cursor.execute(select_query, (guild_id,)) result = cursor.fetchall() # Liste, um die Benutzer und ihre XP zu speichern leaderboard_entries = [] # Benutzernamen über die user_id abrufen und in die Liste einfügen for row in result: user_id = row[0] xp = row[1] level = row[2] # Benutzername mit der user_id abrufen user = await client.fetch_user(user_id) username = user.name leaderboard_entries.append(f"{username}: Level {level}, XP {xp}") cursor.close() close_database_connection(connection) # Erstelle die Nachricht für das Leaderboard leaderboard_message = "\n".join(leaderboard_entries) embed = discord.Embed( title="Leaderboard", description=leaderboard_message, color=0x00ff00 ) await ctx.send(embed=embed) xp_cooldowns = {} @client.event async def on_message(message): """Event-Handler, der XP vergibt, wenn Nachrichten gesendet werden.""" if message.author.bot: return # Ignoriere Nachrichten von Bots # Überprüfe, ob die Nachricht in einem Server gesendet wurde if message.guild is None: await client.process_commands(message) return user_id = message.author.id guild_id = message.guild.id member = message.author # Das Member-Objekt für Datenaktualisierung # XP-Cooldown überprüfen (60 Sekunden) cooldown_key = (user_id, guild_id) current_time = time.time() if cooldown_key in xp_cooldowns: time_since_last_xp = current_time - xp_cooldowns[cooldown_key] if time_since_last_xp < 60: # 60 Sekunden Cooldown await client.process_commands(message) return # Cooldown aktualisieren xp_cooldowns[cooldown_key] = current_time xp_gained = random.randint(2, 25) # Zufällige XP zwischen 2 und 25 vergeben # XP vergeben und Benutzerdaten aktualisieren level_up = await add_xp_to_user(user_id, guild_id, xp_gained, member) # Optional: Level-Up Benachrichtigung senden if level_up: user_data = await load_user_data(user_id, guild_id, member) new_level = user_data["level"] try: await message.channel.send(f"🎉 {member.mention} has reached **Level {new_level}**! Congratulations! 🎉") except: # Falls das Senden fehlschlägt, einfach überspringen pass # Weiterleiten der Nachricht an andere Event-Handler await client.process_commands(message) # Verwenden Sie die Funktion, um Hintergrunddaten zu laden background_data = read_background_data("background_data.txt") @client.event async def on_ready(): # Start background tasks client.loop.create_task(process_ai_queue()) client.loop.create_task(process_live_chat_queue()) # Starte die Queue-Verarbeitung # Initialize process management system await restore_active_processes_on_startup() # Start the process manager if not process_manager.is_running(): process_manager.start() logger.info("Bot is ready!") logger.info(f"Logged in as: {client.user.name}") logger.info(f"Client ID: {client.user.id}") logger.info('------') # Version check version_url = "https://simolzimol.eu/version_chat.txt" current_version = __version__ try: response = requests.get(version_url) if response.status_code == 200: latest_version = response.text.strip() if latest_version != current_version: logger.info(f"New version available: {latest_version}") else: logger.info("Bot is up to date.") else: logger.info("Unable to retrieve version information.") except requests.exceptions.RequestException: logger.info("Failed to connect to version server.") @client.event async def on_command_error(ctx, error): logger.error(f"An error occurred while executing the command: {error}") @client.event async def on_command(ctx): command = ctx.command logger.info(f"Command '{command.name}' was executed by '{ctx.author.name}' in '{ctx.guild.name}'.") @client.hybrid_command() async def points(ctx): """Shows how many points you have.""" user_id = ctx.author.id guild_id = ctx.guild.id # Lade Benutzerdaten aus der MySQL-Datenbank user_data = load_user_data_sync(user_id, guild_id) points = user_data["points"] embed = discord.Embed( title="Points", description=f"You have {points} points.", color=0x3498db ) await ctx.send(embed=embed) @client.hybrid_command() async def permissionlevel(ctx): """Displays the authorization level and rank of the user.""" user_id = ctx.author.id guild_id = ctx.guild.id # Load user data from the MySQL database user_data = load_user_data_sync(user_id, guild_id) permission_level = user_data["permission"] rank = "" if permission_level == 10: rank = "Owner" elif permission_level == 8: rank = "Admin" elif permission_level == 5: rank = "Mod" else: rank = "User" embed = discord.Embed( title="Permission Level", description=f"Your permission level is: {permission_level}. Your rank is: {rank}.", color=0x3498db ) await ctx.send(embed=embed) @client.hybrid_command() async def addpoints(ctx, user: discord.User, amount: int): """Adds a certain number of points to a user.""" user_perms = load_user_data_sync(ctx.author.id, ctx.guild.id) if 5 <= user_perms["permission"]: user_id = user.id guild_id = ctx.guild.id # Lade Benutzerdaten aus der MySQL-Datenbank user_data = load_user_data_sync(user_id, guild_id) # Füge die Punkte hinzu user_data["points"] += amount # Speichere die aktualisierten Benutzerdaten in der MySQL-Datenbank update_user_data(user_data["user_id"], guild_id, "points", user_data["points"]) embed = discord.Embed( title="Points Added", description=f"Added {amount} points to {user.display_name}.", color=0x2ecc71 ) await ctx.send(embed=embed) else: await ctx.send("You don't have permissions.") @client.hybrid_command() async def resetpoints(ctx, user: discord.User): """Resets a user's points to 0.""" user_perms = load_user_data_sync(ctx.author.id, ctx.guild.id) if 5 <= user_perms["permission"]: user_id = user.id guild_id = ctx.guild.id # Lade Benutzerdaten aus der MySQL-Datenbank user_data = load_user_data_sync(user_id, guild_id) # Setze die Punkte auf 0 zurück user_data["points"] = 0 # Speichere die aktualisierten Benutzerdaten in der MySQL-Datenbank update_user_data(user_data["user_id"], guild_id, "points", user_data["points"]) embed = discord.Embed( title="Points Reset", description=f"Reset points for {user.display_name}.", color=0x2ecc71 ) await ctx.send(embed=embed) else: await ctx.send("You don't have permissions.") @client.hybrid_command() async def shutdown_(ctx): """Shuts down the bot (Admin only).""" user_perms = load_user_data_sync(ctx.author.id, ctx.guild.id) if 8 <= user_perms["permission"]: await ctx.send("Shutting down the bot...") await client.logout() exit() else: await ctx.send("You don't have the necessary permissions to use this command.") @client.hybrid_command() async def owner_command(ctx): """Syncs the bot's slash commands (Owner only).""" try: user_perms = load_user_data_sync(ctx.author.id, ctx.guild.id) if 10 <= user_perms["permission"]: await client.tree.sync() await ctx.send("reloaded !") else: await ctx.send("You don't have the necessary permissions to use this command.") except Exception as e: await ctx.send(f"An error occurred while executing the command: {e}") @client.hybrid_command(name="bothelp") async def bothelp(ctx): """Shows all available user commands and their descriptions.""" embed = discord.Embed( title="🤖 Bot Commands - User Guide", description="Here are all the commands available to regular users:", color=0x3498db, timestamp=datetime.now() ) # General Commands embed.add_field( name="🔧 General Commands", value=( "`/bothelp` - Shows this help message\n" "`/points` - Check your current points balance\n" "`/level [user]` - View level and XP information\n" "`/leaderboard` - View the server XP leaderboard\n" "`/permissionlevel` - Check your permission level\n" "`/mywarns` - View your moderation statistics\n" "`/version` - Show bot version information" ), inline=False ) # AI & Interaction Commands embed.add_field( name="🧠 AI & Interaction", value=( "`/askmultus ` - Ask Multus AI a question (costs 5 points)\n" "`/vision ` - Analyze an image with AI\n" "`/startlivechat` - Start live chat mode in channel\n" "`/stoplivechat` - Stop live chat mode in channel\n" "`/summarize ` - Summarize last N messages in channel" ), inline=False ) # Notes & Personal Data embed.add_field( name="📝 Notes & Personal Data", value=( "`/addnotes ` - Add notes from text or URL\n" "`/asknotes ` - Ask questions about your saved notes\n" "`/delnotes` - Delete all your saved notes" ), inline=False ) # Check if user has moderation permissions to show additional commands user_data = load_user_data_sync(ctx.author.id, ctx.guild.id) if user_data["permission"] >= 5: embed.add_field( name="🎁 Giveaway & Management Commands", value=( "`/startgiveaway <subtitle> <duration>` - Create a giveaway\n" "`/processes [action] [type]` - View or manage active processes\n" "`/join` - Join server (if bot has invite permissions)\n" "`/leave` - Leave server (staff only)" ), inline=False ) # Owner-specific commands preview if user_data["permission"] >= 8: embed.add_field( name="🛠️ Advanced Commands Available", value=( "Additional admin commands available. Use `/modhelp` for details." ), inline=False ) embed.set_footer(text="Use /modhelp for moderation commands (requires permission level 5+)") await ctx.send(embed=embed) # Remove the default help command to avoid conflicts client.remove_command('help') @client.hybrid_command(name="help") async def help(ctx): """Shows all available user commands and their descriptions.""" embed = discord.Embed( title="🤖 Bot Commands - User Guide", description="Here are all the commands available to regular users:", color=0x3498db, timestamp=datetime.now() ) # General Commands embed.add_field( name="🔧 General Commands", value=( "`/help` - Shows this help message\n" "`/bothelp` - Alternative help command\n" "`/points` - Check your current points balance\n" "`/level [user]` - View level and XP information\n" "`/leaderboard` - View the server XP leaderboard\n" "`/permissionlevel` - Check your permission level\n" "`/mywarns` - View your moderation statistics\n" "`/version` - Show bot version information" ), inline=False ) # AI & Interaction Commands embed.add_field( name="🧠 AI & Interaction", value=( "`/askmultus <prompt>` - Ask Multus AI a question (costs 5 points)\n" "`/vision <image_url>` - Analyze an image with AI\n" "`/startlivechat` - Start live chat mode in channel\n" "`/stoplivechat` - Stop live chat mode in channel\n" "`/summarize <number>` - Summarize last N messages in channel" ), inline=False ) # Notes & Personal Data embed.add_field( name="📝 Notes & Personal Data", value=( "`/addnotes <type> <source>` - Add notes from text or URL\n" "`/asknotes <question>` - Ask questions about your saved notes\n" "`/delnotes` - Delete all your saved notes" ), inline=False ) # Check if user has moderation permissions to show additional commands user_data = load_user_data_sync(ctx.author.id, ctx.guild.id) if user_data["permission"] >= 5: embed.add_field( name="🎁 Giveaway & Management Commands", value=( "`/startgiveaway <platform> <prize> <winners> <title> <subtitle> <duration>` - Create a giveaway\n" "`/processes [action] [type]` - View or manage active processes\n" "`/join` - Join server (if bot has invite permissions)\n" "`/leave` - Leave server (staff only)" ), inline=False ) # Owner-specific commands preview if user_data["permission"] >= 8: embed.add_field( name="🛠️ Advanced Commands Available", value=( "Additional admin commands available. Use `/modhelp` for details." ), inline=False ) embed.set_footer(text="Use /modhelp for moderation commands (requires permission level 5+)") await ctx.send(embed=embed) @client.hybrid_command() async def modhelp(ctx): """Shows all moderation commands (requires permission level 5 or higher).""" user_data = load_user_data_sync(ctx.author.id, ctx.guild.id) if user_data["permission"] < 5: await ctx.send("❌ You need permission level 5 or higher to view moderation commands.") return embed = discord.Embed( title="🛡️ Moderation Commands - Staff Guide", description="Here are all the moderation commands available to staff members:", color=0xe74c3c, timestamp=datetime.now() ) # Permission Level 5+ (Moderators) embed.add_field( name="👮 Moderator Commands (Level 5+)", value=( "`/warn <user> <reason> [message_id]` - Warn a user (with optional message reference)\n" "`/mute <user> <duration> [reason]` - Mute a user temporarily\n" "`/unmute <user>` - Manually unmute a user\n" "`/modinfo [user]` - View comprehensive user information\n" "`/viewwarn <warning_id>` - View detailed warning information\n" "`/modstats [user]` - View moderation statistics\n" "`/processes <action> [type]` - Manage active processes\n" "`/startgiveaway` - Create server giveaways\n" "`/join` - Make bot join a server\n" "`/leave` - Make bot leave a server" ), inline=False ) # Permission Level 8+ (Admins) if user_data["permission"] >= 8: embed.add_field( name="👑 Admin Commands (Level 8+)", value=( "`/modconfig [setting] [value]` - Configure server moderation settings\n" "`/addpoints <user> <amount>` - Add points to a user\n" "`/resetpoints <user>` - Reset a user's points to 0\n" "`/setlocalpermission <level>` - Set your local permission level\n" "`/addbackgrounddata <data>` - Add data to AI background knowledge\n" "`/toggle_feature <feature> <state>` - Enable/disable bot features" ), inline=False ) # Permission Level 10 (Owner) if user_data["permission"] >= 10: embed.add_field( name="🔧 Owner Commands (Level 10)", value=( "`/shutdown_` - Shutdown the bot\n" "`/owner_command` - Sync slash commands\n" "`/addbackgrounddata` - Modify AI training data\n" "`/toggle_feature` - Control bot features globally" ), inline=False ) # Moderation Configuration Help embed.add_field( name="⚙️ Moderation Configuration", value=( "Use `/modconfig` without parameters to see current settings.\n" "Available settings: mute_role, mute_role_name, auto_create_mute_role,\n" "max_warn_threshold, auto_mute_on_warns, auto_mute_duration,\n" "log_channel, mod_log_enabled" ), inline=False ) # Duration Formats embed.add_field( name="⏱️ Command Examples", value=( "**Mute duration formats:**\n" "`10m` = 10 minutes, `1h` = 1 hour, `2d` = 2 days\n\n" "**Warning with message reference:**\n" "`/warn @user Bad language 1234567890123456789`\n" "The message will be saved even if deleted later." ), inline=False ) # Process Management embed.add_field( name="🔄 Process Management", value=( "`/processes list` - List all active processes\n" "`/processes list mute` - List only mute processes\n" "`/processes cleanup` - Clean up expired processes\n" "`/processes stats` - Show process statistics" ), inline=False ) embed.set_footer(text=f"Your permission level: {user_data['permission']} | Use /help for user commands") await ctx.send(embed=embed) @client.hybrid_command() async def askmultus(ctx, *, prompt: str): """Submits a prompt to Multus for assistance or information. (5 Points)""" if not features["askmultus"]: await ctx.send("Sorry, the askmultus feature is currently disabled.") return user_id = ctx.author.id guild_id = ctx.guild.id # Lade Benutzerdaten aus der MySQL-Datenbank user_data = load_user_data_sync(user_id, guild_id) if user_data["points"] >= 5: user_data["points"] -= 5 # Speichere die aktualisierten Benutzerdaten in der MySQL-Datenbank update_user_data(user_data["user_id"], guild_id, "points", user_data["points"]) # Define the full data and user history field for askmultus introduction = read_introduction() background_data = read_background_data("background_data.txt") current_datetime = get_current_datetime() full_data = introduction + f"\nCurrent Date and Time: {current_datetime}" + background_data user_history_field = "chat_history" # Füge die Anfrage zur Warteschlange hinzu await askmultus_queue.put((ctx, user_data["user_id"], ctx.author.name, prompt, ctx.channel.id, full_data, user_history_field, "local-model")) # Erstelle ein Embed für die Bestätigungsnachricht embed = discord.Embed(title="Multus Assistance Request", color=0x00ff00) embed.add_field(name="Request Received", value=f"Your request has been added to the queue. Position in queue: {askmultus_queue.qsize()}") await ctx.send(embed=embed) else: await ctx.send("You don't have enough points to use this command.") executor = concurrent.futures.ThreadPoolExecutor() async def process_ai_queue(): loop = asyncio.get_running_loop() while True: try: if not askmultus_queue.empty(): ctx, user_id, user_name, prompt, channel_id, full_data, user_history_field, model = await askmultus_queue.get() guild_id = ctx.guild.id user_data = load_user_data_sync(user_id, guild_id) try: user_history = user_data.get(user_history_field, []) # Ensure user_history is a list, not a string if isinstance(user_history, str): try: user_history = json.loads(user_history) except (json.JSONDecodeError, ValueError): user_history = [] elif not isinstance(user_history, list): user_history = [] user_history.append({"role": "user", "content": f"{user_name}: {prompt}"}) messages = [ {"role": "system", "content": full_data}, *user_history ] completion = await loop.run_in_executor(executor, lambda: openai_instance.chat.completions.create( model=model, messages=messages, temperature=0.8, timeout=15, # Limit waiting time for response )) assistant_message = completion.choices[0].message.content channel = client.get_channel(channel_id) # Prepare the embed with split fields if necessary embed = discord.Embed(title="AI Response", color=0x00ff00) embed.add_field(name="Prompt", value=prompt, inline=False) if len(assistant_message) <= 1024: embed.add_field(name="Response", value=assistant_message, inline=False) else: # Split the response into multiple fields if it exceeds 1024 characters parts = [assistant_message[i:i+1024] for i in range(0, len(assistant_message), 1024)] for i, part in enumerate(parts): embed.add_field(name=f"Response Part {i+1}", value=part, inline=False) await channel.send(embed=embed) if ctx.voice_client: # If bot is in a voice channel tts = gTTS(assistant_message, lang="en") tts.save("response.mp3") ctx.voice_client.play(discord.FFmpegPCMAudio("response.mp3")) user_history.append({"role": "assistant", "content": assistant_message}) # Update the relevant user history field update_user_data(user_data["user_id"], guild_id, user_history_field, user_history) except Exception as e: logger.error(f"Processing errors: {e}") finally: askmultus_queue.task_done() except asyncio.CancelledError: break except Exception as e: logger.error(f"Error in process_ai_queue: {e}") await asyncio.sleep(5) @client.hybrid_command() async def vision(ctx, image_url: str): """Analyzes the content of an image.""" if not features["vision"]: await ctx.send("Sorry, the vision feature is currently disabled.") return try: # Read the image and encode it to base64 response = requests.get(image_url) if response.status_code == 200: base64_image = base64.b64encode(response.content).decode("utf-8") else: await ctx.send(f"Failed to retrieve the image from {image_url}.") return # Process the request using OpenAI's Vision model completion = openai_instance.chat.completions.create( model="local-model", messages=[ { "role": "system", "content": "This is a chat between a user and an assistant. The assistant is helping the user to describe an image.", }, { "role": "user", "content": [ {"type": "text", "text": "What’s in this image?"}, {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{base64_image}"}}, ], }, ], max_tokens=1000, stream=True, ) # Send the response to the Discord channel chunks = [] for chunk in completion: if chunk.choices[0].delta.content: chunks.append(chunk.choices[0].delta.content) result = "".join(chunks) await ctx.send(result) except Exception as e: await ctx.send(f"Error analyzing the image: {e}") @client.hybrid_command() async def addbackgrounddata(ctx, *, data: str): """Adds additional background data to the file (Owner only).""" if commands.is_owner(): try: with open("background_data.txt", "a", encoding="utf-8") as file: file.write("\n" + data) await ctx.send("Background data added successfully.") except Exception as e: await ctx.send(f"Error adding background data: {e}") else: await ctx.send("You don't have the necessary permissions to use this command.") @client.hybrid_command() async def summarize(ctx, number: int): """Summarizes the last x messages in the chat (Admin only).""" if not features["summarize"]: await ctx.send("Sorry, the summarize feature is currently disabled.") return guild_id = ctx.guild.id user_perms = load_user_data_sync(ctx.author.id, guild_id) if 5 < user_perms["permission"]: try: # Fetch the last 10 messages in the channel messages = [] async for message in ctx.channel.history(limit=number): messages.append(message) # Extract the content of each message message_contents = [message.content for message in messages] # Join the message contents into a single string messages_combined = "\n".join(message_contents) introduction = read_introduction() full_data = introduction + background_data # Process the combined messages using OpenAI's summarization model completion = openai_instance.chat.completions.create( model="text-davinci-003", # Choose an appropriate summarization model messages=[ {"role": "system", "content": "Summarizing the last x messages in the chat: "}, {"role": "user", "content": messages_combined}, ], max_tokens=1000, stream=False, ) # Extract the summarized text from the completion summary = completion.choices[0].message.content # Send the summarized text to the Discord channel await ctx.send(summary) except Exception as e: await ctx.send(f"An error occurred while summarizing the messages: {e}") else: await ctx.send("You don't have the necessary permissions to use this command.") @client.hybrid_command() async def join(ctx): """Makes the bot join a voice channel.""" if ctx.author.voice: channel = ctx.author.voice.channel await channel.connect() await ctx.send(f"Joined {channel}") else: await ctx.send("You are not connected to a voice channel.") @client.hybrid_command() async def leave(ctx): """Makes the bot leave the voice channel.""" if ctx.voice_client: await ctx.voice_client.disconnect() await ctx.send("Left the voice channel.") else: await ctx.send("I am not in a voice channel.") @client.hybrid_command() async def toggle_feature(ctx, feature: str, state: str): """Allows admin to enable or disable bot features.""" guild_id = ctx.guild.id user_id = ctx.author.id user_data = load_user_data_sync(user_id, guild_id) user_perms = user_data["permission"] if user_perms < 8: # Nur Admins (permission level >= 8) können Funktionen aktivieren/deaktivieren await ctx.send("You do not have the necessary permissions to toggle features.") return global features if feature.lower() not in features: await ctx.send(f"Feature {feature} not found.") return if state.lower() == "on": features[feature.lower()] = True await ctx.send(f"Feature {feature} enabled.") elif state.lower() == "off": features[feature.lower()] = False await ctx.send(f"Feature {feature} disabled.") else: await ctx.send("Please specify 'on' or 'off'.") await ctx.send("Please specify 'on' or 'off'.") @client.hybrid_command() async def processes(ctx, action: str = "list", process_type: str = None): """Manages active processes. Actions: list, cleanup, status""" guild_id = ctx.guild.id user_perms = load_user_data_sync(ctx.author.id, guild_id) if user_perms["permission"] < 8: # Only admins can manage processes await ctx.send("You don't have permission to manage processes.") return if action.lower() == "list": processes = get_active_processes(process_type=process_type, guild_id=guild_id) if not processes: await ctx.send("No active processes found.") return embed = discord.Embed(title="Active Processes", color=0x00ff00) for process in processes[:10]: # Limit to 10 processes process_info = f"Type: {process['process_type']}\n" process_info += f"Status: {process['status']}\n" if process['end_time']: time_left = process['end_time'] - datetime.now() if time_left.total_seconds() > 0: process_info += f"Time left: {time_left}\n" else: process_info += "**EXPIRED**\n" if process['data']: data = process['data'] if 'title' in data: process_info += f"Title: {data['title']}\n" if 'participant_count' in data: process_info += f"Participants: {data['participant_count']}\n" embed.add_field( name=f"{process['process_type'].title()} - {process['uuid'][:8]}...", value=process_info, inline=True ) if len(processes) > 10: embed.set_footer(text=f"Showing 10 of {len(processes)} processes") await ctx.send(embed=embed) elif action.lower() == "cleanup": expired = cleanup_expired_processes() await ctx.send(f"Cleaned up {len(expired)} expired processes.") elif action.lower() == "status": active_count = len(get_active_processes(status="active", guild_id=guild_id)) completed_count = len(get_active_processes(status="completed", guild_id=guild_id)) expired_count = len(get_active_processes(status="expired", guild_id=guild_id)) embed = discord.Embed(title="Process Status", color=0x3498db) embed.add_field(name="Active", value=str(active_count), inline=True) embed.add_field(name="Completed", value=str(completed_count), inline=True) embed.add_field(name="Expired", value=str(expired_count), inline=True) embed.add_field(name="Process Manager", value="Running" if process_manager.is_running() else "Stopped", inline=False) await ctx.send(embed=embed) else: await ctx.send("Invalid action. Use: list, cleanup, or status") @client.hybrid_command() async def version(ctx): """Displays the current version of the bot.""" await ctx.send(f"The current version of the bot is: {__version__}") # ================================ GUILD SETTINGS SYSTEM ================================ def get_guild_settings(guild_id): """Lädt die Guild-Einstellungen aus der Datenbank""" connection = None cursor = None try: connection = connect_to_database() cursor = connection.cursor() select_query = "SELECT * FROM guild_settings WHERE guild_id = %s" cursor.execute(select_query, (guild_id,)) result = cursor.fetchone() if result: return { "guild_id": result[0], "mute_role_id": result[1], "mute_role_name": result[2] or "Muted", "auto_create_mute_role": bool(result[3]) if result[3] is not None else True, "max_warn_threshold": result[4] or 3, "auto_mute_on_warns": bool(result[5]) if result[5] is not None else False, "auto_mute_duration": result[6] or "1h", "log_channel_id": result[7], "mod_log_enabled": bool(result[8]) if result[8] is not None else True } else: # Erstelle Default-Einstellungen default_settings = { "guild_id": guild_id, "mute_role_id": None, "mute_role_name": "Muted", "auto_create_mute_role": True, "max_warn_threshold": 3, "auto_mute_on_warns": False, "auto_mute_duration": "1h", "log_channel_id": None, "mod_log_enabled": True } save_guild_settings(guild_id, default_settings) return default_settings except Exception as e: logger.error(f"Error loading guild settings: {e}") # Return default settings on error return { "guild_id": guild_id, "mute_role_id": None, "mute_role_name": "Muted", "auto_create_mute_role": True, "max_warn_threshold": 3, "auto_mute_on_warns": False, "auto_mute_duration": "1h", "log_channel_id": None, "mod_log_enabled": True } finally: if cursor: cursor.close() if connection: close_database_connection(connection) def save_guild_settings(guild_id, settings): """Speichert Guild-Einstellungen in der Datenbank""" connection = None cursor = None try: connection = connect_to_database() cursor = connection.cursor() insert_query = """ INSERT INTO guild_settings (guild_id, mute_role_id, mute_role_name, auto_create_mute_role, max_warn_threshold, auto_mute_on_warns, auto_mute_duration, log_channel_id, mod_log_enabled) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) ON DUPLICATE KEY UPDATE mute_role_id = VALUES(mute_role_id), mute_role_name = VALUES(mute_role_name), auto_create_mute_role = VALUES(auto_create_mute_role), max_warn_threshold = VALUES(max_warn_threshold), auto_mute_on_warns = VALUES(auto_mute_on_warns), auto_mute_duration = VALUES(auto_mute_duration), log_channel_id = VALUES(log_channel_id), mod_log_enabled = VALUES(mod_log_enabled) """ cursor.execute(insert_query, ( guild_id, settings.get("mute_role_id"), settings.get("mute_role_name", "Muted"), settings.get("auto_create_mute_role", True), settings.get("max_warn_threshold", 3), settings.get("auto_mute_on_warns", False), settings.get("auto_mute_duration", "1h"), settings.get("log_channel_id"), settings.get("mod_log_enabled", True) )) connection.commit() logger.info(f"Guild settings saved for guild {guild_id}") except Exception as e: logger.error(f"Error saving guild settings: {e}") if connection: connection.rollback() finally: if cursor: cursor.close() if connection: close_database_connection(connection) async def get_or_create_mute_role(guild, settings): """Holt oder erstellt die Mute-Rolle basierend auf Guild-Einstellungen""" mute_role = None # Versuche zuerst über ID zu finden if settings["mute_role_id"]: mute_role = guild.get_role(settings["mute_role_id"]) if mute_role: return mute_role # Versuche über Name zu finden mute_role = discord.utils.get(guild.roles, name=settings["mute_role_name"]) if mute_role: # Update die ID in den Einstellungen settings["mute_role_id"] = mute_role.id save_guild_settings(guild.id, settings) return mute_role # Erstelle neue Rolle, falls auto_create_mute_role aktiviert ist if settings["auto_create_mute_role"]: try: mute_role = await guild.create_role( name=settings["mute_role_name"], color=discord.Color.dark_gray(), reason="Auto-created mute role for moderation system" ) # Konfiguriere Berechtigungen für alle Kanäle for channel in guild.channels: try: await channel.set_permissions( mute_role, send_messages=False, speak=False, add_reactions=False, create_private_threads=False, create_public_threads=False, send_messages_in_threads=False ) except discord.Forbidden: logger.warning(f"Could not set permissions for {channel.name} in guild {guild.id}") continue # Speichere die neue Rolle-ID settings["mute_role_id"] = mute_role.id save_guild_settings(guild.id, settings) logger.info(f"Created mute role '{settings['mute_role_name']}' for guild {guild.id}") return mute_role except discord.Forbidden: logger.error(f"No permission to create mute role in guild {guild.id}") return None return None # ================================ MODERATION SYSTEM ================================ # Moderation Helper Functions def check_moderation_permission(user_permission): """Checks if the user has moderation rights (Permission 5 or higher)""" return user_permission >= 5 async def log_moderation_action(guild, action_type, moderator, target_user, reason, duration=None, additional_info=None): """Logs moderation actions to the configured log channel""" try: guild_settings = get_guild_settings(guild.id) # Check if logging is enabled and channel is configured if not guild_settings["mod_log_enabled"] or not guild_settings["log_channel_id"]: return log_channel = guild.get_channel(guild_settings["log_channel_id"]) if not log_channel: logger.warning(f"Log channel {guild_settings['log_channel_id']} not found in guild {guild.id}") return # Create log embed color_map = { "warn": 0xff9500, "mute": 0xff0000, "unmute": 0x00ff00, "kick": 0xff6600, "ban": 0x8b0000, "unban": 0x00ff00 } embed = discord.Embed( title=f"🛡️ Moderation Action: {action_type.title()}", color=color_map.get(action_type.lower(), 0x3498db), timestamp=datetime.now() ) embed.add_field(name="👤 Target User", value=f"{target_user.mention}\n`{target_user.id}`", inline=True) embed.add_field(name="👮 Moderator", value=f"{moderator.mention}\n`{moderator.id}`", inline=True) embed.add_field(name="📝 Reason", value=reason, inline=True) if duration: embed.add_field(name="⏱️ Duration", value=duration, inline=True) if additional_info: for key, value in additional_info.items(): embed.add_field(name=key, value=value, inline=True) embed.set_thumbnail(url=target_user.display_avatar.url) embed.set_footer(text=f"Action ID: {guild.id}-{target_user.id}-{int(datetime.now().timestamp())}") await log_channel.send(embed=embed) logger.info(f"Logged {action_type} action for user {target_user.id} in guild {guild.id}") except Exception as e: logger.error(f"Error logging moderation action: {e}") async def save_warning_to_database(user_id, guild_id, moderator_id, reason, timestamp=None, message_data=None, message_id=None): """Saves individual warning records to the database with optional message data and context""" connection = None cursor = None try: connection = connect_to_database() cursor = connection.cursor() if timestamp is None: timestamp = datetime.now() # Prepare message data if provided message_id_db = message_id # Use provided message_id message_content = None message_attachments = None message_author_id = None message_channel_id = None context_messages = None if message_data: if isinstance(message_data, dict) and "main_message" in message_data: # New format with context messages main_msg = message_data.get("main_message") if main_msg: message_id_db = main_msg.get('id') message_content = main_msg.get('content') message_attachments = main_msg.get('attachments') message_author_id = main_msg.get('author_id') message_channel_id = main_msg.get('channel_id') # Store all context messages as JSON context_messages = json.dumps(message_data.get("context_messages", [])) else: # Old format - single message message_id_db = message_data.get('id') message_content = message_data.get('content') message_attachments = message_data.get('attachments') # JSON string message_author_id = message_data.get('author_id') message_channel_id = message_data.get('channel_id') insert_query = """ INSERT INTO user_warnings (user_id, guild_id, moderator_id, reason, message_id, message_content, message_attachments, message_author_id, message_channel_id, context_messages, created_at) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) """ cursor.execute(insert_query, ( user_id, guild_id, moderator_id, reason, message_id_db, message_content, message_attachments, message_author_id, message_channel_id, context_messages, timestamp )) connection.commit() logger.info(f"Saved warning record for user {user_id} in guild {guild_id} with context") return cursor.lastrowid except Exception as e: logger.error(f"Error saving warning to database: {e}") if connection: connection.rollback() return None finally: if cursor: cursor.close() if connection: close_database_connection(connection) def create_warnings_table(): """Creates the user_warnings table if it doesn't exist""" connection = None cursor = None try: connection = connect_to_database() cursor = connection.cursor() create_table_query = """ CREATE TABLE IF NOT EXISTS user_warnings ( id INT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL, guild_id BIGINT NOT NULL, moderator_id BIGINT NOT NULL, reason TEXT NOT NULL, message_id BIGINT NULL, message_content LONGTEXT NULL, message_attachments LONGTEXT NULL, message_author_id BIGINT NULL, message_channel_id BIGINT NULL, context_messages LONGTEXT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_user_guild (user_id, guild_id), INDEX idx_created_at (created_at), INDEX idx_message_id (message_id) ) """ cursor.execute(create_table_query) # Add new columns if they don't exist (for existing databases) alter_queries = [ "ALTER TABLE user_warnings ADD COLUMN message_id BIGINT NULL", "ALTER TABLE user_warnings ADD COLUMN message_content LONGTEXT NULL", "ALTER TABLE user_warnings ADD COLUMN message_attachments LONGTEXT NULL", "ALTER TABLE user_warnings ADD COLUMN message_author_id BIGINT NULL", "ALTER TABLE user_warnings ADD COLUMN message_channel_id BIGINT NULL", "ALTER TABLE user_warnings ADD COLUMN context_messages LONGTEXT NULL", "ALTER TABLE user_warnings ADD INDEX idx_message_id (message_id)" ] for alter_query in alter_queries: try: cursor.execute(alter_query) except Exception: # Column already exists, ignore error pass connection.commit() logger.info("User warnings table checked/created successfully") except Exception as e: logger.error(f"Error creating warnings table: {e}") finally: if cursor: cursor.close() if connection: close_database_connection(connection) async def get_user_warnings(user_id, guild_id): """Retrieves all warning records for a user""" connection = None cursor = None try: connection = connect_to_database() cursor = connection.cursor() select_query = """ SELECT id, moderator_id, reason, created_at, message_id, message_content, message_attachments, message_author_id, message_channel_id, context_messages FROM user_warnings WHERE user_id = %s AND guild_id = %s ORDER BY created_at DESC """ cursor.execute(select_query, (user_id, guild_id)) results = cursor.fetchall() warnings = [] for row in results: warnings.append({ "id": row[0], "moderator_id": row[1], "reason": row[2], "created_at": row[3], "message_id": row[4], "message_content": row[5], "message_attachments": row[6], "message_author_id": row[7], "message_channel_id": row[8], "context_messages": row[9] }) return warnings except Exception as e: logger.error(f"Error retrieving user warnings: {e}") return [] finally: if cursor: cursor.close() if connection: close_database_connection(connection) async def get_message_data(channel, message_id, context_range=3): """Retrieves and processes message data for warning documentation with context messages""" try: # Get the main message main_message = await channel.fetch_message(message_id) # Get context messages (before and after) context_messages = [] try: # Get messages around the target message async for msg in channel.history(limit=context_range * 2 + 1, around=main_message.created_at): context_messages.append(msg) # Sort messages by timestamp context_messages.sort(key=lambda m: m.created_at) except Exception as e: logger.warning(f"Could not fetch context messages: {e}") context_messages = [main_message] # Process all messages (main + context) all_messages_data = [] for message in context_messages: # Process attachments for this message attachments_data = [] for attachment in message.attachments: attachment_info = { "filename": attachment.filename, "url": attachment.url, "proxy_url": attachment.proxy_url, "size": attachment.size, "content_type": attachment.content_type } # Download and encode image attachments for permanent storage if attachment.content_type and attachment.content_type.startswith('image/'): try: import aiohttp import base64 async with aiohttp.ClientSession() as session: async with session.get(attachment.url) as response: if response.status == 200 and len(await response.read()) < 8 * 1024 * 1024: # Max 8MB image_data = await response.read() attachment_info["data"] = base64.b64encode(image_data).decode('utf-8') except Exception as e: logger.warning(f"Could not download attachment {attachment.filename}: {e}") attachments_data.append(attachment_info) # Process embeds for this message embeds_data = [] for embed in message.embeds: embed_info = { "title": embed.title, "description": embed.description, "url": embed.url, "color": embed.color.value if embed.color else None, "timestamp": embed.timestamp.isoformat() if embed.timestamp else None } embeds_data.append(embed_info) # Create message data msg_data = { "id": message.id, "content": message.content, "author_id": message.author.id, "author_name": message.author.display_name, "author_username": message.author.name, "channel_id": message.channel.id, "attachments": json.dumps(attachments_data) if attachments_data else None, "embeds": json.dumps(embeds_data) if embeds_data else None, "created_at": message.created_at.isoformat(), "edited_at": message.edited_at.isoformat() if message.edited_at else None, "message_type": str(message.type), "flags": message.flags.value if message.flags else 0, "is_main_message": message.id == message_id # Mark the main referenced message } all_messages_data.append(msg_data) # Return structured data with main message and context return { "main_message": next((msg for msg in all_messages_data if msg["is_main_message"]), None), "context_messages": all_messages_data, "context_range": context_range, "total_messages": len(all_messages_data) } return message_data except discord.NotFound: logger.warning(f"Message {message_id} not found") return None except discord.Forbidden: logger.warning(f"No permission to access message {message_id}") return None except Exception as e: logger.error(f"Error retrieving message data: {e}") return None async def save_user_roles(user_id, guild_id, roles): """Saves a user's roles before a mute""" connection = None cursor = None try: connection = connect_to_database() cursor = connection.cursor() # Serialize the role IDs role_ids = [str(role.id) for role in roles if not role.is_default()] serialized_roles = json.dumps(role_ids) insert_query = """ INSERT INTO user_saved_roles (user_id, guild_id, roles, saved_at) VALUES (%s, %s, %s, %s) ON DUPLICATE KEY UPDATE roles = %s, saved_at = %s """ current_time = datetime.now() cursor.execute(insert_query, (user_id, guild_id, serialized_roles, current_time, serialized_roles, current_time)) connection.commit() logger.info(f"Saved roles for user {user_id} in guild {guild_id}") except Exception as e: logger.error(f"Error saving user roles: {e}") if connection: connection.rollback() finally: if cursor: cursor.close() if connection: close_database_connection(connection) async def restore_user_roles(user, guild): """Restores a user's saved roles""" connection = None cursor = None try: connection = connect_to_database() cursor = connection.cursor() select_query = "SELECT roles FROM user_saved_roles WHERE user_id = %s AND guild_id = %s" cursor.execute(select_query, (user.id, guild.id)) result = cursor.fetchone() if result: role_ids = json.loads(result[0]) roles_to_add = [] for role_id in role_ids: role = guild.get_role(int(role_id)) if role and role < guild.me.top_role: # Check if bot can assign the role roles_to_add.append(role) if roles_to_add: await user.add_roles(*roles_to_add, reason="Mute expired - restoring roles") logger.info(f"Restored {len(roles_to_add)} roles for user {user.id}") # Delete the saved roles delete_query = "DELETE FROM user_saved_roles WHERE user_id = %s AND guild_id = %s" cursor.execute(delete_query, (user.id, guild.id)) connection.commit() except Exception as e: logger.error(f"Error restoring user roles: {e}") if connection: connection.rollback() finally: if cursor: cursor.close() if connection: close_database_connection(connection) @client.hybrid_command() async def warn(ctx, user: discord.User, reason: str = "No reason provided", message_id: str = None, context_range: int = 3): """Warns a user (Requires Permission Level 5 or higher) Usage: /warn @user "Inappropriate behavior" /warn @user "Bad language" 1407754702564884622 /warn @user "Spam" 1407754702564884622 15 Parameters: - user: The user to warn - reason: Reason for the warning - message_id: Optional message ID to reference (required for context_range) - context_range: Number of messages before/after to archive (only works with message_id) Note: context_range parameter only works when message_id is also provided! """ # Check if it's a slash command and defer if needed is_slash_command = hasattr(ctx, 'interaction') and ctx.interaction if is_slash_command: await ctx.defer() # Helper function for sending responses async def send_response(content=None, embed=None, ephemeral=False, file=None): try: if is_slash_command: await ctx.followup.send(content=content, embed=embed, ephemeral=ephemeral, file=file) else: await ctx.send(content=content, embed=embed, file=file) except Exception as e: logger.error(f"Error sending response: {e}") # Fallback to regular send if followup fails try: await ctx.send(content=content, embed=embed, file=file) except: pass try: # Parse message ID and context range from reason if provided inline original_reason = reason # Check if reason ends with potential message ID and/or context range reason_words = reason.split() parsed_message_id = message_id parsed_context_range = 3 # Default context range, only used if message_id is provided # Look for patterns like "reason 1234567890123456789" or "reason 1234567890123456789 15" if len(reason_words) >= 2: # Check if last word is a number (could be context range) if reason_words[-1].isdigit() and len(reason_words[-1]) <= 3: potential_context = int(reason_words[-1]) if 1 <= potential_context <= 25: # Valid context range parsed_context_range = potential_context reason_words = reason_words[:-1] # Remove context range from reason # Check if last word (after removing context) is a message ID if len(reason_words) >= 2 and len(reason_words[-1]) >= 17 and len(reason_words[-1]) <= 20 and reason_words[-1].isdigit(): parsed_message_id = reason_words[-1] reason_words = reason_words[:-1] # Remove message ID from reason # Update reason without the message ID and context range reason = " ".join(reason_words) # Only use context_range parameter if message_id is also provided if parsed_message_id and message_id and context_range != 3: # If message_id was provided as parameter and context_range was also set, use it parsed_context_range = context_range elif not parsed_message_id: # If no message_id was found, reset context_range to default parsed_context_range = 3 # Validate and limit context range if parsed_context_range < 1: parsed_context_range = 1 elif parsed_context_range > 25: parsed_context_range = 25 # message_data will be populated if message_id is provided message_data = None # Try to get message data if message ID was provided if parsed_message_id: # Convert message_id string to int try: message_id_int = int(parsed_message_id) except ValueError: await send_response(content=f"❌ Invalid message ID: {parsed_message_id}") return # Try to get message data from current channel first with specified context range message_data = await get_message_data(ctx.channel, message_id_int, parsed_context_range) # If not found in current channel, try other channels the bot can access if message_data is None: # Limit search to avoid spam - only check first 10 channels channels_to_check = ctx.guild.text_channels[:10] for channel in channels_to_check: if channel.id == ctx.channel.id: continue # Skip current channel, already checked try: message_data = await get_message_data(channel, message_id_int, parsed_context_range) if message_data is not None: break except discord.Forbidden: continue # Load moderator data mod_data = await load_user_data(ctx.author.id, ctx.guild.id) # Check moderation rights if not check_moderation_permission(mod_data["permission"]): embed = discord.Embed( title="❌ Insufficient Permissions", description="You need moderation permissions (Level 5 or higher) to use this command.", color=0xff0000 ) await send_response(embed=embed, ephemeral=True) return # Cannot warn yourself if user.id == ctx.author.id: embed = discord.Embed( title="❌ Invalid Action", description="You cannot warn yourself!", color=0xff0000 ) await send_response(embed=embed, ephemeral=True) return # Check if target has higher permissions target_data = await load_user_data(user.id, ctx.guild.id) if target_data["permission"] >= mod_data["permission"]: embed = discord.Embed( title="❌ Insufficient Permissions", description="You cannot warn someone with equal or higher permissions than you.", color=0xff0000 ) await send_response(embed=embed, ephemeral=True) return # Increase warn count target_data["warns"] += 1 update_user_data(user.id, ctx.guild.id, "warns", target_data["warns"]) # Save detailed warning record to database warning_id = await save_warning_to_database( user_id=user.id, guild_id=ctx.guild.id, moderator_id=ctx.author.id, reason=reason, message_data=message_data, message_id=message_id_int if message_id else None ) # Get guild settings for threshold checking guild_settings = get_guild_settings(ctx.guild.id) warn_threshold = guild_settings.get("max_warn_threshold", 3) # Create embed embed = discord.Embed( title="⚠️ Warning Issued", description=f"{user.mention} has been warned.", color=0xff9500, timestamp=datetime.now() ) embed.add_field(name="📝 Reason", value=reason, inline=False) embed.add_field(name="👮 Moderator", value=ctx.author.mention, inline=True) embed.add_field(name="⚠️ Warning Count", value=f"{target_data['warns']}/{warn_threshold}", inline=True) if warning_id: embed.add_field(name="🆔 Warning ID", value=str(warning_id), inline=True) # Add message information if available if message_data: # Handle new context message format if isinstance(message_data, dict) and "main_message" in message_data: main_msg = message_data.get("main_message") context_msgs = message_data.get("context_messages", []) if main_msg: message_info = f"**Message ID:** `{main_msg['id']}`\n" message_info += f"**Channel:** <#{main_msg['channel_id']}>\n" message_info += f"**Author:** {main_msg['author_name']}\n" if main_msg['content']: content_preview = main_msg['content'] if len(content_preview) > 100: content_preview = content_preview[:97] + "..." message_info += f"**Content:** {content_preview}\n" # Show attachment info if main_msg['attachments']: attachments = json.loads(main_msg['attachments']) if attachments: attachment_names = [att['filename'] for att in attachments[:3]] message_info += f"**Attachments:** {', '.join(attachment_names)}" if len(attachments) > 3: message_info += f" +{len(attachments) - 3} more" embed.add_field(name="📄 Referenced Message", value=message_info, inline=False) else: # Handle old format for backward compatibility message_info = f"**Message ID:** `{message_data.get('id', 'Unknown')}`\n" message_info += f"**Channel:** <#{message_data.get('channel_id', 'Unknown')}>\n" if message_data.get('content'): content_preview = message_data['content'] if len(content_preview) > 100: content_preview = content_preview[:97] + "..." message_info += f"**Content:** {content_preview}\n" embed.add_field(name="📄 Referenced Message", value=message_info, inline=False) elif message_id: embed.add_field( name="⚠️ Message Not Found", value=f"Could not retrieve message `{message_id}` (deleted or no permission)", inline=False ) embed.set_footer(text=f"User ID: {user.id}") embed.set_thumbnail(url=user.display_avatar.url) # Check if user has reached the warning threshold if target_data['warns'] >= warn_threshold: auto_mute_enabled = guild_settings.get("auto_mute_on_warns", False) if auto_mute_enabled: mute_role_id = guild_settings.get("mute_role_id") if mute_role_id and ctx.guild.get_role(mute_role_id): mute_role = ctx.guild.get_role(mute_role_id) try: member = ctx.guild.get_member(user.id) if member: await member.add_roles(mute_role, reason=f"Automatic mute: {warn_threshold} warnings reached") embed.add_field( name="🔇 Automatic Action", value=f"User has been automatically muted for reaching {warn_threshold} warnings.", inline=False ) # Log the automatic mute action await log_moderation_action( guild=ctx.guild, action_type="mute", moderator=ctx.author, target_user=user, reason=f"Automatic mute: {warn_threshold} warnings reached", additional_info={"Trigger": f"Warning #{target_data['warns']}"} ) except discord.Forbidden: embed.add_field( name="⚠️ Warning", value="Could not automatically mute user due to insufficient permissions.", inline=False ) else: embed.add_field( name="🚨 Threshold Reached", value=f"User has reached the warning threshold ({warn_threshold} warnings). Consider further action.", inline=False ) await send_response(embed=embed) # Log the warning action log_additional_info = { "Warning Count": f"{target_data['warns']}/{warn_threshold}", "Warning ID": str(warning_id) if warning_id else "N/A" } if message_data: log_additional_info["Referenced Message"] = f"ID: {message_data['id']}" log_additional_info["Message Channel"] = f"<#{message_data['channel_id']}>" if message_data['content']: content_preview = message_data['content'][:200] + "..." if len(message_data['content']) > 200 else message_data['content'] log_additional_info["Message Content"] = content_preview await log_moderation_action( guild=ctx.guild, action_type="warn", moderator=ctx.author, target_user=user, reason=reason, additional_info=log_additional_info ) # Try to DM the user try: dm_embed = discord.Embed( title=f"⚠️ Warning from {ctx.guild.name}", color=0xff9500, timestamp=datetime.now() ) dm_embed.add_field(name="👮 Moderator", value=ctx.author.display_name, inline=True) dm_embed.add_field(name="📝 Reason", value=reason, inline=False) dm_embed.add_field(name="⚠️ Total Warnings", value=f"{target_data['warns']}/{warn_threshold}", inline=True) if target_data['warns'] >= warn_threshold: dm_embed.add_field( name="🔇 Additional Action", value="You have reached the warning threshold. Further violations may result in more severe punishments.", inline=False ) await user.send(embed=dm_embed) except discord.Forbidden: # User has DMs disabled pass # Log the action logger.info(f"User {user.id} warned by {ctx.author.id} in guild {ctx.guild.id}. Reason: {reason}") except Exception as e: logger.error(f"Error in warn command: {e}") embed = discord.Embed( title="❌ Error", description="An error occurred while processing the warning. Please try again.", color=0xff0000 ) await send_response(embed=embed) async def get_active_mute_info(user_id, guild_id): """Gets information about active mute for a user""" try: connection = connect_to_database() cursor = connection.cursor() # Check for active mute process select_query = """ SELECT uuid, end_time, data, created_at FROM active_processes WHERE process_type = 'mute' AND status = 'active' AND user_id = %s AND guild_id = %s AND end_time > NOW() ORDER BY created_at DESC LIMIT 1 """ cursor.execute(select_query, (user_id, guild_id)) result = cursor.fetchone() if result: uuid, end_time, data_json, created_at = result data = json.loads(data_json) if data_json else {} return { "uuid": uuid, "end_time": end_time, "reason": data.get("reason", "No reason provided"), "moderator_id": data.get("moderator_id"), "created_at": created_at } cursor.close() close_database_connection(connection) return None except Exception as e: logger.error(f"Error checking active mute: {e}") return None @client.hybrid_command() async def account(ctx, user: discord.User = None): """Shows comprehensive account status including warnings, mutes, and current punishments""" # Check if it's a slash command and defer if needed is_slash_command = hasattr(ctx, 'interaction') and ctx.interaction if is_slash_command: await ctx.defer() # Helper function for sending responses async def send_response(content=None, embed=None, ephemeral=False, file=None): try: if is_slash_command: await ctx.followup.send(content=content, embed=embed, ephemeral=ephemeral, file=file) else: await ctx.send(content=content, embed=embed, file=file) except Exception as e: logger.error(f"Error sending response: {e}") # Fallback to regular send if followup fails try: await ctx.send(content=content, embed=embed, file=file) except: pass try: # Determine target user (self or specified user for moderators) target_user = user if user else ctx.author is_self_check = target_user.id == ctx.author.id # If checking another user, verify moderation permissions if not is_self_check: mod_data = await load_user_data(ctx.author.id, ctx.guild.id) if not check_moderation_permission(mod_data["permission"]): embed = discord.Embed( title="❌ Insufficient Permissions", description="You need moderation permissions (Level 5 or higher) to check other users' accounts.", color=0xff0000 ) await send_response(embed=embed, ephemeral=True) user_data = await load_user_data(target_user.id, ctx.guild.id) embed = discord.Embed( title=f"📊 Account Status: {target_user.display_name}", description=f"Complete account overview for {target_user.mention}", color=0x3498db, timestamp=datetime.now() ) # Get guild settings for thresholds guild_settings = get_guild_settings(ctx.guild.id) # Get detailed warning records warning_records = await get_user_warnings(target_user.id, ctx.guild.id) # Warning information with threshold warn_threshold = guild_settings.get("max_warn_threshold", 3) warn_color = "🟢" if user_data["warns"] == 0 else "🟡" if user_data["warns"] < warn_threshold else "🔴" embed.add_field( name=f"{warn_color} Warnings", value=f"**{user_data['warns']}/{warn_threshold}** warnings", inline=True ) # Mute information mute_color = "🟢" if user_data["mutes"] == 0 else "🟡" if user_data["mutes"] < 3 else "🔴" embed.add_field( name=f"{mute_color} Mutes", value=f"**{user_data['mutes']}** mute(s)", inline=True ) # AI Ban information ai_ban_color = "🟢" if user_data["ai_ban"] == 0 else "🔴" embed.add_field( name=f"{ai_ban_color} AI Violations", value=f"**{user_data['ai_ban']}** violation(s)", inline=True ) # Check for active mute active_mute_info = await get_active_mute_info(target_user.id, ctx.guild.id) if active_mute_info: mute_text = f"🔇 **CURRENTLY MUTED**\n" mute_text += f"Ends: <t:{int(active_mute_info['end_time'].timestamp())}:R>\n" mute_text += f"Reason: {active_mute_info['reason'][:50]}{'...' if len(active_mute_info['reason']) > 50 else ''}" embed.add_field(name="🚨 Active Punishment", value=mute_text, inline=False) # Status assessment total_infractions = user_data["warns"] + user_data["mutes"] + user_data["ai_ban"] if active_mute_info: status = "🔇 **Currently Muted** - Active punishment" status_color = 0xff0000 elif total_infractions == 0: status = "✅ **Clean Record** - No infractions" status_color = 0x00ff00 elif total_infractions <= 2: status = "⚠️ **Minor Infractions** - Stay careful" status_color = 0xffa500 elif total_infractions <= 5: status = "🔶 **Multiple Infractions** - Improve behavior" status_color = 0xff6600 else: status = "🔴 **High Risk** - Serious moderation attention" status_color = 0xff0000 embed.add_field(name="📈 Account Status", value=status, inline=False) embed.color = status_color # Detailed warning history (show last 5 warnings) if warning_records: warning_history = "**Recent Warning History:**\n" display_count = min(5, len(warning_records)) for i in range(display_count): record = warning_records[i] moderator = ctx.guild.get_member(record["moderator_id"]) mod_name = moderator.display_name if moderator else f"ID: {record['moderator_id']}" # Format date warning_date = record["created_at"].strftime("%d.%m.%Y %H:%M") # Truncate reason if too long reason = record["reason"] if len(reason) > 40: reason = reason[:37] + "..." warning_line = f"`{warning_date}` **{mod_name}**: {reason}" # Add message indicator if warning was linked to a message if record.get("message_id"): warning_line += " 📄" warning_history += warning_line + "\n" if len(warning_records) > 5: warning_history += f"\n*... and {len(warning_records) - 5} more warning(s)*" embed.add_field(name="📋 Warning Details", value=warning_history, inline=False) # Account details member = ctx.guild.get_member(target_user.id) if member: account_details = "" account_details += f"**Joined Server:** <t:{int(member.joined_at.timestamp())}:D>\n" account_details += f"**Account Created:** <t:{int(target_user.created_at.timestamp())}:D>\n" account_details += f"**Permission Level:** {user_data['permission']}\n" account_details += f"**XP:** {user_data.get('xp', 0)} (Level {user_data.get('level', 1)})" embed.add_field(name="👤 Account Info", value=account_details, inline=True) # Role information if member.roles[1:]: # Exclude @everyone role_list = [role.mention for role in member.roles[1:]][:5] # Show max 5 roles role_text = ", ".join(role_list) if len(member.roles) > 6: role_text += f" +{len(member.roles) - 6} more" embed.add_field(name="🎭 Roles", value=role_text, inline=True) # Get guild settings for thresholds threshold_info = f"Warning threshold: **{warn_threshold}** warnings" auto_mute_enabled = guild_settings.get("auto_mute_on_warns", False) if auto_mute_enabled: auto_mute_duration = guild_settings.get("auto_mute_duration", "1 hour") threshold_info += f"\nAuto-mute: **{auto_mute_duration}** (at {warn_threshold} warnings)" embed.add_field(name="⚙️ Server Settings", value=threshold_info, inline=False) # Tips for improvement (only show for users with infractions and when checking self) if total_infractions > 0 and is_self_check: tips = ( "💡 **Tips to improve:**\n" "• Follow server rules carefully\n" "• Be respectful to other members\n" "• Ask moderators if you're unsure about something\n" "• Avoid spam and inappropriate content" ) embed.add_field(name="Improvement Guide", value=tips, inline=False) embed.set_thumbnail(url=target_user.display_avatar.url) embed.set_footer(text=f"User ID: {target_user.id} | Checked by: {ctx.author.display_name}") await send_response(embed=embed) except Exception as e: logger.error(f"Error in account command: {e}") await send_response(content="❌ An error occurred while retrieving account information.") @client.hybrid_command() async def mywarns(ctx): """Shows your account status (alias for /account)""" await account(ctx, user=None) @client.hybrid_command() async def modinfo(ctx, user: discord.User = None): """Shows comprehensive moderation information about a user (Requires Permission Level 5 or higher)""" try: # Load moderator data mod_data = await load_user_data(ctx.author.id, ctx.guild.id) # Check moderation rights if not check_moderation_permission(mod_data["permission"]): embed = discord.Embed( title="❌ Insufficient Permissions", description="You need moderation permissions (Level 5 or higher) to use this command.", color=0xff0000 ) await ctx.send(embed=embed, ephemeral=True) return # Default to command author if no user specified if user is None: user = ctx.author # Get member object for additional Discord info member = ctx.guild.get_member(user.id) # Load user data user_data = await load_user_data(user.id, ctx.guild.id) # Get guild settings guild_settings = get_guild_settings(ctx.guild.id) warn_threshold = guild_settings.get("max_warn_threshold", 3) # Get detailed warning records warning_records = await get_user_warnings(user.id, ctx.guild.id) # Create main embed embed = discord.Embed( title="🛡️ Moderation Information", description=f"Comprehensive data for {user.mention}", color=0x3498db, timestamp=datetime.now() ) # Basic user information user_info = f"**Username:** {user.name}\n" user_info += f"**Display Name:** {user.display_name}\n" user_info += f"**User ID:** `{user.id}`\n" if member: user_info += f"**Joined Server:** <t:{int(member.joined_at.timestamp())}:F>\n" user_info += f"**Joined Discord:** <t:{int(user.created_at.timestamp())}:F>\n" # Account age account_age = datetime.now(user.created_at.tzinfo) - user.created_at server_age = datetime.now(member.joined_at.tzinfo) - member.joined_at user_info += f"**Account Age:** {account_age.days} days\n" user_info += f"**Server Membership:** {server_age.days} days" else: user_info += f"**Created:** <t:{int(user.created_at.timestamp())}:F>\n" user_info += "**Status:** Not in server" embed.add_field(name="👤 User Information", value=user_info, inline=True) # Permission and role information perm_info = f"**Permission Level:** {user_data['permission']}\n" if member: # Get roles (excluding @everyone) roles = [role for role in member.roles if not role.is_default()] if roles: role_list = ", ".join([role.mention for role in roles[:10]]) # Limit to 10 roles if len(roles) > 10: role_list += f" +{len(roles) - 10} more" perm_info += f"**Roles ({len(roles)}):** {role_list}\n" else: perm_info += "**Roles:** None\n" # Highest role highest_role = member.top_role perm_info += f"**Highest Role:** {highest_role.mention}" else: perm_info += "**Roles:** User not in server" embed.add_field(name="🎭 Permissions & Roles", value=perm_info, inline=True) # Moderation statistics mod_stats = f"**Warnings:** {user_data['warns']}/{warn_threshold}\n" mod_stats += f"**Mutes:** {user_data['mutes']}\n" mod_stats += f"**AI Violations:** {user_data['ai_ban']}\n" # Calculate total infractions total_infractions = user_data["warns"] + user_data["mutes"] + user_data["ai_ban"] mod_stats += f"**Total Infractions:** {total_infractions}" # Risk assessment if total_infractions == 0: risk_level = "🟢 Low Risk" embed.color = 0x00ff00 elif total_infractions <= 2: risk_level = "🟡 Medium Risk" embed.color = 0xffa500 elif total_infractions <= 5: risk_level = "🟠 High Risk" embed.color = 0xff6600 else: risk_level = "🔴 Critical Risk" embed.color = 0xff0000 mod_stats += f"\n**Risk Level:** {risk_level}" embed.add_field(name="📊 Moderation Statistics", value=mod_stats, inline=False) # Recent warning history (last 3 warnings) if warning_records: warning_history = "" display_count = min(3, len(warning_records)) for i in range(display_count): record = warning_records[i] moderator = ctx.guild.get_member(record["moderator_id"]) mod_name = moderator.display_name if moderator else f"ID: {record['moderator_id']}" # Format date warning_date = record["created_at"].strftime("%d.%m.%Y %H:%M") # Truncate reason if too long reason = record["reason"] if len(reason) > 50: reason = reason[:47] + "..." warning_line = f"`{warning_date}` **{mod_name}**: {reason}" # Add message indicator and content preview if available if record.get("message_id"): warning_line += " 📄" if record.get("message_content"): content_preview = record["message_content"][:30] + "..." if len(record["message_content"]) > 30 else record["message_content"] warning_line += f"\n *Message: {content_preview}*" warning_history += warning_line + "\n" if len(warning_records) > 3: warning_history += f"*... and {len(warning_records) - 3} more warning(s)*" embed.add_field(name="📋 Recent Warnings", value=warning_history, inline=False) # Server activity (if member) if member: activity_info = "" # Current status if member.status != discord.Status.offline: activity_info += f"**Status:** {str(member.status).title()}\n" else: activity_info += "**Status:** Offline\n" # Current activity if member.activity: activity_type = str(member.activity.type).replace('ActivityType.', '').title() activity_info += f"**Activity:** {activity_type} - {member.activity.name}\n" # Voice channel if member.voice: activity_info += f"**Voice Channel:** {member.voice.channel.mention}\n" # Mobile/Desktop if member.is_on_mobile(): activity_info += "**Platform:** Mobile 📱" else: activity_info += "**Platform:** Desktop 🖥️" if activity_info: embed.add_field(name="🎮 Current Activity", value=activity_info, inline=True) # Server settings relevant to this user settings_info = f"**Warning Threshold:** {warn_threshold}\n" auto_mute_enabled = guild_settings.get("auto_mute_on_warns", False) if auto_mute_enabled: auto_mute_duration = guild_settings.get("auto_mute_duration", "1 hour") settings_info += f"**Auto-Mute:** Enabled ({auto_mute_duration})\n" else: settings_info += "**Auto-Mute:** Disabled\n" settings_info += f"**Moderation Logs:** {'Enabled' if guild_settings.get('mod_log_enabled', False) else 'Disabled'}" embed.add_field(name="⚙️ Server Settings", value=settings_info, inline=True) # Set thumbnail and footer embed.set_thumbnail(url=user.display_avatar.url) embed.set_footer(text=f"Requested by {ctx.author.display_name} • Use /warn, /mute for actions") await ctx.send(embed=embed) # Log the modinfo request logger.info(f"Modinfo requested for user {user.id} by {ctx.author.id} in guild {ctx.guild.id}") except Exception as e: logger.error(f"Error in modinfo command: {e}") embed = discord.Embed( title="❌ Error", description="An error occurred while retrieving user information. Please try again.", color=0xff0000 ) await ctx.send(embed=embed) @client.hybrid_command() async def viewwarn(ctx, warning_id: int): """View detailed information about a specific warning (Requires Permission Level 5 or higher)""" # Check if it's a slash command and defer if needed is_slash_command = hasattr(ctx, 'interaction') and ctx.interaction if is_slash_command: await ctx.defer() # Helper function for sending responses async def send_response(content=None, embed=None, ephemeral=False, file=None): try: if is_slash_command: await ctx.followup.send(content=content, embed=embed, ephemeral=ephemeral, file=file) else: await ctx.send(content=content, embed=embed, file=file) except Exception as e: logger.error(f"Error sending response: {e}") # Fallback to regular send if followup fails try: await ctx.send(content=content, embed=embed, file=file) except: pass try: # Load moderator data mod_data = await load_user_data(ctx.author.id, ctx.guild.id) # Check moderation rights if not check_moderation_permission(mod_data["permission"]): embed = discord.Embed( title="❌ Insufficient Permissions", description="You need moderation permissions (Level 5 or higher) to use this command.", color=0xff0000 ) await send_response(embed=embed, ephemeral=True) return # Get warning details from database connection = None cursor = None try: connection = connect_to_database() cursor = connection.cursor() select_query = """ SELECT user_id, guild_id, moderator_id, reason, created_at, message_id, message_content, message_attachments, message_author_id, message_channel_id, context_messages FROM user_warnings WHERE id = %s AND guild_id = %s """ cursor.execute(select_query, (warning_id, ctx.guild.id)) result = cursor.fetchone() if not result: embed = discord.Embed( title="❌ Warning Not Found", description=f"No warning with ID {warning_id} found in this server.", color=0xff0000 ) await send_response(embed=embed, ephemeral=True) return # Parse result user_id, guild_id, moderator_id, reason, created_at, message_id, message_content, message_attachments, message_author_id, message_channel_id, context_messages = result # Get user and moderator objects warned_user = await client.fetch_user(user_id) moderator = await client.fetch_user(moderator_id) # Create detailed embed embed = discord.Embed( title=f"⚠️ Warning Details - ID: {warning_id}", color=0xff9500, timestamp=created_at ) embed.add_field(name="👤 Warned User", value=f"{warned_user.mention}\n`{warned_user.id}`", inline=True) embed.add_field(name="👮 Moderator", value=f"{moderator.mention}\n`{moderator.id}`", inline=True) embed.add_field(name="📅 Date", value=f"<t:{int(created_at.timestamp())}:F>", inline=True) embed.add_field(name="📝 Reason", value=reason, inline=False) # Add message information if available if message_id: message_info = f"**Message ID:** `{message_id}`\n" if message_channel_id: message_info += f"**Channel:** <#{message_channel_id}>\n" if message_author_id: try: msg_author = await client.fetch_user(message_author_id) message_info += f"**Author:** {msg_author.mention}\n" except: message_info += f"**Author ID:** `{message_author_id}`\n" if message_content: content_display = message_content if len(content_display) > 500: content_display = content_display[:497] + "..." message_info += f"**Content:**\n```\n{content_display}\n```" embed.add_field(name="📄 Referenced Message", value=message_info, inline=False) # Handle attachments if message_attachments: try: attachments = json.loads(message_attachments) if attachments: attachment_info = "" for i, att in enumerate(attachments[:3]): # Show max 3 attachments attachment_info += f"**{att['filename']}** ({att['size']} bytes)\n" # Show image if available and encoded if att.get('data') and att['content_type'].startswith('image/'): try: import base64 import io # Create temporary file-like object image_data = base64.b64decode(att['data']) file = discord.File(io.BytesIO(image_data), filename=att['filename']) # Send image separately if it's the first attachment if i == 0: await send_response(content=f"📎 **Attachment from Warning {warning_id}:**", file=file) except Exception as e: logger.warning(f"Could not display attachment: {e}") if len(attachments) > 3: attachment_info += f"*... and {len(attachments) - 3} more attachment(s)*" embed.add_field(name="📎 Attachments", value=attachment_info, inline=False) except Exception as e: logger.error(f"Error processing attachments: {e}") embed.set_thumbnail(url=warned_user.display_avatar.url) embed.set_footer(text=f"Warning ID: {warning_id} | Guild: {ctx.guild.name}") # Display context messages if available if context_messages: try: context_data = json.loads(context_messages) if context_data and len(context_data) > 1: context_display = "**📋 Message Context:**\n" for i, msg in enumerate(context_data): timestamp = datetime.fromisoformat(msg['created_at'].replace('Z', '+00:00')) author_name = msg.get('author_name', 'Unknown') content = msg.get('content', '*No content*') # Truncate long messages if len(content) > 100: content = content[:97] + "..." # Mark the main message marker = "🎯 " if msg.get('is_main_message') else "💬 " context_display += f"{marker}**{author_name}** (<t:{int(timestamp.timestamp())}:t>):\n`{content}`\n\n" # Limit to prevent embed overflow if len(context_display) > 1800: context_display += "*... (truncated)*" break # Send context as separate message to avoid embed limits context_embed = discord.Embed( title=f"📋 Message Context for Warning {warning_id}", description=context_display, color=0x3498db ) await send_response(embed=context_embed) except Exception as e: logger.error(f"Error displaying context messages: {e}") await send_response(embed=embed) finally: if cursor: cursor.close() if connection: close_database_connection(connection) except Exception as e: logger.error(f"Error in viewwarn command: {e}") embed = discord.Embed( title="❌ Error", description="An error occurred while retrieving warning details. Please try again.", color=0xff0000 ) await send_response(embed=embed) @client.hybrid_command() async def mute(ctx, user: discord.User, duration: str, reason: str = "No reason provided", message_id: str = None, context_range: int = 3): """Mutes a user for a specified duration (Requires Permission Level 5 or higher) Usage: /mute @user 10m "Inappropriate behavior" /mute @user 1h "Bad language" 1407754702564884622 /mute @user 1h "Bad language" 1407754702564884622 15 Parameters: - user: The user to mute - duration: Duration (10m, 1h, 2d) - reason: Reason for the mute (can include message ID and context range) - message_id: Optional message ID to reference (required for context_range) - context_range: Number of context messages to archive (only works with message_id) Duration examples: - 10m = 10 minutes - 1h = 1 hour - 2d = 2 days You can also specify message ID and context range in the reason: "Bad language 1407754702564884622 15" (15 messages before/after) Note: context_range parameter only works when message_id is also provided! """ # Check if it's a slash command and defer if needed is_slash_command = hasattr(ctx, 'interaction') and ctx.interaction if is_slash_command: await ctx.defer() # Helper function for sending responses async def send_response(content=None, embed=None, ephemeral=False, file=None): try: if is_slash_command: await ctx.followup.send(content=content, embed=embed, ephemeral=ephemeral, file=file) else: await ctx.send(content=content, embed=embed, file=file) except Exception as e: logger.error(f"Error sending response: {e}") # Fallback to regular send if followup fails try: await ctx.send(content=content, embed=embed, file=file) except: pass try: # Parse message ID and context range from reason if they look valid original_reason = reason message_data = None parsed_context_range = 3 # Default context range, only used if message_id is provided # Check if reason contains potential message ID and context range reason_words = reason.split() if len(reason_words) >= 2: # Check for pattern: "reason text 1234567890123456789 15" potential_msg_id = reason_words[-2] if len(reason_words) >= 2 else None potential_context = reason_words[-1] if len(reason_words) >= 1 else None # Check if last two elements are message ID and context range if (potential_msg_id and len(potential_msg_id) >= 17 and len(potential_msg_id) <= 20 and potential_msg_id.isdigit() and potential_context and len(potential_context) <= 3 and potential_context.isdigit()): parsed_context_range = int(potential_context) message_id = potential_msg_id reason = " ".join(reason_words[:-2]) # Remove both from reason elif len(reason_words) >= 1: # Check if reason ends with a potential message ID only potential_msg_id = reason_words[-1] if len(potential_msg_id) >= 17 and len(potential_msg_id) <= 20 and potential_msg_id.isdigit(): message_id = potential_msg_id reason = " ".join(reason_words[:-1]) # Remove message ID from reason # Only use context_range parameter if message_id is also provided if message_id and context_range != 3: # If message_id was provided and context_range was also set, use it parsed_context_range = context_range elif not message_id: # If no message_id was found, reset context_range to default parsed_context_range = 3 # Validate and limit context range if parsed_context_range < 1: parsed_context_range = 1 elif parsed_context_range > 25: parsed_context_range = 25 # Try to get message data if message ID was provided if message_id: try: message_id_int = int(message_id) except ValueError: await send_response(content=f"❌ Invalid message ID: {message_id}") return # Try to get message data from current channel first message_data = await get_message_data(ctx.channel, message_id_int, context_range=parsed_context_range) # If not found in current channel, try other channels if message_data is None: # Limit search to avoid spam - only check first 10 channels plus current channel channels_to_check = [ctx.channel] + [ch for ch in ctx.guild.text_channels[:10] if ch.id != ctx.channel.id] for channel in channels_to_check[1:]: # Skip current channel, already checked try: message_data = await get_message_data(channel, message_id_int, context_range=parsed_context_range) if message_data is not None: break except discord.Forbidden: continue # Load moderator data mod_data = await load_user_data(ctx.author.id, ctx.guild.id) # Check moderation rights if not check_moderation_permission(mod_data["permission"]): embed = discord.Embed( title="❌ Insufficient Permissions", description="You need moderation permissions (Level 5 or higher) to use this command.", color=0xff0000 ) await send_response(embed=embed, ephemeral=True) return # Cannot mute yourself if user.id == ctx.author.id: embed = discord.Embed( title="❌ Invalid Action", description="You cannot mute yourself!", color=0xff0000 ) await send_response(embed=embed, ephemeral=True) return # Parse duration time_units = {'m': 60, 'h': 3600, 'd': 86400} if not duration or not duration[-1] in time_units or not duration[:-1].isdigit(): embed = discord.Embed( title="❌ Invalid Duration", description="Invalid time format. Use: 10m, 1h, 2d", color=0xff0000 ) await send_response(embed=embed, ephemeral=True) return duration_seconds = int(duration[:-1]) * time_units[duration[-1]] end_time = datetime.now() + timedelta(seconds=duration_seconds) # Get member object member = ctx.guild.get_member(user.id) if not member: embed = discord.Embed( title="❌ User Not Found", description="User not found on this server.", color=0xff0000 ) await send_response(embed=embed, ephemeral=True) return # Load guild settings guild_settings = get_guild_settings(ctx.guild.id) # Save current roles await save_user_roles(user.id, ctx.guild.id, member.roles) # Remove all roles except @everyone roles_to_remove = [role for role in member.roles if not role.is_default()] if roles_to_remove: await member.remove_roles(*roles_to_remove, reason=f"Muted by {ctx.author}") # Get or create mute role mute_role = await get_or_create_mute_role(ctx.guild, guild_settings) if not mute_role: embed = discord.Embed( title="❌ Mute Role Error", description="Could not find or create mute role. Check server settings.", color=0xff0000 ) await send_response(embed=embed, ephemeral=True) return # Add mute role await member.add_roles(mute_role, reason=f"Muted by {ctx.author} for {duration}") # Update user data user_data = await load_user_data(user.id, ctx.guild.id) user_data["mutes"] += 1 update_user_data(user.id, ctx.guild.id, "mutes", user_data["mutes"]) # Create active process for auto-unmute process_data = { "user_id": user.id, "guild_id": ctx.guild.id, "channel_id": ctx.channel.id, "reason": reason, "moderator_id": ctx.author.id, "mute_role_id": mute_role.id } process_uuid = create_active_process( process_type="mute", guild_id=ctx.guild.id, channel_id=ctx.channel.id, user_id=user.id, target_id=user.id, end_time=end_time, data=process_data ) # Create embed embed = discord.Embed( title="🔇 User Muted", description=f"{user.mention} has been muted.", color=0xff0000, timestamp=datetime.now() ) embed.add_field(name="⏱️ Duration", value=duration, inline=True) embed.add_field(name="⏰ Ends At", value=f"<t:{int(end_time.timestamp())}:F>", inline=True) embed.add_field(name="📝 Reason", value=reason, inline=False) embed.add_field(name="👮 Moderator", value=ctx.author.mention, inline=True) embed.add_field(name="🔇 Mute Count", value=f"{user_data['mutes']}", inline=True) # Add message information if available if message_data: # Handle new context message format if isinstance(message_data, dict) and "main_message" in message_data: main_msg = message_data.get("main_message") context_msgs = message_data.get("context_messages", []) if main_msg: message_info = f"**Message ID:** `{main_msg['id']}`\n" message_info += f"**Channel:** <#{main_msg['channel_id']}>\n" message_info += f"**Author:** {main_msg['author_name']}\n" if main_msg['content']: content_preview = main_msg['content'][:200] + "..." if len(main_msg['content']) > 200 else main_msg['content'] message_info += f"**Content:** {content_preview}" embed.add_field(name="📄 Referenced Message", value=message_info, inline=False) # Process attachments for archival if any if main_msg.get('attachments'): try: attachments_data = json.loads(main_msg['attachments']) if attachments_data: attachment_info = "" for i, att in enumerate(attachments_data[:3]): # Show first 3 attachments attachment_info += f"• {att.get('filename', 'Unknown file')}\n" if len(attachments_data) > 3: attachment_info += f"• +{len(attachments_data) - 3} more attachments" embed.add_field(name="📎 Archived Attachments", value=attachment_info, inline=False) except: pass else: # Handle old format for backward compatibility message_info = f"**Message ID:** `{message_data.get('id', 'Unknown')}`\n" message_info += f"**Channel:** <#{message_data.get('channel_id', 'Unknown')}>\n" message_info += f"**Author:** <@{message_data.get('author_id', 'Unknown')}>\n" if message_data.get('content'): content_preview = message_data['content'][:200] + "..." if len(message_data['content']) > 200 else message_data['content'] message_info += f"**Content:** {content_preview}" embed.add_field(name="📄 Referenced Message", value=message_info, inline=False) elif message_id: embed.add_field(name="📄 Referenced Message", value=f"Message ID: `{message_id}` (Message not found or inaccessible)", inline=False) embed.set_footer(text=f"User ID: {user.id} | Process ID: {str(process_uuid)[:8]}") embed.set_thumbnail(url=user.display_avatar.url) await send_response(embed=embed) # Log the mute action log_additional_info = { "Duration": duration, "Mute Count": str(user_data['mutes']), "Process ID": str(process_uuid)[:8] } if message_data: log_additional_info["Referenced Message"] = f"ID: {message_data['id']} in <#{message_data['channel_id']}>" await log_moderation_action( guild=ctx.guild, action_type="mute", moderator=ctx.author, target_user=user, reason=reason, duration=duration, additional_info=log_additional_info ) # Try to DM the user try: dm_embed = discord.Embed( title="🔇 You have been muted", description=f"You have been muted in **{ctx.guild.name}**", color=0xff0000, timestamp=datetime.now() ) dm_embed.add_field(name="⏱️ Duration", value=duration, inline=True) dm_embed.add_field(name="⏰ Ends At", value=f"<t:{int(end_time.timestamp())}:F>", inline=True) dm_embed.add_field(name="📝 Reason", value=reason, inline=False) dm_embed.add_field(name="👮 Moderator", value=ctx.author.display_name, inline=True) if message_data and message_data['content']: content_preview = message_data['content'][:200] + "..." if len(message_data['content']) > 200 else message_data['content'] dm_embed.add_field(name="📄 Referenced Message", value=f"```{content_preview}```", inline=False) dm_embed.set_footer(text=f"Server: {ctx.guild.name}") await user.send(embed=dm_embed) except discord.Forbidden: pass # User has DMs disabled # Log the action logger.info(f"User {user.id} muted by {ctx.author.id} in guild {ctx.guild.id} for {duration}. Reason: {reason}") except Exception as e: logger.error(f"Error in mute command: {e}") embed = discord.Embed( title="❌ Error", description="An error occurred while processing the mute. Please try again.", color=0xff0000 ) await send_response(embed=embed) @client.hybrid_command() async def unmute(ctx, user: discord.User): """Entmutet einen Benutzer manuell (Benötigt Permission Level 5 oder höher)""" try: # Lade Moderator-Daten mod_data = await load_user_data(ctx.author.id, ctx.guild.id) # Überprüfe Moderationsrechte if not check_moderation_permission(mod_data["permission"]): await ctx.send("❌ Du hast keine Berechtigung, diesen Befehl zu verwenden. (Benötigt Permission Level 5 oder höher)") return # Hole Member-Objekt member = ctx.guild.get_member(user.id) if not member: await ctx.send("❌ Benutzer nicht auf diesem Server gefunden.") return # Lade Guild-Einstellungen guild_settings = get_guild_settings(ctx.guild.id) # Finde Mute-Rolle basierend auf Einstellungen mute_role = None if guild_settings["mute_role_id"]: mute_role = ctx.guild.get_role(guild_settings["mute_role_id"]) if not mute_role: mute_role = discord.utils.get(ctx.guild.roles, name=guild_settings["mute_role_name"]) if not mute_role or mute_role not in member.roles: await ctx.send("❌ Benutzer ist nicht gemutet.") return # Entferne Mute-Rolle await member.remove_roles(mute_role, reason=f"Unmuted by {ctx.author}") # Stelle Rollen wieder her await restore_user_roles(member, ctx.guild) # Finde und aktualisiere aktiven Mute-Prozess active_processes = get_active_processes(process_type="mute", guild_id=ctx.guild.id) for process in active_processes: if process["target_id"] == user.id: update_process_status(process["uuid"], "cancelled_manual") break # Erstelle Embed embed = discord.Embed( title="🔊 Benutzer entmutet", description=f"{user.mention} wurde entmutet.", color=0x00ff00, timestamp=datetime.now() ) embed.add_field(name="Moderator", value=ctx.author.mention, inline=True) embed.set_footer(text=f"User ID: {user.id}") await ctx.send(embed=embed) # Log the action logger.info(f"User {user.id} unmuted by {ctx.author.id} in guild {ctx.guild.id}") except Exception as e: logger.error(f"Error in unmute command: {e}") await ctx.send("❌ Ein Fehler ist aufgetreten beim Entmuten des Benutzers.") @client.hybrid_command() async def modstats(ctx, user: discord.User = None): """Zeigt Moderationsstatistiken für einen Benutzer an""" try: # Falls kein User angegeben, zeige eigene Stats target_user = user or ctx.author # Lade User-Daten user_data = await load_user_data(target_user.id, ctx.guild.id) # Erstelle Embed embed = discord.Embed( title=f"📊 Moderationsstatistiken", description=f"Statistiken für {target_user.mention}", color=0x3498db, timestamp=datetime.now() ) embed.add_field(name="🤖 AI Bans", value=user_data.get("ai_ban", 0), inline=True) embed.add_field(name="🔇 Mutes", value=user_data.get("mutes", 0), inline=True) embed.add_field(name="⚠️ Warnungen", value=user_data.get("warns", 0), inline=True) embed.add_field(name="🛡️ Permission Level", value=user_data.get("permission", 0), inline=True) embed.add_field(name="⭐ Level", value=user_data.get("level", 1), inline=True) embed.add_field(name="💰 Punkte", value=user_data.get("points", 0), inline=True) embed.set_thumbnail(url=target_user.display_avatar.url) embed.set_footer(text=f"User ID: {target_user.id}") await ctx.send(embed=embed) except Exception as e: logger.error(f"Error in modstats command: {e}") await ctx.send("❌ Ein Fehler ist aufgetreten beim Laden der Moderationsstatistiken.") @client.hybrid_command() async def modconfig(ctx, setting: str = None, *, value: str = None): """Konfiguriert Moderationseinstellungen für den Server (Benötigt Permission Level 8 oder höher) Verfügbare Einstellungen: - mute_role <@role/role_name> - Setzt die Mute-Rolle - mute_role_name <name> - Setzt den Namen für auto-erstellte Mute-Rollen - auto_create_mute_role <true/false> - Auto-Erstellung von Mute-Rollen - max_warn_threshold <number> - Anzahl Warnungen vor Auto-Aktion - auto_mute_on_warns <true/false> - Auto-Mute bei zu vielen Warnungen - auto_mute_duration <duration> - Dauer für Auto-Mutes (z.B. 1h, 30m) - log_channel <#channel> - Kanal für Moderations-Logs - mod_log_enabled <true/false> - Aktiviert/Deaktiviert Moderations-Logs """ try: # Lade Moderator-Daten mod_data = await load_user_data(ctx.author.id, ctx.guild.id) # Überprüfe Admin-Rechte (Level 8+) if mod_data["permission"] < 8: await ctx.send("❌ Du hast keine Berechtigung, Moderationseinstellungen zu ändern. (Benötigt Permission Level 8 oder höher)") return # Lade aktuelle Einstellungen guild_settings = get_guild_settings(ctx.guild.id) # Zeige Einstellungen an, falls keine Parameter if not setting: embed = discord.Embed( title="🛠️ Moderationseinstellungen", description=f"Aktuelle Einstellungen für **{ctx.guild.name}**", color=0x3498db, timestamp=datetime.now() ) # Mute-Rolle Info mute_role_info = "Nicht gesetzt" if guild_settings["mute_role_id"]: role = ctx.guild.get_role(guild_settings["mute_role_id"]) mute_role_info = role.mention if role else f"❌ Rolle nicht gefunden (ID: {guild_settings['mute_role_id']})" embed.add_field(name="🔇 Mute-Rolle", value=mute_role_info, inline=False) embed.add_field(name="📝 Mute-Rollen-Name", value=guild_settings["mute_role_name"], inline=True) embed.add_field(name="🔧 Auto-Erstellen", value="✅" if guild_settings["auto_create_mute_role"] else "❌", inline=True) embed.add_field(name="⚠️ Warn-Limit", value=guild_settings["max_warn_threshold"], inline=True) embed.add_field(name="🔄 Auto-Mute bei Warns", value="✅" if guild_settings["auto_mute_on_warns"] else "❌", inline=True) embed.add_field(name="⏱️ Auto-Mute-Dauer", value=guild_settings["auto_mute_duration"], inline=True) # Log-Kanal Info log_info = "Nicht gesetzt" if guild_settings["log_channel_id"]: channel = ctx.guild.get_channel(guild_settings["log_channel_id"]) log_info = channel.mention if channel else f"❌ Kanal nicht gefunden (ID: {guild_settings['log_channel_id']})" embed.add_field(name="📊 Log-Kanal", value=log_info, inline=True) embed.add_field(name="📝 Logs Aktiviert", value="✅" if guild_settings["mod_log_enabled"] else "❌", inline=True) embed.set_footer(text="Verwende -modconfig <setting> <value> zum Ändern") await ctx.send(embed=embed) return # Ändere Einstellungen setting = setting.lower() if setting == "mute_role": if not value: await ctx.send("❌ Bitte gib eine Rolle an: `-modconfig mute_role @MuteRole`") return # Parse Rolle role = None if value.startswith("<@&") and value.endswith(">"): role_id = int(value[3:-1]) role = ctx.guild.get_role(role_id) else: role = discord.utils.get(ctx.guild.roles, name=value) if not role: await ctx.send("❌ Rolle nicht gefunden.") return guild_settings["mute_role_id"] = role.id guild_settings["mute_role_name"] = role.name save_guild_settings(ctx.guild.id, guild_settings) await ctx.send(f"✅ Mute-Rolle auf {role.mention} gesetzt.") elif setting == "mute_role_name": if not value: await ctx.send("❌ Bitte gib einen Namen an: `-modconfig mute_role_name Stumm`") return guild_settings["mute_role_name"] = value save_guild_settings(ctx.guild.id, guild_settings) await ctx.send(f"✅ Mute-Rollen-Name auf `{value}` gesetzt.") elif setting == "auto_create_mute_role": if value.lower() in ["true", "1", "ja", "yes", "on"]: guild_settings["auto_create_mute_role"] = True await ctx.send("✅ Auto-Erstellung von Mute-Rollen aktiviert.") elif value.lower() in ["false", "0", "nein", "no", "off"]: guild_settings["auto_create_mute_role"] = False await ctx.send("✅ Auto-Erstellung von Mute-Rollen deaktiviert.") else: await ctx.send("❌ Ungültiger Wert. Verwende: true/false") return save_guild_settings(ctx.guild.id, guild_settings) elif setting == "max_warn_threshold": try: threshold = int(value) if threshold < 1 or threshold > 10: await ctx.send("❌ Warn-Limit muss zwischen 1 und 10 liegen.") return guild_settings["max_warn_threshold"] = threshold save_guild_settings(ctx.guild.id, guild_settings) await ctx.send(f"✅ Warn-Limit auf {threshold} gesetzt.") except ValueError: await ctx.send("❌ Ungültiger Wert. Verwende eine Nummer zwischen 1 und 10.") return elif setting == "auto_mute_on_warns": if value.lower() in ["true", "1", "ja", "yes", "on"]: guild_settings["auto_mute_on_warns"] = True await ctx.send("✅ Auto-Mute bei zu vielen Warnungen aktiviert.") elif value.lower() in ["false", "0", "nein", "no", "off"]: guild_settings["auto_mute_on_warns"] = False await ctx.send("✅ Auto-Mute bei zu vielen Warnungen deaktiviert.") else: await ctx.send("❌ Ungültiger Wert. Verwende: true/false") return save_guild_settings(ctx.guild.id, guild_settings) elif setting == "auto_mute_duration": # Validiere Dauer-Format time_units = {'m': 60, 'h': 3600, 'd': 86400} if not value or not value[-1] in time_units or not value[:-1].isdigit(): await ctx.send("❌ Ungültiges Zeitformat. Verwende: 10m, 1h, 2d") return guild_settings["auto_mute_duration"] = value save_guild_settings(ctx.guild.id, guild_settings) await ctx.send(f"✅ Auto-Mute-Dauer auf {value} gesetzt.") elif setting == "log_channel": if not value: await ctx.send("❌ Bitte gib einen Kanal an: `-modconfig log_channel #mod-log`") return # Parse Kanal channel = None if value.startswith("<#") and value.endswith(">"): channel_id = int(value[2:-1]) channel = ctx.guild.get_channel(channel_id) else: channel = discord.utils.get(ctx.guild.channels, name=value.replace("#", "")) if not channel: await ctx.send("❌ Kanal nicht gefunden.") return guild_settings["log_channel_id"] = channel.id save_guild_settings(ctx.guild.id, guild_settings) await ctx.send(f"✅ Log-Kanal auf {channel.mention} gesetzt.") elif setting == "mod_log_enabled": if value.lower() in ["true", "1", "ja", "yes", "on"]: guild_settings["mod_log_enabled"] = True await ctx.send("✅ Moderations-Logs aktiviert.") elif value.lower() in ["false", "0", "nein", "no", "off"]: guild_settings["mod_log_enabled"] = False await ctx.send("✅ Moderations-Logs deaktiviert.") else: await ctx.send("❌ Ungültiger Wert. Verwende: true/false") return save_guild_settings(ctx.guild.id, guild_settings) else: await ctx.send("❌ Unbekannte Einstellung. Verwende `-modconfig` ohne Parameter für eine Liste aller Einstellungen.") except Exception as e: logger.error(f"Error in modconfig command: {e}") await ctx.send("❌ Ein Fehler ist aufgetreten beim Konfigurieren der Moderationseinstellungen.") # Cache-Ordner für Notizen CACHE_DIR = "cache" if not os.path.exists(CACHE_DIR): os.makedirs(CACHE_DIR) # Profilbild-Ordner erstellen PROFILE_IMAGES_DIR = "static/profile_images" if not os.path.exists(PROFILE_IMAGES_DIR): os.makedirs(PROFILE_IMAGES_DIR) def get_url_hash(url): """Erstellt einen Hash aus der URL für Vergleichszwecke""" if not url: return None return hashlib.md5(url.encode('utf-8')).hexdigest() def get_local_profile_path(user_id): """Gibt den lokalen Pfad für das Profilbild eines Users zurück""" return os.path.join(PROFILE_IMAGES_DIR, f"user_{user_id}.png") def get_web_profile_path(user_id): """Gibt den Web-Pfad für das Profilbild eines Users zurück""" return f"/static/profile_images/user_{user_id}.png" async def download_and_save_profile_image(user_id, discord_url): """Lädt ein Profilbild herunter und speichert es lokal""" if not discord_url: return "/static/default_profile.png" try: local_path = get_local_profile_path(user_id) web_path = get_web_profile_path(user_id) # Überprüfe, ob das Bild bereits existiert und der Hash gleich ist hash_file = local_path + ".hash" current_hash = get_url_hash(discord_url) if os.path.exists(local_path) and os.path.exists(hash_file): with open(hash_file, 'r') as f: stored_hash = f.read().strip() if stored_hash == current_hash: logger.info(f"Profile image for user {user_id} is up to date, skipping download") return web_path # Download das Bild logger.info(f"Downloading profile image for user {user_id} from {discord_url}") # Use a session with timeout for better connection handling session = requests.Session() session.timeout = (10, 15) # (connection timeout, read timeout) response = session.get(discord_url, timeout=15) if response.status_code == 200: # Speichere das Bild with open(local_path, 'wb') as f: f.write(response.content) # Speichere den Hash with open(hash_file, 'w') as f: f.write(current_hash) logger.info(f"Successfully downloaded and saved profile image for user {user_id}") return web_path else: logger.warning(f"Failed to download profile image for user {user_id}: HTTP {response.status_code}") return "/static/default_profile.png" except Exception as e: logger.error(f"Error downloading profile image for user {user_id}: {e}") return "/static/default_profile.png" # Cache-Ordner für Notizen @client.hybrid_command() async def addnotes(ctx, type: str, source: str = None, attachment: discord.Attachment = None): """Adds a note that can be consulted later. For text files: /addnotes txt [attach file] For websites: /addnotes url https://example.com """ # Check if it's a slash command and defer if needed is_slash_command = hasattr(ctx, 'interaction') and ctx.interaction if is_slash_command: await ctx.defer() # Helper function for sending responses async def send_response(content=None, embed=None, ephemeral=False): try: if is_slash_command: if embed: await ctx.followup.send(embed=embed, ephemeral=ephemeral) else: await ctx.followup.send(content, ephemeral=ephemeral) else: if embed: await ctx.send(embed=embed) else: await ctx.send(content) except Exception as e: logger.error(f"Error sending response: {e}") # Fallback to regular send if followup fails try: if embed: await ctx.send(embed=embed) else: await ctx.send(content) except: pass user_id = ctx.author.id guild_id = ctx.guild.id user_cache_dir = os.path.join(CACHE_DIR, f"{str(guild_id)}_{str(user_id)}") if not os.path.exists(user_cache_dir): os.makedirs(user_cache_dir) note_file = os.path.join(user_cache_dir, "notes.txt") if type.lower() == "txt": # Check for attachment parameter first (Slash Command) if attachment: if attachment.content_type and attachment.content_type.startswith('text/'): try: content = await attachment.read() text_content = content.decode('utf-8') with open(note_file, "a", encoding="utf-8") as file: file.write(f"\n--- From file: {attachment.filename} ---\n") file.write(text_content + "\n") await send_response(f"✅ Text file `{attachment.filename}` added as notes for user {ctx.author.name}.") except UnicodeDecodeError: await send_response("❌ Error: File is not a valid text file.") except Exception as e: await send_response(f"❌ Error reading file: {e}") else: await send_response("❌ Please attach a text file (.txt, .md, etc.)") # Fallback for prefix commands with message attachments elif hasattr(ctx, 'message') and ctx.message and ctx.message.attachments: attachment = ctx.message.attachments[0] try: content = await attachment.read() text_content = content.decode('utf-8') with open(note_file, "a", encoding="utf-8") as file: file.write(f"\n--- From file: {attachment.filename} ---\n") file.write(text_content + "\n") await send_response(f"✅ Text file `{attachment.filename}` added as notes for user {ctx.author.name}.") except UnicodeDecodeError: await send_response("❌ Error: File is not a valid text file.") except Exception as e: await send_response(f"❌ Error reading file: {e}") else: await send_response("❌ No text file attached. Please use the attachment parameter for Slash Commands.") elif type.lower() == "url": if not source: await send_response("❌ Please provide a URL: `/addnotes url https://example.com`") return try: response = requests.get(source) if response.status_code == 200: # HTML-Parsen und nur Text extrahieren soup = BeautifulSoup(response.text, 'html.parser') # Entfernen von Header- und Footer-Elementen for element in soup(['header', 'footer', 'nav', 'aside']): element.decompose() text = soup.get_text() # Entfernen von überflüssigen Leerzeilen cleaned_text = "\n".join([line.strip() for line in text.splitlines() if line.strip()]) with open(note_file, "a", encoding="utf-8") as file: file.write(f"\n--- From URL: {source} ---\n") file.write(cleaned_text + "\n") await send_response(f"✅ Website content from `{source}` added as notes for user {ctx.author.name}.") else: await send_response(f"❌ Failed to retrieve the website from {source}. HTTP {response.status_code}") except Exception as e: await send_response(f"❌ Error fetching website: {e}") else: await send_response("❌ Invalid type. Use 'txt' for text files or 'url' for website URLs.") @client.hybrid_command() async def asknotes(ctx, *, question: str): """Asks a question about your saved notes.""" # Check if it's a slash command and defer if needed is_slash_command = hasattr(ctx, 'interaction') and ctx.interaction if is_slash_command: await ctx.defer() # Helper function for sending responses async def send_response(content=None, embed=None, ephemeral=False): try: if is_slash_command: if embed: await ctx.followup.send(embed=embed, ephemeral=ephemeral) else: await ctx.followup.send(content, ephemeral=ephemeral) else: if embed: await ctx.send(embed=embed) else: await ctx.send(content) except Exception as e: logger.error(f"Error sending response: {e}") # Fallback to regular send if followup fails try: if embed: await ctx.send(embed=embed) else: await ctx.send(content) except: pass user_id = ctx.author.id guild_id = ctx.guild.id user_cache_dir = os.path.join(CACHE_DIR, f"{str(guild_id)}_{str(user_id)}") note_file = os.path.join(user_cache_dir, "notes.txt") asknotesintroduction = read_askintroduction() if not os.path.exists(note_file): await send_response(f"No notes found for user {ctx.author.name}.") return with open(note_file, "r", encoding="utf-8") as file: notes = file.read() # Define the full data and user history field for asknotes full_data = asknotesintroduction user_history_field = "asknotes_history" # Füge die Anfrage zur Warteschlange hinzu await askmultus_queue.put((ctx, user_id, ctx.author.name, question, ctx.channel.id, full_data, user_history_field, "text-davinci-003")) # Erstelle ein Embed für die Bestätigungsnachricht embed = discord.Embed(title="Notes Query", color=0x00ff00) embed.add_field(name="Request Received", value="Your request has been added to the queue. Processing it now...") await send_response(embed=embed) @client.hybrid_command() async def delnotes(ctx): """Deletes all saved notes and the asknotes history for the user.""" # Check if it's a slash command and defer if needed is_slash_command = hasattr(ctx, 'interaction') and ctx.interaction # Helper function for sending responses async def send_response(content=None, embed=None, ephemeral=False): try: if is_slash_command: if embed: await ctx.followup.send(embed=embed, ephemeral=ephemeral) else: await ctx.followup.send(content, ephemeral=ephemeral) else: if embed: await ctx.send(embed=embed) else: await ctx.send(content) except Exception as e: logger.error(f"Error sending response: {e}") # Fallback to regular send if followup fails try: if embed: await ctx.send(embed=embed) else: await ctx.send(content) except: pass user_id = ctx.author.id guild_id = ctx.guild.id user_cache_dir = os.path.join(CACHE_DIR, f"{str(guild_id)}_{str(user_id)}") if os.path.exists(user_cache_dir): # Lösche die gespeicherten Notizen im Cache-Ordner shutil.rmtree(user_cache_dir) # Setze die asknotes-Historie in der Datenbank zurück try: update_user_data(user_id, guild_id, "asknotes_history", None) await send_response(f"All notes and asknotes history deleted for user {ctx.author.name}.") except Exception as e: await send_response(f"Error deleting asknotes history: {e}") else: await send_response(f"No notes found for user {ctx.author.name}.") try: # Initialize database tables create_warnings_table() logger.info("Database tables initialized successfully") loop.run_until_complete(client.start(TOKEN)) except KeyboardInterrupt: loop.run_until_complete(client.logout()) finally: loop.close()