7833 lines
325 KiB
Python
7833 lines
325 KiB
Python
__version__ = "dev-0.9.8"
|
||
__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 re
|
||
import aiohttp
|
||
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=15, # Weiter reduziert von 25 auf 15 (App: 8, Bot: 15 = 23 total)
|
||
pool_reset_session=True,
|
||
autocommit=True,
|
||
host=DB_HOST,
|
||
port=DB_PORT,
|
||
user=DB_USER,
|
||
password=DB_PASSWORD,
|
||
database=DB_DATABASE
|
||
)
|
||
|
||
def connect_to_database():
|
||
"""Holt eine Verbindung aus dem Pool"""
|
||
try:
|
||
connection = pool.get_connection()
|
||
return connection
|
||
except mysql.connector.PoolError as e:
|
||
logger.error(f"Pool error: {e}")
|
||
raise e
|
||
|
||
def close_database_connection(connection):
|
||
"""Gibt eine Verbindung an den Pool zurück"""
|
||
if connection and connection.is_connected():
|
||
connection.close()
|
||
|
||
async def create_user_data_with_member(user_id, guild_id, member=None):
|
||
"""Erstellt neue User-Daten mit korrekten Informationen vom Member-Objekt"""
|
||
nickname = member.display_name if member else ""
|
||
|
||
# Profilbild herunterladen und lokal speichern
|
||
if member and member.display_avatar:
|
||
discord_avatar_url = str(member.display_avatar.url)
|
||
profile_picture = await download_and_save_profile_image(user_id, discord_avatar_url)
|
||
else:
|
||
profile_picture = "/static/default_profile.png"
|
||
|
||
join_date = member.joined_at.date() if member and member.joined_at else None
|
||
|
||
user_data = {
|
||
"user_id": user_id,
|
||
"guild_id": guild_id,
|
||
"permission": 0,
|
||
"points": 0,
|
||
"ban": 0,
|
||
"askmultus": 0,
|
||
"filter_value": 0,
|
||
"rank": 0,
|
||
"chat_history": [],
|
||
"asknotes_history": [],
|
||
"xp": 0,
|
||
"level": 1,
|
||
"nickname": nickname,
|
||
"ai_ban": 0,
|
||
"mutes": 0,
|
||
"warns": 0
|
||
}
|
||
|
||
insert_user_data(
|
||
user_data["user_id"],
|
||
user_data["guild_id"],
|
||
user_data["permission"],
|
||
user_data["points"],
|
||
user_data["ban"],
|
||
user_data["askmultus"],
|
||
user_data["filter_value"],
|
||
user_data["chat_history"],
|
||
user_data["xp"],
|
||
user_data["level"],
|
||
nickname,
|
||
profile_picture,
|
||
join_date
|
||
)
|
||
|
||
logger.info(f"Created new user data for {nickname} (ID: {user_id}) with join_date: {join_date} and profile_picture: {profile_picture}")
|
||
return user_data
|
||
|
||
def load_user_data_from_mysql(user_id, guild_id):
|
||
connection = None
|
||
cursor = None
|
||
try:
|
||
connection = connect_to_database()
|
||
cursor = connection.cursor()
|
||
select_query = "SELECT * FROM user_data WHERE user_id = %s AND guild_id = %s"
|
||
cursor.execute(select_query, (user_id, guild_id))
|
||
result = cursor.fetchone()
|
||
|
||
if result:
|
||
user_data = {
|
||
"user_id": result[0],
|
||
"guild_id": result[1],
|
||
"permission": result[2],
|
||
"points": int(result[3]),
|
||
"ban": result[4],
|
||
"askmultus": result[5],
|
||
"filter_value": result[6],
|
||
"rank": result[7],
|
||
"chat_history": json.loads(result[8]) if result[8] else [],
|
||
"asknotes_history": json.loads(result[9]) if result[9] else [],
|
||
"xp": int(result[10]) if result[10] is not None else 0,
|
||
"level": int(result[11]) if result[11] is not None else 1,
|
||
"nickname": result[12],
|
||
"ai_ban": int(result[15]) if len(result) > 15 and result[15] is not None else 0,
|
||
"mutes": int(result[16]) if len(result) > 16 and result[16] is not None else 0,
|
||
"warns": 0 # Will be calculated from user_warnings table
|
||
}
|
||
else:
|
||
user_data = {
|
||
"user_id": user_id,
|
||
"guild_id": guild_id,
|
||
"permission": 0,
|
||
"points": 0,
|
||
"ban": 0,
|
||
"askmultus": 0,
|
||
"filter_value": 0,
|
||
"rank": 0,
|
||
"chat_history": [],
|
||
"asknotes_history": [],
|
||
"xp": 0,
|
||
"level": 1,
|
||
"nickname": "",
|
||
"ai_ban": 0,
|
||
"mutes": 0,
|
||
"warns": 0
|
||
}
|
||
|
||
# Count warnings from user_warnings table
|
||
warning_count_query = "SELECT COUNT(*) FROM user_warnings WHERE user_id = %s AND guild_id = %s AND aktiv = TRUE"
|
||
cursor.execute(warning_count_query, (user_id, guild_id))
|
||
warning_count = cursor.fetchone()[0]
|
||
user_data["warns"] = warning_count
|
||
|
||
return user_data
|
||
except Exception as e:
|
||
logger.error(f"Error loading user data from MySQL: {e}")
|
||
return None
|
||
finally:
|
||
if cursor:
|
||
cursor.close()
|
||
if connection:
|
||
close_database_connection(connection)
|
||
|
||
cached_user_data = {}
|
||
pending_deletion = {}
|
||
|
||
async def cache_user_data(user_id, guild_id, data):
|
||
cached_user_data[(user_id, guild_id)] = data
|
||
|
||
# Setze die Daten nach 30 Sekunden auf die Löschliste
|
||
if (user_id, guild_id) not in pending_deletion:
|
||
pending_deletion[(user_id, guild_id)] = asyncio.get_event_loop().call_later(30, lambda: remove_user_data_from_cache(user_id, guild_id))
|
||
|
||
def remove_user_data_from_cache(user_id, guild_id):
|
||
# Entferne den Benutzer aus dem Cache und der Löschliste
|
||
if (user_id, guild_id) in cached_user_data:
|
||
del cached_user_data[(user_id, guild_id)]
|
||
if (user_id, guild_id) in pending_deletion:
|
||
pending_deletion[(user_id, guild_id)].cancel()
|
||
del pending_deletion[(user_id, guild_id)]
|
||
|
||
async def load_user_data(user_id, guild_id, member=None):
|
||
if (user_id, guild_id) in cached_user_data:
|
||
return cached_user_data[(user_id, guild_id)]
|
||
|
||
# Daten aus der Datenbank laden oder neu anlegen
|
||
user_data = load_user_data_from_mysql(user_id, guild_id)
|
||
|
||
# Wenn keine User-Daten existieren, erstelle neue mit Member-Informationen
|
||
if not user_data or user_data.get("user_id") is None:
|
||
user_data = await create_user_data_with_member(user_id, guild_id, member)
|
||
|
||
asyncio.ensure_future(cache_user_data(user_id, guild_id, user_data))
|
||
return user_data
|
||
|
||
def load_user_data_sync(user_id, guild_id):
|
||
"""Synchrone Version von load_user_data für bestehende Commands"""
|
||
if (user_id, guild_id) in cached_user_data:
|
||
return cached_user_data[(user_id, guild_id)]
|
||
|
||
# Daten aus der Datenbank laden oder neu anlegen
|
||
user_data = load_user_data_from_mysql(user_id, guild_id)
|
||
|
||
# Wenn keine User-Daten existieren, erstelle neue mit Default-Werten
|
||
if not user_data or user_data.get("user_id") is None:
|
||
user_data = {
|
||
"user_id": user_id,
|
||
"guild_id": guild_id,
|
||
"permission": 0,
|
||
"points": 0,
|
||
"ban": 0,
|
||
"askmultus": 0,
|
||
"filter_value": 0,
|
||
"rank": 0,
|
||
"chat_history": [],
|
||
"asknotes_history": [],
|
||
"xp": 0,
|
||
"level": 1,
|
||
"nickname": "",
|
||
"ai_ban": 0,
|
||
"mutes": 0,
|
||
"warns": 0
|
||
}
|
||
insert_user_data(
|
||
user_data["user_id"],
|
||
user_data["guild_id"],
|
||
user_data["permission"],
|
||
user_data["points"],
|
||
user_data["ban"],
|
||
user_data["askmultus"],
|
||
user_data["filter_value"],
|
||
user_data["chat_history"],
|
||
user_data["xp"],
|
||
user_data["level"],
|
||
user_data["nickname"],
|
||
"/static/default_profile.png"
|
||
)
|
||
|
||
asyncio.ensure_future(cache_user_data(user_id, guild_id, user_data))
|
||
return user_data
|
||
|
||
def get_global_permission(user_id):
|
||
connection = None
|
||
cursor = None
|
||
try:
|
||
connection = connect_to_database()
|
||
cursor = connection.cursor()
|
||
select_query = "SELECT global_permission FROM bot_data WHERE user_id = %s"
|
||
cursor.execute(select_query, (user_id,))
|
||
result = cursor.fetchone()
|
||
return result[0] if result else None
|
||
except Exception as e:
|
||
logger.error(f"Error getting global permission: {e}")
|
||
return None
|
||
finally:
|
||
if cursor:
|
||
cursor.close()
|
||
if connection:
|
||
close_database_connection(connection)
|
||
|
||
def save_global_permission(user_id, permission_level):
|
||
connection = None
|
||
cursor = None
|
||
try:
|
||
connection = connect_to_database()
|
||
cursor = connection.cursor()
|
||
insert_query = """
|
||
INSERT INTO bot_data (user_id, global_permission)
|
||
VALUES (%s, %s)
|
||
ON DUPLICATE KEY UPDATE global_permission = %s
|
||
"""
|
||
cursor.execute(insert_query, (user_id, permission_level, permission_level))
|
||
connection.commit()
|
||
logger.info(f"Successfully saved global permission for user {user_id}: {permission_level}")
|
||
except Exception as e:
|
||
logger.error(f"Error saving global permission: {e}")
|
||
if connection:
|
||
connection.rollback()
|
||
raise e
|
||
finally:
|
||
if cursor:
|
||
cursor.close()
|
||
if connection:
|
||
close_database_connection(connection)
|
||
|
||
#-----------------------------------------------------------------------------------------------------------
|
||
|
||
# Active Processes System - Robust system for storing and managing active processes
|
||
def create_active_process(process_type, guild_id, channel_id=None, user_id=None, target_id=None,
|
||
start_time=None, end_time=None, status="active", data=None, metadata=None):
|
||
"""Creates a new active process entry in the database"""
|
||
connection = None
|
||
cursor = None
|
||
try:
|
||
connection = connect_to_database()
|
||
cursor = connection.cursor()
|
||
process_uuid = uuid.uuid4()
|
||
|
||
insert_query = """
|
||
INSERT INTO active_processes (uuid, process_type, guild_id, channel_id, user_id, target_id,
|
||
start_time, end_time, status, data, metadata, created_at)
|
||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||
"""
|
||
|
||
current_time = datetime.now()
|
||
start_time = start_time or current_time
|
||
|
||
# Serialize data and metadata as JSON
|
||
data_json = json.dumps(data) if data else None
|
||
metadata_json = json.dumps(metadata) if metadata else None
|
||
|
||
cursor.execute(insert_query, (
|
||
str(process_uuid), process_type, guild_id, channel_id, user_id, target_id,
|
||
start_time, end_time, status, data_json, metadata_json, current_time
|
||
))
|
||
connection.commit()
|
||
|
||
logger.info(f"Created active process: {process_type} with UUID: {process_uuid}")
|
||
return process_uuid
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error creating active process: {e}")
|
||
if connection:
|
||
connection.rollback()
|
||
raise e
|
||
finally:
|
||
if cursor:
|
||
cursor.close()
|
||
if connection:
|
||
close_database_connection(connection)
|
||
|
||
def get_active_processes(process_type=None, guild_id=None, status="active"):
|
||
"""Retrieves active processes from the database with optional filters"""
|
||
connection = None
|
||
cursor = None
|
||
try:
|
||
connection = connect_to_database()
|
||
cursor = connection.cursor()
|
||
|
||
query = "SELECT * FROM active_processes WHERE status = %s"
|
||
params = [status]
|
||
|
||
if process_type:
|
||
query += " AND process_type = %s"
|
||
params.append(process_type)
|
||
|
||
if guild_id:
|
||
query += " AND guild_id = %s"
|
||
params.append(guild_id)
|
||
|
||
query += " ORDER BY created_at ASC"
|
||
|
||
cursor.execute(query, params)
|
||
results = cursor.fetchall()
|
||
|
||
processes = []
|
||
for row in results:
|
||
process = {
|
||
"uuid": row[0],
|
||
"process_type": row[1],
|
||
"guild_id": row[2],
|
||
"channel_id": row[3],
|
||
"user_id": row[4],
|
||
"target_id": row[5],
|
||
"start_time": row[6],
|
||
"end_time": row[7],
|
||
"status": row[8],
|
||
"data": json.loads(row[9]) if row[9] else None,
|
||
"metadata": json.loads(row[10]) if row[10] else None,
|
||
"created_at": row[11],
|
||
"updated_at": row[12]
|
||
}
|
||
processes.append(process)
|
||
|
||
return processes
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error retrieving active processes: {e}")
|
||
return []
|
||
finally:
|
||
if cursor:
|
||
cursor.close()
|
||
if connection:
|
||
close_database_connection(connection)
|
||
|
||
def update_process_status(process_uuid, status, data=None, metadata=None):
|
||
"""Updates the status and optionally data/metadata of an active process"""
|
||
connection = None
|
||
cursor = None
|
||
try:
|
||
connection = connect_to_database()
|
||
cursor = connection.cursor()
|
||
|
||
# Prepare update fields
|
||
update_fields = ["status = %s", "updated_at = %s"]
|
||
params = [status, datetime.now()]
|
||
|
||
if data is not None:
|
||
update_fields.append("data = %s")
|
||
params.append(json.dumps(data))
|
||
|
||
if metadata is not None:
|
||
update_fields.append("metadata = %s")
|
||
params.append(json.dumps(metadata))
|
||
|
||
params.append(str(process_uuid))
|
||
|
||
update_query = f"UPDATE active_processes SET {', '.join(update_fields)} WHERE uuid = %s"
|
||
cursor.execute(update_query, params)
|
||
connection.commit()
|
||
|
||
logger.info(f"Updated process {process_uuid} status to: {status}")
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error updating process status: {e}")
|
||
if connection:
|
||
connection.rollback()
|
||
return False
|
||
finally:
|
||
if cursor:
|
||
cursor.close()
|
||
if connection:
|
||
close_database_connection(connection)
|
||
|
||
def delete_process(process_uuid):
|
||
"""Deletes a process from the database"""
|
||
connection = None
|
||
cursor = None
|
||
try:
|
||
connection = connect_to_database()
|
||
cursor = connection.cursor()
|
||
|
||
delete_query = "DELETE FROM active_processes WHERE uuid = %s"
|
||
cursor.execute(delete_query, (str(process_uuid),))
|
||
connection.commit()
|
||
|
||
logger.info(f"Deleted process: {process_uuid}")
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error deleting process: {e}")
|
||
if connection:
|
||
connection.rollback()
|
||
return False
|
||
finally:
|
||
if cursor:
|
||
cursor.close()
|
||
if connection:
|
||
close_database_connection(connection)
|
||
|
||
def cleanup_expired_processes():
|
||
"""Cleans up expired processes and marks them as completed"""
|
||
connection = None
|
||
cursor = None
|
||
try:
|
||
connection = connect_to_database()
|
||
cursor = connection.cursor()
|
||
|
||
current_time = datetime.now()
|
||
|
||
# Find expired processes
|
||
select_query = """
|
||
SELECT uuid, process_type, data FROM active_processes
|
||
WHERE status = 'active' AND end_time IS NOT NULL AND end_time <= %s
|
||
"""
|
||
cursor.execute(select_query, (current_time,))
|
||
expired_processes = cursor.fetchall()
|
||
|
||
# Update expired processes
|
||
if expired_processes:
|
||
update_query = """
|
||
UPDATE active_processes
|
||
SET status = 'expired', updated_at = %s
|
||
WHERE status = 'active' AND end_time IS NOT NULL AND end_time <= %s
|
||
"""
|
||
cursor.execute(update_query, (current_time, current_time))
|
||
connection.commit()
|
||
|
||
logger.info(f"Marked {len(expired_processes)} processes as expired")
|
||
|
||
return expired_processes
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error cleaning up expired processes: {e}")
|
||
return []
|
||
finally:
|
||
if cursor:
|
||
cursor.close()
|
||
if connection:
|
||
close_database_connection(connection)
|
||
|
||
# Process Management Task
|
||
@tasks.loop(minutes=1)
|
||
async def process_manager():
|
||
"""Main task that manages all active processes"""
|
||
try:
|
||
# Clean up expired processes first
|
||
expired_processes = cleanup_expired_processes()
|
||
|
||
# Handle expired processes
|
||
for uuid_str, process_type, data_json in expired_processes:
|
||
await handle_expired_process(uuid_str, process_type, json.loads(data_json) if data_json else {})
|
||
|
||
# Check for processes that need handling
|
||
active_processes = get_active_processes(status="active")
|
||
|
||
for process in active_processes:
|
||
if process["end_time"] and datetime.now() >= process["end_time"]:
|
||
await handle_process_completion(process)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in process manager: {e}")
|
||
|
||
# Contact Messages Task
|
||
@tasks.loop(minutes=15)
|
||
async def check_contact_messages():
|
||
"""Automatically checks for pending contact messages every 15 minutes"""
|
||
try:
|
||
connection = connect_to_database()
|
||
cursor = connection.cursor()
|
||
|
||
# Hole alle pending Nachrichten
|
||
cursor.execute("""
|
||
SELECT id, user_id, username, subject, category, priority, message,
|
||
server_context, submitted_at
|
||
FROM contact_messages
|
||
WHERE status = 'pending'
|
||
ORDER BY submitted_at ASC
|
||
""")
|
||
|
||
pending_messages = cursor.fetchall()
|
||
|
||
if pending_messages:
|
||
logger.info(f"Found {len(pending_messages)} pending contact messages")
|
||
|
||
processed_count = 0
|
||
for msg in pending_messages:
|
||
msg_id, user_id, username, subject, category, priority, message, server_context, submitted_at = msg
|
||
|
||
try:
|
||
# Hole User-Informationen von Discord
|
||
user = client.get_user(int(user_id))
|
||
if not user:
|
||
try:
|
||
user = await client.fetch_user(int(user_id))
|
||
except:
|
||
user = None
|
||
|
||
message_data = {
|
||
'user_id': user_id,
|
||
'user_name': user.display_name if user else username.split('#')[0],
|
||
'username': username,
|
||
'avatar_url': user.avatar.url if user and user.avatar else None,
|
||
'subject': subject,
|
||
'category': category,
|
||
'priority': priority,
|
||
'message': message,
|
||
'server_context': server_context
|
||
}
|
||
|
||
# Sende an Admin
|
||
success = await send_contact_message_to_admin(message_data)
|
||
|
||
if success:
|
||
# Markiere als versendet
|
||
cursor.execute("""
|
||
UPDATE contact_messages
|
||
SET status = 'sent', responded_at = %s
|
||
WHERE id = %s
|
||
""", (int(time.time()), msg_id))
|
||
processed_count += 1
|
||
logger.info(f"Sent contact message {msg_id} to admin")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error processing contact message {msg_id}: {e}")
|
||
continue
|
||
|
||
connection.commit()
|
||
|
||
if processed_count > 0:
|
||
logger.info(f"Automatically processed {processed_count} contact messages")
|
||
|
||
cursor.close()
|
||
close_database_connection(connection)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in check_contact_messages task: {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:
|
||
guild_id = data.get("guild_id")
|
||
channel_id = data.get("channel_id")
|
||
|
||
# Use process_uuid as giveaway_id since that's our new system
|
||
giveaway_id = str(process_uuid)
|
||
|
||
logger.info(f"Processing expired giveaway with ID: {giveaway_id[:8]}...")
|
||
|
||
# Try to get giveaway from memory first, then from database
|
||
giveaway = None
|
||
if giveaway_id in giveaways:
|
||
giveaway = giveaways[giveaway_id]
|
||
logger.info(f"Found giveaway {giveaway_id[:8]} in memory")
|
||
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:
|
||
# Create enhanced winner announcement
|
||
winner_embed = discord.Embed(
|
||
title="🎉 Giveaway Winners Announced!",
|
||
description=f"**{giveaway.title}** has ended!",
|
||
color=0xFFD700,
|
||
timestamp=datetime.now()
|
||
)
|
||
|
||
# Enhanced prize display with game info
|
||
if hasattr(giveaway, 'game_info') and giveaway.game_info and giveaway.game_info.get('name'):
|
||
if hasattr(giveaway, 'game_url') and giveaway.game_url:
|
||
prize_text = f"**[{giveaway.game_info['name']}]({giveaway.game_url})**"
|
||
else:
|
||
prize_text = f"**{giveaway.game_info['name']}**"
|
||
else:
|
||
if hasattr(giveaway, 'game_url') and giveaway.game_url:
|
||
prize_text = f"**[{giveaway.prize}]({giveaway.game_url})**"
|
||
else:
|
||
prize_text = f"**{giveaway.prize}**"
|
||
|
||
winner_embed.add_field(name="🎁 Prize", value=prize_text, inline=True)
|
||
winner_embed.add_field(name="🎮 Platform", value=f"**{giveaway.platform}**", inline=True)
|
||
winner_embed.add_field(name="👥 Total Participants", value=f"**{len(giveaway.participants)}**", inline=True)
|
||
|
||
# List winners
|
||
winner_list = []
|
||
for i, winner in enumerate(winners, 1):
|
||
winner_list.append(f"🏆 **#{i}** {winner.mention}")
|
||
|
||
winner_embed.add_field(name="🎊 Winners", value="\n".join(winner_list), inline=False)
|
||
|
||
# Add sponsor info if available
|
||
if hasattr(giveaway, 'sponsor') and giveaway.sponsor:
|
||
winner_embed.add_field(name="💝 Sponsored by", value=f"**{giveaway.sponsor}**", inline=False)
|
||
|
||
winner_embed.add_field(name="📧 Next Steps",
|
||
value="Winners have been sent a DM with prize claim instructions!",
|
||
inline=False)
|
||
|
||
# Set game image if available
|
||
if hasattr(giveaway, 'game_info') and giveaway.game_info and giveaway.game_info.get('image_url'):
|
||
winner_embed.set_image(url=giveaway.game_info['image_url'])
|
||
winner_embed.set_thumbnail(url="https://cdn.discordapp.com/emojis/1028701098145587302.png")
|
||
else:
|
||
winner_embed.set_thumbnail(url="https://cdn.discordapp.com/emojis/1028701098145587302.png")
|
||
|
||
winner_embed.set_footer(text="Congratulations to all winners! 🎉")
|
||
|
||
await giveaway.ctx.send(embed=winner_embed)
|
||
|
||
# Process winners with enhanced DM
|
||
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)
|
||
|
||
# Enhanced winner DM
|
||
dm_embed = discord.Embed(
|
||
title="<EFBFBD> Congratulations! You Won!",
|
||
description=f"You are a winner in the **{giveaway.title}** giveaway!",
|
||
color=0xFFD700,
|
||
timestamp=datetime.now()
|
||
)
|
||
|
||
# Enhanced prize display in DM
|
||
if hasattr(giveaway, 'game_info') and giveaway.game_info and giveaway.game_info.get('name'):
|
||
if hasattr(giveaway, 'game_url') and giveaway.game_url:
|
||
prize_text = f"**[{giveaway.game_info['name']}]({giveaway.game_url})**"
|
||
else:
|
||
prize_text = f"**{giveaway.game_info['name']}**"
|
||
else:
|
||
if hasattr(giveaway, 'game_url') and giveaway.game_url:
|
||
prize_text = f"**[{giveaway.prize}]({giveaway.game_url})**"
|
||
else:
|
||
prize_text = f"**{giveaway.prize}**"
|
||
|
||
dm_embed.add_field(name="🎁 Your Prize", value=prize_text, inline=True)
|
||
dm_embed.add_field(name="🎮 Platform", value=f"**{giveaway.platform}**", inline=True)
|
||
dm_embed.add_field(name="🏆 Position", value=f"**#{i+1}**", inline=True)
|
||
|
||
if hasattr(giveaway, 'sponsor') and giveaway.sponsor:
|
||
dm_embed.add_field(name="💝 Sponsored by", value=f"**{giveaway.sponsor}**", inline=False)
|
||
|
||
dm_embed.add_field(name="🔗 Claim Your Prize",
|
||
value=f"[Click here to claim your prize]({GIVEAWAY_WEBSITE_URL}{giveaway.guild_id}/{winner_uuid})",
|
||
inline=False)
|
||
|
||
dm_embed.set_footer(text=f"Server: {giveaway.ctx.guild.name}")
|
||
|
||
# Set game image in DM if available
|
||
if hasattr(giveaway, 'game_info') and giveaway.game_info and giveaway.game_info.get('image_url'):
|
||
dm_embed.set_thumbnail(url=giveaway.game_info['image_url'])
|
||
else:
|
||
dm_embed.set_thumbnail(url="https://cdn.discordapp.com/emojis/1028701098145587302.png")
|
||
|
||
await winner.send(embed=dm_embed)
|
||
except Exception as e:
|
||
logger.error(f"Error processing winner {winner.name}: {e}")
|
||
else:
|
||
# Enhanced "no participants" message
|
||
no_participants_embed = discord.Embed(
|
||
title="😔 Giveaway Ended",
|
||
description=f"**{giveaway.title}** has ended with no participants.",
|
||
color=0xFF6B6B,
|
||
timestamp=datetime.now()
|
||
)
|
||
|
||
no_participants_embed.add_field(name="🎁 Prize", value=f"**{giveaway.prize}**", inline=True)
|
||
no_participants_embed.add_field(name="🎮 Platform", value=f"**{giveaway.platform}**", inline=True)
|
||
|
||
if hasattr(giveaway, 'sponsor') and giveaway.sponsor:
|
||
no_participants_embed.add_field(name="💝 Sponsored by", value=f"**{giveaway.sponsor}**", inline=False)
|
||
|
||
no_participants_embed.set_footer(text="Better luck next time!")
|
||
|
||
await giveaway.ctx.send(embed=no_participants_embed)
|
||
|
||
# Clean up
|
||
if giveaway_id in giveaways:
|
||
del giveaways[giveaway_id]
|
||
update_process_status(process_uuid, "completed")
|
||
|
||
logger.info(f"Successfully completed expired giveaway {giveaway_id}")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error handling expired giveaway {process_uuid}: {e}")
|
||
update_process_status(process_uuid, "failed")
|
||
|
||
async def handle_expired_mute(process_uuid, data):
|
||
"""Handles expired mute processes"""
|
||
try:
|
||
guild_id = data.get("guild_id")
|
||
user_id = data.get("user_id")
|
||
mute_role_id = data.get("mute_role_id")
|
||
channel_id = data.get("channel_id")
|
||
reason = data.get("reason", "Automatic unmute - time expired")
|
||
|
||
if not guild_id or not user_id:
|
||
logger.error(f"Missing guild_id or user_id in mute process {process_uuid}")
|
||
update_process_status(process_uuid, "failed")
|
||
return
|
||
|
||
# Get guild and member
|
||
guild = client.get_guild(int(guild_id))
|
||
if not guild:
|
||
logger.error(f"Guild {guild_id} not found for expired mute {process_uuid}")
|
||
update_process_status(process_uuid, "failed")
|
||
return
|
||
|
||
member = guild.get_member(int(user_id))
|
||
if not member:
|
||
logger.info(f"User {user_id} no longer in guild {guild_id}, marking mute as completed")
|
||
update_process_status(process_uuid, "completed")
|
||
return
|
||
|
||
# Get mute role
|
||
mute_role = None
|
||
if mute_role_id:
|
||
mute_role = guild.get_role(int(mute_role_id))
|
||
|
||
# If mute role not found, try to get it from guild settings
|
||
if not mute_role:
|
||
guild_settings = get_guild_settings(guild_id)
|
||
if guild_settings and guild_settings.get("mute_role_id"):
|
||
mute_role = guild.get_role(int(guild_settings["mute_role_id"]))
|
||
|
||
# Remove mute role if user still has it
|
||
if mute_role and mute_role in member.roles:
|
||
await member.remove_roles(mute_role, reason="Automatic unmute - time expired")
|
||
logger.info(f"Removed mute role from user {user_id} in guild {guild_id}")
|
||
|
||
# Restore previous roles if they were saved
|
||
try:
|
||
restored_roles = await restore_user_roles(member, guild)
|
||
if restored_roles:
|
||
logger.info(f"Restored {len(restored_roles)} roles for user {user_id} in guild {guild_id}")
|
||
except Exception as e:
|
||
logger.warning(f"Could not restore roles for user {user_id}: {e}")
|
||
|
||
# Send notification to mod log channel (preferred) or original channel as fallback
|
||
notification_sent = False
|
||
|
||
# Try to send to mod log channel first
|
||
try:
|
||
guild_settings = get_guild_settings(guild_id)
|
||
if guild_settings and guild_settings.get("mod_log_channel_id"):
|
||
mod_log_channel = guild.get_channel(int(guild_settings["mod_log_channel_id"]))
|
||
if mod_log_channel:
|
||
embed = discord.Embed(
|
||
title="🔊 User Automatically Unmuted",
|
||
description=f"{member.mention} has been automatically unmuted.",
|
||
color=0x00ff00,
|
||
timestamp=datetime.now()
|
||
)
|
||
embed.add_field(name="👤 User", value=f"{member.mention}\n`{member.id}`", inline=True)
|
||
embed.add_field(name="📝 Reason", value="Mute duration expired", inline=True)
|
||
embed.add_field(name="📍 Original Channel", value=f"<#{channel_id}>" if channel_id else "Unknown", inline=True)
|
||
embed.set_footer(text=f"Process ID: {process_uuid}")
|
||
embed.set_thumbnail(url=member.display_avatar.url)
|
||
await mod_log_channel.send(embed=embed)
|
||
notification_sent = True
|
||
logger.info(f"Sent auto-unmute notification to mod log channel for user {user_id}")
|
||
except Exception as e:
|
||
logger.warning(f"Could not send unmute notification to mod log channel: {e}")
|
||
|
||
# Fallback to original channel if mod log failed
|
||
if not notification_sent and channel_id:
|
||
try:
|
||
channel = guild.get_channel(int(channel_id))
|
||
if channel:
|
||
embed = discord.Embed(
|
||
title="🔊 User Automatically Unmuted",
|
||
description=f"{member.mention} has been automatically unmuted.",
|
||
color=0x00ff00,
|
||
timestamp=datetime.now()
|
||
)
|
||
embed.add_field(name="📝 Reason", value="Mute duration expired", inline=False)
|
||
embed.set_footer(text=f"Process ID: {process_uuid}")
|
||
await channel.send(embed=embed)
|
||
notification_sent = True
|
||
logger.info(f"Sent auto-unmute notification to original channel for user {user_id}")
|
||
except Exception as e:
|
||
logger.warning(f"Could not send unmute notification to original channel: {e}")
|
||
|
||
if not notification_sent:
|
||
logger.warning(f"No channel available for auto-unmute notification for user {user_id}")
|
||
|
||
# Try to DM the user
|
||
try:
|
||
user = await client.fetch_user(int(user_id))
|
||
if user:
|
||
dm_embed = discord.Embed(
|
||
title="🔊 You have been unmuted",
|
||
description=f"Your mute in **{guild.name}** has expired.",
|
||
color=0x00ff00,
|
||
timestamp=datetime.now()
|
||
)
|
||
dm_embed.set_footer(text=f"Server: {guild.name}")
|
||
await user.send(embed=dm_embed)
|
||
except (discord.Forbidden, discord.NotFound):
|
||
pass # User has DMs disabled or doesn't exist
|
||
|
||
# Update user_mutes table to mark as auto-unmuted
|
||
try:
|
||
mute_record = await get_mute_by_process_uuid(process_uuid)
|
||
if mute_record:
|
||
await deactivate_mute(mute_record['id'], auto_unmuted=True)
|
||
logger.info(f"Updated mute record {mute_record['id']} as auto-unmuted")
|
||
except Exception as e:
|
||
logger.error(f"Error updating user_mutes table for process {process_uuid}: {e}")
|
||
|
||
logger.info(f"Successfully unmuted user {user_id} in guild {guild_id}")
|
||
update_process_status(process_uuid, "completed")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error handling expired mute {process_uuid}: {e}")
|
||
update_process_status(process_uuid, "failed")
|
||
|
||
async def handle_expired_ban(process_uuid, data):
|
||
"""Handles expired ban processes - placeholder for future implementation"""
|
||
try:
|
||
# TODO: Implement ban removal logic
|
||
guild_id = data.get("guild_id")
|
||
user_id = data.get("user_id")
|
||
|
||
logger.info(f"Ban expired for user {user_id} in guild {guild_id}")
|
||
update_process_status(process_uuid, "completed")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error handling expired ban {process_uuid}: {e}")
|
||
|
||
async def handle_process_completion(process):
|
||
"""Generic handler for process completion"""
|
||
try:
|
||
await handle_expired_process(process["uuid"], process["process_type"], process["data"] or {})
|
||
except Exception as e:
|
||
logger.error(f"Error in process completion handler: {e}")
|
||
|
||
async def restore_active_processes_on_startup():
|
||
"""Restores active processes when the bot starts up"""
|
||
try:
|
||
logger.info("Restoring active processes from database...")
|
||
|
||
# Get all active processes
|
||
processes = get_active_processes(status="active")
|
||
|
||
restored_count = 0
|
||
for process in processes:
|
||
try:
|
||
if process["process_type"] == "giveaway":
|
||
await restore_giveaway_process(process)
|
||
restored_count += 1
|
||
elif process["process_type"] == "mute":
|
||
await restore_mute_process(process)
|
||
restored_count += 1
|
||
# Add more process types as needed
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error restoring process {process['uuid']}: {e}")
|
||
|
||
logger.info(f"Successfully restored {restored_count} active processes")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error restoring active processes: {e}")
|
||
|
||
async def restore_giveaway_process(process):
|
||
"""Restores a giveaway process from the database"""
|
||
try:
|
||
data = process["data"] or {}
|
||
giveaway_id = data.get("giveaway_id")
|
||
|
||
if giveaway_id:
|
||
# Check if giveaway is still valid and not expired
|
||
if process["end_time"] and datetime.now() < process["end_time"]:
|
||
# Create a minimal giveaway object for restoration
|
||
# Note: This is a simplified restoration - full ctx recreation may not be possible
|
||
logger.info(f"Restored giveaway process {giveaway_id} from database")
|
||
|
||
# Start the process manager if it's not already running
|
||
if not process_manager.is_running():
|
||
process_manager.start()
|
||
else:
|
||
# Mark as expired if past end time
|
||
update_process_status(process["uuid"], "expired")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error restoring giveaway process: {e}")
|
||
|
||
async def restore_mute_process(process):
|
||
"""Restores a mute process from the database"""
|
||
try:
|
||
logger.info(f"Restoring mute process {process['uuid']}")
|
||
|
||
# Extract process data
|
||
data = process.get("data", {})
|
||
user_id = data.get("user_id") or process.get("user_id")
|
||
guild_id = data.get("guild_id") or process.get("guild_id")
|
||
end_time = process.get("end_time")
|
||
|
||
if not user_id or not guild_id or not end_time:
|
||
logger.error(f"Missing required data for mute process {process['uuid']}")
|
||
update_process_status(process["uuid"], "error")
|
||
return
|
||
|
||
# Get guild and user
|
||
guild = client.get_guild(int(guild_id))
|
||
if not guild:
|
||
logger.error(f"Guild {guild_id} not found for mute process {process['uuid']}")
|
||
update_process_status(process["uuid"], "error")
|
||
return
|
||
|
||
member = guild.get_member(int(user_id))
|
||
if not member:
|
||
logger.info(f"User {user_id} no longer in guild {guild_id}, marking mute process as completed")
|
||
update_process_status(process["uuid"], "completed")
|
||
return
|
||
|
||
# Check if mute has already expired
|
||
if datetime.now() >= end_time:
|
||
logger.info(f"Mute process {process['uuid']} has already expired, executing unmute")
|
||
await handle_expired_process(process["uuid"], "mute", data)
|
||
else:
|
||
# Schedule the unmute for when it's supposed to end
|
||
remaining_time = (end_time - datetime.now()).total_seconds()
|
||
logger.info(f"Scheduling mute process {process['uuid']} to complete in {remaining_time:.0f} seconds")
|
||
|
||
# Check if the user is actually still muted (has mute role)
|
||
mute_role_id = data.get("mute_role_id")
|
||
if mute_role_id:
|
||
mute_role = guild.get_role(int(mute_role_id))
|
||
if mute_role and mute_role in member.roles:
|
||
logger.info(f"User {user_id} still has mute role, mute process {process['uuid']} is valid")
|
||
else:
|
||
logger.info(f"User {user_id} no longer has mute role, marking process as completed")
|
||
update_process_status(process["uuid"], "completed")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error restoring mute process: {e}")
|
||
update_process_status(process["uuid"], "error")
|
||
|
||
#-----------------------------------------------------------------------------------------------------------
|
||
|
||
async def update_all_users(batch_size=20, pause_duration=1):
|
||
connection = connect_to_database()
|
||
cursor = connection.cursor()
|
||
cursor.execute("SELECT DISTINCT guild_id FROM guilds")
|
||
guilds = cursor.fetchall()
|
||
cursor.close()
|
||
close_database_connection(connection)
|
||
|
||
for guild_id_tuple in guilds:
|
||
guild_id = guild_id_tuple[0]
|
||
guild = client.get_guild(int(guild_id))
|
||
if guild:
|
||
members = guild.members
|
||
total_members = len(members)
|
||
for i in range(0, total_members, batch_size):
|
||
batch = members[i:i + batch_size]
|
||
|
||
for member in batch:
|
||
user_id = member.id
|
||
user_data = load_user_data_sync(user_id, guild_id)
|
||
|
||
# Daten aktualisieren
|
||
nickname = member.display_name
|
||
profile_picture = str(member.display_avatar.url) if member.display_avatar else None
|
||
join_date = member.joined_at.date() if member.joined_at else None
|
||
leave_date = None if member in guild.members else datetime.now().date()
|
||
|
||
update_user_data(user_id, guild_id, "nickname", nickname)
|
||
update_user_data(user_id, guild_id, "profile_picture", profile_picture)
|
||
update_user_data(user_id, guild_id, "join_date", join_date)
|
||
update_user_data(user_id, guild_id, "leave_date", leave_date)
|
||
|
||
# Pause nach jeder Charge
|
||
await asyncio.sleep(pause_duration)
|
||
|
||
def save_giveaway_to_db(guild_id, platform, name, prize_uuid, game_key):
|
||
connection = None
|
||
cursor = None
|
||
try:
|
||
connection = connect_to_database()
|
||
cursor = connection.cursor()
|
||
insert_query = """
|
||
INSERT INTO giveaway_data (guild_id, uuid, platform, name, game_key)
|
||
VALUES (%s, %s, %s, %s, %s)
|
||
"""
|
||
data = (guild_id, str(prize_uuid), platform, name, game_key)
|
||
cursor.execute(insert_query, data)
|
||
connection.commit()
|
||
logger.info(f"Successfully saved giveaway to database: UUID={prize_uuid}")
|
||
except Exception as e:
|
||
logger.error(f"Error saving giveaway to database: {e}")
|
||
if connection:
|
||
connection.rollback()
|
||
raise e
|
||
finally:
|
||
if cursor:
|
||
cursor.close()
|
||
if connection:
|
||
close_database_connection(connection)
|
||
|
||
def create_winner_slots_in_db(guild_id, platform, name, num_winners, game_key="PREDEFINED_GAME_KEY"):
|
||
"""Erstellt vorab Datenbank-Einträge für alle möglichen Gewinner mit eigenen UUIDs"""
|
||
winner_uuids = []
|
||
for i in range(num_winners):
|
||
connection = None
|
||
cursor = None
|
||
try:
|
||
connection = connect_to_database()
|
||
cursor = connection.cursor()
|
||
winner_uuid = uuid.uuid4()
|
||
insert_query = """
|
||
INSERT INTO giveaway_data (guild_id, uuid, platform, name, game_key, winner_dc_id)
|
||
VALUES (%s, %s, %s, %s, %s, %s)
|
||
"""
|
||
# winner_dc_id ist zunächst NULL, wird später beim Gewinn gesetzt
|
||
data = (guild_id, str(winner_uuid), platform, name, game_key, None)
|
||
cursor.execute(insert_query, data)
|
||
connection.commit()
|
||
winner_uuids.append(winner_uuid)
|
||
logger.info(f"Created winner slot {i+1}/{num_winners} with UUID: {winner_uuid}")
|
||
except Exception as e:
|
||
logger.error(f"Error creating winner slot {i+1}: {e}")
|
||
if connection:
|
||
connection.rollback()
|
||
raise e
|
||
finally:
|
||
if cursor:
|
||
cursor.close()
|
||
if connection:
|
||
close_database_connection(connection)
|
||
|
||
return winner_uuids
|
||
|
||
def save_winner_to_db(guild_id, platform, name, winner_dc_id, game_key="PREDEFINED_GAME_KEY"):
|
||
"""Erstellt einen eigenen Datenbankeintrag für jeden Gewinner mit eigener UUID"""
|
||
connection = None
|
||
cursor = None
|
||
try:
|
||
connection = connect_to_database()
|
||
cursor = connection.cursor()
|
||
winner_uuid = uuid.uuid4()
|
||
insert_query = """
|
||
INSERT INTO giveaway_data (guild_id, uuid, platform, name, game_key, winner_dc_id)
|
||
VALUES (%s, %s, %s, %s, %s, %s)
|
||
"""
|
||
data = (guild_id, str(winner_uuid), platform, name, game_key, winner_dc_id)
|
||
cursor.execute(insert_query, data)
|
||
connection.commit()
|
||
logger.info(f"Successfully saved winner to database: UUID={winner_uuid}, winner_dc_id={winner_dc_id}")
|
||
return winner_uuid
|
||
except Exception as e:
|
||
logger.error(f"Error saving winner to database: {e}")
|
||
if connection:
|
||
connection.rollback()
|
||
raise e
|
||
finally:
|
||
if cursor:
|
||
cursor.close()
|
||
if connection:
|
||
close_database_connection(connection)
|
||
|
||
async def process_sponsor_mention(ctx, sponsor: str):
|
||
"""Process sponsor mention to extract display name"""
|
||
try:
|
||
# Check if it's a user mention <@123456> or <@!123456>
|
||
mention_pattern = r'<@!?(\d+)>'
|
||
match = re.search(mention_pattern, sponsor)
|
||
|
||
if match:
|
||
user_id = int(match.group(1))
|
||
try:
|
||
# Try to get user from guild first (includes display name)
|
||
user = ctx.guild.get_member(user_id)
|
||
if user:
|
||
return user.display_name
|
||
|
||
# Fallback to fetching user globally
|
||
user = await client.fetch_user(user_id)
|
||
if user:
|
||
return user.name
|
||
|
||
# If all fails, return formatted user ID
|
||
return f"User#{user_id}"
|
||
|
||
except Exception as e:
|
||
logger.warning(f"Could not fetch user {user_id}: {e}")
|
||
return f"User#{user_id}"
|
||
else:
|
||
# Not a mention, return the text as-is
|
||
return sponsor
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error processing sponsor mention: {e}")
|
||
return sponsor
|
||
|
||
async def extract_game_info(game_url: str):
|
||
"""Extract game information from various game store URLs"""
|
||
try:
|
||
# Steam URL pattern
|
||
steam_pattern = r'https?://store\.steampowered\.com/app/(\d+)'
|
||
steam_match = re.search(steam_pattern, game_url)
|
||
|
||
if steam_match:
|
||
app_id = steam_match.group(1)
|
||
return await get_steam_game_info(app_id)
|
||
|
||
# Epic Games Store URL pattern
|
||
epic_pattern = r'https?://store\.epicgames\.com/[^/]+/p/([^/?]+)'
|
||
epic_match = re.search(epic_pattern, game_url)
|
||
|
||
if epic_match:
|
||
game_slug = epic_match.group(1)
|
||
return {
|
||
'name': game_slug.replace('-', ' ').title(),
|
||
'image_url': None,
|
||
'store_url': game_url,
|
||
'platform': 'Epic Games'
|
||
}
|
||
|
||
# Generic fallback
|
||
return {
|
||
'name': None,
|
||
'image_url': None,
|
||
'store_url': game_url,
|
||
'platform': 'Unknown'
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error extracting game info from URL {game_url}: {e}")
|
||
return None
|
||
|
||
async def get_steam_game_info(app_id: str):
|
||
"""Get game information from Steam API"""
|
||
try:
|
||
async with aiohttp.ClientSession() as session:
|
||
# Steam Store API
|
||
steam_api_url = f"https://store.steampowered.com/api/appdetails?appids={app_id}"
|
||
|
||
async with session.get(steam_api_url) as response:
|
||
if response.status == 200:
|
||
data = await response.json()
|
||
|
||
if app_id in data and data[app_id]['success']:
|
||
game_data = data[app_id]['data']
|
||
|
||
return {
|
||
'name': game_data.get('name', 'Unknown Game'),
|
||
'image_url': game_data.get('header_image'),
|
||
'store_url': f"https://store.steampowered.com/app/{app_id}",
|
||
'platform': 'Steam',
|
||
'description': game_data.get('short_description', ''),
|
||
'price': game_data.get('price_overview', {}).get('final_formatted', 'Free')
|
||
}
|
||
|
||
# Fallback if API fails
|
||
return {
|
||
'name': None,
|
||
'image_url': f"https://cdn.akamai.steamstatic.com/steam/apps/{app_id}/header.jpg",
|
||
'store_url': f"https://store.steampowered.com/app/{app_id}",
|
||
'platform': 'Steam'
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error fetching Steam game info for app {app_id}: {e}")
|
||
return {
|
||
'name': None,
|
||
'image_url': f"https://cdn.akamai.steamstatic.com/steam/apps/{app_id}/header.jpg",
|
||
'store_url': f"https://store.steampowered.com/app/{app_id}",
|
||
'platform': 'Steam'
|
||
}
|
||
|
||
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, sponsor=None, game_url=None, game_info=None, sponsor_display=None):
|
||
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.sponsor = sponsor # Original sponsor field (for embed fields)
|
||
self.sponsor_display = sponsor_display # Display name for footer
|
||
self.game_url = game_url # Game store URL
|
||
self.game_info = game_info # Extracted game information
|
||
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
|
||
self.message_id = None # Store the giveaway message ID for editing
|
||
|
||
# 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.sponsor = data.get("sponsor", None) # Restore sponsor
|
||
giveaway.sponsor_display = data.get("sponsor_display", None) # Restore sponsor display
|
||
giveaway.game_url = data.get("game_url", None) # Restore game URL
|
||
giveaway.game_info = data.get("game_info", None) # Restore game info
|
||
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
|
||
giveaway.message_id = data.get("message_id", None) # Restore message ID
|
||
return giveaway
|
||
|
||
def create_process_entry(self):
|
||
"""Creates an entry in the active_processes table for this giveaway"""
|
||
try:
|
||
giveaway_data = {
|
||
"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,
|
||
"sponsor": self.sponsor, # Include sponsor in data
|
||
"sponsor_display": self.sponsor_display, # Include sponsor display name
|
||
"game_url": self.game_url, # Include game URL
|
||
"game_info": self.game_info, # Include game info
|
||
"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 giveaway ID and current data"""
|
||
try:
|
||
# Update process data with current giveaway information
|
||
giveaway_data = {
|
||
"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,
|
||
"sponsor": self.sponsor,
|
||
"sponsor_display": self.sponsor_display,
|
||
"game_url": self.game_url,
|
||
"game_info": self.game_info,
|
||
"winner_uuids": [str(uuid) for uuid in self.winner_uuids],
|
||
"prize_uuid": str(self.prize_uuid),
|
||
"game_key": self.game_key,
|
||
"message_id": self.message_id, # Include message ID
|
||
"participants": [{"id": p.id, "name": p.name} for p in self.participants]
|
||
}
|
||
|
||
if self.process_uuid:
|
||
update_process_status(self.process_uuid, "active", data=giveaway_data)
|
||
logger.info(f"Updated process data for giveaway {giveaway_id}")
|
||
except Exception as e:
|
||
logger.error(f"Error updating process data: {e}")
|
||
|
||
def save_giveaway_data(self):
|
||
"""Save current giveaway data to database"""
|
||
try:
|
||
giveaway_data = {
|
||
"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,
|
||
"sponsor": self.sponsor,
|
||
"sponsor_display": self.sponsor_display,
|
||
"game_url": self.game_url,
|
||
"game_info": self.game_info,
|
||
"winner_uuids": [str(uuid) for uuid in self.winner_uuids],
|
||
"prize_uuid": str(self.prize_uuid),
|
||
"game_key": self.game_key,
|
||
"message_id": self.message_id, # Include message ID
|
||
"participants": [{"id": p.id, "name": p.name} for p in self.participants]
|
||
}
|
||
|
||
if self.process_uuid:
|
||
update_process_status(self.process_uuid, "active", data=giveaway_data)
|
||
except Exception as e:
|
||
logger.error(f"Error saving giveaway data: {e}")
|
||
|
||
async def update_giveaway_message(self):
|
||
"""Update the original giveaway message with current data"""
|
||
try:
|
||
if not self.message_id:
|
||
logger.warning("No message ID stored for giveaway, cannot update message")
|
||
return False
|
||
|
||
# Get the channel and message
|
||
channel = self.ctx.channel
|
||
try:
|
||
message = await channel.fetch_message(self.message_id)
|
||
except discord.NotFound:
|
||
logger.warning(f"Giveaway message {self.message_id} not found")
|
||
return False
|
||
except discord.Forbidden:
|
||
logger.warning(f"No permission to access message {self.message_id}")
|
||
return False
|
||
|
||
# Recreate the embed with updated data
|
||
unix_end_time = int(time.mktime(self.end_time.timetuple()))
|
||
|
||
embed = discord.Embed(
|
||
title=f"🎉 {self.title}",
|
||
description=f"✨ {self.subtitle}",
|
||
color=0xFFD700,
|
||
timestamp=self.end_time
|
||
)
|
||
|
||
# Game information with clickable link if available
|
||
if self.game_info and self.game_info.get('name'):
|
||
if self.game_url:
|
||
prize_text = f"**[{self.game_info['name']}]({self.game_url})**"
|
||
else:
|
||
prize_text = f"**{self.game_info['name']}**"
|
||
|
||
# Add additional game details if available
|
||
if self.game_info.get('description'):
|
||
prize_text += f"\n*{self.game_info['description'][:100]}{'...' if len(self.game_info.get('description', '')) > 100 else ''}*"
|
||
else:
|
||
# Fallback to original prize text with URL if available
|
||
if self.game_url:
|
||
prize_text = f"**[{self.prize}]({self.game_url})**"
|
||
else:
|
||
prize_text = f"**{self.prize}**"
|
||
|
||
# Main prize information
|
||
embed.add_field(name="🎁 Prize", value=prize_text, inline=True)
|
||
embed.add_field(name="🎮 Platform", value=f"**{self.platform}**", inline=True)
|
||
embed.add_field(name="👥 Winners", value=f"**{self.num_winners}**", inline=True)
|
||
|
||
# Time information
|
||
embed.add_field(name="⏰ Ends", value=f"<t:{unix_end_time}:F>\n<t:{unix_end_time}:R>", inline=False)
|
||
|
||
# Sponsor information if provided
|
||
if self.sponsor:
|
||
embed.add_field(name="💝 Sponsored by", value=f"**{self.sponsor}**", inline=False)
|
||
# Use sponsor_display for footer if available, otherwise fallback to sponsor
|
||
footer_sponsor = self.sponsor_display if self.sponsor_display else self.sponsor
|
||
embed.set_footer(text=f"Giveaway ID: {str(self.process_uuid)[:8]}... • Sponsored by {footer_sponsor}")
|
||
else:
|
||
embed.set_footer(text=f"Giveaway ID: {str(self.process_uuid)[:8]}... • Good luck!")
|
||
|
||
# Set game image if available, otherwise use default
|
||
if self.game_info and self.game_info.get('image_url'):
|
||
embed.set_image(url=self.game_info['image_url'])
|
||
embed.set_thumbnail(url="https://cdn.discordapp.com/emojis/1028701098145587302.png")
|
||
else:
|
||
embed.set_thumbnail(url="https://cdn.discordapp.com/emojis/1028701098145587302.png")
|
||
|
||
# Update the message (keep the same view/buttons)
|
||
await message.edit(embed=embed)
|
||
logger.info(f"Updated giveaway message {self.message_id}")
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error updating giveaway message: {e}")
|
||
return False
|
||
|
||
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, sponsor: str = None, game_url: str = None):
|
||
"""Creates a new giveaway, only available for admins.
|
||
|
||
Parameters:
|
||
- platform: Gaming platform (Steam, Epic, etc.)
|
||
- prize: What's being given away
|
||
- num_winners: Number of winners to select
|
||
- title: Main giveaway title
|
||
- subtitle: Additional description
|
||
- duration: Duration (e.g. 1d, 30m, 2h)
|
||
- sponsor: Optional sponsor mention/name (e.g. @SimolZimol)
|
||
- game_url: Optional game store URL (Steam, Epic, etc.) for clickable link and auto-image
|
||
"""
|
||
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
|
||
|
||
# Parse duration with more formats
|
||
if duration.endswith("m"):
|
||
minutes = int(duration[:-1])
|
||
end_time = datetime.now() + timedelta(minutes=minutes)
|
||
elif duration.endswith("h"):
|
||
hours = int(duration[:-1])
|
||
end_time = datetime.now() + timedelta(hours=hours)
|
||
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, 'h' for hours, or 'd' for days (e.g. 30m, 2h, 1d).")
|
||
return
|
||
|
||
# Extract game information from URL if provided
|
||
game_info = None
|
||
if game_url:
|
||
game_info = await extract_game_info(game_url)
|
||
|
||
# Process sponsor mention to get display name
|
||
sponsor_display = None
|
||
if sponsor:
|
||
sponsor_display = await process_sponsor_mention(ctx, sponsor)
|
||
|
||
# Create new giveaway
|
||
giveaway = Giveaway(ctx, platform, prize, num_winners, title, subtitle, duration, end_time, sponsor, game_url, game_info, sponsor_display)
|
||
|
||
# Use the process UUID as the giveaway ID (more reliable and unique)
|
||
giveaway_id = str(giveaway.process_uuid)
|
||
giveaways[giveaway_id] = giveaway
|
||
|
||
# Update the process data with the actual giveaway ID
|
||
giveaway.update_process_data(giveaway_id)
|
||
|
||
# Create enhanced button
|
||
button = Button(label="🎉 Join Giveaway", style=discord.ButtonStyle.green, custom_id=f"giveaway_{giveaway_id}", emoji="🎁")
|
||
view = View()
|
||
view.add_item(button)
|
||
unix_end_time = int(time.mktime(end_time.timetuple()))
|
||
|
||
# Enhanced embed design
|
||
embed = discord.Embed(
|
||
title=f"🎉 {title}",
|
||
description=f"✨ {subtitle}",
|
||
color=0xFFD700, # Gold color for premium look
|
||
timestamp=end_time
|
||
)
|
||
|
||
# Game information with clickable link if available
|
||
if game_info and game_info.get('name'):
|
||
if game_url:
|
||
prize_text = f"**[{game_info['name']}]({game_url})**"
|
||
else:
|
||
prize_text = f"**{game_info['name']}**"
|
||
|
||
# Add additional game details if available
|
||
if game_info.get('description'):
|
||
prize_text += f"\n*{game_info['description'][:100]}{'...' if len(game_info.get('description', '')) > 100 else ''}*"
|
||
else:
|
||
# Fallback to original prize text with URL if available
|
||
if game_url:
|
||
prize_text = f"**[{prize}]({game_url})**"
|
||
else:
|
||
prize_text = f"**{prize}**"
|
||
|
||
# Main prize information
|
||
embed.add_field(name="🎁 Prize", value=prize_text, inline=True)
|
||
embed.add_field(name="🎮 Platform", value=f"**{platform}**", inline=True)
|
||
embed.add_field(name="👥 Winners", value=f"**{num_winners}**", inline=True)
|
||
|
||
# Time information
|
||
embed.add_field(name="⏰ Ends", value=f"<t:{unix_end_time}:F>\n<t:{unix_end_time}:R>", inline=False)
|
||
|
||
# Sponsor information if provided
|
||
if sponsor:
|
||
embed.add_field(name="💝 Sponsored by", value=f"**{sponsor}**", inline=False)
|
||
# Use sponsor_display for footer if available, otherwise fallback to sponsor
|
||
footer_sponsor = sponsor_display if sponsor_display else sponsor
|
||
embed.set_footer(text=f"Giveaway ID: {giveaway_id[:8]}... • Sponsored by {footer_sponsor}")
|
||
else:
|
||
embed.set_footer(text=f"Giveaway ID: {giveaway_id[:8]}... • Good luck!")
|
||
|
||
# Set game image if available, otherwise use default
|
||
if game_info and game_info.get('image_url'):
|
||
embed.set_image(url=game_info['image_url'])
|
||
embed.set_thumbnail(url="https://cdn.discordapp.com/emojis/1028701098145587302.png")
|
||
else:
|
||
embed.set_thumbnail(url="https://cdn.discordapp.com/emojis/1028701098145587302.png")
|
||
|
||
# Send the message and store the message ID
|
||
message = await ctx.send(embed=embed, view=view)
|
||
giveaway.message_id = message.id
|
||
|
||
# Save the updated giveaway data including message_id
|
||
giveaway.save_giveaway_data()
|
||
|
||
# Start the process manager if it's not already running
|
||
if not process_manager.is_running():
|
||
process_manager.start()
|
||
|
||
@client.hybrid_command()
|
||
async def editgiveaway(ctx, giveaway_id: str, field: str, *, new_value: str):
|
||
"""Edit an active giveaway (Only available for admins)
|
||
|
||
Parameters:
|
||
- giveaway_id: The giveaway ID (first 8 characters of UUID are enough)
|
||
- field: What to edit (sponsor, title, subtitle, prize, duration)
|
||
- new_value: The new value for the field
|
||
"""
|
||
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 edit giveaways.")
|
||
return
|
||
|
||
# Find giveaway by partial ID
|
||
matching_giveaway = None
|
||
matching_id = None
|
||
|
||
for giv_id, giveaway in giveaways.items():
|
||
if giv_id.startswith(giveaway_id) or giveaway_id in giv_id:
|
||
matching_giveaway = giveaway
|
||
matching_id = giv_id
|
||
break
|
||
|
||
if not matching_giveaway:
|
||
await ctx.send(f"❌ No active giveaway found with ID starting with `{giveaway_id}`")
|
||
return
|
||
|
||
# Validate field
|
||
valid_fields = ["sponsor", "title", "subtitle", "prize", "duration"]
|
||
if field.lower() not in valid_fields:
|
||
await ctx.send(f"❌ Invalid field. Valid fields: {', '.join(valid_fields)}")
|
||
return
|
||
|
||
# Store old value for comparison
|
||
old_value = getattr(matching_giveaway, field.lower(), "Not set")
|
||
|
||
try:
|
||
# Edit the field
|
||
if field.lower() == "sponsor":
|
||
matching_giveaway.sponsor = new_value
|
||
# Process sponsor mention for display
|
||
matching_giveaway.sponsor_display = await process_sponsor_mention(ctx, new_value)
|
||
elif field.lower() == "title":
|
||
matching_giveaway.title = new_value
|
||
elif field.lower() == "subtitle":
|
||
matching_giveaway.subtitle = new_value
|
||
elif field.lower() == "prize":
|
||
matching_giveaway.prize = new_value
|
||
elif field.lower() == "duration":
|
||
# Parse new duration and update end_time
|
||
if new_value.endswith("m"):
|
||
minutes = int(new_value[:-1])
|
||
new_end_time = datetime.now() + timedelta(minutes=minutes)
|
||
elif new_value.endswith("h"):
|
||
hours = int(new_value[:-1])
|
||
new_end_time = datetime.now() + timedelta(hours=hours)
|
||
elif new_value.endswith("d"):
|
||
days = int(new_value[:-1])
|
||
new_end_time = datetime.now() + timedelta(days=days)
|
||
else:
|
||
await ctx.send("❌ Invalid duration format. Use 'm' for minutes, 'h' for hours, or 'd' for days.")
|
||
return
|
||
|
||
matching_giveaway.duration = new_value
|
||
matching_giveaway.end_time = new_end_time
|
||
|
||
# Update the process end_time in database
|
||
try:
|
||
# Use existing update_process_status function to update end_time
|
||
connection = connect_to_database()
|
||
cursor = connection.cursor()
|
||
|
||
update_query = "UPDATE active_processes SET end_time = %s WHERE uuid = %s"
|
||
cursor.execute(update_query, (new_end_time, str(matching_giveaway.process_uuid)))
|
||
connection.commit()
|
||
|
||
cursor.close()
|
||
close_database_connection(connection)
|
||
|
||
logger.info(f"Updated process end_time for giveaway {matching_giveaway.process_uuid}")
|
||
except Exception as e:
|
||
logger.error(f"Error updating process end_time: {e}")
|
||
|
||
# Save updated giveaway data to database
|
||
try:
|
||
matching_giveaway.save_giveaway_data()
|
||
except Exception as e:
|
||
logger.error(f"Error updating process data: {e}")
|
||
|
||
# Update the giveaway message with new data
|
||
message_updated = False
|
||
try:
|
||
message_updated = await matching_giveaway.update_giveaway_message()
|
||
if not message_updated:
|
||
logger.warning(f"Could not update giveaway message for {matching_id[:8]}")
|
||
except Exception as e:
|
||
logger.error(f"Error updating giveaway message: {e}")
|
||
|
||
# Success message
|
||
embed = discord.Embed(
|
||
title="✅ Giveaway Updated",
|
||
description=f"Successfully updated the giveaway!",
|
||
color=0x00ff00,
|
||
timestamp=datetime.now()
|
||
)
|
||
|
||
embed.add_field(name="🆔 Giveaway ID", value=f"`{matching_id[:8]}...`", inline=True)
|
||
embed.add_field(name="📝 Field Updated", value=f"**{field.title()}**", inline=True)
|
||
embed.add_field(name="👤 Updated by", value=ctx.author.mention, inline=True)
|
||
|
||
embed.add_field(name="🔄 Changes", value=f"**Old:** {old_value}\n**New:** {new_value}", inline=False)
|
||
|
||
if field.lower() == "duration":
|
||
embed.add_field(name="⏰ New End Time",
|
||
value=f"<t:{int(matching_giveaway.end_time.timestamp())}:F>\n<t:{int(matching_giveaway.end_time.timestamp())}:R>",
|
||
inline=False)
|
||
|
||
# Show message update status
|
||
if message_updated:
|
||
embed.add_field(name="📝 Message Status", value="✅ Original post updated", inline=False)
|
||
embed.set_footer(text="Changes are immediately effective • Original giveaway post updated")
|
||
else:
|
||
embed.add_field(name="📝 Message Status", value="⚠️ Could not update original post", inline=False)
|
||
embed.set_footer(text="Changes are effective • Original post may need manual update")
|
||
|
||
await ctx.send(embed=embed)
|
||
|
||
logger.info(f"Giveaway {matching_id[:8]} edited by {ctx.author.id}: {field} = {new_value}")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error editing giveaway: {e}")
|
||
await ctx.send(f"❌ Error editing giveaway: {str(e)}")
|
||
|
||
@client.hybrid_command()
|
||
async def updategiveawaymessage(ctx, giveaway_id: str):
|
||
"""Manually update a giveaway message (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 update giveaway messages.")
|
||
return
|
||
|
||
# Find giveaway by partial ID
|
||
matching_giveaway = None
|
||
matching_id = None
|
||
|
||
for giv_id, giveaway in giveaways.items():
|
||
if giv_id.startswith(giveaway_id) or giveaway_id in giv_id:
|
||
matching_giveaway = giveaway
|
||
matching_id = giv_id
|
||
break
|
||
|
||
if not matching_giveaway:
|
||
await ctx.send(f"❌ No active giveaway found with ID starting with `{giveaway_id}`")
|
||
return
|
||
|
||
try:
|
||
# Try to update the message
|
||
message_updated = await matching_giveaway.update_giveaway_message()
|
||
|
||
if message_updated:
|
||
embed = discord.Embed(
|
||
title="✅ Message Updated",
|
||
description=f"Successfully updated the giveaway message!",
|
||
color=0x00ff00,
|
||
timestamp=datetime.now()
|
||
)
|
||
embed.add_field(name="🆔 Giveaway ID", value=f"`{matching_id[:8]}...`", inline=True)
|
||
embed.add_field(name="📝 Updated by", value=ctx.author.mention, inline=True)
|
||
embed.set_footer(text="Original giveaway post has been refreshed")
|
||
else:
|
||
embed = discord.Embed(
|
||
title="❌ Update Failed",
|
||
description=f"Could not update the giveaway message.",
|
||
color=0xff0000,
|
||
timestamp=datetime.now()
|
||
)
|
||
embed.add_field(name="🆔 Giveaway ID", value=f"`{matching_id[:8]}...`", inline=True)
|
||
embed.add_field(name="❓ Possible Reasons",
|
||
value="• Message was deleted\n• Bot lacks permissions\n• Message ID not found",
|
||
inline=False)
|
||
embed.set_footer(text="Check logs for more details")
|
||
|
||
await ctx.send(embed=embed)
|
||
|
||
logger.info(f"Manual message update attempted for giveaway {matching_id[:8]} by {ctx.author.id}: {message_updated}")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error updating giveaway message: {e}")
|
||
await ctx.send(f"❌ Error updating giveaway message: {str(e)}")
|
||
|
||
@client.hybrid_command()
|
||
async def listgiveaways(ctx):
|
||
"""List all active giveaways in this server (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 list giveaways.")
|
||
return
|
||
|
||
server_giveaways = []
|
||
|
||
# First, check memory giveaways
|
||
for giv_id, giveaway in giveaways.items():
|
||
if giveaway.guild_id == guild_id and not giveaway.is_finished():
|
||
server_giveaways.append((giv_id, giveaway, "memory"))
|
||
|
||
# Also check active_processes table for giveaways not in memory
|
||
try:
|
||
active_processes = get_active_processes(process_type="giveaway", guild_id=guild_id)
|
||
|
||
for process in active_processes:
|
||
process_uuid = process["uuid"]
|
||
|
||
# Skip if already in memory
|
||
if str(process_uuid) in giveaways:
|
||
continue
|
||
|
||
# Check if process is still active and not expired
|
||
if process["status"] == "active" and process["end_time"] > datetime.now():
|
||
try:
|
||
# Create a temporary giveaway object from process data
|
||
guild = ctx.guild
|
||
channel = guild.get_channel(process["channel_id"])
|
||
|
||
if channel:
|
||
# Create minimal context
|
||
class MinimalContext:
|
||
def __init__(self, channel):
|
||
self.channel = channel
|
||
self.guild = channel.guild
|
||
|
||
temp_ctx = MinimalContext(channel)
|
||
temp_giveaway = Giveaway.from_process_data(temp_ctx, process["data"])
|
||
temp_giveaway.end_time = process["end_time"]
|
||
temp_giveaway.process_uuid = process_uuid
|
||
|
||
server_giveaways.append((str(process_uuid), temp_giveaway, "database"))
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error creating temp giveaway from process {process_uuid}: {e}")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error fetching active processes for giveaways: {e}")
|
||
|
||
if not server_giveaways:
|
||
embed = discord.Embed(
|
||
title="📋 No Active Giveaways",
|
||
description="There are currently no active giveaways in this server.",
|
||
color=0x3498db
|
||
)
|
||
await ctx.send(embed=embed)
|
||
return
|
||
|
||
# Create list embed
|
||
embed = discord.Embed(
|
||
title="🎉 Active Giveaways",
|
||
description=f"Found **{len(server_giveaways)}** active giveaway(s) in this server:",
|
||
color=0xFFD700,
|
||
timestamp=datetime.now()
|
||
)
|
||
|
||
for giv_id, giveaway, source in server_giveaways[:10]: # Limit to 10 for readability
|
||
# Calculate remaining time
|
||
remaining = giveaway.end_time - datetime.now()
|
||
if remaining.total_seconds() > 0:
|
||
days = remaining.days
|
||
hours, remainder = divmod(remaining.seconds, 3600)
|
||
minutes, _ = divmod(remainder, 60)
|
||
|
||
if days > 0:
|
||
time_left = f"{days}d {hours}h {minutes}m"
|
||
elif hours > 0:
|
||
time_left = f"{hours}h {minutes}m"
|
||
else:
|
||
time_left = f"{minutes}m"
|
||
else:
|
||
time_left = "Expired (processing...)"
|
||
|
||
# Giveaway info
|
||
prize_text = giveaway.prize
|
||
if hasattr(giveaway, 'game_info') and giveaway.game_info and giveaway.game_info.get('name'):
|
||
prize_text = giveaway.game_info['name']
|
||
|
||
giveaway_info = f"**Prize:** {prize_text}\n"
|
||
giveaway_info += f"**Platform:** {giveaway.platform}\n"
|
||
giveaway_info += f"**Winners:** {giveaway.num_winners}\n"
|
||
giveaway_info += f"**Participants:** {len(giveaway.participants)}\n"
|
||
giveaway_info += f"**Time Left:** {time_left}\n"
|
||
|
||
if hasattr(giveaway, 'sponsor_display') and giveaway.sponsor_display:
|
||
giveaway_info += f"**Sponsor:** {giveaway.sponsor_display}\n"
|
||
|
||
# Add source indicator
|
||
source_emoji = "💾" if source == "database" else "🧠"
|
||
source_text = "DB" if source == "database" else "MEM"
|
||
|
||
giveaway_info += f"**ID:** `{giv_id[:8]}...` {source_emoji}"
|
||
|
||
embed.add_field(
|
||
name=f"🎁 {giveaway.title}",
|
||
value=giveaway_info,
|
||
inline=True
|
||
)
|
||
|
||
if len(server_giveaways) > 10:
|
||
embed.add_field(
|
||
name="ℹ️ Note",
|
||
value=f"Showing first 10 of {len(server_giveaways)} giveaways.",
|
||
inline=False
|
||
)
|
||
|
||
# Add helpful footer
|
||
db_count = sum(1 for _, _, source in server_giveaways if source == "database")
|
||
if db_count > 0:
|
||
embed.set_footer(text=f"💾 = Loaded from DB | Use /editgiveaway <id> <field> <value> to edit | Use /loadgiveaway <id> to load into memory")
|
||
else:
|
||
embed.set_footer(text="Use /editgiveaway <id> <field> <value> to edit a giveaway")
|
||
|
||
await ctx.send(embed=embed)
|
||
|
||
@client.hybrid_command()
|
||
async def loadgiveaway(ctx, giveaway_id: str):
|
||
"""Load a giveaway from database into memory (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 load giveaways.")
|
||
return
|
||
|
||
try:
|
||
# Find giveaway in active_processes
|
||
active_processes = get_active_processes(process_type="giveaway", guild_id=guild_id)
|
||
|
||
matching_process = None
|
||
for process in active_processes:
|
||
if str(process["uuid"]).startswith(giveaway_id) or giveaway_id in str(process["uuid"]):
|
||
matching_process = process
|
||
break
|
||
|
||
if not matching_process:
|
||
await ctx.send(f"❌ No giveaway found with ID starting with `{giveaway_id}`")
|
||
return
|
||
|
||
process_uuid = matching_process["uuid"]
|
||
|
||
# Check if already loaded in memory
|
||
if str(process_uuid) in giveaways:
|
||
await ctx.send(f"ℹ️ Giveaway `{str(process_uuid)[:8]}...` is already loaded in memory.")
|
||
return
|
||
|
||
# Create giveaway object and load into memory
|
||
guild = ctx.guild
|
||
channel = guild.get_channel(matching_process["channel_id"])
|
||
|
||
if not channel:
|
||
await ctx.send(f"❌ Original channel not found. Cannot load giveaway.")
|
||
return
|
||
|
||
# Create minimal context
|
||
class MinimalContext:
|
||
def __init__(self, channel):
|
||
self.channel = channel
|
||
self.guild = channel.guild
|
||
|
||
temp_ctx = MinimalContext(channel)
|
||
giveaway = Giveaway.from_process_data(temp_ctx, matching_process["data"])
|
||
giveaway.end_time = matching_process["end_time"]
|
||
giveaway.process_uuid = process_uuid
|
||
|
||
# Restore participants from stored data
|
||
stored_participants = matching_process["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}")
|
||
|
||
# Load into memory
|
||
giveaways[str(process_uuid)] = giveaway
|
||
|
||
# Success message
|
||
embed = discord.Embed(
|
||
title="✅ Giveaway Loaded",
|
||
description=f"Successfully loaded giveaway into memory!",
|
||
color=0x00ff00,
|
||
timestamp=datetime.now()
|
||
)
|
||
|
||
embed.add_field(name="🎁 Title", value=giveaway.title, inline=True)
|
||
embed.add_field(name="🆔 ID", value=f"`{str(process_uuid)[:8]}...`", inline=True)
|
||
embed.add_field(name="👥 Participants", value=f"{len(giveaway.participants)}", inline=True)
|
||
|
||
remaining = giveaway.end_time - datetime.now()
|
||
if remaining.total_seconds() > 0:
|
||
embed.add_field(name="⏰ Time Left", value=f"<t:{int(giveaway.end_time.timestamp())}:R>", inline=True)
|
||
else:
|
||
embed.add_field(name="⏰ Status", value="⚠️ Expired (will process soon)", inline=True)
|
||
|
||
embed.set_footer(text="Giveaway is now fully functional in memory")
|
||
|
||
await ctx.send(embed=embed)
|
||
|
||
logger.info(f"Loaded giveaway {str(process_uuid)[:8]} into memory by {ctx.author.id}")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error loading giveaway: {e}")
|
||
await ctx.send(f"❌ Error loading giveaway: {str(e)}")
|
||
|
||
@client.hybrid_command()
|
||
async def loadallgiveaways(ctx):
|
||
"""Load all giveaways from database into memory (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 load giveaways.")
|
||
return
|
||
|
||
try:
|
||
# Get all active giveaway processes for this guild
|
||
active_processes = get_active_processes(process_type="giveaway", guild_id=guild_id)
|
||
|
||
if not active_processes:
|
||
await ctx.send("📋 No giveaways found in database.")
|
||
return
|
||
|
||
loaded_count = 0
|
||
already_loaded = 0
|
||
failed_count = 0
|
||
failed_details = []
|
||
|
||
embed = discord.Embed(
|
||
title="🔄 Loading Giveaways...",
|
||
description="Processing giveaways from database...",
|
||
color=0xffaa00,
|
||
timestamp=datetime.now()
|
||
)
|
||
|
||
message = await ctx.send(embed=embed)
|
||
|
||
for process in active_processes:
|
||
process_uuid = process["uuid"]
|
||
|
||
# Skip if already in memory
|
||
if str(process_uuid) in giveaways:
|
||
already_loaded += 1
|
||
continue
|
||
|
||
try:
|
||
# Get channel
|
||
guild = ctx.guild
|
||
channel = guild.get_channel(process["channel_id"])
|
||
|
||
if not channel:
|
||
failed_count += 1
|
||
failed_details.append(f"Channel not found for {str(process_uuid)[:8]}...")
|
||
continue
|
||
|
||
# Create minimal context
|
||
class MinimalContext:
|
||
def __init__(self, channel):
|
||
self.channel = channel
|
||
self.guild = channel.guild
|
||
|
||
temp_ctx = MinimalContext(channel)
|
||
giveaway = Giveaway.from_process_data(temp_ctx, process["data"])
|
||
giveaway.end_time = process["end_time"]
|
||
giveaway.process_uuid = process_uuid
|
||
|
||
# Restore participants
|
||
stored_participants = process["data"].get("participants", [])
|
||
for participant_data in stored_participants:
|
||
try:
|
||
user = await client.fetch_user(participant_data["id"])
|
||
giveaway.participants.append(user)
|
||
except:
|
||
pass # Skip invalid participants
|
||
|
||
# Load into memory
|
||
giveaways[str(process_uuid)] = giveaway
|
||
loaded_count += 1
|
||
|
||
except Exception as e:
|
||
failed_count += 1
|
||
failed_details.append(f"{str(process_uuid)[:8]}...: {str(e)[:50]}")
|
||
logger.error(f"Failed to load giveaway {process_uuid}: {e}")
|
||
|
||
# Final result
|
||
embed = discord.Embed(
|
||
title="✅ Giveaway Loading Complete",
|
||
color=0x00ff00,
|
||
timestamp=datetime.now()
|
||
)
|
||
|
||
embed.add_field(name="✅ Loaded", value=f"{loaded_count} giveaways", inline=True)
|
||
embed.add_field(name="ℹ️ Already in Memory", value=f"{already_loaded} giveaways", inline=True)
|
||
embed.add_field(name="❌ Failed", value=f"{failed_count} giveaways", inline=True)
|
||
|
||
if failed_details:
|
||
embed.add_field(name="📋 Failed Details", value="\n".join(failed_details[:5]), inline=False)
|
||
if len(failed_details) > 5:
|
||
embed.add_field(name="", value=f"... and {len(failed_details) - 5} more", inline=False)
|
||
|
||
embed.set_footer(text="All available giveaways have been processed")
|
||
|
||
await message.edit(embed=embed)
|
||
|
||
logger.info(f"Loaded {loaded_count} giveaways into memory by {ctx.author.id}")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error loading all giveaways: {e}")
|
||
await ctx.send(f"❌ Error loading giveaways: {str(e)}")
|
||
|
||
async def load_active_giveaways():
|
||
"""Load all active giveaways from database into memory on startup"""
|
||
try:
|
||
# Get all active giveaway processes
|
||
active_processes = get_active_processes(process_type="giveaway")
|
||
|
||
if not active_processes:
|
||
logger.info("No active giveaways found in database")
|
||
return
|
||
|
||
loaded_count = 0
|
||
failed_count = 0
|
||
|
||
for process in active_processes:
|
||
try:
|
||
process_uuid = process["uuid"]
|
||
|
||
# Skip if already in memory
|
||
if str(process_uuid) in giveaways:
|
||
continue
|
||
|
||
# Get guild and channel
|
||
guild_id = process["guild_id"]
|
||
channel_id = process["channel_id"]
|
||
|
||
guild = client.get_guild(guild_id)
|
||
if not guild:
|
||
failed_count += 1
|
||
continue
|
||
|
||
channel = guild.get_channel(channel_id)
|
||
if not channel:
|
||
failed_count += 1
|
||
continue
|
||
|
||
# Create minimal context
|
||
class MinimalContext:
|
||
def __init__(self, channel):
|
||
self.channel = channel
|
||
self.guild = channel.guild
|
||
|
||
temp_ctx = MinimalContext(channel)
|
||
giveaway = Giveaway.from_process_data(temp_ctx, process["data"])
|
||
giveaway.end_time = process["end_time"]
|
||
giveaway.process_uuid = process_uuid
|
||
|
||
# Restore participants
|
||
stored_participants = process["data"].get("participants", [])
|
||
for participant_data in stored_participants:
|
||
try:
|
||
user = await client.fetch_user(participant_data["id"])
|
||
giveaway.participants.append(user)
|
||
except:
|
||
pass # Skip invalid participants
|
||
|
||
# Load into memory
|
||
giveaways[str(process_uuid)] = giveaway
|
||
loaded_count += 1
|
||
|
||
except Exception as e:
|
||
failed_count += 1
|
||
logger.error(f"Failed to load giveaway {process.get('uuid', 'unknown')}: {e}")
|
||
|
||
if loaded_count > 0:
|
||
logger.info(f"Loaded {loaded_count} active giveaways into memory")
|
||
if failed_count > 0:
|
||
logger.warning(f"Failed to load {failed_count} giveaways")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error loading active giveaways: {e}")
|
||
|
||
# -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||
|
||
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 = interaction.data["custom_id"].split("_", 1)[1] # Get everything after "giveaway_"
|
||
giveaway = giveaways.get(giveaway_id)
|
||
|
||
if giveaway:
|
||
if giveaway.is_finished():
|
||
# Enhanced ended message
|
||
ended_embed = discord.Embed(
|
||
title="⏰ Giveaway Ended",
|
||
description="This giveaway has already ended. Stay tuned for future giveaways!",
|
||
color=0xFF6B6B
|
||
)
|
||
await interaction.response.send_message(embed=ended_embed, ephemeral=True)
|
||
else:
|
||
added = giveaway.add_participant(interaction.user)
|
||
if added:
|
||
# Enhanced success message
|
||
success_embed = discord.Embed(
|
||
title="🎉 Successfully Entered!",
|
||
description=f"You're now participating in **{giveaway.title}**!",
|
||
color=0x00FF00
|
||
)
|
||
success_embed.add_field(name="🎁 Prize", value=giveaway.prize, inline=True)
|
||
success_embed.add_field(name="👥 Participants", value=f"{len(giveaway.participants)}", inline=True)
|
||
success_embed.set_footer(text="Good luck! 🍀")
|
||
await interaction.response.send_message(embed=success_embed, ephemeral=True)
|
||
else:
|
||
# Enhanced already participating message
|
||
already_embed = discord.Embed(
|
||
title="ℹ️ Already Participating",
|
||
description=f"You're already entered in **{giveaway.title}**!",
|
||
color=0xFFB347
|
||
)
|
||
already_embed.add_field(name="👥 Current Participants", value=f"{len(giveaway.participants)}", inline=True)
|
||
already_embed.set_footer(text="Good luck! 🍀")
|
||
await interaction.response.send_message(embed=already_embed, 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()
|
||
|
||
# Load active giveaways from database
|
||
await load_active_giveaways()
|
||
|
||
# Start the process manager
|
||
if not process_manager.is_running():
|
||
process_manager.start()
|
||
|
||
# Start the contact messages checker
|
||
if not check_contact_messages.is_running():
|
||
check_contact_messages.start()
|
||
logger.info("Contact messages checker started (15-minute intervals)")
|
||
|
||
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> [message_id] [silent:True]` - Warn a user (with optional silent mode)\n"
|
||
"`/mute <user> <duration> [reason] [message_id] [silent:True]` - Mute a user temporarily (with optional silent mode)\n"
|
||
"`/unmute <user>` - Manually unmute a user (deactivates all active mutes)\n"
|
||
"`/viewmute <id_or_uuid>` - View detailed mute information by ID or UUID\n"
|
||
"`/listmutes [status]` - List mutes (active, completed, expired, cancelled, all)\n"
|
||
"`/modinfo [user]` - View comprehensive user information\n"
|
||
"`/viewwarn <warning_id>` - View detailed warning information\n"
|
||
"`/removewarn <warning_id>` - Deactivate a warning (Level 6+)\n"
|
||
"`/restorewarn <warning_id>` - Reactivate a warning (Level 6+)\n"
|
||
"`/modstats [user]` - View moderation statistics\n"
|
||
"`/processes <action> [type]` - Manage active processes\n"
|
||
"`/startgiveaway` - Create server giveaways with Steam/Epic integration\n"
|
||
"`/editgiveaway <id> <field> <value>` - Edit active giveaways (auto-updates post)\n"
|
||
"`/listgiveaways` - List all active giveaways (memory + database)\n"
|
||
"`/loadgiveaway <id>` - Load specific giveaway from database\n"
|
||
"`/loadallgiveaways` - Load all giveaways from database\n"
|
||
"`/updategiveawaymessage <id>` - Manually refresh giveaway post\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
|
||
)
|
||
|
||
# Mute System Features
|
||
embed.add_field(
|
||
name="🔇 Advanced Mute System",
|
||
value=(
|
||
"• **Persistent Records**: All mutes saved permanently in database\n"
|
||
"• **Message Context**: Referenced messages archived with full context\n"
|
||
"• **Auto-Unmute**: Automatic role restoration when mute expires\n"
|
||
"• **Mute IDs**: Easy-to-use numeric IDs for tracking (e.g. `/viewmute 123`)\n"
|
||
"• **Multiple Mutes**: Can handle multiple active mutes per user\n"
|
||
"• **Smart Notifications**: Auto-unmute alerts sent to mod log channel\n"
|
||
"• **Role Restoration**: Automatically restores previous roles after mute\n"
|
||
"• **Silent Mode**: Optional discrete moderation (ephemeral responses only)"
|
||
),
|
||
inline=False
|
||
)
|
||
|
||
# Silent Mode Features
|
||
embed.add_field(
|
||
name="🔇 Silent Mode Features",
|
||
value=(
|
||
"• **Discrete Moderation**: Use `silent:True` parameter with warn/mute commands\n"
|
||
"• **Ephemeral Responses**: Only moderator sees the action confirmation\n"
|
||
"• **User Notifications**: Target user still receives DM about action\n"
|
||
"• **Full Logging**: Mod log channel records all actions normally\n"
|
||
"• **No Public Messages**: Regular users don't see moderation announcements\n"
|
||
"• **Perfect for**: Handling sensitive issues without public drama"
|
||
),
|
||
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"
|
||
"mod_log_channel_id, mod_log_enabled"
|
||
),
|
||
inline=False
|
||
)
|
||
|
||
# Duration Formats and Examples
|
||
embed.add_field(
|
||
name="⏱️ Command Examples & Formats",
|
||
value=(
|
||
"**Mute duration formats:**\n"
|
||
"`10m` = 10 minutes, `1h` = 1 hour, `2d` = 2 days\n\n"
|
||
"**Mute with message reference:**\n"
|
||
"`/mute @user 1h Spam 1234567890123456789`\n"
|
||
"Saves message content and context automatically.\n\n"
|
||
"**View mute details:**\n"
|
||
"`/viewmute 123` (by Mute ID) or `/viewmute abc12345` (by UUID)\n\n"
|
||
"**List active mutes:**\n"
|
||
"`/listmutes active` - Shows all currently active mutes with IDs"
|
||
),
|
||
inline=False
|
||
)
|
||
|
||
# Process Management
|
||
embed.add_field(
|
||
name="🔄 Process Management",
|
||
value=(
|
||
"`/processes list` - List all active processes\n"
|
||
"`/processes list mute` - List only mute processes\n"
|
||
"`/processes cleanup` - Clean up expired processes\n"
|
||
"`/processes stats` - Show process statistics"
|
||
),
|
||
inline=False
|
||
)
|
||
|
||
embed.set_footer(text=f"Your permission level: {user_data['permission']} | Use /help for user commands")
|
||
await ctx.send(embed=embed)
|
||
|
||
@client.hybrid_command()
|
||
async def askmultus(ctx, *, prompt: str):
|
||
"""Submits a prompt to Multus for assistance or information. (5 Points)"""
|
||
if not features["askmultus"]:
|
||
await ctx.send("Sorry, the askmultus feature is currently disabled.")
|
||
return
|
||
|
||
user_id = ctx.author.id
|
||
guild_id = ctx.guild.id
|
||
|
||
# Lade Benutzerdaten aus der MySQL-Datenbank
|
||
user_data = load_user_data_sync(user_id, guild_id)
|
||
|
||
if user_data["points"] >= 5:
|
||
user_data["points"] -= 5
|
||
|
||
# Speichere die aktualisierten Benutzerdaten in der MySQL-Datenbank
|
||
update_user_data(user_data["user_id"], guild_id, "points", user_data["points"])
|
||
|
||
# Define the full data and user history field for askmultus
|
||
introduction = read_introduction()
|
||
background_data = read_background_data("background_data.txt")
|
||
current_datetime = get_current_datetime()
|
||
full_data = introduction + f"\nCurrent Date and Time: {current_datetime}" + background_data
|
||
user_history_field = "chat_history"
|
||
|
||
# Füge die Anfrage zur Warteschlange hinzu
|
||
await askmultus_queue.put((ctx, user_data["user_id"], ctx.author.name, prompt, ctx.channel.id, full_data, user_history_field, "local-model"))
|
||
|
||
# Erstelle ein Embed für die Bestätigungsnachricht
|
||
embed = discord.Embed(title="Multus Assistance Request", color=0x00ff00)
|
||
embed.add_field(name="Request Received", value=f"Your request has been added to the queue. Position in queue: {askmultus_queue.qsize()}")
|
||
await ctx.send(embed=embed)
|
||
else:
|
||
await ctx.send("You don't have enough points to use this command.")
|
||
|
||
executor = concurrent.futures.ThreadPoolExecutor()
|
||
|
||
async def process_ai_queue():
|
||
loop = asyncio.get_running_loop()
|
||
while True:
|
||
try:
|
||
if not askmultus_queue.empty():
|
||
ctx, user_id, user_name, prompt, channel_id, full_data, user_history_field, model = await askmultus_queue.get()
|
||
|
||
guild_id = ctx.guild.id
|
||
user_data = load_user_data_sync(user_id, guild_id)
|
||
|
||
try:
|
||
user_history = user_data.get(user_history_field, [])
|
||
|
||
# Ensure user_history is a list, not a string
|
||
if isinstance(user_history, str):
|
||
try:
|
||
user_history = json.loads(user_history)
|
||
except (json.JSONDecodeError, ValueError):
|
||
user_history = []
|
||
elif not isinstance(user_history, list):
|
||
user_history = []
|
||
|
||
user_history.append({"role": "user", "content": f"{user_name}: {prompt}"})
|
||
|
||
messages = [
|
||
{"role": "system", "content": full_data},
|
||
*user_history
|
||
]
|
||
|
||
completion = await loop.run_in_executor(executor, lambda: openai_instance.chat.completions.create(
|
||
model=model,
|
||
messages=messages,
|
||
temperature=0.8,
|
||
timeout=15, # Limit waiting time for response
|
||
))
|
||
|
||
assistant_message = completion.choices[0].message.content
|
||
|
||
channel = client.get_channel(channel_id)
|
||
|
||
# Prepare the embed with split fields if necessary
|
||
embed = discord.Embed(title="AI Response", color=0x00ff00)
|
||
embed.add_field(name="Prompt", value=prompt, inline=False)
|
||
|
||
if len(assistant_message) <= 1024:
|
||
embed.add_field(name="Response", value=assistant_message, inline=False)
|
||
else:
|
||
# Split the response into multiple fields if it exceeds 1024 characters
|
||
parts = [assistant_message[i:i+1024] for i in range(0, len(assistant_message), 1024)]
|
||
for i, part in enumerate(parts):
|
||
embed.add_field(name=f"Response Part {i+1}", value=part, inline=False)
|
||
|
||
await channel.send(embed=embed)
|
||
|
||
if ctx.voice_client: # If bot is in a voice channel
|
||
tts = gTTS(assistant_message, lang="en")
|
||
tts.save("response.mp3")
|
||
ctx.voice_client.play(discord.FFmpegPCMAudio("response.mp3"))
|
||
|
||
user_history.append({"role": "assistant", "content": assistant_message})
|
||
|
||
# Update the relevant user history field
|
||
update_user_data(user_data["user_id"], guild_id, user_history_field, user_history)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Processing errors: {e}")
|
||
finally:
|
||
askmultus_queue.task_done()
|
||
except asyncio.CancelledError:
|
||
break
|
||
except Exception as e:
|
||
logger.error(f"Error in process_ai_queue: {e}")
|
||
await asyncio.sleep(5)
|
||
|
||
@client.hybrid_command()
|
||
async def vision(ctx, image_url: str):
|
||
"""Analyzes the content of an image."""
|
||
if not features["vision"]:
|
||
await ctx.send("Sorry, the vision feature is currently disabled.")
|
||
return
|
||
|
||
try:
|
||
# Read the image and encode it to base64
|
||
response = requests.get(image_url)
|
||
if response.status_code == 200:
|
||
base64_image = base64.b64encode(response.content).decode("utf-8")
|
||
else:
|
||
await ctx.send(f"Failed to retrieve the image from {image_url}.")
|
||
return
|
||
|
||
# Process the request using OpenAI's Vision model
|
||
completion = openai_instance.chat.completions.create(
|
||
model="local-model",
|
||
messages=[
|
||
{
|
||
"role": "system",
|
||
"content": "This is a chat between a user and an assistant. The assistant is helping the user to describe an image.",
|
||
},
|
||
{
|
||
"role": "user",
|
||
"content": [
|
||
{"type": "text", "text": "What’s in this image?"},
|
||
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{base64_image}"}},
|
||
],
|
||
},
|
||
],
|
||
max_tokens=1000,
|
||
stream=True,
|
||
)
|
||
|
||
# Send the response to the Discord channel
|
||
chunks = []
|
||
for chunk in completion:
|
||
if chunk.choices[0].delta.content:
|
||
chunks.append(chunk.choices[0].delta.content)
|
||
|
||
result = "".join(chunks)
|
||
await ctx.send(result)
|
||
|
||
except Exception as e:
|
||
await ctx.send(f"Error analyzing the image: {e}")
|
||
|
||
|
||
@client.hybrid_command()
|
||
async def addbackgrounddata(ctx, *, data: str):
|
||
"""Adds additional background data to the file (Owner only)."""
|
||
if commands.is_owner():
|
||
try:
|
||
with open("background_data.txt", "a", encoding="utf-8") as file:
|
||
file.write("\n" + data)
|
||
await ctx.send("Background data added successfully.")
|
||
except Exception as e:
|
||
await ctx.send(f"Error adding background data: {e}")
|
||
else:
|
||
await ctx.send("You don't have the necessary permissions to use this command.")
|
||
|
||
|
||
@client.hybrid_command()
|
||
async def summarize(ctx, number: int):
|
||
"""Summarizes the last x messages in the chat (Admin only)."""
|
||
if not features["summarize"]:
|
||
await ctx.send("Sorry, the summarize feature is currently disabled.")
|
||
return
|
||
|
||
guild_id = ctx.guild.id
|
||
user_perms = load_user_data_sync(ctx.author.id, guild_id)
|
||
if 5 < user_perms["permission"]:
|
||
try:
|
||
# Fetch the last 10 messages in the channel
|
||
messages = []
|
||
async for message in ctx.channel.history(limit=number):
|
||
messages.append(message)
|
||
|
||
# Extract the content of each message
|
||
message_contents = [message.content for message in messages]
|
||
|
||
# Join the message contents into a single string
|
||
messages_combined = "\n".join(message_contents)
|
||
introduction = read_introduction()
|
||
full_data = introduction + background_data
|
||
|
||
|
||
# Process the combined messages using OpenAI's summarization model
|
||
completion = openai_instance.chat.completions.create(
|
||
model="text-davinci-003", # Choose an appropriate summarization model
|
||
messages=[
|
||
{"role": "system", "content": "Summarizing the last x messages in the chat: "},
|
||
{"role": "user", "content": messages_combined},
|
||
],
|
||
max_tokens=1000,
|
||
stream=False,
|
||
)
|
||
|
||
# Extract the summarized text from the completion
|
||
summary = completion.choices[0].message.content
|
||
|
||
# Send the summarized text to the Discord channel
|
||
await ctx.send(summary)
|
||
|
||
except Exception as e:
|
||
await ctx.send(f"An error occurred while summarizing the messages: {e}")
|
||
else:
|
||
await ctx.send("You don't have the necessary permissions to use this command.")
|
||
|
||
@client.hybrid_command()
|
||
async def join(ctx):
|
||
"""Makes the bot join a voice channel."""
|
||
if ctx.author.voice:
|
||
channel = ctx.author.voice.channel
|
||
await channel.connect()
|
||
await ctx.send(f"Joined {channel}")
|
||
else:
|
||
await ctx.send("You are not connected to a voice channel.")
|
||
|
||
@client.hybrid_command()
|
||
async def leave(ctx):
|
||
"""Makes the bot leave the voice channel."""
|
||
if ctx.voice_client:
|
||
await ctx.voice_client.disconnect()
|
||
await ctx.send("Left the voice channel.")
|
||
else:
|
||
await ctx.send("I am not in a voice channel.")
|
||
|
||
@client.hybrid_command()
|
||
async def toggle_feature(ctx, feature: str, state: str):
|
||
"""Allows admin to enable or disable bot features."""
|
||
guild_id = ctx.guild.id
|
||
user_id = ctx.author.id
|
||
user_data = load_user_data_sync(user_id, guild_id)
|
||
user_perms = user_data["permission"]
|
||
|
||
if user_perms < 8: # Nur Admins (permission level >= 8) können Funktionen aktivieren/deaktivieren
|
||
await ctx.send("You do not have the necessary permissions to toggle features.")
|
||
return
|
||
|
||
global features
|
||
|
||
if feature.lower() not in features:
|
||
await ctx.send(f"Feature {feature} not found.")
|
||
return
|
||
|
||
if state.lower() == "on":
|
||
features[feature.lower()] = True
|
||
await ctx.send(f"Feature {feature} enabled.")
|
||
elif state.lower() == "off":
|
||
features[feature.lower()] = False
|
||
await ctx.send(f"Feature {feature} disabled.")
|
||
else:
|
||
await ctx.send("Please specify 'on' or 'off'.")
|
||
|
||
await ctx.send("Please specify 'on' or 'off'.")
|
||
|
||
@client.hybrid_command()
|
||
async def processes(ctx, action: str = "list", process_type: str = None):
|
||
"""Manages active processes. Actions: list, cleanup, status"""
|
||
guild_id = ctx.guild.id
|
||
user_perms = load_user_data_sync(ctx.author.id, guild_id)
|
||
|
||
if user_perms["permission"] < 8: # Only admins can manage processes
|
||
await ctx.send("You don't have permission to manage processes.")
|
||
return
|
||
|
||
if action.lower() == "list":
|
||
processes = get_active_processes(process_type=process_type, guild_id=guild_id)
|
||
|
||
if not processes:
|
||
await ctx.send("No active processes found.")
|
||
return
|
||
|
||
embed = discord.Embed(title="Active Processes", color=0x00ff00)
|
||
|
||
for process in processes[:10]: # Limit to 10 processes
|
||
process_info = f"Type: {process['process_type']}\n"
|
||
process_info += f"Status: {process['status']}\n"
|
||
|
||
if process['end_time']:
|
||
time_left = process['end_time'] - datetime.now()
|
||
if time_left.total_seconds() > 0:
|
||
process_info += f"Time left: {time_left}\n"
|
||
else:
|
||
process_info += "**EXPIRED**\n"
|
||
|
||
if process['data']:
|
||
data = process['data']
|
||
if 'title' in data:
|
||
process_info += f"Title: {data['title']}\n"
|
||
if 'participant_count' in data:
|
||
process_info += f"Participants: {data['participant_count']}\n"
|
||
|
||
embed.add_field(
|
||
name=f"{process['process_type'].title()} - {process['uuid'][:8]}...",
|
||
value=process_info,
|
||
inline=True
|
||
)
|
||
|
||
if len(processes) > 10:
|
||
embed.set_footer(text=f"Showing 10 of {len(processes)} processes")
|
||
|
||
await ctx.send(embed=embed)
|
||
|
||
elif action.lower() == "cleanup":
|
||
expired = cleanup_expired_processes()
|
||
await ctx.send(f"Cleaned up {len(expired)} expired processes.")
|
||
|
||
elif action.lower() == "status":
|
||
active_count = len(get_active_processes(status="active", guild_id=guild_id))
|
||
completed_count = len(get_active_processes(status="completed", guild_id=guild_id))
|
||
expired_count = len(get_active_processes(status="expired", guild_id=guild_id))
|
||
|
||
embed = discord.Embed(title="Process Status", color=0x3498db)
|
||
embed.add_field(name="Active", value=str(active_count), inline=True)
|
||
embed.add_field(name="Completed", value=str(completed_count), inline=True)
|
||
embed.add_field(name="Expired", value=str(expired_count), inline=True)
|
||
embed.add_field(name="Process Manager", value="Running" if process_manager.is_running() else "Stopped", inline=False)
|
||
|
||
await ctx.send(embed=embed)
|
||
|
||
else:
|
||
await ctx.send("Invalid action. Use: list, cleanup, or status")
|
||
|
||
@client.hybrid_command()
|
||
async def version(ctx):
|
||
"""Displays the current version of the bot."""
|
||
await ctx.send(f"The current version of the bot is: {__version__}")
|
||
|
||
# ================================ GUILD SETTINGS SYSTEM ================================
|
||
|
||
def get_guild_settings(guild_id):
|
||
"""Lädt die Guild-Einstellungen aus der Datenbank"""
|
||
connection = None
|
||
cursor = None
|
||
try:
|
||
connection = connect_to_database()
|
||
cursor = connection.cursor()
|
||
|
||
select_query = "SELECT * FROM guild_settings WHERE guild_id = %s"
|
||
cursor.execute(select_query, (guild_id,))
|
||
result = cursor.fetchone()
|
||
|
||
if result:
|
||
return {
|
||
"guild_id": result[0],
|
||
"mute_role_id": result[1],
|
||
"mute_role_name": result[2] or "Muted",
|
||
"auto_create_mute_role": bool(result[3]) if result[3] is not None else True,
|
||
"max_warn_threshold": result[4] or 3,
|
||
"auto_mute_on_warns": bool(result[5]) if result[5] is not None else False,
|
||
"auto_mute_duration": result[6] or "1h",
|
||
"log_channel_id": result[7],
|
||
"mod_log_enabled": bool(result[8]) if result[8] is not None else True
|
||
}
|
||
else:
|
||
# Erstelle Default-Einstellungen
|
||
default_settings = {
|
||
"guild_id": guild_id,
|
||
"mute_role_id": None,
|
||
"mute_role_name": "Muted",
|
||
"auto_create_mute_role": True,
|
||
"max_warn_threshold": 3,
|
||
"auto_mute_on_warns": False,
|
||
"auto_mute_duration": "1h",
|
||
"log_channel_id": None,
|
||
"mod_log_enabled": True
|
||
}
|
||
save_guild_settings(guild_id, default_settings)
|
||
return default_settings
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error loading guild settings: {e}")
|
||
# Return default settings on error
|
||
return {
|
||
"guild_id": guild_id,
|
||
"mute_role_id": None,
|
||
"mute_role_name": "Muted",
|
||
"auto_create_mute_role": True,
|
||
"max_warn_threshold": 3,
|
||
"auto_mute_on_warns": False,
|
||
"auto_mute_duration": "1h",
|
||
"log_channel_id": None,
|
||
"mod_log_enabled": True
|
||
}
|
||
finally:
|
||
if cursor:
|
||
cursor.close()
|
||
if connection:
|
||
close_database_connection(connection)
|
||
|
||
def save_guild_settings(guild_id, settings):
|
||
"""Speichert Guild-Einstellungen in der Datenbank"""
|
||
connection = None
|
||
cursor = None
|
||
try:
|
||
connection = connect_to_database()
|
||
cursor = connection.cursor()
|
||
|
||
insert_query = """
|
||
INSERT INTO guild_settings (guild_id, mute_role_id, mute_role_name, auto_create_mute_role,
|
||
max_warn_threshold, auto_mute_on_warns, auto_mute_duration,
|
||
log_channel_id, mod_log_enabled)
|
||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||
ON DUPLICATE KEY UPDATE
|
||
mute_role_id = VALUES(mute_role_id),
|
||
mute_role_name = VALUES(mute_role_name),
|
||
auto_create_mute_role = VALUES(auto_create_mute_role),
|
||
max_warn_threshold = VALUES(max_warn_threshold),
|
||
auto_mute_on_warns = VALUES(auto_mute_on_warns),
|
||
auto_mute_duration = VALUES(auto_mute_duration),
|
||
log_channel_id = VALUES(log_channel_id),
|
||
mod_log_enabled = VALUES(mod_log_enabled)
|
||
"""
|
||
|
||
cursor.execute(insert_query, (
|
||
guild_id,
|
||
settings.get("mute_role_id"),
|
||
settings.get("mute_role_name", "Muted"),
|
||
settings.get("auto_create_mute_role", True),
|
||
settings.get("max_warn_threshold", 3),
|
||
settings.get("auto_mute_on_warns", False),
|
||
settings.get("auto_mute_duration", "1h"),
|
||
settings.get("log_channel_id"),
|
||
settings.get("mod_log_enabled", True)
|
||
))
|
||
connection.commit()
|
||
|
||
logger.info(f"Guild settings saved for guild {guild_id}")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error saving guild settings: {e}")
|
||
if connection:
|
||
connection.rollback()
|
||
finally:
|
||
if cursor:
|
||
cursor.close()
|
||
if connection:
|
||
close_database_connection(connection)
|
||
|
||
async def get_or_create_mute_role(guild, settings):
|
||
"""Holt oder erstellt die Mute-Rolle basierend auf Guild-Einstellungen"""
|
||
mute_role = None
|
||
|
||
# Versuche zuerst über ID zu finden
|
||
if settings["mute_role_id"]:
|
||
mute_role = guild.get_role(settings["mute_role_id"])
|
||
if mute_role:
|
||
return mute_role
|
||
|
||
# Versuche über Name zu finden
|
||
mute_role = discord.utils.get(guild.roles, name=settings["mute_role_name"])
|
||
if mute_role:
|
||
# Update die ID in den Einstellungen
|
||
settings["mute_role_id"] = mute_role.id
|
||
save_guild_settings(guild.id, settings)
|
||
return mute_role
|
||
|
||
# Erstelle neue Rolle, falls auto_create_mute_role aktiviert ist
|
||
if settings["auto_create_mute_role"]:
|
||
try:
|
||
mute_role = await guild.create_role(
|
||
name=settings["mute_role_name"],
|
||
color=discord.Color.dark_gray(),
|
||
reason="Auto-created mute role for moderation system"
|
||
)
|
||
|
||
# Konfiguriere Berechtigungen für alle Kanäle
|
||
for channel in guild.channels:
|
||
try:
|
||
await channel.set_permissions(
|
||
mute_role,
|
||
send_messages=False,
|
||
speak=False,
|
||
add_reactions=False,
|
||
create_private_threads=False,
|
||
create_public_threads=False,
|
||
send_messages_in_threads=False
|
||
)
|
||
except discord.Forbidden:
|
||
logger.warning(f"Could not set permissions for {channel.name} in guild {guild.id}")
|
||
continue
|
||
|
||
# Speichere die neue Rolle-ID
|
||
settings["mute_role_id"] = mute_role.id
|
||
save_guild_settings(guild.id, settings)
|
||
|
||
logger.info(f"Created mute role '{settings['mute_role_name']}' for guild {guild.id}")
|
||
return mute_role
|
||
|
||
except discord.Forbidden:
|
||
logger.error(f"No permission to create mute role in guild {guild.id}")
|
||
return None
|
||
|
||
return None
|
||
|
||
# ================================ MODERATION SYSTEM ================================
|
||
|
||
# Moderation Helper Functions
|
||
def check_moderation_permission(user_permission):
|
||
"""Checks if the user has moderation rights (Permission 5 or higher)"""
|
||
return user_permission >= 5
|
||
|
||
async def log_moderation_action(guild, action_type, moderator, target_user, reason, duration=None, additional_info=None):
|
||
"""Logs moderation actions to the configured log channel"""
|
||
try:
|
||
guild_settings = get_guild_settings(guild.id)
|
||
|
||
# Check if logging is enabled and channel is configured
|
||
if not guild_settings["mod_log_enabled"] or not guild_settings["log_channel_id"]:
|
||
return
|
||
|
||
log_channel = guild.get_channel(guild_settings["log_channel_id"])
|
||
if not log_channel:
|
||
logger.warning(f"Log channel {guild_settings['log_channel_id']} not found in guild {guild.id}")
|
||
return
|
||
|
||
# Create log embed
|
||
color_map = {
|
||
"warn": 0xff9500,
|
||
"mute": 0xff0000,
|
||
"unmute": 0x00ff00,
|
||
"kick": 0xff6600,
|
||
"ban": 0x8b0000,
|
||
"unban": 0x00ff00
|
||
}
|
||
|
||
embed = discord.Embed(
|
||
title=f"🛡️ Moderation Action: {action_type.title()}",
|
||
color=color_map.get(action_type.lower(), 0x3498db),
|
||
timestamp=datetime.now()
|
||
)
|
||
|
||
embed.add_field(name="👤 Target User", value=f"{target_user.mention}\n`{target_user.id}`", inline=True)
|
||
embed.add_field(name="👮 Moderator", value=f"{moderator.mention}\n`{moderator.id}`", inline=True)
|
||
embed.add_field(name="📝 Reason", value=reason, inline=True)
|
||
|
||
if duration:
|
||
embed.add_field(name="⏱️ Duration", value=duration, inline=True)
|
||
|
||
if additional_info:
|
||
for key, value in additional_info.items():
|
||
embed.add_field(name=key, value=value, inline=True)
|
||
|
||
embed.set_thumbnail(url=target_user.display_avatar.url)
|
||
embed.set_footer(text=f"Action ID: {guild.id}-{target_user.id}-{int(datetime.now().timestamp())}")
|
||
|
||
await log_channel.send(embed=embed)
|
||
logger.info(f"Logged {action_type} action for user {target_user.id} in guild {guild.id}")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error logging moderation action: {e}")
|
||
|
||
async def save_warning_to_database(user_id, guild_id, moderator_id, reason, timestamp=None, message_data=None, message_id=None):
|
||
"""Saves individual warning records to the database with optional message data and context"""
|
||
connection = None
|
||
cursor = None
|
||
try:
|
||
connection = connect_to_database()
|
||
cursor = connection.cursor()
|
||
|
||
if timestamp is None:
|
||
timestamp = datetime.now()
|
||
|
||
# Prepare message data if provided
|
||
message_id_db = message_id # Use provided message_id
|
||
message_content = None
|
||
message_attachments = None
|
||
message_author_id = None
|
||
message_channel_id = None
|
||
context_messages = None
|
||
|
||
if message_data:
|
||
if isinstance(message_data, dict) and "main_message" in message_data:
|
||
# New format with context messages
|
||
main_msg = message_data.get("main_message")
|
||
if main_msg:
|
||
message_id_db = main_msg.get('id')
|
||
message_content = main_msg.get('content')
|
||
message_attachments = main_msg.get('attachments')
|
||
message_author_id = main_msg.get('author_id')
|
||
message_channel_id = main_msg.get('channel_id')
|
||
|
||
# Store all context messages as JSON
|
||
context_messages = json.dumps(message_data.get("context_messages", []))
|
||
else:
|
||
# Old format - single message
|
||
message_id_db = message_data.get('id')
|
||
message_content = message_data.get('content')
|
||
message_attachments = message_data.get('attachments') # JSON string
|
||
message_author_id = message_data.get('author_id')
|
||
message_channel_id = message_data.get('channel_id')
|
||
|
||
insert_query = """
|
||
INSERT INTO user_warnings (user_id, guild_id, moderator_id, reason, message_id,
|
||
message_content, message_attachments, message_author_id,
|
||
message_channel_id, context_messages, aktiv, created_at)
|
||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||
"""
|
||
|
||
cursor.execute(insert_query, (
|
||
user_id, guild_id, moderator_id, reason, message_id_db,
|
||
message_content, message_attachments, message_author_id,
|
||
message_channel_id, context_messages, True, timestamp
|
||
))
|
||
connection.commit()
|
||
|
||
logger.info(f"Saved warning record for user {user_id} in guild {guild_id} with context")
|
||
return cursor.lastrowid
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error saving warning to database: {e}")
|
||
if connection:
|
||
connection.rollback()
|
||
return None
|
||
finally:
|
||
if cursor:
|
||
cursor.close()
|
||
if connection:
|
||
close_database_connection(connection)
|
||
|
||
def create_warnings_table():
|
||
"""Creates the user_warnings table if it doesn't exist"""
|
||
connection = None
|
||
cursor = None
|
||
try:
|
||
connection = connect_to_database()
|
||
cursor = connection.cursor()
|
||
|
||
create_table_query = """
|
||
CREATE TABLE IF NOT EXISTS user_warnings (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
user_id BIGINT NOT NULL,
|
||
guild_id BIGINT NOT NULL,
|
||
moderator_id BIGINT NOT NULL,
|
||
reason TEXT NOT NULL,
|
||
message_id BIGINT NULL,
|
||
message_content LONGTEXT NULL,
|
||
message_attachments LONGTEXT NULL,
|
||
message_author_id BIGINT NULL,
|
||
message_channel_id BIGINT NULL,
|
||
context_messages LONGTEXT NULL,
|
||
aktiv BOOLEAN DEFAULT TRUE,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
INDEX idx_user_guild (user_id, guild_id),
|
||
INDEX idx_created_at (created_at),
|
||
INDEX idx_message_id (message_id),
|
||
INDEX idx_aktiv (aktiv)
|
||
)
|
||
"""
|
||
|
||
cursor.execute(create_table_query)
|
||
|
||
# Add new columns if they don't exist (for existing databases)
|
||
alter_queries = [
|
||
"ALTER TABLE user_warnings ADD COLUMN message_id BIGINT NULL",
|
||
"ALTER TABLE user_warnings ADD COLUMN message_content LONGTEXT NULL",
|
||
"ALTER TABLE user_warnings ADD COLUMN message_attachments LONGTEXT NULL",
|
||
"ALTER TABLE user_warnings ADD COLUMN message_author_id BIGINT NULL",
|
||
"ALTER TABLE user_warnings ADD COLUMN message_channel_id BIGINT NULL",
|
||
"ALTER TABLE user_warnings ADD COLUMN context_messages LONGTEXT NULL",
|
||
"ALTER TABLE user_warnings ADD COLUMN aktiv BOOLEAN DEFAULT TRUE",
|
||
"ALTER TABLE user_warnings ADD INDEX idx_message_id (message_id)",
|
||
"ALTER TABLE user_warnings ADD INDEX idx_aktiv (aktiv)"
|
||
]
|
||
|
||
for alter_query in alter_queries:
|
||
try:
|
||
cursor.execute(alter_query)
|
||
except Exception:
|
||
# Column already exists, ignore error
|
||
pass
|
||
|
||
connection.commit()
|
||
logger.info("User warnings table checked/created successfully")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error creating warnings table: {e}")
|
||
finally:
|
||
if cursor:
|
||
cursor.close()
|
||
if connection:
|
||
close_database_connection(connection)
|
||
|
||
def create_contact_messages_table():
|
||
"""Creates the contact_messages table if it doesn't exist"""
|
||
connection = None
|
||
cursor = None
|
||
try:
|
||
connection = connect_to_database()
|
||
cursor = connection.cursor()
|
||
|
||
create_table_query = """
|
||
CREATE TABLE IF NOT EXISTS contact_messages (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
user_id VARCHAR(50) NOT NULL,
|
||
username VARCHAR(100) NOT NULL,
|
||
subject VARCHAR(100) NOT NULL,
|
||
category VARCHAR(50) NOT NULL,
|
||
priority VARCHAR(20) NOT NULL,
|
||
message TEXT NOT NULL,
|
||
server_context VARCHAR(200),
|
||
submitted_at BIGINT NOT NULL,
|
||
status VARCHAR(20) DEFAULT 'pending',
|
||
responded_at BIGINT,
|
||
response TEXT,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
INDEX idx_user_id (user_id),
|
||
INDEX idx_status (status),
|
||
INDEX idx_submitted_at (submitted_at)
|
||
)
|
||
"""
|
||
|
||
cursor.execute(create_table_query)
|
||
connection.commit()
|
||
logger.info("Contact messages table checked/created successfully")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error creating contact messages table: {e}")
|
||
finally:
|
||
if cursor:
|
||
cursor.close()
|
||
if connection:
|
||
close_database_connection(connection)
|
||
|
||
def create_mutes_table():
|
||
"""Creates the user_mutes table if it doesn't exist"""
|
||
connection = None
|
||
cursor = None
|
||
try:
|
||
connection = connect_to_database()
|
||
cursor = connection.cursor()
|
||
|
||
create_table_query = """
|
||
CREATE TABLE IF NOT EXISTS user_mutes (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
user_id VARCHAR(50) NOT NULL,
|
||
guild_id VARCHAR(50) NOT NULL,
|
||
moderator_id VARCHAR(50) NOT NULL,
|
||
reason TEXT NOT NULL,
|
||
duration VARCHAR(20) NOT NULL,
|
||
start_time TIMESTAMP NOT NULL,
|
||
end_time TIMESTAMP NOT NULL,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
status VARCHAR(20) DEFAULT 'active',
|
||
process_uuid VARCHAR(36),
|
||
channel_id VARCHAR(50),
|
||
mute_role_id VARCHAR(50),
|
||
message_id BIGINT,
|
||
message_content TEXT,
|
||
message_attachments JSON,
|
||
message_author_id VARCHAR(50),
|
||
message_channel_id VARCHAR(50),
|
||
context_messages JSON,
|
||
aktiv BOOLEAN DEFAULT TRUE,
|
||
unmuted_at TIMESTAMP NULL,
|
||
unmuted_by VARCHAR(50) NULL,
|
||
auto_unmuted BOOLEAN DEFAULT FALSE,
|
||
INDEX idx_user_guild (user_id, guild_id),
|
||
INDEX idx_guild (guild_id),
|
||
INDEX idx_moderator (moderator_id),
|
||
INDEX idx_process_uuid (process_uuid),
|
||
INDEX idx_aktiv (aktiv),
|
||
INDEX idx_status (status)
|
||
)
|
||
"""
|
||
|
||
cursor.execute(create_table_query)
|
||
connection.commit()
|
||
logger.info("User mutes table created or already exists")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error creating user mutes table: {e}")
|
||
finally:
|
||
if cursor:
|
||
cursor.close()
|
||
if connection:
|
||
close_database_connection(connection)
|
||
|
||
async def save_mute_to_database(user_id, guild_id, moderator_id, reason, duration, start_time, end_time,
|
||
process_uuid=None, channel_id=None, mute_role_id=None, message_data=None, message_id=None):
|
||
"""Saves individual mute records to the database with optional message data and context"""
|
||
connection = None
|
||
cursor = None
|
||
try:
|
||
connection = connect_to_database()
|
||
cursor = connection.cursor()
|
||
|
||
# Extract message data if provided
|
||
message_content = None
|
||
message_attachments = None
|
||
message_author_id = None
|
||
message_channel_id = None
|
||
context_messages = None
|
||
|
||
if message_data:
|
||
if isinstance(message_data, dict) and "main_message" in message_data:
|
||
# New format with context
|
||
main_msg = message_data.get("main_message", {})
|
||
message_content = main_msg.get("content")
|
||
message_attachments = main_msg.get("attachments")
|
||
message_author_id = main_msg.get("author_id")
|
||
message_channel_id = main_msg.get("channel_id")
|
||
context_messages = json.dumps(message_data.get("context_messages", []))
|
||
else:
|
||
# Old format or simple dict
|
||
message_content = message_data.get("content")
|
||
message_attachments = message_data.get("attachments")
|
||
message_author_id = message_data.get("author_id")
|
||
message_channel_id = message_data.get("channel_id")
|
||
|
||
# Convert JSON fields
|
||
if message_attachments and isinstance(message_attachments, str):
|
||
# Already JSON string
|
||
pass
|
||
elif message_attachments:
|
||
# Convert to JSON string
|
||
message_attachments = json.dumps(message_attachments)
|
||
|
||
insert_query = """
|
||
INSERT INTO user_mutes (
|
||
user_id, guild_id, moderator_id, reason, duration, start_time, end_time,
|
||
process_uuid, channel_id, mute_role_id, message_id, message_content,
|
||
message_attachments, message_author_id, message_channel_id, context_messages
|
||
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||
"""
|
||
|
||
cursor.execute(insert_query, (
|
||
user_id, guild_id, moderator_id, reason, duration, start_time, end_time,
|
||
str(process_uuid) if process_uuid else None, channel_id, mute_role_id,
|
||
message_id, message_content, message_attachments, message_author_id,
|
||
message_channel_id, context_messages
|
||
))
|
||
|
||
mute_id = cursor.lastrowid
|
||
connection.commit()
|
||
|
||
logger.info(f"Mute record saved to database: ID={mute_id}, User={user_id}, Guild={guild_id}")
|
||
return mute_id
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error saving mute to database: {e}")
|
||
if connection:
|
||
connection.rollback()
|
||
return None
|
||
finally:
|
||
if cursor:
|
||
cursor.close()
|
||
if connection:
|
||
close_database_connection(connection)
|
||
|
||
async def get_user_mutes(user_id, guild_id, active_only=True):
|
||
"""Retrieves mute records for a user
|
||
|
||
Args:
|
||
user_id: Discord user ID
|
||
guild_id: Discord guild ID
|
||
active_only: If True, only returns active mutes. If False, returns all mutes.
|
||
"""
|
||
connection = None
|
||
cursor = None
|
||
try:
|
||
connection = connect_to_database()
|
||
cursor = connection.cursor()
|
||
|
||
if active_only:
|
||
select_query = """
|
||
SELECT * FROM user_mutes
|
||
WHERE user_id = %s AND guild_id = %s AND aktiv = TRUE
|
||
ORDER BY created_at DESC
|
||
"""
|
||
else:
|
||
select_query = """
|
||
SELECT * FROM user_mutes
|
||
WHERE user_id = %s AND guild_id = %s
|
||
ORDER BY created_at DESC
|
||
"""
|
||
|
||
cursor.execute(select_query, (user_id, guild_id))
|
||
results = cursor.fetchall()
|
||
|
||
# Convert results to list of dictionaries
|
||
mutes = []
|
||
columns = [desc[0] for desc in cursor.description]
|
||
for row in results:
|
||
mute_dict = dict(zip(columns, row))
|
||
mutes.append(mute_dict)
|
||
|
||
return mutes
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error retrieving user mutes: {e}")
|
||
return []
|
||
finally:
|
||
if cursor:
|
||
cursor.close()
|
||
if connection:
|
||
close_database_connection(connection)
|
||
|
||
async def deactivate_mute(mute_id, unmuted_by=None, auto_unmuted=False):
|
||
"""Deactivates a mute by setting aktiv to FALSE and recording unmute info"""
|
||
connection = None
|
||
cursor = None
|
||
try:
|
||
connection = connect_to_database()
|
||
cursor = connection.cursor()
|
||
|
||
update_query = """
|
||
UPDATE user_mutes
|
||
SET aktiv = FALSE, unmuted_at = NOW(), unmuted_by = %s, auto_unmuted = %s, status = 'completed'
|
||
WHERE id = %s
|
||
"""
|
||
|
||
cursor.execute(update_query, (unmuted_by, auto_unmuted, mute_id))
|
||
connection.commit()
|
||
|
||
if cursor.rowcount > 0:
|
||
logger.info(f"Mute {mute_id} deactivated successfully")
|
||
return True
|
||
else:
|
||
logger.warning(f"No mute found with ID {mute_id}")
|
||
return False
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error deactivating mute: {e}")
|
||
if connection:
|
||
connection.rollback()
|
||
return False
|
||
finally:
|
||
if cursor:
|
||
cursor.close()
|
||
if connection:
|
||
close_database_connection(connection)
|
||
|
||
async def get_mute_by_process_uuid(process_uuid):
|
||
"""Gets mute record by process UUID"""
|
||
connection = None
|
||
cursor = None
|
||
try:
|
||
connection = connect_to_database()
|
||
cursor = connection.cursor()
|
||
|
||
select_query = """
|
||
SELECT * FROM user_mutes
|
||
WHERE process_uuid = %s
|
||
ORDER BY created_at DESC
|
||
LIMIT 1
|
||
"""
|
||
|
||
cursor.execute(select_query, (str(process_uuid),))
|
||
result = cursor.fetchone()
|
||
|
||
if result:
|
||
columns = [desc[0] for desc in cursor.description]
|
||
mute_dict = dict(zip(columns, result))
|
||
return mute_dict
|
||
return None
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error getting mute by process UUID: {e}")
|
||
return None
|
||
finally:
|
||
if cursor:
|
||
cursor.close()
|
||
if connection:
|
||
close_database_connection(connection)
|
||
|
||
async def send_contact_message_to_admin(message_data):
|
||
"""Sends a contact message to the admin via Discord DM"""
|
||
try:
|
||
# Your Discord User ID
|
||
ADMIN_DISCORD_ID = 253922739709018114
|
||
|
||
admin_user = client.get_user(ADMIN_DISCORD_ID)
|
||
if not admin_user:
|
||
admin_user = await client.fetch_user(ADMIN_DISCORD_ID)
|
||
|
||
if admin_user:
|
||
# Priority emoji mapping
|
||
priority_emojis = {
|
||
"low": "🟢",
|
||
"medium": "🟡",
|
||
"high": "🟠",
|
||
"urgent": "🔴"
|
||
}
|
||
|
||
# Category emoji mapping
|
||
category_emojis = {
|
||
"bug_report": "🐛",
|
||
"feature_request": "💡",
|
||
"account_issue": "👤",
|
||
"moderation": "🛡️",
|
||
"giveaway": "🎁",
|
||
"privacy": "🔒",
|
||
"technical": "⚙️",
|
||
"other": "❓"
|
||
}
|
||
|
||
# Create Discord Embed
|
||
embed = discord.Embed(
|
||
title="📨 New Contact Form Submission",
|
||
color=0x667eea,
|
||
timestamp=datetime.utcnow()
|
||
)
|
||
|
||
# User Information
|
||
embed.add_field(
|
||
name="👤 User Information",
|
||
value=f"**Name:** {message_data.get('user_name', 'Unknown')}\n"
|
||
f"**Username:** {message_data.get('username', 'Unknown')}\n"
|
||
f"**Discord ID:** `{message_data.get('user_id', 'Unknown')}`",
|
||
inline=False
|
||
)
|
||
|
||
# Message Details
|
||
embed.add_field(
|
||
name="📋 Message Details",
|
||
value=f"**Subject:** {message_data.get('subject', 'No subject')}\n"
|
||
f"**Category:** {category_emojis.get(message_data.get('category', 'other'), '❓')} {message_data.get('category', 'other').replace('_', ' ').title()}\n"
|
||
f"**Priority:** {priority_emojis.get(message_data.get('priority', 'low'), '⚪')} {message_data.get('priority', 'low').upper()}",
|
||
inline=False
|
||
)
|
||
|
||
# Server Context (if provided)
|
||
if message_data.get('server_context'):
|
||
embed.add_field(
|
||
name="🖥️ Server Context",
|
||
value=message_data.get('server_context'),
|
||
inline=False
|
||
)
|
||
|
||
# Message Content
|
||
message_content = message_data.get('message', 'No message content')
|
||
if len(message_content) > 1024:
|
||
message_content = message_content[:1021] + "..."
|
||
|
||
embed.add_field(
|
||
name="💬 Message",
|
||
value=f"```{message_content}```",
|
||
inline=False
|
||
)
|
||
|
||
# Footer
|
||
embed.set_footer(text="Multus Bot Web Panel Contact Form")
|
||
|
||
# Set avatar thumbnail if available
|
||
if message_data.get('avatar_url'):
|
||
embed.set_thumbnail(url=message_data.get('avatar_url'))
|
||
|
||
await admin_user.send(embed=embed)
|
||
logger.info(f"Contact message sent to admin for user {message_data.get('user_id')}")
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error sending contact message to admin: {e}")
|
||
return False
|
||
|
||
async def get_user_warnings(user_id, guild_id, active_only=True):
|
||
"""Retrieves warning records for a user
|
||
|
||
Args:
|
||
user_id: Discord user ID
|
||
guild_id: Discord guild ID
|
||
active_only: If True, only returns active warnings. If False, returns all warnings.
|
||
"""
|
||
connection = None
|
||
cursor = None
|
||
try:
|
||
connection = connect_to_database()
|
||
cursor = connection.cursor()
|
||
|
||
select_query = """
|
||
SELECT id, moderator_id, reason, created_at, message_id, message_content,
|
||
message_attachments, message_author_id, message_channel_id, context_messages, aktiv
|
||
FROM user_warnings
|
||
WHERE user_id = %s AND guild_id = %s {}
|
||
ORDER BY created_at DESC
|
||
""".format("AND aktiv = TRUE" if active_only else "")
|
||
|
||
cursor.execute(select_query, (user_id, guild_id))
|
||
results = cursor.fetchall()
|
||
|
||
warnings = []
|
||
for row in results:
|
||
warnings.append({
|
||
"id": row[0],
|
||
"moderator_id": row[1],
|
||
"reason": row[2],
|
||
"created_at": row[3],
|
||
"message_id": row[4],
|
||
"message_content": row[5],
|
||
"message_attachments": row[6],
|
||
"message_author_id": row[7],
|
||
"message_channel_id": row[8],
|
||
"context_messages": row[9],
|
||
"aktiv": row[10]
|
||
})
|
||
|
||
return warnings
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error retrieving user warnings: {e}")
|
||
return []
|
||
finally:
|
||
if cursor:
|
||
cursor.close()
|
||
if connection:
|
||
close_database_connection(connection)
|
||
|
||
async def deactivate_warning(warning_id):
|
||
"""Deactivates a warning by setting aktiv to FALSE"""
|
||
connection = None
|
||
cursor = None
|
||
try:
|
||
connection = connect_to_database()
|
||
cursor = connection.cursor()
|
||
|
||
update_query = "UPDATE user_warnings SET aktiv = FALSE WHERE id = %s"
|
||
cursor.execute(update_query, (warning_id,))
|
||
connection.commit()
|
||
|
||
if cursor.rowcount > 0:
|
||
logger.info(f"Deactivated warning with ID {warning_id}")
|
||
return True
|
||
else:
|
||
logger.warning(f"No warning found with ID {warning_id}")
|
||
return False
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error deactivating warning: {e}")
|
||
if connection:
|
||
connection.rollback()
|
||
return False
|
||
finally:
|
||
if cursor:
|
||
cursor.close()
|
||
if connection:
|
||
close_database_connection(connection)
|
||
|
||
async def reactivate_warning(warning_id):
|
||
"""Reactivates a warning by setting aktiv to TRUE"""
|
||
connection = None
|
||
cursor = None
|
||
try:
|
||
connection = connect_to_database()
|
||
cursor = connection.cursor()
|
||
|
||
update_query = "UPDATE user_warnings SET aktiv = TRUE WHERE id = %s"
|
||
cursor.execute(update_query, (warning_id,))
|
||
connection.commit()
|
||
|
||
if cursor.rowcount > 0:
|
||
logger.info(f"Reactivated warning with ID {warning_id}")
|
||
return True
|
||
else:
|
||
logger.warning(f"No warning found with ID {warning_id}")
|
||
return False
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error reactivating warning: {e}")
|
||
if connection:
|
||
connection.rollback()
|
||
return False
|
||
finally:
|
||
if cursor:
|
||
cursor.close()
|
||
if connection:
|
||
close_database_connection(connection)
|
||
|
||
async def get_message_data(channel, message_id, context_range=3):
|
||
"""Retrieves and processes message data for warning documentation with context messages"""
|
||
try:
|
||
# Get the main message
|
||
main_message = await channel.fetch_message(message_id)
|
||
|
||
# Get context messages (before and after)
|
||
context_messages = []
|
||
try:
|
||
# Get messages around the target message
|
||
async for msg in channel.history(limit=context_range * 2 + 1, around=main_message.created_at):
|
||
context_messages.append(msg)
|
||
|
||
# Sort messages by timestamp
|
||
context_messages.sort(key=lambda m: m.created_at)
|
||
except Exception as e:
|
||
logger.warning(f"Could not fetch context messages: {e}")
|
||
context_messages = [main_message]
|
||
|
||
# Process all messages (main + context)
|
||
all_messages_data = []
|
||
|
||
for message in context_messages:
|
||
# Process attachments for this message
|
||
attachments_data = []
|
||
for attachment in message.attachments:
|
||
attachment_info = {
|
||
"filename": attachment.filename,
|
||
"url": attachment.url,
|
||
"proxy_url": attachment.proxy_url,
|
||
"size": attachment.size,
|
||
"content_type": attachment.content_type
|
||
}
|
||
|
||
# Download and encode image attachments for permanent storage
|
||
if attachment.content_type and attachment.content_type.startswith('image/'):
|
||
try:
|
||
import aiohttp
|
||
import base64
|
||
|
||
async with aiohttp.ClientSession() as session:
|
||
async with session.get(attachment.url) as response:
|
||
if response.status == 200 and len(await response.read()) < 8 * 1024 * 1024: # Max 8MB
|
||
image_data = await response.read()
|
||
attachment_info["data"] = base64.b64encode(image_data).decode('utf-8')
|
||
except Exception as e:
|
||
logger.warning(f"Could not download attachment {attachment.filename}: {e}")
|
||
|
||
attachments_data.append(attachment_info)
|
||
|
||
# Process embeds for this message
|
||
embeds_data = []
|
||
for embed in message.embeds:
|
||
embed_info = {
|
||
"title": embed.title,
|
||
"description": embed.description,
|
||
"url": embed.url,
|
||
"color": embed.color.value if embed.color else None,
|
||
"timestamp": embed.timestamp.isoformat() if embed.timestamp else None
|
||
}
|
||
embeds_data.append(embed_info)
|
||
|
||
# Create message data
|
||
msg_data = {
|
||
"id": message.id,
|
||
"content": message.content,
|
||
"author_id": message.author.id,
|
||
"author_name": message.author.display_name,
|
||
"author_username": message.author.name,
|
||
"channel_id": message.channel.id,
|
||
"attachments": json.dumps(attachments_data) if attachments_data else None,
|
||
"embeds": json.dumps(embeds_data) if embeds_data else None,
|
||
"created_at": message.created_at.isoformat(),
|
||
"edited_at": message.edited_at.isoformat() if message.edited_at else None,
|
||
"message_type": str(message.type),
|
||
"flags": message.flags.value if message.flags else 0,
|
||
"is_main_message": message.id == message_id # Mark the main referenced message
|
||
}
|
||
|
||
all_messages_data.append(msg_data)
|
||
|
||
# Return structured data with main message and context
|
||
return {
|
||
"main_message": next((msg for msg in all_messages_data if msg["is_main_message"]), None),
|
||
"context_messages": all_messages_data,
|
||
"context_range": context_range,
|
||
"total_messages": len(all_messages_data)
|
||
}
|
||
|
||
return message_data
|
||
|
||
except discord.NotFound:
|
||
logger.warning(f"Message {message_id} not found")
|
||
return None
|
||
except discord.Forbidden:
|
||
logger.warning(f"No permission to access message {message_id}")
|
||
return None
|
||
except Exception as e:
|
||
logger.error(f"Error retrieving message data: {e}")
|
||
return None
|
||
|
||
async def save_user_roles(user_id, guild_id, roles):
|
||
"""Saves a user's roles before a mute"""
|
||
connection = None
|
||
cursor = None
|
||
try:
|
||
connection = connect_to_database()
|
||
cursor = connection.cursor()
|
||
|
||
# Serialize the role IDs
|
||
role_ids = [str(role.id) for role in roles if not role.is_default()]
|
||
serialized_roles = json.dumps(role_ids)
|
||
|
||
insert_query = """
|
||
INSERT INTO user_saved_roles (user_id, guild_id, roles, saved_at)
|
||
VALUES (%s, %s, %s, %s)
|
||
ON DUPLICATE KEY UPDATE roles = %s, saved_at = %s
|
||
"""
|
||
current_time = datetime.now()
|
||
cursor.execute(insert_query, (user_id, guild_id, serialized_roles, current_time, serialized_roles, current_time))
|
||
connection.commit()
|
||
|
||
logger.info(f"Saved roles for user {user_id} in guild {guild_id}")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error saving user roles: {e}")
|
||
if connection:
|
||
connection.rollback()
|
||
finally:
|
||
if cursor:
|
||
cursor.close()
|
||
if connection:
|
||
close_database_connection(connection)
|
||
|
||
async def restore_user_roles(user, guild):
|
||
"""Restores a user's saved roles"""
|
||
connection = None
|
||
cursor = None
|
||
try:
|
||
connection = connect_to_database()
|
||
cursor = connection.cursor()
|
||
|
||
select_query = "SELECT roles FROM user_saved_roles WHERE user_id = %s AND guild_id = %s"
|
||
cursor.execute(select_query, (user.id, guild.id))
|
||
result = cursor.fetchone()
|
||
|
||
if result:
|
||
role_ids = json.loads(result[0])
|
||
roles_to_add = []
|
||
|
||
for role_id in role_ids:
|
||
role = guild.get_role(int(role_id))
|
||
if role and role < guild.me.top_role: # Check if bot can assign the role
|
||
roles_to_add.append(role)
|
||
|
||
if roles_to_add:
|
||
await user.add_roles(*roles_to_add, reason="Mute expired - restoring roles")
|
||
logger.info(f"Restored {len(roles_to_add)} roles for user {user.id}")
|
||
|
||
# Delete the saved roles
|
||
delete_query = "DELETE FROM user_saved_roles WHERE user_id = %s AND guild_id = %s"
|
||
cursor.execute(delete_query, (user.id, guild.id))
|
||
connection.commit()
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error restoring user roles: {e}")
|
||
if connection:
|
||
connection.rollback()
|
||
finally:
|
||
if cursor:
|
||
cursor.close()
|
||
if connection:
|
||
close_database_connection(connection)
|
||
|
||
@client.hybrid_command()
|
||
async def warn(ctx, user: discord.User, reason: str = "No reason provided", message_id: str = None, context_range: int = 3, silent: bool = False):
|
||
"""Warns a user (Requires Permission Level 5 or higher)
|
||
|
||
Usage:
|
||
/warn @user "Inappropriate behavior"
|
||
/warn @user "Bad language" 1407754702564884622
|
||
/warn @user "Spam" 1407754702564884622 15
|
||
/warn @user "Bad behavior" silent:True (silent mode - no public announcement)
|
||
|
||
Parameters:
|
||
- user: The user to warn
|
||
- reason: Reason for the warning
|
||
- message_id: Optional message ID to reference (required for context_range)
|
||
- context_range: Number of messages before/after to archive (only works with message_id)
|
||
- silent: If True, only send ephemeral response to mod (no public message)
|
||
|
||
Note: context_range parameter only works when message_id is also provided!
|
||
"""
|
||
# Check if it's a slash command and defer if needed
|
||
is_slash_command = hasattr(ctx, 'interaction') and ctx.interaction
|
||
if silent:
|
||
logger.info(f"Silent warn command - is_slash_command: {is_slash_command}, has interaction: {hasattr(ctx, 'interaction')}, interaction value: {getattr(ctx, 'interaction', None)}")
|
||
|
||
# For slash commands, always defer to ensure we have a response method
|
||
if is_slash_command:
|
||
await ctx.defer(ephemeral=silent) # Defer as ephemeral if silent mode
|
||
|
||
# Helper function for sending responses
|
||
async def send_response(content=None, embed=None, ephemeral=False, file=None):
|
||
try:
|
||
if is_slash_command:
|
||
if hasattr(ctx, 'followup') and ctx.followup:
|
||
await ctx.followup.send(content=content, embed=embed, ephemeral=ephemeral, file=file)
|
||
elif hasattr(ctx, 'response') and not ctx.response.is_done():
|
||
await ctx.response.send_message(content=content, embed=embed, ephemeral=ephemeral, file=file)
|
||
else:
|
||
await ctx.send(content=content, embed=embed, file=file)
|
||
else:
|
||
await ctx.send(content=content, embed=embed, file=file)
|
||
except Exception as e:
|
||
logger.error(f"Error sending response in warn command: {e}")
|
||
# Final fallback - try basic send
|
||
try:
|
||
if embed:
|
||
await ctx.send(embed=embed)
|
||
elif content:
|
||
await ctx.send(content=content)
|
||
except Exception as fallback_error:
|
||
logger.error(f"Fallback send also failed: {fallback_error}")
|
||
|
||
try:
|
||
# Parse message ID and context range from reason if provided inline
|
||
original_reason = reason
|
||
|
||
# Check if reason ends with potential message ID and/or context range
|
||
reason_words = reason.split()
|
||
parsed_message_id = message_id
|
||
parsed_context_range = 3 # Default context range, only used if message_id is provided
|
||
|
||
# Look for patterns like "reason 1234567890123456789" or "reason 1234567890123456789 15"
|
||
if len(reason_words) >= 2:
|
||
# Check if last word is a number (could be context range)
|
||
if reason_words[-1].isdigit() and len(reason_words[-1]) <= 3:
|
||
potential_context = int(reason_words[-1])
|
||
if 1 <= potential_context <= 25: # Valid context range
|
||
parsed_context_range = potential_context
|
||
reason_words = reason_words[:-1] # Remove context range from reason
|
||
|
||
# Check if last word (after removing context) is a message ID
|
||
if len(reason_words) >= 2 and len(reason_words[-1]) >= 17 and len(reason_words[-1]) <= 20 and reason_words[-1].isdigit():
|
||
parsed_message_id = reason_words[-1]
|
||
reason_words = reason_words[:-1] # Remove message ID from reason
|
||
|
||
# Update reason without the message ID and context range
|
||
reason = " ".join(reason_words)
|
||
|
||
# Only use context_range parameter if message_id is also provided
|
||
if parsed_message_id and message_id and context_range != 3:
|
||
# If message_id was provided as parameter and context_range was also set, use it
|
||
parsed_context_range = context_range
|
||
elif not parsed_message_id:
|
||
# If no message_id was found, reset context_range to default
|
||
parsed_context_range = 3
|
||
|
||
# Validate and limit context range
|
||
if parsed_context_range < 1:
|
||
parsed_context_range = 1
|
||
elif parsed_context_range > 25:
|
||
parsed_context_range = 25
|
||
|
||
# message_data will be populated if message_id is provided
|
||
message_data = None
|
||
|
||
# Try to get message data if message ID was provided
|
||
if parsed_message_id:
|
||
# Convert message_id string to int
|
||
try:
|
||
message_id_int = int(parsed_message_id)
|
||
except ValueError:
|
||
await send_response(content=f"❌ Invalid message ID: {parsed_message_id}")
|
||
return
|
||
# Try to get message data from current channel first with specified context range
|
||
message_data = await get_message_data(ctx.channel, message_id_int, parsed_context_range)
|
||
|
||
# If not found in current channel, try other channels the bot can access
|
||
if message_data is None:
|
||
# Limit search to avoid spam - only check first 10 channels
|
||
channels_to_check = ctx.guild.text_channels[:10]
|
||
for channel in channels_to_check:
|
||
if channel.id == ctx.channel.id:
|
||
continue # Skip current channel, already checked
|
||
try:
|
||
message_data = await get_message_data(channel, message_id_int, parsed_context_range)
|
||
if message_data is not None:
|
||
break
|
||
except discord.Forbidden:
|
||
continue
|
||
|
||
# Load moderator data
|
||
mod_data = await load_user_data(ctx.author.id, ctx.guild.id)
|
||
|
||
# Check moderation rights
|
||
if not check_moderation_permission(mod_data["permission"]):
|
||
embed = discord.Embed(
|
||
title="❌ Insufficient Permissions",
|
||
description="You need moderation permissions (Level 5 or higher) to use this command.",
|
||
color=0xff0000
|
||
)
|
||
await send_response(embed=embed, ephemeral=True)
|
||
return
|
||
|
||
# Cannot warn yourself
|
||
if user.id == ctx.author.id:
|
||
embed = discord.Embed(
|
||
title="❌ Invalid Action",
|
||
description="You cannot warn yourself!",
|
||
color=0xff0000
|
||
)
|
||
await send_response(embed=embed, ephemeral=True)
|
||
return
|
||
|
||
# Check if target has higher permissions
|
||
target_data = await load_user_data(user.id, ctx.guild.id)
|
||
|
||
if target_data["permission"] >= mod_data["permission"]:
|
||
embed = discord.Embed(
|
||
title="❌ Insufficient Permissions",
|
||
description="You cannot warn someone with equal or higher permissions than you.",
|
||
color=0xff0000
|
||
)
|
||
await send_response(embed=embed, ephemeral=True)
|
||
return
|
||
|
||
# Increase warn count
|
||
target_data["warns"] += 1
|
||
update_user_data(user.id, ctx.guild.id, "warns", target_data["warns"])
|
||
|
||
# Save detailed warning record to database
|
||
warning_id = await save_warning_to_database(
|
||
user_id=user.id,
|
||
guild_id=ctx.guild.id,
|
||
moderator_id=ctx.author.id,
|
||
reason=reason,
|
||
message_data=message_data,
|
||
message_id=int(parsed_message_id) if parsed_message_id else None
|
||
)
|
||
|
||
# Get guild settings for threshold checking
|
||
guild_settings = get_guild_settings(ctx.guild.id)
|
||
warn_threshold = guild_settings.get("max_warn_threshold", 3)
|
||
|
||
# Create embed
|
||
embed = discord.Embed(
|
||
title="⚠️ Warning Issued",
|
||
description=f"{user.mention} has been warned.",
|
||
color=0xff9500,
|
||
timestamp=datetime.now()
|
||
)
|
||
embed.add_field(name="📝 Reason", value=reason, inline=False)
|
||
embed.add_field(name="👮 Moderator", value=ctx.author.mention, inline=True)
|
||
embed.add_field(name="⚠️ Warning Count", value=f"{target_data['warns']}/{warn_threshold}", inline=True)
|
||
|
||
if warning_id:
|
||
embed.add_field(name="🆔 Warning ID", value=str(warning_id), inline=True)
|
||
|
||
# Add message information if available
|
||
if message_data:
|
||
# Handle new context message format
|
||
if isinstance(message_data, dict) and "main_message" in message_data:
|
||
main_msg = message_data.get("main_message")
|
||
context_msgs = message_data.get("context_messages", [])
|
||
|
||
if main_msg:
|
||
message_info = f"**Message ID:** `{main_msg['id']}`\n"
|
||
message_info += f"**Channel:** <#{main_msg['channel_id']}>\n"
|
||
message_info += f"**Author:** {main_msg['author_name']}\n"
|
||
|
||
if main_msg['content']:
|
||
content_preview = main_msg['content']
|
||
if len(content_preview) > 100:
|
||
content_preview = content_preview[:97] + "..."
|
||
message_info += f"**Content:** {content_preview}\n"
|
||
|
||
# Show attachment info
|
||
if main_msg['attachments']:
|
||
attachments = json.loads(main_msg['attachments'])
|
||
if attachments:
|
||
attachment_names = [att['filename'] for att in attachments[:3]]
|
||
message_info += f"**Attachments:** {', '.join(attachment_names)}"
|
||
if len(attachments) > 3:
|
||
message_info += f" +{len(attachments) - 3} more"
|
||
|
||
embed.add_field(name="📄 Referenced Message", value=message_info, inline=False)
|
||
else:
|
||
# Handle old format for backward compatibility
|
||
message_info = f"**Message ID:** `{message_data.get('id', 'Unknown')}`\n"
|
||
message_info += f"**Channel:** <#{message_data.get('channel_id', 'Unknown')}>\n"
|
||
|
||
if message_data.get('content'):
|
||
content_preview = message_data['content']
|
||
if len(content_preview) > 100:
|
||
content_preview = content_preview[:97] + "..."
|
||
message_info += f"**Content:** {content_preview}\n"
|
||
|
||
embed.add_field(name="📄 Referenced Message", value=message_info, inline=False)
|
||
elif message_id:
|
||
embed.add_field(
|
||
name="⚠️ Message Not Found",
|
||
value=f"Could not retrieve message `{message_id}` (deleted or no permission)",
|
||
inline=False
|
||
)
|
||
|
||
embed.set_footer(text=f"User ID: {user.id}")
|
||
embed.set_thumbnail(url=user.display_avatar.url)
|
||
|
||
# Check if user has reached the warning threshold
|
||
if target_data['warns'] >= warn_threshold:
|
||
auto_mute_enabled = guild_settings.get("auto_mute_on_warns", False)
|
||
if auto_mute_enabled:
|
||
mute_role_id = guild_settings.get("mute_role_id")
|
||
if mute_role_id and ctx.guild.get_role(mute_role_id):
|
||
mute_role = ctx.guild.get_role(mute_role_id)
|
||
try:
|
||
member = ctx.guild.get_member(user.id)
|
||
if member:
|
||
await member.add_roles(mute_role, reason=f"Automatic mute: {warn_threshold} warnings reached")
|
||
embed.add_field(
|
||
name="🔇 Automatic Action",
|
||
value=f"User has been automatically muted for reaching {warn_threshold} warnings.",
|
||
inline=False
|
||
)
|
||
|
||
# Log the automatic mute action
|
||
await log_moderation_action(
|
||
guild=ctx.guild,
|
||
action_type="mute",
|
||
moderator=ctx.author,
|
||
target_user=user,
|
||
reason=f"Automatic mute: {warn_threshold} warnings reached",
|
||
additional_info={"Trigger": f"Warning #{target_data['warns']}"}
|
||
)
|
||
except discord.Forbidden:
|
||
embed.add_field(
|
||
name="⚠️ Warning",
|
||
value="Could not automatically mute user due to insufficient permissions.",
|
||
inline=False
|
||
)
|
||
else:
|
||
embed.add_field(
|
||
name="🚨 Threshold Reached",
|
||
value=f"User has reached the warning threshold ({warn_threshold} warnings). Consider further action.",
|
||
inline=False
|
||
)
|
||
|
||
# Send response based on silent mode
|
||
if silent:
|
||
# Silent mode: create special silent embed and send as ephemeral
|
||
silent_embed = discord.Embed(
|
||
title="🔇 Silent Warning Issued",
|
||
description=f"{user.mention} has been warned silently.",
|
||
color=0xff9500,
|
||
timestamp=datetime.now()
|
||
)
|
||
silent_embed.add_field(name="📝 Reason", value=reason, inline=False)
|
||
silent_embed.add_field(name="⚠️ Warning Count", value=f"{target_data['warns']}/{warn_threshold}", inline=True)
|
||
if warning_id:
|
||
silent_embed.add_field(name="🆔 Warning ID", value=str(warning_id), inline=True)
|
||
|
||
silent_embed.add_field(name="🔔 Actions Taken",
|
||
value="• User received DM notification\n• Mod log entry created\n• No public announcement",
|
||
inline=False)
|
||
silent_embed.set_footer(text=f"Silent Mode • User ID: {user.id}")
|
||
silent_embed.set_thumbnail(url=user.display_avatar.url)
|
||
|
||
# Send as ephemeral
|
||
if is_slash_command:
|
||
if hasattr(ctx, 'followup') and ctx.followup is not None:
|
||
await ctx.followup.send(embed=silent_embed, ephemeral=True)
|
||
logger.info(f"Silent warning sent via ctx.followup.send (ephemeral)")
|
||
elif hasattr(ctx, 'interaction') and ctx.interaction:
|
||
await ctx.interaction.followup.send(embed=silent_embed, ephemeral=True)
|
||
logger.info(f"Silent warning sent via ctx.interaction.followup.send (ephemeral)")
|
||
else:
|
||
logger.error(f"Silent warning failed: No followup available")
|
||
else:
|
||
logger.error(f"Silent warning attempted with prefix command - not supported")
|
||
else:
|
||
# Normal mode: send public response
|
||
await send_response(embed=embed)
|
||
|
||
# Log the warning action
|
||
log_additional_info = {
|
||
"Warning Count": f"{target_data['warns']}/{warn_threshold}",
|
||
"Warning ID": str(warning_id) if warning_id else "N/A"
|
||
}
|
||
|
||
if message_data:
|
||
# Handle both old and new message_data formats
|
||
if isinstance(message_data, dict) and "main_message" in message_data:
|
||
main_msg = message_data.get("main_message")
|
||
if main_msg:
|
||
log_additional_info["Referenced Message"] = f"ID: {main_msg.get('id')}"
|
||
log_additional_info["Message Channel"] = f"<#{main_msg.get('channel_id')}>"
|
||
if main_msg.get('content'):
|
||
content_preview = main_msg['content'][:200] + "..." if len(main_msg['content']) > 200 else main_msg['content']
|
||
log_additional_info["Message Content"] = content_preview
|
||
else:
|
||
# Handle old format
|
||
log_additional_info["Referenced Message"] = f"ID: {message_data.get('id')}"
|
||
log_additional_info["Message Channel"] = f"<#{message_data.get('channel_id')}>"
|
||
if message_data.get('content'):
|
||
content_preview = message_data['content'][:200] + "..." if len(message_data['content']) > 200 else message_data['content']
|
||
log_additional_info["Message Content"] = content_preview
|
||
|
||
await log_moderation_action(
|
||
guild=ctx.guild,
|
||
action_type="warn",
|
||
moderator=ctx.author,
|
||
target_user=user,
|
||
reason=reason,
|
||
additional_info=log_additional_info
|
||
)
|
||
|
||
# Try to DM the user
|
||
try:
|
||
dm_embed = discord.Embed(
|
||
title=f"⚠️ Warning from {ctx.guild.name}",
|
||
color=0xff9500,
|
||
timestamp=datetime.now()
|
||
)
|
||
dm_embed.add_field(name="👮 Moderator", value=ctx.author.display_name, inline=True)
|
||
dm_embed.add_field(name="📝 Reason", value=reason, inline=False)
|
||
dm_embed.add_field(name="⚠️ Total Warnings", value=f"{target_data['warns']}/{warn_threshold}", inline=True)
|
||
|
||
if target_data['warns'] >= warn_threshold:
|
||
dm_embed.add_field(
|
||
name="🔇 Additional Action",
|
||
value="You have reached the warning threshold. Further violations may result in more severe punishments.",
|
||
inline=False
|
||
)
|
||
|
||
await user.send(embed=dm_embed)
|
||
except discord.Forbidden:
|
||
# User has DMs disabled
|
||
pass
|
||
|
||
# Log the action
|
||
logger.info(f"User {user.id} warned by {ctx.author.id} in guild {ctx.guild.id}. Reason: {reason}")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in warn command: {e}")
|
||
embed = discord.Embed(
|
||
title="❌ Error",
|
||
description="An error occurred while processing the warning. Please try again.",
|
||
color=0xff0000
|
||
)
|
||
await send_response(embed=embed)
|
||
|
||
async def get_active_mute_info(user_id, guild_id):
|
||
"""Gets information about active mute for a user"""
|
||
try:
|
||
connection = connect_to_database()
|
||
cursor = connection.cursor()
|
||
|
||
# Check for active mute process
|
||
select_query = """
|
||
SELECT uuid, end_time, data, created_at
|
||
FROM active_processes
|
||
WHERE process_type = 'mute'
|
||
AND status = 'active'
|
||
AND user_id = %s
|
||
AND guild_id = %s
|
||
AND end_time > NOW()
|
||
ORDER BY created_at DESC
|
||
LIMIT 1
|
||
"""
|
||
|
||
cursor.execute(select_query, (user_id, guild_id))
|
||
result = cursor.fetchone()
|
||
|
||
if result:
|
||
uuid, end_time, data_json, created_at = result
|
||
data = json.loads(data_json) if data_json else {}
|
||
|
||
return {
|
||
"uuid": uuid,
|
||
"end_time": end_time,
|
||
"reason": data.get("reason", "No reason provided"),
|
||
"moderator_id": data.get("moderator_id"),
|
||
"created_at": created_at
|
||
}
|
||
|
||
cursor.close()
|
||
close_database_connection(connection)
|
||
return None
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error checking active mute: {e}")
|
||
return None
|
||
|
||
@client.hybrid_command()
|
||
async def account(ctx, user: discord.User = None):
|
||
"""Shows comprehensive account status including warnings, mutes, and current punishments"""
|
||
# Check if it's a slash command and defer if needed
|
||
is_slash_command = hasattr(ctx, 'interaction') and ctx.interaction
|
||
if is_slash_command:
|
||
await ctx.defer()
|
||
|
||
# Helper function for sending responses
|
||
async def send_response(content=None, embed=None, ephemeral=False, file=None):
|
||
try:
|
||
if is_slash_command:
|
||
if hasattr(ctx, 'followup') and ctx.followup:
|
||
await ctx.followup.send(content=content, embed=embed, ephemeral=ephemeral, file=file)
|
||
elif hasattr(ctx, 'response') and not ctx.response.is_done():
|
||
await ctx.response.send_message(content=content, embed=embed, ephemeral=ephemeral, file=file)
|
||
else:
|
||
await ctx.send(content=content, embed=embed, file=file)
|
||
else:
|
||
await ctx.send(content=content, embed=embed, file=file)
|
||
except Exception as e:
|
||
logger.error(f"Error sending response in account command: {e}")
|
||
# Final fallback - try basic send
|
||
try:
|
||
if embed:
|
||
await ctx.send(embed=embed)
|
||
elif content:
|
||
await ctx.send(content=content)
|
||
except Exception as fallback_error:
|
||
logger.error(f"Fallback send also failed: {fallback_error}")
|
||
|
||
try:
|
||
# Determine target user (self or specified user for moderators)
|
||
target_user = user if user else ctx.author
|
||
is_self_check = target_user.id == ctx.author.id
|
||
|
||
# If checking another user, verify moderation permissions
|
||
if not is_self_check:
|
||
mod_data = await load_user_data(ctx.author.id, ctx.guild.id)
|
||
if not check_moderation_permission(mod_data["permission"]):
|
||
embed = discord.Embed(
|
||
title="❌ Insufficient Permissions",
|
||
description="You need moderation permissions (Level 5 or higher) to check other users' accounts.",
|
||
color=0xff0000
|
||
)
|
||
await send_response(embed=embed, ephemeral=True)
|
||
|
||
user_data = await load_user_data(target_user.id, ctx.guild.id)
|
||
|
||
embed = discord.Embed(
|
||
title=f"📊 Account Status: {target_user.display_name}",
|
||
description=f"Complete account overview for {target_user.mention}",
|
||
color=0x3498db,
|
||
timestamp=datetime.now()
|
||
)
|
||
|
||
# Get guild settings for thresholds
|
||
guild_settings = get_guild_settings(ctx.guild.id)
|
||
|
||
# Get detailed warning records (active warnings only for user view)
|
||
warning_records = await get_user_warnings(target_user.id, ctx.guild.id, active_only=True)
|
||
|
||
# Warning information with threshold
|
||
warn_threshold = guild_settings.get("max_warn_threshold", 3)
|
||
warn_color = "🟢" if user_data["warns"] == 0 else "🟡" if user_data["warns"] < warn_threshold else "🔴"
|
||
embed.add_field(
|
||
name=f"{warn_color} Warnings",
|
||
value=f"**{user_data['warns']}/{warn_threshold}** warnings",
|
||
inline=True
|
||
)
|
||
|
||
# Mute information
|
||
mute_color = "🟢" if user_data["mutes"] == 0 else "🟡" if user_data["mutes"] < 3 else "🔴"
|
||
embed.add_field(
|
||
name=f"{mute_color} Mutes",
|
||
value=f"**{user_data['mutes']}** mute(s)",
|
||
inline=True
|
||
)
|
||
|
||
# AI Ban information
|
||
ai_ban_color = "🟢" if user_data["ai_ban"] == 0 else "🔴"
|
||
embed.add_field(
|
||
name=f"{ai_ban_color} AI Violations",
|
||
value=f"**{user_data['ai_ban']}** violation(s)",
|
||
inline=True
|
||
)
|
||
|
||
# Check for active mute
|
||
active_mute_info = await get_active_mute_info(target_user.id, ctx.guild.id)
|
||
if active_mute_info:
|
||
mute_text = f"🔇 **CURRENTLY MUTED**\n"
|
||
mute_text += f"Ends: <t:{int(active_mute_info['end_time'].timestamp())}:R>\n"
|
||
mute_text += f"Reason: {active_mute_info['reason'][:50]}{'...' if len(active_mute_info['reason']) > 50 else ''}"
|
||
embed.add_field(name="🚨 Active Punishment", value=mute_text, inline=False)
|
||
|
||
# Status assessment
|
||
total_infractions = user_data["warns"] + user_data["mutes"] + user_data["ai_ban"]
|
||
if active_mute_info:
|
||
status = "🔇 **Currently Muted** - Active punishment"
|
||
status_color = 0xff0000
|
||
elif total_infractions == 0:
|
||
status = "✅ **Clean Record** - No infractions"
|
||
status_color = 0x00ff00
|
||
elif total_infractions <= 2:
|
||
status = "⚠️ **Minor Infractions** - Stay careful"
|
||
status_color = 0xffa500
|
||
elif total_infractions <= 5:
|
||
status = "🔶 **Multiple Infractions** - Improve behavior"
|
||
status_color = 0xff6600
|
||
else:
|
||
status = "🔴 **High Risk** - Serious moderation attention"
|
||
status_color = 0xff0000
|
||
|
||
embed.add_field(name="📈 Account Status", value=status, inline=False)
|
||
embed.color = status_color
|
||
|
||
# Detailed warning history (show last 5 warnings)
|
||
if warning_records:
|
||
warning_history = "**Recent Warning History:**\n"
|
||
display_count = min(5, len(warning_records))
|
||
|
||
for i in range(display_count):
|
||
record = warning_records[i]
|
||
moderator = ctx.guild.get_member(record["moderator_id"])
|
||
mod_name = moderator.display_name if moderator else f"ID: {record['moderator_id']}"
|
||
|
||
# Format date
|
||
warning_date = record["created_at"].strftime("%d.%m.%Y %H:%M")
|
||
|
||
# Truncate reason if too long
|
||
reason = record["reason"]
|
||
if len(reason) > 40:
|
||
reason = reason[:37] + "..."
|
||
|
||
warning_line = f"`{warning_date}` **{mod_name}**: {reason}"
|
||
|
||
# Add message indicator if warning was linked to a message
|
||
if record.get("message_id"):
|
||
warning_line += " 📄"
|
||
|
||
warning_history += warning_line + "\n"
|
||
|
||
if len(warning_records) > 5:
|
||
warning_history += f"\n*... and {len(warning_records) - 5} more warning(s)*"
|
||
|
||
embed.add_field(name="📋 Warning Details", value=warning_history, inline=False)
|
||
|
||
# Account details
|
||
member = ctx.guild.get_member(target_user.id)
|
||
if member:
|
||
account_details = ""
|
||
account_details += f"**Joined Server:** <t:{int(member.joined_at.timestamp())}:D>\n"
|
||
account_details += f"**Account Created:** <t:{int(target_user.created_at.timestamp())}:D>\n"
|
||
account_details += f"**Permission Level:** {user_data['permission']}\n"
|
||
account_details += f"**XP:** {user_data.get('xp', 0)} (Level {user_data.get('level', 1)})"
|
||
embed.add_field(name="👤 Account Info", value=account_details, inline=True)
|
||
|
||
# Role information
|
||
if member.roles[1:]: # Exclude @everyone
|
||
role_list = [role.mention for role in member.roles[1:]][:5] # Show max 5 roles
|
||
role_text = ", ".join(role_list)
|
||
if len(member.roles) > 6:
|
||
role_text += f" +{len(member.roles) - 6} more"
|
||
embed.add_field(name="🎭 Roles", value=role_text, inline=True)
|
||
|
||
# Get guild settings for thresholds
|
||
threshold_info = f"Warning threshold: **{warn_threshold}** warnings"
|
||
auto_mute_enabled = guild_settings.get("auto_mute_on_warns", False)
|
||
if auto_mute_enabled:
|
||
auto_mute_duration = guild_settings.get("auto_mute_duration", "1 hour")
|
||
threshold_info += f"\nAuto-mute: **{auto_mute_duration}** (at {warn_threshold} warnings)"
|
||
|
||
embed.add_field(name="⚙️ Server Settings", value=threshold_info, inline=False)
|
||
|
||
# Tips for improvement (only show for users with infractions and when checking self)
|
||
if total_infractions > 0 and is_self_check:
|
||
tips = (
|
||
"💡 **Tips to improve:**\n"
|
||
"• Follow server rules carefully\n"
|
||
"• Be respectful to other members\n"
|
||
"• Ask moderators if you're unsure about something\n"
|
||
"• Avoid spam and inappropriate content"
|
||
)
|
||
embed.add_field(name="Improvement Guide", value=tips, inline=False)
|
||
|
||
embed.set_thumbnail(url=target_user.display_avatar.url)
|
||
embed.set_footer(text=f"User ID: {target_user.id} | Checked by: {ctx.author.display_name}")
|
||
|
||
await send_response(embed=embed)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in account command: {e}")
|
||
await send_response(content="❌ An error occurred while retrieving account information.")
|
||
|
||
@client.hybrid_command()
|
||
async def mywarns(ctx):
|
||
"""Shows your account status (alias for /account)"""
|
||
await account(ctx, user=None)
|
||
|
||
@client.hybrid_command()
|
||
async def modinfo(ctx, user: discord.User = None):
|
||
"""Shows comprehensive moderation information about a user (Requires Permission Level 5 or higher)"""
|
||
try:
|
||
# Load moderator data
|
||
mod_data = await load_user_data(ctx.author.id, ctx.guild.id)
|
||
|
||
# Check moderation rights
|
||
if not check_moderation_permission(mod_data["permission"]):
|
||
embed = discord.Embed(
|
||
title="❌ Insufficient Permissions",
|
||
description="You need moderation permissions (Level 5 or higher) to use this command.",
|
||
color=0xff0000
|
||
)
|
||
await ctx.send(embed=embed, ephemeral=True)
|
||
return
|
||
|
||
# Default to command author if no user specified
|
||
if user is None:
|
||
user = ctx.author
|
||
|
||
# Get member object for additional Discord info
|
||
member = ctx.guild.get_member(user.id)
|
||
|
||
# Load user data
|
||
user_data = await load_user_data(user.id, ctx.guild.id)
|
||
|
||
# Get guild settings
|
||
guild_settings = get_guild_settings(ctx.guild.id)
|
||
warn_threshold = guild_settings.get("max_warn_threshold", 3)
|
||
|
||
# Get detailed warning records (all warnings for moderator view)
|
||
warning_records = await get_user_warnings(user.id, ctx.guild.id, active_only=False)
|
||
|
||
# Create main embed
|
||
embed = discord.Embed(
|
||
title="🛡️ Moderation Information",
|
||
description=f"Comprehensive data for {user.mention}",
|
||
color=0x3498db,
|
||
timestamp=datetime.now()
|
||
)
|
||
|
||
# Basic user information
|
||
user_info = f"**Username:** {user.name}\n"
|
||
user_info += f"**Display Name:** {user.display_name}\n"
|
||
user_info += f"**User ID:** `{user.id}`\n"
|
||
|
||
if member:
|
||
user_info += f"**Joined Server:** <t:{int(member.joined_at.timestamp())}:F>\n"
|
||
user_info += f"**Joined Discord:** <t:{int(user.created_at.timestamp())}:F>\n"
|
||
|
||
# Account age
|
||
account_age = datetime.now(user.created_at.tzinfo) - user.created_at
|
||
server_age = datetime.now(member.joined_at.tzinfo) - member.joined_at
|
||
user_info += f"**Account Age:** {account_age.days} days\n"
|
||
user_info += f"**Server Membership:** {server_age.days} days"
|
||
else:
|
||
user_info += f"**Created:** <t:{int(user.created_at.timestamp())}:F>\n"
|
||
user_info += "**Status:** Not in server"
|
||
|
||
embed.add_field(name="👤 User Information", value=user_info, inline=True)
|
||
|
||
# Permission and role information
|
||
perm_info = f"**Permission Level:** {user_data['permission']}\n"
|
||
|
||
if member:
|
||
# Get roles (excluding @everyone)
|
||
roles = [role for role in member.roles if not role.is_default()]
|
||
if roles:
|
||
role_list = ", ".join([role.mention for role in roles[:10]]) # Limit to 10 roles
|
||
if len(roles) > 10:
|
||
role_list += f" +{len(roles) - 10} more"
|
||
perm_info += f"**Roles ({len(roles)}):** {role_list}\n"
|
||
else:
|
||
perm_info += "**Roles:** None\n"
|
||
|
||
# Highest role
|
||
highest_role = member.top_role
|
||
perm_info += f"**Highest Role:** {highest_role.mention}"
|
||
else:
|
||
perm_info += "**Roles:** User not in server"
|
||
|
||
embed.add_field(name="🎭 Permissions & Roles", value=perm_info, inline=True)
|
||
|
||
# Moderation statistics
|
||
mod_stats = f"**Warnings:** {user_data['warns']}/{warn_threshold}\n"
|
||
mod_stats += f"**Mutes:** {user_data['mutes']}\n"
|
||
mod_stats += f"**AI Violations:** {user_data['ai_ban']}\n"
|
||
|
||
# Calculate total infractions
|
||
total_infractions = user_data["warns"] + user_data["mutes"] + user_data["ai_ban"]
|
||
mod_stats += f"**Total Infractions:** {total_infractions}"
|
||
|
||
# Risk assessment
|
||
if total_infractions == 0:
|
||
risk_level = "🟢 Low Risk"
|
||
embed.color = 0x00ff00
|
||
elif total_infractions <= 2:
|
||
risk_level = "🟡 Medium Risk"
|
||
embed.color = 0xffa500
|
||
elif total_infractions <= 5:
|
||
risk_level = "🟠 High Risk"
|
||
embed.color = 0xff6600
|
||
else:
|
||
risk_level = "🔴 Critical Risk"
|
||
embed.color = 0xff0000
|
||
|
||
mod_stats += f"\n**Risk Level:** {risk_level}"
|
||
|
||
embed.add_field(name="📊 Moderation Statistics", value=mod_stats, inline=False)
|
||
|
||
# Recent warning history (last 3 warnings)
|
||
if warning_records:
|
||
warning_history = ""
|
||
display_count = min(3, len(warning_records))
|
||
|
||
for i in range(display_count):
|
||
record = warning_records[i]
|
||
moderator = ctx.guild.get_member(record["moderator_id"])
|
||
mod_name = moderator.display_name if moderator else f"ID: {record['moderator_id']}"
|
||
|
||
# Format date
|
||
warning_date = record["created_at"].strftime("%d.%m.%Y %H:%M")
|
||
|
||
# Truncate reason if too long
|
||
reason = record["reason"]
|
||
if len(reason) > 50:
|
||
reason = reason[:47] + "..."
|
||
|
||
# Add status indicator
|
||
status_indicator = "🟢" if record.get("aktiv", True) else "🔴"
|
||
warning_line = f"`{warning_date}` {status_indicator} **{mod_name}**: {reason}"
|
||
|
||
# Add message indicator and content preview if available
|
||
if record.get("message_id"):
|
||
warning_line += " 📄"
|
||
if record.get("message_content"):
|
||
content_preview = record["message_content"][:30] + "..." if len(record["message_content"]) > 30 else record["message_content"]
|
||
warning_line += f"\n *Message: {content_preview}*"
|
||
|
||
warning_history += warning_line + "\n"
|
||
|
||
if len(warning_records) > 3:
|
||
warning_history += f"*... and {len(warning_records) - 3} more warning(s)*"
|
||
|
||
warning_history += f"\n🟢 = Active Warning | 🔴 = Deactivated Warning"
|
||
|
||
embed.add_field(name="📋 Recent Warnings", value=warning_history, inline=False)
|
||
|
||
# Server activity (if member)
|
||
if member:
|
||
activity_info = ""
|
||
|
||
# Current status
|
||
if member.status != discord.Status.offline:
|
||
activity_info += f"**Status:** {str(member.status).title()}\n"
|
||
else:
|
||
activity_info += "**Status:** Offline\n"
|
||
|
||
# Current activity
|
||
if member.activity:
|
||
activity_type = str(member.activity.type).replace('ActivityType.', '').title()
|
||
activity_info += f"**Activity:** {activity_type} - {member.activity.name}\n"
|
||
|
||
# Voice channel
|
||
if member.voice:
|
||
activity_info += f"**Voice Channel:** {member.voice.channel.mention}\n"
|
||
|
||
# Mobile/Desktop
|
||
if member.is_on_mobile():
|
||
activity_info += "**Platform:** Mobile 📱"
|
||
else:
|
||
activity_info += "**Platform:** Desktop 🖥️"
|
||
|
||
if activity_info:
|
||
embed.add_field(name="🎮 Current Activity", value=activity_info, inline=True)
|
||
|
||
# Server settings relevant to this user
|
||
settings_info = f"**Warning Threshold:** {warn_threshold}\n"
|
||
auto_mute_enabled = guild_settings.get("auto_mute_on_warns", False)
|
||
if auto_mute_enabled:
|
||
auto_mute_duration = guild_settings.get("auto_mute_duration", "1 hour")
|
||
settings_info += f"**Auto-Mute:** Enabled ({auto_mute_duration})\n"
|
||
else:
|
||
settings_info += "**Auto-Mute:** Disabled\n"
|
||
|
||
settings_info += f"**Moderation Logs:** {'Enabled' if guild_settings.get('mod_log_enabled', False) else 'Disabled'}"
|
||
|
||
embed.add_field(name="⚙️ Server Settings", value=settings_info, inline=True)
|
||
|
||
# Set thumbnail and footer
|
||
embed.set_thumbnail(url=user.display_avatar.url)
|
||
embed.set_footer(text=f"Requested by {ctx.author.display_name} • Use /warn, /mute for actions")
|
||
|
||
await ctx.send(embed=embed)
|
||
|
||
# Log the modinfo request
|
||
logger.info(f"Modinfo requested for user {user.id} by {ctx.author.id} in guild {ctx.guild.id}")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in modinfo command: {e}")
|
||
embed = discord.Embed(
|
||
title="❌ Error",
|
||
description="An error occurred while retrieving user information. Please try again.",
|
||
color=0xff0000
|
||
)
|
||
await ctx.send(embed=embed)
|
||
|
||
@client.hybrid_command()
|
||
async def viewwarn(ctx, warning_id: int):
|
||
"""View detailed information about a specific warning (Requires Permission Level 5 or higher)"""
|
||
# Check if it's a slash command and defer if needed
|
||
is_slash_command = hasattr(ctx, 'interaction') and ctx.interaction
|
||
if is_slash_command:
|
||
await ctx.defer()
|
||
|
||
# Helper function for sending responses
|
||
async def send_response(content=None, embed=None, ephemeral=False, file=None):
|
||
try:
|
||
if is_slash_command:
|
||
if hasattr(ctx, 'followup') and ctx.followup:
|
||
await ctx.followup.send(content=content, embed=embed, ephemeral=ephemeral, file=file)
|
||
elif hasattr(ctx, 'response') and not ctx.response.is_done():
|
||
await ctx.response.send_message(content=content, embed=embed, ephemeral=ephemeral, file=file)
|
||
else:
|
||
await ctx.send(content=content, embed=embed, file=file)
|
||
else:
|
||
await ctx.send(content=content, embed=embed, file=file)
|
||
except Exception as e:
|
||
logger.error(f"Error sending response in viewwarn command: {e}")
|
||
# Fallback to regular send if followup fails
|
||
try:
|
||
await ctx.send(content=content, embed=embed, file=file)
|
||
except:
|
||
pass
|
||
|
||
try:
|
||
# Load moderator data
|
||
mod_data = await load_user_data(ctx.author.id, ctx.guild.id)
|
||
|
||
# Check moderation rights
|
||
if not check_moderation_permission(mod_data["permission"]):
|
||
embed = discord.Embed(
|
||
title="❌ Insufficient Permissions",
|
||
description="You need moderation permissions (Level 5 or higher) to use this command.",
|
||
color=0xff0000
|
||
)
|
||
await send_response(embed=embed, ephemeral=True)
|
||
return
|
||
|
||
# Get warning details from database
|
||
connection = None
|
||
cursor = None
|
||
try:
|
||
connection = connect_to_database()
|
||
cursor = connection.cursor()
|
||
|
||
select_query = """
|
||
SELECT user_id, guild_id, moderator_id, reason, created_at, message_id,
|
||
message_content, message_attachments, message_author_id, message_channel_id, context_messages, aktiv
|
||
FROM user_warnings
|
||
WHERE id = %s AND guild_id = %s
|
||
"""
|
||
|
||
cursor.execute(select_query, (warning_id, ctx.guild.id))
|
||
result = cursor.fetchone()
|
||
|
||
if not result:
|
||
embed = discord.Embed(
|
||
title="❌ Warning Not Found",
|
||
description=f"No warning with ID {warning_id} found in this server.",
|
||
color=0xff0000
|
||
)
|
||
await send_response(embed=embed, ephemeral=True)
|
||
return
|
||
|
||
# Parse result
|
||
user_id, guild_id, moderator_id, reason, created_at, message_id, message_content, message_attachments, message_author_id, message_channel_id, context_messages, aktiv = result
|
||
|
||
# Get user and moderator objects
|
||
warned_user = await client.fetch_user(user_id)
|
||
moderator = await client.fetch_user(moderator_id)
|
||
|
||
# Create detailed embed
|
||
embed = discord.Embed(
|
||
title=f"⚠️ Warning Details - ID: {warning_id}",
|
||
color=0xff9500,
|
||
timestamp=created_at
|
||
)
|
||
|
||
embed.add_field(name="👤 Warned User", value=f"{warned_user.mention}\n`{warned_user.id}`", inline=True)
|
||
embed.add_field(name="👮 Moderator", value=f"{moderator.mention}\n`{moderator.id}`", inline=True)
|
||
embed.add_field(name="📅 Date", value=f"<t:{int(created_at.timestamp())}:F>", inline=True)
|
||
|
||
# Add status field
|
||
status_text = "🟢 **Active**" if aktiv else "🔴 **Deactivated**"
|
||
embed.add_field(name="📊 Status", value=status_text, inline=True)
|
||
embed.add_field(name="🆔 Warning ID", value=f"`{warning_id}`", inline=True)
|
||
embed.add_field(name="", value="", inline=True) # Empty field for spacing
|
||
|
||
embed.add_field(name="📝 Reason", value=reason, inline=False)
|
||
|
||
# Add message information if available
|
||
if message_id:
|
||
message_info = f"**Message ID:** `{message_id}`\n"
|
||
|
||
if message_channel_id:
|
||
message_info += f"**Channel:** <#{message_channel_id}>\n"
|
||
|
||
if message_author_id:
|
||
try:
|
||
msg_author = await client.fetch_user(message_author_id)
|
||
message_info += f"**Author:** {msg_author.mention}\n"
|
||
except:
|
||
message_info += f"**Author ID:** `{message_author_id}`\n"
|
||
|
||
if message_content:
|
||
content_display = message_content
|
||
if len(content_display) > 500:
|
||
content_display = content_display[:497] + "..."
|
||
message_info += f"**Content:**\n```\n{content_display}\n```"
|
||
|
||
embed.add_field(name="📄 Referenced Message", value=message_info, inline=False)
|
||
|
||
# Handle attachments
|
||
if message_attachments:
|
||
try:
|
||
attachments = json.loads(message_attachments)
|
||
if attachments:
|
||
attachment_info = ""
|
||
for i, att in enumerate(attachments[:3]): # Show max 3 attachments
|
||
attachment_info += f"**{att['filename']}** ({att['size']} bytes)\n"
|
||
|
||
# Show image if available and encoded
|
||
if att.get('data') and att['content_type'].startswith('image/'):
|
||
try:
|
||
import base64
|
||
import io
|
||
|
||
# Create temporary file-like object
|
||
image_data = base64.b64decode(att['data'])
|
||
file = discord.File(io.BytesIO(image_data), filename=att['filename'])
|
||
|
||
# Send image separately if it's the first attachment
|
||
if i == 0:
|
||
await send_response(content=f"📎 **Attachment from Warning {warning_id}:**", file=file)
|
||
except Exception as e:
|
||
logger.warning(f"Could not display attachment: {e}")
|
||
|
||
if len(attachments) > 3:
|
||
attachment_info += f"*... and {len(attachments) - 3} more attachment(s)*"
|
||
|
||
embed.add_field(name="📎 Attachments", value=attachment_info, inline=False)
|
||
except Exception as e:
|
||
logger.error(f"Error processing attachments: {e}")
|
||
|
||
embed.set_thumbnail(url=warned_user.display_avatar.url)
|
||
embed.set_footer(text=f"Warning ID: {warning_id} | Guild: {ctx.guild.name}")
|
||
|
||
# Display context messages if available
|
||
if context_messages:
|
||
try:
|
||
context_data = json.loads(context_messages)
|
||
if context_data and len(context_data) > 1:
|
||
context_display = "**📋 Message Context:**\n"
|
||
|
||
for i, msg in enumerate(context_data):
|
||
timestamp = datetime.fromisoformat(msg['created_at'].replace('Z', '+00:00'))
|
||
author_name = msg.get('author_name', 'Unknown')
|
||
content = msg.get('content', '*No content*')
|
||
|
||
# Truncate long messages
|
||
if len(content) > 100:
|
||
content = content[:97] + "..."
|
||
|
||
# Mark the main message
|
||
marker = "🎯 " if msg.get('is_main_message') else "💬 "
|
||
|
||
context_display += f"{marker}**{author_name}** (<t:{int(timestamp.timestamp())}:t>):\n`{content}`\n\n"
|
||
|
||
# Limit to prevent embed overflow
|
||
if len(context_display) > 1800:
|
||
context_display += "*... (truncated)*"
|
||
break
|
||
|
||
# Send context as separate message to avoid embed limits
|
||
context_embed = discord.Embed(
|
||
title=f"📋 Message Context for Warning {warning_id}",
|
||
description=context_display,
|
||
color=0x3498db
|
||
)
|
||
await send_response(embed=context_embed)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error displaying context messages: {e}")
|
||
|
||
await send_response(embed=embed)
|
||
|
||
finally:
|
||
if cursor:
|
||
cursor.close()
|
||
if connection:
|
||
close_database_connection(connection)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in viewwarn command: {e}")
|
||
embed = discord.Embed(
|
||
title="❌ Error",
|
||
description="An error occurred while retrieving warning details. Please try again.",
|
||
color=0xff0000
|
||
)
|
||
await send_response(embed=embed)
|
||
|
||
@client.hybrid_command()
|
||
async def viewmute(ctx, identifier: str):
|
||
"""View detailed information about a specific mute (Requires Permission Level 5 or higher)
|
||
|
||
Parameters:
|
||
- identifier: Mute ID (e.g. 123) or Process UUID (e.g. abc123def-456...)
|
||
"""
|
||
# Check if it's a slash command and defer if needed
|
||
is_slash_command = hasattr(ctx, 'interaction') and ctx.interaction
|
||
if is_slash_command:
|
||
await ctx.defer()
|
||
|
||
# Helper function for sending responses
|
||
async def send_response(content=None, embed=None, ephemeral=False, file=None):
|
||
try:
|
||
if is_slash_command:
|
||
if hasattr(ctx, 'followup') and ctx.followup:
|
||
await ctx.followup.send(content=content, embed=embed, ephemeral=ephemeral, file=file)
|
||
elif hasattr(ctx, 'response') and not ctx.response.is_done():
|
||
await ctx.response.send_message(content=content, embed=embed, ephemeral=ephemeral, file=file)
|
||
else:
|
||
await ctx.send(content=content, embed=embed, file=file)
|
||
else:
|
||
await ctx.send(content=content, embed=embed, file=file)
|
||
except Exception as e:
|
||
logger.error(f"Error sending response in viewmute command: {e}")
|
||
# Final fallback - try basic send
|
||
try:
|
||
if embed:
|
||
await ctx.send(embed=embed)
|
||
elif content:
|
||
await ctx.send(content=content)
|
||
except Exception as fallback_error:
|
||
logger.error(f"Fallback send also failed: {fallback_error}")
|
||
|
||
try:
|
||
# Load moderator data
|
||
mod_data = await load_user_data(ctx.author.id, ctx.guild.id)
|
||
|
||
# Check moderation rights
|
||
if not check_moderation_permission(mod_data["permission"]):
|
||
embed = discord.Embed(
|
||
title="❌ Insufficient Permissions",
|
||
description="You need moderation permissions (Level 5 or higher) to use this command.",
|
||
color=0xff0000
|
||
)
|
||
await send_response(embed=embed, ephemeral=True)
|
||
return
|
||
|
||
# Determine if identifier is a mute ID (numeric) or process UUID (alphanumeric)
|
||
is_mute_id = identifier.isdigit()
|
||
|
||
# Get mute details from user_mutes database (preferred) or active_processes as fallback
|
||
connection = None
|
||
cursor = None
|
||
try:
|
||
connection = connect_to_database()
|
||
cursor = connection.cursor()
|
||
|
||
# Try to find mute in user_mutes table
|
||
if is_mute_id:
|
||
# Search by mute ID
|
||
select_query = """
|
||
SELECT * FROM user_mutes
|
||
WHERE id = %s AND guild_id = %s
|
||
LIMIT 1
|
||
"""
|
||
cursor.execute(select_query, (int(identifier), ctx.guild.id))
|
||
else:
|
||
# Search by process UUID
|
||
select_query = """
|
||
SELECT * FROM user_mutes
|
||
WHERE process_uuid = %s AND guild_id = %s
|
||
ORDER BY created_at DESC
|
||
LIMIT 1
|
||
"""
|
||
cursor.execute(select_query, (identifier, ctx.guild.id))
|
||
|
||
mute_result = cursor.fetchone()
|
||
|
||
if mute_result:
|
||
# Found in user_mutes table
|
||
columns = [desc[0] for desc in cursor.description]
|
||
mute_data = dict(zip(columns, mute_result))
|
||
|
||
# Get user and moderator objects
|
||
muted_user = await client.fetch_user(int(mute_data['user_id']))
|
||
moderator = await client.fetch_user(int(mute_data['moderator_id']))
|
||
|
||
# Get channel
|
||
channel = ctx.guild.get_channel(int(mute_data['channel_id'])) if mute_data['channel_id'] else None
|
||
|
||
# Create detailed embed
|
||
embed = discord.Embed(
|
||
title=f"🔇 Mute Details - ID: {mute_data['id']}",
|
||
color=0xff0000,
|
||
timestamp=mute_data['created_at']
|
||
)
|
||
|
||
embed.add_field(name="👤 Muted User", value=f"{muted_user.mention}\n`{muted_user.id}`", inline=True)
|
||
embed.add_field(name="👮 Moderator", value=f"{moderator.mention}\n`{moderator.id}`", inline=True)
|
||
embed.add_field(name="📅 Muted At", value=f"<t:{int(mute_data['start_time'].timestamp())}:F>", inline=True)
|
||
|
||
# Add status information
|
||
status_emoji = {"active": "🟢", "completed": "✅", "expired": "⏰", "cancelled": "❌"}.get(mute_data['status'], "❓")
|
||
aktiv_status = "🟢 Active" if mute_data['aktiv'] else "🔴 Inactive"
|
||
status_text = f"{status_emoji} **{mute_data['status'].title()}** ({aktiv_status})"
|
||
embed.add_field(name="📊 Status", value=status_text, inline=True)
|
||
|
||
# End time and duration info
|
||
if mute_data['end_time']:
|
||
if mute_data['aktiv'] and mute_data['status'] == 'active':
|
||
embed.add_field(name="⏰ Ends At", value=f"<t:{int(mute_data['end_time'].timestamp())}:F>\n<t:{int(mute_data['end_time'].timestamp())}:R>", inline=True)
|
||
else:
|
||
embed.add_field(name="⏰ Ended At", value=f"<t:{int(mute_data['end_time'].timestamp())}:F>", inline=True)
|
||
|
||
embed.add_field(name="⏱️ Duration", value=mute_data['duration'], inline=True)
|
||
|
||
# Add reason
|
||
embed.add_field(name="📝 Reason", value=mute_data['reason'], inline=False)
|
||
|
||
# Add channel information
|
||
if channel:
|
||
embed.add_field(name="📍 Channel", value=f"{channel.mention}\n`{channel.id}`", inline=True)
|
||
|
||
# Add mute role information
|
||
if mute_data['mute_role_id']:
|
||
mute_role = ctx.guild.get_role(int(mute_data['mute_role_id']))
|
||
if mute_role:
|
||
embed.add_field(name="🎭 Mute Role", value=f"{mute_role.mention}\n`{mute_role.id}`", inline=True)
|
||
else:
|
||
embed.add_field(name="🎭 Mute Role", value=f"❌ Deleted Role\n`{mute_data['mute_role_id']}`", inline=True)
|
||
|
||
# Unmute information
|
||
if not mute_data['aktiv'] and mute_data['unmuted_at']:
|
||
unmute_info = f"<t:{int(mute_data['unmuted_at'].timestamp())}:F>"
|
||
if mute_data['unmuted_by']:
|
||
unmuter = await client.fetch_user(int(mute_data['unmuted_by']))
|
||
unmute_info += f"\nBy: {unmuter.mention}"
|
||
if mute_data['auto_unmuted']:
|
||
unmute_info += "\n🤖 Automatic unmute"
|
||
embed.add_field(name="🔓 Unmuted At", value=unmute_info, inline=True)
|
||
|
||
# Message reference if available
|
||
if mute_data['message_id'] and mute_data['message_content']:
|
||
content_preview = mute_data['message_content'][:100] + "..." if len(mute_data['message_content']) > 100 else mute_data['message_content']
|
||
embed.add_field(name="📄 Referenced Message", value=f"ID: `{mute_data['message_id']}`\nContent: {content_preview}", inline=False)
|
||
|
||
embed.add_field(name="🆔 Process UUID", value=f"`{mute_data['process_uuid']}`", inline=False)
|
||
embed.add_field(name="🆔 Mute Record ID", value=f"`{mute_data['id']}`", inline=True)
|
||
|
||
embed.set_thumbnail(url=muted_user.display_avatar.url)
|
||
embed.set_footer(text=f"Mute Record from Database | Server: {ctx.guild.name}")
|
||
|
||
await send_response(embed=embed)
|
||
return
|
||
|
||
else:
|
||
# Fallback to active_processes table (only if identifier is UUID format)
|
||
if not is_mute_id:
|
||
select_query = """
|
||
SELECT uuid, process_type, guild_id, channel_id, user_id, target_id,
|
||
created_at, end_time, status, data
|
||
FROM active_processes
|
||
WHERE uuid = %s AND guild_id = %s AND process_type = 'mute'
|
||
"""
|
||
|
||
cursor.execute(select_query, (identifier, ctx.guild.id))
|
||
result = cursor.fetchone()
|
||
|
||
if result:
|
||
# Process old format result...
|
||
# [Continue with existing fallback logic]
|
||
pass
|
||
|
||
# If no results found
|
||
embed = discord.Embed(
|
||
title="❌ Mute Not Found",
|
||
description=f"No mute with {'ID' if is_mute_id else 'UUID'} `{identifier}` found in this server.",
|
||
color=0xff0000
|
||
)
|
||
await send_response(embed=embed, ephemeral=True)
|
||
return
|
||
|
||
# Parse result (fallback to old format)
|
||
uuid, process_type, guild_id, channel_id, user_id, target_id, created_at, end_time, status, data = result
|
||
|
||
# Parse data JSON
|
||
import json
|
||
proc_data = json.loads(data) if data else {}
|
||
|
||
# Get user and moderator objects
|
||
muted_user = await client.fetch_user(target_id)
|
||
moderator_id = proc_data.get('moderator_id', user_id)
|
||
moderator = await client.fetch_user(moderator_id)
|
||
|
||
# Get channel
|
||
channel = ctx.guild.get_channel(channel_id) if channel_id else None
|
||
|
||
# Create detailed embed
|
||
embed = discord.Embed(
|
||
title=f"🔇 Mute Details - ID: {uuid[:8]}",
|
||
color=0xff0000,
|
||
timestamp=created_at
|
||
)
|
||
|
||
embed.add_field(name="👤 Muted User", value=f"{muted_user.mention}\n`{muted_user.id}`", inline=True)
|
||
embed.add_field(name="👮 Moderator", value=f"{moderator.mention}\n`{moderator_id}`", inline=True)
|
||
embed.add_field(name="📅 Muted At", value=f"<t:{int(created_at.timestamp())}:F>", inline=True)
|
||
|
||
# Add status and duration information
|
||
status_emoji = {"active": "🟢", "completed": "✅", "expired": "⏰", "cancelled": "❌"}.get(status, "❓")
|
||
status_text = f"{status_emoji} **{status.title()}**"
|
||
embed.add_field(name="📊 Status", value=status_text, inline=True)
|
||
|
||
if end_time:
|
||
if status == "active":
|
||
embed.add_field(name="⏰ Ends At", value=f"<t:{int(end_time.timestamp())}:F>\n<t:{int(end_time.timestamp())}:R>", inline=True)
|
||
else:
|
||
embed.add_field(name="⏰ Ended At", value=f"<t:{int(end_time.timestamp())}:F>", inline=True)
|
||
|
||
embed.add_field(name="🆔 Process UUID", value=f"`{uuid}`", inline=True)
|
||
|
||
# Add reason
|
||
reason = mute_data.get('reason', 'No reason provided')
|
||
embed.add_field(name="📝 Reason", value=reason, inline=False)
|
||
|
||
# Add channel information
|
||
if channel:
|
||
embed.add_field(name="📍 Channel", value=f"{channel.mention}\n`{channel.id}`", inline=True)
|
||
|
||
# Add mute role information
|
||
mute_role_id = mute_data.get('mute_role_id')
|
||
if mute_role_id:
|
||
mute_role = ctx.guild.get_role(mute_role_id)
|
||
if mute_role:
|
||
embed.add_field(name="🎭 Mute Role", value=f"{mute_role.mention}\n`{mute_role.id}`", inline=True)
|
||
else:
|
||
embed.add_field(name="🎭 Mute Role", value=f"❌ Deleted Role\n`{mute_role_id}`", inline=True)
|
||
|
||
# Add duration calculation if still active
|
||
if status == "active" and end_time:
|
||
from datetime import datetime
|
||
now = datetime.now()
|
||
if end_time > now:
|
||
duration_left = end_time - now
|
||
days = duration_left.days
|
||
hours, remainder = divmod(duration_left.seconds, 3600)
|
||
minutes, _ = divmod(remainder, 60)
|
||
|
||
duration_text = []
|
||
if days > 0:
|
||
duration_text.append(f"{days}d")
|
||
if hours > 0:
|
||
duration_text.append(f"{hours}h")
|
||
if minutes > 0:
|
||
duration_text.append(f"{minutes}m")
|
||
|
||
embed.add_field(name="⏳ Time Remaining", value=" ".join(duration_text) if duration_text else "Less than 1 minute", inline=True)
|
||
|
||
embed.set_thumbnail(url=muted_user.display_avatar.url)
|
||
embed.set_footer(text=f"Process Type: {process_type.title()} | Server: {ctx.guild.name}")
|
||
|
||
await send_response(embed=embed)
|
||
|
||
finally:
|
||
if cursor:
|
||
cursor.close()
|
||
if connection:
|
||
close_database_connection(connection)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in viewmute command: {e}")
|
||
embed = discord.Embed(
|
||
title="❌ Error",
|
||
description="An error occurred while retrieving mute details. Please try again.",
|
||
color=0xff0000
|
||
)
|
||
await send_response(embed=embed)
|
||
|
||
@client.hybrid_command()
|
||
async def removewarn(ctx, warning_id: int):
|
||
"""Deactivates a warning (hides from /account but keeps data) - Level 6+ required"""
|
||
# Check if it's a slash command and defer if needed
|
||
is_slash_command = hasattr(ctx, 'interaction') and ctx.interaction
|
||
if is_slash_command:
|
||
await ctx.defer()
|
||
|
||
# Helper function for sending responses
|
||
async def send_response(content=None, embed=None, ephemeral=False, file=None):
|
||
try:
|
||
if is_slash_command:
|
||
if hasattr(ctx, 'followup') and ctx.followup:
|
||
await ctx.followup.send(content=content, embed=embed, ephemeral=ephemeral, file=file)
|
||
elif hasattr(ctx, 'response') and not ctx.response.is_done():
|
||
await ctx.response.send_message(content=content, embed=embed, ephemeral=ephemeral, file=file)
|
||
else:
|
||
await ctx.send(content=content, embed=embed, file=file)
|
||
else:
|
||
await ctx.send(content=content, embed=embed, file=file)
|
||
except Exception as e:
|
||
logger.error(f"Error sending response in removewarn command: {e}")
|
||
try:
|
||
await ctx.send(content=content, embed=embed, file=file)
|
||
except:
|
||
pass
|
||
|
||
try:
|
||
# Check permissions
|
||
user_data = await load_user_data(ctx.author.id, ctx.guild.id)
|
||
if user_data["permission"] < 6:
|
||
embed = discord.Embed(
|
||
title="❌ Insufficient Permissions",
|
||
description="You need Level 6 or higher permissions to deactivate warnings.",
|
||
color=0xff0000
|
||
)
|
||
await send_response(embed=embed, ephemeral=True)
|
||
return
|
||
|
||
# Get warning info first
|
||
connection = connect_to_database()
|
||
cursor = connection.cursor()
|
||
|
||
cursor.execute("SELECT user_id, guild_id, reason, aktiv FROM user_warnings WHERE id = %s", (warning_id,))
|
||
warning_data = cursor.fetchone()
|
||
|
||
if not warning_data:
|
||
embed = discord.Embed(
|
||
title="❌ Warning Not Found",
|
||
description=f"No warning found with ID `{warning_id}`.",
|
||
color=0xff0000
|
||
)
|
||
await send_response(embed=embed, ephemeral=True)
|
||
return
|
||
|
||
if warning_data[1] != ctx.guild.id:
|
||
embed = discord.Embed(
|
||
title="❌ Invalid Warning",
|
||
description="This warning doesn't belong to this server.",
|
||
color=0xff0000
|
||
)
|
||
await send_response(embed=embed, ephemeral=True)
|
||
return
|
||
|
||
if not warning_data[3]: # Already inactive
|
||
embed = discord.Embed(
|
||
title="⚠️ Warning Already Inactive",
|
||
description=f"Warning `{warning_id}` is already deactivated.",
|
||
color=0xffa500
|
||
)
|
||
await send_response(embed=embed, ephemeral=True)
|
||
return
|
||
|
||
# Deactivate the warning
|
||
success = await deactivate_warning(warning_id)
|
||
|
||
if success:
|
||
try:
|
||
target_user = await client.fetch_user(warning_data[0])
|
||
user_name = target_user.display_name
|
||
except:
|
||
user_name = f"User {warning_data[0]}"
|
||
|
||
embed = discord.Embed(
|
||
title="✅ Warning Deactivated",
|
||
description=f"Warning `{warning_id}` for {user_name} has been deactivated.\n\n**Reason:** {warning_data[2]}\n\nThe warning is now hidden from `/account` but data is preserved for admin review.",
|
||
color=0x00ff00
|
||
)
|
||
await send_response(embed=embed)
|
||
|
||
# Log the action
|
||
await log_moderation_action(
|
||
ctx.guild.id,
|
||
f"Warning `{warning_id}` deactivated by {ctx.author.display_name}",
|
||
ctx.author
|
||
)
|
||
else:
|
||
embed = discord.Embed(
|
||
title="❌ Error",
|
||
description="Failed to deactivate warning. Please try again.",
|
||
color=0xff0000
|
||
)
|
||
await send_response(embed=embed)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in removewarn command: {e}")
|
||
embed = discord.Embed(
|
||
title="❌ Error",
|
||
description="An error occurred while deactivating the warning.",
|
||
color=0xff0000
|
||
)
|
||
await send_response(embed=embed)
|
||
finally:
|
||
if cursor:
|
||
cursor.close()
|
||
if connection:
|
||
close_database_connection(connection)
|
||
|
||
@client.hybrid_command()
|
||
async def restorewarn(ctx, warning_id: int):
|
||
"""Reactivates a previously deactivated warning - Level 6+ required"""
|
||
# Check if it's a slash command and defer if needed
|
||
is_slash_command = hasattr(ctx, 'interaction') and ctx.interaction
|
||
if is_slash_command:
|
||
await ctx.defer()
|
||
|
||
# Helper function for sending responses
|
||
async def send_response(content=None, embed=None, ephemeral=False, file=None):
|
||
try:
|
||
if is_slash_command:
|
||
if hasattr(ctx, 'followup') and ctx.followup:
|
||
await ctx.followup.send(content=content, embed=embed, ephemeral=ephemeral, file=file)
|
||
elif hasattr(ctx, 'response') and not ctx.response.is_done():
|
||
await ctx.response.send_message(content=content, embed=embed, ephemeral=ephemeral, file=file)
|
||
else:
|
||
await ctx.send(content=content, embed=embed, file=file)
|
||
else:
|
||
await ctx.send(content=content, embed=embed, file=file)
|
||
except Exception as e:
|
||
logger.error(f"Error sending response in restorewarn command: {e}")
|
||
try:
|
||
await ctx.send(content=content, embed=embed, file=file)
|
||
except:
|
||
pass
|
||
|
||
try:
|
||
# Check permissions
|
||
user_data = await load_user_data(ctx.author.id, ctx.guild.id)
|
||
if user_data["permission"] < 6:
|
||
embed = discord.Embed(
|
||
title="❌ Insufficient Permissions",
|
||
description="You need Level 6 or higher permissions to reactivate warnings.",
|
||
color=0xff0000
|
||
)
|
||
await send_response(embed=embed, ephemeral=True)
|
||
return
|
||
|
||
# Get warning info first
|
||
connection = connect_to_database()
|
||
cursor = connection.cursor()
|
||
|
||
cursor.execute("SELECT user_id, guild_id, reason, aktiv FROM user_warnings WHERE id = %s", (warning_id,))
|
||
warning_data = cursor.fetchone()
|
||
|
||
if not warning_data:
|
||
embed = discord.Embed(
|
||
title="❌ Warning Not Found",
|
||
description=f"No warning found with ID `{warning_id}`.",
|
||
color=0xff0000
|
||
)
|
||
await send_response(embed=embed, ephemeral=True)
|
||
return
|
||
|
||
if warning_data[1] != ctx.guild.id:
|
||
embed = discord.Embed(
|
||
title="❌ Invalid Warning",
|
||
description="This warning doesn't belong to this server.",
|
||
color=0xff0000
|
||
)
|
||
await send_response(embed=embed, ephemeral=True)
|
||
return
|
||
|
||
if warning_data[3]: # Already active
|
||
embed = discord.Embed(
|
||
title="⚠️ Warning Already Active",
|
||
description=f"Warning `{warning_id}` is already active.",
|
||
color=0xffa500
|
||
)
|
||
await send_response(embed=embed, ephemeral=True)
|
||
return
|
||
|
||
# Reactivate the warning
|
||
success = await reactivate_warning(warning_id)
|
||
|
||
if success:
|
||
try:
|
||
target_user = await client.fetch_user(warning_data[0])
|
||
user_name = target_user.display_name
|
||
except:
|
||
user_name = f"User {warning_data[0]}"
|
||
|
||
embed = discord.Embed(
|
||
title="✅ Warning Reactivated",
|
||
description=f"Warning `{warning_id}` for {user_name} has been reactivated.\n\n**Reason:** {warning_data[2]}\n\nThe warning is now visible in `/account` again.",
|
||
color=0x00ff00
|
||
)
|
||
await send_response(embed=embed)
|
||
|
||
# Log the action
|
||
await log_moderation_action(
|
||
ctx.guild.id,
|
||
f"Warning `{warning_id}` reactivated by {ctx.author.display_name}",
|
||
ctx.author
|
||
)
|
||
else:
|
||
embed = discord.Embed(
|
||
title="❌ Error",
|
||
description="Failed to reactivate warning. Please try again.",
|
||
color=0xff0000
|
||
)
|
||
await send_response(embed=embed)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in restorewarn command: {e}")
|
||
embed = discord.Embed(
|
||
title="❌ Error",
|
||
description="An error occurred while reactivating the warning.",
|
||
color=0xff0000
|
||
)
|
||
await send_response(embed=embed)
|
||
finally:
|
||
if cursor:
|
||
cursor.close()
|
||
if connection:
|
||
close_database_connection(connection)
|
||
|
||
@client.hybrid_command()
|
||
async def mute(ctx, user: discord.User, duration: str, reason: str = "No reason provided", message_id: str = None, context_range: int = 3, silent: bool = False):
|
||
"""Mutes a user for a specified duration (Requires Permission Level 5 or higher)
|
||
|
||
Usage:
|
||
/mute @user 10m "Inappropriate behavior"
|
||
/mute @user 1h "Bad language" 1407754702564884622
|
||
/mute @user 1h "Bad language" 1407754702564884622 15
|
||
/mute @user 30m "Spamming" silent:True (silent mode - no public announcement)
|
||
|
||
Parameters:
|
||
- user: The user to mute
|
||
- duration: Duration (10m, 1h, 2d)
|
||
- reason: Reason for the mute (can include message ID and context range)
|
||
- message_id: Optional message ID to reference (required for context_range)
|
||
- context_range: Number of context messages to archive (only works with message_id)
|
||
- silent: If True, only send ephemeral response to mod (no public message)
|
||
|
||
Duration examples:
|
||
- 10m = 10 minutes
|
||
- 1h = 1 hour
|
||
- 2d = 2 days
|
||
|
||
You can also specify message ID and context range in the reason:
|
||
"Bad language 1407754702564884622 15" (15 messages before/after)
|
||
|
||
Note: context_range parameter only works when message_id is also provided!
|
||
"""
|
||
# Check if it's a slash command and defer if needed
|
||
is_slash_command = hasattr(ctx, 'interaction') and ctx.interaction
|
||
|
||
# For slash commands, always defer to ensure we have a response method
|
||
if is_slash_command:
|
||
await ctx.defer(ephemeral=silent) # Defer as ephemeral if silent mode
|
||
|
||
# Helper function for sending responses
|
||
async def send_response(content=None, embed=None, ephemeral=False, file=None):
|
||
try:
|
||
if is_slash_command:
|
||
if hasattr(ctx, 'followup') and ctx.followup:
|
||
await ctx.followup.send(content=content, embed=embed, ephemeral=ephemeral, file=file)
|
||
elif hasattr(ctx, 'response') and not ctx.response.is_done():
|
||
await ctx.response.send_message(content=content, embed=embed, ephemeral=ephemeral, file=file)
|
||
else:
|
||
await ctx.send(content=content, embed=embed, file=file)
|
||
else:
|
||
await ctx.send(content=content, embed=embed, file=file)
|
||
except Exception as e:
|
||
logger.error(f"Error sending response in mute command: {e}")
|
||
# Final fallback - try basic send
|
||
try:
|
||
if embed:
|
||
await ctx.send(embed=embed)
|
||
elif content:
|
||
await ctx.send(content=content)
|
||
except Exception as fallback_error:
|
||
logger.error(f"Fallback send also failed: {fallback_error}")
|
||
|
||
try:
|
||
# Parse message ID and context range from reason if they look valid
|
||
original_reason = reason
|
||
message_data = None
|
||
parsed_context_range = 3 # Default context range, only used if message_id is provided
|
||
|
||
# Check if reason contains potential message ID and context range
|
||
reason_words = reason.split()
|
||
if len(reason_words) >= 2:
|
||
# Check for pattern: "reason text 1234567890123456789 15"
|
||
potential_msg_id = reason_words[-2] if len(reason_words) >= 2 else None
|
||
potential_context = reason_words[-1] if len(reason_words) >= 1 else None
|
||
|
||
# Check if last two elements are message ID and context range
|
||
if (potential_msg_id and len(potential_msg_id) >= 17 and len(potential_msg_id) <= 20 and potential_msg_id.isdigit() and
|
||
potential_context and len(potential_context) <= 3 and potential_context.isdigit()):
|
||
parsed_context_range = int(potential_context)
|
||
message_id = potential_msg_id
|
||
reason = " ".join(reason_words[:-2]) # Remove both from reason
|
||
elif len(reason_words) >= 1:
|
||
# Check if reason ends with a potential message ID only
|
||
potential_msg_id = reason_words[-1]
|
||
if len(potential_msg_id) >= 17 and len(potential_msg_id) <= 20 and potential_msg_id.isdigit():
|
||
message_id = potential_msg_id
|
||
reason = " ".join(reason_words[:-1]) # Remove message ID from reason
|
||
|
||
# Only use context_range parameter if message_id is also provided
|
||
if message_id and context_range != 3:
|
||
# If message_id was provided and context_range was also set, use it
|
||
parsed_context_range = context_range
|
||
elif not message_id:
|
||
# If no message_id was found, reset context_range to default
|
||
parsed_context_range = 3
|
||
|
||
# Validate and limit context range
|
||
if parsed_context_range < 1:
|
||
parsed_context_range = 1
|
||
elif parsed_context_range > 25:
|
||
parsed_context_range = 25
|
||
|
||
# Try to get message data if message ID was provided
|
||
if message_id:
|
||
try:
|
||
message_id_int = int(message_id)
|
||
except ValueError:
|
||
await send_response(content=f"❌ Invalid message ID: {message_id}")
|
||
return
|
||
|
||
# Try to get message data from current channel first
|
||
message_data = await get_message_data(ctx.channel, message_id_int, context_range=parsed_context_range)
|
||
|
||
# If not found in current channel, try other channels
|
||
if message_data is None:
|
||
# Limit search to avoid spam - only check first 10 channels plus current channel
|
||
channels_to_check = [ctx.channel] + [ch for ch in ctx.guild.text_channels[:10] if ch.id != ctx.channel.id]
|
||
for channel in channels_to_check[1:]: # Skip current channel, already checked
|
||
try:
|
||
message_data = await get_message_data(channel, message_id_int, context_range=parsed_context_range)
|
||
if message_data is not None:
|
||
break
|
||
except discord.Forbidden:
|
||
continue
|
||
|
||
# Load moderator data
|
||
mod_data = await load_user_data(ctx.author.id, ctx.guild.id)
|
||
|
||
# Check moderation rights
|
||
if not check_moderation_permission(mod_data["permission"]):
|
||
embed = discord.Embed(
|
||
title="❌ Insufficient Permissions",
|
||
description="You need moderation permissions (Level 5 or higher) to use this command.",
|
||
color=0xff0000
|
||
)
|
||
await send_response(embed=embed, ephemeral=True)
|
||
return
|
||
|
||
# Cannot mute yourself
|
||
if user.id == ctx.author.id:
|
||
embed = discord.Embed(
|
||
title="❌ Invalid Action",
|
||
description="You cannot mute yourself!",
|
||
color=0xff0000
|
||
)
|
||
await send_response(embed=embed, ephemeral=True)
|
||
return
|
||
|
||
# Parse duration
|
||
time_units = {'m': 60, 'h': 3600, 'd': 86400}
|
||
if not duration or not duration[-1] in time_units or not duration[:-1].isdigit():
|
||
embed = discord.Embed(
|
||
title="❌ Invalid Duration",
|
||
description="Invalid time format. Use: 10m, 1h, 2d",
|
||
color=0xff0000
|
||
)
|
||
await send_response(embed=embed, ephemeral=True)
|
||
return
|
||
|
||
duration_seconds = int(duration[:-1]) * time_units[duration[-1]]
|
||
end_time = datetime.now() + timedelta(seconds=duration_seconds)
|
||
|
||
# Get member object
|
||
member = ctx.guild.get_member(user.id)
|
||
if not member:
|
||
embed = discord.Embed(
|
||
title="❌ User Not Found",
|
||
description="User not found on this server.",
|
||
color=0xff0000
|
||
)
|
||
await send_response(embed=embed, ephemeral=True)
|
||
return
|
||
|
||
# Load guild settings
|
||
guild_settings = get_guild_settings(ctx.guild.id)
|
||
|
||
# Save current roles
|
||
await save_user_roles(user.id, ctx.guild.id, member.roles)
|
||
|
||
# Remove all roles except @everyone
|
||
roles_to_remove = [role for role in member.roles if not role.is_default()]
|
||
if roles_to_remove:
|
||
await member.remove_roles(*roles_to_remove, reason=f"Muted by {ctx.author}")
|
||
|
||
# Get or create mute role
|
||
mute_role = await get_or_create_mute_role(ctx.guild, guild_settings)
|
||
if not mute_role:
|
||
embed = discord.Embed(
|
||
title="❌ Mute Role Error",
|
||
description="Could not find or create mute role. Check server settings.",
|
||
color=0xff0000
|
||
)
|
||
await send_response(embed=embed, ephemeral=True)
|
||
return
|
||
|
||
# Add mute role
|
||
await member.add_roles(mute_role, reason=f"Muted by {ctx.author} for {duration}")
|
||
|
||
# Update user data
|
||
user_data = await load_user_data(user.id, ctx.guild.id)
|
||
user_data["mutes"] += 1
|
||
update_user_data(user.id, ctx.guild.id, "mutes", user_data["mutes"])
|
||
|
||
# Create active process for auto-unmute
|
||
process_data = {
|
||
"user_id": user.id,
|
||
"guild_id": ctx.guild.id,
|
||
"channel_id": ctx.channel.id,
|
||
"reason": reason,
|
||
"moderator_id": ctx.author.id,
|
||
"mute_role_id": mute_role.id
|
||
}
|
||
|
||
process_uuid = create_active_process(
|
||
process_type="mute",
|
||
guild_id=ctx.guild.id,
|
||
channel_id=ctx.channel.id,
|
||
user_id=user.id,
|
||
target_id=user.id,
|
||
end_time=end_time,
|
||
data=process_data
|
||
)
|
||
|
||
# Save detailed mute record to database
|
||
mute_id = await save_mute_to_database(
|
||
user_id=user.id,
|
||
guild_id=ctx.guild.id,
|
||
moderator_id=ctx.author.id,
|
||
reason=reason,
|
||
duration=duration,
|
||
start_time=datetime.now(),
|
||
end_time=end_time,
|
||
process_uuid=process_uuid,
|
||
channel_id=ctx.channel.id,
|
||
mute_role_id=mute_role.id,
|
||
message_data=message_data,
|
||
message_id=int(message_id) if message_id else None
|
||
)
|
||
|
||
# Create embed
|
||
embed = discord.Embed(
|
||
title="🔇 User Muted",
|
||
description=f"{user.mention} has been muted.",
|
||
color=0xff0000,
|
||
timestamp=datetime.now()
|
||
)
|
||
embed.add_field(name="⏱️ Duration", value=duration, inline=True)
|
||
embed.add_field(name="⏰ Ends At", value=f"<t:{int(end_time.timestamp())}:F>", inline=True)
|
||
embed.add_field(name="📝 Reason", value=reason, inline=False)
|
||
embed.add_field(name="👮 Moderator", value=ctx.author.mention, inline=True)
|
||
embed.add_field(name="🔇 Mute Count", value=f"{user_data['mutes']}", inline=True)
|
||
|
||
# Add message information if available
|
||
if message_data:
|
||
# Handle new context message format
|
||
if isinstance(message_data, dict) and "main_message" in message_data:
|
||
main_msg = message_data.get("main_message")
|
||
context_msgs = message_data.get("context_messages", [])
|
||
|
||
if main_msg:
|
||
message_info = f"**Message ID:** `{main_msg['id']}`\n"
|
||
message_info += f"**Channel:** <#{main_msg['channel_id']}>\n"
|
||
message_info += f"**Author:** {main_msg['author_name']}\n"
|
||
|
||
if main_msg.get('content'):
|
||
content_preview = main_msg['content'][:200] + "..." if len(main_msg['content']) > 200 else main_msg['content']
|
||
message_info += f"**Content:** {content_preview}"
|
||
|
||
embed.add_field(name="📄 Referenced Message", value=message_info, inline=False)
|
||
|
||
# Process attachments for archival if any
|
||
if main_msg.get('attachments'):
|
||
try:
|
||
attachments_data = json.loads(main_msg['attachments'])
|
||
if attachments_data:
|
||
attachment_info = ""
|
||
for i, att in enumerate(attachments_data[:3]): # Show first 3 attachments
|
||
attachment_info += f"• {att.get('filename', 'Unknown file')}\n"
|
||
if len(attachments_data) > 3:
|
||
attachment_info += f"• +{len(attachments_data) - 3} more attachments"
|
||
embed.add_field(name="📎 Archived Attachments", value=attachment_info, inline=False)
|
||
except:
|
||
pass
|
||
else:
|
||
# Handle old format for backward compatibility
|
||
message_info = f"**Message ID:** `{message_data.get('id', 'Unknown')}`\n"
|
||
message_info += f"**Channel:** <#{message_data.get('channel_id', 'Unknown')}>\n"
|
||
message_info += f"**Author:** <@{message_data.get('author_id', 'Unknown')}>\n"
|
||
if message_data.get('content'):
|
||
content_preview = message_data['content'][:200] + "..." if len(message_data['content']) > 200 else message_data['content']
|
||
message_info += f"**Content:** {content_preview}"
|
||
embed.add_field(name="📄 Referenced Message", value=message_info, inline=False)
|
||
elif message_id:
|
||
embed.add_field(name="📄 Referenced Message", value=f"Message ID: `{message_id}` (Message not found or inaccessible)", inline=False)
|
||
|
||
# Add Mute Record ID field
|
||
embed.add_field(name="🆔 Mute Record ID", value=f"`{mute_id}`", inline=True)
|
||
|
||
embed.set_footer(text=f"User ID: {user.id} | Process ID: {str(process_uuid)[:8]} | Use /viewmute {mute_id} for details")
|
||
embed.set_thumbnail(url=user.display_avatar.url)
|
||
|
||
# Send response based on silent mode
|
||
if silent:
|
||
# Silent mode: ephemeral response to moderator only
|
||
silent_embed = discord.Embed(
|
||
title="🔇 Silent Mute Applied",
|
||
description=f"{user.mention} has been muted silently.",
|
||
color=0xff0000,
|
||
timestamp=datetime.now()
|
||
)
|
||
silent_embed.add_field(name="⏱️ Duration", value=duration, inline=True)
|
||
silent_embed.add_field(name="⏰ Ends At", value=f"<t:{int(end_time.timestamp())}:F>", inline=True)
|
||
silent_embed.add_field(name="📝 Reason", value=reason, inline=False)
|
||
silent_embed.add_field(name="🔇 Mute Count", value=f"{user_data['mutes']}", inline=True)
|
||
silent_embed.add_field(name="🆔 Mute Record ID", value=f"`{mute_id}`", inline=True)
|
||
|
||
silent_embed.add_field(name="🔔 Actions Taken",
|
||
value="• User muted with timeout role\n• User received DM notification\n• Mod log entry created\n• No public announcement",
|
||
inline=False)
|
||
silent_embed.set_footer(text=f"Silent Mode • User ID: {user.id} | Use /viewmute {mute_id} for details")
|
||
silent_embed.set_thumbnail(url=user.display_avatar.url)
|
||
|
||
# Send ephemeral response - use followup since we deferred
|
||
try:
|
||
if is_slash_command:
|
||
# Since we deferred with ephemeral=silent, use followup
|
||
# Make sure followup is available and properly initialized
|
||
if hasattr(ctx, 'followup') and ctx.followup is not None:
|
||
await ctx.followup.send(embed=silent_embed, ephemeral=True)
|
||
logger.info(f"Silent mute sent via ctx.followup.send (ephemeral)")
|
||
elif hasattr(ctx, 'interaction') and ctx.interaction:
|
||
# Direct interaction followup as fallback
|
||
await ctx.interaction.followup.send(embed=silent_embed, ephemeral=True)
|
||
logger.info(f"Silent mute sent via ctx.interaction.followup.send (ephemeral)")
|
||
else:
|
||
logger.error(f"Silent mute failed: No followup available - ctx.followup: {getattr(ctx, 'followup', None)}")
|
||
raise Exception("No followup available after defer")
|
||
else:
|
||
# For prefix commands, we can't do true ephemeral, so log error instead
|
||
logger.error(f"Silent mute attempted with prefix command - not supported")
|
||
raise Exception("Silent mode only works with slash commands")
|
||
except Exception as e:
|
||
logger.error(f"Error sending silent mute response: {e}")
|
||
# Send error to mod log instead of fallback message
|
||
try:
|
||
await log_moderation_action(
|
||
guild=ctx.guild,
|
||
action_type="mute_error",
|
||
moderator=ctx.author,
|
||
target_user=user,
|
||
reason=f"Silent mute issued but ephemeral response failed: {str(e)}",
|
||
duration=duration,
|
||
additional_info={
|
||
"Original Reason": reason,
|
||
"Mute ID": str(mute_id) if mute_id else "N/A",
|
||
"Duration": duration,
|
||
"Error Details": str(e),
|
||
"Command Type": "Slash" if is_slash_command else "Prefix",
|
||
"Fallback": "User received DM notification normally"
|
||
}
|
||
)
|
||
except Exception as log_error:
|
||
logger.error(f"Failed to log silent mute error: {log_error}")
|
||
|
||
# Silent mode is complete - exit here to prevent normal logging/responses
|
||
return
|
||
|
||
else:
|
||
# Normal mode: public response
|
||
await send_response(embed=embed)
|
||
|
||
# Log the mute action
|
||
log_additional_info = {
|
||
"Mute Count": str(user_data['mutes']),
|
||
"Process ID": str(process_uuid)[:8],
|
||
"Mute Record ID": str(mute_id)
|
||
}
|
||
|
||
if message_data:
|
||
# Handle new context message format
|
||
if isinstance(message_data, dict) and "main_message" in message_data:
|
||
main_msg = message_data.get("main_message")
|
||
if main_msg:
|
||
log_additional_info["Referenced Message"] = f"ID: {main_msg['id']} in <#{main_msg['channel_id']}>"
|
||
else:
|
||
# Handle old format
|
||
log_additional_info["Referenced Message"] = f"ID: {message_data['id']} in <#{message_data['channel_id']}>"
|
||
|
||
await log_moderation_action(
|
||
guild=ctx.guild,
|
||
action_type="mute",
|
||
moderator=ctx.author,
|
||
target_user=user,
|
||
reason=reason,
|
||
duration=duration,
|
||
additional_info=log_additional_info
|
||
)
|
||
|
||
# Try to DM the user
|
||
try:
|
||
dm_embed = discord.Embed(
|
||
title="🔇 You have been muted",
|
||
description=f"You have been muted in **{ctx.guild.name}**",
|
||
color=0xff0000,
|
||
timestamp=datetime.now()
|
||
)
|
||
dm_embed.add_field(name="⏱️ Duration", value=duration, inline=True)
|
||
dm_embed.add_field(name="⏰ Ends At", value=f"<t:{int(end_time.timestamp())}:F>", inline=True)
|
||
dm_embed.add_field(name="📝 Reason", value=reason, inline=False)
|
||
dm_embed.add_field(name="👮 Moderator", value=ctx.author.display_name, inline=True)
|
||
|
||
if message_data and message_data['content']:
|
||
content_preview = message_data['content'][:200] + "..." if len(message_data['content']) > 200 else message_data['content']
|
||
dm_embed.add_field(name="📄 Referenced Message", value=f"```{content_preview}```", inline=False)
|
||
|
||
dm_embed.set_footer(text=f"Server: {ctx.guild.name}")
|
||
await user.send(embed=dm_embed)
|
||
except discord.Forbidden:
|
||
pass # User has DMs disabled
|
||
|
||
# Log the action
|
||
logger.info(f"User {user.id} muted by {ctx.author.id} in guild {ctx.guild.id} for {duration}. Reason: {reason}")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in mute command: {e}")
|
||
embed = discord.Embed(
|
||
title="❌ Error",
|
||
description="An error occurred while processing the mute. Please try again.",
|
||
color=0xff0000
|
||
)
|
||
await send_response(embed=embed)
|
||
|
||
@client.hybrid_command()
|
||
async def unmute(ctx, user: discord.User):
|
||
"""Unmutes a user manually (Requires Permission Level 5 or higher)"""
|
||
# Check if it's a slash command and defer if needed
|
||
is_slash_command = hasattr(ctx, 'interaction') and ctx.interaction
|
||
if is_slash_command:
|
||
await ctx.defer()
|
||
|
||
# Helper function for sending responses
|
||
async def send_response(content=None, embed=None, ephemeral=False, file=None):
|
||
try:
|
||
if is_slash_command:
|
||
if hasattr(ctx, 'followup') and ctx.followup:
|
||
await ctx.followup.send(content=content, embed=embed, ephemeral=ephemeral, file=file)
|
||
elif hasattr(ctx, 'response') and not ctx.response.is_done():
|
||
await ctx.response.send_message(content=content, embed=embed, ephemeral=ephemeral, file=file)
|
||
else:
|
||
await ctx.send(content=content, embed=embed, file=file)
|
||
else:
|
||
await ctx.send(content=content, embed=embed, file=file)
|
||
except Exception as e:
|
||
logger.error(f"Error sending response in unmute command: {e}")
|
||
try:
|
||
if embed:
|
||
await ctx.send(embed=embed)
|
||
elif content:
|
||
await ctx.send(content=content)
|
||
except Exception as fallback_error:
|
||
logger.error(f"Fallback send also failed: {fallback_error}")
|
||
|
||
try:
|
||
# Load moderator data
|
||
mod_data = await load_user_data(ctx.author.id, ctx.guild.id)
|
||
|
||
# Check moderation permissions
|
||
if not check_moderation_permission(mod_data["permission"]):
|
||
embed = discord.Embed(
|
||
title="❌ Insufficient Permissions",
|
||
description="You need moderation permissions (Level 5 or higher) to use this command.",
|
||
color=0xff0000
|
||
)
|
||
await send_response(embed=embed, ephemeral=True)
|
||
return
|
||
|
||
# Get member object
|
||
member = ctx.guild.get_member(user.id)
|
||
if not member:
|
||
embed = discord.Embed(
|
||
title="❌ User Not Found",
|
||
description="User not found on this server.",
|
||
color=0xff0000
|
||
)
|
||
await send_response(embed=embed, ephemeral=True)
|
||
return
|
||
|
||
# Load guild settings
|
||
guild_settings = get_guild_settings(ctx.guild.id)
|
||
|
||
# Find mute role based on settings
|
||
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:
|
||
embed = discord.Embed(
|
||
title="❌ User Not Muted",
|
||
description="This user is not currently muted.",
|
||
color=0xff0000
|
||
)
|
||
await send_response(embed=embed, ephemeral=True)
|
||
return
|
||
|
||
# Find active mute records in user_mutes table
|
||
connection = None
|
||
cursor = None
|
||
active_mute_records = []
|
||
|
||
try:
|
||
connection = connect_to_database()
|
||
cursor = connection.cursor()
|
||
|
||
# Get active mute records for this user
|
||
select_query = """
|
||
SELECT id, process_uuid, reason, start_time, end_time, duration
|
||
FROM user_mutes
|
||
WHERE user_id = %s AND guild_id = %s AND aktiv = TRUE AND status = 'active'
|
||
ORDER BY start_time DESC
|
||
"""
|
||
|
||
cursor.execute(select_query, (user.id, ctx.guild.id))
|
||
results = cursor.fetchall()
|
||
|
||
for result in results:
|
||
mute_id, process_uuid, reason, start_time, end_time, duration = result
|
||
active_mute_records.append({
|
||
'id': mute_id,
|
||
'process_uuid': process_uuid,
|
||
'reason': reason,
|
||
'start_time': start_time,
|
||
'end_time': end_time,
|
||
'duration': duration
|
||
})
|
||
|
||
finally:
|
||
if cursor:
|
||
cursor.close()
|
||
if connection:
|
||
close_database_connection(connection)
|
||
|
||
if not active_mute_records:
|
||
embed = discord.Embed(
|
||
title="❌ No Active Mutes Found",
|
||
description="No active mute records found for this user in the database.",
|
||
color=0xff0000
|
||
)
|
||
await send_response(embed=embed, ephemeral=True)
|
||
return
|
||
|
||
# Remove mute role
|
||
await member.remove_roles(mute_role, reason=f"Manually unmuted by {ctx.author}")
|
||
|
||
# Restore user roles
|
||
try:
|
||
restored_roles = await restore_user_roles(member, ctx.guild)
|
||
restored_count = len(restored_roles) if restored_roles else 0
|
||
except Exception as e:
|
||
logger.warning(f"Could not restore roles for user {user.id}: {e}")
|
||
restored_count = 0
|
||
|
||
# Deactivate all active mute records
|
||
deactivated_mutes = []
|
||
for mute_record in active_mute_records:
|
||
try:
|
||
await deactivate_mute(mute_record['id'], unmuted_by=ctx.author.id, auto_unmuted=False)
|
||
deactivated_mutes.append(mute_record)
|
||
except Exception as e:
|
||
logger.error(f"Error deactivating mute record {mute_record['id']}: {e}")
|
||
|
||
# Cancel active processes
|
||
cancelled_processes = []
|
||
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")
|
||
cancelled_processes.append(process["uuid"])
|
||
|
||
# Create success embed
|
||
embed = discord.Embed(
|
||
title="🔊 User Unmuted Successfully",
|
||
description=f"{user.mention} has been manually unmuted.",
|
||
color=0x00ff00,
|
||
timestamp=datetime.now()
|
||
)
|
||
|
||
embed.add_field(name="👮 Moderator", value=ctx.author.mention, inline=True)
|
||
embed.add_field(name="🔓 Mutes Deactivated", value=str(len(deactivated_mutes)), inline=True)
|
||
|
||
if restored_count > 0:
|
||
embed.add_field(name="🎭 Roles Restored", value=str(restored_count), inline=True)
|
||
|
||
# Show deactivated mute details
|
||
if deactivated_mutes:
|
||
mute_details = []
|
||
for mute in deactivated_mutes[:3]: # Show first 3 mutes
|
||
mute_info = f"**ID {mute['id']}**: {mute['reason'][:30]}{'...' if len(mute['reason']) > 30 else ''}"
|
||
mute_info += f" ({mute['duration']})"
|
||
mute_details.append(mute_info)
|
||
|
||
if len(deactivated_mutes) > 3:
|
||
mute_details.append(f"*+{len(deactivated_mutes) - 3} more mutes deactivated*")
|
||
|
||
embed.add_field(name="📋 Deactivated Mutes", value="\n".join(mute_details), inline=False)
|
||
|
||
embed.set_footer(text=f"User ID: {user.id} | {len(cancelled_processes)} process(es) cancelled")
|
||
embed.set_thumbnail(url=user.display_avatar.url)
|
||
|
||
await send_response(embed=embed)
|
||
|
||
# Log the action
|
||
log_additional_info = {
|
||
"Mutes Deactivated": str(len(deactivated_mutes)),
|
||
"Roles Restored": str(restored_count),
|
||
"Processes Cancelled": str(len(cancelled_processes))
|
||
}
|
||
|
||
await log_moderation_action(
|
||
guild=ctx.guild,
|
||
action_type="unmute",
|
||
moderator=ctx.author,
|
||
target_user=user,
|
||
reason="Manual unmute",
|
||
additional_info=log_additional_info
|
||
)
|
||
|
||
# Try to DM the user
|
||
try:
|
||
dm_embed = discord.Embed(
|
||
title="🔊 You have been unmuted",
|
||
description=f"Your mute in **{ctx.guild.name}** has been manually removed by a moderator.",
|
||
color=0x00ff00,
|
||
timestamp=datetime.now()
|
||
)
|
||
dm_embed.add_field(name="👮 Unmuted by", value=ctx.author.mention, inline=True)
|
||
dm_embed.set_footer(text=f"Server: {ctx.guild.name}")
|
||
await user.send(embed=dm_embed)
|
||
except (discord.Forbidden, discord.NotFound):
|
||
pass # User has DMs disabled or doesn't exist
|
||
|
||
logger.info(f"User {user.id} manually unmuted by {ctx.author.id} in guild {ctx.guild.id} - {len(deactivated_mutes)} mute(s) deactivated")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in unmute command: {e}")
|
||
embed = discord.Embed(
|
||
title="❌ Error",
|
||
description="An error occurred while unmuting the user.",
|
||
color=0xff0000
|
||
)
|
||
await send_response(embed=embed, ephemeral=True)
|
||
|
||
@client.hybrid_command()
|
||
async def listmutes(ctx, status: str = "active"):
|
||
"""List all mutes in the server (Requires Permission Level 5 or higher)
|
||
|
||
Parameters:
|
||
- status: Filter by status (active, completed, expired, all) - Default: active
|
||
"""
|
||
# Check if it's a slash command and defer if needed
|
||
is_slash_command = hasattr(ctx, 'interaction') and ctx.interaction
|
||
if is_slash_command:
|
||
await ctx.defer()
|
||
|
||
# Helper function for sending responses
|
||
async def send_response(content=None, embed=None, ephemeral=False, file=None):
|
||
try:
|
||
if is_slash_command:
|
||
if hasattr(ctx, 'followup') and ctx.followup:
|
||
await ctx.followup.send(content=content, embed=embed, ephemeral=ephemeral, file=file)
|
||
elif hasattr(ctx, 'response') and not ctx.response.is_done():
|
||
await ctx.response.send_message(content=content, embed=embed, ephemeral=ephemeral, file=file)
|
||
else:
|
||
await ctx.send(content=content, embed=embed, file=file)
|
||
else:
|
||
await ctx.send(content=content, embed=embed, file=file)
|
||
except Exception as e:
|
||
logger.error(f"Error sending response in listmutes command: {e}")
|
||
# Final fallback
|
||
try:
|
||
if embed:
|
||
await ctx.send(embed=embed)
|
||
elif content:
|
||
await ctx.send(content=content)
|
||
except Exception as fallback_error:
|
||
logger.error(f"Fallback send also failed: {fallback_error}")
|
||
|
||
try:
|
||
# Load moderator data
|
||
mod_data = await load_user_data(ctx.author.id, ctx.guild.id)
|
||
|
||
# Check moderation rights
|
||
if not check_moderation_permission(mod_data["permission"]):
|
||
embed = discord.Embed(
|
||
title="❌ Insufficient Permissions",
|
||
description="You need moderation permissions (Level 5 or higher) to use this command.",
|
||
color=0xff0000
|
||
)
|
||
await send_response(embed=embed, ephemeral=True)
|
||
return
|
||
|
||
# Validate status parameter
|
||
valid_statuses = ["active", "completed", "expired", "cancelled", "all"]
|
||
if status.lower() not in valid_statuses:
|
||
embed = discord.Embed(
|
||
title="❌ Invalid Status",
|
||
description=f"Invalid status `{status}`. Valid options: {', '.join(valid_statuses)}",
|
||
color=0xff0000
|
||
)
|
||
await send_response(embed=embed, ephemeral=True)
|
||
return
|
||
|
||
# Get mutes from user_mutes database
|
||
connection = None
|
||
cursor = None
|
||
try:
|
||
connection = connect_to_database()
|
||
cursor = connection.cursor()
|
||
|
||
if status.lower() == "all":
|
||
select_query = """
|
||
SELECT id, process_uuid, user_id, created_at, end_time, status, reason,
|
||
duration, aktiv, moderator_id, auto_unmuted, unmuted_at
|
||
FROM user_mutes
|
||
WHERE guild_id = %s
|
||
ORDER BY created_at DESC
|
||
LIMIT 50
|
||
"""
|
||
cursor.execute(select_query, (ctx.guild.id,))
|
||
else:
|
||
# Map status filters
|
||
if status.lower() == "active":
|
||
where_condition = "aktiv = 1 AND status = 'active'"
|
||
elif status.lower() == "completed":
|
||
where_condition = "aktiv = 0 OR status = 'completed'"
|
||
elif status.lower() == "expired":
|
||
where_condition = "status = 'expired' OR (end_time < NOW() AND aktiv = 0)"
|
||
elif status.lower() == "cancelled":
|
||
where_condition = "status = 'cancelled'"
|
||
|
||
select_query = f"""
|
||
SELECT id, process_uuid, user_id, created_at, end_time, status, reason,
|
||
duration, aktiv, moderator_id, auto_unmuted, unmuted_at
|
||
FROM user_mutes
|
||
WHERE guild_id = %s AND ({where_condition})
|
||
ORDER BY created_at DESC
|
||
LIMIT 50
|
||
"""
|
||
cursor.execute(select_query, (ctx.guild.id,))
|
||
|
||
results = cursor.fetchall()
|
||
|
||
if not results:
|
||
embed = discord.Embed(
|
||
title="📋 No Mutes Found",
|
||
description=f"No mutes with status `{status}` found in this server.",
|
||
color=0x3498db
|
||
)
|
||
await send_response(embed=embed)
|
||
return
|
||
|
||
# Create embed with mute list
|
||
embed = discord.Embed(
|
||
title=f"🔇 Server Mutes - {status.title()}",
|
||
description=f"Found {len(results)} mute(s) in {ctx.guild.name}",
|
||
color=0xff0000,
|
||
timestamp=datetime.now()
|
||
)
|
||
|
||
mute_list = []
|
||
for result in results[:15]: # Limit to 15 to avoid embed limits
|
||
mute_id, process_uuid, user_id, created_at, end_time, mute_status, reason, duration, aktiv, moderator_id, auto_unmuted, unmuted_at = result
|
||
|
||
try:
|
||
# Get user
|
||
user = ctx.guild.get_member(int(user_id))
|
||
user_display = user.display_name if user else f"Unknown User ({user_id})"
|
||
|
||
# Format reason
|
||
reason_display = reason[:40] + ("..." if len(reason) > 40 else "") if reason else "No reason"
|
||
|
||
# Status emoji based on aktiv status and mute_status
|
||
if aktiv and mute_status == "active":
|
||
status_emoji = "🟢"
|
||
display_status = "Active"
|
||
elif not aktiv and auto_unmuted:
|
||
status_emoji = "⏰"
|
||
display_status = "Auto-Expired"
|
||
elif not aktiv and unmuted_at:
|
||
status_emoji = "✅"
|
||
display_status = "Manually Unmuted"
|
||
elif mute_status == "cancelled":
|
||
status_emoji = "❌"
|
||
display_status = "Cancelled"
|
||
else:
|
||
status_emoji = "❓"
|
||
display_status = mute_status.title()
|
||
|
||
# Time info
|
||
if aktiv and mute_status == "active" and end_time:
|
||
time_info = f"ends <t:{int(end_time.timestamp())}:R>"
|
||
elif unmuted_at:
|
||
time_info = f"unmuted <t:{int(unmuted_at.timestamp())}:d>"
|
||
else:
|
||
time_info = f"<t:{int(created_at.timestamp())}:d>"
|
||
|
||
mute_entry = f"{status_emoji} **{user_display}** - {reason_display}\n`ID: {mute_id}` • {duration} • {time_info}"
|
||
mute_list.append(mute_entry)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error processing mute entry: {e}")
|
||
continue
|
||
|
||
if mute_list:
|
||
mute_text = "\n\n".join(mute_list)
|
||
embed.add_field(name="📋 Mutes", value=mute_text, inline=False)
|
||
|
||
embed.set_footer(text=f"Use /viewmute <id> for detailed info • Showing max 15 results")
|
||
|
||
await send_response(embed=embed)
|
||
|
||
finally:
|
||
if cursor:
|
||
cursor.close()
|
||
if connection:
|
||
close_database_connection(connection)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in listmutes command: {e}")
|
||
embed = discord.Embed(
|
||
title="❌ Error",
|
||
description="An error occurred while retrieving mute list. Please try again.",
|
||
color=0xff0000
|
||
)
|
||
await send_response(embed=embed)
|
||
|
||
@client.hybrid_command()
|
||
async def modstats(ctx, user: discord.User = None):
|
||
"""Zeigt Moderationsstatistiken für einen Benutzer an"""
|
||
try:
|
||
# Falls kein User angegeben, zeige eigene Stats
|
||
target_user = user or ctx.author
|
||
|
||
# Lade User-Daten
|
||
user_data = await load_user_data(target_user.id, ctx.guild.id)
|
||
|
||
# Erstelle Embed
|
||
embed = discord.Embed(
|
||
title=f"📊 Moderationsstatistiken",
|
||
description=f"Statistiken für {target_user.mention}",
|
||
color=0x3498db,
|
||
timestamp=datetime.now()
|
||
)
|
||
|
||
embed.add_field(name="🤖 AI Bans", value=user_data.get("ai_ban", 0), inline=True)
|
||
embed.add_field(name="🔇 Mutes", value=user_data.get("mutes", 0), inline=True)
|
||
embed.add_field(name="⚠️ Warnungen", value=user_data.get("warns", 0), inline=True)
|
||
embed.add_field(name="🛡️ Permission Level", value=user_data.get("permission", 0), inline=True)
|
||
embed.add_field(name="⭐ Level", value=user_data.get("level", 1), inline=True)
|
||
embed.add_field(name="💰 Punkte", value=user_data.get("points", 0), inline=True)
|
||
|
||
embed.set_thumbnail(url=target_user.display_avatar.url)
|
||
embed.set_footer(text=f"User ID: {target_user.id}")
|
||
|
||
await ctx.send(embed=embed)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in modstats command: {e}")
|
||
await ctx.send("❌ Ein Fehler ist aufgetreten beim Laden der Moderationsstatistiken.")
|
||
|
||
@client.hybrid_command()
|
||
async def modconfig(ctx, setting: str = None, *, value: str = None):
|
||
"""Konfiguriert Moderationseinstellungen für den Server (Benötigt Permission Level 8 oder höher)
|
||
|
||
Verfügbare Einstellungen:
|
||
- mute_role <@role/role_name> - Setzt die Mute-Rolle
|
||
- mute_role_name <name> - Setzt den Namen für auto-erstellte Mute-Rollen
|
||
- auto_create_mute_role <true/false> - Auto-Erstellung von Mute-Rollen
|
||
- max_warn_threshold <number> - Anzahl Warnungen vor Auto-Aktion
|
||
- auto_mute_on_warns <true/false> - Auto-Mute bei zu vielen Warnungen
|
||
- auto_mute_duration <duration> - Dauer für Auto-Mutes (z.B. 1h, 30m)
|
||
- log_channel <#channel> - Kanal für Moderations-Logs
|
||
- mod_log_enabled <true/false> - Aktiviert/Deaktiviert Moderations-Logs
|
||
"""
|
||
try:
|
||
# Lade Moderator-Daten
|
||
mod_data = await load_user_data(ctx.author.id, ctx.guild.id)
|
||
|
||
# Überprüfe Admin-Rechte (Level 8+)
|
||
if mod_data["permission"] < 8:
|
||
await ctx.send("❌ Du hast keine Berechtigung, Moderationseinstellungen zu ändern. (Benötigt Permission Level 8 oder höher)")
|
||
return
|
||
|
||
# Lade aktuelle Einstellungen
|
||
guild_settings = get_guild_settings(ctx.guild.id)
|
||
|
||
# Zeige Einstellungen an, falls keine Parameter
|
||
if not setting:
|
||
embed = discord.Embed(
|
||
title="🛠️ Moderationseinstellungen",
|
||
description=f"Aktuelle Einstellungen für **{ctx.guild.name}**",
|
||
color=0x3498db,
|
||
timestamp=datetime.now()
|
||
)
|
||
|
||
# Mute-Rolle Info
|
||
mute_role_info = "Nicht gesetzt"
|
||
if guild_settings["mute_role_id"]:
|
||
role = ctx.guild.get_role(guild_settings["mute_role_id"])
|
||
mute_role_info = role.mention if role else f"❌ Rolle nicht gefunden (ID: {guild_settings['mute_role_id']})"
|
||
|
||
embed.add_field(name="🔇 Mute-Rolle", value=mute_role_info, inline=False)
|
||
embed.add_field(name="📝 Mute-Rollen-Name", value=guild_settings["mute_role_name"], inline=True)
|
||
embed.add_field(name="🔧 Auto-Erstellen", value="✅" if guild_settings["auto_create_mute_role"] else "❌", inline=True)
|
||
embed.add_field(name="⚠️ Warn-Limit", value=guild_settings["max_warn_threshold"], inline=True)
|
||
embed.add_field(name="🔄 Auto-Mute bei Warns", value="✅" if guild_settings["auto_mute_on_warns"] else "❌", inline=True)
|
||
embed.add_field(name="⏱️ Auto-Mute-Dauer", value=guild_settings["auto_mute_duration"], inline=True)
|
||
|
||
# Log-Kanal Info
|
||
log_info = "Nicht gesetzt"
|
||
if guild_settings["log_channel_id"]:
|
||
channel = ctx.guild.get_channel(guild_settings["log_channel_id"])
|
||
log_info = channel.mention if channel else f"❌ Kanal nicht gefunden (ID: {guild_settings['log_channel_id']})"
|
||
|
||
embed.add_field(name="📊 Log-Kanal", value=log_info, inline=True)
|
||
embed.add_field(name="📝 Logs Aktiviert", value="✅" if guild_settings["mod_log_enabled"] else "❌", inline=True)
|
||
|
||
embed.set_footer(text="Verwende -modconfig <setting> <value> zum Ändern")
|
||
await ctx.send(embed=embed)
|
||
return
|
||
|
||
# Ändere Einstellungen
|
||
setting = setting.lower()
|
||
|
||
if setting == "mute_role":
|
||
if not value:
|
||
await ctx.send("❌ Bitte gib eine Rolle an: `-modconfig mute_role @MuteRole`")
|
||
return
|
||
|
||
# Parse Rolle
|
||
role = None
|
||
if value.startswith("<@&") and value.endswith(">"):
|
||
role_id = int(value[3:-1])
|
||
role = ctx.guild.get_role(role_id)
|
||
else:
|
||
role = discord.utils.get(ctx.guild.roles, name=value)
|
||
|
||
if not role:
|
||
await ctx.send("❌ Rolle nicht gefunden.")
|
||
return
|
||
|
||
guild_settings["mute_role_id"] = role.id
|
||
guild_settings["mute_role_name"] = role.name
|
||
save_guild_settings(ctx.guild.id, guild_settings)
|
||
|
||
await ctx.send(f"✅ Mute-Rolle auf {role.mention} gesetzt.")
|
||
|
||
elif setting == "mute_role_name":
|
||
if not value:
|
||
await ctx.send("❌ Bitte gib einen Namen an: `-modconfig mute_role_name Stumm`")
|
||
return
|
||
|
||
guild_settings["mute_role_name"] = value
|
||
save_guild_settings(ctx.guild.id, guild_settings)
|
||
|
||
await ctx.send(f"✅ Mute-Rollen-Name auf `{value}` gesetzt.")
|
||
|
||
elif setting == "auto_create_mute_role":
|
||
if value.lower() in ["true", "1", "ja", "yes", "on"]:
|
||
guild_settings["auto_create_mute_role"] = True
|
||
await ctx.send("✅ Auto-Erstellung von Mute-Rollen aktiviert.")
|
||
elif value.lower() in ["false", "0", "nein", "no", "off"]:
|
||
guild_settings["auto_create_mute_role"] = False
|
||
await ctx.send("✅ Auto-Erstellung von Mute-Rollen deaktiviert.")
|
||
else:
|
||
await ctx.send("❌ Ungültiger Wert. Verwende: true/false")
|
||
return
|
||
|
||
save_guild_settings(ctx.guild.id, guild_settings)
|
||
|
||
elif setting == "max_warn_threshold":
|
||
try:
|
||
threshold = int(value)
|
||
if threshold < 1 or threshold > 10:
|
||
await ctx.send("❌ Warn-Limit muss zwischen 1 und 10 liegen.")
|
||
return
|
||
|
||
guild_settings["max_warn_threshold"] = threshold
|
||
save_guild_settings(ctx.guild.id, guild_settings)
|
||
|
||
await ctx.send(f"✅ Warn-Limit auf {threshold} gesetzt.")
|
||
|
||
except ValueError:
|
||
await ctx.send("❌ Ungültiger Wert. Verwende eine Nummer zwischen 1 und 10.")
|
||
return
|
||
|
||
elif setting == "auto_mute_on_warns":
|
||
if value.lower() in ["true", "1", "ja", "yes", "on"]:
|
||
guild_settings["auto_mute_on_warns"] = True
|
||
await ctx.send("✅ Auto-Mute bei zu vielen Warnungen aktiviert.")
|
||
elif value.lower() in ["false", "0", "nein", "no", "off"]:
|
||
guild_settings["auto_mute_on_warns"] = False
|
||
await ctx.send("✅ Auto-Mute bei zu vielen Warnungen deaktiviert.")
|
||
else:
|
||
await ctx.send("❌ Ungültiger Wert. Verwende: true/false")
|
||
return
|
||
|
||
save_guild_settings(ctx.guild.id, guild_settings)
|
||
|
||
elif setting == "auto_mute_duration":
|
||
# Validiere Dauer-Format
|
||
time_units = {'m': 60, 'h': 3600, 'd': 86400}
|
||
if not value or not value[-1] in time_units or not value[:-1].isdigit():
|
||
await ctx.send("❌ Ungültiges Zeitformat. Verwende: 10m, 1h, 2d")
|
||
return
|
||
|
||
guild_settings["auto_mute_duration"] = value
|
||
save_guild_settings(ctx.guild.id, guild_settings)
|
||
|
||
await ctx.send(f"✅ Auto-Mute-Dauer auf {value} gesetzt.")
|
||
|
||
elif setting == "log_channel":
|
||
if not value:
|
||
await ctx.send("❌ Bitte gib einen Kanal an: `-modconfig log_channel #mod-log`")
|
||
return
|
||
|
||
# Parse Kanal
|
||
channel = None
|
||
if value.startswith("<#") and value.endswith(">"):
|
||
channel_id = int(value[2:-1])
|
||
channel = ctx.guild.get_channel(channel_id)
|
||
else:
|
||
channel = discord.utils.get(ctx.guild.channels, name=value.replace("#", ""))
|
||
|
||
if not channel:
|
||
await ctx.send("❌ Kanal nicht gefunden.")
|
||
return
|
||
|
||
guild_settings["log_channel_id"] = channel.id
|
||
save_guild_settings(ctx.guild.id, guild_settings)
|
||
|
||
await ctx.send(f"✅ Log-Kanal auf {channel.mention} gesetzt.")
|
||
|
||
elif setting == "mod_log_enabled":
|
||
if value.lower() in ["true", "1", "ja", "yes", "on"]:
|
||
guild_settings["mod_log_enabled"] = True
|
||
await ctx.send("✅ Moderations-Logs aktiviert.")
|
||
elif value.lower() in ["false", "0", "nein", "no", "off"]:
|
||
guild_settings["mod_log_enabled"] = False
|
||
await ctx.send("✅ Moderations-Logs deaktiviert.")
|
||
else:
|
||
await ctx.send("❌ Ungültiger Wert. Verwende: true/false")
|
||
return
|
||
|
||
save_guild_settings(ctx.guild.id, guild_settings)
|
||
|
||
else:
|
||
await ctx.send("❌ Unbekannte Einstellung. Verwende `-modconfig` ohne Parameter für eine Liste aller Einstellungen.")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in modconfig command: {e}")
|
||
await ctx.send("❌ Ein Fehler ist aufgetreten beim Konfigurieren der Moderationseinstellungen.")
|
||
|
||
# Cache-Ordner für Notizen
|
||
CACHE_DIR = "cache"
|
||
if not os.path.exists(CACHE_DIR):
|
||
os.makedirs(CACHE_DIR)
|
||
|
||
# Profilbild-Ordner erstellen
|
||
PROFILE_IMAGES_DIR = "static/profile_images"
|
||
if not os.path.exists(PROFILE_IMAGES_DIR):
|
||
os.makedirs(PROFILE_IMAGES_DIR)
|
||
|
||
def get_url_hash(url):
|
||
"""Erstellt einen Hash aus der URL für Vergleichszwecke"""
|
||
if not url:
|
||
return None
|
||
return hashlib.md5(url.encode('utf-8')).hexdigest()
|
||
|
||
def get_local_profile_path(user_id):
|
||
"""Gibt den lokalen Pfad für das Profilbild eines Users zurück"""
|
||
return os.path.join(PROFILE_IMAGES_DIR, f"user_{user_id}.png")
|
||
|
||
def get_web_profile_path(user_id):
|
||
"""Gibt den Web-Pfad für das Profilbild eines Users zurück"""
|
||
return f"/static/profile_images/user_{user_id}.png"
|
||
|
||
async def download_and_save_profile_image(user_id, discord_url):
|
||
"""Lädt ein Profilbild herunter und speichert es lokal"""
|
||
if not discord_url:
|
||
return "/static/default_profile.png"
|
||
|
||
try:
|
||
local_path = get_local_profile_path(user_id)
|
||
web_path = get_web_profile_path(user_id)
|
||
|
||
# Überprüfe, ob das Bild bereits existiert und der Hash gleich ist
|
||
hash_file = local_path + ".hash"
|
||
current_hash = get_url_hash(discord_url)
|
||
|
||
if os.path.exists(local_path) and os.path.exists(hash_file):
|
||
with open(hash_file, 'r') as f:
|
||
stored_hash = f.read().strip()
|
||
|
||
if stored_hash == current_hash:
|
||
logger.info(f"Profile image for user {user_id} is up to date, skipping download")
|
||
return web_path
|
||
|
||
# Download das Bild
|
||
logger.info(f"Downloading profile image for user {user_id} from {discord_url}")
|
||
|
||
# Use a session with timeout for better connection handling
|
||
session = requests.Session()
|
||
session.timeout = (10, 15) # (connection timeout, read timeout)
|
||
|
||
response = session.get(discord_url, timeout=15)
|
||
|
||
if response.status_code == 200:
|
||
# Speichere das Bild
|
||
with open(local_path, 'wb') as f:
|
||
f.write(response.content)
|
||
|
||
# Speichere den Hash
|
||
with open(hash_file, 'w') as f:
|
||
f.write(current_hash)
|
||
|
||
logger.info(f"Successfully downloaded and saved profile image for user {user_id}")
|
||
return web_path
|
||
else:
|
||
logger.warning(f"Failed to download profile image for user {user_id}: HTTP {response.status_code}")
|
||
return "/static/default_profile.png"
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error downloading profile image for user {user_id}: {e}")
|
||
return "/static/default_profile.png"
|
||
|
||
# Cache-Ordner für Notizen
|
||
|
||
@client.hybrid_command()
|
||
async def addnotes(ctx, type: str, source: str = None, attachment: discord.Attachment = None):
|
||
"""Adds a note that can be consulted later.
|
||
|
||
For text files: /addnotes txt [attach file]
|
||
For websites: /addnotes url https://example.com
|
||
"""
|
||
# Check if it's a slash command and defer if needed
|
||
is_slash_command = hasattr(ctx, 'interaction') and ctx.interaction
|
||
if is_slash_command:
|
||
await ctx.defer()
|
||
|
||
# Helper function for sending responses
|
||
async def send_response(content=None, embed=None, ephemeral=False):
|
||
try:
|
||
if is_slash_command:
|
||
if hasattr(ctx, 'followup') and ctx.followup:
|
||
if embed:
|
||
await ctx.followup.send(embed=embed, ephemeral=ephemeral)
|
||
else:
|
||
await ctx.followup.send(content, ephemeral=ephemeral)
|
||
elif hasattr(ctx, 'response') and not ctx.response.is_done():
|
||
if embed:
|
||
await ctx.response.send_message(embed=embed, ephemeral=ephemeral)
|
||
else:
|
||
await ctx.response.send_message(content, ephemeral=ephemeral)
|
||
else:
|
||
if embed:
|
||
await ctx.send(embed=embed)
|
||
else:
|
||
await ctx.send(content)
|
||
else:
|
||
if embed:
|
||
await ctx.send(embed=embed)
|
||
else:
|
||
await ctx.send(content)
|
||
except Exception as e:
|
||
logger.error(f"Error sending response in addnotes command: {e}")
|
||
# Final fallback
|
||
try:
|
||
if embed:
|
||
await ctx.send(embed=embed)
|
||
elif content:
|
||
await ctx.send(content)
|
||
except Exception as fallback_error:
|
||
logger.error(f"Fallback send also failed: {fallback_error}")
|
||
# Fallback to regular send if followup fails
|
||
try:
|
||
if embed:
|
||
await ctx.send(embed=embed)
|
||
else:
|
||
await ctx.send(content)
|
||
except:
|
||
pass
|
||
|
||
user_id = ctx.author.id
|
||
guild_id = ctx.guild.id
|
||
user_cache_dir = os.path.join(CACHE_DIR, f"{str(guild_id)}_{str(user_id)}")
|
||
|
||
if not os.path.exists(user_cache_dir):
|
||
os.makedirs(user_cache_dir)
|
||
|
||
note_file = os.path.join(user_cache_dir, "notes.txt")
|
||
|
||
if type.lower() == "txt":
|
||
# Check for attachment parameter first (Slash Command)
|
||
if attachment:
|
||
if attachment.content_type and attachment.content_type.startswith('text/'):
|
||
try:
|
||
content = await attachment.read()
|
||
text_content = content.decode('utf-8')
|
||
|
||
with open(note_file, "a", encoding="utf-8") as file:
|
||
file.write(f"\n--- From file: {attachment.filename} ---\n")
|
||
file.write(text_content + "\n")
|
||
|
||
await send_response(f"✅ Text file `{attachment.filename}` added as notes for user {ctx.author.name}.")
|
||
except UnicodeDecodeError:
|
||
await send_response("❌ Error: File is not a valid text file.")
|
||
except Exception as e:
|
||
await send_response(f"❌ Error reading file: {e}")
|
||
else:
|
||
await send_response("❌ Please attach a text file (.txt, .md, etc.)")
|
||
# Fallback for prefix commands with message attachments
|
||
elif hasattr(ctx, 'message') and ctx.message and ctx.message.attachments:
|
||
attachment = ctx.message.attachments[0]
|
||
try:
|
||
content = await attachment.read()
|
||
text_content = content.decode('utf-8')
|
||
|
||
with open(note_file, "a", encoding="utf-8") as file:
|
||
file.write(f"\n--- From file: {attachment.filename} ---\n")
|
||
file.write(text_content + "\n")
|
||
|
||
await send_response(f"✅ Text file `{attachment.filename}` added as notes for user {ctx.author.name}.")
|
||
except UnicodeDecodeError:
|
||
await send_response("❌ Error: File is not a valid text file.")
|
||
except Exception as e:
|
||
await send_response(f"❌ Error reading file: {e}")
|
||
else:
|
||
await send_response("❌ No text file attached. Please use the attachment parameter for Slash Commands.")
|
||
|
||
elif type.lower() == "url":
|
||
if not source:
|
||
await send_response("❌ Please provide a URL: `/addnotes url https://example.com`")
|
||
return
|
||
|
||
try:
|
||
response = requests.get(source)
|
||
if response.status_code == 200:
|
||
# HTML-Parsen und nur Text extrahieren
|
||
soup = BeautifulSoup(response.text, 'html.parser')
|
||
|
||
# Entfernen von Header- und Footer-Elementen
|
||
for element in soup(['header', 'footer', 'nav', 'aside']):
|
||
element.decompose()
|
||
|
||
text = soup.get_text()
|
||
|
||
# Entfernen von überflüssigen Leerzeilen
|
||
cleaned_text = "\n".join([line.strip() for line in text.splitlines() if line.strip()])
|
||
|
||
with open(note_file, "a", encoding="utf-8") as file:
|
||
file.write(f"\n--- From URL: {source} ---\n")
|
||
file.write(cleaned_text + "\n")
|
||
await send_response(f"✅ Website content from `{source}` added as notes for user {ctx.author.name}.")
|
||
else:
|
||
await send_response(f"❌ Failed to retrieve the website from {source}. HTTP {response.status_code}")
|
||
except Exception as e:
|
||
await send_response(f"❌ Error fetching website: {e}")
|
||
else:
|
||
await send_response("❌ Invalid type. Use 'txt' for text files or 'url' for website URLs.")
|
||
|
||
@client.hybrid_command()
|
||
async def asknotes(ctx, *, question: str):
|
||
"""Asks a question about your saved notes."""
|
||
# Check if it's a slash command and defer if needed
|
||
is_slash_command = hasattr(ctx, 'interaction') and ctx.interaction
|
||
if is_slash_command:
|
||
await ctx.defer()
|
||
|
||
# Helper function for sending responses
|
||
async def send_response(content=None, embed=None, ephemeral=False):
|
||
try:
|
||
if is_slash_command:
|
||
if embed:
|
||
await ctx.followup.send(embed=embed, ephemeral=ephemeral)
|
||
else:
|
||
await ctx.followup.send(content, ephemeral=ephemeral)
|
||
else:
|
||
if embed:
|
||
await ctx.send(embed=embed)
|
||
else:
|
||
await ctx.send(content)
|
||
except Exception as e:
|
||
logger.error(f"Error sending response: {e}")
|
||
# Fallback to regular send if followup fails
|
||
try:
|
||
if embed:
|
||
await ctx.send(embed=embed)
|
||
else:
|
||
await ctx.send(content)
|
||
except:
|
||
pass
|
||
|
||
user_id = ctx.author.id
|
||
guild_id = ctx.guild.id
|
||
user_cache_dir = os.path.join(CACHE_DIR, f"{str(guild_id)}_{str(user_id)}")
|
||
note_file = os.path.join(user_cache_dir, "notes.txt")
|
||
asknotesintroduction = read_askintroduction()
|
||
|
||
if not os.path.exists(note_file):
|
||
await send_response(f"No notes found for user {ctx.author.name}.")
|
||
return
|
||
|
||
with open(note_file, "r", encoding="utf-8") as file:
|
||
notes = file.read()
|
||
|
||
# Define the full data and user history field for asknotes
|
||
full_data = asknotesintroduction
|
||
user_history_field = "asknotes_history"
|
||
|
||
# Füge die Anfrage zur Warteschlange hinzu
|
||
await askmultus_queue.put((ctx, user_id, ctx.author.name, question, ctx.channel.id, full_data, user_history_field, "text-davinci-003"))
|
||
|
||
# Erstelle ein Embed für die Bestätigungsnachricht
|
||
embed = discord.Embed(title="Notes Query", color=0x00ff00)
|
||
embed.add_field(name="Request Received", value="Your request has been added to the queue. Processing it now...")
|
||
await send_response(embed=embed)
|
||
|
||
@client.hybrid_command()
|
||
async def delnotes(ctx):
|
||
"""Deletes all saved notes and the asknotes history for the user."""
|
||
# Check if it's a slash command and defer if needed
|
||
is_slash_command = hasattr(ctx, 'interaction') and ctx.interaction
|
||
|
||
# Helper function for sending responses
|
||
async def send_response(content=None, embed=None, ephemeral=False):
|
||
try:
|
||
if is_slash_command:
|
||
if embed:
|
||
await ctx.followup.send(embed=embed, ephemeral=ephemeral)
|
||
else:
|
||
await ctx.followup.send(content, ephemeral=ephemeral)
|
||
else:
|
||
if embed:
|
||
await ctx.send(embed=embed)
|
||
else:
|
||
await ctx.send(content)
|
||
except Exception as e:
|
||
logger.error(f"Error sending response: {e}")
|
||
# Fallback to regular send if followup fails
|
||
try:
|
||
if embed:
|
||
await ctx.send(embed=embed)
|
||
else:
|
||
await ctx.send(content)
|
||
except:
|
||
pass
|
||
|
||
user_id = ctx.author.id
|
||
guild_id = ctx.guild.id
|
||
user_cache_dir = os.path.join(CACHE_DIR, f"{str(guild_id)}_{str(user_id)}")
|
||
|
||
if os.path.exists(user_cache_dir):
|
||
# Lösche die gespeicherten Notizen im Cache-Ordner
|
||
shutil.rmtree(user_cache_dir)
|
||
|
||
# Setze die asknotes-Historie in der Datenbank zurück
|
||
try:
|
||
update_user_data(user_id, guild_id, "asknotes_history", None)
|
||
await send_response(f"All notes and asknotes history deleted for user {ctx.author.name}.")
|
||
except Exception as e:
|
||
await send_response(f"Error deleting asknotes history: {e}")
|
||
else:
|
||
await send_response(f"No notes found for user {ctx.author.name}.")
|
||
|
||
@client.command()
|
||
async def process_contact_messages(ctx):
|
||
"""Prozessiert ausstehende Kontaktnachrichten (Admin only)"""
|
||
if ctx.author.id != 253922739709018114: # Ihre Discord ID
|
||
await ctx.send("You don't have permission to use this command.")
|
||
return
|
||
|
||
try:
|
||
connection = connect_to_database()
|
||
cursor = connection.cursor()
|
||
|
||
# Hole alle pending Nachrichten
|
||
cursor.execute("""
|
||
SELECT id, user_id, username, subject, category, priority, message,
|
||
server_context, submitted_at
|
||
FROM contact_messages
|
||
WHERE status = 'pending'
|
||
ORDER BY submitted_at ASC
|
||
""")
|
||
|
||
pending_messages = cursor.fetchall()
|
||
|
||
if not pending_messages:
|
||
await ctx.send("No pending contact messages found.")
|
||
cursor.close()
|
||
close_database_connection(connection)
|
||
return
|
||
|
||
processed_count = 0
|
||
for msg in pending_messages:
|
||
msg_id, user_id, username, subject, category, priority, message, server_context, submitted_at = msg
|
||
|
||
try:
|
||
# Hole User-Informationen von Discord
|
||
user = client.get_user(int(user_id))
|
||
if not user:
|
||
user = await client.fetch_user(int(user_id))
|
||
|
||
message_data = {
|
||
'user_id': user_id,
|
||
'user_name': user.display_name if user else username.split('#')[0],
|
||
'username': username,
|
||
'avatar_url': user.avatar.url if user and user.avatar else None,
|
||
'subject': subject,
|
||
'category': category,
|
||
'priority': priority,
|
||
'message': message,
|
||
'server_context': server_context
|
||
}
|
||
|
||
# Sende an Admin
|
||
success = await send_contact_message_to_admin(message_data)
|
||
|
||
if success:
|
||
# Markiere als versendet
|
||
cursor.execute("""
|
||
UPDATE contact_messages
|
||
SET status = 'sent', responded_at = %s
|
||
WHERE id = %s
|
||
""", (int(time.time()), msg_id))
|
||
processed_count += 1
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error processing contact message {msg_id}: {e}")
|
||
continue
|
||
|
||
connection.commit()
|
||
cursor.close()
|
||
close_database_connection(connection)
|
||
|
||
await ctx.send(f"Processed {processed_count}/{len(pending_messages)} contact messages successfully.")
|
||
|
||
except Exception as e:
|
||
await ctx.send(f"Error processing contact messages: {e}")
|
||
logger.error(f"Error in process_contact_messages: {e}")
|
||
|
||
@client.command()
|
||
async def contact_status(ctx):
|
||
"""Zeigt den Status der Kontaktnachrichten-Überwachung (Admin only)"""
|
||
if ctx.author.id != 253922739709018114: # Ihre Discord ID
|
||
await ctx.send("You don't have permission to use this command.")
|
||
return
|
||
|
||
try:
|
||
connection = connect_to_database()
|
||
cursor = connection.cursor()
|
||
|
||
# Statistiken abrufen
|
||
cursor.execute("SELECT COUNT(*) FROM contact_messages WHERE status = 'pending'")
|
||
pending_count = cursor.fetchone()[0]
|
||
|
||
cursor.execute("SELECT COUNT(*) FROM contact_messages WHERE status = 'sent'")
|
||
sent_count = cursor.fetchone()[0]
|
||
|
||
cursor.execute("SELECT COUNT(*) FROM contact_messages")
|
||
total_count = cursor.fetchone()[0]
|
||
|
||
# Letzte Nachricht info
|
||
cursor.execute("""
|
||
SELECT subject, submitted_at FROM contact_messages
|
||
ORDER BY submitted_at DESC LIMIT 1
|
||
""")
|
||
last_message = cursor.fetchone()
|
||
|
||
cursor.close()
|
||
close_database_connection(connection)
|
||
|
||
# Status des Task Loops
|
||
task_status = "🟢 Running" if check_contact_messages.is_running() else "🔴 Stopped"
|
||
|
||
# Nächste Ausführung berechnen
|
||
if check_contact_messages.is_running():
|
||
next_run = check_contact_messages.next_iteration
|
||
if next_run:
|
||
next_run_str = f"<t:{int(next_run.timestamp())}:R>"
|
||
else:
|
||
next_run_str = "Soon"
|
||
else:
|
||
next_run_str = "Task not running"
|
||
|
||
embed = discord.Embed(
|
||
title="📊 Contact Messages Status",
|
||
color=0x667eea,
|
||
timestamp=datetime.utcnow()
|
||
)
|
||
|
||
embed.add_field(
|
||
name="📋 Message Statistics",
|
||
value=f"**Pending:** {pending_count}\n"
|
||
f"**Processed:** {sent_count}\n"
|
||
f"**Total:** {total_count}",
|
||
inline=True
|
||
)
|
||
|
||
embed.add_field(
|
||
name="⏰ Task Status",
|
||
value=f"**Status:** {task_status}\n"
|
||
f"**Interval:** Every 15 minutes\n"
|
||
f"**Next Check:** {next_run_str}",
|
||
inline=True
|
||
)
|
||
|
||
if last_message:
|
||
last_subject = last_message[0][:50] + "..." if len(last_message[0]) > 50 else last_message[0]
|
||
embed.add_field(
|
||
name="📝 Latest Message",
|
||
value=f"**Subject:** {last_subject}\n"
|
||
f"**Submitted:** <t:{int(last_message[1])}:R>",
|
||
inline=False
|
||
)
|
||
|
||
embed.set_footer(text="Contact Messages Auto-Check System")
|
||
|
||
await ctx.send(embed=embed)
|
||
|
||
except Exception as e:
|
||
await ctx.send(f"Error getting contact status: {e}")
|
||
logger.error(f"Error in contact_status: {e}")
|
||
|
||
try:
|
||
# Initialize database tables
|
||
create_warnings_table()
|
||
create_mutes_table()
|
||
create_contact_messages_table()
|
||
logger.info("Database tables initialized successfully")
|
||
|
||
loop.run_until_complete(client.start(TOKEN))
|
||
except KeyboardInterrupt:
|
||
loop.run_until_complete(client.logout())
|
||
finally:
|
||
loop.close()
|