# bots.py import os import json import asyncio import logging import sys import traceback import time from collections import Counter from discord.ext import commands from dotenv import load_dotenv from bot_discord import DiscordBot from bot_twitch import TwitchBot # Load environment variables load_dotenv() # Load bot configuration CONFIG_PATH = "config.json" try: with open(CONFIG_PATH, "r") as f: config_data = json.load(f) except FileNotFoundError: print("Error: config.json not found.") sys.exit(1) except json.JSONDecodeError as e: print(f"Error parsing config.json: {e}") sys.exit(1) # Global settings ERROR_LOG_FILE = "error_log.txt" DISCORD_USER_ID = os.getenv("DISCORD_OWNER_ID") # Your Discord user ID for DM alerts DISCORD_BOT_TOKEN = os.getenv("DISCORD_BOT_TOKEN") # Discord bot token ERROR_CACHE = [] # Stores pending errors LAST_ERROR_SENT = 0 # Timestamp of last Discord error message ERROR_COOLDOWN = 900 # 15 minutes (900 sec) cooldown between Discord reports ERROR_BATCH_TIMEOUT = 5 # 5 seconds batching delay before sending # Setup logging logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", stream=sys.stdout ) logger = logging.getLogger("Main") ############################### # Error Handling & Discord Alerts ############################### async def send_discord_error_report(bot): """ Sends cached errors as a Discord DM with cooldowns and batching. If it fails, logs to error_log.txt instead. """ global ERROR_CACHE, LAST_ERROR_SENT if not ERROR_CACHE: return # No errors to send # Wait for batching timeout await asyncio.sleep(ERROR_BATCH_TIMEOUT) # Compress duplicate errors error_counter = Counter(ERROR_CACHE) compressed_errors = [f"{err} (Occurred {count} times)" if count > 1 else err for err, count in error_counter.items()] error_message = "\n".join(compressed_errors) # Ensure message fits in Discord limits if len(error_message) > 2000: error_message = error_message[:1990] + "\n... (truncated)" # Update timestamp LAST_ERROR_SENT = time.time() try: user = await bot.fetch_user(DISCORD_USER_ID) await user.send(f"**Bot Error Report**\n\n{error_message}") logger.info(f"[ERROR REPORT] Sent {len(ERROR_CACHE)} errors at {time.strftime('%H:%M:%S')} | Discord Send Status: Success") except Exception as e: # If Discord DM fails, log errors to a file with open(ERROR_LOG_FILE, "a") as f: timestamp = time.strftime('%Y-%m-%d %H:%M:%S') f.write(f"\n----- {len(ERROR_CACHE)} Errors Occurred at {timestamp} -----\n") for err in ERROR_CACHE: f.write(f"{err}\n") logger.error(f"[ERROR REPORT] Failed to send Discord DM. Errors saved to {ERROR_LOG_FILE}") # Clear cache after sending ERROR_CACHE = [] def log_error(error_msg): """ Handles error logging with cooldowns. If multiple errors occur in a short time, they are cached and sent in batch. """ global ERROR_CACHE, LAST_ERROR_SENT ERROR_CACHE.append(error_msg) if time.time() - LAST_ERROR_SENT >= ERROR_COOLDOWN: try: discord_bot except NameError: discord_bot = DiscordBot(config_data, logger) asyncio.create_task(send_discord_error_report(discord_bot)) ############################### # Main Event Loop ############################### global bot_start_time bot_start_time = time.time() async def main(): global discord_bot, twitch_bot discord_bot = DiscordBot(config_data, logger) twitch_bot = TwitchBot(config_data, logger) discord_task = asyncio.create_task(discord_bot.run(DISCORD_BOT_TOKEN)) twitch_task = asyncio.create_task(twitch_bot.run()) await asyncio.gather(discord_task, twitch_task) if __name__ == "__main__": try: asyncio.run(main()) except Exception as e: error_trace = traceback.format_exc() log_error(f"Fatal Error: {e}\n{error_trace}")