OokamiPupV2/globals.py

224 lines
8.8 KiB
Python

import time
import json
import sys
import traceback
import discord
# Store the start time globally.
_bot_start_time = time.time()
def get_bot_start_time():
"""
Retrieve the bot's start time.
This function returns the Unix timestamp (in seconds) when the bot was started.
The timestamp is stored in the global variable `_bot_start_time`, which is set
when the module is first imported.
Returns:
float: The Unix timestamp representing the bot's start time.
"""
return _bot_start_time
def load_config_file():
"""
Load the configuration file.
This function attempts to read the JSON configuration from 'config.json'
in the current directory and return its contents as a dictionary. If the
file is not found or if the file contains invalid JSON, an error message
is printed and the program terminates with a non-zero exit code.
Returns:
dict: The configuration data loaded from 'config.json'.
Raises:
SystemExit: If 'config.json' is missing or cannot be parsed.
"""
CONFIG_PATH = "config.json"
try:
with open(CONFIG_PATH, "r") as f:
config_data = json.load(f)
return config_data
except FileNotFoundError:
print("Error: config.json not found.")
sys.exit(1)
except json.JSONDecodeError as e:
print(f"Error parsing config.json: {e}")
sys.exit(1)
# Load configuration file
config_data = load_config_file()
###############################
# Simple Logging System
###############################
def log(message: str, level="INFO", exec_info=False, linebreaks=False):
"""
Log a message with the specified log level.
Capable of logging individual levels to the terminal and/or logfile separately.
Can also append traceback information if needed, and is capable of preserving/removing linebreaks from log messages as needed.
Args:
message (str): The message to log.
level (str, optional): Log level of the message. Defaults to "INFO".
exec_info (bool, optional): If True, append traceback information. Defaults to False.
linebreaks (bool, optional): If True, preserve line breaks in the log. Defaults to False.
Available levels:
DEBUG - Information useful for debugging.
INFO - Informational messages.
WARNING - Something happened that may lead to issues.
ERROR - A non-critical error has occurred.
CRITICAL - A critical, but non-fatal, error occurred.
FATAL - Fatal error; program exits after logging this.
See:
config.json for further configuration options under "logging".
Example:
log("An error occured during processing", "ERROR", exec_info=True, linebreaks=False)
"""
# Initiate logfile
lfp = config_data["logging"]["logfile_path"] # Log File Path
clfp = f"cur_{lfp}" # Current Log File Path
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 !!!")
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() - 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:
lfp = config_data["logging"]["logfile_path"]
clfp = f"cur_{lfp}"
with open(lfp, "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(clfp, "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)
def reset_curlogfile():
"""
Clear the current log file.
This function constructs the current log file path by prepending 'cur_'
to the log file path specified in the configuration data under the "logging"
section. It then opens the file in write mode, effectively truncating and
clearing its contents.
If an exception occurs while attempting to clear the log file, the error is
silently ignored.
"""
# Initiate logfile
lfp = config_data["logging"]["logfile_path"] # Log File Path
clfp = f"cur_{lfp}" # Current Log File Path
try:
open(clfp, "w")
# log(f"Current-run logfile cleared", "DEBUG")
except Exception as e:
# log(f"Failed to clear current-run logfile: {e}")
pass
def init_db_conn():
"""
Initialize and return a database connection.
This function reads the configuration settings and attempts to establish a
connection to the database by invoking `modules.db.init_db_connection()`. If
no valid connection is obtained (i.e. if the connection is None), it logs a
fatal error and terminates the program using sys.exit(1). If an exception is
raised during the initialization process, the error is logged and the function
returns None.
Returns:
DatabaseConnection or None: A valid database connection object if
successfully established; otherwise, None (or the program may exit if the
connection is missing).
"""
try:
import modules.db
db_conn = modules.db.init_db_connection(config_data)
if not db_conn:
# If we get None, it means a fatal error occurred.
log("Terminating bot due to no DB connection.", "FATAL")
sys.exit(1)
return db_conn
except Exception as e:
log(f"Unable to initialize database!: {e}", "FATAL")
return None
class Constants:
@property
def config_data(self) -> dict:
"""Returns a dictionary of the contents of the config.json config file"""
return load_config_file()
def bot_start_time(self) -> float:
"""Returns the bot epoch start time"""
return _bot_start_time
def primary_discord_guild(self) -> object | None:
"""
Retrieve the primary Discord guild from the configuration.
This function attempts to obtain the primary Discord guild based on the
configuration data stored in `config_data["discord_guilds"]`. It converts the first
guild ID in the list to an integer and then creates a `discord.Object` from it. If the
configuration defines more than one (or fewer than the expected number of) guilds, the function
returns `None` for the guild ID.
Returns:
dict: A dictionary with the following keys:
- "object": A `discord.Object` representing the primary Discord guild if exactly one
guild is defined; otherwise, `None`.
- "id": The integer ID of the primary guild if available; otherwise, `None`.
"""
primary_guild_object = None
primary_guild_int = int(config_data["discord_guilds"][0]) if len(config_data["discord_guilds"]) == 1 else None
if primary_guild_int:
primary_guild_object = discord.Object(id=primary_guild_int)
return_dict = {"object": primary_guild_object, "id": primary_guild_int}
return return_dict
constants = Constants()