modified: bot.py

This commit is contained in:
SimolZimol
2025-08-18 14:25:35 +02:00
parent a6b9459a0c
commit 50d148c094

560
bot.py
View File

@@ -1,4 +1,4 @@
__version__ = "dev-0.9.4"
__version__ = "dev-0.9.5"
__all__ = ["Discordbot-chatai (Discord)"]
__author__ = "SimolZimol"
@@ -12,8 +12,12 @@ import requests
import asyncio
import base64
import mysql.connector
import mysql.connector.pooling
import json
import logging
import time
import random
import hashlib
from datetime import datetime, timedelta
import concurrent.futures
from gtts import gTTS
@@ -466,6 +470,362 @@ def save_global_permission(user_id, permission_level):
#-----------------------------------------------------------------------------------------------------------
# Active Processes System - Robust system for storing and managing active processes
def create_active_process(process_type, guild_id, channel_id=None, user_id=None, target_id=None,
start_time=None, end_time=None, status="active", data=None, metadata=None):
"""Creates a new active process entry in the database"""
connection = None
cursor = None
try:
connection = connect_to_database()
cursor = connection.cursor()
process_uuid = uuid.uuid4()
insert_query = """
INSERT INTO active_processes (uuid, process_type, guild_id, channel_id, user_id, target_id,
start_time, end_time, status, data, metadata, created_at)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
"""
current_time = datetime.now()
start_time = start_time or current_time
# Serialize data and metadata as JSON
data_json = json.dumps(data) if data else None
metadata_json = json.dumps(metadata) if metadata else None
cursor.execute(insert_query, (
str(process_uuid), process_type, guild_id, channel_id, user_id, target_id,
start_time, end_time, status, data_json, metadata_json, current_time
))
connection.commit()
logger.info(f"Created active process: {process_type} with UUID: {process_uuid}")
return process_uuid
except Exception as e:
logger.error(f"Error creating active process: {e}")
if connection:
connection.rollback()
raise e
finally:
if cursor:
cursor.close()
if connection:
close_database_connection(connection)
def get_active_processes(process_type=None, guild_id=None, status="active"):
"""Retrieves active processes from the database with optional filters"""
connection = None
cursor = None
try:
connection = connect_to_database()
cursor = connection.cursor()
query = "SELECT * FROM active_processes WHERE status = %s"
params = [status]
if process_type:
query += " AND process_type = %s"
params.append(process_type)
if guild_id:
query += " AND guild_id = %s"
params.append(guild_id)
query += " ORDER BY created_at ASC"
cursor.execute(query, params)
results = cursor.fetchall()
processes = []
for row in results:
process = {
"uuid": row[0],
"process_type": row[1],
"guild_id": row[2],
"channel_id": row[3],
"user_id": row[4],
"target_id": row[5],
"start_time": row[6],
"end_time": row[7],
"status": row[8],
"data": json.loads(row[9]) if row[9] else None,
"metadata": json.loads(row[10]) if row[10] else None,
"created_at": row[11],
"updated_at": row[12]
}
processes.append(process)
return processes
except Exception as e:
logger.error(f"Error retrieving active processes: {e}")
return []
finally:
if cursor:
cursor.close()
if connection:
close_database_connection(connection)
def update_process_status(process_uuid, status, data=None, metadata=None):
"""Updates the status and optionally data/metadata of an active process"""
connection = None
cursor = None
try:
connection = connect_to_database()
cursor = connection.cursor()
# Prepare update fields
update_fields = ["status = %s", "updated_at = %s"]
params = [status, datetime.now()]
if data is not None:
update_fields.append("data = %s")
params.append(json.dumps(data))
if metadata is not None:
update_fields.append("metadata = %s")
params.append(json.dumps(metadata))
params.append(str(process_uuid))
update_query = f"UPDATE active_processes SET {', '.join(update_fields)} WHERE uuid = %s"
cursor.execute(update_query, params)
connection.commit()
logger.info(f"Updated process {process_uuid} status to: {status}")
return True
except Exception as e:
logger.error(f"Error updating process status: {e}")
if connection:
connection.rollback()
return False
finally:
if cursor:
cursor.close()
if connection:
close_database_connection(connection)
def delete_process(process_uuid):
"""Deletes a process from the database"""
connection = None
cursor = None
try:
connection = connect_to_database()
cursor = connection.cursor()
delete_query = "DELETE FROM active_processes WHERE uuid = %s"
cursor.execute(delete_query, (str(process_uuid),))
connection.commit()
logger.info(f"Deleted process: {process_uuid}")
return True
except Exception as e:
logger.error(f"Error deleting process: {e}")
if connection:
connection.rollback()
return False
finally:
if cursor:
cursor.close()
if connection:
close_database_connection(connection)
def cleanup_expired_processes():
"""Cleans up expired processes and marks them as completed"""
connection = None
cursor = None
try:
connection = connect_to_database()
cursor = connection.cursor()
current_time = datetime.now()
# Find expired processes
select_query = """
SELECT uuid, process_type, data FROM active_processes
WHERE status = 'active' AND end_time IS NOT NULL AND end_time <= %s
"""
cursor.execute(select_query, (current_time,))
expired_processes = cursor.fetchall()
# Update expired processes
if expired_processes:
update_query = """
UPDATE active_processes
SET status = 'expired', updated_at = %s
WHERE status = 'active' AND end_time IS NOT NULL AND end_time <= %s
"""
cursor.execute(update_query, (current_time, current_time))
connection.commit()
logger.info(f"Marked {len(expired_processes)} processes as expired")
return expired_processes
except Exception as e:
logger.error(f"Error cleaning up expired processes: {e}")
return []
finally:
if cursor:
cursor.close()
if connection:
close_database_connection(connection)
# Process Management Task
@tasks.loop(minutes=1)
async def process_manager():
"""Main task that manages all active processes"""
try:
# Clean up expired processes first
expired_processes = cleanup_expired_processes()
# Handle expired processes
for uuid_str, process_type, data_json in expired_processes:
await handle_expired_process(uuid_str, process_type, json.loads(data_json) if data_json else {})
# Check for processes that need handling
active_processes = get_active_processes(status="active")
for process in active_processes:
if process["end_time"] and datetime.now() >= process["end_time"]:
await handle_process_completion(process)
except Exception as e:
logger.error(f"Error in process manager: {e}")
async def handle_expired_process(process_uuid, process_type, data):
"""Handles different types of expired processes"""
try:
if process_type == "giveaway":
await handle_expired_giveaway(process_uuid, data)
elif process_type == "mute":
await handle_expired_mute(process_uuid, data)
elif process_type == "ban":
await handle_expired_ban(process_uuid, data)
# Add more process types as needed
except Exception as e:
logger.error(f"Error handling expired process {process_uuid}: {e}")
async def handle_expired_giveaway(process_uuid, data):
"""Handles expired giveaway processes"""
try:
giveaway_id = data.get("giveaway_id")
if giveaway_id and giveaway_id in giveaways:
giveaway = giveaways[giveaway_id]
# Execute giveaway ending logic
winners = giveaway.pick_winners()
if winners:
winner_mentions = ", ".join([winner.mention for winner in winners])
await giveaway.ctx.send(f"🎉 Congratulations to the winners of the giveaway '{giveaway.title}'! The winners are: {winner_mentions}")
# Process winners
for i, winner in enumerate(winners):
try:
winner_uuid = giveaway.winner_uuids[i]
assign_winner_to_uuid(winner_uuid, winner.id)
await winner.send(f"🎁 Congratulations! You won the giveaway '{giveaway.title}'!\n"
f"Please claim your prize using the following link: {GIVEAWAY_WEBSITE_URL}{giveaway.guild_id}/{winner_uuid}")
except Exception as e:
logger.error(f"Error processing winner {winner.name}: {e}")
else:
await giveaway.ctx.send(f"The giveaway '{giveaway.title}' has ended, but there were no participants.")
# Clean up
del giveaways[giveaway_id]
update_process_status(process_uuid, "completed")
except Exception as e:
logger.error(f"Error handling expired giveaway {process_uuid}: {e}")
async def handle_expired_mute(process_uuid, data):
"""Handles expired mute processes - placeholder for future implementation"""
try:
# TODO: Implement mute removal logic
guild_id = data.get("guild_id")
user_id = data.get("user_id")
logger.info(f"Mute expired for user {user_id} in guild {guild_id}")
update_process_status(process_uuid, "completed")
except Exception as e:
logger.error(f"Error handling expired mute {process_uuid}: {e}")
async def handle_expired_ban(process_uuid, data):
"""Handles expired ban processes - placeholder for future implementation"""
try:
# TODO: Implement ban removal logic
guild_id = data.get("guild_id")
user_id = data.get("user_id")
logger.info(f"Ban expired for user {user_id} in guild {guild_id}")
update_process_status(process_uuid, "completed")
except Exception as e:
logger.error(f"Error handling expired ban {process_uuid}: {e}")
async def handle_process_completion(process):
"""Generic handler for process completion"""
try:
await handle_expired_process(process["uuid"], process["process_type"], process["data"] or {})
except Exception as e:
logger.error(f"Error in process completion handler: {e}")
async def restore_active_processes_on_startup():
"""Restores active processes when the bot starts up"""
try:
logger.info("Restoring active processes from database...")
# Get all active processes
processes = get_active_processes(status="active")
restored_count = 0
for process in processes:
try:
if process["process_type"] == "giveaway":
await restore_giveaway_process(process)
restored_count += 1
# Add more process types as needed
except Exception as e:
logger.error(f"Error restoring process {process['uuid']}: {e}")
logger.info(f"Successfully restored {restored_count} active processes")
except Exception as e:
logger.error(f"Error restoring active processes: {e}")
async def restore_giveaway_process(process):
"""Restores a giveaway process from the database"""
try:
data = process["data"] or {}
giveaway_id = data.get("giveaway_id")
if giveaway_id:
# Check if giveaway is still valid and not expired
if process["end_time"] and datetime.now() < process["end_time"]:
# Create a minimal giveaway object for restoration
# Note: This is a simplified restoration - full ctx recreation may not be possible
logger.info(f"Restored giveaway process {giveaway_id} from database")
# Start the process manager if it's not already running
if not process_manager.is_running():
process_manager.start()
else:
# Mark as expired if past end time
update_process_status(process["uuid"], "expired")
except Exception as e:
logger.error(f"Error restoring giveaway process: {e}")
#-----------------------------------------------------------------------------------------------------------
async def update_all_users(batch_size=20, pause_duration=1):
connection = connect_to_database()
cursor = connection.cursor()
@@ -656,9 +1016,80 @@ class Giveaway:
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()
def create_process_entry(self):
"""Creates an entry in the active_processes table for this giveaway"""
try:
giveaway_data = {
"giveaway_id": len(giveaways) + 1, # Will be set properly when added to giveaways dict
"platform": self.platform,
"prize": self.prize,
"num_winners": self.num_winners,
"title": self.title,
"subtitle": self.subtitle,
"winner_uuids": [str(uuid) for uuid in self.winner_uuids],
"prize_uuid": str(self.prize_uuid),
"game_key": self.game_key
}
giveaway_metadata = {
"duration": self.duration,
"channel_id": self.ctx.channel.id,
"author_id": self.ctx.author.id
}
self.process_uuid = create_active_process(
process_type="giveaway",
guild_id=self.guild_id,
channel_id=self.ctx.channel.id,
user_id=self.ctx.author.id,
start_time=datetime.now(),
end_time=self.end_time,
status="active",
data=giveaway_data,
metadata=giveaway_metadata
)
logger.info(f"Created process entry for giveaway '{self.title}' with process UUID: {self.process_uuid}")
except Exception as e:
logger.error(f"Error creating process entry for giveaway: {e}")
def update_process_data(self, giveaway_id):
"""Updates the process data with the actual giveaway ID"""
try:
if self.process_uuid:
current_processes = get_active_processes()
for process in current_processes:
if process["uuid"] == str(self.process_uuid):
data = process["data"] or {}
data["giveaway_id"] = giveaway_id
update_process_status(self.process_uuid, "active", data=data)
logger.info(f"Updated process data for giveaway {giveaway_id}")
break
except Exception as e:
logger.error(f"Error updating process data: {e}")
def add_participant(self, user):
if user not in self.participants:
self.participants.append(user)
# Update process data with participant 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)
update_process_status(self.process_uuid, "active", data=data)
break
except Exception as e:
logger.error(f"Error updating participant count: {e}")
return True
return False
@@ -677,6 +1108,15 @@ class Giveaway:
logger.info(f"Selected {len(winners)} winners: {[winner.name for winner in winners]}")
return winners
def complete_giveaway(self):
"""Marks the giveaway process as completed"""
try:
if self.process_uuid:
update_process_status(self.process_uuid, "completed")
logger.info(f"Marked giveaway process {self.process_uuid} as completed")
except Exception as e:
logger.error(f"Error completing giveaway process: {e}")
@client.hybrid_command()
async def startgiveaway(ctx, platform: str, prize: str, num_winners: int, title: str, subtitle: str, duration: str):
"""Creates a new giveaway, only available for admins."""
@@ -701,6 +1141,9 @@ async def startgiveaway(ctx, platform: str, prize: str, num_winners: int, title:
giveaway_id = len(giveaways) + 1
giveaways[giveaway_id] = giveaway
# Update the process data with the actual giveaway ID
giveaway.update_process_data(giveaway_id)
button = Button(label="Participate", style=discord.ButtonStyle.green, custom_id=f"giveaway_{giveaway_id}")
view = View()
view.add_item(button)
@@ -714,7 +1157,9 @@ async def startgiveaway(ctx, platform: str, prize: str, num_winners: int, title:
embed.set_footer(text=f"Giveaway ends at {end_time.strftime('%Y-%m-%d %H:%M:%S')}")
await ctx.send(embed=embed, view=view)
check_giveaway.start(giveaway_id)
# Start the process manager if it's not already running
if not process_manager.is_running():
process_manager.start()
# -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
@@ -852,39 +1297,7 @@ async def setlocalpermission(ctx, permission_level: int):
else:
await ctx.send("You do not have permission to set local permissions.")
@tasks.loop(minutes=1)
async def check_giveaway(giveaway_id):
giveaway = giveaways.get(giveaway_id)
if giveaway and giveaway.is_finished():
check_giveaway.stop()
winners = giveaway.pick_winners()
if winners:
winner_mentions = ", ".join([winner.mention for winner in winners])
await giveaway.ctx.send(f"🎉 Congratulations to the winners of the giveaway '{giveaway.title}'! The winners are: {winner_mentions}")
# Verwende die bereits erstellten UUIDs und weise sie den tatsächlichen Gewinnern zu
logger.info(f"Processing {len(winners)} winners for giveaway '{giveaway.title}'")
for i, winner in enumerate(winners):
try:
user_data = load_user_data_sync(winner.id, giveaway.guild_id)
# Verwende die bereits beim Erstellen generierte UUID
winner_uuid = giveaway.winner_uuids[i] # i-te UUID für i-ten Gewinner
logger.info(f"Assigning winner {i+1}/{len(winners)}: {winner.name} (ID: {winner.id}) to existing UUID: {winner_uuid}")
# Verknüpfe die bereits vorhandene UUID mit dem tatsächlichen Gewinner
assign_winner_to_uuid(winner_uuid, winner.id)
await winner.send(f"🎁 Congratulations! You won the giveaway '{giveaway.title}'!\n"
f"Please claim your prize using the following link: {GIVEAWAY_WEBSITE_URL}{giveaway.guild_id}/{winner_uuid}")
logger.info(f"Sent winner notification with UUID: {winner_uuid}")
except Exception as e:
logger.error(f"Error processing winner {winner.name}: {e}")
else:
await giveaway.ctx.send(f"The giveaway '{giveaway.title}' has ended, but there were no participants.")
del giveaways[giveaway_id]
# Old check_giveaway task removed - now handled by process_manager
@client.event
async def on_interaction(interaction):
@@ -1151,8 +1564,17 @@ background_data = read_background_data("background_data.txt")
@client.event
async def on_ready():
# Start background tasks
client.loop.create_task(process_ai_queue())
client.loop.create_task(process_live_chat_queue()) # Starte die Queue-Verarbeitung
# Initialize process management system
await restore_active_processes_on_startup()
# Start the process manager
if not process_manager.is_running():
process_manager.start()
logger.info("Bot is ready!")
logger.info(f"Logged in as: {client.user.name}")
logger.info(f"Client ID: {client.user.id}")
@@ -1573,6 +1995,74 @@ async def toggle_feature(ctx, feature: str, state: str):
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."""