From 50d148c094a27abd3bb8ff1b670132aacce31e06 Mon Sep 17 00:00:00 2001 From: SimolZimol <70102430+SimolZimol@users.noreply.github.com> Date: Mon, 18 Aug 2025 14:25:35 +0200 Subject: [PATCH] modified: bot.py --- bot.py | 560 +++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 525 insertions(+), 35 deletions(-) diff --git a/bot.py b/bot.py index 78363a0..84ec806 100644 --- a/bot.py +++ b/bot.py @@ -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() @@ -655,10 +1015,81 @@ class Giveaway: # 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() + + 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.""" @@ -700,6 +1140,9 @@ async def startgiveaway(ctx, platform: str, prize: str, num_winners: int, title: giveaway = Giveaway(ctx, platform, prize, num_winners, title, subtitle, duration, end_time) giveaway_id = len(giveaways) + 1 giveaways[giveaway_id] = giveaway + + # Update the process data with the actual giveaway ID + giveaway.update_process_data(giveaway_id) button = Button(label="Participate", style=discord.ButtonStyle.green, custom_id=f"giveaway_{giveaway_id}") view = View() @@ -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."""