diff --git a/assets/cogs/fuckup.py b/assets/cogs/fuckup.py deleted file mode 100644 index 1dbc4d0..0000000 --- a/assets/cogs/fuckup.py +++ /dev/null @@ -1,98 +0,0 @@ -import discord -from discord.ext import commands -from modules.image import * -from modules.volta.main import _ -from PIL import Image, ImageEnhance, ImageFilter, ImageOps, ImageChops, ImageColor -import os, random, shutil, tempfile - -async def deepfryimage(path): - with Image.open(path).convert("RGB") as im: - # make it burn - for _ in range(3): - im = im.resize((int(im.width * 0.7), int(im.height * 0.7))) - im = im.resize((int(im.width * 1.5), int(im.height * 1.5))) - im = ImageEnhance.Contrast(im).enhance(random.uniform(5, 10)) - im = ImageEnhance.Sharpness(im).enhance(random.uniform(10, 50)) - im = ImageEnhance.Brightness(im).enhance(random.uniform(1.5, 3)) - r, g, b = im.split() - r = r.point(lambda i: min(255, i * random.uniform(1.2, 2.0))) - g = g.point(lambda i: min(255, i * random.uniform(0.5, 1.5))) - b = b.point(lambda i: min(255, i * random.uniform(0.5, 2.0))) - channels = [r, g, b] - random.shuffle(channels) - im = Image.merge("RGB", tuple(channels)) - overlay_color = tuple(random.randint(0, 255) for _ in range(3)) - overlay = Image.new("RGB", im.size, overlay_color) - im = ImageChops.add(im, overlay, scale=2.0, offset=random.randint(-64, 64)) - - im = im.filter(ImageFilter.EDGE_ENHANCE_MORE) - im = im.filter(ImageFilter.GaussianBlur(radius=random.uniform(0.5, 2))) - for _ in range(3): - tmp_path = tempfile.mktemp(suffix=".jpg") - im.save(tmp_path, format="JPEG", quality=random.randint(5, 15)) - im = Image.open(tmp_path) - if random.random() < 0.3: - im = ImageOps.posterize(im, bits=random.choice([2, 3, 4])) - if random.random() < 0.2: - im = ImageOps.invert(im) - out_path = tempfile.mktemp(suffix=".jpg") - im.save(out_path, format="JPEG", quality=5) - return out_path - - -class whami(commands.Cog): - def __init__(self, bot): - self.bot = bot - - - @commands.command() - async def fuckup(self, ctx): - assets_folder = "assets/images" - temp_input = None - - def get_random_asset_image(): - files = [f for f in os.listdir(assets_folder) if f.lower().endswith(('.png', '.jpg', '.jpeg', '.webp'))] - if not files: - return None - return os.path.join(assets_folder, random.choice(files)) - - if ctx.message.attachments: - attachment = ctx.message.attachments[0] - if attachment.content_type and attachment.content_type.startswith("image/"): - ext = os.path.splitext(attachment.filename)[1] - temp_input = f"tempy{ext}" - await attachment.save(temp_input) - input_path = temp_input - else: - fallback_image = get_random_asset_image() - if fallback_image is None: - await ctx.reply(_('no_image_available')) - return - temp_input = tempfile.mktemp(suffix=os.path.splitext(fallback_image)[1]) - shutil.copy(fallback_image, temp_input) - input_path = temp_input - else: - fallback_image = get_random_asset_image() - if fallback_image is None: - await ctx.reply(_('no_image_available')) - return - temp_input = tempfile.mktemp(suffix=os.path.splitext(fallback_image)[1]) - shutil.copy(fallback_image, temp_input) - input_path = temp_input - - output_path = await gen_meme(input_path) - - if output_path is None or not os.path.isfile(output_path): - if temp_input and os.path.exists(temp_input): - os.remove(temp_input) - await ctx.reply(_('failed_generate_image')) - return - - deepfried_path = await deepfryimage(output_path) - await ctx.send(file=discord.File(deepfried_path)) - - if temp_input and os.path.exists(temp_input): - os.remove(temp_input) - -async def setup(bot): - await bot.add_cog(whami(bot)) diff --git a/assets/cogs/lastfm.py.disabled b/assets/cogs/lastfm.py.disabled deleted file mode 100644 index 822305a..0000000 --- a/assets/cogs/lastfm.py.disabled +++ /dev/null @@ -1,84 +0,0 @@ -import os -import discord -from discord.ext import commands, tasks -import aiohttp -from dotenv import load_dotenv - -load_dotenv() - -#stole most of this code from my old expect bot so dont be suprised if its poorly made - -LASTFM_API_KEY = os.getenv("LASTFM_API_KEY") -LASTFM_USERNAME = os.getenv("LASTFM_USERNAME") - -class LastFmCog(commands.Cog): - def __init__(self, bot): - self.bot = bot - self.current_track = None - self.update_presence_task = None - self.ready = False - bot.loop.create_task(self.wait_until_ready()) - - async def wait_until_ready(self): - await self.bot.wait_until_ready() - self.ready = True - self.update_presence.start() - - @tasks.loop(seconds=60) - async def update_presence(self): - print("Looped!") - if not self.ready: - return - track = await self.fetch_current_track() - if track and track != self.current_track: - self.current_track = track - artist, song = track - activity_name = f"{artist} - {song}" - await self.bot.change_presence(activity=discord.Activity(type=discord.ActivityType.listening, name=activity_name)) - print(f"Updated song to {artist} - {song}") - else: - print("LastFM gave me the same track! not updating...") - - @update_presence.before_loop - async def before_update_presence(self): - await self.bot.wait_until_ready() - - @commands.command(name="lastfm") - async def lastfm_command(self, ctx): - track = await self.fetch_current_track() - if not track: - await ctx.send("No track currently playing or could not fetch data") - return - self.current_track = track - artist, song = track - activity_name = f"{artist} - {song}" - await self.bot.change_presence(activity=discord.Activity(type=discord.ActivityType.listening, name=activity_name)) - await ctx.send(f"Updated presence to: Listening to {activity_name}") - - async def fetch_current_track(self): - url = ( - f"http://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks" - f"&user={LASTFM_USERNAME}&api_key={LASTFM_API_KEY}&format=json&limit=1" - ) - async with aiohttp.ClientSession() as session: - async with session.get(url) as resp: - if resp.status != 200: - return None - data = await resp.json() - - recenttracks = data.get("recenttracks", {}).get("track", []) - if not recenttracks: - return None - - track = recenttracks[0] - if '@attr' in track and track['@attr'].get('nowplaying') == 'true': - artist = track.get('artist', {}).get('#text', 'Unknown Artist') - song = track.get('name', 'Unknown Song') - return artist, song - return None - -async def setup(bot): - if not LASTFM_API_KEY or not LASTFM_USERNAME: - return - else: - await bot.add_cog(LastFmCog(bot)) diff --git a/assets/cogs/songchanger.py b/assets/cogs/songchanger.py deleted file mode 100644 index 36fde47..0000000 --- a/assets/cogs/songchanger.py +++ /dev/null @@ -1,33 +0,0 @@ -import discord -from discord.ext import commands -from modules.globalvars import RED, GREEN, RESET, LOCAL_VERSION_FILE -import os - -class songchange(commands.Cog): - def __init__(self, bot): - self.bot = bot - - def get_local_version(): - if os.path.exists(LOCAL_VERSION_FILE): - with open(LOCAL_VERSION_FILE, "r") as f: - return f.read().strip() - return "0.0.0" - - global local_version - local_version = get_local_version() - - @commands.command() - async def changesong(self, ctx): - if LOCAL_VERSION_FILE > "0.11.8": - await ctx.send(f"Goober is too old! you must have version 0.11.8 you have {local_version}") - return - await ctx.send("Check the terminal! (this does not persist across restarts)") - song = input("\nEnter a song:\n") - try: - await self.bot.change_presence(activity=discord.Activity(type=discord.ActivityType.listening, name=f"{song}")) - print(f"{GREEN}Changed song to {song}{RESET}") - except Exception as e: - print(f"{RED}An error occurred while changing songs..: {str(e)}{RESET}") - -async def setup(bot): - await bot.add_cog(songchange(bot)) diff --git a/assets/cogs/webscraper.py.disabled b/assets/cogs/webscraper.py.disabled deleted file mode 100644 index 351e2e8..0000000 --- a/assets/cogs/webscraper.py.disabled +++ /dev/null @@ -1,113 +0,0 @@ -import discord -from discord.ext import commands -import aiohttp -from bs4 import BeautifulSoup -import json -import asyncio -from urllib.parse import urljoin -from modules.globalvars import ownerid -class WebScraper(commands.Cog): - def __init__(self, bot): - self.bot = bot - self.visited_urls = set() - - async def fetch(self, session, url): - """Fetch the HTML content of a URL.""" - try: - async with session.get(url, timeout=10) as response: - return await response.text() - except Exception as e: - print(f"Failed to fetch {url}: {e}") - return None - - def extract_sentences(self, text): - """Extract sentences from text.""" - sentences = text.split('.') - return [sentence.strip() for sentence in sentences if sentence.strip()] - - def save_to_json(self, sentences): - """Save sentences to memory.json.""" - try: - try: - with open("memory.json", "r") as file: - data = json.load(file) - except (FileNotFoundError, json.JSONDecodeError): - data = [] - data.extend(sentences) - with open("memory.json", "w") as file: - json.dump(data, file, indent=4) - except Exception as e: - print(f"Failed to save to JSON: {e}") - - def undo_last_scrape(self): - """Undo the last scrape by removing the most recent sentences.""" - try: - with open("memory.json", "r") as file: - data = json.load(file) - - if not data: - print("No data to undo.") - return False - - - data = data[:-1] - - with open("memory.json", "w") as file: - json.dump(data, file, indent=4) - - return True - except (FileNotFoundError, json.JSONDecodeError): - print("No data to undo or failed to load JSON.") - return False - except Exception as e: - print(f"Failed to undo last scrape: {e}") - return False - - async def scrape_links(self, session, url, depth=2): - print(f"Scraping: {url}") - self.visited_urls.add(url) - - html = await self.fetch(session, url) - if not html: - return - - soup = BeautifulSoup(html, "html.parser") - - for paragraph in soup.find_all('p'): - sentences = self.extract_sentences(paragraph.get_text()) - self.save_to_json(sentences) - - - @commands.command() - async def start_scrape(self, ctx, start_url: str): - """Command to start the scraping process.""" - if ctx.author.id != ownerid: - await ctx.send("You do not have permission to use this command.") - return - - if not start_url.startswith("http"): - await ctx.send("Please provide a valid URL.") - return - - await ctx.send(f"Starting scrape from {start_url}... This may take a while!") - - async with aiohttp.ClientSession() as session: - await self.scrape_links(session, start_url) - - await ctx.send("Scraping complete! Sentences saved to memory.json.") - - @commands.command() - async def undo_scrape(self, ctx): - """Command to undo the last scrape.""" - if ctx.author.id != ownerid: - await ctx.send("You do not have permission to use this command.") - return - - success = self.undo_last_scrape() - if success: - await ctx.send("Last scrape undone successfully.") - else: - await ctx.send("No data to undo or an error occurred.") - -async def setup(bot): - await bot.add_cog(WebScraper(bot)) diff --git a/assets/cogs/webserver.py b/assets/cogs/webserver.py deleted file mode 100644 index 110c20d..0000000 --- a/assets/cogs/webserver.py +++ /dev/null @@ -1,880 +0,0 @@ -import discord -from discord.ext import commands, tasks -import asyncio -from aiohttp import web -import psutil -import os -import json -from datetime import datetime -import time -import aiohttp -import re -from aiohttp import WSMsgType -from modules.globalvars import VERSION_URL -import sys -import subprocess - -class GooberWeb(commands.Cog): - def __init__(self, bot): - self.bot = bot - self.app = web.Application() - self.runner = None - self.site = None - self.last_command = "No commands executed yet" - self.last_command_time = "Never" - self.start_time = time.time() - self.websockets = set() - - self.app.add_routes([ - web.get('/', self.handle_index), - web.get('/changesong', self.handle_changesong), - web.get('/stats', self.handle_stats), - web.get('/data', self.handle_json_data), - web.get('/ws', self.handle_websocket), - web.get('/styles.css', self.handle_css), - web.get('/settings', self.handle_settings), - web.post('/update_settings', self.handle_update_settings), - web.post('/restart_bot', self.handle_restart_bot), - ]) - - self.bot.loop.create_task(self.start_web_server()) - self.update_clients.start() - - async def restart_bot(self): - await asyncio.sleep(1) - python = sys.executable - os.execl(python, python, *sys.argv) - - async def handle_restart_bot(self, request): - asyncio.create_task(self.restart_bot()) - return web.Response(text="Bot is restarting...") - - async def get_blacklisted_users(self): - blacklisted_ids = os.getenv("BLACKLISTED_USERS", "").split(",") - blacklisted_users = [] - - for user_id in blacklisted_ids: - if not user_id.strip(): - continue - - try: - user = await self.bot.fetch_user(int(user_id)) - blacklisted_users.append({ - "name": f"{user.name}", - "avatar_url": str(user.avatar.url) if user.avatar else str(user.default_avatar.url), - "id": user.id - }) - except discord.NotFound: - blacklisted_users.append({ - "name": f"Unknown User ({user_id})", - "avatar_url": "", - "id": user_id - }) - except discord.HTTPException as e: - print(f"Error fetching user {user_id}: {e}") - continue - - return blacklisted_users - - async def get_enhanced_guild_info(self): - guilds = sorted(self.bot.guilds, key=lambda g: g.member_count, reverse=True) - guild_info = [] - - for guild in guilds: - icon_url = str(guild.icon.url) if guild.icon else "" - guild_info.append({ - "name": guild.name, - "member_count": guild.member_count, - "icon_url": icon_url, - "id": guild.id - }) - - return guild_info - - async def start_web_server(self): - self.runner = web.AppRunner(self.app) - await self.runner.setup() - self.site = web.TCPSite(self.runner, '0.0.0.0', 8080) - await self.site.start() - print("Goober web server started on port 8080") - - async def stop_web_server(self): - await self.site.stop() - await self.runner.cleanup() - print("Web server stopped") - - def cog_unload(self): - self.update_clients.cancel() - self.bot.loop.create_task(self.stop_web_server()) - - @tasks.loop(seconds=5) - async def update_clients(self): - if not self.websockets: - return - - stats = await self.get_bot_stats() - message = json.dumps(stats) - - for ws in set(self.websockets): - try: - await ws.send_str(message) - except ConnectionResetError: - self.websockets.remove(ws) - except Exception as e: - print(f"Error sending to websocket: {e}") - self.websockets.remove(ws) - - async def handle_websocket(self, request): - ws = web.WebSocketResponse() - await ws.prepare(request) - self.websockets.add(ws) - - try: - async for msg in ws: - if msg.type == WSMsgType.ERROR: - print(f"WebSocket error: {ws.exception()}") - finally: - self.websockets.remove(ws) - - return ws - - async def handle_css(self, request): - css_path = os.path.join(os.path.dirname(__file__), 'styles.css') - if os.path.exists(css_path): - return web.FileResponse(css_path) - return web.Response(text="CSS file not found", status=404) - - @commands.Cog.listener() - async def on_message(self, message): - if message.author.bot: - return - - ctx = await self.bot.get_context(message) - if ctx.valid and ctx.command: - self._update_command_stats(ctx.command.name, ctx.author) - - @commands.Cog.listener() - async def on_app_command_completion(self, interaction, command): - self._update_command_stats(command.name, interaction.user) - - def _update_command_stats(self, command_name, user): - self.last_command = f"{command_name} (by {user.name})" - self.last_command_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - if self.websockets: - asyncio.create_task(self.update_clients()) - - async def get_bot_stats(self): - process = psutil.Process(os.getpid()) - mem_info = process.memory_full_info() - cpu_percent = psutil.cpu_percent() - process_cpu = process.cpu_percent() - - memory_json_size = "N/A" - if os.path.exists("memory.json"): - memory_json_size = f"{os.path.getsize('memory.json') / 1024:.2f} KB" - - guild_info = await self.get_enhanced_guild_info() - blacklisted_users = await self.get_blacklisted_users() - - uptime_seconds = int(time.time() - self.start_time) - uptime_str = f"{uptime_seconds // 86400}d {(uptime_seconds % 86400) // 3600}h {(uptime_seconds % 3600) // 60}m {uptime_seconds % 60}s" - - return { - "ram_usage": f"{mem_info.rss / 1024 / 1024:.2f} MB", - "cpu_usage": f"{process_cpu}%", - "system_cpu": f"{cpu_percent}%", - "memory_json_size": memory_json_size, - "guild_count": len(guild_info), - "bl_count": len(blacklisted_users), - "guilds": guild_info, - "blacklisted_users": blacklisted_users, - "last_command": self.last_command, - "last_command_time": self.last_command_time, - "bot_uptime": uptime_str, - "latency": f"{self.bot.latency * 1000:.2f} ms", - "bot_name": self.bot.user.name, - "bot_avatar_url": str(self.bot.user.avatar.url) if self.bot.user.avatar else "", - "authenticated": os.getenv("gooberauthenticated"), - "lastmsg": os.getenv("gooberlatestgen"), - "localversion": os.getenv("gooberlocal_version"), - "latestversion": os.getenv("gooberlatest_version"), - "owner": os.getenv("ownerid") - } - - async def handle_update(self, request): - if os.path.exists("goob/update.py"): - return web.FileResponse("goob/update.py") - return web.Response(text="Update file not found", status=404) - - async def handle_changesong(self, request): - song = request.query.get('song', '') - if song: - await self.bot.change_presence(activity=discord.Activity(type=discord.ActivityType.listening, name=song)) - return web.Response(text=f"Changed song to: {song}") - return web.Response(text="Please provide a song parameter", status=400) - - async def handle_changes(self, request): - if os.path.exists("goob/changes.txt"): - return web.FileResponse("goob/changes.txt") - return web.Response(text="Changelog not found", status=404) - - async def read_env_file(self): - env_vars = {} - try: - with open('.env', 'r') as f: - for line in f: - line = line.strip() - if not line or line.startswith('#') or '=' not in line: - continue - - key, value = line.split('=', 1) - key = key.strip() - if key in ['splashtext', 'DISCORD_BOT_TOKEN']: - continue - - env_vars[key] = value.strip('"\'') - except FileNotFoundError: - print(".env file not found") - return env_vars - - - async def handle_settings(self, request): - env_vars = await self.read_env_file() - - # Get config.py variables - config_vars = {} - try: - with open('config.py', 'r') as f: - for line in f: - if line.startswith('VERSION_URL'): - config_vars['VERSION_URL'] = line.split('=', 1)[1].strip().strip('"') - except FileNotFoundError: - pass - - settings_html = """ - - - - Goober Settings - - - -
-

