- 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
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")

View File

@ -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")

107
bots.py
View File

@ -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")

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
(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

View File

@ -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"]
}

View File

@ -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)