- Reverted logging to a simpler terminal-only

- Added log levels in config.json
- Reworked requirements.txt
pull/1/head
Kami 2025-02-01 20:50:54 +01:00
parent 9727927b66
commit 82aec3dc5f
6 changed files with 82 additions and 121 deletions

View File

@ -5,10 +5,10 @@ import importlib
import cmd_discord import cmd_discord
class DiscordBot(commands.Bot): class DiscordBot(commands.Bot):
def __init__(self, config, logger): def __init__(self, config, log_func):
super().__init__(command_prefix="!", intents=discord.Intents.all()) super().__init__(command_prefix="!", intents=discord.Intents.all())
self.config = config self.config = config
self.logger = logger self.log = log_func # Use the logging function from bots.py
self.load_commands() self.load_commands()
def load_commands(self): def load_commands(self):
@ -18,21 +18,15 @@ class DiscordBot(commands.Bot):
try: try:
importlib.reload(cmd_discord) importlib.reload(cmd_discord)
cmd_discord.setup(self) cmd_discord.setup(self)
self.logger.info("Discord commands loaded successfully.") self.log("Discord commands loaded successfully.", "INFO")
except Exception as e: 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): async def on_ready(self):
self.logger.info(f"Discord bot is online as {self.user}") self.log(f"Discord bot is online as {self.user}", "INFO")
# @commands.command()
# async def reload(self, ctx):
# """ Reload all Discord commands dynamically. """
# self.load_commands()
# await ctx.send("Commands reloaded!")
async def run(self, token): async def run(self, token):
try: try:
await super().start(token) await super().start(token)
except Exception as e: except Exception as e:
self.logger.error(f"Discord bot error: {e}") self.log(f"Discord bot error: {e}", "CRITICAL")

View File