Goober Settings

-
- """ - - for key, value in env_vars.items(): - settings_html += f""" -
- - -
- """ - - for key, value in config_vars.items(): - settings_html += f""" -
- - -
- """ - - settings_html += """ - -
-
- -
-
- - - """ - - return web.Response(text=settings_html, content_type='text/html') - - async def handle_update_settings(self, request): - data = await request.post() - env_text = "" - - try: - with open('.env', 'r') as f: - env_text = f.read() - except FileNotFoundError: - pass - - def replace_match(match): - key = match.group(1) - value = match.group(2) - if key in ['splashtext', 'DISCORD_BOT_TOKEN']: - return match.group(0) - if key in data: - new_value = data[key] - if not (new_value.startswith('"') and new_value.endswith('"')): - new_value = f'"{new_value}"' - return f'{key}={new_value}' - return match.group(0) - - env_text = re.sub(r'^(\w+)=([\s\S]+?)(?=\n\w+=|\Z)', replace_match, env_text, flags=re.MULTILINE) - - with open('.env', 'w') as f: - f.write(env_text.strip() + '\n') - - if 'VERSION_URL' in data: - config_text = "" - try: - with open('config.py', 'r') as f: - config_text = f.read() - except FileNotFoundError: - pass - - config_text = re.sub(r'^(VERSION_URL\s*=\s*").+?"', f'\\1{data["VERSION_URL"]}"', config_text, flags=re.MULTILINE) - - with open('config.py', 'w') as f: - f.write(config_text.strip() + '\n') - - return aiohttp.web.Response(text="Settings updated successfully!") - - async def handle_index(self, request): - stats = await self.get_bot_stats() - - guild_list_html = "" - for guild in stats['guilds']: - icon_html = f'guild icon' if guild["icon_url"] else '
' - guild_list_html += f""" -
- {icon_html} -
-
{guild["name"]}
-
{guild["member_count"]} members
-
-
- """ - blacklisted_users_html = "" - for user in stats['blacklisted_users']: - avatar_html = f'user avatar' if user["avatar_url"] else '
' - blacklisted_users_html += f""" -
- {avatar_html} -
-
{user["name"]}
-
ID: {user["id"]}
-
-
- """ - - owner_id = stats.get('owner') - owner = None - owner_username = "Owner" - owner_pfp = "" - - if owner_id: - try: - owner = await self.bot.fetch_user(int(owner_id)) - owner_username = f"{owner.name}" - owner_pfp = str(owner.avatar.url) if owner and owner.avatar else "" - except: - pass - - - html_content = f""" - - - - goobs central - - - -
- -
Welcome, {owner_username}
-
-
-
- RAM: - {stats['ram_usage']} -
-
- CPU: - {stats['system_cpu']} -
-
- Latency: - {stats['latency']} -
-
- JSON Size: - {stats['memory_json_size']} -
-
- Uptime: - {stats['bot_uptime']} -
-
- -
-
- botvatar -

