- 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
|
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")
|
||||||
|
|
|
@ -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
105
bots.py
|
@ -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")
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue