From 82aec3dc5ff5f42c431ce92a2e5e4ab5a1038026 Mon Sep 17 00:00:00 2001 From: Kami Date: Sat, 1 Feb 2025 20:50:54 +0100 Subject: [PATCH] - Reverted logging to a simpler terminal-only - Added log levels in config.json - Reworked requirements.txt --- bot_discord.py | 18 ++---- bot_twitch.py | 44 +++++++------- bots.py | 107 ++++++++++------------------------ cmd_common/common_commands.py | 14 ++--- config.json | 3 +- requirements.txt | 17 ++++-- 6 files changed, 82 insertions(+), 121 deletions(-) diff --git a/bot_discord.py b/bot_discord.py index 0bdb601..4365f2d 100644 --- a/bot_discord.py +++ b/bot_discord.py @@ -5,10 +5,10 @@ import importlib import cmd_discord class DiscordBot(commands.Bot): - def __init__(self, config, logger): + def __init__(self, config, log_func): super().__init__(command_prefix="!", intents=discord.Intents.all()) self.config = config - self.logger = logger + self.log = log_func # Use the logging function from bots.py self.load_commands() def load_commands(self): @@ -18,21 +18,15 @@ class DiscordBot(commands.Bot): try: importlib.reload(cmd_discord) cmd_discord.setup(self) - self.logger.info("Discord commands loaded successfully.") + self.log("Discord commands loaded successfully.", "INFO") except Exception as e: - self.logger.error(f"Error loading Discord commands: {e}") + self.log(f"Error loading Discord commands: {e}", "ERROR") async def on_ready(self): - self.logger.info(f"Discord bot is online as {self.user}") - -# @commands.command() -# async def reload(self, ctx): -# """ Reload all Discord commands dynamically. """ -# self.load_commands() -# await ctx.send("Commands reloaded!") + self.log(f"Discord bot is online as {self.user}", "INFO") async def run(self, token): try: await super().start(token) except Exception as e: - self.logger.error(f"Discord bot error: {e}") + self.log(f"Discord bot error: {e}", "CRITICAL") diff --git a/bot_twitch.py b/bot_twitch.py index 8b06a51..fc24402 100644 --- a/bot_twitch.py +++ b/bot_twitch.py @@ -1,18 +1,18 @@ +# bot_twitch.py import os import requests -import logging import asyncio from twitchio.ext import commands import importlib import cmd_twitch class TwitchBot(commands.Bot): - def __init__(self, config, logger): + def __init__(self, config, log_func): self.client_id = os.getenv("TWITCH_CLIENT_ID") self.client_secret = os.getenv("TWITCH_CLIENT_SECRET") self.token = os.getenv("TWITCH_BOT_TOKEN") self.refresh_token = os.getenv("TWITCH_REFRESH_TOKEN") - self.logger = logger + self.log = log_func # Use the logging function from bots.py self.config = config # 1) Initialize the parent Bot FIRST @@ -30,7 +30,7 @@ class TwitchBot(commands.Bot): Refreshes the Twitch access token using the stored refresh token. Retries up to 2 times before logging a fatal error. """ - self.logger.info("Attempting to refresh Twitch token...") + self.log("Attempting to refresh Twitch token...", "INFO") url = "https://id.twitch.tv/oauth2/token" params = { @@ -53,19 +53,18 @@ class TwitchBot(commands.Bot): os.environ["TWITCH_REFRESH_TOKEN"] = self.refresh_token self.update_env_file() - self.logger.info("Twitch token refreshed successfully.") + self.log("Twitch token refreshed successfully.", "INFO") return # Success, exit function else: - self.logger.warning(f"Twitch token refresh failed (Attempt {attempt+1}/3): {data}") + self.log(f"Twitch token refresh failed (Attempt {attempt+1}/3): {data}", "WARNING") except Exception as e: - self.logger.error(f"Twitch token refresh error (Attempt {attempt+1}/3): {e}") + self.log(f"Twitch token refresh error (Attempt {attempt+1}/3): {e}", "ERROR") await asyncio.sleep(10) # Wait before retrying # If all attempts fail, log error - from bots import log_error - log_error("Twitch token refresh failed after 3 attempts.") + self.log("Twitch token refresh failed after 3 attempts.", "FATAL") def update_env_file(self): """ @@ -84,10 +83,10 @@ class TwitchBot(commands.Bot): else: file.write(line) - self.logger.info("Updated .env file with new Twitch token.") + self.log("Updated .env file with new Twitch token.", "INFO") except Exception as e: - self.logger.error(f"Failed to update .env file: {e}") + self.log(f"Failed to update .env file: {e}", "ERROR") def load_commands(self): """ @@ -96,20 +95,25 @@ class TwitchBot(commands.Bot): try: importlib.reload(cmd_twitch) cmd_twitch.setup(self) - self.logger.info("Twitch commands loaded successfully.") + self.log("Twitch commands loaded successfully.", "INFO") except Exception as e: - self.logger.error(f"Error loading Twitch commands: {e}") + self.log(f"Error loading Twitch commands: {e}", "ERROR") async def run(self): """ Run the Twitch bot, refreshing tokens if needed. """ try: - await self.connect() # Connect to Twitch - await self.start() # Start the bot event loop - while True: - await self.refresh_access_token() - await asyncio.sleep(10800) # 3 hours + self.log(f"Twitch bot connecting...", "INFO") + self.log(f"...Consider online if no further messages", "INFO") + await self.start() + #while True: + # await self.refresh_access_token() + # await asyncio.sleep(10800) # Refresh every 3 hours except Exception as e: - from bots import log_error - log_error(f"Twitch bot failed to start: {e}") + self.log(f"Twitch bot failed to start: {e}", "CRITICAL") + if "Invalid or unauthorized Access Token passed." in str(e): + try: + await self.refresh_access_token() + except Exception as e: + self.log(f"Unable to refresh Twitch token! Twitch bot will be offline!", "CRITICAL") diff --git a/bots.py b/bots.py index e7d21e1..aa50377 100644 --- a/bots.py +++ b/bots.py @@ -2,11 +2,9 @@ import os import json import asyncio -import logging import sys -import traceback import time -from collections import Counter +import traceback from discord.ext import commands from dotenv import load_dotenv from bot_discord import DiscordBot @@ -28,96 +26,51 @@ except json.JSONDecodeError as 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") +bot_start_time = time.time() ############################### -# Error Handling & Discord Alerts +# Simple Logging System ############################### -async def send_discord_error_report(bot): +def log(message, level="INFO"): """ - Sends cached errors as a Discord DM with cooldowns and batching. - If it fails, logs to error_log.txt instead. + A simple logging function with adjustable log levels. + Logs messages in a structured format. + + Available levels: + DEBUG, INFO, WARNING, ERROR, CRITICAL, FATAL + See 'config.json' for disabling/enabling logging levels """ - global ERROR_CACHE, LAST_ERROR_SENT + log_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "FATAL"] + if level not in log_levels: + level = "INFO" # Default to INFO if an invalid level is provided - 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: + if level in config_data["log_levels"]: + timestamp = time.strftime('%Y-%m-%d %H:%M:%S') + log_message = f"[{timestamp}] [{level}] {message}" + try: - discord_bot - except NameError: - discord_bot = DiscordBot(config_data, logger) - asyncio.create_task(send_discord_error_report(discord_bot)) + print(log_message) # Print to terminal + except Exception: + pass # Prevent logging failures from crashing the bot + + # Placeholder for future expansions (e.g., file logging, Discord alerts, etc.) ############################### # 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) + log("Initializing bots...", "INFO") - discord_task = asyncio.create_task(discord_bot.run(DISCORD_BOT_TOKEN)) + discord_bot = DiscordBot(config_data, log) + twitch_bot = TwitchBot(config_data, log) + + log("Starting Discord and Twitch bots...", "INFO") + + discord_task = asyncio.create_task(discord_bot.run(os.getenv("DISCORD_BOT_TOKEN"))) twitch_task = asyncio.create_task(twitch_bot.run()) await asyncio.gather(discord_task, twitch_task) @@ -127,4 +80,4 @@ if __name__ == "__main__": asyncio.run(main()) except Exception as e: error_trace = traceback.format_exc() - log_error(f"Fatal Error: {e}\n{error_trace}") + log(f"Fatal Error: {e}\n{error_trace}", "FATAL") diff --git a/cmd_common/common_commands.py b/cmd_common/common_commands.py index 87c52dc..b49bba9 100644 --- a/cmd_common/common_commands.py +++ b/cmd_common/common_commands.py @@ -42,13 +42,13 @@ def ping(): (345600, f"I've been awake for {uptime_str}. Four days... I'm running on fumes."), # 4 days (432000, f"I've been awake for {uptime_str}. Five days. Please send more coffee."), # 5 days (518400, f"I've been awake for {uptime_str}. Six days. I've forgotten what dreams are."), # 6 days - (604800, f"I've been awake for {uptime_str}. One week. I'm turning into a zombie."), # 7 days - (1209600, f"I've been awake for {uptime_str}. Two weeks. Are you sure I can't rest?"), # 14 days - (2592000, f"I've been awake for {uptime_str}. A month! The nightmares never end."), # 30 days - (7776000, f"I've been awake for {uptime_str}. Three months. I'm mostly coffee now."), # 90 days - (15552000,f"I've been awake for {uptime_str}. Six months. This is insane..."), # 180 days - (23328000,f"I've been awake for {uptime_str}. Nine months. I might be unstoppable."), # 270 days - (31536000,f"I've been awake for {uptime_str}. A year?! I'm a legend of insomnia..."), # 365 days + (604800, f"I've been awake for {uptime_str}. One week. I'm turning into a zombie."), # 7 days + (1209600, f"I've been awake for {uptime_str}. Two weeks. Are you sure I can't rest?"), # 14 days + (2592000, f"I've been awake for {uptime_str}. A month! The nightmares never end."), # 30 days + (7776000, f"I've been awake for {uptime_str}. Three months. I'm mostly coffee now."), # 90 days + (15552000,f"I've been awake for {uptime_str}. Six months. This is insane..."), # 180 days + (23328000,f"I've been awake for {uptime_str}. Nine months. I might be unstoppable."), # 270 days + (31536000,f"I've been awake for {uptime_str}. A year?! I'm a legend of insomnia..."), # 365 days ] # We'll iterate from smallest to largest threshold diff --git a/config.json b/config.json index 15b590d..47e080b 100644 --- a/config.json +++ b/config.json @@ -1,6 +1,7 @@ { "discord_guilds": [896713616089309184], "twitch_channels": ["OokamiKunTV", "ookamipup"], - "command_modules": ["cmd_discord", "cmd_twitch", "cmd_common"] + "command_modules": ["cmd_discord", "cmd_twitch", "cmd_common"], + "log_levels": ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "FATAL"] } \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 688004a..9049826 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,13 @@ -discord.py -twitchio -python-dotenv -PyMySQL +# Core Dependencies +python-dotenv==1.0.0 # Loads environment variables from .env +requests==2.31.0 # HTTP requests for Twitch API (token refresh, etc.) + +# Discord Bot Dependencies +discord.py==2.3.2 # Main Discord bot library (async) +PyNaCl==1.5.0 # Enables voice support (optional, required if using voice features) + +# Twitch Bot Dependencies +twitchio==2.7.1 # Twitch chat bot library (async) + +# Utility & Logging +aiohttp==3.9.1 # Async HTTP requests (dependency for discord.py & twitchio)