Compare commits

..

7 commits

Author SHA1 Message Date
WhatDidYouExpect
d6b51c787a commit before utter laptop death 2025-07-23 16:58:21 +02:00
WhatDidYouExpect
4e111b410d mostly working idfk 2025-07-23 15:47:29 +02:00
WhatDidYouExpect
b0ba03f97d changed the last of expect's bots embed (may god rest his soul) to just pingembed 2025-07-23 13:38:54 +02:00
WhatDidYouExpect
10ab18bc51 cleared out the last of the image command 2025-07-23 13:38:09 +02:00
WhatDidYouExpect
b01bd5b80b this didnt fucking commit i fucking hate you git 2025-07-22 20:06:59 +02:00
WhatDidYouExpect
d300a3c812 fix later too tired 2025-07-22 20:03:11 +02:00
WhatDidYouExpect
8021d17d27 lobotmized goober and cleared out junk 2025-07-22 19:34:46 +02:00
39 changed files with 1082 additions and 2513 deletions

2
.gitignore vendored
View file

@ -13,3 +13,5 @@ translation_report.txt
translationcompleteness.py translationcompleteness.py
modules/volta modules/volta
log.txt log.txt
settings/settings.json
settings/splash.txt

View file

@ -1,5 +1,5 @@
knockoff of genai basically :p knockoff of genai basically :p
THIS!! IS THE ACTUAL REPO!!!! NOT THE OTHER ONE!!! THIS ONE!!!
Special thanks to [Charlie's Computers](https://github.com/PowerPCFan) for being the only one I know of that's hosting Goober 24/7 Special thanks to [Charlie's Computers](https://github.com/PowerPCFan) for being the only one I know of that's hosting Goober 24/7

View file

@ -1,25 +0,0 @@
# goobers custom commands
[Hello World!](https://github.com/WhatDidYouExpect/goober/blob/main/cogs/hello.py)
by expect
[WhoAmI (lists username and nickname)](https://github.com/WhatDidYouExpect/goober/blob/main/cogs/whoami.py)
by PowerPCFan
[Cog Manager](https://github.com/WhatDidYouExpect/goober/blob/main/cogs/cogmanager.py)
by expect
[Web Scraper](https://raw.githubusercontent.com/WhatDidYouExpect/goober/refs/heads/main/cogs/webscraper.py)
by expect (requires goober version 0.11.7.2 or higher)
[Status Changer](https://raw.githubusercontent.com/WhatDidYouExpect/goober/refs/heads/main/cogs/songchanger.py)
by expect (requires goober version 0.11.8 or higher)
[Status Changer](https://raw.githubusercontent.com/WhatDidYouExpect/goober/refs/heads/main/cogs/songchanger.py)
by expect (requires goober version 0.11.8 or higher)
[webUI](https://raw.githubusercontent.com/WhatDidYouExpect/goober/refs/heads/main/cogs/webserver.py)
by expect (requires goober version 0.11.8 or higher)
[LastFM](https://raw.githubusercontent.com/WhatDidYouExpect/goober/refs/heads/main/cogs/webserver.py)
by expect (no idea what version it needs i've only tried it on 1.0.3)
- you have to add LASTFM_USERNAME and LASTFM_API_KEY to your .env

View file

@ -1,20 +1,17 @@
import discord import discord
from discord.ext import commands from discord.ext import commands
from modules.globalvars import ownerid from modules.permission import requires_admin
class FileSync(commands.Cog): class FileSync(commands.Cog):
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
self.mode = None self.mode = None
self.peer_id = None self.peer_id = None
self.awaiting_file = False self.awaiting_file = False
@requires_admin()
@commands.command() @commands.command()
async def syncfile(self, ctx, mode: str, peer: discord.User): async def syncfile(self, ctx, mode: str, peer: discord.User):
self.mode = mode.lower() self.mode = mode.lower()
self.peer_id = peer.id self.peer_id = peer.id
if ctx.author.id != ownerid:
await ctx.send("You don't have permission to execute this command.")
return
if self.mode == "s": if self.mode == "s":
await ctx.send(f"<@{self.peer_id}> FILE_TRANSFER_REQUEST") await ctx.send(f"<@{self.peer_id}> FILE_TRANSFER_REQUEST")
await ctx.send(file=discord.File("memory.json")) await ctx.send(file=discord.File("memory.json"))

View file

@ -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))

View file

@ -0,0 +1,196 @@
import os
import platform
import subprocess
from typing import Dict, List
import discord
from discord import Colour
from discord.ext import commands
import discord.ext
import discord.ext.commands
from modules.globalvars import local_version
from modules.volta.main import _ , set_language
from modules.permission import requires_admin
from modules.sentenceprocessing import send_message
from modules.settings import instance as settings_manager
from modules.version import check_for_update
import requests
settings = settings_manager.settings
def get_git_origin_raw():
try:
result = subprocess.run(
["git", "config", "--get", "remote.origin.url"],
capture_output=True, text=True, check=True
)
return result.stdout.strip()
except Exception:
return "Failed to get git origin"
class BaseCommands(commands.Cog):
def __init__(self, bot):
self.bot: discord.ext.commands.Bot = bot
def get_git_origin_raw():
try:
result = subprocess.run(
["git", "config", "--get", "remote.origin.url"],
capture_output=True, text=True, check=True
)
return result.stdout.strip()
except Exception:
return "https://forgejo.expect.ovh/gooberinc/goober" # fallback if git fails
@commands.command()
async def help(self, ctx: commands.Context) -> None:
embed: discord.Embed = discord.Embed(
title=f"{_('command_help_embed_title')}",
description=f"{_('command_help_embed_desc')}",
color=discord.Colour(0x000000),
)
command_categories = {
f"{_('command_help_categories_general')}": [
"mem",
"talk",
"about",
"ping",
"impact",
"demotivator",
"help",
],
f"{_('command_help_categories_admin')}": ["stats", "retrain", "setlanguage"],
}
custom_commands: List[str] = []
for cog_name, cog in self.bot.cogs.items():
for command in cog.get_commands():
if (
command.name
not in command_categories[f"{_('command_help_categories_general')}"]
and command.name
not in command_categories[f"{_('command_help_categories_admin')}"]
):
custom_commands.append(command.name)
if custom_commands:
embed.add_field(
name=_('command_help_categories_custom'),
value="\n".join(
[
f"{settings['bot']['prefix']}{command}"
for command in custom_commands
]
),
inline=False,
)
for category, commands_list in command_categories.items():
commands_in_category: str = "\n".join(
[f"{settings['bot']['prefix']}{command}" for command in commands_list]
)
embed.add_field(name=category, value=commands_in_category, inline=False)
await send_message(ctx, embed=embed)
@requires_admin()
@commands.command()
async def setlanguage(self, ctx: commands.Context, locale: str) -> None:
await ctx.defer()
set_language(locale)
await ctx.send(":thumbsup:")
@commands.command()
async def ping(self, ctx: commands.Context) -> None:
await ctx.defer()
latency: int = round(self.bot.latency * 1000)
embed: discord.Embed = discord.Embed(
title="Pong!!",
description=(
settings["bot"]["misc"]["ping_line"],
f"`{_('command_ping_embed_desc')}: {latency}ms`\n",
),
color=discord.Colour(0x000000),
)
embed.set_footer(
text=f"{_('command_ping_footer')} {ctx.author.name}",
icon_url=ctx.author.display_avatar.url,
)
await ctx.send(embed=embed)
@commands.command()
async def about(self, ctx: commands.Context) -> None:
latest_version: str = check_for_update(slient=True)
embed: discord.Embed = discord.Embed(title=f"{(_('command_about_embed_title'))}", description="", color=Colour(0x000000))
embed.add_field(name=f"{(_('command_about_embed_field1'))}", value=f"{settings['name']}", inline=False)
embed.add_field(name=f"{(_('command_about_embed_field2name'))}", value=f"{(_('command_about_embed_field2value')).format(local_version=local_version, latest_version=latest_version)}", inline=False)
embed.add_field(name=f"Git", value=get_git_origin_raw())
embed.add_field(name=f"OS", value=platform.platform())
await send_message(ctx, embed=embed)
@commands.command()
async def stats(self, ctx: commands.Context) -> None:
memory_file: str = "memory.json"
file_size: int = os.path.getsize(memory_file)
with open(memory_file, "r") as file:
line_count: int = sum(1 for _ in file)
embed: discord.Embed = discord.Embed(
title=f"{_('command_stats_embed_title')}",
description=f"{_('command_stats_embed_desc')}",
color=discord.Colour(0x000000),
)
embed.add_field(
name=f"{_('command_stats_embed_field1name')}",
value=f"placeholder",
inline=False,
)
with open(settings["splash_text_loc"], "r") as f:
splash_text = "".join(f.readlines())
embed.add_field(
name=_('command_stats_embed_field3name'),
value=_('command_stats_embed_field3value').format(
NAME=settings["name"],
PREFIX=settings["bot"]["prefix"],
ownerid=settings["bot"]["owner_ids"][0],
PING_LINE=settings["bot"]["misc"]["ping_line"],
showmemenabled=settings["bot"]["allow_show_mem_command"],
USERTRAIN_ENABLED=settings["bot"]["user_training"],
song=settings["bot"]["misc"]["active_song"],
splashtext=splash_text
),
inline=False,
)
await send_message(ctx, embed=embed)
@commands.command()
async def mem(self, ctx: commands.Context) -> None:
if not settings["bot"]["allow_show_mem_command"]:
return
with open(settings["bot"]["active_memory"], "rb") as f:
data: bytes = f.read()
response = requests.post(
"https://litterbox.catbox.moe/resources/internals/api.php",
data={"reqtype": "fileupload", "time": "1h"},
files={"fileToUpload": data},
)
await send_message(ctx, response.text)
async def setup(bot: discord.ext.commands.Bot):
print("Setting up base_commands")
bot.remove_command("help")
await bot.add_cog(BaseCommands(bot))

View file

@ -1,18 +1,15 @@
import discord import discord
from discord.ext import commands from discord.ext import commands
from modules.globalvars import ownerid from modules.permission import requires_admin
COG_PREFIX = "assets.cogs." COG_PREFIX = "assets.cogs."
class CogManager(commands.Cog): class CogManager(commands.Cog):
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
@requires_admin()
@commands.command() @commands.command()
async def load(self, ctx, cog_name: str = None): async def load(self, ctx, cog_name: str = None):
if ctx.author.id != ownerid:
await ctx.send("You do not have permission to use this command.")
return
if cog_name is None: if cog_name is None:
await ctx.send("Please provide the cog name to load.") await ctx.send("Please provide the cog name to load.")
return return
@ -21,12 +18,9 @@ class CogManager(commands.Cog):
await ctx.send(f"Loaded cog `{cog_name}` successfully.") await ctx.send(f"Loaded cog `{cog_name}` successfully.")
except Exception as e: except Exception as e:
await ctx.send(f"Error loading cog `{cog_name}`: {e}") await ctx.send(f"Error loading cog `{cog_name}`: {e}")
@requires_admin()
@commands.command() @commands.command()
async def unload(self, ctx, cog_name: str = None): async def unload(self, ctx, cog_name: str = None):
if ctx.author.id != ownerid:
await ctx.send("You do not have permission to use this command.")
return
if cog_name is None: if cog_name is None:
await ctx.send("Please provide the cog name to unload.") await ctx.send("Please provide the cog name to unload.")
return return
@ -35,12 +29,9 @@ class CogManager(commands.Cog):
await ctx.send(f"Unloaded cog `{cog_name}` successfully.") await ctx.send(f"Unloaded cog `{cog_name}` successfully.")
except Exception as e: except Exception as e:
await ctx.send(f"Error unloading cog `{cog_name}`: {e}") await ctx.send(f"Error unloading cog `{cog_name}`: {e}")
@requires_admin()
@commands.command() @commands.command()
async def reload(self, ctx, cog_name: str = None): async def reload(self, ctx, cog_name: str = None):
if ctx.author.id != ownerid:
await ctx.send("You do not have permission to use this command.")
return
if cog_name is None: if cog_name is None:
await ctx.send("Please provide the cog name to reload.") await ctx.send("Please provide the cog name to reload.")
return return

View file

@ -0,0 +1,112 @@
import os
import random
import re
import discord
from discord.ext import commands
import discord.ext
import discord.ext.commands
from modules.markovmemory import save_markov_model, train_markov_model, load_markov_model
from modules.permission import requires_admin
from modules.sentenceprocessing import (
improve_sentence_coherence,
is_positive,
rephrase_for_coherence,
send_message,
)
from modules.volta.main import _
import logging
from typing import List, Optional, Set
import json
import time
import markovify
logger = logging.getLogger("goober")
from modules.settings import instance as settings_manager
settings = settings_manager.settings
class Markov(commands.Cog):
def __init__(self, bot):
self.bot: discord.ext.commands.Bot = bot
@requires_admin()
@commands.command()
async def retrain(self, ctx: discord.ext.commands.Context):
markov_model: Optional[markovify.Text] = load_markov_model()
message_ref: discord.Message | None = await send_message(
ctx, f"{_('command_markov_retrain')}"
)
if message_ref is None:
logger.error("Failed to send message!")
return
try:
with open(settings["bot"]["active_memory"], "r") as f:
memory: List[str] = json.load(f)
except FileNotFoundError:
await send_message(ctx, f"{_('command_markov_memory_not_found')}")
return
except json.JSONDecodeError:
await send_message(ctx, f"{_('command_markov_memory_is_corrupt')}")
return
data_size: int = len(memory)
processing_message_ref: discord.Message | None = await send_message(
ctx, f"{(_('command_markov_retraining').format(data_size=data_size))}"
)
if processing_message_ref is None:
logger.error("Couldnt find message processing message!")
start_time: float = time.time()
model = train_markov_model(memory)
if not model:
logger.error("Failed to train markov model")
await ctx.send("Failed to retrain!")
return False
markov_model = model
save_markov_model(markov_model)
logger.debug(f"Completed retraining in {round(time.time() - start_time,3)}s")
await send_message(
ctx,
_('command_markov_retraining').format(data_size=data_size),
edit=True,
message_reference=processing_message_ref,
)
@commands.command()
async def talk(self, ctx: commands.Context, sentence_size: int = 5) -> None:
markov_model: Optional[markovify.Text] = load_markov_model()
if markov_model is None:
await send_message(ctx, _("command_markovcommand_talk_insufficent_text"))
return
raw_sentence = None
if sentence_size == 1:
raw_sentence = markov_model.make_short_sentence(max_chars=100, tries=100)
else:
raw_sentence = markov_model.make_sentence(tries=100, max_words=sentence_size)
print(raw_sentence)
if random.random() < 0.9 and is_positive(raw_sentence):
gif_url = random.choice(settings["bot"]["misc"]["positive_gifs"])
raw_sentence = f"{raw_sentence}\n[jif]({gif_url})"
os.environ["gooberlatestgen"] = raw_sentence
await send_message(ctx, raw_sentence)
async def setup(bot):
await bot.add_cog(Markov(bot))

View file

@ -0,0 +1,89 @@
import discord
from discord.ext import commands
from modules.permission import requires_admin
from modules.settings import instance as settings_manager
settings = settings_manager.settings
class PermissionManager(commands.Cog):
def __init__(self, bot):
self.bot = bot
@requires_admin()
@commands.command()
async def add_owner(self, ctx: commands.Context, member: discord.Member):
settings["bot"]["owner_ids"].append(member.id)
settings_manager.commit()
embed = discord.Embed(
title="Permissions",
description=f"Set {member.name} as an owner",
color=discord.Color.blue(),
)
await ctx.send(embed=embed)
@requires_admin()
@commands.command()
async def remove_owner(self, ctx: commands.Context, member: discord.Member):
try:
settings["bot"]["owner_ids"].remove(member.id)
settings_manager.commit()
except ValueError:
await ctx.send("User is not an owner!")
return
embed = discord.Embed(
title="Permissions",
description=f"Removed {member.name} from being an owner",
color=discord.Color.blue(),
)
await ctx.send(embed=embed)
@requires_admin()
@commands.command()
async def blacklist_user(self, ctx: commands.Context, member: discord.Member):
settings["bot"]["blacklisted_users"].append(member.id)
settings_manager.add_admin_log_event(
{
"action": "add",
"author": ctx.author.id,
"change": "blacklisted_users",
"messageId": ctx.message.id,
"target": member.id,
}
)
settings_manager.commit()
embed = discord.Embed(
title="Blacklist",
description=f"Added {member.name} to the blacklist",
color=discord.Color.blue(),
)
await ctx.send(embed=embed)
@requires_admin()
@commands.command()
async def unblacklist_user(self, ctx: commands.Context, member: discord.Member):
try:
settings["bot"]["blacklisted_users"].remove(member.id)
settings_manager.commit()
except ValueError:
await ctx.send("User is not on the blacklist!")
return
embed = discord.Embed(
title="Blacklist",
description=f"Removed {member.name} from blacklist",
color=discord.Color.blue(),
)
await ctx.send(embed=embed)
async def setup(bot):
await bot.add_cog(PermissionManager(bot))

View file

@ -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))

View file

@ -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))

View file

@ -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))

View file

@ -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 = """
<!DOCTYPE html>
<html>
<head>
<title>Goober Settings</title>
<style>
body { background-color: #121212; color: #ffffff; font-family: 'Segoe UI', sans-serif; }
h1 { color: #ff5555; text-align: center; }
.settings-container { max-width: 800px; margin: auto; background-color: #1e1e1e; padding: 20px; border-radius: 8px; }
.form-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; color: #ff9999; }
input { width: 100%; padding: 8px; background-color: #252525; color: white; border: 1px solid #444; border-radius: 4px; }
button { background-color: #5f1b1b; color: white; border: none; padding: 10px; border-radius: 4px; cursor: pointer; }
button:hover { background-color: #7a2323; }
</style>
</head>
<body>
<div class='settings-container'>
<h1>Goober Settings</h1>
<form id='settingsForm' action='/update_settings' method='post'>
"""
for key, value in env_vars.items():
settings_html += f"""
<div class='form-group'>
<label for='{key}'>{key}</label>
<input type='text' id='{key}' name='{key}' value='{value}'>
</div>
"""
for key, value in config_vars.items():
settings_html += f"""
<div class='form-group'>
<label for='{key}'>{key}</label>
<input type='text' id='{key}' name='{key}' value='{value}'>
</div>
"""
settings_html += """
<button type='submit'>Save Settings</button>
</form>
<form action="/restart_bot" method="POST">
<button type="submit">Restart</button>
</form>
</div>
</body>
</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'<img src="{guild["icon_url"]}" alt="guild icon" class="guild-icon">' if guild["icon_url"] else '<div class="guild-icon-placeholder"></div>'
guild_list_html += f"""
<div class="guild-item">
{icon_html}
<div class="guild-info">
<div class="guild-name">{guild["name"]}</div>
<div class="guild-members">{guild["member_count"]} members</div>
</div>
</div>
"""
blacklisted_users_html = ""
for user in stats['blacklisted_users']:
avatar_html = f'<img src="{user["avatar_url"]}" alt="user avatar" class="user-avatar">' if user["avatar_url"] else '<div class="user-avatar-placeholder"></div>'
blacklisted_users_html += f"""
<div class="blacklisted-user">
{avatar_html}
<div class="user-info">
<div class="user-name">{user["name"]}</div>
<div class="user-id">ID: {user["id"]}</div>
</div>
</div>
"""
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"""
<!DOCTYPE html>
<html>
<head>
<title>goobs central</title>
<style>
#loading-screen {{
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #000;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 9999;
transition: opacity 1.5s ease-out;
}}
#loading-screen.fade-out {{
opacity: 0;
pointer-events: none;
}}
#welcome-message {{
color: #fff;
font-size: 2em;
margin-bottom: 20px;
text-align: center;
text-shadow: 0 0 10px #ff5555;
}}
#owner-avatar {{
width: 100px;
height: 100px;
border-radius: 50%;
object-fit: cover;
border: 3px solid #5f1b1b;
box-shadow: 0 0 20px #ff5555;
}}
body {{
background-color: #121212;
color: #ffffff;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 0;
line-height: 1.6;
}}
.topnav {{
background-color: #2a0a0a;
overflow: hidden;
display: flex;
flex-wrap: wrap;
justify-content: center;
padding: 10px;
gap: 15px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
}}
.stat-item {{
gap: 5px;
color: white;
font-size: 14px;
background-color: #1a1a1a;
padding: 8px 15px;
border-radius: 6px;
align-items: center;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
position: relative;
transition: all 0.3s ease;
}}
.stat-item:hover {{
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4);
}}
.stat-item::after {{
content: '';
position: absolute;
bottom: -5px;
left: 50%;
transform: translateX(-50%);
width: 60%;
height: 3px;
background: linear-gradient(90deg, transparent, #ff5555, transparent);
opacity: 0;
transition: opacity 0.3s ease;
}}
.stat-item:hover::after {{
opacity: 1;
}}
.stat-title {{
font-weight: bold;
color: #ff9999;
}}
.stat-item span:not(.stat-title) {{
font-weight: bold;
color: #ffffff;
}}
.center {{
text-align: center;
max-width: 800px;
margin: 20px auto;
padding: 0 20px;
}}
.bot-info {{
display: flex;
align-items: center;
justify-content: center;
gap: 15px;
margin-bottom: 10px;
}}
.bot-avatar {{
width: 80px;
height: 80px;
border-radius: 50%;
border: 3px solid #5f1b1b;
object-fit: cover;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
}}
hr {{
border: 0;
height: 1px;
background-image: linear-gradient(to right, transparent, #5f1b1b, transparent);
margin: 20px 0;
}}
.stat-container-row {{
display: flex;
justify-content: space-between;
gap: 30px;
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}}
.stat-container {{
flex: 1;
background-color: #1e1e1e;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
min-width: 0;
}}
.stat-title {{
color: #ff5555;
font-size: 1.1em;
margin-bottom: 10px;
border-bottom: 1px solid #333;
padding-bottom: 5px;
}}
.guild-item {{
display: flex;
align-items: center;
gap: 15px;
padding: 10px;
margin: 5px 0;
background-color: #252525;
border-radius: 5px;
transition: background-color 0.2s;
}}
.guild-item:hover {{
background-color: #333;
}}
.guild-icon {{
width: 48px;
height: 48px;
border-radius: 50%;
object-fit: cover;
}}
.guild-icon-placeholder {{
width: 48px;
height: 48px;
border-radius: 50%;
background-color: #7289da;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
}}
.guild-info {{
display: flex;
flex-direction: column;
flex-grow: 1;
min-width: 0;
}}
.guild-name {{
font-weight: bold;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}}
.guild-members {{
font-size: 0.8em;
color: #99aab5;
}}
.blacklisted-user {{
display: flex;
align-items: center;
gap: 15px;
padding: 10px;
margin: 5px 0;
background-color: #2a1a1a;
border-radius: 5px;
border-left: 3px solid #ff5555;
}}
.user-avatar {{
width: 48px;
height: 48px;
border-radius: 50%;
object-fit: cover;
}}
.user-avatar-placeholder {{
width: 48px;
height: 48px;
border-radius: 50%;
background-color: #7289da;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
}}
.user-info {{
display: flex;
flex-direction: column;
}}
.user-name {{
font-weight: bold;
color: #ff5555;
}}
.user-id {{
font-size: 0.8em;
color: #99aab5;
}}
input[type="text"] {{
background-color: #252525;
color: white;
border: 1px solid #444;
padding: 8px;
border-radius: 4px;
width: 200px;
margin-right: 10px;
}}
button {{
background-color: #5f1b1b;
color: white;
border: none;
padding: 8px 15px;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
}}
button:hover {{
background-color: #7a2323;
}}
#guild-list, #blacklisted-users {{
max-height: 400px;
overflow-y: auto;
padding-right: 5px;
}}
#guild-list::-webkit-scrollbar, #blacklisted-users::-webkit-scrollbar {{
width: 6px;
}}
#guild-list::-webkit-scrollbar-track, #blacklisted-users::-webkit-scrollbar-track {{
background: #1a1a1a;
}}
#guild-list::-webkit-scrollbar-thumb, #blacklisted-users::-webkit-scrollbar-thumb {{
background-color: #5f1b1b;
border-radius: 3px;
}}
@media (max-width: 768px) {{
.stat-container-row {{
flex-direction: column;
}}
.topnav {{
gap: 10px;
padding: 10px 5px;
}}
.stat-item {{
font-size: 12px;
padding: 8px 12px;
}}
}}
</style>
</head>
<body>
<div id="loading-screen">
<img id="owner-avatar" src="{owner_pfp}" onerror="this.style.display='none'">
<div id="welcome-message"><b>Welcome, {owner_username}</b></div>
</div>
<div class="topnav">
<div class="stat-item" id="ram-usage">
<span class="stat-title">RAM:</span>
<span>{stats['ram_usage']}</span>
</div>
<div class="stat-item" id="system-cpu">
<span class="stat-title">CPU:</span>
<span>{stats['system_cpu']}</span>
</div>
<div class="stat-item" id="latency">
<span class="stat-title">Latency:</span>
<span>{stats['latency']}</span>
</div>
<div class="stat-item" id="json-size">
<span class="stat-title">JSON Size:</span>
<span>{stats['memory_json_size']}</span>
</div>
<div class="stat-item" id="uptime">
<span class="stat-title">Uptime:</span>
<span>{stats['bot_uptime']}</span>
</div>
</div>
<div class="center">
<div class="bot-info">
<img src="{stats['bot_avatar_url']}" alt="botvatar" class="bot-avatar" id="bot-avatar">
<h1 id="bot-name">{stats['bot_name']}</h1>
</div>
<hr>
<p>your stupid little goober that learns off other people's messages</p>
</div>
<div class="stat-container-row">
<div class="stat-container">
<div class="stat-title">Last Command</div>
<div id="last-command">{stats['last_command']}</div>
<div style="font-size: 0.9em; color: #999;" id="last-command-time">at {stats['last_command_time']}</div>
<br>
<div class="stat-title">Logged into goober central</div>
<div id="last-command">{stats['authenticated']}</div>
<br>
<div class="stat-title">Last generated message</div>
<div id="last-command">{stats['lastmsg']}</div>
<br>
<div class="stat-title">Version</div>
<div id="last-command">Installed Version: {stats['localversion']}</div>
<div id="last-command">Latest Version: {stats['latestversion']}</div>
<br>
<div class="stat-title">goober-central URL</div>
<div id="last-command">{VERSION_URL}</div>
<br>
<div class="stat-title">Change song</div>
<form action="/changesong" method="get">
<input type="text" name="song" placeholder="Enter song name...">
<button type="submit">
change song
</button>
</form>
</div>
<div class="stat-container">
<div class="stat-title">Servers (<span id="guild-count">{stats['guild_count']}</span>)</div>
<div id="guild-list">
{guild_list_html}
</div>
<br>
<div class="stat-title">Blacklisted Users (<span id="guild-count">{stats['bl_count']})</div>
<div id="blacklisted-users">
{blacklisted_users_html if stats['blacklisted_users'] else "<div>No blacklisted users</div>"}
</div>
</div>
</div>
<script>
window.addEventListener('load', function() {{
setTimeout(function() {{
const loadingScreen = document.getElementById('loading-screen');
loadingScreen.classList.add('fade-out');
setTimeout(function() {{
loadingScreen.remove();
}}, 1500);
}}, 1500);
}});
const ws = new WebSocket('ws://' + window.location.host + '/ws');
ws.onmessage = function(event) {{
const data = JSON.parse(event.data);
document.getElementById('ram-usage').innerHTML = `<span class="stat-title">RAM:</span> <span>${{data.ram_usage}}</span>`;
document.getElementById('cpu-usage').innerHTML = `<span class="stat-title">CPU:</span> <span>${{data.cpu_usage}}</span>`;
document.getElementById('system-cpu').innerHTML = `<span class="stat-title">System CPU:</span> <span>${{data.system_cpu}}</span>`;
document.getElementById('latency').innerHTML = `<span class="stat-title">Latency:</span> <span>${{data.latency}}</span>`;
document.getElementById('json-size').innerHTML = `<span class="stat-title">JSON Size:</span> <span>${{data.memory_json_size}}</span>`;
document.getElementById('uptime').innerHTML = `<span class="stat-title">Uptime:</span> <span>${{data.bot_uptime}}</span>`;
document.getElementById('bot-name').textContent = data.bot_name;
const botAvatar = document.getElementById('bot-avatar');
if (botAvatar.src !== data.bot_avatar_url) {{
botAvatar.src = data.bot_avatar_url;
}}
document.getElementById('last-command').textContent = data.last_command;
document.getElementById('last-command-time').textContent = `at ${{data.last_command_time}}`;
document.getElementById('guild-count').textContent = data.guild_count;
let guildListHtml = '';
data.guilds.forEach(guild => {{
const iconHtml = guild.icon_url
? `<img src="${{guild.icon_url}}" alt="guild icon" class="guild-icon">`
: '<div class="guild-icon-placeholder"></div>';
guildListHtml += `
<div class="guild-item">
${{iconHtml}}
<div class="guild-info">
<div class="guild-name">${{guild.name}}</div>
<div class="guild-members">${{guild.member_count}} members</div>
</div>
</div>
`;
}});
document.getElementById('guild-list').innerHTML = guildListHtml;
let blacklistedUsersHtml = '';
if (data.blacklisted_users && data.blacklisted_users.length > 0) {{
data.blacklisted_users.forEach(user => {{
const avatarHtml = user.avatar_url
? `<img src="${{user.avatar_url}}" alt="user avatar" class="user-avatar">`
: '<div class="user-avatar-placeholder"></div>';
blacklistedUsersHtml += `
<div class="blacklisted-user">
${{avatarHtml}}
<div class="user-info">
<div class="user-name">${{user.name}}</div>
<div class="user-id">ID: ${{user.id}}</div>
</div>
</div>
`;
}});
}} else {{
blacklistedUsersHtml = '<div>No blacklisted users</div>';
}}
document.getElementById('blacklisted-users').innerHTML = blacklistedUsersHtml;
}};
ws.onclose = function() {{
console.log('WebSocket disconnected');
}};
</script>
</body>
</html>
"""
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))

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 746 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 355 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

528
bot.py
View file

@ -1,528 +0,0 @@
import os
import re
import json
import time
import random
import traceback
import subprocess
import tempfile
import shutil
import psutil
import asyncio
import platform
import sys
from typing import List, Dict, Set, Optional, Tuple, Any, Union, Callable, Coroutine, TypeVar, Type
import logging
from modules.globalvars import *
from modules.prestartchecks import start_checks
from modules.logger import GooberFormatter
import logging
logger = logging.getLogger("goober")
logger.setLevel(logging.DEBUG)
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.DEBUG)
console_handler.setFormatter(GooberFormatter())
file_handler = logging.FileHandler("log.txt", mode="w+", encoding="UTF-8")
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(GooberFormatter(colors=False))
logger.addHandler(console_handler)
logger.addHandler(file_handler)
# Print splash text and check for updates
print(splashtext) # Print splash text (from modules/globalvars.py)
start_checks()
import requests
import discord
from discord.ext import commands
from discord import app_commands
from discord import Colour, Embed, File, Interaction, Message
from discord.abc import Messageable
from discord.ext import commands
from modules.volta.main import _, set_language
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
sys.excepthook = handle_exception
check_for_update() # Check for updates (from modules/version.py)
# Type aliases
T = TypeVar('T')
MessageContext = Union[commands.Context, discord.Interaction]
MessageReference = Union[Message, discord.WebhookMessage]
# Constants with type hints
positive_gifs: List[str] = os.getenv("POSITIVE_GIFS", "").split(',')
currenthash: str = ""
launched: bool = False
slash_commands_enabled: bool = False
# Load memory and Markov model for text generation
memory: List[str] = load_memory()
markov_model: Optional[markovify.Text] = load_markov_model()
if not markov_model:
logger.error(_('markov_model_not_found'))
memory = load_memory()
markov_model = train_markov_model(memory)
generated_sentences: Set[str] = set()
used_words: Set[str] = set()
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("_"):
cog_name = filename[:-3]
module_path = folder_name.replace("/", ".").replace("\\", ".") + f".{cog_name}"
try:
await bot.load_extension(module_path)
logger.info(f"{(_('loaded_cog'))} {cog_name}")
except Exception as e:
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:
global launched
global slash_commands_enabled
global NAME
global status
folder_name: str = "cogs"
if launched:
return
await load_cogs_from_folder(bot)
try:
synced: List[discord.app_commands.AppCommand] = await bot.tree.sync()
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.")
quit()
except Exception as e:
logger.error(f"{_('fail_commands_sync')} {e}")
traceback.print_exc()
quit()
if not song:
return
status = {
"idle": discord.Status.idle,
"dnd": discord.Status.dnd,
"invisible": discord.Status.invisible,
"online": discord.Status.online
}.get(status.lower(), discord.Status.online)
await bot.change_presence(status=status, activity=discord.Activity(type=discord.ActivityType.listening, name=f"{song}"))
launched = True
@bot.event
async def on_command_error(ctx: commands.Context, error: commands.CommandError) -> None:
from modules.unhandledexception import handle_exception
if isinstance(error, commands.CommandInvokeError):
original: Exception = 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"{(_('command_desc_retrain'))}")
async def retrain(ctx: commands.Context) -> None:
if ctx.author.id != ownerid:
return
message_ref: MessageReference = await send_message(ctx, f"{(_('command_markov_retrain'))}")
try:
with open(MEMORY_FILE, 'r') as f:
memory: List[str] = json.load(f)
except FileNotFoundError:
await send_message(ctx, f"{(_('command_markov_memory_not_found'))}")
return
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:
await send_message(ctx, f"{(_('command_talk_insufficent_text'))}")
return
response: Optional[str] = None
for _ in range(20):
if sentence_size == 1:
response = markov_model.make_short_sentence(max_chars=100, tries=100)
if response:
response = response.split()[0]
else:
response = markov_model.make_sentence(tries=100, max_words=sentence_size)
if response and response not in generated_sentences:
if sentence_size > 1:
response = improve_sentence_coherence(response)
generated_sentences.add(response)
break
if response:
cleaned_response: str = re.sub(r'[^\w\s]', '', response).lower()
coherent_response: str = rephrase_for_coherence(cleaned_response)
if random.random() < 0.9 and is_positive(coherent_response):
gif_url: str = random.choice(positive_gifs)
combined_message: str = f"{coherent_response}\n[jif]({gif_url})"
else:
combined_message: str = coherent_response
logger.info(combined_message)
os.environ['gooberlatestgen'] = combined_message
await send_message(ctx, combined_message)
else:
await send_message(ctx, f"{(_('command_talk_generation_fail'))}")
@bot.hybrid_command(description=f"RAM")
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")
# 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"
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_meme(input_path, custom_text=text)
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
await ctx.send(file=discord.File(output_path))
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'))}")
async def help(ctx: commands.Context) -> None:
embed: discord.Embed = discord.Embed(
title=f"{(_('command_help_embed_title'))}",
description=f"{(_('command_help_embed_desc'))}",
color=Colour(0x000000)
)
command_categories: Dict[str, List[str]] = {
f"{(_('command_help_categories_general'))}": ["mem", "talk", "about", "ping", "impact", "demotivator", "help"],
f"{(_('command_help_categories_admin'))}": ["stats", "retrain", "setlanguage"]
}
custom_commands: List[str] = []
for cog_name, cog in bot.cogs.items():
for command in cog.get_commands():
if command.name not in command_categories[f"{(_('command_help_categories_general'))}"] and command.name not in command_categories[f"{(_('command_help_categories_admin'))}"]:
custom_commands.append(command.name)
if custom_commands:
embed.add_field(name=f"{(_('command_help_categories_custom'))}", value="\n".join([f"{PREFIX}{command}" for command in custom_commands]), inline=False)
for category, commands_list in command_categories.items():
commands_in_category: str = "\n".join([f"{PREFIX}{command}" for command in commands_list])
embed.add_field(name=category, value=commands_in_category, inline=False)
await send_message(ctx, embed=embed)
@bot.hybrid_command(description=f"{(_('command_desc_setlang'))}")
@app_commands.describe(locale="Choose your language")
async def setlanguage(ctx: commands.Context, locale: str) -> None:
if ctx.author.id != ownerid:
await ctx.send(":thumbsdown:")
return
await ctx.defer()
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
if str(message.author.id) in BLACKLISTED_USERS:
return
if message.content.startswith((f"{PREFIX}talk", f"{PREFIX}mem", f"{PREFIX}help", f"{PREFIX}stats", f"{PREFIX}")):
logger.info(f"{(_('command_ran')).format(message=message)}")
await bot.process_commands(message)
return
if message.content:
if not USERTRAIN_ENABLED:
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}")
await bot.process_commands(message)
@bot.event
async def on_interaction(interaction: discord.Interaction) -> None:
name = None
if interaction.data.get('name') is None:
name = "Unknown"
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:
try:
if isinstance(ctx, discord.Interaction):
if not ctx.response.is_done():
await ctx.response.send_message(_('blacklisted'), ephemeral=True)
else:
await ctx.followup.send(_('blacklisted'), ephemeral=True)
else:
await ctx.send(_('blacklisted_user'), ephemeral=True)
except:
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()
latency: int = round(bot.latency * 1000)
LOLembed: discord.Embed = discord.Embed(
title="Pong!!",
description=(
f"{PING_LINE}\n"
f"`{(_('command_ping_embed_desc'))}: {latency}ms`\n"
),
color=Colour(0x000000)
)
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:
print("-----------------------------------\n\n")
latest_version: str = check_for_update()
print("-----------------------------------")
embed: discord.Embed = discord.Embed(title=f"{(_('command_about_embed_title'))}", description="", color=Colour(0x000000))
embed.add_field(name=f"{(_('command_about_embed_field1'))}", value=f"{NAME}", inline=False)
embed.add_field(name=f"{(_('command_about_embed_field2name'))}", value=f"{(_('command_about_embed_field2value')).format(local_version=local_version, latest_version=latest_version)}", inline=False)
embed.add_field(name=f"Git", value=get_git_remote_url())
embed.add_field(name=f"OS", value=platform.platform())
await send_message(ctx, embed=embed)
# Command: Show bot statistics (admin only)
@bot.hybrid_command(description="stats")
async def stats(ctx: commands.Context) -> None:
if ctx.author.id != ownerid:
return
print("-----------------------------------\n\n")
latest_version: str = check_for_update()
print("-----------------------------------")
memory_file: str = 'memory.json'
file_size: int = os.path.getsize(memory_file)
with open(memory_file, 'r') as file:
line_count: int = sum(1 for _ in file)
embed: discord.Embed = discord.Embed(title=f"{(_('command_stats_embed_title'))}", description=f"{(_('command_stats_embed_desc'))}", color=Colour(0x000000))
embed.add_field(name=f"{(_('command_stats_embed_field1name'))}", value=f"{(_('command_stats_embed_field1value')).format(file_size=file_size, line_count=line_count)}", inline=False)
embed.add_field(name=f"{(_('command_stats_embed_field2name'))}", value=f"{(_('command_stats_embed_field2value')).format(local_version=local_version, latest_version=latest_version)}", inline=False)
embed.add_field(name=f"{(_('command_stats_embed_field3name'))}", value=f"{(_('command_stats_embed_field3value')).format(NAME=NAME, PREFIX=PREFIX, ownerid=ownerid, PING_LINE=PING_LINE, showmemenabled=showmemenabled, USERTRAIN_ENABLED=USERTRAIN_ENABLED, song=song, splashtext=splashtext)}", inline=False)
embed.add_field(name=f"OS", value=platform.platform())
embed.add_field(name="Python Version", value=platform.python_version())
await send_message(ctx, embed=embed)
# Command: Upload memory.json to litterbox.catbox.moe and return the link
@bot.hybrid_command()
async def mem(ctx: commands.Context) -> None:
if showmemenabled != "true":
return
command: str = """curl -F "reqtype=fileupload" -F "time=1h" -F "fileToUpload=@memory.json" https://litterbox.catbox.moe/resources/internals/api.php"""
memorylitter: subprocess.CompletedProcess = subprocess.run(command, shell=True, capture_output=True, text=True)
logger.debug(memorylitter)
await send_message(ctx, memorylitter.stdout.strip())
# Helper: Improve sentence coherence (simple capitalization fix)
def improve_sentence_coherence(sentence: str) -> str:
# Capitalizes "i" to "I" in the sentence
sentence = sentence.replace(" i ", " I ")
return sentence
# Start the bot
bot.run(TOKEN)

View file

@ -1,245 +0,0 @@
import discord
from discord.ext import commands, tasks
import json
import markovify
import nltk
from nltk.tokenize import word_tokenize
import random
import os
import time
import re
from dotenv import load_dotenv
load_dotenv()
# download NLTK data files
nltk.download('punkt')
MEMORY_FILE = "memory.json"
MEMORY_LOADED_FILE = "MEMORY_LOADED"
def load_memory():
data = []
# Try to load data from MEMORY_FILE
try:
with open(MEMORY_FILE, "r") as f:
data = json.load(f)
except FileNotFoundError:
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
filtered_memory = [line for line in memory if isinstance(line, str)]
if additional_data:
filtered_memory.extend(line for line in additional_data if isinstance(line, str))
if not filtered_memory:
return None
text = "\n".join(filtered_memory)
model = markovify.NewlineText(text, state_size=2)
return model
#this doesnt work and im extremely pissed and mad
def append_mentions_to_18digit_integer(message):
pattern = r'\b\d{18}\b'
return re.sub(pattern, lambda match: f"<@{match.group(0)}>", message)
def preprocess_message(message):
message = append_mentions_to_18digit_integer(message)
tokens = word_tokenize(message)
tokens = [token for token in tokens if token.isalnum()]
return " ".join(tokens)
intents = discord.Intents.default()
intents.messages = True
intents.message_content = True
bot = commands.Bot(command_prefix="g!", intents=intents)
memory = load_memory()
markov_model = train_markov_model(memory)
generated_sentences = set()
used_words = set()
@bot.event
async def on_ready():
print(f"Logged in as {bot.user}")
post_message.start()
positive_keywords = ["happy", "good", "great", "amazing", "awesome", "joy", "love", "fantastic", "positive", "cheerful", "victory", "favorite", "lmao", "lol", "xd", "XD", "xD", "Xd"]
positive_gifs = [
"https://tenor.com/view/chill-guy-my-new-character-gif-2777893510283028272",
"https://tenor.com/view/goodnight-goodnight-friends-weezer-weezer-goodnight-gif-7322052181075806988"
]
def is_positive(sentence):
sentence_lower = sentence.lower()
return any(keyword in sentence_lower for keyword in positive_keywords)
@bot.command()
async def ask(ctx):
await ctx.send("Command undergoing fixes!")
#not really lol
@bot.command()
async def talk(ctx):
if markov_model:
response = None
for _ in range(10): # im going to shit my pants 10 times to get a coherent sentence
response = markov_model.make_sentence(tries=100)
if response and response not in generated_sentences:
# preprocess shit for grammer
response = improve_sentence_coherence(response)
generated_sentences.add(response)
break
if response:
async with ctx.typing():
cleaned_response = re.sub(r'[^\w\s]', '', response)
cleaned_response = cleaned_response.lower()
coherent_response = rephrase_for_coherence(cleaned_response)
if random.random() < 0.9:
if is_positive(coherent_response):
gif_url = random.choice(positive_gifs)
combined_message = f"{coherent_response}\n[jif]({gif_url})"
await ctx.send(combined_message)
else:
await ctx.send(coherent_response)
else:
await ctx.send(coherent_response)
else:
await ctx.send("I have nothing to say right now!")
else:
await ctx.send("I need to learn more from messages before I can talk.")
def improve_sentence_coherence(sentence):
sentence = sentence.replace(" i ", " I ")
return sentence
def rephrase_for_coherence(sentence):
words = sentence.split()
coherent_sentence = " ".join(words)
return coherent_sentence
bot.help_command = None
@bot.command()
async def help(ctx, *args):
if args:
command_name = args[0]
command = bot.get_command(command_name)
if command:
embed = discord.Embed(
title=f"Help: g!{command_name}",
description=f"**Description:** {command.help}",
color=discord.Color.blue()
)
await ctx.send(embed=embed)
else:
await ctx.send(f"Command `{command_name}` not found.")
else:
embed = discord.Embed(
title="Bot Help",
description="List of commands grouped by category.",
color=discord.Color.blue()
)
command_categories = {
"General": ["show_memory", "talk", "ask", "ping"],
"Debug": ["word_usage"]
}
for category, commands_list in command_categories.items():
commands_in_category = "\n".join([f"g!{command}" for command in commands_list])
embed.add_field(name=category, value=commands_in_category, inline=False)
await ctx.send(embed=embed)
@bot.event
async def on_message(message):
global memory, markov_model, last_random_talk_time
if message.author.bot:
return
if message.content.startswith(("g!talk", "g!show_memory", "g!help", "g!")):
await bot.process_commands(message)
return
if message.content:
formatted_message = append_mentions_to_18digit_integer(message.content)
cleaned_message = preprocess_message(formatted_message)
if cleaned_message:
memory.append(cleaned_message)
save_memory(memory)
markov_model = train_markov_model(memory)
# process any commands in the message
await bot.process_commands(message)
@bot.command()
async def ping(ctx):
await ctx.defer()
#stolen from my expect bot very proud
latency = round(bot.latency * 1000)
LOLembed = discord.Embed(
title="Pong!!",
description=(
f"The Beretta fires fast and won't make you feel any better!\n"
f"`Bot Latency: {latency}ms`\n"
),
color=discord.Color.blue()
)
LOLembed.set_footer(text=f"Requested by {ctx.author.name}", icon_url=ctx.author.avatar.url)
await ctx.send(embed=LOLembed) # use ctx.send instead of respond because it has nothing to respond to and its not a slash command
@bot.command()
async def show_memory(ctx):
memory = load_memory()
memory_text = json.dumps(memory, indent=4)
if len(memory_text) > 1024:
with open(MEMORY_FILE, "r") as f:
await ctx.send(" ", file=discord.File(f, MEMORY_FILE))
else:
embed = discord.Embed(title="Memory Contents", description="The bot's memory.", color=discord.Color.blue())
embed.add_field(name="Memory Data", value=f"```json\n{memory_text}\n```", inline=False)
await ctx.send(embed=embed)
def improve_sentence_coherence(sentence):
sentence = sentence.replace(" i ", " I ")
return sentence
@tasks.loop(minutes=60)
async def post_message():
channel_id = 1296141985253691433
channel = bot.get_channel(channel_id)
if channel and markov_model:
response = None
for _ in range(10):
response = markov_model.make_sentence(tries=100)
if response and response not in generated_sentences:
generated_sentences.add(response)
break
if response:
await channel.send(response)
# run the bot
TOKEN = os.getenv("DISCORDBOTTOKEN", "0")
bot.run(TOKEN)

