# bots.py import os import json import asyncio import sys import time import traceback import globals from functools import partial from discord.ext import commands from dotenv import load_dotenv from bot_discord import DiscordBot from bot_twitch import TwitchBot #from modules.db import init_db_connection, run_db_operation #from modules.db import ensure_quotes_table, ensure_users_table, ensure_chatlog_table, checkenable_db_fk from modules import db, utility # Load environment variables load_dotenv() # Load bot configuration config_data = globals.load_config_file() # Initiate logfile logfile_path = config_data["logging"]["logfile_path"] logfile = open(logfile_path, "a") cur_logfile_path = f"cur_{logfile_path}" cur_logfile = open(cur_logfile_path, "w") if not config_data["logging"]["terminal"]["log_to_terminal"] and not config_data["logging"]["file"]["log_to_file"]: print(f"!!! WARNING !!! CONSOLE AND LOGFILE OUTPUT DISABLED !!!\n!!! NO LOGS WILL BE PROVIDED !!!") ############################### # Simple Logging System ############################### def log(message, level="INFO", exec_info=False): """ A simple logging function with adjustable log levels. Logs messages in a structured format. Available levels:\n DEBUG = Information useful for debugging\n INFO = Informational messages\n WARNING = Something happened that may lead to issues\n ERROR = A non-critical error has happened\n CRITICAL = A critical, but non-fatal, error\n FATAL = Fatal error. Program exits after logging this\n\n See 'config.json' for disabling/enabling logging levels """ from modules import utility 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 level in config_data["logging"]["log_levels"] or level == "FATAL": elapsed = time.time() - globals.get_bot_start_time() uptime_str, _ = utility.format_uptime(elapsed) timestamp = time.strftime('%Y-%m-%d %H:%M:%S') log_message = f"[{timestamp} - {uptime_str}] [{level}] {message}" # Include traceback for certain error levels if exec_info or level in ["CRITICAL", "FATAL"]: log_message += f"\n{traceback.format_exc()}" # Print to terminal if enabled # 'FATAL' errors override settings # Checks config file to see enabled/disabled logging levels if config_data["logging"]["terminal"]["log_to_terminal"] or level == "FATAL": config_level_format = f"log_{level.lower()}" if config_data["logging"]["terminal"][config_level_format] or level == "FATAL": print(log_message) # Write to file if enabled # 'FATAL' errors override settings # Checks config file to see enabled/disabled logging levels if config_data["logging"]["file"]["log_to_file"] or level == "FATAL": config_level_format = f"log_{level.lower()}" if config_data["logging"]["file"][config_level_format] or level == "FATAL": try: lf = config_data["logging"]["logfile_path"] clf = f"cur_{lf}" with open(lf, "a", encoding="utf-8") as logfile: # Write to permanent logfile logfile.write(f"{log_message}\n") logfile.flush() # Ensure it gets written immediately with open(clf, "a", encoding="utf-8") as c_logfile: # Write to this-run logfile c_logfile.write(f"{log_message}\n") c_logfile.flush() # Ensure it gets written immediately except Exception as e: print(f"[WARNING] Failed to write to logfile: {e}") # Handle fatal errors with shutdown if level == "FATAL": print(f"!!! FATAL ERROR LOGGED, SHUTTING DOWN !!!") sys.exit(1) ############################### # Main Event Loop ############################### async def main(): global discord_bot, twitch_bot, db_conn # Log initial start log("--------------- BOT STARTUP ---------------") # Before creating your DiscordBot/TwitchBot, initialize DB try: db_conn = db.init_db_connection(config_data, log) if not db_conn: # If we get None, it means FATAL. We might sys.exit(1) or handle it differently. log("Terminating bot due to no DB connection.", "FATAL") sys.exit(1) except Exception as e: log(f"Unable to initialize database!: {e}", "FATAL") try: # Ensure FKs are enabled db.checkenable_db_fk(db_conn, log) except Exception as e: log(f"Unable to ensure Foreign keys are enabled: {e}", "WARNING") # auto-create the quotes table if it doesn't exist tables = { "Bot events table": partial(db.ensure_bot_events_table, db_conn, log), "Quotes table": partial(db.ensure_quotes_table, db_conn, log), "Users table": partial(db.ensure_users_table, db_conn, log), "Chatlog table": partial(db.ensure_chatlog_table, db_conn, log), "Howls table": partial(db.ensure_userhowls_table, db_conn, log), "Discord activity table": partial(db.ensure_discord_activity_table, db_conn, log), "Account linking table": partial(db.ensure_link_codes_table, db_conn, log) } try: for table, func in tables.items(): func() # Call the function with db_conn and log already provided log(f"{table} ensured.", "DEBUG") except Exception as e: log(f"Unable to ensure DB tables exist: {e}", "FATAL") log("Initializing bots...") # Create both bots discord_bot = DiscordBot(config_data, log) twitch_bot = TwitchBot(config_data, log) # Log startup utility.log_bot_startup(db_conn, log) # Provide DB connection to both bots try: discord_bot.set_db_connection(db_conn) twitch_bot.set_db_connection(db_conn) log(f"Initialized database connection to both bots") except Exception as e: log(f"Unable to initialize database connection to one or both bots: {e}", "FATAL") log("Starting Discord and Twitch bots...") discord_task = asyncio.create_task(discord_bot.run(os.getenv("DISCORD_BOT_TOKEN"))) twitch_task = asyncio.create_task(twitch_bot.run()) from modules.utility import dev_func enable_dev_func = False if enable_dev_func: dev_func_result = dev_func(db_conn, log, enable_dev_func) log(f"dev_func output: {dev_func_result}") await asyncio.gather(discord_task, twitch_task) if __name__ == "__main__": try: asyncio.run(main()) except KeyboardInterrupt: utility.log_bot_shutdown(db_conn, log, intent="User Shutdown") except Exception as e: error_trace = traceback.format_exc() log(f"Fatal Error: {e}\n{error_trace}", "FATAL") utility.log_bot_shutdown(db_conn, log)