{stats['bot_name']}

-
-
-

your stupid little goober that learns off other people's messages

-
- -
-
-
Last Command
-
{stats['last_command']}
-
at {stats['last_command_time']}
-
-
Logged into goober central
-
{stats['authenticated']}
-
-
Last generated message
-
{stats['lastmsg']}
-
-
Version
-
Installed Version: {stats['localversion']}
-
Latest Version: {stats['latestversion']}
-
-
goober-central URL
-
{VERSION_URL}
-
-
Change song
-
- - -
-
- -
-
Servers ({stats['guild_count']})
-
- {guild_list_html} -
-
-
Blacklisted Users ({stats['bl_count']})
-
- {blacklisted_users_html if stats['blacklisted_users'] else "
No blacklisted users
"} -
-
-
- - - - """ - - return web.Response(text=html_content, content_type='text/html') - - async def handle_stats(self, request): - return await self.handle_index(request) - - async def handle_json_data(self, request): - stats = await self.get_bot_stats() - return web.json_response(stats) - -async def setup(bot): - await bot.add_cog(GooberWeb(bot)) diff --git a/assets/fonts/TNR.ttf b/assets/fonts/TNR.ttf deleted file mode 100644 index 51261a0..0000000 Binary files a/assets/fonts/TNR.ttf and /dev/null differ diff --git a/bot.py b/main.py similarity index 78% rename from bot.py rename to main.py index bbee94d..6ad7ca1 100644 --- a/bot.py +++ b/main.py @@ -49,11 +49,9 @@ from modules.markovmemory import * from modules.version import * from modules.sentenceprocessing import * from modules.unhandledexception import handle_exception -from modules.image import gen_meme, gen_demotivator -from modules.minigames import guessthenumber, hangman +from modules.image import gen_meme sys.excepthook = handle_exception -check_for_update() # Check for updates (from modules/version.py) -# Type aliases +check_for_update() T = TypeVar('T') MessageContext = Union[commands.Context, discord.Interaction] MessageReference = Union[Message, discord.WebhookMessage] @@ -75,6 +73,17 @@ if not markov_model: generated_sentences: Set[str] = set() used_words: Set[str] = set() +def get_git_remote_url(): + try: + url = subprocess.check_output( + ["git", "config", "--get", "remote.origin.url"], + text=True, + stderr=subprocess.DEVNULL, + ).strip() + return url + except subprocess.CalledProcessError: + return "Unknown" + async def load_cogs_from_folder(bot, folder_name="assets/cogs"): for filename in os.listdir(folder_name): if filename.endswith(".py") and not filename.startswith("_"): @@ -87,14 +96,6 @@ async def load_cogs_from_folder(bot, folder_name="assets/cogs"): logger.error(f"{(_('cog_fail'))} {cog_name} {e}") traceback.print_exc() -async def send_alive_ping_periodically() -> None: - while True: - try: - requests.post(f"{VERSION_URL}/aliveping", json={"name": NAME}) - except Exception as e: - logger.error(f"{(_('error_sending_alive_ping'))}{RESET} {e}") - await asyncio.sleep(60) - # Event: Called when the bot is ready @bot.event async def on_ready() -> None: @@ -113,7 +114,6 @@ async def on_ready() -> None: logger.info(f"{_('synced_commands')} {len(synced)} {(_('synced_commands2'))}") slash_commands_enabled = True logger.info(f"{(_('started')).format(name=NAME)}") - bot.loop.create_task(send_alive_ping_periodically()) except discord.errors.Forbidden as perm_error: logger.error(f"Permission error while syncing commands: {perm_error}") logger.error("Make sure the bot has the 'applications.commands' scope and is invited with the correct permissions.") @@ -150,11 +150,11 @@ async def on_command_error(ctx: commands.Context, error: commands.CommandError) context=f"Command: {ctx.command} | User: {ctx.author}" ) -# Command: Retrain the Markov model from memory @bot.hybrid_command(description=f"{(_('command_desc_retrain'))}") async def retrain(ctx: commands.Context) -> None: if ctx.author.id != ownerid: return + global markov_model message_ref: MessageReference = await send_message(ctx, f"{(_('command_markov_retrain'))}") try: @@ -166,22 +166,13 @@ async def retrain(ctx: commands.Context) -> None: except json.JSONDecodeError: await send_message(ctx, f"{(_('command_markov_memory_is_corrupt'))}") return - data_size: int = len(memory) processed_data: int = 0 processing_message_ref: MessageReference = await send_message(ctx, f"{(_('command_markov_retraining')).format(processed_data=processed_data, data_size=data_size)}") - start_time: float = time.time() - - for i, data in enumerate(memory): - processed_data += 1 - - global markov_model markov_model = train_markov_model(memory) save_markov_model(markov_model) - await send_message(ctx, f"{_('command_markov_retrain_successful').format(data_size=data_size)}", edit=True, message_reference=processing_message_ref) -# Command: Generate a sentence using the Markov model @bot.hybrid_command(description=f"{(_('command_desc_talk'))}") async def talk(ctx: commands.Context, sentence_size: int = 5) -> None: if not markov_model: @@ -221,9 +212,8 @@ async def talk(ctx: commands.Context, sentence_size: int = 5) -> None: async def ramusage(ctx): process = psutil.Process(os.getpid()) mem = process.memory_info().rss - await send_message(ctx, f"Total memory used: {mem / 1024 / 1024:.2f} MB") + await send_message(ctx, f"{mem / 1024 / 1024:.2f} MB") -# Command: Generate an image @bot.hybrid_command(description=f"{(_('command_desc_help'))}") async def impact(ctx: commands.Context, text: Optional[str] = None) -> None: assets_folder: str = "assets/images" @@ -273,55 +263,6 @@ async def impact(ctx: commands.Context, text: Optional[str] = None) -> None: if temp_input and os.path.exists(temp_input): os.remove(temp_input) -# New demotivator command -@bot.hybrid_command(description="Generate a demotivator poster with two lines of text") -async def demotivator(ctx: commands.Context) -> None: - assets_folder: str = "assets/images" - temp_input: Optional[str] = None - - def get_random_asset_image() -> Optional[str]: - files: List[str] = [f for f in os.listdir(assets_folder) if f.lower().endswith(('.png', '.jpg', '.jpeg', '.webp'))] - if not files: - return None - return os.path.join(assets_folder, random.choice(files)) - - if ctx.message.attachments: - attachment: discord.Attachment = ctx.message.attachments[0] - if attachment.content_type and attachment.content_type.startswith("image/"): - ext: str = os.path.splitext(attachment.filename)[1] - temp_input = f"tempy{ext}" - await attachment.save(temp_input) - input_path: str = temp_input - else: - fallback_image: Optional[str] = get_random_asset_image() - if fallback_image is None: - await ctx.reply(_('no_image_available')) - return - temp_input = tempfile.mktemp(suffix=os.path.splitext(fallback_image)[1]) - shutil.copy(fallback_image, temp_input) - input_path = temp_input - else: - fallback_image = get_random_asset_image() - if fallback_image is None: - await ctx.reply(_('no_image_available')) - return - temp_input = tempfile.mktemp(suffix=os.path.splitext(fallback_image)[1]) - shutil.copy(fallback_image, temp_input) - input_path = temp_input - - output_path: Optional[str] = await gen_demotivator(input_path) - - if output_path is None or not os.path.isfile(output_path): - if temp_input and os.path.exists(temp_input): - os.remove(temp_input) - await ctx.reply("Failed to generate demotivator.") - return - - await ctx.send(file=discord.File(output_path)) - - if temp_input and os.path.exists(temp_input): - os.remove(temp_input) - bot.remove_command('help') # Command: Show help information @bot.hybrid_command(description=f"{(_('command_desc_help'))}") @@ -362,11 +303,9 @@ async def setlanguage(ctx: commands.Context, locale: str) -> None: set_language(locale) await ctx.send(":thumbsup:") -# Event: Called on every message @bot.event async def on_message(message: discord.Message) -> None: global memory, markov_model - EMOJIS = ["\U0001F604", "\U0001F44D", "\U0001F525", "\U0001F4AF", "\U0001F389", "\U0001F60E"] # originally was emojis but it would probably shit itself on systems without unicode so.... if message.author.bot: return @@ -383,38 +322,7 @@ async def on_message(message: discord.Message) -> None: return formatted_message: str = message.content cleaned_message: str = formatted_message - if cleaned_message: - memory.append(cleaned_message) - message_metadata = { - "user_id": str(message.author.id), - "user_name": str(message.author), - "guild_id": str(message.guild.id) if message.guild else "DM", - "guild_name": str(message.guild.name) if message.guild else "DM", - "channel_id": str(message.channel.id), - "channel_name": str(message.channel), - "message": message.content, - "timestamp": time.time() - } - try: - if isinstance(memory, list): - memory.append({"_meta": message_metadata}) - else: - logger.warning("Memory is not a list; can't append metadata") - except Exception as e: - logger.warning(f"Failed to append metadata to memory: {e}") - - save_memory(memory) - - sentiment_score = is_positive(message.content) # doesnt work but im scared to change the logic now please ignore - if sentiment_score > 0.8: - if REACT != "True": - return - emoji = random.choice(EMOJIS) - try: - await message.add_reaction(emoji) - except Exception as e: - logger.info(f"Failed to react with emoji: {e}") - + save_memory(memory) await bot.process_commands(message) @bot.event @@ -425,8 +333,6 @@ async def on_interaction(interaction: discord.Interaction) -> None: else: name = interaction.data['name'] logger.info(f"{(_('command_ran_s')).format(interaction=interaction)}{name}") - -# Global check: Block blacklisted users from running commands @bot.check async def block_blacklisted(ctx: commands.Context) -> bool: if str(ctx.author.id) in BLACKLISTED_USERS: @@ -442,8 +348,6 @@ async def block_blacklisted(ctx: commands.Context) -> bool: pass return False return True - -# Command: Show bot latency @bot.hybrid_command(description=f"{(_('command_desc_ping'))}") async def ping(ctx: commands.Context) -> None: await ctx.defer() @@ -460,18 +364,6 @@ async def ping(ctx: commands.Context) -> None: LOLembed.set_footer(text=f"{(_('command_ping_footer'))} {ctx.author.name}", icon_url=ctx.author.avatar.url) await ctx.send(embed=LOLembed) - -def get_git_remote_url(): - try: - url = subprocess.check_output( - ["git", "config", "--get", "remote.origin.url"], - text=True, - stderr=subprocess.DEVNULL, - ).strip() - return url - except subprocess.CalledProcessError: - return "Unknown" - # Command: Show about information @bot.hybrid_command(description=f"{(_('command_about_desc'))}") async def about(ctx: commands.Context) -> None: diff --git a/modules/globalvars.py b/modules/globalvars.py index ac37ed4..f7dcda4 100644 --- a/modules/globalvars.py +++ b/modules/globalvars.py @@ -56,7 +56,7 @@ latest_version = "0.0.0" local_version = "2.3.5" os.environ['gooberlocal_version'] = local_version REACT = os.getenv("REACT") -if get_git_branch() == "dev": +if get_git_branch() != "main": beta = True # this makes goober think its a beta version, so it will not update to the latest stable version or run any version checks else: diff --git a/modules/image.py b/modules/image.py index d3807df..9e9bc3c 100644 --- a/modules/image.py +++ b/modules/image.py @@ -12,10 +12,6 @@ generated_sentences = set() def load_font(size): return ImageFont.truetype("assets/fonts/Impact.ttf", size=size) - -def load_tnr(size): - return ImageFont.truetype("assets/fonts/TNR.ttf", size=size) - def draw_text_with_outline(draw, text, x, y, font): outline_offsets = [(-2, -2), (-2, 2), (2, -2), (2, 2), (0, -2), (0, 2), (-2, 0), (2, 0)] for ox, oy in outline_offsets: @@ -115,63 +111,3 @@ async def gen_meme(input_image_path, sentence_size=5, max_attempts=10, custom_te draw_text_with_outline(draw, truncated, (width - text_width) / 2, 0, font) img.save(input_image_path) return input_image_path - -async def gen_demotivator(input_image_path, max_attempts=5): - markov_model = load_markov_model() - if not markov_model or not os.path.isfile(input_image_path): - return None - - attempt = 0 - while attempt < max_attempts: - with Image.open(input_image_path).convert("RGB") as img: - size = max(img.width, img.height) - frame_thick = int(size * 0.0054) - inner_size = size - 2 * frame_thick - resized_img = img.resize((inner_size, inner_size), Image.LANCZOS) - framed = Image.new("RGB", (size, size), "white") - framed.paste(resized_img, (frame_thick, frame_thick)) - landscape_w = int(size * 1.5) - caption_h = int(size * 0.3) - canvas_h = framed.height + caption_h - canvas = Image.new("RGB", (landscape_w, canvas_h), "black") - # the above logic didnt even work, fml - fx = (landscape_w - framed.width) // 2 - canvas.paste(framed, (fx, 0)) - - draw = ImageDraw.Draw(canvas) - - title = subtitle = None - for _ in range(20): - t = markov_model.make_sentence(tries=100, max_words=4) - s = markov_model.make_sentence(tries=100, max_words=5) - if t and s and t != s: - title = t.upper() - subtitle = s.capitalize() - break - if not title: title = "DEMOTIVATOR" - if not subtitle: subtitle = "no text generated" - - title_sz = int(caption_h * 0.4) - sub_sz = int(caption_h * 0.25) - title_font = load_tnr(title_sz) - sub_font = load_tnr(sub_sz) - - bbox = draw.textbbox((0, 0), title, font=title_font) - txw, txh = bbox[2] - bbox[0], bbox[3] - bbox[1] - tx = (landscape_w - txw) // 2 - ty = framed.height + int(caption_h * 0.1) - draw_text_with_outline(draw, title, tx, ty, title_font) - - bbox = draw.textbbox((0, 0), subtitle, font=sub_font) - sxw, sxh = bbox[2] - bbox[0], bbox[3] - bbox[1] - sx = (landscape_w - sxw) // 2 - sy = ty + txh + int(caption_h * 0.05) - for ox, oy in [(-1, -1), (1, -1), (-1, 1), (1, 1)]: - draw.text((sx + ox, sy + oy), subtitle, font=sub_font, fill="black") - draw.text((sx, sy), subtitle, font=sub_font, fill="#AAAAAA") - - canvas.save(input_image_path) - return input_image_path - - attempt += 1 - return None diff --git a/modules/markovmemory.py b/modules/markovmemory.py index 3235035..5014c48 100644 --- a/modules/markovmemory.py +++ b/modules/markovmemory.py @@ -6,7 +6,7 @@ from modules.globalvars import * from modules.volta.main import _ import logging logger = logging.getLogger("goober") -# Get file size and line count for a given file path + def get_file_info(file_path): try: file_size = os.path.getsize(file_path) @@ -15,12 +15,8 @@ def get_file_info(file_path): return {"file_size_bytes": file_size, "line_count": len(lines)} except Exception as e: return {"error": str(e)} - -# Load memory data from file, or use default dataset if not loaded yet def load_memory(): data = [] - - # Try to load data from MEMORY_FILE try: with open(MEMORY_FILE, "r") as f: data = json.load(f) @@ -28,12 +24,9 @@ def load_memory(): pass return data - -# Save memory data to MEMORY_FILE def save_memory(memory): with open(MEMORY_FILE, "w") as f: json.dump(memory, f, indent=4) - def train_markov_model(memory, additional_data=None): if not memory: return None @@ -45,14 +38,9 @@ def train_markov_model(memory, additional_data=None): text = "\n".join(filtered_memory) model = markovify.NewlineText(text, state_size=2) return model - -# Save the Markov model to a pickle file def save_markov_model(model, filename='markov_model.pkl'): with open(filename, 'wb') as f: pickle.dump(model, f) - logger.info(f"Markov model saved to {filename}.") - -# Load the Markov model from a pickle file def load_markov_model(filename='markov_model.pkl'): try: with open(filename, 'rb') as f: diff --git a/modules/minigames.py b/modules/minigames.py deleted file mode 100644 index 0a74a82..0000000 --- a/modules/minigames.py +++ /dev/null @@ -1,71 +0,0 @@ -import random -import discord -from discord import ui, Interaction, TextStyle -from discord.ext import commands -import aiohttp -import asyncio -from modules.globalvars import bot -from modules.volta.main import _ - -# @bot.hybrid_command(description=_('minigames_guess_the_number')) -async def guessthenumber(ctx: commands.Context): - number = random.randint(1, 10) - class GuessModal(ui.Modal, title=_('minigames_guess_the_number')): - guess = ui.TextInput(label=_('minigames_your_guess'), style=TextStyle.short) - async def on_submit(self, interaction: Interaction): - try: - user_guess = int(self.guess.value) - except: - await interaction.response.send_message(_('minigames_invalid_number'), ephemeral=True) - return - if user_guess == number: - await interaction.response.send_message(_('minigames_correct'), ephemeral=True) - else: - await interaction.response.send_message(f"{_('minigames_wrong_number')} {number}.", ephemeral=True) - async def button_callback(interaction: Interaction): - await interaction.response.send_modal(GuessModal()) - button = ui.Button(label=_('minigames_guess_button'), style=discord.ButtonStyle.primary) - button.callback = button_callback - view = ui.View() - view.add_item(button) - await ctx.send(_('minigames_click_to_guess'), view=view) - -# @bot.hybrid_command(description=_('minigames_hangman')) nope nope nope fuck no nope no thanks no nuh uh not today nope -async def hangman(ctx: commands.Context): - async with aiohttp.ClientSession() as session: - async with session.get("https://random-word-api.herokuapp.com/word?number=1") as resp: - if resp.status != 200: - await ctx.send("Failed to get a random word.") - return - data = await resp.json() - word = data[0].lower() - print(word) - guessed_letters = set() - wrong_guesses = 0 - max_wrong = 6 - def display_word(): - return " ".join([c if c in guessed_letters else "_" for c in word]) - class GuessModal(ui.Modal, title=_('minigames_hangman_guess')): - letter = ui.TextInput(label=_('minigames_hangman_user_letter_guess'), style=TextStyle.short, max_length=1) - async def on_submit(self, interaction: Interaction): - nonlocal guessed_letters, wrong_guesses - guess = self.letter.value.lower() - if guess in guessed_letters: - await interaction.response.send_message(f"{_('minigames_hangman_already_guessed')}'{guess}'!", ephemeral=True) - return - guessed_letters.add(guess) - if guess not in word: - wrong_guesses += 1 - if all(c in guessed_letters for c in word): - await interaction.response.edit_message(content=f"{_('minigames_hangman_won')} **{word}**", view=None) - elif wrong_guesses >= max_wrong: - await interaction.response.edit_message(content=f"{_('minigames_hangman_lost')} **{word}**", view=None) - else: - await interaction.response.edit_message(content=_('minigames_hangman_game').format(display_word=display_word(),wrong_guesses=wrong_guesses,max_wrong=max_wrong), view=view) - async def button_callback(interaction: Interaction): - await interaction.response.send_modal(GuessModal()) - button = ui.Button(label=_('minigames_click_to_guess'), style=discord.ButtonStyle.primary) - button.callback = button_callback - view = ui.View() - view.add_item(button) - await ctx.send(_('minigames_hangman_game').format(display_word=display_word,wrong_guesses=wrong_guesses,max_wrong=max_wrong), view=view) \ No newline at end of file diff --git a/modules/prestartchecks.py b/modules/prestartchecks.py index bb952f4..29e155f 100644 --- a/modules/prestartchecks.py +++ b/modules/prestartchecks.py @@ -28,8 +28,7 @@ def check_for_model(): logger.info("Model is installed.") else: logger.info("Model is not installed.") - - + def iscloned(): if os.path.exists(".git"): return True diff --git a/modules/unhandledexception.py b/modules/unhandledexception.py index 9521e72..eac98e8 100644 --- a/modules/unhandledexception.py +++ b/modules/unhandledexception.py @@ -6,18 +6,14 @@ from modules.volta.main import _ 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}{_('unhandled_exception')}{RESET}") - - if context: print(f"{RED}Context: {context}{RESET}") diff --git a/modules/version.py b/modules/version.py index a5c1132..37e2264 100644 --- a/modules/version.py +++ b/modules/version.py @@ -21,7 +21,6 @@ def is_remote_ahead(branch='main', remote='origin'): count = run_cmd(f'git rev-list --count HEAD..{remote}/{branch}') return int(count) > 0 -# Automatically update the local repository if the remote is ahead def auto_update(branch='main', remote='origin'): if launched == True: print(_("already_started"))