View file

@ -1,29 +1 @@
DISCORDBOTTOKEN= DISCORDBOTTOKEN=
BOTPREFIX="g."
PINGLINE="The Beretta fires fast and won't make you feel any better!"
BLACKLISTEDUSERS=
OWNERID=
USERTRAINENABLED="true"
SHOWMEMENABLED="true"
LOCALE=fi
NAME=goober
AUTOUPDATE="True"
SONG="Basket Case - Green Day"
CHECKSDISABLED="Frue"
REACT="True"
STATUS="idle"
POSITIVEGIFS="https://media.discordapp.net/attachments/821047460151427135/1181371808566493184/jjpQGeno.gif, https://tenor.com/view/chill-guy-my-new-character-gif-2777893510283028272,https://tenor.com/view/goodnight-goodnight-friends-weezer-weezer-goodnight-gif-7322052181075806988"
SPLASHTEXT="
SS\
SS |
SSSSSS\ SSSSSS\ SSSSSS\ SSSSSSS\ SSSSSS\ SSSSSS\
SS __SS\ SS __SS\ SS __SS\ SS __SS\ SS __SS\ SS __SS\
SS / SS |SS / SS |SS / SS |SS | SS |SSSSSSSS |SS | \__|
SS | SS |SS | SS |SS | SS |SS | SS |SS ____|SS |
\SSSSSSS |\SSSSSS |\SSSSSS |SSSSSSS |\SSSSSSS\ SS |
\____SS | \______/ \______/ \_______/ \_______|\__|
SS\ SS |
\SSSSSS |
\______/
"

