diff --git a/bot.py b/bot.py index 7243c20..f40f981 100644 --- a/bot.py +++ b/bot.py @@ -25,11 +25,15 @@ from modules.translations import * from modules.markovmemory import * from modules.version import * from modules.sentenceprocessing import * +from modules.prestartchecks import start_checks +from modules.unhandledexception import handle_exception from discord.ext import commands, tasks from discord import app_commands +sys.excepthook = handle_exception # Print splash text and check for updates print(splashtext) # Print splash text (from modules/globalvars.py) +start_checks() check_for_update() # Check for updates (from modules/version.py) launched = False @@ -111,6 +115,7 @@ used_words = set() @bot.event async def on_ready(): global launched + global slash_commands_enabled folder_name = "cogs" if launched == True: return @@ -146,6 +151,24 @@ async def on_ready(): # Load positive GIF URLs from environment variable positive_gifs = os.getenv("POSITIVE_GIFS").split(',') +@bot.event +async def on_command_error(ctx, error): + from modules.unhandledexception import handle_exception + + if isinstance(error, commands.CommandInvokeError): + original = error.original + handle_exception( + type(original), original, original.__traceback__, + context=f"Command: {ctx.command} | User: {ctx.author}" + ) + else: + handle_exception( + type(error), error, error.__traceback__, + context=f"Command: {ctx.command} | User: {ctx.author}" + ) + + + # Command: Retrain the Markov model from memory @bot.hybrid_command(description=f"{get_translation(LOCALE, 'command_desc_retrain')}") async def retrain(ctx): diff --git a/modules/globalvars.py b/modules/globalvars.py index 8d9ee07..7e3b7ce 100644 --- a/modules/globalvars.py +++ b/modules/globalvars.py @@ -40,5 +40,5 @@ song = os.getenv("song") arch = platform.machine() slash_commands_enabled = False latest_version = "0.0.0" -local_version = "0.15.3" +local_version = "0.15.4" os.environ['gooberlocal_version'] = local_version diff --git a/modules/prestartchecks.py b/modules/prestartchecks.py new file mode 100644 index 0000000..916e88e --- /dev/null +++ b/modules/prestartchecks.py @@ -0,0 +1,178 @@ +import time +import os +import psutil +import sys +import subprocess +import pkg_resources +import ast +import requests + +from modules.globalvars import * +from ping3 import ping + +def check_requirements(): + #making this made me cry + STD_LIB_MODULES = { + "os", "sys", "time", "ast", "asyncio", "re", "subprocess", "json", + "datetime", "threading", "math", "logging", "functools", "itertools", + "collections", "shutil", "socket", "types", "enum", "pathlib", + "inspect", "traceback", "platform", "typing", "warnings", "email", + "http", "urllib", "argparse", "io", "copy", "pickle", "gzip", "csv", + } + PACKAGE_ALIASES = { + "discord": "discord.py", + } + parent_dir = os.path.dirname(os.path.abspath(__file__)) + requirements_path = os.path.join(parent_dir, '..', 'requirements.txt') + requirements_path = os.path.abspath(requirements_path) + if not os.path.exists(requirements_path): + print(f"{RED}requirements.txt not found at {requirements_path} was it tampered with?{RESET}") + return + with open(requirements_path, 'r') as f: + lines = f.readlines() + requirements = { + line.strip() for line in lines + if line.strip() and not line.startswith('#') + } + cogs_dir = os.path.abspath(os.path.join(parent_dir, '..', 'cogs')) + if os.path.isdir(cogs_dir): + for filename in os.listdir(cogs_dir): + if filename.endswith('.py'): + filepath = os.path.join(cogs_dir, filename) + with open(filepath, 'r', encoding='utf-8') as f: + try: + tree = ast.parse(f.read(), filename=filename) + for node in ast.walk(tree): + if isinstance(node, ast.Import): + for alias in node.names: + pkg = alias.name.split('.')[0] + # ADD FILTER HERE + if pkg in STD_LIB_MODULES or pkg == 'modules': + continue + requirements.add(pkg) + elif isinstance(node, ast.ImportFrom): + if node.module: + pkg = node.module.split('.')[0] + # ADD FILTER HERE + if pkg in STD_LIB_MODULES or pkg == 'modules': + continue + requirements.add(pkg) + except Exception as e: + print(f"{YELLOW}Warning: Failed to parse imports from {filename}: {e}{RESET}") + else: + print(f"{YELLOW}Cogs directory not found at {cogs_dir}, skipping scan.{RESET}") + installed_packages = {pkg.key for pkg in pkg_resources.working_set} + missing = [] + for req in sorted(requirements): + if req in STD_LIB_MODULES or req == 'modules': + print(f"{GREEN}STD LIB / LOCAL{RESET} {req} (skipped check)") + continue + check_name = PACKAGE_ALIASES.get(req, req) + try: + pkg_resources.require(check_name) + print(f"[{GREEN} OK {RESET}] {check_name}") + except pkg_resources.DistributionNotFound: + print(f"[ {RED}MISSING{RESET} ] {check_name} is not installed") + missing.append(check_name) + except pkg_resources.VersionConflict as e: + print(f"[ {YELLOW}VERSION CONFLICT{RESET} ]{check_name} -> {e.report()}") + missing.append(check_name) + if missing: + print("\nMissing or conflicting packages detected:") + for pkg in missing: + print(f" - {pkg}") + print(f"Telling goober central at {VERSION_URL}") + payload = { + "name": NAME, + "version": local_version, + "slash_commands": f"{slash_commands_enabled}\n\n**Error**\nMissing packages have been detected, Failed to start", + "token": gooberTOKEN + } + # Send ping to server + response = requests.post(VERSION_URL+"/ping", json=payload) + sys.exit(1) + else: + print("\nAll requirements are satisfied.") + + +def check_latency(): + + host = "1.1.1.1" # change this to google later + latency = ping(host) + + if latency is not None: + print(f"Ping to {host}: {latency * 1000:.2f} ms") + if latency * 1000 > 300: + print(f"{YELLOW}High latency detected! You may experience delays in response times.{RESET}") + else: + print("Ping failed.") + +def check_memory(): + try: + memory_info = psutil.virtual_memory() + total_memory = memory_info.total / (1024 ** 3) + used_memory = memory_info.used / (1024 ** 3) + free_memory = memory_info.available / (1024 ** 3) + + print(f"Memory Usage: {used_memory:.2f} GB / {total_memory:.2f} GB ({(used_memory / total_memory) * 100:.2f}%)") + if used_memory > total_memory * 0.9: + print(f"{YELLOW}Memory usage is above 90% ({(used_memory / total_memory) * 100:.2f}%). Consider freeing up memory.{RESET}") + print(f"Total Memory: {total_memory:.2f} GB") + print(f"Used Memory: {used_memory:.2f} GB") + + if free_memory < 1: + print(f"{RED}Low free memory detected! Only {free_memory:.2f} GB available.{RESET}") + sys.exit(1) + except ImportError: + print("psutil is not installed. Memory check skipped.") + +def check_memoryjson(): + try: + print(f"Memory file: {os.path.getsize(MEMORY_FILE) / (1024 ** 2):.2f} MB") + if os.path.getsize(MEMORY_FILE) == 1_073_741_824: + print(f"{YELLOW}Memory file is 1GB, consider clearing it to free up space.{RESET}") + except FileNotFoundError: + print(f"{YELLOW}Memory file not found.{RESET}") + +def presskey2skip(timeout): + if os.name == 'nt': + import msvcrt + start_time = time.time() + while True: + if msvcrt.kbhit(): + msvcrt.getch() + break + if time.time() - start_time > timeout: + break + time.sleep(0.1) + else: + import select + import sys + import termios + import tty + + fd = sys.stdin.fileno() + old_settings = termios.tcgetattr(fd) + try: + tty.setcbreak(fd) + start_time = time.time() + while True: + if select.select([sys.stdin], [], [], 0)[0]: + sys.stdin.read(1) + break + if time.time() - start_time > timeout: + break + time.sleep(0.1) + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) + +def start_checks(): + print("Running pre-start checks...") + check_requirements() + check_latency() + check_memory() + check_memoryjson() + print("Continuing in 5 seconds... Press any key to skip.") + presskey2skip(5) + os.system('cls' if os.name == 'nt' else 'clear') + print(splashtext) \ No newline at end of file diff --git a/modules/unhandledexception.py b/modules/unhandledexception.py new file mode 100644 index 0000000..9c4ac84 --- /dev/null +++ b/modules/unhandledexception.py @@ -0,0 +1,23 @@ +import sys +import traceback +import os +from modules.globalvars import RED, RESET, splashtext + +def handle_exception(exc_type, exc_value, exc_traceback, *, context=None): + os.system('cls' if os.name == 'nt' else 'clear') + + if issubclass(exc_type, KeyboardInterrupt): + sys.__excepthook__(exc_type, exc_value, exc_traceback) + return + + print(splashtext) + print(f"{RED}=====BEGINNING OF TRACEBACK====={RESET}") + traceback.print_exception(exc_type, exc_value, exc_traceback) + print(f"{RED}========END OF TRACEBACK========{RESET}") + print(f"{RED}An unhandled exception occurred. Please report this issue on GitHub.{RESET}") + + if context: + print(f"{RED}Context: {context}{RESET}") + + + diff --git a/requirements.txt b/requirements.txt index 0da8506..48933cd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,5 @@ requests psutil better_profanity python-dotenv +ping3 +setuptools \ No newline at end of file