@ -1,18 +1,18 @@
# bot_twitch.py
import os import os
import requests import requests
import logging
import asyncio import asyncio
from twitchio.ext import commands from twitchio.ext import commands
import importlib import importlib
import cmd_twitch import cmd_twitch
class TwitchBot(commands.Bot): 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_id = os.getenv("TWITCH_CLIENT_ID")
self.client_secret = os.getenv("TWITCH_CLIENT_SECRET") self.client_secret = os.getenv("TWITCH_CLIENT_SECRET")
self.token = os.getenv("TWITCH_BOT_TOKEN") self.token = os.getenv("TWITCH_BOT_TOKEN")
self.refresh_token = os.getenv("TWITCH_REFRESH_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 self.config = config
# 1) Initialize the parent Bot FIRST # 1) Initialize the parent Bot FIRST
@ -30,7 +30,7 @@ class TwitchBot(commands.Bot):
Refreshes the Twitch access token using the stored refresh token. Refreshes the Twitch access token using the stored refresh token.
Retries up to 2 times before logging a fatal error. 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" url = "https://id.twitch.tv/oauth2/token"
params = { params = {
@ -53,19 +53,18 @@ class TwitchBot(commands.Bot):
os.environ["TWITCH_REFRESH_TOKEN"] = self.refresh_token os.environ["TWITCH_REFRESH_TOKEN"] = self.refresh_token
self.update_env_file() self.update_env_file()
self.logger.info("Twitch token refreshed successfully.") self.log("Twitch token refreshed successfully.", "INFO")
return # Success, exit function return # Success, exit function
else: 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: 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 await asyncio.sleep(10) # Wait before retrying
# If all attempts fail, log error # If all attempts fail, log error
from bots import log_error self.log("Twitch token refresh failed after 3 attempts.", "FATAL")
log_error("Twitch token refresh failed after 3 attempts.")
def update_env_file(self): def update_env_file(self):
""" """
@ -84,10 +83,10 @@ class TwitchBot(commands.Bot):
else: else:
file.write(line) 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: 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): def load_commands(self):
""" """
@ -96,20 +95,25 @@ class TwitchBot(commands.Bot):
try: try:
importlib.reload(cmd_twitch) importlib.reload(cmd_twitch)
cmd_twitch.setup(self) cmd_twitch.setup(self)
self.logger.info("Twitch commands loaded successfully.") self.log("Twitch commands loaded successfully.", "INFO")
except Exception as e: 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): async def run(self):
""" """
Run the Twitch bot, refreshing tokens if needed. Run the Twitch bot, refreshing tokens if needed.
""" """
try: try:
await self.connect() # Connect to Twitch self.log(f"Twitch bot connecting...", "INFO")
await self.start() # Start the bot event loop self.log(f"...Consider online if no further messages", "INFO")
while True: await self.start()
await self.refresh_access_token() #while True:
await asyncio.sleep(10800) # 3 hours # await self.refresh_access_token()
# await asyncio.sleep(10800) # Refresh every 3 hours
except Exception as e: except Exception as e:
from bots import log_error self.log(f"Twitch bot failed to start: {e}", "CRITICAL")
log_error(f"Twitch bot failed to start: {e}") 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")

105
bots.py
View File

@ -2,11 +2,9 @@
import os import os
import json import json
import asyncio import asyncio
import logging
import sys import sys
import traceback
import time import time
from collections import Counter import traceback
from discord.ext import commands from discord.ext import commands
from dotenv import load_dotenv from dotenv import load_dotenv
from bot_discord import DiscordBot from bot_discord import DiscordBot
@ -28,96 +26,51 @@ except json.JSONDecodeError as e:
sys.exit(1) sys.exit(1)
# Global settings # Global settings
ERROR_LOG_FILE = "error_log.txt" bot_start_time = time.time()
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 # 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. A simple logging function with adjustable log levels.
If it fails, logs to error_log.txt instead. 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: if level in config_data["log_levels"]:
return # No errors to send timestamp = time.strftime('%Y-%m-%d %H:%M:%S')
log_message = f"[{timestamp}] [{level}] {message}"
# 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: try:
discord_bot print(log_message) # Print to terminal
except NameError: except Exception:
discord_bot = DiscordBot(config_data, logger) pass # Prevent logging failures from crashing the bot
asyncio.create_task(send_discord_error_report(discord_bot))
# Placeholder for future expansions (e.g., file logging, Discord alerts, etc.)
############################### ###############################
# Main Event Loop # Main Event Loop
############################### ###############################
global bot_start_time
bot_start_time = time.time()
async def main(): async def main():
global discord_bot, twitch_bot global discord_bot, twitch_bot
discord_bot = DiscordBot(config_data, logger) log("Initializing bots...", "INFO")
twitch_bot = TwitchBot(config_data, logger)
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()) twitch_task = asyncio.create_task(twitch_bot.run())
await asyncio.gather(discord_task, twitch_task) await asyncio.gather(discord_task, twitch_task)
@ -127,4 +80,4 @@ if __name__ == "__main__":
asyncio.run(main()) asyncio.run(main())
except Exception as e: except Exception as e:
error_trace = traceback.format_exc() error_trace = traceback.format_exc()
log_error(f"Fatal Error: {e}\n{error_trace}") log(f"Fatal Error: {e}\n{error_trace}", "FATAL")

View File

@ -42,13 +42,13 @@ def ping():
(345600, f"I've been awake for {uptime_str}. Four days... I'm running on fumes."), # 4 days (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 (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 (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 (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 (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 (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 (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 (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 (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 (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 # We'll iterate from smallest to largest threshold

View File

@ -1,6 +1,7 @@
{ {
"discord_guilds": [896713616089309184], "discord_guilds": [896713616089309184],
"twitch_channels": ["OokamiKunTV", "ookamipup"], "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"]
} }

View File

@ -1,4 +1,13 @@
discord.py # Core Dependencies
twitchio python-dotenv==1.0.0 # Loads environment variables from .env
python-dotenv requests==2.31.0 # HTTP requests for Twitch API (token refresh, etc.)
PyMySQL
# 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)