302
main.py Normal file
View file

@ -0,0 +1,302 @@
import os
import re
import json
import time
import random
import traceback
import subprocess
import tempfile
import shutil
import sys
from typing import (
List,
Dict,
Literal,
Set,
Optional,
Tuple,
Any,
TypedDict,
Union,
Callable,
Coroutine,
TypeVar,
Type,
)
import logging
from modules.prestartchecks import start_checks
from modules.logger import GooberFormatter
from modules.volta.main import *
import logging
from modules.settings import Settings as SettingsManager
from modules.permission import requires_admin
from modules.volta.main import _
from modules.version import check_for_update
check_for_update()
logger = logging.getLogger("goober")
logger.setLevel(logging.DEBUG)
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.DEBUG)
console_handler.setFormatter(GooberFormatter())
file_handler = logging.FileHandler("log.txt", mode="w+", encoding="UTF-8")
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(GooberFormatter(colors=False))
logger.addHandler(console_handler)
logger.addHandler(file_handler)
settings_manager = SettingsManager()
settings = settings_manager.settings
splash_text: str = ""
with open(settings["splash_text_loc"], "r", encoding="UTF-8") as f:
splash_text = "".join(f.readlines())
print(splash_text)
start_checks()
import discord
from discord.ext import commands
from discord import app_commands
from discord import Colour, Message
from better_profanity import profanity
from discord.ext import commands
from modules.markovmemory import *
from modules.sentenceprocessing import *
from modules.unhandledexception import handle_exception
sys.excepthook = handle_exception
class MessageMetadata(TypedDict):
user_id: str
user_name: str
guild_id: str | Literal["DM"]
guild_name: str | Literal["DM"]
channel_id: str
channel_name: str
message: str
timestamp: float
# Constants with type hints
positive_gifs: List[str] = settings["bot"]["misc"]["positive_gifs"]
name = settings["name"]
currenthash: str = ""
launched: bool = False
slash_commands_enabled: bool = False
# Set up Discord bot intents and create bot instance
intents: discord.Intents = discord.Intents.default()
intents.messages = True
intents.presences = True
intents.members = True
intents.message_content = True
bot: commands.Bot = commands.Bot(
command_prefix=settings["bot"]["prefix"],
intents=intents,
allowed_mentions=discord.AllowedMentions(
everyone=False, roles=False, users=False, replied_user=True
),
)
# Load memory and Markov model for text generation
memory: List[str | Dict[Literal["_meta"], MessageMetadata]] = load_memory()
generated_sentences: Set[str] = set()
used_words: Set[str] = set()
async def load_cogs_from_folder(bot: commands.Bot, folder_name="assets/cogs"):
for filename in [file for file in os.listdir(folder_name) if file.endswith(".py")]:
cog_name: str = filename[:-3]
if (
"internal" not in folder_name
and cog_name not in settings["bot"]["enabled_cogs"]
):
logger.debug(f"Skipping cog {cog_name} (not in enabled cogs)")
continue
module_path = folder_name.replace("/", ".").replace("\\", ".") + f".{cog_name}"
try:
await bot.load_extension(module_path)
logger.info(f"{_('loaded_cog')} {cog_name}")
except Exception as e:
logger.error(f"{_('cog_fail')} {cog_name} {e}")
traceback.print_exc()
# Event: Called when the bot is ready
@bot.event
async def on_ready() -> None:
global launched
folder_name: str = "cogs"
if launched:
return
await load_cogs_from_folder(bot)
await load_cogs_from_folder(bot, "assets/cogs/internal")
try:
synced: List[discord.app_commands.AppCommand] = await bot.tree.sync()
logger.info(f"{_('synced_commands')} {len(synced)} {_('synced_commands2')}")
logger.info(_('started').format(name=name))
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."
)
quit()
except Exception as e:
logger.error(f"{_('fail_commands_sync')} {e}")
traceback.print_exc()
quit()
if not settings["bot"]["misc"]["active_song"]:
return
await bot.change_presence(
activity=discord.Activity(
type=discord.ActivityType.listening,
name=settings["bot"]["misc"]["active_song"],
)
)
launched = True
bot.remove_command('help')
@bot.event
async def on_command_error(ctx: commands.Context, error: commands.CommandError) -> None:
from modules.unhandledexception import handle_exception
if isinstance(error, commands.CommandInvokeError):
original: Exception = 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}",
)
# Event: Called on every message
@bot.event
async def on_message(message: discord.Message) -> None:
global memory
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
if str(message.author.id) in settings["bot"]["blacklisted_users"]:
return
commands = [
settings["bot"]["prefix"] + command.name for command in bot.tree.get_commands()
]
if message.content.startswith(tuple(commands)):
logger.info(f"{(_('command_ran')).format(message=message)}")
await bot.process_commands(message)
return
if message.content:
if not settings["bot"]["user_training"]:
return
formatted_message: str = message.content
cleaned_message: str = preprocess_message(formatted_message)
if cleaned_message:
memory.append(cleaned_message)
message_metadata: MessageMetadata = {
"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 not settings["bot"]["react_to_messages"]:
return
emoji = random.choice(EMOJIS)
try:
await message.add_reaction(emoji)
except Exception as e:
logger.info(f"Failed to react with emoji: {e}")
await bot.process_commands(message)
# Event: Called on every interaction (slash command, etc.)
@bot.event
async def on_interaction(interaction: discord.Interaction) -> None:
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 ctx.author.id not in settings["bot"]["blacklisted_users"]:
return True
try:
if isinstance(ctx, discord.Interaction):
if not ctx.response.is_done():
await ctx.response.send_message(_('blacklisted'), ephemeral=True)
else:
await ctx.followup.send(_('blacklisted'), ephemeral=True)
else:
await ctx.send(_('blacklisted_user'), ephemeral=True)
except:
return False
return True
# Helper: Improve sentence coherence (simple capitalization fix)
def improve_sentence_coherence(sentence: str) -> str:
# Capitalizes "i" to "I" in the sentence
sentence = sentence.replace(" i ", " I ")
return sentence
# Start the bot
if __name__ == "__main__":
bot.run(os.environ.get("DISCORDBOTTOKEN", ""))

View file

@ -1,72 +1,71 @@
import os import os
import platform import platform
from typing import Callable, List
from dotenv import load_dotenv from dotenv import load_dotenv
import pathlib import pathlib
import discord
from discord.ext import commands
from discord import app_commands
from discord import Colour, Embed, File, Interaction, Message
from discord.abc import Messageable
from discord.ext import commands
import subprocess import subprocess
def get_git_branch(): def get_git_branch():
try: try:
branch = subprocess.check_output( branch = (
["git", "rev-parse", "--abbrev-ref", "HEAD"], subprocess.check_output(
stderr=subprocess.DEVNULL ["git", "rev-parse", "--abbrev-ref", "HEAD"], stderr=subprocess.DEVNULL
).decode('utf-8').strip() )
.decode("utf-8")
.strip()
)
return branch return branch
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
return None return None
env_path = pathlib.Path(__file__).parent.parent / '.env'
env_path = pathlib.Path(__file__).parent.parent / ".env"
load_dotenv(dotenv_path=env_path) load_dotenv(dotenv_path=env_path)
available_cogs: Callable[[], List[str]] = lambda: [
file[:-3] for file in os.listdir("assets/cogs") if file.endswith(".py")
]
ANSI = "\033[" ANSI = "\033["
RED = f"{ANSI}31m" RED = f"{ANSI}31m"
GREEN = f"{ANSI}32m" GREEN = f"{ANSI}32m"
YELLOW = f"{ANSI}33m" YELLOW = f"{ANSI}33m"
PURPLE = f"{ANSI}35m" PURPLE = f"{ANSI}35m"
DEBUG = f"{ANSI}1;30m" DEBUG = f"{ANSI}90m"
RESET = f"{ANSI}0m" RESET = f"{ANSI}0m"
VERSION_URL = "https://raw.githubusercontent.com/gooberinc/version/main" VERSION_URL = "https://raw.githubusercontent.com/gooberinc/version/main"
UPDATE_URL = VERSION_URL+"/latest_version.json" UPDATE_URL = VERSION_URL + "/latest_version.json"
print(UPDATE_URL) print(UPDATE_URL)
LOCAL_VERSION_FILE = "current_version.txt" LOCAL_VERSION_FILE = "current_version.txt"
TOKEN = os.getenv("DISCORDBOTTOKEN", "0")
PREFIX = os.getenv("BOTPREFIX", "g.") # TOKEN = os.getenv("DISCORDBOTTOKEN", "0")
PING_LINE = os.getenv("PINGLINE") # PREFIX = os.getenv("BOTPREFIX", "g.")
CHECKS_DISABLED = os.getenv("CHECKSDISABLED") # PING_LINE = os.getenv("PINGLINE")
LOCALE = os.getenv("LOCALE", "en") # CHECKS_DISABLED = os.getenv("CHECKSDISABLED")
gooberTOKEN = os.getenv("GOOBERTOKEN") # LOCALE = os.getenv("LOCALE", "en")
splashtext = os.getenv("SPLASHTEXT") # BLACKLISTED_USERS = os.getenv("BLACKLISTEDUSERS", "").split(",")
ownerid = int(os.getenv("OWNERID", "0")) # USERTRAIN_ENABLED = os.getenv("USERTRAINENABLED", "true").lower() == "true"
status = os.getenv("STATUS") # NAME = os.getenv("NAME")
showmemenabled = os.getenv("SHOWMEMENABLED") # MEMORY_FILE = "memory.json"
BLACKLISTED_USERS = os.getenv("BLACKLISTEDUSERS", "").split(",") # MEMORY_LOADED_FILE = "MEMORY_LOADED" # is this still even used?? okay just checked its used in the markov module
USERTRAIN_ENABLED = os.getenv("USERTRAINENABLED", "true").lower() == "true" # ALIVEPING = os.getenv("ALIVEPING")
NAME = os.getenv("NAME") # AUTOUPDATE = os.getenv("AUTOUPDATE")
MEMORY_FILE = "memory.json" # REACT = os.getenv("REACT")
MEMORY_LOADED_FILE = "MEMORY_LOADED" # is this still even used?? okay just checked its used in the markov module
ALIVEPING = os.getenv("ALIVEPING") # gooberTOKEN = os.getenv("GOOBERTOKEN")
AUTOUPDATE = os.getenv("AUTOUPDATE") # splashtext = os.getenv("SPLASHTEXT")
song = os.getenv("SONG") # ownerid = int(os.getenv("OWNERID", "0"))
# showmemenabled = os.getenv("SHOWMEMENABLED")
# IGNOREWARNING = False # is this either??? i don't think so?
# song = os.getenv("song")
arch = platform.machine() arch = platform.machine()
slash_commands_enabled = True # 100% broken, its a newer enough version so its probably enabled by default.... fix this at somepoint or hard code it in goober central code
launched = False launched = False
latest_version = "0.0.0" latest_version = "0.0.0"
local_version = "2.3.5" local_version = "3.0.0"
os.environ['gooberlocal_version'] = local_version os.environ["gooberlocal_version"] = local_version
REACT = os.getenv("REACT") beta = get_git_branch() == "dev"
if get_git_branch() == "dev":
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:
beta = False
# Set up Discord bot intents and create bot instance
intents: discord.Intents = discord.Intents.default()
intents.messages = True
intents.presences = True
intents.members = True
intents.message_content = True
bot: commands.Bot = commands.Bot(command_prefix=PREFIX, intents=intents, allowed_mentions=discord.AllowedMentions(everyone=False, roles=False, users=False, replied_user=True))

View file

@ -1,177 +0,0 @@
import os
import re
import random
import shutil
import tempfile
from typing import Optional, List
from PIL import Image, ImageDraw, ImageFont, ImageOps
from modules.markovmemory import load_markov_model
from modules.sentenceprocessing import improve_sentence_coherence, rephrase_for_coherence
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:
draw.text((x + ox, y + oy), text, font=font, fill="black")
draw.text((x, y), text, font=font, fill="white")
def fits_in_width(text, font, max_width, draw):
bbox = draw.textbbox((0, 0), text, font=font)
text_width = bbox[2] - bbox[0]
return text_width <= max_width
def split_text_to_fit(text, font, max_width, draw):
words = text.split()
for i in range(len(words), 0, -1):
top_text = " ".join(words[:i])
bottom_text = " ".join(words[i:])
if fits_in_width(top_text, font, max_width, draw) and fits_in_width(bottom_text, font, max_width, draw):
return top_text, bottom_text
midpoint = len(words) // 2
return " ".join(words[:midpoint]), " ".join(words[midpoint:])
async def gen_meme(input_image_path, sentence_size=5, max_attempts=10, custom_text=None):
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("RGBA") as img:
draw = ImageDraw.Draw(img)
width, height = img.size
font_size = int(height / 10)
font = load_font(font_size)
response = None
if custom_text:
response = custom_text
else:
for _ in range(20):
if sentence_size == 1:
candidate = markov_model.make_short_sentence(max_chars=100, tries=100)
if candidate:
candidate = candidate.split()[0]
else:
candidate = markov_model.make_sentence(tries=100, max_words=sentence_size)
if candidate and candidate not in generated_sentences:
if sentence_size > 1:
candidate = improve_sentence_coherence(candidate)
generated_sentences.add(candidate)
response = candidate
break
if not response:
response = "NO TEXT GENERATED"
cleaned_response = re.sub(r'[^\w\s]', '', response).lower()
coherent_response = rephrase_for_coherence(cleaned_response).upper()
bbox = draw.textbbox((0, 0), coherent_response, font=font)
text_width = bbox[2] - bbox[0]
text_height_px = bbox[3] - bbox[1]
max_text_height = height // 4
if text_width <= width and text_height_px <= max_text_height:
draw_text_with_outline(draw, coherent_response, (width - text_width) / 2, 0, font)
img.save(input_image_path)
return input_image_path
else:
top_text, bottom_text = split_text_to_fit(coherent_response, font, width, draw)
top_bbox = draw.textbbox((0, 0), top_text, font=font)
bottom_bbox = draw.textbbox((0, 0), bottom_text, font=font)
top_height = top_bbox[3] - top_bbox[1]
bottom_height = bottom_bbox[3] - bottom_bbox[1]
if top_height <= max_text_height and bottom_height <= max_text_height:
draw_text_with_outline(draw, top_text, (width - (top_bbox[2] - top_bbox[0])) / 2, 0, font)
y_bottom = height - bottom_height - int(height * 0.04)
draw_text_with_outline(draw, bottom_text, (width - (bottom_bbox[2] - bottom_bbox[0])) / 2, y_bottom, font)
img.save(input_image_path)
return input_image_path
attempt += 1
with Image.open(input_image_path).convert("RGBA") as img:
draw = ImageDraw.Draw(img)
width, height = img.size
font_size = int(height / 10)
font = load_font(font_size)
truncated = coherent_response[:100]
bbox = draw.textbbox((0, 0), truncated, font=font)
text_width = bbox[2] - bbox[0]
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

View file

@ -3,9 +3,16 @@ import json
import markovify import markovify
import pickle import pickle
from modules.globalvars import * from modules.globalvars import *
from modules.volta.main import _
import logging import logging
from modules.volta.main import _
from modules.settings import instance as settings_manager
settings = settings_manager.settings
logger = logging.getLogger("goober") logger = logging.getLogger("goober")
# Get file size and line count for a given file path # Get file size and line count for a given file path
def get_file_info(file_path): def get_file_info(file_path):
try: try:
@ -16,46 +23,52 @@ def get_file_info(file_path):
except Exception as e: except Exception as e:
return {"error": str(e)} return {"error": str(e)}
# Load memory data from file, or use default dataset if not loaded yet # Load memory data from file, or use default dataset if not loaded yet
def load_memory(): def load_memory():
data = [] data = []
# Try to load data from MEMORY_FILE # Try to load data from MEMORY_FILE
try: try:
with open(MEMORY_FILE, "r") as f: with open(settings["bot"]["active_memory"], "r") as f:
data = json.load(f) data = json.load(f)
except FileNotFoundError: except FileNotFoundError:
pass pass
return data return data
# Save memory data to MEMORY_FILE # Save memory data to MEMORY_FILE
def save_memory(memory): def save_memory(memory):
with open(MEMORY_FILE, "w") as f: with open(settings["bot"]["active_memory"], "w") as f:
json.dump(memory, f, indent=4) json.dump(memory, f, indent=4)
def train_markov_model(memory, additional_data=None):
def train_markov_model(memory, additional_data=None) -> markovify.NewlineText | None:
if not memory: if not memory:
return None return None
filtered_memory = [line for line in memory if isinstance(line, str)] filtered_memory = [line for line in memory if isinstance(line, str)]
if additional_data: if additional_data:
filtered_memory.extend(line for line in additional_data if isinstance(line, str)) filtered_memory.extend(
line for line in additional_data if isinstance(line, str)
)
if not filtered_memory: if not filtered_memory:
return None return None
text = "\n".join(filtered_memory) text = "\n".join(filtered_memory)
model = markovify.NewlineText(text, state_size=2) model = markovify.NewlineText(text, state_size=2)
return model return model
# Save the Markov model to a pickle file def save_markov_model(model, filename="markov_model.pkl"):
def save_markov_model(model, filename='markov_model.pkl'): with open(filename, "wb") as f:
with open(filename, 'wb') as f:
pickle.dump(model, f) pickle.dump(model, f)
logger.info(f"Markov model saved to {filename}.") logger.info(f"Markov model saved to {filename}.")
# Load the Markov model from a pickle file def load_markov_model(filename="markov_model.pkl"):
def load_markov_model(filename='markov_model.pkl'):
try: try:
with open(filename, 'rb') as f: with open(filename, "rb") as f:
model = pickle.load(f) model = pickle.load(f)
logger.info(f"{_('model_loaded')} {filename}.{RESET}") logger.info(f"{_('model_loaded')} {filename}.{RESET}")
return model return model

View file

@ -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)

