- Reverted logging to a simpler terminal-only
- Added log levels in config.json - Reworked requirements.txtpull/1/head
parent
9727927b66
commit
82aec3dc5f
|
@ -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")
|
||||
|
|
|
@ -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")
|
||||
|
|
103
bots.py
103
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:
|
||||
if level in config_data["log_levels"]:
|
||||
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")
|
||||
log_message = f"[{timestamp}] [{level}] {message}"
|
||||
|
||||
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))
|
||||
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")
|
||||
|
|
|
@ -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"]
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue