3420 lines
131 KiB
Python
3420 lines
131 KiB
Python
__version__ = "dev-0.9.6"
|
||
__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": int(result[17]) if len(result) > 17 and result[17] is not None else 0
|
||
}
|
||
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
|
||
}
|
||
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"]
|
||
)
|
||
|
||
return user_data
|
||
except Exception as e:
|
||
logger.error(f"Error loading user data from MySQL: {e}")
|
||
# Return default user data in case of error
|
||
return {
|
||
"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": ""
|
||
}
|
||
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 - placeholder for future implementation"""
|
||
try:
|
||
# TODO: Implement mute removal logic
|
||
guild_id = data.get("guild_id")
|
||
user_id = data.get("user_id")
|
||
|
||
logger.info(f"Mute 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 mute {process_uuid}: {e}")
|
||
|
||
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
|
||
# 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 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 <t:{unix_end_time}:R>",
|
||
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 <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)
|
||
|
||
# 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]` - Warn a user\n"
|
||
"`/mute <user> <duration> [reason]` - Mute a user temporarily\n"
|
||
"`/unmute <user>` - Manually unmute a user\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="⏱️ Duration Formats",
|
||
value=(
|
||
"When using mute commands:\n"
|
||
"`10m` = 10 minutes\n"
|
||
"`1h` = 1 hour\n"
|
||
"`2d` = 2 days"
|
||
),
|
||
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 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"):
|
||
"""Warns 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"]):
|
||
await ctx.send("❌ You don't have permission to use this command. (Requires Permission Level 5 or higher)")
|
||
return
|
||
|
||
# Load user data
|
||
user_data = await load_user_data(user.id, ctx.guild.id)
|
||
|
||
# Increase warn count
|
||
user_data["warns"] += 1
|
||
update_user_data(user.id, ctx.guild.id, "warns", user_data["warns"])
|
||
|
||
# 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"{user_data['warns']}", 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} warned by {ctx.author.id} in guild {ctx.guild.id}. Reason: {reason}")
|
||
|
||
# Auto-actions based on warning count
|
||
if user_data["warns"] >= 3:
|
||
embed_auto = discord.Embed(
|
||
title="🚨 Auto-action triggered",
|
||
description=f"{user.mention} has reached {user_data['warns']} warnings!",
|
||
color=0xff0000
|
||
)
|
||
embed_auto.add_field(name="Recommendation", value="Consider further moderation measures", inline=False)
|
||
await ctx.send(embed=embed_auto)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in warn command: {e}")
|
||
await ctx.send("❌ An error occurred while warning the user.")
|
||
|
||
@client.hybrid_command()
|
||
async def mywarns(ctx):
|
||
"""Shows your current warning count and moderation statistics"""
|
||
try:
|
||
user_data = await load_user_data(ctx.author.id, ctx.guild.id)
|
||
|
||
embed = discord.Embed(
|
||
title="📊 Your Moderation Statistics",
|
||
description=f"Moderation data for {ctx.author.mention}",
|
||
color=0x3498db,
|
||
timestamp=datetime.now()
|
||
)
|
||
|
||
# Warning information
|
||
warn_color = "🟢" if user_data["warns"] == 0 else "🟡" if user_data["warns"] < 3 else "🔴"
|
||
embed.add_field(
|
||
name=f"{warn_color} Warnings",
|
||
value=f"**{user_data['warns']}** warning(s)",
|
||
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
|
||
)
|
||
|
||
# Status assessment
|
||
total_infractions = user_data["warns"] + user_data["mutes"] + user_data["ai_ban"]
|
||
if 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="📈 Status", value=status, inline=False)
|
||
embed.color = status_color
|
||
|
||
# Get guild settings for thresholds
|
||
guild_settings = get_guild_settings(ctx.guild.id)
|
||
threshold_info = f"Warning threshold: **{guild_settings['max_warn_threshold']}** warnings"
|
||
if guild_settings["auto_mute_on_warns"]:
|
||
threshold_info += f"\nAuto-mute duration: **{guild_settings['auto_mute_duration']}**"
|
||
|
||
embed.add_field(name="⚙️ Server Settings", value=threshold_info, inline=False)
|
||
|
||
# Tips for improvement
|
||
if total_infractions > 0:
|
||
tips = (
|
||
"<EFBFBD> **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=ctx.author.display_avatar.url)
|
||
embed.set_footer(text=f"User ID: {ctx.author.id}")
|
||
|
||
await ctx.send(embed=embed)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in mywarns command: {e}")
|
||
await ctx.send("❌ An error occurred while retrieving your warning information.")
|
||
|
||
@client.hybrid_command()
|
||
async def mute(ctx, user: discord.User, duration: str, *, reason: str = "No reason provided"):
|
||
"""Mutes a user for a specified duration (Requires Permission Level 5 or higher)
|
||
|
||
Duration examples:
|
||
- 10m = 10 minutes
|
||
- 1h = 1 hour
|
||
- 2d = 2 days
|
||
"""
|
||
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"]):
|
||
await ctx.send("❌ You don't have permission to use this command. (Requires Permission Level 5 or higher)")
|
||
return
|
||
|
||
# Parse duration
|
||
time_units = {'m': 60, 'h': 3600, 'd': 86400}
|
||
if not duration[-1] in time_units or not duration[:-1].isdigit():
|
||
await ctx.send("❌ Invalid time format. Use: 10m, 1h, 2d")
|
||
return
|
||
|
||
duration_seconds = int(duration[:-1]) * time_units[duration[-1]]
|
||
end_time = datetime.now() + timedelta(seconds=duration_seconds)
|
||
|
||
# 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)
|
||
|
||
# Speichere aktuelle Rollen
|
||
await save_user_roles(user.id, ctx.guild.id, member.roles)
|
||
|
||
# Entferne alle Rollen außer @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}")
|
||
|
||
# Hole oder erstelle Mute-Rolle basierend auf Einstellungen
|
||
mute_role = await get_or_create_mute_role(ctx.guild, guild_settings)
|
||
if not mute_role:
|
||
await ctx.send("❌ Konnte Mute-Rolle nicht finden oder erstellen. Überprüfe die Server-Einstellungen.")
|
||
return
|
||
|
||
# Vergebe Mute-Rolle
|
||
await member.add_roles(mute_role, reason=f"Muted by {ctx.author} for {duration}")
|
||
|
||
# Update User-Daten
|
||
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"])
|
||
|
||
# Erstelle Active Process für 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
|
||
)
|
||
|
||
# Erstelle Embed
|
||
embed = discord.Embed(
|
||
title="🔇 Benutzer gemutet",
|
||
description=f"{user.mention} wurde gemutet.",
|
||
color=0xff0000,
|
||
timestamp=datetime.now()
|
||
)
|
||
embed.add_field(name="Dauer", value=duration, inline=True)
|
||
embed.add_field(name="Endet am", value=end_time.strftime("%Y-%m-%d %H:%M:%S"), inline=True)
|
||
embed.add_field(name="Grund", 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)
|
||
embed.set_footer(text=f"User ID: {user.id} | Process ID: {str(process_uuid)[:8]}")
|
||
|
||
await ctx.send(embed=embed)
|
||
|
||
# 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}")
|
||
await ctx.send("❌ Ein Fehler ist aufgetreten beim Muten des Benutzers.")
|
||
|
||
@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.")
|
||
|
||
# Erweitere handle_expired_mute Funktion
|
||
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")
|
||
|
||
if not all([guild_id, user_id]):
|
||
logger.error(f"Missing data in mute process {process_uuid}")
|
||
update_process_status(process_uuid, "failed")
|
||
return
|
||
|
||
# Hole Guild und Member
|
||
guild = client.get_guild(guild_id)
|
||
if not guild:
|
||
logger.error(f"Guild {guild_id} not found for mute process {process_uuid}")
|
||
update_process_status(process_uuid, "failed")
|
||
return
|
||
|
||
member = guild.get_member(user_id)
|
||
if not member:
|
||
logger.warning(f"Member {user_id} not found in guild {guild_id} for mute process {process_uuid}")
|
||
update_process_status(process_uuid, "completed")
|
||
return
|
||
|
||
# Finde Mute-Rolle basierend auf Guild-Einstellungen
|
||
guild_settings = get_guild_settings(guild_id)
|
||
mute_role = None
|
||
|
||
if mute_role_id:
|
||
mute_role = guild.get_role(mute_role_id)
|
||
|
||
if not mute_role and guild_settings["mute_role_id"]:
|
||
mute_role = guild.get_role(guild_settings["mute_role_id"])
|
||
|
||
if not mute_role:
|
||
mute_role = discord.utils.get(guild.roles, name=guild_settings["mute_role_name"])
|
||
|
||
# Entferne Mute-Rolle falls vorhanden
|
||
if mute_role and mute_role in member.roles:
|
||
await member.remove_roles(mute_role, reason="Mute expired automatically")
|
||
|
||
# Stelle Rollen wieder her
|
||
await restore_user_roles(member, guild)
|
||
|
||
update_process_status(process_uuid, "completed")
|
||
logger.info(f"Successfully unmuted user {user_id} in guild {guild_id} (expired mute)")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error handling expired mute {process_uuid}: {e}")
|
||
update_process_status(process_uuid, "failed")
|
||
|
||
# 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):
|
||
"""Adds a note that can be consulted later. Use 'txt' for text files or 'url' for website URLs."""
|
||
await ctx.defer() # Signalisiert, dass die Bearbeitung des Befehls begonnen hat
|
||
|
||
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":
|
||
if ctx.message.attachments:
|
||
attachment = ctx.message.attachments[0]
|
||
await attachment.save(note_file)
|
||
await ctx.send(f"Text file added as notes for user {ctx.author.name}.")
|
||
else:
|
||
await ctx.send("No text file attached.")
|
||
elif type.lower() == "url":
|
||
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(cleaned_text + "\n")
|
||
await ctx.send(f"Website content added as notes for user {ctx.author.name}.")
|
||
else:
|
||
await ctx.send(f"Failed to retrieve the website from {source}.")
|
||
except Exception as e:
|
||
await ctx.send(f"Error fetching website: {e}")
|
||
else:
|
||
await ctx.send("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."""
|
||
await ctx.defer()
|
||
|
||
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 ctx.send(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 ctx.send(embed=embed)
|
||
|
||
@client.hybrid_command()
|
||
async def delnotes(ctx):
|
||
"""Deletes all saved notes and the asknotes history for the user."""
|
||
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 ctx.send(f"All notes and asknotes history deleted for user {ctx.author.name}.")
|
||
except Exception as e:
|
||
await ctx.send(f"Error deleting asknotes history: {e}")
|
||
else:
|
||
await ctx.send(f"No notes found for user {ctx.author.name}.")
|
||
|
||
try:
|
||
loop.run_until_complete(client.start(TOKEN))
|
||
except KeyboardInterrupt:
|
||
loop.run_until_complete(client.logout())
|
||
finally:
|
||
loop.close()
|