37
modules/permission.py Normal file
View file

@ -0,0 +1,37 @@
from functools import wraps
import discord
import discord.ext
import discord.ext.commands
from modules.settings import Settings as SettingsManager
import logging
logger = logging.getLogger("goober")
settings_manager = SettingsManager()
settings = settings_manager.settings
class PermissionError(Exception):
pass
def requires_admin():
async def wrapper(ctx: discord.ext.commands.Context):
print(ctx.author.id)
if ctx.author.id not in settings["bot"]["owner_ids"]:
await ctx.send("You don't have the necessary permissions to run this command!")
return
command = ctx.command
if not command:
logger.info(f"Unknown command ran {ctx.message}")
else:
logger.info(
f'Command {settings["bot"]["prefix"]}{command.name} @{ctx.author.name}'
)
return True
return discord.ext.commands.check(wrapper)

View file

@ -1,4 +1,5 @@
from modules.globalvars import * from modules.globalvars import *
from modules.settings import Settings as SettingsManager
from modules.volta.main import _, check_missing_translations from modules.volta.main import _, check_missing_translations
import time import time
import os import os
@ -14,6 +15,13 @@ import logging
logger = logging.getLogger("goober") logger = logging.getLogger("goober")
settings_manager = SettingsManager()
settings = settings_manager.settings
MEMORY_FILE = settings["bot"]["active_memory"]
with open(settings["splash_text_loc"], "r", encoding="UTF-8") as f:
splash_text = "".join(f.readlines())
# import shutil # import shutil
psutilavaliable = True psutilavaliable = True
try: try:
@ -29,7 +37,6 @@ def check_for_model():
else: else:
logger.info("Model is not installed.") logger.info("Model is not installed.")
def iscloned(): def iscloned():
if os.path.exists(".git"): if os.path.exists(".git"):
return True return True
@ -38,130 +45,112 @@ def iscloned():
sys.exit(1) sys.exit(1)
def get_stdlib_modules(): def get_stdlib_modules():
stdlib_path = pathlib.Path(sysconfig.get_paths()['stdlib']) stdlib = pathlib.Path(sysconfig.get_paths()['stdlib'])
modules = set() modules = set(sys.builtin_module_names)
if hasattr(sys, 'builtin_module_names'):
modules.update(sys.builtin_module_names) modules.update(
for file in stdlib_path.glob('*.py'): f.stem for f in stdlib.glob('*.py') if f.stem != '__init__'
if file.stem != '__init__': )
modules.add(file.stem) modules.update(
for folder in stdlib_path.iterdir(): d.name for d in stdlib.iterdir() if (d / '__init__.py').exists()
if folder.is_dir() and (folder / '__init__.py').exists(): )
modules.add(folder.name) modules.update(
for file in stdlib_path.glob('*.*'): f.stem for f in stdlib.glob('*') if f.suffix in ('.so', '.pyd')
if file.suffix in ('.so', '.pyd'): )
modules.add(file.stem)
return modules return modules
def check_requirements(): def check_requirements():
STD_LIB_MODULES = get_stdlib_modules() stdlib = get_stdlib_modules()
PACKAGE_ALIASES = { aliases = {
"discord": "discord.py", "discord": "discord.py",
"better_profanity": "better-profanity", "better_profanity": "better-profanity",
"dotenv": "python-dotenv", "dotenv": "python-dotenv",
"pil": "pillow" "pil": "pillow"
} }
parent_dir = os.path.dirname(os.path.abspath(__file__)) req_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'requirements.txt'))
requirements_path = os.path.abspath(os.path.join(parent_dir, '..', 'requirements.txt')) if not os.path.exists(req_path):
logger.error(_('requirements_not_found').format(path=req_path))
if not os.path.exists(requirements_path):
logger.error(f"{(_('requirements_not_found')).format(path=requirements_path)}")
return return
with open(requirements_path, 'r') as f: with open(req_path) as f:
lines = f.readlines() requirements = {
requirements = set() aliases.get(line.split('==')[0].strip().lower(), line.split('==')[0].strip().lower())
for line in lines: for line in f if line.strip() and not line.startswith('#')
line = line.strip() }
if line and not line.startswith('#'):
base_pkg = line.split('==')[0].lower()
aliased_pkg = PACKAGE_ALIASES.get(base_pkg, base_pkg)
requirements.add(aliased_pkg)
installed_packages = {dist.metadata['Name'].lower() for dist in importlib.metadata.distributions()} installed = {d.metadata['Name'].lower() for d in importlib.metadata.distributions()}
missing = [] missing = []
for req in sorted(requirements): for pkg in sorted(requirements):
if req in STD_LIB_MODULES or req == 'modules': if pkg in stdlib or pkg == 'modules':
print((_('std_lib_local_skipped')).format(package=req)) print(_('std_lib_local_skipped').format(package=pkg))
continue continue
if pkg in installed:
check_name = req.lower() logger.info(_('ok_installed').format(package=pkg))
if check_name in installed_packages:
logger.info(f"{_('ok_installed').format(package=check_name)} {check_name}")
else: else:
logger.error(f"{(_('missing_package')).format(package=check_name)} {check_name} {(_('missing_package2'))}") logger.error(f"{_('missing_package').format(package=pkg)} {pkg} {_('missing_package2')}")
missing.append(check_name) missing.append(pkg)
if missing: if missing:
logger.error(_('missing_packages_detected')) logger.error(_('missing_packages_detected'))
for pkg in missing: for pkg in missing:
print(f" - {pkg}") print(f" - {pkg}")
sys.exit(1) sys.exit(1)
else:
logger.info(_('all_requirements_satisfied')) logger.info(_('all_requirements_satisfied'))
def check_latency(): def check_latency():
host = "1.1.1.1" host = "1.1.1.1"
system = platform.system() system = platform.system()
if system == "Windows": cmd, pattern = {
cmd = ["ping", "-n", "1", "-w", "1000", host] "Windows": (["ping", "-n", "1", "-w", "1000", host], r"Average = (\d+)ms"),
latency_pattern = r"Average = (\d+)ms" "Darwin": (["ping", "-c", "1", host], r"time=([\d\.]+) ms")
}.get(system, (["ping", "-c", "1", "-W", "1", host], r"time=([\d\.]+) ms"))
elif system == "Darwin":
cmd = ["ping", "-c", "1", host]
latency_pattern = r"time=([\d\.]+) ms"
else:
cmd = ["ping", "-c", "1", "-W", "1", host]
latency_pattern = r"time=([\d\.]+) ms"
try: try:
result = subprocess.run( result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
cmd, if result.returncode != 0:
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
if result.returncode == 0:
match = re.search(latency_pattern, result.stdout)
if match:
latency_ms = float(match.group(1))
logger.info((_('ping_to')).format(host=host, latency=latency_ms))
if latency_ms > 300:
logger.warning(f"{(_('high_latency'))}")
else:
logger.warning((_('could_not_parse_latency')))
else:
print(result.stderr) print(result.stderr)
logger.error(f"{(_('ping_failed')).format(host=host)}{RESET}") return logger.error(_('ping_failed').format(host=host) + RESET)
match = re.search(pattern, result.stdout)
if not match:
return logger.warning(_('could_not_parse_latency'))
latency = float(match.group(1))
logger.info(_('ping_to').format(host=host, latency=latency))
if latency > 300:
logger.warning(_('high_latency'))
except Exception as e: except Exception as e:
logger.error((_('error_running_ping')).format(error=e)) logger.error(_('error_running_ping').format(error=e))
def check_memory(): def check_memory():
if psutilavaliable == False: if not psutilavaliable:
return return
try:
memory_info = psutil.virtual_memory() # type: ignore
total_memory = memory_info.total / (1024 ** 3)
used_memory = memory_info.used / (1024 ** 3)
free_memory = memory_info.available / (1024 ** 3)
logger.info((_('memory_usage')).format(used=used_memory, total=total_memory, percent=(used_memory / total_memory) * 100)) try:
if used_memory > total_memory * 0.9: mem = psutil.virtual_memory() # type: ignore
print(f"{YELLOW}{(_('memory_above_90')).format(percent=(used_memory / total_memory) * 100)}{RESET}") total = mem.total / 1e9
logger.info((_('total_memory')).format(total=total_memory)) used = mem.used / 1e9
logger.info((_('used_memory')).format(used=used_memory)) free = mem.available / 1e9
if free_memory < 1: percent_used = (used / total) * 100
logger.warning(f"{(_('low_free_memory')).format(free=free_memory)}")
logger.info(_('memory_usage').format(used=used, total=total, percent=percent_used))
if percent_used > 90:
print(f"{YELLOW}{_('memory_above_90').format(percent=percent_used)}{RESET}")
logger.info(_('total_memory').format(total=total))
logger.info(_('used_memory').format(used=used))
if free < 1:
logger.warning(_('low_free_memory').format(free=free))
sys.exit(1) sys.exit(1)
except ImportError: except ImportError:
logger.error(_('psutil_not_installed')) # todo: translate this into italian and put it in the translations "psutil is not installed. Memory check skipped." logger.error(_('psutil_not_installed'))
def check_cpu(): def check_cpu():
if psutilavaliable == False: if psutilavaliable == False:
@ -178,22 +167,23 @@ def check_cpu():
def check_memoryjson(): def check_memoryjson():
try: try:
logger.info((_('memory_file')).format(size=os.path.getsize(MEMORY_FILE) / (1024 ** 2))) size_mb = os.path.getsize(MEMORY_FILE) / (1024 ** 2)
if os.path.getsize(MEMORY_FILE) > 1_073_741_824: logger.info(_('memory_file').format(size=size_mb))
logger.warning(f"{(_('memory_file_large'))}") if size_mb > 1024:
logger.warning(_('memory_file_large'))
try: try:
with open(MEMORY_FILE, 'r', encoding='utf-8') as f: with open(MEMORY_FILE, 'r', encoding='utf-8') as f:
json.load(f) json.load(f)
except json.JSONDecodeError as e: except (json.JSONDecodeError, UnicodeDecodeError) as e:
logger.error(f"{(_('memory_file_corrupted')).format(error=e)}") msg = _('memory_file_corrupted') if isinstance(e, json.JSONDecodeError) else _('memory_file_encoding')
logger.warning(f"{(_('consider_backup_memory'))}") logger.error(msg.format(error=e))
except UnicodeDecodeError as e: logger.warning(_('consider_backup_memory'))
logger.error(f"{(_('memory_file_encoding')).format(error=e)}")
logger.warning(f"{(_('consider_backup_memory'))}")
except Exception as e: except Exception as e:
logger.error(f"{(_('error_reading_memory')).format(error=e)}") logger.error(_('error_reading_memory').format(error=e))
except FileNotFoundError: except FileNotFoundError:
logger(f"{(_('memory_file_not_found'))}") logger.error(_('memory_file_not_found'))
def presskey2skip(timeout): def presskey2skip(timeout):
if os.name == 'nt': if os.name == 'nt':
@ -228,8 +218,8 @@ def presskey2skip(timeout):
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
beta = beta beta = beta
def start_checks(): def start_checks():
if CHECKS_DISABLED == "True": if settings["disable_checks"]:
logger.warning(f"{(_('checks_disabled'))}") logger.warning(f"{_('checks_disabled')}")
return return
logger.info(_('running_prestart_checks')) logger.info(_('running_prestart_checks'))
check_for_model() check_for_model()
@ -252,4 +242,3 @@ def start_checks():
logger.info(_('continuing_in_seconds').format(seconds=5)) logger.info(_('continuing_in_seconds').format(seconds=5))
presskey2skip(timeout=5) presskey2skip(timeout=5)
os.system('cls' if os.name == 'nt' else 'clear') os.system('cls' if os.name == 'nt' else 'clear')
print(splashtext)

View file

@ -37,29 +37,27 @@ def is_positive(sentence):
return sentiment_score > 0.6 # had to raise the bar because it kept saying "death to jews" was fine and it kept reacting to them return sentiment_score > 0.6 # had to raise the bar because it kept saying "death to jews" was fine and it kept reacting to them
async def send_message(ctx, message=None, embed=None, file=None, edit=False, message_reference=None): async def send_message(ctx, message=None, embed=None, file=None, edit=False, message_reference=None):
if edit and message_reference: try:
try: if edit and message_reference:
await message_reference.edit(content=message, embed=embed) await message_reference.edit(content=message, embed=embed)
except Exception as e: return message_reference
await ctx.send(f"{RED}{(_('edit_fail'))} {e}{RESET}")
else: send_kwargs = {}
if message:
send_kwargs['content'] = message
if embed:
send_kwargs['embed'] = embed
if file:
send_kwargs['file'] = file
if hasattr(ctx, "respond"): if hasattr(ctx, "respond"):
sent_message = None return await ctx.respond(**send_kwargs, ephemeral=False)
if embed:
sent_message = await ctx.respond(embed=embed, ephemeral=False)
elif message:
sent_message = await ctx.respond(message, ephemeral=False)
if file:
sent_message = await ctx.respond(file=file, ephemeral=False)
else: else:
sent_message = None return await ctx.send(**send_kwargs)
if embed:
sent_message = await ctx.send(embed=embed) except Exception as e:
elif message: await ctx.send(f"{RED}{(_('edit_fail'))} {e}{RESET}")
sent_message = await ctx.send(message)
if file:
sent_message = await ctx.send(file=file)
return sent_message
def preprocess_message(message): def preprocess_message(message):
message = message message = message

58
modules/settings.py Normal file
View file

@ -0,0 +1,58 @@
import json
import os
from typing import List, TypedDict
import copy
class MiscBotOptions(TypedDict):
ping_line: str
active_song: str
positive_gifs: List[str]
block_profanity: bool
class BotSettings(TypedDict):
prefix: str
owner_ids: List[int]
blacklisted_users: List[int]
user_training: bool
allow_show_mem_command: bool
react_to_messages: bool
misc: MiscBotOptions
enabled_cogs: List[str]
active_memory: str
class SettingsType(TypedDict):
bot: BotSettings
locale: str
name: str
auto_update: bool
disable_checks: bool
splash_text_loc: str
class Settings:
def __init__(self) -> None:
self.path = os.path.join(".", "settings", "settings.json")
if not os.path.exists(self.path):
raise FileNotFoundError("settings.json file does not exist!")
with open(self.path, "r") as f:
self._kv_store = json.load(f)
self.settings: SettingsType = self._kv_store # type: ignore
self.original_settings = copy.deepcopy(self.settings)
def get_locale(self) -> str:
# Return locale or None if missing
return self.settings.get("locale", None)
def commit(self) -> None:
with open(self.path, "w") as f:
json.dump(self.settings, f, indent=4)
self.original_settings = copy.deepcopy(self.settings)
def discard(self) -> None:
self.settings = copy.deepcopy(self.original_settings)
# Usage
instance = Settings()
locale = instance.get_locale()
print("Locale:", locale)

View file

@ -1,23 +1,16 @@
import sys import sys
import traceback import traceback
import os from modules.globalvars import RED, RESET
from modules.globalvars import RED, RESET, splashtext
from modules.volta.main import _ from modules.volta.main import _
def handle_exception(exc_type, exc_value, exc_traceback, *, context=None): 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): if issubclass(exc_type, KeyboardInterrupt):
sys.__excepthook__(exc_type, exc_value, exc_traceback) sys.__excepthook__(exc_type, exc_value, exc_traceback)
return return
print(splashtext)
print(f"{RED}=====BEGINNING OF TRACEBACK====={RESET}") print(f"{RED}=====BEGINNING OF TRACEBACK====={RESET}")
traceback.print_exception(exc_type, exc_value, exc_traceback) traceback.print_exception(exc_type, exc_value, exc_traceback)
print(f"{RED}========END OF TRACEBACK========{RESET}") print(f"{RED}========END OF TRACEBACK========{RESET}")
print(f"{RED}{_('unhandled_exception')}{RESET}") print(f"{RED}{_('unhandled_exception')}{RESET}")
if context: if context:
print(f"{RED}Context: {context}{RESET}") print(f"{RED}Context: {context}{RESET}")

View file

@ -21,7 +21,6 @@ def is_remote_ahead(branch='main', remote='origin'):
count = run_cmd(f'git rev-list --count HEAD..{remote}/{branch}') count = run_cmd(f'git rev-list --count HEAD..{remote}/{branch}')
return int(count) > 0 return int(count) > 0
# Automatically update the local repository if the remote is ahead
def auto_update(branch='main', remote='origin'): def auto_update(branch='main', remote='origin'):
if launched == True: if launched == True:
print(_("already_started")) print(_("already_started"))
@ -70,7 +69,7 @@ def get_latest_version_info():
return None return None
# Check if an update is available and perform update if needed # Check if an update is available and perform update if needed
def check_for_update(): def check_for_update(slient=False):
global latest_version, local_version, launched global latest_version, local_version, launched
latest_version_info = get_latest_version_info() latest_version_info = get_latest_version_info()
@ -90,16 +89,17 @@ def check_for_update():
logger.error(f"{RED}{_('cant_find_local_version')}{RESET}") logger.error(f"{RED}{_('cant_find_local_version')}{RESET}")
return return
# Compare local and latest versions # Compare local and latest versions
if local_version < latest_version: if slient != True:
logger.info(f"{YELLOW}{_('new_version').format(latest_version=latest_version, local_version=local_version)}{RESET}") if local_version < latest_version:
logger.info(f"{YELLOW}{_('changelog').format(VERSION_URL=VERSION_URL)}{RESET}") logger.info(f"{YELLOW}{_('new_version').format(latest_version=latest_version, local_version=local_version)}{RESET}")
auto_update() logger.info(f"{YELLOW}{_('changelog').format(VERSION_URL=VERSION_URL)}{RESET}")
elif beta == True: auto_update()
logger.warning(f"You are running an \"unstable\" version of Goober, do not expect it to work properly.\nVersion {local_version}\nServer: {latest_version}{RESET}") elif beta == True:
elif local_version > latest_version: logger.warning(f"You are running an \"unstable\" version of Goober, do not expect it to work properly.\nVersion {local_version}\nServer: {latest_version}{RESET}")
logger.warning(f"{_('modification_warning')}") elif local_version > latest_version:
elif local_version == latest_version: logger.warning(f"{_('modification_warning')}")
logger.info(f"{_('latest_version')} {local_version}") elif local_version == latest_version:
logger.info(f"{_('latest_version2').format(VERSION_URL=VERSION_URL)}\n\n") logger.info(f"{_('latest_version')} {local_version}")
launched = True logger.info(f"{_('latest_version2').format(VERSION_URL=VERSION_URL)}\n\n")
launched = True
return latest_version return latest_version

View file

@ -50,12 +50,50 @@ def find_dotenv(start_path: pathlib.Path) -> pathlib.Path | None:
current = current.parent current = current.parent
return None return None
env_path = find_dotenv(pathlib.Path(__file__).parent) def load_settings_json() -> dict | None:
if env_path: start_path = working_dir.resolve()
load_dotenv(dotenv_path=env_path) current = start_path.resolve()
print(f"[VOLTA] {GREEN}Loaded .env from {env_path}{RESET}") while current != current.parent:
candidate = current / "settings.json"
if candidate.exists():
try:
with open(candidate, "r", encoding="utf-8") as f:
data = json.load(f)
print(f"[VOLTA] {GREEN}Loaded settings.json locale '{data.get('locale')}' from {candidate}{RESET}")
return data
except Exception as e:
print(f"[VOLTA] {RED}Failed to load settings.json at {candidate}: {e}{RESET}")
return None
current = current.parent
for root, dirs, files in os.walk(start_path):
if "settings.json" in files:
candidate = pathlib.Path(root) / "settings.json"
try:
with open(candidate, "r", encoding="utf-8") as f:
data = json.load(f)
print(f"[VOLTA] {GREEN}Loaded settings.json locale '{data.get('locale')}' from {candidate}{RESET}")
return data
except Exception as e:
print(f"[VOLTA] {RED}Failed to load settings.json at {candidate}: {e}{RESET}")
return None
print(f"[VOLTA] {YELLOW}No settings.json found scanning up or down from {start_path}{RESET}")
return None
settings = load_settings_json()
if settings and "locale" in settings:
LOCALE = settings["locale"]
else: else:
print(f"[VOLTA] {YELLOW}No .env file found from {__file__} upwards.{RESET}") env_path = find_dotenv(pathlib.Path(__file__).parent)
if env_path:
load_dotenv(dotenv_path=env_path)
print(f"[VOLTA] {GREEN}Loaded .env from {env_path}{RESET}")
else:
print(f"[VOLTA] {YELLOW}No .env file found from {__file__} upwards.{RESET}")
LOCALE = os.getenv("LOCALE") or None
locales_dirs.extend(find_locales_dirs(module_dir)) locales_dirs.extend(find_locales_dirs(module_dir))
if working_dir != module_dir: if working_dir != module_dir:

View file

@ -0,0 +1,26 @@
{
"bot": {
"prefix": "g.",
"owner_ids": [
],
"blacklisted_users": [],
"user_training": true,
"allow_show_mem_command": true,
"react_to_messages": true,
"misc": {
"ping_line": "The Beretta fires fast and won't make you feel any better!",
"active_song": "Basket Case - Green Day",
"positive_gifs": [],
"block_profanity": false
},
"active_memory": "memory.json",
"enabled_cogs": [
]
},
"locale": "fi",
"name": "goober",
"auto_update": true,
"disable_checks": false,
"splash_text_loc": "settings/splash.txt"
}

View file

@ -0,0 +1,11 @@
SS\
SS |
SSSSSS\ SSSSSS\ SSSSSS\ SSSSSSS\ SSSSSS\ SSSSSS\
SS __SS\ SS __SS\ SS __SS\ SS __SS\ SS __SS\ SS __SS\
SS / SS |SS / SS |SS / SS |SS | SS |SSSSSSSS |SS | \__|
SS | SS |SS | SS |SS | SS |SS | SS |SS ____|SS |
\SSSSSSS |\SSSSSS |\SSSSSS |SSSSSSS |\SSSSSSS\ SS |
\____SS | \______/ \______/ \_______/ \_______|\__|
SS\ SS |
\SSSSSS |
\______/