Compare commits

..

40 commits

Author SHA1 Message Date
19c55634b3 fixed trying to connect to localhost 2025-07-27 14:51:17 +03:00
4f4821d9fa added more logging to sync manager and added better examples for cogs 2025-07-27 14:36:38 +03:00
a20e9eb9f0 added sync hub 2025-07-27 12:24:51 +03:00
991264620a Improved updater 2025-07-26 22:31:50 +03:00
2c3946b67d Improved updater 2025-07-26 22:29:10 +03:00
d6c6605e2a improved updater 2025-07-26 22:24:17 +03:00
c0030b77b0 added restart 2025-07-26 22:18:39 +03:00
3705fd1866 updated readme 2025-07-26 20:09:13 +03:00
185ec16131 added handling for empty breaking news 2025-07-26 20:08:48 +03:00
9215602ecc added cpu stats 2025-07-26 14:36:20 +03:00
e8779f16df added cpu info to stats 2025-07-26 14:27:47 +03:00
6acb1f68d3 added ram details 2025-07-26 14:05:57 +03:00
7b951b5597 Fixed wrong arg 2025-07-26 13:25:16 +03:00
4043f3b5ff disabled exiting if too low memory 2025-07-26 13:23:08 +03:00
541c19e3ad added more logging to image gen 2025-07-26 00:29:20 +03:00
97cdd7be79 added warning for missing settings 2025-07-26 00:12:43 +03:00
068829702e added ability to customize actibity further, and changed settings to example 2025-07-26 00:10:31 +03:00
b67aebd9b1 changed string type for value 2025-07-25 23:44:09 +03:00
13038e3d20 fixed some string errors 2025-07-25 23:39:50 +03:00
375e5deaf2 made code work on < python3.12 2025-07-25 23:32:56 +03:00
90d301c054 made more commands respect memory setting 2025-07-25 23:22:16 +03:00
5773e5d083 made the locale option work 2025-07-25 23:19:24 +03:00
fe17dfb552 added breaking news cog and allowed cogs to have their own settings 2025-07-25 23:17:45 +03:00
f83f8deab5 fixed wrong variable name 2025-07-25 10:46:10 +03:00
369b9833d2 removed admin loG 2025-07-23 16:14:15 +03:00
9454ee9384 Updated gitignore 2025-07-23 16:13:25 +03:00
9f79928efd added decorator to mem command 2025-07-23 16:07:08 +03:00
ade9d88086 changed from creating new settings instance into using a shared one 2025-07-23 15:42:13 +03:00
7e21a10dde loaded default cogs 2025-07-23 14:52:24 +03:00
92dbc06b26 cleaned up bot.py seperating it into different files, added formatting ignore rules for key_compiler, modified prestart checks 2025-07-23 14:51:57 +03:00
f186e079da added permission wrapper 2025-07-23 10:19:08 +03:00
f7042ed8a7 rewrote env parts and fixed some frigging issues 2025-07-22 19:32:19 +03:00
99ab5d334e changed filesharing types 2025-07-22 13:20:21 +03:00
93b6589643 updated fileshare.py 2025-07-22 13:13:47 +03:00
18aa1c998f ?? 2025-07-22 13:09:15 +03:00
WhatDidYouExpect
5ff2a24dde
prolly shouldve done it b4 hand 2025-07-18 01:55:52 +02:00
WhatDidYouExpect
eb5109424b
Update README.md 2025-07-17 18:50:39 +02:00
WhatDidYouExpect
c56b2dbc32
Update README.md 2025-07-17 16:03:47 +02:00
WhatDidYouExpect
6a04ada599
Merge pull request #23 from gooberinc/dev
Dev
2025-07-16 19:29:29 +02:00
WhatDidYouExpect
754d760252
Merge pull request #22 from gooberinc/dev
Dev
2025-07-16 11:08:06 +02:00
64 changed files with 5808 additions and 1120 deletions

1
.env.example Normal file
View file

@ -0,0 +1 @@
DISCORD_BOT_TOKEN=""

3
.gitignore vendored
View file

@ -13,5 +13,6 @@ translation_report.txt
translationcompleteness.py translationcompleteness.py
modules/volta modules/volta
log.txt log.txt
settings/admin_logs.json
settings/settings.json settings/settings.json
settings/splash.txt assets/images/cached/*

2
.gitmodules vendored
View file

@ -1,3 +1,3 @@
[submodule "modules/volta"] [submodule "modules/volta"]
path = modules/volta path = modules/volta
url = https://forgejo.expect.ovh/gooberinc/volta url = https://github.com/gooberinc/volta

View file

@ -1,9 +1,2 @@
knockoff of genai basically :p Real repo: https://forgejo.expect.ovh/gooberinc/goober
THIS!! IS THE ACTUAL REPO!!!! NOT THE OTHER ONE!!! THIS ONE!!! This is just a fork that was made the upstream
Special thanks to [Charlie's Computers](https://github.com/PowerPCFan) for being the only one I know of that's hosting Goober 24/7
[Goober Central](https://github.com/whatdidyouexpect/goober-central)
[Another mirror](https://forgejo.expect.ovh/gooberinc/goober)
no promises that it'll be stable

28
assets/cogs/README.md Normal file
View file

@ -0,0 +1,28 @@
# 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
[TensorFlow integration](https://github.com/WhatDidYouExpect/goober/blob/main/cogs/tf.py)
by SuperSilly2 (requires Python 3.7 - 3.10, tensorflow-metal/tensorflow-gpu and tensorflow/tensorflow-macos)
[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

@ -0,0 +1,153 @@
from typing import List
import discord
from discord.ext import commands
import markovify
from PIL import Image, ImageDraw, ImageFont
import os
from modules.markovmemory import load_markov_model
from textwrap import wrap
import logging
from modules.settings import instance as settings_manager
import re
import time
from modules.sync_conenctor import instance as sync_hub
logger = logging.getLogger("goober")
settings = settings_manager.settings
class BreakingNews(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot: commands.Bot = bot
self.font_size = 90
self.image_margin = -25
self.font: ImageFont.FreeTypeFont = ImageFont.truetype(
os.path.join("assets", "fonts", "SpecialGothic.ttf"), self.font_size
)
self.model: markovify.NewlineText | None = load_markov_model()
@commands.command()
async def auto_create(self, ctx: commands.Context, enabled: str | None):
if enabled not in ["yes", "no"]:
await ctx.send(
f'Please use {settings["bot"]["prefix"]}auto_create <yes | no>'
)
return False
mode: bool = enabled == "yes"
settings_manager.set_plugin_setting(
"breaking_news", {"create_from_message_content": mode}
)
await ctx.send("Changed setting!")
@commands.Cog.listener()
async def on_message(self, message: discord.Message):
if not settings_manager.get_plugin_settings(
"breaking_news", {"create_from_message_content": False}
).get("create_from_message_content"):
logger.debug("Ignoring message - create_from_message_content not enabled")
return
if not message.content.lower().startswith("breaking news:"):
logger.debug("Ignoring message - doesnt start with breaking news:")
return
if not sync_hub.can_breaking_news(message.id):
logger.debug("Sync hub denied breaking news request")
return
texts = re.split("breaking news:", message.content, flags=re.IGNORECASE)
logger.debug(texts)
try:
text = texts[1].strip()
if not self.model:
await message.reply("No news specified and model not found!")
return False
text = text or self.model.make_sentence(max_chars=50, tries=50)
path = self.__insert_text(text)
except IndexError:
if self.model is None:
await message.reply("No model loaded and no breaking news specified")
return False
path = self.__insert_text(
self.model.make_sentence(max_chars=50, tries=50) or ""
)
await message.reply("You didn't specify any breaking news!")
with open(path, "rb") as f:
await message.reply(file=discord.File(f))
@commands.command()
async def breaking_news(self, ctx: commands.Context, *args):
if not self.model:
await ctx.send("Please supply a message!")
return False
message = " ".join(args) or self.model.make_sentence(max_chars=50, tries=50)
if not message:
await ctx.send("Please supply a message!")
return False
with open(self.__insert_text(message), "rb") as f:
await ctx.send(content="Breaking news!", file=discord.File(f))
def __insert_text(self, text):
start = time.time()
base_image_data: Image.ImageFile.ImageFile = Image.open(
os.path.join("assets", "images", "breaking_news.png")
)
base_image: ImageDraw.ImageDraw = ImageDraw.Draw(base_image_data)
MAX_IMAGE_WIDTH = base_image_data.width - self.image_margin
if len(text) * self.font_size > MAX_IMAGE_WIDTH:
parts = wrap(text, MAX_IMAGE_WIDTH // self.font_size)
logger.debug(parts)
for index, part in enumerate(parts):
text_size = base_image.textlength(part, self.font)
base_image.text(
(
self.image_margin / 2 + ((MAX_IMAGE_WIDTH - text_size) / 2),
(base_image_data.height * 0.2) + index * self.font_size,
),
part,
font=self.font,
)
else:
text_size = base_image.textlength(text, self.font)
base_image.text(
(
self.image_margin / 2 + ((MAX_IMAGE_WIDTH - text_size) / 2),
(base_image_data.height * 0.2),
),
text,
font=self.font,
)
path_folders = os.path.join("assets", "images", "cache")
os.makedirs(path_folders, exist_ok=True)
path = os.path.join(path_folders, "breaking_news.png")
with open(path, "wb") as f:
base_image_data.save(f)
logger.info(f"Generation took {time.time() - start}s")
return path
async def setup(bot: commands.Bot):
await bot.add_cog(BreakingNews(bot))

View file

@ -2,56 +2,40 @@ import random
import discord import discord
from discord.ext import commands from discord.ext import commands
class eightball(commands.Cog): class eightball(commands.Cog):
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
@commands.command() @commands.command()
async def eightball(self, ctx): async def eightball(self, ctx):
answer = random.randint(1, 20) answer = random.choice(
text = "Nothing" [
if answer==1: "It is certain.",
text = "It is certain." "It is decidedly so.",
elif answer==2: "Without a doubt.",
text = "It is decidedly so." "Yes definitely.",
elif answer==3: "You may rely on it.",
text = "Without a doubt." "As I see it, yes.",
elif answer==4: "Most likely.",
text = "Yes definitely." "Outlook good.",
elif answer==5: "Yes.",
text = "You may rely on it." "Signs point to yes.",
elif answer==6: "Reply hazy, try again.",
text = "As I see it, yes." "Ask again later.",
elif answer==7: "Better not tell you now.",
text = "Most likely." "Cannot predict now.",
elif answer==8: "Concentrate and ask again.",
text = "Outlook good." "Don't count on it.",
elif answer==9: "My reply is no.",
text = "Yes." "My sources say no.",
elif answer==10: "Outlook not so good.",
text = "Signs point to yes." "Very doubtful.",
elif answer==11: ]
text = "Reply hazy, try again." )
elif answer==12:
text = "Ask again later." await ctx.send(answer)
elif answer==13:
text = "Better not tell you now."
elif answer==14:
text = "Cannot predict now."
elif answer==15:
text = "Concentrate and ask again."
elif answer==16:
text = "Don't count on it."
elif answer==17:
text = "My reply is no."
elif answer==18:
text = "My sources say no."
elif answer==19:
text = "Outlook not so good."
elif answer==20:
text = "Very doubtful."
await ctx.send(text)
async def setup(bot): async def setup(bot):
await bot.add_cog(eightball(bot)) await bot.add_cog(eightball(bot))

131
assets/cogs/example.py Normal file
View file

@ -0,0 +1,131 @@
import discord
from discord.ext import commands
from discord import app_commands
import discord.ext
import discord.ext.commands
import random
from modules.permission import requires_admin
from modules.sentenceprocessing import send_message
from modules.settings import instance as settings_manager
from typing import TypedDict
# Name according to your cog (e.g a random number generator -> RandomNumber)
class Example(commands.Cog):
# __init__ method is required with these exact parameters
def __init__(self, bot: discord.ext.commands.Bot): # type hinting (aka : discord.ext.commands.Bot) isn't necessary, but provides better intellisense in code editors
self.bot: discord.ext.commands.Bot = bot
# a basic ping slash command which utilizes embeds
@app_commands.command(name="ping", description="A command that sends a ping!")
async def ping(self, interaction: discord.Interaction):
await interaction.response.defer()
example_embed = discord.Embed(
title="Pong!!",
description="The Beretta fires fast and won't make you feel any better!",
color=discord.Color.blue(),
)
example_embed.set_footer(
text=f"Requested by {interaction.user.name}",
icon_url=interaction.user.display_avatar,
)
await interaction.followup.send(embed=example_embed)
# a basic command (aka prefix.random_number)
# Shows how to get parameters, and how to send messages using goobers message thing
@commands.command()
async def random_number(self, ctx: commands.Context, minimum: int | None, maximum: int | None): # every argument after ctx is a part of the command, aka "g.random_number 0 5" would set minimum as 0 and maximum as 5
# We should always assume that command parameters are None, since someone can gall g.randon_number.
if minimum is None:
await send_message(ctx, message="Please specify the minimum number!")
return # make sure we dont continue
if maximum is None:
await send_message(ctx, message="Please specify the maximum number!")
return # make sure we dont continue
number = random.randint(minimum, maximum)
example_embed = discord.Embed(
title="Random number generator",
description=f"Random number: {number}",
color=discord.Color.blue(),
)
example_embed.set_footer(
text=f"Requested by {ctx.author.name}",
icon_url=ctx.author.display_avatar,
)
await send_message(ctx, embed=example_embed)
# A command which requires the executor to be an admin, and takes a discord user as an argument
@requires_admin() # from modules.permission import requires_admin
@commands.command()
async def ban_user(self, ctx: commands.Context, target: discord.Member | None, reason: str | None):
if target is None:
await send_message(ctx, "Please specify a user by pinging them!")
return
await target.ban(reason=reason)
await send_message(ctx, message=f"Banned user {target.name}!")
# Changing and getting plugin settings, defining a settings schmea
@commands.command()
async def change_hello_message(self, ctx: commands.Context, new_message: str | None):
COG_NAME = "example" # change this to whatever you want, but keep it the same accross your cog
if new_message is None:
await send_message(ctx, "Please specify a new message!")
return
# Generating a settings schema (optional)
# from typing import TypedDict
class IntroSettings(TypedDict):
message: str
class SettingsType(TypedDict):
intro: IntroSettings
leave_message: str
# End of optional typing
# Note: if you decide to do this, please place these at the top of the file! (but after imports)
default_settings: SettingsType = { # use default_settings = { if you didnt define the types
"intro": {
"message": "Hello user!"
},
"leave_message": "Goodbye user!"
}
# from modules.settings import instance as settings_manager
# get current plugin settings
# change "example" to your cog name
settings: SettingsType = settings_manager.get_plugin_settings(COG_NAME, default=default_settings) #type: ignore[assignment]
# Now you can use settings easily!
current_message = settings["intro"]["message"]
await send_message(ctx, message=f"Current message: {current_message}")
# Changing plugin settings
settings["intro"]["message"] = "brand new message!"
settings_manager.set_plugin_setting(COG_NAME, settings)
new_message = settings["intro"]["message"]
await send_message(ctx, message=f"New message: {new_message}")
async def setup(bot):
await bot.add_cog(Example(bot))

View file

@ -1,45 +1,70 @@
import discord import discord
import discord.context_managers
from discord.ext import commands from discord.ext import commands
import logging
from typing import Literal, get_args, cast
from modules.permission import requires_admin from modules.permission import requires_admin
from modules.settings import instance as settings_manager
settings = settings_manager.settings
logger = logging.getLogger("goober")
AvailableModes = Literal["r", "s"]
class FileSync(commands.Cog): class FileSync(commands.Cog):
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot: discord.Client = bot
self.mode = None self.mode: AvailableModes | None = None
self.peer_id = None self.peer_id = None
self.awaiting_file = False self.awaiting_file = False
@requires_admin() @requires_admin()
@commands.command() @commands.command()
async def syncfile(self, ctx, mode: str, peer: discord.User): async def syncfile(self, ctx: commands.Context, mode: str, peer: discord.User):
self.mode = mode.lower() if self.mode not in get_args(AvailableModes):
await ctx.send("Invalid mode, use 's' or 'r'.")
return
self.mode = cast(AvailableModes, mode.lower())
self.peer_id = peer.id self.peer_id = peer.id
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(settings["bot"]["active_memory"]))
await ctx.send("File sent in this channel.") await ctx.send("File sent in this channel.")
elif self.mode == "r": elif self.mode == "r":
await ctx.send("Waiting for incoming file...") await ctx.send("Waiting for incoming file...")
self.awaiting_file = True self.awaiting_file = True
else:
await ctx.send("Invalid mode, use 's' or 'r'.")
@commands.Cog.listener() @commands.Cog.listener()
async def on_message(self, message): async def on_message(self, message: discord.Message):
if message.author == self.bot.user or not self.awaiting_file: if message.author == self.bot.user or not self.awaiting_file:
return return
if message.author.id != self.peer_id: if message.author.id != self.peer_id:
return return
if message.content == "FILE_TRANSFER_REQUEST": if message.content == "FILE_TRANSFER_REQUEST":
print("Ping received. Awaiting file...") logger.info("Ping received. Awaiting file...")
if message.attachments: if not message.attachments:
for attachment in message.attachments: return
if attachment.filename.endswith(".json"):
filename = "received_memory.json" for attachment in message.attachments:
await attachment.save(filename) if not attachment.filename.endswith(".json"):
print(f"File saved as {filename}") continue
await message.channel.send("File received and saved.")
self.awaiting_file = False filename = "received_memory.json"
with open(filename, "wb") as f:
await attachment.save(f)
logger.info(f"File saved as {filename}")
await message.channel.send("File received and saved.")
self.awaiting_file = False
async def setup(bot): async def setup(bot):
await bot.add_cog(FileSync(bot)) await bot.add_cog(FileSync(bot))

103
assets/cogs/fuckup.py Normal file
View file

@ -0,0 +1,103 @@
import discord
from discord.ext import commands
from modules.image import *
from PIL import Image, ImageEnhance, ImageFilter, ImageOps, ImageChops, ImageColor
import os, random, shutil, tempfile
import modules.keys as k
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(k.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(k.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(k.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

@ -1,57 +1,38 @@
import os import os
import platform
import subprocess
from typing import Dict, List from typing import Dict, List
import discord import discord
from discord import Colour
from discord.ext import commands from discord.ext import commands
import discord.ext import discord.ext
import discord.ext.commands import discord.ext.commands
from modules.globalvars import local_version import modules.keys as k
from modules.volta.main import _ , set_language
from modules.permission import requires_admin from modules.permission import requires_admin
from modules.sentenceprocessing import send_message from modules.sentenceprocessing import send_message
from modules.settings import instance as settings_manager from modules.settings import instance as settings_manager
from modules.version import check_for_update
import requests import requests
import psutil
import cpuinfo
import sys
import subprocess
import updater
from modules.sync_conenctor import instance as sync_connector
settings = settings_manager.settings 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): class BaseCommands(commands.Cog):
def __init__(self, bot): def __init__(self, bot):
self.bot: discord.ext.commands.Bot = 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() @commands.command()
async def help(self, ctx: commands.Context) -> None: async def help(self, ctx: commands.Context) -> None:
embed: discord.Embed = discord.Embed( embed: discord.Embed = discord.Embed(
title=f"{_('command_help_embed_title')}", title=f"{k.command_help_embed_title()}",
description=f"{_('command_help_embed_desc')}", description=f"{k.command_help_embed_desc()}",
color=discord.Colour(0x000000), color=discord.Colour(0x000000),
) )
command_categories = { command_categories = {
f"{_('command_help_categories_general')}": [ f"{k.command_help_categories_general()}": [
"mem", "mem",
"talk", "talk",
"about", "about",
@ -60,7 +41,7 @@ class BaseCommands(commands.Cog):
"demotivator", "demotivator",
"help", "help",
], ],
f"{_('command_help_categories_admin')}": ["stats", "retrain", "setlanguage"], f"{k.command_help_categories_admin()}": ["stats", "retrain", "setlanguage"],
} }
custom_commands: List[str] = [] custom_commands: List[str] = []
@ -68,15 +49,15 @@ class BaseCommands(commands.Cog):
for command in cog.get_commands(): for command in cog.get_commands():
if ( if (
command.name command.name
not in command_categories[f"{_('command_help_categories_general')}"] not in command_categories[f"{k.command_help_categories_general()}"]
and command.name and command.name
not in command_categories[f"{_('command_help_categories_admin')}"] not in command_categories[f"{k.command_help_categories_admin()}"]
): ):
custom_commands.append(command.name) custom_commands.append(command.name)
if custom_commands: if custom_commands:
embed.add_field( embed.add_field(
name=_('command_help_categories_custom'), name=f"{k.command_help_categories_custom()}",
value="\n".join( value="\n".join(
[ [
f"{settings['bot']['prefix']}{command}" f"{settings['bot']['prefix']}{command}"
@ -92,14 +73,17 @@ class BaseCommands(commands.Cog):
) )
embed.add_field(name=category, value=commands_in_category, inline=False) embed.add_field(name=category, value=commands_in_category, inline=False)
await send_message(ctx, embed=embed) await send_message(ctx, embed=embed)
@requires_admin() @requires_admin()
@commands.command() @commands.command()
async def setlanguage(self, ctx: commands.Context, locale: str) -> None: async def setlanguage(self, ctx: commands.Context, locale: str) -> None:
await ctx.defer() await ctx.defer()
set_language(locale) k.change_language(locale)
settings["locale"] = locale # type: ignore
settings_manager.commit()
await ctx.send(":thumbsup:") await ctx.send(":thumbsup:")
@commands.command() @commands.command()
@ -111,12 +95,12 @@ class BaseCommands(commands.Cog):
title="Pong!!", title="Pong!!",
description=( description=(
settings["bot"]["misc"]["ping_line"], settings["bot"]["misc"]["ping_line"],
f"`{_('command_ping_embed_desc')}: {latency}ms`\n", f"`{k.command_ping_embed_desc()}: {latency}ms`\n",
), ),
color=discord.Colour(0x000000), color=discord.Colour(0x000000),
) )
embed.set_footer( embed.set_footer(
text=f"{_('command_ping_footer')} {ctx.author.name}", text=f"{k.command_ping_footer()} {ctx.author.name}",
icon_url=ctx.author.display_avatar.url, icon_url=ctx.author.display_avatar.url,
) )
@ -124,55 +108,87 @@ class BaseCommands(commands.Cog):
@commands.command() @commands.command()
async def about(self, ctx: commands.Context) -> None: async def about(self, ctx: commands.Context) -> None:
latest_version: str = check_for_update(slient=True) embed: discord.Embed = discord.Embed(
embed: discord.Embed = discord.Embed(title=f"{(_('command_about_embed_title'))}", description="", color=Colour(0x000000)) title=k.command_about_embed_title(),
embed.add_field(name=f"{(_('command_about_embed_field1'))}", value=f"{settings['name']}", inline=False) description="",
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) color=discord.Colour(0x000000),
embed.add_field(name=f"Git", value=get_git_origin_raw()) )
embed.add_field(name=f"OS", value=platform.platform())
embed.add_field(
name=k.command_about_embed_field1(),
value=settings['name'],
inline=False,
)
embed.add_field(name="Github", value=f"https://github.com/gooberinc/goober")
await send_message(ctx, embed=embed) await send_message(ctx, embed=embed)
@commands.command() @commands.command()
async def stats(self, ctx: commands.Context) -> None: async def stats(self, ctx: commands.Context) -> None:
memory_file: str = "memory.json" memory_file: str = settings["bot"]["active_memory"]
file_size: int = os.path.getsize(memory_file) file_size: int = os.path.getsize(memory_file)
memory_info = psutil.virtual_memory() # type: ignore
total_memory = memory_info.total / (1024**3)
used_memory = memory_info.used / (1024**3)
cpu_name = cpuinfo.get_cpu_info()["brand_raw"]
with open(memory_file, "r") as file: with open(memory_file, "r") as file:
line_count: int = sum(1 for _ in file) line_count: int = sum(1 for _ in file)
embed: discord.Embed = discord.Embed( embed: discord.Embed = discord.Embed(
title=f"{_('command_stats_embed_title')}", title=f"{k.command_stats_embed_title()}",
description=f"{_('command_stats_embed_desc')}", description=f"{k.command_stats_embed_desc()}",
color=discord.Colour(0x000000), color=discord.Colour(0x000000),
) )
embed.add_field( embed.add_field(
name=f"{_('command_stats_embed_field1name')}", name=f"{k.command_stats_embed_field1name()}",
value=f"placeholder", value=f"{k.command_stats_embed_field1value(file_size=file_size, line_count=line_count)}",
inline=False, inline=False,
) )
embed.add_field(
name=k.system_info(),
value=f"""
{k.memory_usage(used=round(used_memory,2), total=round(total_memory,2), percent=round(used_memory/total_memory * 100))}
{k.cpu_info(cpu_name)}
"""
)
with open(settings["splash_text_loc"], "r") as f: with open(settings["splash_text_loc"], "r") as f:
splash_text = "".join(f.readlines()) splash_text = "".join(f.readlines())
embed.add_field( embed.add_field(
name=_('command_stats_embed_field3name'), name=f"{k.command_stats_embed_field3name()}",
value=_('command_stats_embed_field3value').format( value=f"""{k.command_stats_embed_field3value(
NAME=settings["name"], NAME=settings["name"], PREFIX=settings["bot"]["prefix"], ownerid=settings["bot"]["owner_ids"][0],
PREFIX=settings["bot"]["prefix"], PING_LINE=settings["bot"]["misc"]["ping_line"], showmemenabled=settings["bot"]["allow_show_mem_command"],
ownerid=settings["bot"]["owner_ids"][0], USERTRAIN_ENABLED=settings["bot"]["user_training"], song=settings["bot"]["misc"]["activity"]["content"],
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 splashtext=splash_text
), )}""",
inline=False, inline=False,
) )
await send_message(ctx, embed=embed) await send_message(ctx, embed=embed)
@requires_admin()
@commands.command()
async def restart(self, ctx: commands.Context):
await ctx.send("Restarting...")
os.execv(sys.executable, [sys.executable] + sys.argv)
@requires_admin()
@commands.command()
async def force_update(self, ctx: commands.Context):
await ctx.send("Forcefully updating...")
updater.force_update()
os.execv(sys.executable, [sys.executable] + sys.argv)
@requires_admin()
@commands.command() @commands.command()
async def mem(self, ctx: commands.Context) -> None: async def mem(self, ctx: commands.Context) -> None:
if not settings["bot"]["allow_show_mem_command"]: if not settings["bot"]["allow_show_mem_command"]:
@ -189,6 +205,25 @@ class BaseCommands(commands.Cog):
await send_message(ctx, response.text) await send_message(ctx, response.text)
@requires_admin()
@commands.command()
async def test_synchub(self, ctx: commands.Context, message_id: str | None) -> None:
message_id = message_id or "0"
status = sync_connector.can_react(int(message_id))
await send_message(ctx, f"Is allowed to react to message id {message_id}? {status} (connection active? {sync_connector.connected})")
@requires_admin()
@commands.command()
async def connect_synchub(self, ctx: commands.Context) -> None:
await send_message(ctx, "Trying to connect...")
connected = sync_connector.try_to_connect()
if connected:
await send_message(ctx, "Succesfully connected to sync hub!")
else:
await send_message(ctx, "Failed to connect to sync hub")
async def setup(bot: discord.ext.commands.Bot): async def setup(bot: discord.ext.commands.Bot):
print("Setting up base_commands") print("Setting up base_commands")

View file

@ -1,15 +1,52 @@
import discord import discord
from discord.ext import commands from discord.ext import commands
import discord.ext
import discord.ext.commands
from modules.permission import requires_admin from modules.permission import requires_admin
from modules.settings import instance as settings_manager
from modules.globalvars import available_cogs
settings = settings_manager.settings
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() @requires_admin()
@commands.command() @commands.command()
async def load(self, ctx, cog_name: str = None): async def enable(self, ctx, cog_name: str):
try:
await self.bot.load_extension(COG_PREFIX + cog_name)
await ctx.send(f"Loaded cog `{cog_name}` successfully.")
settings["bot"]["enabled_cogs"].append(cog_name)
settings_manager.add_admin_log_event(
{
"action": "add",
"author": ctx.author.id,
"change": "enabled_cogs",
"messageId": ctx.message.id,
"target": cog_name,
}
)
settings_manager.commit()
except Exception as e:
await ctx.send(f"Error enabling cog `{cog_name}`: {e}")
@requires_admin()
@commands.command()
async def load(self, ctx, cog_name: str | None = None):
if cog_name is None:
await ctx.send("Give cog_name")
return
if cog_name[:-3] not in settings["bot"]["enabled_cogs"]:
await ctx.send("Please enable the cog first!")
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
@ -18,9 +55,10 @@ 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() @requires_admin()
@commands.command() @commands.command()
async def unload(self, ctx, cog_name: str = None): async def unload(self, ctx, cog_name: str | None = None):
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
@ -29,12 +67,40 @@ 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() @requires_admin()
@commands.command() @commands.command()
async def reload(self, ctx, cog_name: str = None): async def disable(self, ctx, cog_name: str | None = None):
if cog_name is None:
await ctx.send("Please provide the cog name to disable.")
return
try:
await self.bot.unload_extension(COG_PREFIX + cog_name)
await ctx.send(f"Unloaded cog `{cog_name}` successfully.")
settings["bot"]["enabled_cogs"].remove(cog_name)
settings_manager.add_admin_log_event(
{
"action": "del",
"author": ctx.author.id,
"change": "enabled_cogs",
"messageId": ctx.message.id,
"target": cog_name,
}
)
settings_manager.commit()
except Exception as e:
await ctx.send(f"Error unloading cog `{cog_name}`: {e}")
@requires_admin()
@commands.command()
async def reload(self, ctx, cog_name: str | None = None):
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
if cog_name[:-3] not in settings["bot"]["enabled_cogs"]:
await ctx.send("Please enable the cog first!")
return
try: try:
await self.bot.unload_extension(COG_PREFIX + cog_name) await self.bot.unload_extension(COG_PREFIX + cog_name)
await self.bot.load_extension(COG_PREFIX + cog_name) await self.bot.load_extension(COG_PREFIX + cog_name)
@ -50,9 +116,14 @@ class CogManager(commands.Cog):
await ctx.send("No cogs are currently loaded.") await ctx.send("No cogs are currently loaded.")
return return
embed = discord.Embed(title="Loaded Cogs", description="Here is a list of all currently loaded cogs:") embed = discord.Embed(
embed.add_field(name="Cogs", value="\n".join(cogs), inline=False) title="Loaded Cogs",
description="Here is a list of all currently loaded cogs:",
)
embed.add_field(name="Loaded cogs", value="\n".join(cogs), inline=False)
embed.add_field(name="Available cogs", value="\n".join(available_cogs()))
await ctx.send(embed=embed) await ctx.send(embed=embed)
async def setup(bot): async def setup(bot):
await bot.add_cog(CogManager(bot)) await bot.add_cog(CogManager(bot))

View file

@ -7,7 +7,11 @@ from discord.ext import commands
import discord.ext import discord.ext
import discord.ext.commands import discord.ext.commands
from modules.markovmemory import save_markov_model, train_markov_model, load_markov_model from modules.markovmemory import (
load_markov_model,
save_markov_model,
train_markov_model,
)
from modules.permission import requires_admin from modules.permission import requires_admin
from modules.sentenceprocessing import ( from modules.sentenceprocessing import (
improve_sentence_coherence, improve_sentence_coherence,
@ -15,7 +19,7 @@ from modules.sentenceprocessing import (
rephrase_for_coherence, rephrase_for_coherence,
send_message, send_message,
) )
from modules.volta.main import _ import modules.keys as k
import logging import logging
from typing import List, Optional, Set from typing import List, Optional, Set
import json import json
@ -33,13 +37,13 @@ class Markov(commands.Cog):
def __init__(self, bot): def __init__(self, bot):
self.bot: discord.ext.commands.Bot = bot self.bot: discord.ext.commands.Bot = bot
self.model: markovify.NewlineText | None = load_markov_model()
@requires_admin() @requires_admin()
@commands.command() @commands.command()
async def retrain(self, ctx: discord.ext.commands.Context): 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( message_ref: discord.Message | None = await send_message(
ctx, f"{_('command_markov_retrain')}" ctx, f"{k.command_markov_retrain()}"
) )
if message_ref is None: if message_ref is None:
@ -50,16 +54,16 @@ class Markov(commands.Cog):
with open(settings["bot"]["active_memory"], "r") as f: with open(settings["bot"]["active_memory"], "r") as f:
memory: List[str] = json.load(f) memory: List[str] = json.load(f)
except FileNotFoundError: except FileNotFoundError:
await send_message(ctx, f"{_('command_markov_memory_not_found')}") await send_message(ctx, f"{k.command_markov_memory_not_found()}")
return return
except json.JSONDecodeError: except json.JSONDecodeError:
await send_message(ctx, f"{_('command_markov_memory_is_corrupt')}") await send_message(ctx, f"{k.command_markov_memory_is_corrupt()}")
return return
data_size: int = len(memory) data_size: int = len(memory)
processing_message_ref: discord.Message | None = await send_message( processing_message_ref: discord.Message | None = await send_message(
ctx, f"{(_('command_markov_retraining').format(data_size=data_size))}" ctx, f"{k.command_markov_retraining(data_size)}"
) )
if processing_message_ref is None: if processing_message_ref is None:
logger.error("Couldnt find message processing message!") logger.error("Couldnt find message processing message!")
@ -72,40 +76,47 @@ class Markov(commands.Cog):
await ctx.send("Failed to retrain!") await ctx.send("Failed to retrain!")
return False return False
markov_model = model self.model = model
save_markov_model(markov_model) save_markov_model(self.model)
logger.debug(f"Completed retraining in {round(time.time() - start_time,3)}s") logger.debug(f"Completed retraining in {round(time.time() - start_time,3)}s")
await send_message( await send_message(
ctx, ctx,
_('command_markov_retraining').format(data_size=data_size), f"{k.command_markov_retrain_successful(data_size)}",
edit=True, edit=True,
message_reference=processing_message_ref, message_reference=processing_message_ref,
) )
@commands.command() @commands.command()
async def talk(self, ctx: commands.Context, sentence_size: int = 5) -> None: async def talk(self, ctx: commands.Context, sentence_size: int = 5) -> None:
markov_model: Optional[markovify.Text] = load_markov_model() if not self.model:
if markov_model is None: await send_message(ctx, f"{k.command_talk_insufficent_text()}")
await send_message(ctx, _("command_markovcommand_talk_insufficent_text"))
return return
raw_sentence = None response: str = ""
if sentence_size == 1: if sentence_size == 1:
raw_sentence = markov_model.make_short_sentence(max_chars=100, tries=100) response = (
self.model.make_short_sentence(max_chars=200, tries=700)
or k.command_talk_generation_fail()
)
else: else:
raw_sentence = markov_model.make_sentence(tries=100, max_words=sentence_size) response = improve_sentence_coherence(
print(raw_sentence) self.model.make_sentence(tries=100, max_words=sentence_size)
or k.command_talk_generation_fail()
)
if random.random() < 0.9 and is_positive(raw_sentence): cleaned_response: str = re.sub(r"[^\w\s]", "", response).lower()
gif_url = random.choice(settings["bot"]["misc"]["positive_gifs"]) coherent_response: str = rephrase_for_coherence(cleaned_response)
raw_sentence = f"{raw_sentence}\n[jif]({gif_url})"
os.environ["gooberlatestgen"] = raw_sentence if random.random() < 0.9 and is_positive(coherent_response):
await send_message(ctx, raw_sentence) gif_url: str = random.choice(settings["bot"]["misc"]["positive_gifs"])
coherent_response = f"{coherent_response}\n[jif]({gif_url})"
os.environ["gooberlatestgen"] = coherent_response
await send_message(ctx, coherent_response)
async def setup(bot): async def setup(bot):

View file

@ -15,7 +15,18 @@ class PermissionManager(commands.Cog):
@commands.command() @commands.command()
async def add_owner(self, ctx: commands.Context, member: discord.Member): async def add_owner(self, ctx: commands.Context, member: discord.Member):
settings["bot"]["owner_ids"].append(member.id) settings["bot"]["owner_ids"].append(member.id)
settings_manager.add_admin_log_event(
{
"action": "add",
"author": ctx.author.id,
"change": "owner_ids",
"messageId": ctx.message.id,
"target": member.id,
}
)
settings_manager.commit() settings_manager.commit()
embed = discord.Embed( embed = discord.Embed(
title="Permissions", title="Permissions",
description=f"Set {member.name} as an owner", description=f"Set {member.name} as an owner",
@ -29,6 +40,15 @@ class PermissionManager(commands.Cog):
async def remove_owner(self, ctx: commands.Context, member: discord.Member): async def remove_owner(self, ctx: commands.Context, member: discord.Member):
try: try:
settings["bot"]["owner_ids"].remove(member.id) settings["bot"]["owner_ids"].remove(member.id)
settings_manager.add_admin_log_event(
{
"action": "del",
"author": ctx.author.id,
"change": "owner_ids",
"messageId": ctx.message.id,
"target": member.id,
}
)
settings_manager.commit() settings_manager.commit()
except ValueError: except ValueError:
await ctx.send("User is not an owner!") await ctx.send("User is not an owner!")
@ -70,6 +90,15 @@ class PermissionManager(commands.Cog):
async def unblacklist_user(self, ctx: commands.Context, member: discord.Member): async def unblacklist_user(self, ctx: commands.Context, member: discord.Member):
try: try:
settings["bot"]["blacklisted_users"].remove(member.id) settings["bot"]["blacklisted_users"].remove(member.id)
settings_manager.add_admin_log_event(
{
"action": "del",
"author": ctx.author.id,
"change": "blacklisted_users",
"messageId": ctx.message.id,
"target": member.id,
}
)
settings_manager.commit() settings_manager.commit()
except ValueError: except ValueError:

94
assets/cogs/lastfm.py Normal file
View file

@ -0,0 +1,94 @@
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,123 +0,0 @@
import discord
from discord.ext import commands
from discord import app_commands
import aiohttp
import re
class Lyrics(commands.Cog):
def __init__(self, bot):
self.bot = bot
@app_commands.command(name="lyrics", description="Get lyrics for a song")
@app_commands.describe(
artist="Name of the artist",
song="Title of the song",
language="Target language code (optional)"
)
@app_commands.choices(language=[
app_commands.Choice(name="Bulgarian", value="bg"),
app_commands.Choice(name="Czech", value="cs"),
app_commands.Choice(name="Danish", value="da"),
app_commands.Choice(name="German", value="de"),
app_commands.Choice(name="Greek", value="el"),
app_commands.Choice(name="English", value="en"),
app_commands.Choice(name="Spanish", value="es"),
app_commands.Choice(name="Estonian", value="et"),
app_commands.Choice(name="Finnish", value="fi"),
app_commands.Choice(name="French", value="fr"),
app_commands.Choice(name="Irish", value="ga"),
app_commands.Choice(name="Croatian", value="hr"),
app_commands.Choice(name="Hungarian", value="hu"),
app_commands.Choice(name="Italian", value="it"),
app_commands.Choice(name="Lithuanian", value="lt"),
app_commands.Choice(name="Latvian", value="lv"),
app_commands.Choice(name="Maltese", value="mt"),
app_commands.Choice(name="Dutch", value="nl"),
app_commands.Choice(name="Polish", value="pl"),
app_commands.Choice(name="Portuguese", value="pt"),
app_commands.Choice(name="Romanian", value="ro"),
app_commands.Choice(name="Slovak", value="sk"),
app_commands.Choice(name="Slovene", value="sl"),
app_commands.Choice(name="Swedish", value="sv"),
])
async def lyrics(self, interaction: discord.Interaction, artist: str = None, song: str = None, language: app_commands.Choice[str] = None):
await interaction.response.defer()
if not artist or not song:
member = interaction.guild.get_member(interaction.user.id)
if not member:
member = await interaction.guild.fetch_member(interaction.user.id)
act_artist, act_song = await self.get_artist_song_from_presence(member)
if act_artist and act_song:
artist = artist or act_artist
song = song or act_song
else:
await interaction.followup.send("No artist or song provided and couldn't find it from your current activity.")
return
lyrics = await self.fetch_lyrics(artist, song)
if not lyrics:
await interaction.followup.send(f"Could not find lyrics for **{artist} - {song}**")
return
if language:
translated = await self.translate_text(lyrics, language.value)
if translated:
lyrics = translated
if len(lyrics) > 1900:
lyrics = lyrics[:1900] + "\n\n[...lyrics truncated...]"
embed = discord.Embed(
title=f"{artist} - {song}",
description=lyrics,
color=discord.Color.blue()
)
embed.set_footer(text=f"Requested by {interaction.user}", icon_url=interaction.user.display_avatar.url)
await interaction.followup.send(embed=embed)
async def get_artist_song_from_presence(self, member: discord.Member):
for activity in member.activities:
if isinstance(activity, discord.Spotify):
return activity.artist, activity.title
return None, None
async def fetch_lyrics(self, artist, song):
artist_q = artist.replace(' ', '+').lower()
song_q = song.replace(' ', '+').lower()
url = f"https://lrclib.net/api/get?artist_name={artist_q}&track_name={song_q}"
print(url)
async with aiohttp.ClientSession() as session:
try:
async with session.get(url) as resp:
if resp.status != 200:
return None
data = await resp.json()
return data.get('plainLyrics')
except Exception:
return None
async def translate_text(self, text: str, target_lang: str) -> str | None:
translate_url = "https://translate.googleapis.com/translate_a/single"
params = {
"client": "gtx",
"sl": "auto",
"tl": target_lang,
"dt": "t",
"q": text
}
async with aiohttp.ClientSession() as session:
try:
async with session.get(translate_url, params=params) as resp:
if resp.status != 200:
return None
result = await resp.json()
translated_chunks = [item[0] for item in result[0] if item[0]]
return ''.join(translated_chunks)
except Exception:
return None
async def setup(bot):
await bot.add_cog(Lyrics(bot))

View file

@ -2,7 +2,12 @@ from discord.ext import commands
import discord import discord
from collections import defaultdict, Counter from collections import defaultdict, Counter
import datetime import datetime
from modules.globalvars import ownerid from modules.permission import requires_admin
from modules.settings import instance as settings_manager
settings = settings_manager.settings
class StatsCog(commands.Cog): class StatsCog(commands.Cog):
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
@ -29,38 +34,54 @@ class StatsCog(commands.Cog):
async def on_command(self, ctx): async def on_command(self, ctx):
self.command_usage[ctx.command.qualified_name] += 1 self.command_usage[ctx.command.qualified_name] += 1
@requires_admin()
@commands.command() @commands.command()
async def spyware(self, ctx): async def spyware(self, ctx):
if ctx.author.id != ownerid:
return
uptime = datetime.datetime.utcnow() - self.start_time uptime = datetime.datetime.utcnow() - self.start_time
hours_elapsed = max((uptime.total_seconds() / 3600), 1) hours_elapsed = max((uptime.total_seconds() / 3600), 1)
avg_per_hour = self.total_messages / hours_elapsed avg_per_hour = self.total_messages / hours_elapsed
if self.messages_per_hour: if self.messages_per_hour:
peak_hour, peak_count = max(self.messages_per_hour.items(), key=lambda x: x[1]) peak_hour, peak_count = max(
self.messages_per_hour.items(), key=lambda x: x[1]
)
else: else:
peak_hour, peak_count = "N/A", 0 peak_hour, peak_count = "N/A", 0
top_users = self.user_message_counts.most_common(5) top_users = self.user_message_counts.most_common(5)
embed = discord.Embed(title="Community Stats", color=discord.Color.blue()) embed = discord.Embed(title="Community Stats", color=discord.Color.blue())
embed.add_field(name="Uptime", value=str(uptime).split('.')[0], inline=False) embed.add_field(name="Uptime", value=str(uptime).split(".")[0], inline=False)
embed.add_field(name="Total Messages", value=str(self.total_messages), inline=True) embed.add_field(
embed.add_field(name="Active Users", value=str(len(self.active_users)), inline=True) name="Total Messages", value=str(self.total_messages), inline=True
embed.add_field(name="Avg Messages/Hour", value=f"{avg_per_hour:.2f}", inline=True) )
embed.add_field(name="Peak Hour (UTC)", value=f"{peak_hour}: {peak_count} messages", inline=True) embed.add_field(
name="Active Users", value=str(len(self.active_users)), inline=True
)
embed.add_field(
name="Avg Messages/Hour", value=f"{avg_per_hour:.2f}", inline=True
)
embed.add_field(
name="Peak Hour (UTC)",
value=f"{peak_hour}: {peak_count} messages",
inline=True,
)
top_str = "\n".join( top_str = (
f"<@{user_id}>: {count} messages" for user_id, count in top_users "\n".join(f"<@{user_id}>: {count} messages" for user_id, count in top_users)
) or "No data" or "No data"
)
embed.add_field(name="Top Chatters", value=top_str, inline=False) embed.add_field(name="Top Chatters", value=top_str, inline=False)
cmd_str = "\n".join( cmd_str = (
f"{cmd}: {count}" for cmd, count in self.command_usage.most_common(5) "\n".join(
) or "No commands used yet" f"{cmd}: {count}" for cmd, count in self.command_usage.most_common(5)
)
or "No commands used yet"
)
embed.add_field(name="Top Commands", value=cmd_str, inline=False) embed.add_field(name="Top Commands", value=cmd_str, inline=False)
await ctx.send(embed=embed) await ctx.send(embed=embed)
async def setup(bot): async def setup(bot):
await bot.add_cog(StatsCog(bot)) await bot.add_cog(StatsCog(bot))

View file

@ -1,22 +0,0 @@
import discord
from discord.ext import commands
from discord import app_commands
class Ping(commands.Cog):
def __init__(self, bot):
self.bot = bot
@app_commands.command(name="slashcommand", description="slashcommandexample")
async def ping(self, interaction: discord.Interaction):
await interaction.response.defer()
exampleembed = discord.Embed(
title="Pong!!",
description="The Beretta fires fast and won't make you feel any better!",
color=discord.Color.blue()
)
exampleembed.set_footer(text=f"Requested by {interaction.user.name}", icon_url=interaction.user.avatar.url)
await interaction.followup.send(embed=exampleembed)
async def setup(bot):
await bot.add_cog(Ping(bot))

View file

@ -0,0 +1,38 @@
import discord
from discord.ext import commands
from modules.globalvars import RED, GREEN, RESET, LOCAL_VERSION_FILE
import os
from modules.permission import requires_admin
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()
@requires_admin()
@commands.command()
async def changesong(self, ctx, song: str):
await ctx.send(f"Changed song to {song}")
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))

191
assets/cogs/tf.py Normal file
View file

@ -0,0 +1,191 @@
import discord
from discord.ext import commands
import os
import numpy as np
import json
import pickle
import functools
import re
import time
import asyncio
ready = True
MODEL_MATCH_STRING = r"[0-9]{2}_[0-9]{2}_[0-9]{4}-[0-9]{2}_[0-9]{2}"
try:
import tensorflow as tf
import keras
from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences
from keras.models import Sequential, load_model
from keras.layers import Embedding, LSTM, Dense
from keras.backend import clear_session
if tf.config.list_physical_devices("GPU"):
print("Using GPU acceleration")
elif tf.config.list_physical_devices("Metal"):
print("Using Metal for macOS acceleration")
except ImportError:
print(
"ERROR: Failed to import TensorFlow. Ensure you have the correct dependencies:"
)
print("tensorflow>=2.15.0")
print("For macOS (Apple Silicon): tensorflow-metal")
ready = False
class TFCallback(keras.callbacks.Callback):
def __init__(self, bot, progress_embed: discord.Embed, message):
self.embed = progress_embed
self.bot = bot
self.message = message
self.times = [time.time()]
async def send_message(self, message: str, description: str, **kwargs):
if "epoch" in kwargs:
self.times.append(time.time())
avg_epoch_time = np.mean(np.diff(self.times))
description = f"ETA: {round(avg_epoch_time)}s"
self.embed.add_field(
name=f"<t:{round(time.time())}:t> - {message}",
value=description,
inline=False,
)
await self.message.edit(embed=self.embed)
def on_train_end(self, logs=None):
self.bot.loop.create_task(
self.send_message("Training stopped", "Training has been stopped.")
)
def on_epoch_begin(self, epoch, logs=None):
self.bot.loop.create_task(
self.send_message(
f"Starting epoch {epoch}", "This might take a while", epoch=True
)
)
def on_epoch_end(self, epoch, logs=None):
self.bot.loop.create_task(
self.send_message(
f"Epoch {epoch} ended",
f"Accuracy: {round(logs.get('accuracy', 0.0), 4)}",
)
)
class Ai:
def __init__(self):
model_path = settings.get("model_path")
if model_path:
self.__load_model(model_path)
self.is_loaded = model_path is not None
self.batch_size = 64
def generate_model_name(self):
return time.strftime("%d_%m_%Y-%H_%M", time.localtime())
def __load_model(self, model_path):
clear_session()
self.model = load_model(os.path.join(model_path, "model.h5"))
model_name = os.path.basename(model_path)
try:
with open(os.path.join(model_path, "tokenizer.pkl"), "rb") as f:
self.tokenizer = pickle.load(f)
except FileNotFoundError:
print("Failed to load tokenizer, using default.")
self.tokenizer = Tokenizer()
with open("memory.json", "r") as f:
self.tokenizer.fit_on_texts(json.load(f))
self.is_loaded = True
def reload_model(self):
clear_session()
model_path = settings.get("model_path")
if model_path:
self.__load_model(model_path)
self.is_loaded = True
async def run_async(self, func, bot, *args, **kwargs):
return await bot.loop.run_in_executor(
None, functools.partial(func, *args, **kwargs)
)
class Learning(Ai):
def create_model(self, memory, epochs=2):
memory = memory[:2000]
tokenizer = Tokenizer()
tokenizer.fit_on_texts(memory)
sequences = tokenizer.texts_to_sequences(memory)
X, y = [], []
for seq in sequences:
for i in range(1, len(seq)):
X.append(seq[:i])
y.append(seq[i])
maxlen = max(map(len, X))
X = pad_sequences(X, maxlen=maxlen, padding="pre")
y = np.array(y)
model = Sequential(
[
Embedding(input_dim=VOCAB_SIZE, output_dim=128, input_length=maxlen),
LSTM(64),
Dense(VOCAB_SIZE, activation="softmax"),
]
)
model.compile(
optimizer="adam",
loss="sparse_categorical_crossentropy",
metrics=["accuracy"],
)
history = model.fit(X, y, epochs=epochs, batch_size=64, callbacks=[tf_callback])
self.save_model(model, tokenizer, history)
def save_model(self, model, tokenizer, history, name=None):
name = name or self.generate_model_name()
model_dir = os.path.join("models", name)
os.makedirs(model_dir, exist_ok=True)
with open(os.path.join(model_dir, "info.json"), "w") as f:
json.dump(history.history, f)
with open(os.path.join(model_dir, "tokenizer.pkl"), "wb") as f:
pickle.dump(tokenizer, f)
model.save(os.path.join(model_dir, "model.h5"))
class Generation(Ai):
def generate_sentence(self, word_amount, seed):
if not self.is_loaded:
return False
for _ in range(word_amount):
token_list = self.tokenizer.texts_to_sequences([seed])[0]
token_list = pad_sequences(
[token_list], maxlen=self.model.input_shape[1], padding="pre"
)
predicted_word_index = np.argmax(
self.model.predict(token_list, verbose=0), axis=-1
)[0]
output_word = next(
(
w
for w, i in self.tokenizer.word_index.items()
if i == predicted_word_index
),
"",
)
seed += " " + output_word
return seed
VOCAB_SIZE = 100_000
settings = {}
learning = Learning()
generation = Generation()
tf_callback = None
async def setup(bot):
await bot.add_cog(Tf(bot))

111
assets/cogs/webscraper.py Normal file
View file

@ -0,0 +1,111 @@
import discord
from discord.ext import commands
import aiohttp
from bs4 import BeautifulSoup
import json
import asyncio
from urllib.parse import urljoin
from modules.permission import requires_admin
from modules.settings import instance as settings_manager
settings = settings_manager.settings
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)
@requires_admin()
@commands.command()
async def start_scrape(self, ctx, start_url: str):
"""Command to start the scraping process."""
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.")
@requires_admin()
@commands.command()
async def undo_scrape(self, ctx):
"""Command to undo the last scrape."""
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))

921
assets/cogs/webserver.py Normal file
View file

@ -0,0 +1,921 @@
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):
if self.site is None or self.runner is None:
return
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))

View file

@ -1,6 +1,7 @@
import discord import discord
from discord.ext import commands from discord.ext import commands
class whoami(commands.Cog): class whoami(commands.Cog):
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
@ -13,12 +14,13 @@ class whoami(commands.Cog):
embed = discord.Embed( embed = discord.Embed(
title="User Information", title="User Information",
description=f"Your User ID is: {user_id}\n" description=f"Your User ID is: {user_id}\n"
f"Your username is: {username}\n" f"Your username is: {username}\n"
f"Your nickname in this server is: <@{user_id}>", f"Your nickname in this server is: <@{user_id}>",
color=discord.Color.blue() color=discord.Color.blue(),
) )
await ctx.send(embed=embed) await ctx.send(embed=embed)
async def setup(bot): async def setup(bot):
await bot.add_cog(whoami(bot)) await bot.add_cog(whoami(bot))

BIN
assets/fonts/Impact.ttf Normal file

Binary file not shown.

Binary file not shown.

BIN
assets/fonts/TNR.ttf Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

BIN
assets/images/bibinos.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

BIN
assets/images/cache/breaking_news.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

BIN
assets/images/crash.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

BIN
assets/images/crash2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 746 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

View file

@ -1,23 +1,8 @@
{ {
"minigames_hangman_game": "Word: {display_word()}\nWrong guesses: {wrong_guesses}/{max_wrong}",
"minigames_hangman_lost": "You lost! The word was:",
"minigames_hangman_won": "You won! The word was:",
"minigames_hangman_already_guessed": "You already guessed",
"minigames_hangman_user_letter_guess": "Your letter guess",
"minigames_hangman_guess": "Guess a Letter",
"minigames_hangman_api_failed": "Failed to get a random word.",
"minigames_hangman": "Play Hangman with a random word",
"minigames_click_to_guess": "Click to guess a number from 1 to 10",
"minigames_guess_button": "Guess",
"minigames_wrong_number": "Wrong! The number was",
"minigames_correct": "Correct!",
"minigames_invalid_number": "Invalid number!",
"minigames_guess_the_number": "Guess the number",
"minigames_your_guess": "Your guess (1-10)",
"memory_file_valid": "The memory.json file is valid!", "memory_file_valid": "The memory.json file is valid!",
"file_aint_uft8": "File is not valid UTF-8 text. Might be binary or corrupted.", "file_aint_uft8": "File is not valid UTF-8 text. Might be binary or corrupted.",
"psutil_not_installed": "Memory check skipped.", "psutil_not_installed": "Memory check skipped.",
"not_cloned": "Goober is not cloned! Please clone it from Git.", "not_cloned": "Goober is not cloned! Please clone it from GitHub.",
"checks_disabled": "Checks are disabled!", "checks_disabled": "Checks are disabled!",
"unhandled_exception": "An unhandled exception occurred. Please report this issue on GitHub.", "unhandled_exception": "An unhandled exception occurred. Please report this issue on GitHub.",
"active_users:": "Active users:", "active_users:": "Active users:",
@ -146,6 +131,14 @@
"command_stats_embed_field2name": "Version", "command_stats_embed_field2name": "Version",
"command_stats_embed_field2value": "Local: {local_version} \nLatest: {latest_version}", "command_stats_embed_field2value": "Local: {local_version} \nLatest: {latest_version}",
"command_stats_embed_field3name": "Variable Info", "command_stats_embed_field3name": "Variable Info",
"command_stats_embed_field3value": "Name: {NAME} \nPrefix: {PREFIX} \nOwner ID: {ownerid}\nPing line: {PING_LINE} \nMemory Sharing Enabled: {showmemenabled} \nUser Training Enabled: {USERTRAIN_ENABLED}\nSong: {song} \nSplashtext: ```{splashtext}```" "command_stats_embed_field3value": "Name: {NAME} \nPrefix: {PREFIX} \nOwner ID: {ownerid}\nPing line: {PING_LINE} \nMemory Sharing Enabled: {showmemenabled} \nUser Training Enabled: {USERTRAIN_ENABLED}\nSong: {song} \nSplashtext: ```{splashtext}```",
"no_image_available": "No images available!",
"failed_generate_image": "Failed to generate an image",
"markov_model_not_found": "Markov model not found!",
"blacklisted": "blacklisted",
"blacklisted_user": "Blacklisted user",
"edit_fail": "Failed to edit message",
"system_info": "System information",
"cpu_info": "CPU: {cpu}"
} }

View file

@ -102,7 +102,7 @@
"command_markov_retrain": "Uudelleenkoulutetaan markov-mallia... Odota.", "command_markov_retrain": "Uudelleenkoulutetaan markov-mallia... Odota.",
"command_markov_memory_not_found": "Virhe: muistitiedostoa ei löytynyt!", "command_markov_memory_not_found": "Virhe: muistitiedostoa ei löytynyt!",
"command_markov_memory_is_corrupt": "Virhe: muistitiedosto on korruptoitu!", "command_markov_memory_is_corrupt": "Virhe: muistitiedosto on korruptoitu!",
"command_markov_retraining": "Käsitellään {processed_data}/{data_size} datapisteestä...", "command_markov_retraining": "Käsitellään {processed_data}/{data_size} datapistettä...",
"command_markov_retrain_successful": "Markov-malli koulutettiin uudestaan {data_size} datapisteellä!", "command_markov_retrain_successful": "Markov-malli koulutettiin uudestaan {data_size} datapisteellä!",
"command_desc_talk":"puhuu ja sillei", "command_desc_talk":"puhuu ja sillei",
"command_talk_insufficent_text": "Minun pitää oppia lisää viesteistä ennen kun puhun.", "command_talk_insufficent_text": "Minun pitää oppia lisää viesteistä ennen kun puhun.",
@ -131,5 +131,6 @@
"command_stats_embed_field2value": "Paikallinen: {local_version} \nUusin: {latest_version}", "command_stats_embed_field2value": "Paikallinen: {local_version} \nUusin: {latest_version}",
"command_stats_embed_field3name": "Muuttajainformaatio", "command_stats_embed_field3name": "Muuttajainformaatio",
"command_stats_embed_field3value": "Nimi: {NAME} \nEtuliite: {PREFIX} \nOmistajan ID: {ownerid}\nPing-linja: {PING_LINE} \nMuistin jako päällä: {showmemenabled} \nOppiminen käyttäjistä: {USERTRAIN_ENABLED}\nLaulu: {song} \nRoisketeksti: ```{splashtext}```" "command_stats_embed_field3value": "Nimi: {NAME} \nEtuliite: {PREFIX} \nOmistajan ID: {ownerid}\nPing-linja: {PING_LINE} \nMuistin jako päällä: {showmemenabled} \nOppiminen käyttäjistä: {USERTRAIN_ENABLED}\nLaulu: {song} \nRoisketeksti: ```{splashtext}```"
} }

130
assets/locales/fr.json Normal file
View file

@ -0,0 +1,130 @@
{
"checks_disabled": "Les vérifications sont désactivées !",
"unhandled_exception": "Une exception non gérée est survenue. Merci de rapporter ce problème sur GitHub.",
"active_users:": "Utilisateurs actifs :",
"spacy_initialized": "spaCy et spacytextblob sont prêts.",
"spacy_model_not_found": "Le modèle spaCy est introuvable ! Téléchargement en cours...",
"env_file_not_found": "Le fichier .env est introuvable ! Créez-en un avec les variables nécessaires.",
"error_fetching_active_users": "Erreur lors de la récupération des utilisateurs actifs : {error}",
"error_sending_alive_ping": "Erreur lors de lenvoi du ping actif : {error}",
"already_started": "Jai déjà démarré ! Je ne me mets pas à jour...",
"please_restart": "Redémarre, stp !",
"local_ahead": "Local {remote}/{branch} est en avance ou à jour. Pas de mise à jour...",
"remote_ahead": "Remote {remote}/{branch} est en avance. Mise à jour en cours...",
"cant_find_local_version": "Je ne trouve pas la variable local_version ! Ou elle a été modifiée et ce nest pas un entier !",
"running_prestart_checks": "Exécution des vérifications préalables au démarrage...",
"continuing_in_seconds": "Reprise dans {seconds} secondes... Appuie sur une touche pour passer.",
"missing_requests_psutil": "requests et psutil manquants ! Installe-les avec pip : `pip install requests psutil`",
"requirements_not_found": "requirements.txt introuvable à {path}, a-t-il été modifié ?",
"warning_failed_parse_imports": "Avertissement : Échec du parsing des imports depuis {filename} : {error}",
"cogs_dir_not_found": "Répertoire des cogs introuvable à {path}, scan ignoré.",
"std_lib_local_skipped": "LIB STD / LOCAL {package} (vérification sautée)",
"ok_installed": "OK",
"missing_package": "MANQUANT",
"missing_package2": "nest pas installé",
"missing_packages_detected": "Packages manquants détectés :",
"telling_goober_central": "Envoi à goober central à {url}",
"failed_to_contact": "Impossible de contacter {url} : {error}",
"all_requirements_satisfied": "Toutes les dépendances sont satisfaites.",
"ping_to": "Ping vers {host} : {latency} ms",
"high_latency": "Latence élevée détectée ! Tu pourrais avoir des délais de réponse.",
"could_not_parse_latency": "Impossible danalyser la latence.",
"ping_failed": "Ping vers {host} échoué.",
"error_running_ping": "Erreur lors du ping : {error}",
"memory_usage": "Utilisation mémoire : {used} Go / {total} Go ({percent}%)",
"memory_above_90": "Usage mémoire au-dessus de 90% ({percent}%). Pense à libérer de la mémoire.",
"total_memory": "Mémoire totale : {total} Go",
"used_memory": "Mémoire utilisée : {used} Go",
"low_free_memory": "Mémoire libre faible détectée ! Seulement {free} Go disponibles.",
"measuring_cpu": "Mesure de lusage CPU par cœur...",
"core_usage": "Cœur {idx} : [{bar}] {usage}%",
"total_cpu_usage": "Usage total CPU : {usage}%",
"high_avg_cpu": "Moyenne CPU élevée : {usage}%",
"really_high_cpu": "Charge CPU vraiment élevée ! Le système pourrait ralentir ou planter.",
"memory_file": "Fichier mémoire : {size} Mo",
"memory_file_large": "Fichier mémoire de 1 Go ou plus, pense à le nettoyer pour libérer de lespace.",
"memory_file_corrupted": "Fichier mémoire corrompu ! Erreur JSON : {error}",
"consider_backup_memory": "Pense à sauvegarder et recréer le fichier mémoire.",
"memory_file_encoding": "Problèmes dencodage du fichier mémoire : {error}",
"error_reading_memory": "Erreur lecture fichier mémoire : {error}",
"memory_file_not_found": "Fichier mémoire introuvable.",
"modification_warning": "Goober a été modifié ! Toutes les modifications seront perdues lors d'une mise à jour !",
"reported_version": "Version rapportée :",
"current_hash": "Hachage actuel :",
"not_found": "n'est pas trouvé !",
"version_error": "Impossible de récupérer les informations de version. Code d'état",
"loaded_cog": "Cog chargé :",
"loaded_cog2": "Module chargé :",
"cog_fail": "Échec du chargement du cog :",
"cog_fail2": "Échec du chargement du module :",
"no_model": "Aucun modèle Markov sauvegardé trouvé. Démarrage à partir de zéro.",
"folder_created": "Dossier '{folder_name}' créé.",
"folder_exists": "Le dossier '{folder_name}' existe déjà. Ignorons...",
"logged_in": "Connecté en tant que",
"synced_commands": "Synchronisé",
"synced_commands2": "commandes !",
"fail_commands_sync": "Échec de la synchronisation des commandes :",
"started": "{name} a démarré !",
"name_check": "Erreur lors de la vérification de la disponibilité du nom :",
"name_taken": "Le nom est déjà pris. Veuillez choisir un autre nom.",
"name_check2": "Erreur lors de la vérification de la disponibilité du nom :",
"add_token": "Token : {token}\nVeuillez ajouter ce token à votre fichier .env comme",
"token_exists": "Le token existe déjà dans .env. Utilisation du token existant.",
"registration_error": "Erreur lors de l'enregistrement :",
"version_backup": "Sauvegarde créée :",
"backup_error": "Erreur : {LOCAL_VERSION_FILE} introuvable pour la sauvegarde.",
"model_loaded": "Modèle Markov chargé depuis",
"fetch_update_fail": "Impossible de récupérer les informations de mise à jour.",
"invalid_server": "Erreur : Informations de version invalides reçues du serveur.",
"goober_server_alert": "Alerte du serveur Goober central !\n",
"new_version": "Nouvelle version disponible : {latest_version} (Actuelle : {local_version})",
"changelog": "Consultez {VERSION_URL}/goob/changes.txt pour voir les modifications\n\n",
"invalid_version": "La version : {local_version} n'est pas valide !",
"invalid_version2": "Si c'est intentionnel, ignorez ce message. Sinon, appuyez sur Y pour récupérer une version valide depuis le serveur, quelle que soit la version actuelle de Goober.",
"invalid_version3": "La version actuelle sera sauvegardée dans current_version.bak..",
"input": "(Y ou toute autre touche pour ignorer...)",
"modification_ignored": "Vous avez modifié",
"modification_ignored2": "IGNOREWARNING est désactivé..",
"latest_version": "Vous utilisez la dernière version :",
"latest_version2": "Consultez {VERSION_URL}/goob/changes.txt pour voir les modifications",
"pinging_disabled": "Le ping est désactivé ! Je ne préviens pas le serveur que je suis en ligne...",
"goober_ping_success": "Connecté à Goober central en tant que {NAME}",
"goober_ping_fail": "Échec de l'envoi des données. Le serveur a retourné le code d'état :",
"goober_ping_fail2": "Une erreur est survenue lors de l'envoi des données :",
"sentence_positivity": "La positivité de la phrase est :",
"command_edit_fail": "Échec de la modification du message :",
"command_desc_retrain": "Réentraîne manuellement le modèle Markov.",
"command_markov_retrain": "Réentraînement du modèle Markov... Veuillez patienter.",
"command_markov_memory_not_found": "Erreur : fichier de mémoire introuvable !",
"command_markov_memory_is_corrupt": "Erreur : le fichier de mémoire est corrompu !",
"command_markov_retraining": "Traitement de {processed_data}/{data_size} points de données...",
"command_markov_retrain_successful": "Modèle Markov réentraîné avec succès en utilisant {data_size} points de données !",
"command_desc_talk": "parle et tout ça",
"command_talk_insufficent_text": "Je dois apprendre plus de messages avant de pouvoir parler.",
"command_talk_generation_fail": "Je n'ai rien à dire pour le moment !",
"command_desc_help": "aide",
"command_help_embed_title": "Aide du bot",
"command_help_embed_desc": "Liste des commandes regroupées par catégorie.",
"command_help_categories_general": "Général",
"command_help_categories_admin": "Administration",
"command_help_categories_custom": "Commandes personnalisées",
"command_ran": "Info : {message.author.name} a exécuté {message.content}",
"command_ran_s": "Info : {interaction.user} a exécuté ",
"command_desc_ping": "ping",
"command_ping_embed_desc": "Latence du bot :",
"command_ping_footer": "Demandé par",
"command_about_desc": "à propos",
"command_about_embed_title": "À propos de moi",
"command_about_embed_field1": "Nom",
"command_about_embed_field2name": "Version",
"command_about_embed_field2value": "Locale : {local_version} \nDernière : {latest_version}",
"command_desc_stats": "statistiques",
"command_stats_embed_title": "Statistiques du bot",
"command_stats_embed_desc": "Données sur la mémoire du bot.",
"command_stats_embed_field1name": "Statistiques du fichier",
"command_stats_embed_field1value": "Taille : {file_size} octets\nLignes : {line_count}",
"command_stats_embed_field2name": "Version",
"command_stats_embed_field2value": "Locale : {local_version} \nDernière : {latest_version}",
"command_stats_embed_field3name": "Informations variables",
"command_stats_embed_field3value": "Nom : {NAME} \nPréfixe : {PREFIX} \nID du propriétaire : {ownerid}\nLigne de ping : {PING_LINE} \nPartage de mémoire activé : {showmemenabled} \nEntraînement utilisateur activé : {USERTRAIN_ENABLED} \nChanson : {song} \nTexte de démarrage : ```{splashtext}```"
}

View file

@ -1,149 +0,0 @@
{
"minigames_hangman_game": "Mot à deviner : {display_word()}\nMauvaises guesses : {wrong_guesses}/{max_wrong}",
"minigames_hangman_lost": "T'es échoué solide! Le mot était :",
"minigames_hangman_won": "T'as gagné en masse! Le mot était :",
"minigames_hangman_already_guessed": "T'as déjà essayé ça mon chum",
"minigames_hangman_user_letter_guess": "Ta guess de lettre",
"minigames_hangman_guess": "Devine une lettre",
"minigames_hangman_api_failed": "Ça a chié en essayant d'avoir un mot aléatoire.",
"minigames_hangman": "Jouer au Pendu avec un mot pogné au hasard",
"minigames_click_to_guess": "Clique pour deviner un chiffre entre 1 pis 10",
"minigames_guess_button": "Devine",
"minigames_wrong_number": "Nope! C'était",
"minigames_correct": "Bonne guess!",
"minigames_invalid_number": "Chiffre pas valide!",
"minigames_guess_the_number": "Devine le chiffre",
"minigames_your_guess": "Ta guess (1-10)",
"memory_file_valid": "Le fichier memory.json est correct!",
"file_aint_uft8": "Le fichier est pas du bon UTF-8. Ça doit être binaire ou scrap.",
"psutil_not_installed": "Vérification de mémoire skipée.",
"not_cloned": "Goober est pas cloné! Va donc le cloner depuis Git.",
"checks_disabled": "Les checks sont désactivées!",
"unhandled_exception": "Y'a eu une erreur pas prévue. Rapporte ça sur GitHub mon gars.",
"active_users:": "Monde actif :",
"spacy_initialized": "spaCy pis spacytextblob sont prêts.",
"spacy_model_not_found": "Le modèle spaCy est introuvable! On le télécharge...",
"env_file_not_found": "Le fichier .env est pas là! Fais-en un avec les variables nécessaires.",
"error_fetching_active_users": "Ça a chié en essayant de pogner les utilisateurs actifs : {error}",
"error_sending_alive_ping": "Ça a chié en envoyant le ping : {error}",
"already_started": "J'suis déjà parti! J'me mets pas à jour...",
"please_restart": "Redémarre-moi donc!",
"local_ahead": "La version locale {remote}/{branch} est à jour. Pas besoin d'update...",
"remote_ahead": "La version remote {remote}/{branch} est en avance. On update...",
"cant_find_local_version": "J'arrive pas à trouver la variable local_version! Ou ben elle a été modifiée pis c'est pas un chiffre!",
"running_prestart_checks": "On fait les checks avant de partir...",
"continuing_in_seconds": "On continue dans {seconds} secondes... Appuie sur une touche pour skip.",
"missing_requests_psutil": "Y manque requests pis psutil! Installe-les avec pip : `pip install requests psutil`",
"requirements_not_found": "requirements.txt introuvable à {path}, est-ce qu'il a été modifié?",
"warning_failed_parse_imports": "Attention : Ça a chié en lisant les imports de {filename} : {error}",
"cogs_dir_not_found": "Le dossier des cogs est pas à {path}, on skip le scan.",
"std_lib_local_skipped": "LIB STD / LOCAL {package} (check skipé)",
"ok_installed": "OK",
"missing_package": "MANQUANT",
"missing_package2": "est pas installé",
"missing_packages_detected": "Y'a des affaires qui manquent :",
"telling_goober_central": "J'envoie ça à goober central à {url}",
"failed_to_contact": "J'ai pas réussi à contacter {url} : {error}",
"all_requirements_satisfied": "Tout ce qu'il faut est installé.",
"ping_to": "Ping à {host} : {latency} ms",
"high_latency": "Latence élevée! Ça pourrait être lent.",
"could_not_parse_latency": "J'ai pas pu comprendre la latence.",
"ping_failed": "Le ping à {host} a chié.",
"error_running_ping": "Ça a chié en faisant le ping : {error}",
"memory_usage": "Mémoire utilisée : {used} Go / {total} Go ({percent}%)",
"memory_above_90": "La mémoire est à plus de 90% ({percent}%). Libère de la mémoire.",
"total_memory": "Mémoire totale : {total} Go",
"used_memory": "Mémoire utilisée : {used} Go",
"low_free_memory": "Y'a presque plus de mémoire! Juste {free} Go de libre.",
"measuring_cpu": "On check l'usage CPU par coeur...",
"core_usage": "Coeur {idx} : [{bar}] {usage}%",
"total_cpu_usage": "Usage total CPU : {usage}%",
"high_avg_cpu": "CPU trop élevé : {usage}%",
"really_high_cpu": "Le CPU est en tabarnak! Ça pourrait crasher.",
"memory_file": "Fichier mémoire : {size} Mo",
"memory_file_large": "Fichier mémoire de 1 Go ou plus, nettoie ça pour faire de la place.",
"memory_file_corrupted": "Fichier mémoire scrap! Erreur JSON : {error}",
"consider_backup_memory": "Pense à faire un backup pis recréer le fichier mémoire.",
"memory_file_encoding": "Problème d'encodage du fichier mémoire : {error}",
"error_reading_memory": "Ça a chié en lisant le fichier mémoire : {error}",
"memory_file_not_found": "Fichier mémoire pas trouvé.",
"modification_warning": "Goober a été modifié! Tes modifications vont être perdues à l'update!",
"reported_version": "Version rapportée :",
"current_hash": "Hash actuel :",
"not_found": "est pas trouvé!",
"version_error": "J'ai pas pu avoir les infos de version. Code d'état",
"loaded_cog": "Cog chargé :",
"loaded_cog2": "Module chargé :",
"cog_fail": "Ça a chié en chargeant le cog :",
"cog_fail2": "Ça a chié en chargeant le module :",
"no_model": "Y'a pas de modèle Markov de sauvegardé. On part de zéro.",
"folder_created": "Dossier '{folder_name}' créé.",
"folder_exists": "Le dossier '{folder_name}' existe déjà. On skip...",
"logged_in": "Connecté en tant que",
"synced_commands": "Synchronisé",
"synced_commands2": "commandes!",
"fail_commands_sync": "Ça a chié en synchronisant les commandes :",
"started": "{name} est parti!",
"name_check": "Ça a chié en checkant si le nom est libre :",
"name_taken": "Le nom est déjà pris. Choisis-en un autre.",
"name_check2": "Ça a chié en checkant si le nom est libre :",
"add_token": "Token : {token}\nAjoute ce token dans ton .env comme",
"token_exists": "Le token existe déjà dans .env. On utilise celui-là.",
"registration_error": "Ça a chié en s'enregistrant :",
"version_backup": "Backup créé :",
"backup_error": "Erreur : {LOCAL_VERSION_FILE} pas trouvé pour le backup.",
"model_loaded": "Modèle Markov chargé depuis",
"fetch_update_fail": "J'ai pas pu avoir les infos d'update.",
"invalid_server": "Erreur : Infos de version invalides du serveur.",
"goober_server_alert": "Alerte du serveur Goober central!\n",
"new_version": "Nouvelle version disponible : {latest_version} (Actuelle : {local_version})",
"changelog": "Va voir {VERSION_URL}/goob/changes.txt pour les changements\n\n",
"invalid_version": "La version : {local_version} est pas valide!",
"invalid_version2": "Si c'est fait exprès, ignore ça. Sinon, appuie sur Y pour avoir une version valide du serveur, peu importe ta version actuelle de Goober.",
"invalid_version3": "La version actuelle va être backupée dans current_version.bak..",
"input": "(Y ou n'importe quelle touche pour skip...)",
"modification_ignored": "T'as modifié",
"modification_ignored2": "IGNOREWARNING est désactivé..",
"latest_version": "T'as la dernière version :",
"latest_version2": "Va voir {VERSION_URL}/goob/changes.txt pour les changements",
"pinging_disabled": "Le ping est désactivé! J'dis pas au serveur que j'suis en ligne...",
"goober_ping_success": "Connecté à Goober central en tant que {NAME}",
"goober_ping_fail": "Ça a chié en envoyant les données. Le serveur a retourné :",
"goober_ping_fail2": "Ça a chié en envoyant les données :",
"sentence_positivity": "La phrase est positive à :",
"command_edit_fail": "Ça a chié en éditant le message :",
"command_desc_retrain": "Réentraîne le modèle Markov à la main.",
"command_markov_retrain": "Réentraînement du modèle Markov... Attend un peu.",
"command_markov_memory_not_found": "Erreur : fichier mémoire pas trouvé!",
"command_markov_memory_is_corrupt": "Erreur : fichier mémoire scrap!",
"command_markov_retraining": "Traitement de {processed_data}/{data_size} points de données...",
"command_markov_retrain_successful": "Modèle Markov réentraîné avec succès avec {data_size} points de données!",
"command_desc_talk": "parle pis toute",
"command_talk_insufficent_text": "J'ai pas assez appris pour pouvoir parler.",
"command_talk_generation_fail": "J'ai rien à dire pour l'instant!",
"command_desc_help": "aide",
"command_help_embed_title": "Aide du bot",
"command_help_embed_desc": "Liste des commandes par catégorie.",
"command_help_categories_general": "Général",
"command_help_categories_admin": "Admin",
"command_help_categories_custom": "Commandes perso",
"command_ran": "Info : {message.author.name} a fait {message.content}",
"command_ran_s": "Info : {interaction.user} a fait ",
"command_desc_ping": "ping",
"command_ping_embed_desc": "Latence du bot :",
"command_ping_footer": "Demandé par",
"command_about_desc": "à propos",
"command_about_embed_title": "À propos de moi",
"command_about_embed_field1": "Nom",
"command_about_embed_field2name": "Version",
"command_about_embed_field2value": "Locale : {local_version} \nDernière : {latest_version}",
"command_desc_stats": "stats",
"command_stats_embed_title": "Stats du bot",
"command_stats_embed_desc": "Infos sur la mémoire du bot.",
"command_stats_embed_field1name": "Stats du fichier",
"command_stats_embed_field1value": "Taille : {file_size} octets\nLignes : {line_count}",
"command_stats_embed_field2name": "Version",
"command_stats_embed_field2value": "Locale : {local_version} \nDernière : {latest_version}",
"command_stats_embed_field3name": "Infos variables",
"command_stats_embed_field3value": "Nom : {NAME} \nPréfixe : {PREFIX} \nID du proprio : {ownerid}\nLigne de ping : {PING_LINE} \nPartage de mémoire activé : {showmemenabled} \nEntraînement utilisateur activé : {USERTRAIN_ENABLED} \nChanson : {song} \nTexte de démarrage : ```{splashtext}```"
}

View file

@ -1,23 +1,8 @@
{ {
"minigames_hangman_game": "Parola: {display_word()}\nErrori: {wrong_guesses}/{max_wrong}",
"minigames_hangman_lost": "Hai perso! La parola era:",
"minigames_hangman_won": "Hai vinto! La parola era:",
"minigames_hangman_already_guessed": "Hai già indovinato",
"minigames_hangman_user_letter_guess": "La tua lettera",
"minigames_hangman_guess": "Indovina una lettera",
"minigames_hangman_api_failed": "Impossibile ottenere una parola casuale.",
"minigames_hangman": "Gioca all'impiccato con una parola casuale",
"minigames_click_to_guess": "Clicca per indovinare un numero da 1 a 10",
"minigames_guess_button": "Indovina",
"minigames_wrong_number": "Sbagliato! Il numero era",
"minigames_correct": "Corretto!",
"minigames_invalid_number": "Numero non valido!",
"minigames_guess_the_number": "Indovina il numero",
"minigames_your_guess": "Il tuo numero (1-10)",
"memory_file_valid": "Il file JSON è valido!", "memory_file_valid": "Il file JSON è valido!",
"file_aint_utf8": "Il file non è un UTF-8 valido. Forse è binario?", "file_aint_utf8": "Il file non è un UTF-8 valido. Forse è binario?",
"psutil_not_installed": "Controllo memoria saltato.", "psutil_not_installed": "Controllo memoria saltato.",
"not_cloned": "Goober non è stato clonato! Clonalo da Git.", "not_cloned": "Goober non è stato clonato! Clonalo da GitHub.",
"checks_disabled": "I controlli sono disabilitati!", "checks_disabled": "I controlli sono disabilitati!",
"unhandled_exception": "Si è verificata un'eccezione non gestita. Segnala questo problema su GitHub, per favore.", "unhandled_exception": "Si è verificata un'eccezione non gestita. Segnala questo problema su GitHub, per favore.",
"active_users:": "Utenti attivi:", "active_users:": "Utenti attivi:",

View file

@ -1,3 +1,20 @@
import logging
from modules.logger import GooberFormatter
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)
import os import os
import re import re
import json import json
@ -25,33 +42,38 @@ from typing import (
) )
import logging import logging
from modules.prestartchecks import start_checks from modules.prestartchecks import start_checks
from modules.logger import GooberFormatter import modules.keys as k
from modules.volta.main import * from modules import key_compiler
import logging import logging
from modules.settings import Settings as SettingsManager from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
from modules.settings import instance as settings_manager, ActivityType
from modules.permission import requires_admin from modules.permission import requires_admin
from modules.volta.main import _ from modules.sync_conenctor import instance as sync_connector
from modules.version import check_for_update
check_for_update()
logger = logging.getLogger("goober")
logger.setLevel(logging.DEBUG)
console_handler = logging.StreamHandler() import threading
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) def build_keys():
logger.addHandler(file_handler) key_compiler.build_result(
"en",
"assets/locales",
types=True,
output_path="modules/keys.py",
generate_comments=True,
)
build_keys()
settings_manager = SettingsManager()
settings = settings_manager.settings settings = settings_manager.settings
splash_text: str = "" splash_text: str = ""
k.change_language(settings["locale"])
with open(settings["splash_text_loc"], "r", encoding="UTF-8") as f: with open(settings["splash_text_loc"], "r", encoding="UTF-8") as f:
splash_text = "".join(f.readlines()) splash_text = "".join(f.readlines())
print(splash_text) print(splash_text)
@ -69,6 +91,7 @@ from discord.ext import commands
from modules.markovmemory import * from modules.markovmemory import *
from modules.sentenceprocessing import * from modules.sentenceprocessing import *
from modules.unhandledexception import handle_exception from modules.unhandledexception import handle_exception
from modules.image import gen_demotivator
sys.excepthook = handle_exception sys.excepthook = handle_exception
@ -86,18 +109,15 @@ class MessageMetadata(TypedDict):
# Constants with type hints # Constants with type hints
positive_gifs: List[str] = settings["bot"]["misc"]["positive_gifs"] positive_gifs: List[str] = settings["bot"]["misc"]["positive_gifs"]
name = settings["name"]
currenthash: str = "" currenthash: str = ""
launched: bool = False launched: bool = False
slash_commands_enabled: bool = False slash_commands_enabled: bool = False
# Set up Discord bot intents and create bot instance # Set up Discord bot intents and create bot instance
intents: discord.Intents = discord.Intents.default() intents: discord.Intents = discord.Intents.default()
intents.messages = True intents.messages = True
intents.presences = True
intents.members = True
intents.message_content = True intents.message_content = True
bot: commands.Bot = commands.Bot( bot: commands.Bot = commands.Bot(
command_prefix=settings["bot"]["prefix"], command_prefix=settings["bot"]["prefix"],
intents=intents, intents=intents,
@ -108,6 +128,11 @@ bot: commands.Bot = commands.Bot(
# Load memory and Markov model for text generation # Load memory and Markov model for text generation
memory: List[str | Dict[Literal["_meta"], MessageMetadata]] = load_memory() memory: List[str | Dict[Literal["_meta"], MessageMetadata]] = load_memory()
markov_model: Optional[markovify.Text] = load_markov_model()
if not markov_model:
logger.error(k.markov_model_not_found())
memory = load_memory()
markov_model = train_markov_model(memory)
generated_sentences: Set[str] = set() generated_sentences: Set[str] = set()
used_words: Set[str] = set() used_words: Set[str] = set()
@ -128,9 +153,9 @@ async def load_cogs_from_folder(bot: commands.Bot, folder_name="assets/cogs"):
try: try:
await bot.load_extension(module_path) await bot.load_extension(module_path)
logger.info(f"{_('loaded_cog')} {cog_name}") logger.info(f"{k.loaded_cog()} {cog_name}")
except Exception as e: except Exception as e:
logger.error(f"{_('cog_fail')} {cog_name} {e}") logger.error(f"{k.cog_fail()} {cog_name} {e}")
traceback.print_exc() traceback.print_exc()
@ -143,13 +168,13 @@ async def on_ready() -> None:
if launched: if launched:
return return
await load_cogs_from_folder(bot)
await load_cogs_from_folder(bot, "assets/cogs/internal") await load_cogs_from_folder(bot, "assets/cogs/internal")
await load_cogs_from_folder(bot)
try: try:
synced: List[discord.app_commands.AppCommand] = await bot.tree.sync() synced: List[discord.app_commands.AppCommand] = await bot.tree.sync()
logger.info(f"{_('synced_commands')} {len(synced)} {_('synced_commands2')}") logger.info(f"{k.synced_commands()} {len(synced)} {k.synced_commands2()}")
logger.info(_('started').format(name=name)) logger.info(k.started(settings["name"]))
except discord.errors.Forbidden as perm_error: except discord.errors.Forbidden as perm_error:
logger.error(f"Permission error while syncing commands: {perm_error}") logger.error(f"Permission error while syncing commands: {perm_error}")
@ -158,21 +183,36 @@ async def on_ready() -> None:
) )
quit() quit()
except Exception as e: except Exception as e:
logger.error(f"{_('fail_commands_sync')} {e}") logger.error(f"{k.fail_commands_sync()} {e}")
traceback.print_exc() traceback.print_exc()
quit() quit()
if not settings["bot"]["misc"]["active_song"]: if not settings["bot"]["misc"]["activity"]["content"]:
return return
activity_type = discord.ActivityType.unknown
settings_activity = settings["bot"]["misc"]["activity"]["type"]
activities: Dict[ActivityType, discord.ActivityType] = {
"listening": discord.ActivityType.listening,
"playing": discord.ActivityType.playing,
"streaming": discord.ActivityType.streaming,
"competing": discord.ActivityType.competing,
"watching": discord.ActivityType.watching,
}
await bot.change_presence( await bot.change_presence(
activity=discord.Activity( activity=discord.Activity(
type=discord.ActivityType.listening, type=activities.get(
name=settings["bot"]["misc"]["active_song"], settings["bot"]["misc"]["activity"]["type"],
discord.ActivityType.unknown,
),
name=settings["bot"]["misc"]["activity"]["content"],
) )
) )
launched = True launched = True
bot.remove_command('help')
@bot.event @bot.event
async def on_command_error(ctx: commands.Context, error: commands.CommandError) -> None: async def on_command_error(ctx: commands.Context, error: commands.CommandError) -> None:
@ -194,10 +234,66 @@ async def on_command_error(ctx: commands.Context, error: commands.CommandError)
context=f"Command: {ctx.command} | User: {ctx.author}", context=f"Command: {ctx.command} | User: {ctx.author}",
) )
# 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: str | None = 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}"
with open(temp_input, "wb") as f:
await attachment.save(f)
input_path: str = temp_input
else:
fallback_image: Optional[str] = get_random_asset_image()
if fallback_image is None:
await ctx.reply(k.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(k.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) # type: ignore
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)
# Event: Called on every message # Event: Called on every message
@bot.event @bot.event
async def on_message(message: discord.Message) -> None: async def on_message(message: discord.Message) -> None:
global memory global memory, markov_model
EMOJIS = [ EMOJIS = [
"\U0001f604", "\U0001f604",
"\U0001f44d", "\U0001f44d",
@ -209,7 +305,7 @@ async def on_message(message: discord.Message) -> None:
if message.author.bot: if message.author.bot:
return return
if str(message.author.id) in settings["bot"]["blacklisted_users"]: if message.author.id in settings["bot"]["blacklisted_users"]:
return return
commands = [ commands = [
@ -217,15 +313,21 @@ async def on_message(message: discord.Message) -> None:
] ]
if message.content.startswith(tuple(commands)): if message.content.startswith(tuple(commands)):
logger.info(f"{(_('command_ran')).format(message=message)}") logger.info(f"{k.command_ran(message.author.name, message.content)}")
await bot.process_commands(message) await bot.process_commands(message)
return return
if (
profanity.contains_profanity(message.content)
and settings["bot"]["misc"]["block_profanity"]
):
return
if message.content: if message.content:
if not settings["bot"]["user_training"]: if not settings["bot"]["user_training"]:
return return
formatted_message: str = message.content formatted_message: str = append_mentions_to_18digit_integer(message.content)
cleaned_message: str = preprocess_message(formatted_message) cleaned_message: str = preprocess_message(formatted_message)
if cleaned_message: if cleaned_message:
memory.append(cleaned_message) memory.append(cleaned_message)
@ -256,6 +358,11 @@ async def on_message(message: discord.Message) -> None:
if sentiment_score > 0.8: if sentiment_score > 0.8:
if not settings["bot"]["react_to_messages"]: if not settings["bot"]["react_to_messages"]:
return return
if not sync_connector.can_react(message.id):
logger.info("Sync hub determined that this instance cannot react")
return
emoji = random.choice(EMOJIS) emoji = random.choice(EMOJIS)
try: try:
await message.add_reaction(emoji) await message.add_reaction(emoji)
@ -268,7 +375,7 @@ async def on_message(message: discord.Message) -> None:
# Event: Called on every interaction (slash command, etc.) # Event: Called on every interaction (slash command, etc.)
@bot.event @bot.event
async def on_interaction(interaction: discord.Interaction) -> None: async def on_interaction(interaction: discord.Interaction) -> None:
logger.info(f"{(_('command_ran_s')).format(interaction=interaction)}{name}") logger.info(f"{k.command_ran_s(interaction.user.name)} {interaction.user.name}")
# Global check: Block blacklisted users from running commands # Global check: Block blacklisted users from running commands
@ -280,11 +387,11 @@ async def block_blacklisted(ctx: commands.Context) -> bool:
try: try:
if isinstance(ctx, discord.Interaction): if isinstance(ctx, discord.Interaction):
if not ctx.response.is_done(): if not ctx.response.is_done():
await ctx.response.send_message(_('blacklisted'), ephemeral=True) await ctx.response.send_message(k.blacklisted(), ephemeral=True)
else: else:
await ctx.followup.send(_('blacklisted'), ephemeral=True) await ctx.followup.send(k.blacklisted(), ephemeral=True)
else: else:
await ctx.send(_('blacklisted_user'), ephemeral=True) await ctx.send(k.blacklisted_user(), ephemeral=True)
except: except:
return False return False
@ -297,6 +404,40 @@ def improve_sentence_coherence(sentence: str) -> str:
sentence = sentence.replace(" i ", " I ") sentence = sentence.replace(" i ", " I ")
return sentence return sentence
class OnMyWatch:
watchDirectory = "assets/locales"
def __init__(self):
self.observer = Observer()
def run(self):
event_handler = Handler()
self.observer.schedule(event_handler, self.watchDirectory, recursive=True)
self.observer.start()
try:
while True:
time.sleep(5)
except:
self.observer.stop()
print("Observer Stopped")
self.observer.join()
class Handler(FileSystemEventHandler):
def on_any_event(self, event):
if event.is_directory:
return None
elif event.event_type == "modified":
build_keys()
observer = Observer()
observer.schedule(Handler(), "assets/locales")
observer.start()
# Start the bot # Start the bot
if __name__ == "__main__": if __name__ == "__main__":
bot.run(os.environ.get("DISCORDBOTTOKEN", "")) bot.run(os.environ.get("DISCORD_BOT_TOKEN", ""))

View file

@ -1 +0,0 @@
DISCORDBOTTOKEN=

View file

@ -66,6 +66,6 @@ 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 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 = "3.0.0" local_version = "2.3.3"
os.environ["gooberlocal_version"] = local_version os.environ["gooberlocal_version"] = local_version
beta = get_git_branch() == "dev" beta = get_git_branch() == "dev"

217
modules/image.py Normal file
View file

@ -0,0 +1,217 @@
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):
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
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

220
modules/key_compiler.py Normal file
View file

@ -0,0 +1,220 @@
# The MIT License (MIT)
# Copyright (c) 2025 ctih1
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import os
from typing import Dict, List, Literal
import json
import sys
import logging
import time
NOTICE = """
# This file was automatically created from localization JSON files.
# DO NOT EDIT THIS FILE DIRECTLY. If you want to edit a translation, please use the language's JSON file.
#fmt: off
"""
logger = logging.getLogger("kaannos")
class LanguageCollector:
def __init__(self, language_dir: str) -> None:
self.path: str = language_dir
self.languages: Dict[str, Dict[str, str]] = {}
for file in os.listdir(self.path):
if not file.endswith(".json") or len(file) > 7:
logger.debug(f"Skipping {file}")
continue
locale: str = file.split(".json")[0]
logger.info(f"Discovered {file}")
with open(os.path.join(self.path, file), "r", encoding="UTF-8") as f:
keys: Dict[str, str] = json.load(f)
self.languages[locale] = keys
self.find_missing_keys()
def find_missing_keys(self) -> None:
primary_language_keys: Dict[str, str] = self.languages["en"]
for key in primary_language_keys:
for language in self.languages:
if key not in self.languages[language]:
logger.warning(f"Key {key} missing from {language}")
for language in self.languages:
for key in self.languages[language]:
if key not in primary_language_keys:
logger.warning(f"Leftover key {key} found from {language}")
class Script:
def __init__(self) -> None:
self.script: str = ""
def add_line(self, content, indent: int = 0, newline: bool = True) -> None:
tabs = "\t" * indent
newline_content = "\n" if newline else ""
self.script += f"{tabs}{content}{newline_content}"
def process_name(key: str) -> str:
return key.replace(" ", "_").replace(":", "").lower()
def find_args(string: str) -> List[str]:
variable_open: bool = False
temp_content: str = ""
variables: List[str] = []
for char in string:
if variable_open:
if char == "}":
variable_open = False
variables.append(temp_content)
temp_content = ""
continue
if char == "{":
raise SyntaxError("Variable already open!")
temp_content += char
else:
if char == "}":
raise SyntaxError("Trying to close a nonexistant variable")
if char == "{":
variable_open = True
return variables
def convert_args(
inp: str, vars: List[str], mode: Literal["brackets", "none"] = "brackets"
) -> str:
replacements = {".": "_", ",": "_"}
for var in vars:
cleaned_var = var
for key, val in replacements.items():
cleaned_var = cleaned_var.replace(key, val)
if mode == "none":
inp = inp.replace(f"{var}", f"{cleaned_var}")
else:
inp = inp.replace(f"{{{var}}}", f"{{{cleaned_var}}}")
return inp
class GenerateScript:
def __init__(
self,
primary_lang: str,
language_data: Dict[str, Dict[str, str]],
use_typing: bool = True,
output_path: str = "out.py",
generate_comments: bool = True,
):
self.data = language_data
self.primary = primary_lang
self.script = Script()
self.uses_typing: bool = use_typing
self.output = output_path
self.generate_comments = generate_comments
def create(self):
# I really don't like this implementation but also it works
self.script.add_line(NOTICE)
if self.uses_typing:
self.script.add_line("from typing import Literal, List")
self.script.add_line(f"Language=Literal{list(self.data.keys())}")
self.script.add_line(
f"languages: List[Language] = {list(self.data.keys())}"
)
self.script.add_line(f"default_lang: Language | str='{self.primary}'")
self.script.add_line(
"def change_language(new_lang: Language | str) -> None: global default_lang; default_lang = new_lang"
)
else:
self.script.add_line(f"languages = {list(self.data.keys())}")
self.script.add_line(f"default_lang='{self.primary}'")
self.script.add_line(
"def change_language(new_lang): global default_lang; default_lang = new_lang"
)
self.primary_data = self.data[self.primary]
for key in self.primary_data:
args = find_args(self.primary_data[key])
self.script.add_line(
f"def {process_name(key)}({convert_args(','.join([*args, 'lang:str|None=None' if self.uses_typing else 'lang']), args, 'none')}):"
)
if self.generate_comments:
self.script.add_line('"""', 1)
self.script.add_line("### Locales", 1)
for language in self.data:
self.script.add_line(
f"- {language.capitalize()}: **{self.data[language].get(key, self.primary_data[key])}**",
1,
)
self.script.add_line('"""', 1)
self.script.add_line("if not lang: lang=default_lang", 1)
for language in self.data:
formatted_map = "{"
for arg in args:
formatted_map += f'"{convert_args(arg, args, "none")}": {convert_args(arg, args, "none")},'
formatted_map = formatted_map[:-1] + "}"
self.script.add_line(
f"""if lang == '{language}': return {convert_args(json.dumps(
self.data[language].get(key,self.primary_data[key]),
ensure_ascii=False
), args)}{f'.format_map({formatted_map})' if len(args) > 0 else ''}""",
1,
)
self.script.add_line(
"else: raise ValueError(f'Invalid language {lang}')", 1
)
with open(self.output, "w", encoding="UTF-8") as f:
f.write(self.script.script)
def build_result(
primary_lang: str,
locale_dir: str,
types: bool,
output_path: str,
generate_comments: bool = True,
):
start = time.time()
lc = LanguageCollector(locale_dir)
GenerateScript(
primary_lang, lc.languages, types, output_path, generate_comments
).create()
logger.info(f"Done in {time.time() - start}s")

2340
modules/keys.py Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,30 +1,28 @@
import logging import logging
import re
from modules.globalvars import * from modules.globalvars import *
class GooberFormatter(logging.Formatter): class GooberFormatter(logging.Formatter):
def __init__(self, colors: bool = True): # Disable colors for TXT output def __init__(self, colors: bool = True): # Disable colors for TXT output
self.colors = colors self.colors = colors
self._format = f"[ %(levelname)-8s ]: %(message)s {DEBUG} [%(asctime)s.%(msecs)03d] (%(filename)s:%(funcName)s) {RESET}" self._format = f"[ %(levelname)-8s ]: %(message)s {DEBUG} [%(asctime)s.%(msecs)03d] (%(filename)s:%(funcName)s) {RESET}"
self.FORMATS = { self.FORMATS = {
logging.DEBUG: DEBUG + self._format + RESET, logging.DEBUG: DEBUG + self._format + RESET,
logging.INFO: self._format.replace("%(levelname)-8s", f"{GREEN}%(levelname)-8s{RESET}"), logging.INFO: self._format.replace(
"%(levelname)-8s", f"{GREEN}%(levelname)-8s{RESET}"
),
logging.WARNING: YELLOW + self._format + RESET, logging.WARNING: YELLOW + self._format + RESET,
logging.ERROR: RED + self._format + RESET, logging.ERROR: RED + self._format + RESET,
logging.CRITICAL: PURPLE + self._format + RESET logging.CRITICAL: PURPLE + self._format + RESET,
} }
def format(self, record: logging.LogRecord): def format(self, record: logging.LogRecord):
ansiescape = re.compile(r'\x1B[@-_][0-?]*[ -/]*[@-~]')
if self.colors: if self.colors:
log_fmt = self.FORMATS.get(record.levelno) # Add colors log_fmt = self.FORMATS.get(record.levelno) # Add colors
else: else:
log_fmt = self._format # Just use the default format log_fmt = self._format # Just use the default format
formatter = logging.Formatter(log_fmt, datefmt="%m/%d/%y %H:%M:%S") formatter = logging.Formatter(log_fmt, datefmt="%m/%d/%y %H:%M:%S")
formatted = formatter.format(record) return formatter.format(record)
if not self.colors:
formatted = ansiescape.sub('', formatted)
return formatted

View file

@ -4,7 +4,7 @@ import markovify
import pickle import pickle
from modules.globalvars import * from modules.globalvars import *
import logging import logging
from modules.volta.main import _ import modules.keys as k
from modules.settings import instance as settings_manager from modules.settings import instance as settings_manager
settings = settings_manager.settings settings = settings_manager.settings
@ -61,17 +61,21 @@ def train_markov_model(memory, additional_data=None) -> markovify.NewlineText |
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"{k.model_loaded()} {filename}.{RESET}")
return model return model
except FileNotFoundError: except FileNotFoundError:
logger.error(f"{filename} {_('not_found')}{RESET}") logger.error(f"{filename} {k.not_found()}{RESET}")
return None return None

View file

@ -4,12 +4,11 @@ import discord
import discord.ext import discord.ext
import discord.ext.commands import discord.ext.commands
from modules.settings import Settings as SettingsManager from modules.settings import instance as settings_manager
import logging import logging
logger = logging.getLogger("goober") logger = logging.getLogger("goober")
settings_manager = SettingsManager()
settings = settings_manager.settings settings = settings_manager.settings
@ -19,11 +18,11 @@ class PermissionError(Exception):
def requires_admin(): def requires_admin():
async def wrapper(ctx: discord.ext.commands.Context): async def wrapper(ctx: discord.ext.commands.Context):
print(ctx.author.id)
if ctx.author.id not in settings["bot"]["owner_ids"]: if ctx.author.id not in settings["bot"]["owner_ids"]:
await ctx.send("You don't have the necessary permissions to run this command!") await ctx.send(
return "You don't have the necessary permissions to run this command!"
)
return False
command = ctx.command command = ctx.command
if not command: if not command:

View file

@ -1,6 +1,4 @@
from modules.globalvars import * from modules.globalvars import *
from modules.settings import Settings as SettingsManager
from modules.volta.main import _, check_missing_translations
import time import time
import os import os
import sys import sys
@ -12,16 +10,15 @@ import re
from spacy.util import is_package from spacy.util import is_package
import importlib.metadata import importlib.metadata
import logging import logging
import modules.keys as k
from modules.settings import instance as settings_manager
from modules.sync_conenctor import instance as sync_hub
settings = settings_manager.settings
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 +26,8 @@ try:
import psutil import psutil
except ImportError: except ImportError:
psutilavaliable = False psutilavaliable = False
logger.error(_('missing_requests_psutil')) logger.error(k.missing_requests_psutil())
def check_for_model(): def check_for_model():
if is_package("en_core_web_sm"): if is_package("en_core_web_sm"):
@ -37,157 +35,204 @@ 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
else: else:
logger.error(f"{_('not_cloned')}") logger.error(f"{k.not_cloned()}")
sys.exit(1) sys.exit(1)
def get_stdlib_modules():
stdlib = pathlib.Path(sysconfig.get_paths()['stdlib'])
modules = set(sys.builtin_module_names)
modules.update( def get_stdlib_modules():
f.stem for f in stdlib.glob('*.py') if f.stem != '__init__' stdlib_path = pathlib.Path(sysconfig.get_paths()["stdlib"])
) modules = set()
modules.update( if hasattr(sys, "builtin_module_names"):
d.name for d in stdlib.iterdir() if (d / '__init__.py').exists() modules.update(sys.builtin_module_names)
) for file in stdlib_path.glob("*.py"):
modules.update( if file.stem != "__init__":
f.stem for f in stdlib.glob('*') if f.suffix in ('.so', '.pyd') modules.add(file.stem)
) for folder in stdlib_path.iterdir():
if folder.is_dir() and (folder / "__init__.py").exists():
modules.add(folder.name)
for file in stdlib_path.glob("*.*"):
if file.suffix in (".so", ".pyd"):
modules.add(file.stem)
return modules return modules
def check_requirements(): def check_requirements():
stdlib = get_stdlib_modules() STD_LIB_MODULES = get_stdlib_modules()
aliases = { PACKAGE_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",
"websocket": "websocket-client"
} }
req_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'requirements.txt')) parent_dir = os.path.dirname(os.path.abspath(__file__))
if not os.path.exists(req_path): requirements_path = os.path.abspath(
logger.error(_('requirements_not_found').format(path=req_path)) os.path.join(parent_dir, "..", "requirements.txt")
)
if not os.path.exists(requirements_path):
logger.error(f"{k.requirements_not_found(path=requirements_path)}")
return return
with open(req_path) as f: with open(requirements_path, "r") as f:
requirements = { lines = f.readlines()
aliases.get(line.split('==')[0].strip().lower(), line.split('==')[0].strip().lower()) requirements = set()
for line in f if line.strip() and not line.startswith('#') for line in lines:
} 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 = {d.metadata['Name'].lower() for d in importlib.metadata.distributions()} installed_packages = {
dist.metadata["Name"].lower() for dist in importlib.metadata.distributions()
}
missing = [] missing = []
for pkg in sorted(requirements): for req in sorted(requirements):
if pkg in stdlib or pkg == 'modules': if req in STD_LIB_MODULES or req == "modules":
print(_('std_lib_local_skipped').format(package=pkg)) print(k.std_lib_local_skipped(package=req))
continue continue
if pkg in installed:
logger.info(_('ok_installed').format(package=pkg)) check_name = req.lower()
if check_name in installed_packages:
logger.info(f"{k.ok_installed()} {check_name}")
else: else:
logger.error(f"{_('missing_package').format(package=pkg)} {pkg} {_('missing_package2')}") logger.error(f"{k.missing_package()} {check_name} {k.missing_package2()}")
missing.append(pkg) missing.append(check_name)
if missing: if missing:
logger.error(_('missing_packages_detected')) logger.error(k.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(k.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()
cmd, pattern = { if system == "Windows":
"Windows": (["ping", "-n", "1", "-w", "1000", host], r"Average = (\d+)ms"), cmd = ["ping", "-n", "1", "-w", "1000", host]
"Darwin": (["ping", "-c", "1", host], r"time=([\d\.]+) ms") latency_pattern = r"Average = (\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(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) result = subprocess.run(
if result.returncode != 0: cmd, 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(k.ping_to(host=host, latency=latency_ms))
if latency_ms > 300:
logger.warning(f"{k.high_latency()}")
else:
logger.warning(k.could_not_parse_latency())
else:
print(result.stderr) print(result.stderr)
return logger.error(_('ping_failed').format(host=host) + RESET) logger.error(f"{k.ping_failed(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(k.error_running_ping(error=e))
def check_memory(): def check_memory():
if not psutilavaliable: if psutilavaliable == False:
return return
try: try:
mem = psutil.virtual_memory() # type: ignore memory_info = psutil.virtual_memory() # type: ignore
total = mem.total / 1e9 total_memory = memory_info.total / (1024**3)
used = mem.used / 1e9 used_memory = memory_info.used / (1024**3)
free = mem.available / 1e9 free_memory = memory_info.available / (1024**3)
percent_used = (used / total) * 100
logger.info(_('memory_usage').format(used=used, total=total, percent=percent_used)) logger.info(
if percent_used > 90: k.memory_usage(
print(f"{YELLOW}{_('memory_above_90').format(percent=percent_used)}{RESET}") used=used_memory,
logger.info(_('total_memory').format(total=total)) total=total_memory,
logger.info(_('used_memory').format(used=used)) percent=(used_memory / total_memory) * 100,
if free < 1: )
logger.warning(_('low_free_memory').format(free=free)) )
sys.exit(1) if used_memory > total_memory * 0.9:
print(
f"{YELLOW}{k.memory_above_90(percent=(used_memory / total_memory) * 100)}{RESET}"
)
logger.info(k.total_memory(total=total_memory))
logger.info(k.used_memory(used=used_memory))
if free_memory < 1:
logger.warning(f"{k.low_free_memory(free=free_memory)}")
except ImportError: except ImportError:
logger.error(_('psutil_not_installed')) logger.error(
k.psutil_not_installed()
) # todo: translate this into italian and put it in the translations "psutil is not installed. Memory check skipped."
def check_cpu(): def check_cpu():
if psutilavaliable == False: if psutilavaliable == False:
return return
logger.info((_('measuring_cpu'))) logger.info(k.measuring_cpu())
cpu_per_core = psutil.cpu_percent(interval=1, percpu=True) # type: ignore cpu_per_core = psutil.cpu_percent(interval=1, percpu=True) # type: ignore
total_cpu = sum(cpu_per_core) / len(cpu_per_core) total_cpu = sum(cpu_per_core) / len(cpu_per_core)
logger.info((_('total_cpu_usage')).format(usage=total_cpu)) logger.info(k.total_cpu_usage(usage=total_cpu))
if total_cpu > 85: if total_cpu > 85:
logger.warning(f"{(_('high_avg_cpu')).format(usage=total_cpu)}") logger.warning(f"{k.high_avg_cpu(usage=total_cpu)}")
if total_cpu > 95: if total_cpu > 95:
logger.error(_('really_high_cpu')) logger.error(k.really_high_cpu())
sys.exit(1) sys.exit(1)
def check_memoryjson(): def check_memoryjson():
try: try:
size_mb = os.path.getsize(MEMORY_FILE) / (1024 ** 2) logger.info(
logger.info(_('memory_file').format(size=size_mb)) k.memory_file(
if size_mb > 1024: size=os.path.getsize(settings["bot"]["active_memory"]) / (1024**2)
logger.warning(_('memory_file_large')) )
)
if os.path.getsize(settings["bot"]["active_memory"]) > 1_073_741_824:
logger.warning(f"{k.memory_file_large()}")
try: try:
with open(MEMORY_FILE, 'r', encoding='utf-8') as f: with open(settings["bot"]["active_memory"], "r", encoding="utf-8") as f:
json.load(f) json.load(f)
except (json.JSONDecodeError, UnicodeDecodeError) as e:
msg = _('memory_file_corrupted') if isinstance(e, json.JSONDecodeError) else _('memory_file_encoding')
logger.error(msg.format(error=e))
logger.warning(_('consider_backup_memory'))
except Exception as e:
logger.error(_('error_reading_memory').format(error=e))
except json.JSONDecodeError as e:
logger.error(f"{k.memory_file_corrupted(error=e)}")
logger.warning(f"{k.consider_backup_memory()}")
except UnicodeDecodeError as e:
logger.error(f"{k.memory_file_encoding(error=e)}")
logger.warning(f"{k.consider_backup_memory()}")
except Exception as e:
logger.error(f"{k.error_reading_memory(error=e)}")
except FileNotFoundError: except FileNotFoundError:
logger.error(_('memory_file_not_found')) logger.info(f"{k.memory_file_not_found()}")
def presskey2skip(timeout): def presskey2skip(timeout):
if os.name == 'nt': if os.name == "nt":
import msvcrt import msvcrt
start_time = time.time() start_time = time.time()
while True: while True:
if msvcrt.kbhit(): if msvcrt.kbhit():
@ -216,29 +261,44 @@ def presskey2skip(timeout):
time.sleep(0.1) time.sleep(0.1)
finally: finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
def check_synchub():
if not sync_hub.connected:
logger.warning("Sync hub not connected properly! The bot will not be able to react to messages, or create breaking news unless you disable synchub in settings")
else:
logger.info("Sync hub is conencted")
beta = beta beta = beta
def start_checks(): def start_checks():
if settings["disable_checks"]: if settings["disable_checks"]:
logger.warning(f"{_('checks_disabled')}") logger.warning(f"{k.checks_disabled()}")
return return
logger.info(_('running_prestart_checks'))
logger.info(k.running_prestart_checks())
check_for_model() check_for_model()
iscloned() iscloned()
check_missing_translations()
check_requirements() check_requirements()
check_latency() check_latency()
check_memory() check_memory()
check_memoryjson() check_memoryjson()
check_cpu() check_cpu()
check_synchub()
if os.path.exists(".env"): if os.path.exists(".env"):
pass pass
else: else:
logger.warning(f"{(_('env_file_not_found'))}") logger.warning(f"{k.env_file_not_found()}")
sys.exit(1) sys.exit(1)
if beta == True: if beta == True:
logger.warning(f"this build isnt finished yet, some things might not work as expected") logger.warning(
f"this build isnt finished yet, some things might not work as expected"
)
else: else:
pass pass
logger.info(_('continuing_in_seconds').format(seconds=5)) logger.info(k.continuing_in_seconds(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")
with open(settings["splash_text_loc"], "r") as f:
print("".join(f.readlines()))

View file

@ -1,12 +1,15 @@
import re import re
import discord.ext
import discord.ext.commands
from modules.globalvars import * from modules.globalvars import *
from modules.volta.main import _
import spacy import spacy
from spacy.tokens import Doc from spacy.tokens import Doc
from spacytextblob.spacytextblob import SpacyTextBlob from spacytextblob.spacytextblob import SpacyTextBlob
import discord
import modules.keys as k
import logging import logging
logger = logging.getLogger("goober") logger = logging.getLogger("goober")
@ -14,12 +17,13 @@ def check_resources():
try: try:
nlp = spacy.load("en_core_web_sm") nlp = spacy.load("en_core_web_sm")
except OSError: except OSError:
logging.critical((_('spacy_model_not_found'))) logging.critical(k.spacy_model_not_found())
spacy.cli.download("en_core_web_sm") spacy.cli.download("en_core_web_sm") # type: ignore
nlp = spacy.load("en_core_web_sm") nlp = spacy.load("en_core_web_sm")
if "spacytextblob" not in nlp.pipe_names: if "spacytextblob" not in nlp.pipe_names:
nlp.add_pipe("spacytextblob") nlp.add_pipe("spacytextblob")
logger.info((_('spacy_initialized'))) logger.info(k.spacy_initialized())
check_resources() check_resources()
@ -27,47 +31,65 @@ nlp = spacy.load("en_core_web_sm")
nlp.add_pipe("spacytextblob") nlp.add_pipe("spacytextblob")
Doc.set_extension("polarity", getter=lambda doc: doc._.blob.polarity) Doc.set_extension("polarity", getter=lambda doc: doc._.blob.polarity)
def is_positive(sentence): def is_positive(sentence):
doc = nlp(sentence) doc = nlp(sentence)
sentiment_score = doc._.polarity # from spacytextblob sentiment_score = doc._.polarity # from spacytextblob
debug_message = f"{(_('sentence_positivity'))} {sentiment_score}{RESET}" debug_message = f"{k.sentence_positivity()} {sentiment_score}{RESET}"
logger.debug(debug_message) logger.debug(debug_message)
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):
try: async def send_message(
if edit and message_reference: ctx: discord.ext.commands.Context,
message: str | None = None,
embed: discord.Embed | None = None,
file: discord.File | None = None,
edit: bool = False,
message_reference: discord.Message | None = None,
) -> discord.Message | None:
sent_message: discord.Message | None = None
if edit and message_reference:
try:
await message_reference.edit(content=message, embed=embed) await message_reference.edit(content=message, embed=embed)
return message_reference return message_reference
except Exception as e:
await ctx.send(f"{k.edit_fail()} {e}")
return None
send_kwargs = {} if embed:
if message: sent_message = await ctx.send(embed=embed, content=message)
send_kwargs['content'] = message elif file:
if embed: sent_message = await ctx.send(file=file, content=message)
send_kwargs['embed'] = embed else:
if file: sent_message = await ctx.send(content=message)
send_kwargs['file'] = file
if hasattr(ctx, "respond"): return sent_message
return await ctx.respond(**send_kwargs, ephemeral=False)
else:
return await ctx.send(**send_kwargs)
except Exception as e:
await ctx.send(f"{RED}{(_('edit_fail'))} {e}{RESET}") def append_mentions_to_18digit_integer(message):
pattern = r"\b\d{18}\b"
return re.sub(pattern, lambda match: "", message)
def preprocess_message(message): def preprocess_message(message):
message = message message = append_mentions_to_18digit_integer(message)
doc = nlp(message) doc = nlp(message)
tokens = [token.text for token in doc if token.is_alpha or token.is_digit] tokens = [token.text for token in doc if token.is_alpha or token.is_digit]
return " ".join(tokens) return " ".join(tokens)
def improve_sentence_coherence(sentence): def improve_sentence_coherence(sentence):
return "" return re.sub(r"\bi\b", "I", sentence)
def rephrase_for_coherence(sentence): def rephrase_for_coherence(sentence):
coherent_sentence = sentence words = sentence.split()
coherent_sentence = " ".join(words)
return coherent_sentence return coherent_sentence

View file

@ -1,14 +1,30 @@
import json import json
import os import os
from typing import List, TypedDict from typing import Dict, List, Literal, Mapping, Any, TypedDict
from modules.keys import Language
import logging
import copy import copy
logger = logging.getLogger("goober")
ActivityType = Literal["listening", "playing", "streaming", "competing", "watching"]
class SyncHub(TypedDict):
url: str
enabled: bool
class Activity(TypedDict):
content: str
type: ActivityType
class MiscBotOptions(TypedDict): class MiscBotOptions(TypedDict):
ping_line: str ping_line: str
active_song: str activity: Activity
positive_gifs: List[str] positive_gifs: List[str]
block_profanity: bool block_profanity: bool
class BotSettings(TypedDict): class BotSettings(TypedDict):
prefix: str prefix: str
owner_ids: List[int] owner_ids: List[int]
@ -19,40 +35,121 @@ class BotSettings(TypedDict):
misc: MiscBotOptions misc: MiscBotOptions
enabled_cogs: List[str] enabled_cogs: List[str]
active_memory: str active_memory: str
sync_hub: SyncHub
class SettingsType(TypedDict): class SettingsType(TypedDict):
bot: BotSettings bot: BotSettings
locale: str locale: Language
name: str name: str
auto_update: bool auto_update: bool
disable_checks: bool disable_checks: bool
splash_text_loc: str splash_text_loc: str
cog_settings: Dict[str, Mapping[Any, Any]]
class AdminLogEvent(TypedDict):
messageId: int
author: int
target: str | int
action: Literal["del", "add", "set"]
change: Literal["owner_ids", "blacklisted_users", "enabled_cogs"]
class Settings: class Settings:
def __init__(self) -> None: def __init__(self) -> None:
self.path = os.path.join(".", "settings", "settings.json") global instance
instance = self
self.path: str = os.path.join(".", "settings", "settings.json")
if not os.path.exists(self.path): if not os.path.exists(self.path):
raise FileNotFoundError("settings.json file does not exist!") logger.critical(
f"Missing settings file from {self.path}! Did you forget to copy settings.example.json?"
)
raise ValueError("settings.json file does not exist!")
self.settings: SettingsType
self.original_settings: SettingsType
with open(self.path, "r") as f: with open(self.path, "r") as f:
self._kv_store = json.load(f) self.__kv_store: dict = json.load(f)
self.settings: SettingsType = self._kv_store # type: ignore self.settings = SettingsType(self.__kv_store) # type: ignore
self.original_settings = copy.deepcopy(self.settings) self.original_settings = copy.deepcopy(self.settings)
def get_locale(self) -> str: self.log_path: str = os.path.join(".", "settings", "admin_logs.json")
# Return locale or None if missing
return self.settings.get("locale", None) self.migrate()
def migrate(self):
active_song: str | None = (
self.settings.get("bot", {}).get("misc", {}).get("active_song")
)
if active_song:
logger.warning("Found deprecated active_song, migrating")
self.settings["bot"]["misc"]["activity"] = {
"content": active_song,
"type": "listening",
}
del self.settings["bot"]["misc"]["active_song"] # type: ignore
sync_hub: SyncHub | None = self.settings.get("bot", {}).get("sync_hub")
if not sync_hub:
logger.warning("Adding sync hub settings")
self.settings["bot"]["sync_hub"] = {
"enabled": True,
"url": "ws://goober.frii.site"
}
self.commit()
def reload_settings(self) -> None:
with open(self.path, "r") as f:
self.__kv_store: dict = json.load(f)
self.settings = SettingsType(self.__kv_store) # type: ignore
self.original_settings = copy.deepcopy(self.settings)
def commit(self) -> None: def commit(self) -> None:
with open(self.path, "w") as f: with open(self.path, "w") as f:
json.dump(self.settings, f, indent=4) json.dump(self.settings, f, ensure_ascii=False, indent=4)
self.original_settings = copy.deepcopy(self.settings)
self.original_settings = self.settings
def discard(self) -> None: def discard(self) -> None:
self.settings = copy.deepcopy(self.original_settings) self.settings = self.original_settings
# Usage def get_plugin_settings(
instance = Settings() self, plugin_name: str, default: Mapping[Any, Any]
locale = instance.get_locale() ) -> Mapping[Any, Any]:
print("Locale:", locale) return self.settings["cog_settings"].get(plugin_name, default)
def set_plugin_setting(
self, plugin_name: str, new_settings: Mapping[Any, Any]
) -> None:
"""Changes a plugin setting. Commits changes"""
self.settings["cog_settings"][plugin_name] = new_settings
self.commit()
def add_admin_log_event(self, event: AdminLogEvent):
if not os.path.exists(self.log_path):
logger.warning("Admin log doesn't exist!")
with open(self.log_path, "w") as f:
json.dump([], f)
with open(self.log_path, "r") as f:
logs: List[AdminLogEvent] = json.load(f)
logs.append(event)
with open(self.log_path, "w") as f:
json.dump(logs, f, ensure_ascii=False, indent=4)
instance: Settings = Settings()

94
modules/sync_conenctor.py Normal file
View file

@ -0,0 +1,94 @@
import websocket
from modules.settings import instance as settings_manager
import logging
logger = logging.getLogger("goober")
settings = settings_manager.settings
class SyncConnector:
def __init__(self, url: str):
self.connected: bool = True
self.url = url
self.client: websocket.WebSocket | None = None
self.try_to_connect()
def __connect(self) -> bool:
try:
self.client = websocket.create_connection(self.url)
except OSError as e:
logger.debug(e)
logger.debug(e.strerror)
return False
return True
def try_to_connect(self) -> bool:
if self.__connect():
logger.info("Connected to sync hub!")
self.connected = True
else:
logger.error("Failed to connect to sync hub.. Disabling for the time being")
self.connected = False
return self.connected
def can_react(self, message_id: int) -> bool:
"""
Checks if goober can react to a messsage
"""
return self.can_event(message_id, "react")
def can_breaking_news(self, message_id: int) -> bool:
"""
Checks if goober can send a breaking news alert
"""
return self.can_event(message_id, "breaking_news")
def can_event(self, message_id: int, event: str, retry_depth: int = 0) -> bool:
"""
Checks if goober can send a breaking news alert
"""
logger.debug(f"Checking {event} for message {message_id}")
if not settings["bot"]["sync_hub"]["enabled"]:
logger.info("Skipping sync hub check")
return True
if retry_depth > 2:
logger.error("Too many retries. Returning false")
return False
if not self.client:
logger.error("Client no connected")
return False
if not self.connected:
logger.warning("Not connected to sync hub.. Returning False to avoid conflicts")
return False
try:
self.client.send(f"event={event};ref={message_id}")
return self.client.recv() == "unhandled"
except ConnectionResetError:
logger.error("Connection to sync hub reset! Retrying...")
if not self.__connect():
logger.error("Failed to reconnect to sync hub... Disabling")
self.connected = False
return False
logger.info("Managed to reconnect to sync hub! Retrying requests")
self.connected = True
return self.can_event(message_id, event, retry_depth+1)
instance = SyncConnector(settings["bot"]["sync_hub"]["url"])

View file

@ -1,18 +1,29 @@
import sys import sys
import traceback import traceback
import os
from modules.settings import instance as settings_manager
import logging
from modules.globalvars import RED, RESET from modules.globalvars import RED, RESET
from modules.volta.main import _ import modules.keys as k
settings = settings_manager.settings
logger = logging.getLogger("goober")
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
with open(settings["splash_text_loc"], "r") as f:
print("".join(f.readlines()))
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}{k.unhandled_exception()}{RESET}")
if context: if context:
print(f"{RED}Context: {context}{RESET}") print(f"{RED}Context: {context}{RESET}")

View file

@ -1,105 +0,0 @@
from modules.volta.main import _
from modules.globalvars import *
import requests
import subprocess
import sys
import logging
import json
import time
import random
logger = logging.getLogger("goober")
launched = False
# Run a shell command and return its output
def run_cmd(cmd):
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
return result.stdout.strip()
# Check if the remote branch is ahead of the local branch
def is_remote_ahead(branch='main', remote='origin'):
run_cmd(f'git fetch {remote}')
count = run_cmd(f'git rev-list --count HEAD..{remote}/{branch}')
return int(count) > 0
def auto_update(branch='main', remote='origin'):
if launched == True:
print(_("already_started"))
return
if AUTOUPDATE != "True":
pass # Auto-update is disabled
if is_remote_ahead(branch, remote):
print(_( "remote_ahead").format(remote=remote, branch=branch))
pull_result = run_cmd(f'git pull {remote} {branch}')
logger.info(pull_result)
logger.info(_( "please_restart"))
sys.exit(0)
else:
logger.info(_( "local_ahead").format(remote=remote, branch=branch))
def get_latest_version_info():
try:
unique_suffix = f"{int(time.time())}_{random.randint(0, 9999)}"
url = f"{UPDATE_URL}?_={unique_suffix}"
curl_cmd = [
"curl",
"-s",
"-H", "Cache-Control: no-cache",
"-H", "Pragma: no-cache",
url
]
result = subprocess.run(curl_cmd, capture_output=True, text=True, timeout=5)
content = result.stdout
if result.returncode != 0:
logger.error(f"curl failed with return code {result.returncode}")
return None
try:
data = json.loads(content)
return data
except json.JSONDecodeError:
logger.error("JSON decode failed")
logger.error(content[:500])
return None
except Exception as e:
logger.error(f"Exception in get_latest_version_info: {e}")
return None
# Check if an update is available and perform update if needed
def check_for_update(slient=False):
global latest_version, local_version, launched
latest_version_info = get_latest_version_info()
if not latest_version_info:
logger.error(f"{_('fetch_update_fail')}")
return None, None
latest_version = latest_version_info.get("version")
os.environ['gooberlatest_version'] = latest_version
download_url = latest_version_info.get("download_url")
if not latest_version or not download_url:
logger.error(f"{RED}{_('invalid_server')}{RESET}")
return None, None
# Check if local_version is valid
if local_version == "0.0.0" or None:
logger.error(f"{RED}{_('cant_find_local_version')}{RESET}")
return
# Compare local and latest versions
if slient != True:
if local_version < latest_version:
logger.info(f"{YELLOW}{_('new_version').format(latest_version=latest_version, local_version=local_version)}{RESET}")
logger.info(f"{YELLOW}{_('changelog').format(VERSION_URL=VERSION_URL)}{RESET}")
auto_update()
elif beta == True:
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 local_version > latest_version:
logger.warning(f"{_('modification_warning')}")
elif local_version == latest_version:
logger.info(f"{_('latest_version')} {local_version}")
logger.info(f"{_('latest_version2').format(VERSION_URL=VERSION_URL)}\n\n")
launched = True
return latest_version

View file

@ -1,258 +0,0 @@
# If you're seeing this after cloning the Goober repo, note that this is a standalone module for translations.
# While it's used by Goober Core, it lives in its own repository and should not be modified here.
# For updates or contributions, visit: https://github.com/gooberinc/volta
# Also, Note to self: Add more comments it needs more love
import os
import locale
import json
import pathlib
import threading
import platform
import sys
import time
from dotenv import load_dotenv
from functools import lru_cache
ANSI = "\033["
RED = f"{ANSI}31m"
GREEN = f"{ANSI}32m"
YELLOW = f"{ANSI}33m"
DEBUG = f"{ANSI}1;30m"
RESET = f"{ANSI}0m"
LOCALE = os.getenv("LOCALE")
module_dir = pathlib.Path(__file__).parent.parent
working_dir = pathlib.Path.cwd()
EXCLUDE_DIRS = {'.git', '__pycache__'}
locales_dirs = []
ENGLISH_MISSING = False
FALLBACK_LOCALE = "en"
if os.getenv("fallback_locale"):
FALLBACK_LOCALE = os.getenv("fallback_locale")
def find_locales_dirs(base_path):
found = []
for root, dirs, files in os.walk(base_path):
dirs[:] = [d for d in dirs if d not in EXCLUDE_DIRS]
if 'locales' in dirs:
locales_path = pathlib.Path(root) / 'locales'
found.append(locales_path)
dirs.remove('locales')
return found
def find_dotenv(start_path: pathlib.Path) -> pathlib.Path | None:
current = start_path.resolve()
while current != current.parent:
candidate = current / ".env"
if candidate.exists():
return candidate
current = current.parent
return None
def load_settings_json() -> dict | None:
start_path = working_dir.resolve()
current = start_path.resolve()
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:
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))
if working_dir != module_dir:
locales_dirs.extend(find_locales_dirs(working_dir))
translations = {}
_file_mod_times = {}
def get_system_locale():
system = platform.system() # fallback incase locale isnt set
if system == "Windows":
lang, _ = locale.getdefaultlocale()
return lang or os.getenv("LANG")
elif system == "Darwin":
try:
import subprocess
result = subprocess.run(
["defaults", "read", "-g", "AppleLocale"],
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
text=True
)
return result.stdout.strip() or locale.getdefaultlocale()[0]
except Exception:
return locale.getdefaultlocale()[0]
elif system == "Linux":
return (
os.getenv("LC_ALL") or
os.getenv("LANG") or
locale.getdefaultlocale()[0]
)
return locale.getdefaultlocale()[0]
def load_translations():
global translations, _file_mod_times
translations.clear()
_file_mod_times.clear()
for locales_dir in locales_dirs:
for filename in os.listdir(locales_dir):
if filename.endswith(".json"):
lang_code = filename[:-5]
file_path = locales_dir / filename
try:
with open(file_path, "r", encoding="utf-8") as f:
data = json.load(f)
if lang_code not in translations:
translations[lang_code] = {}
translations[lang_code].update(data)
_file_mod_times[(lang_code, file_path)] = file_path.stat().st_mtime
except Exception as e:
print(f"[VOLTA] {RED}Failed loading {file_path}: {e}{RESET}")
def reload_if_changed():
while True:
for (lang_code, file_path), last_mtime in list(_file_mod_times.items()):
try:
current_mtime = file_path.stat().st_mtime
if current_mtime != last_mtime:
print(f"[VOLTA] {RED}Translation file changed: {file_path}, reloading...{RESET}")
_lookup_translation.cache_clear()
load_translations()
break
except FileNotFoundError:
print(f"[VOLTA] {RED}Translation file removed: {file_path}{RESET}")
_file_mod_times.pop((lang_code, file_path), None)
if lang_code in translations:
translations.pop(lang_code, None)
def set_language(lang: str):
global LOCALE, ENGLISH_MISSING
if not LOCALE:
LOCALE = get_system_locale()
elif lang in translations:
LOCALE = lang
else:
print(f"[VOLTA] {RED}Language '{lang}' not found, defaulting to 'en'{RESET}")
if FALLBACK_LOCALE in translations:
LOCALE = FALLBACK_LOCALE
else:
print(f"[VOLTA] {RED}The fallback translations cannot be found! No fallback available.{RESET}")
ENGLISH_MISSING = True
_lookup_translation.cache_clear()
def check_missing_translations(LOCALE=LOCALE):
global ENGLISH_MISSING
load_translations()
if FALLBACK_LOCALE not in translations:
print(f"[VOLTA] {RED}Fallback translations ({FALLBACK_LOCALE}.json) missing from assets/locales.{RESET}")
ENGLISH_MISSING = True
return
if LOCALE == "en":
print("[VOLTA] Locale is English, skipping missing key check.")
return
en_keys = set(translations.get("en", {}).keys())
locale_keys = set(translations.get(LOCALE, {}).keys())
missing_keys = en_keys - locale_keys
total_keys = len(en_keys)
missing_count = len(missing_keys)
if missing_count > 0:
percent_missing = (missing_count / total_keys) * 100
if percent_missing == 100:
print(f"[VOLTA] {YELLOW}Warning: All keys are missing in locale '{LOCALE}'! Defaulting back to {FALLBACK_LOCALE}{RESET}")
set_language(FALLBACK_LOCALE)
elif percent_missing > 0:
print(f"[VOLTA] {YELLOW}Warning: {missing_count}/{total_keys} keys missing in locale '{LOCALE}' ({percent_missing:.1f}%)!{RESET}")
for key in sorted(missing_keys):
print(f" - {key}")
time.sleep(2)
else:
print(f"[VOLTA] All translation keys present for locale: {LOCALE}")
printedsystemfallback = False
@lru_cache(maxsize=600)
def _lookup_translation(lang: str, key: str):
return translations.get(lang, {}).get(key)
def get_translation(lang: str, key: str):
global printedsystemfallback
if ENGLISH_MISSING:
return f"[VOLTA] {RED}No fallback available!{RESET}"
val = _lookup_translation(lang, key)
if val:
return val
sys_lang = get_system_locale().split("_")[0] if get_system_locale() else None
if sys_lang and sys_lang != lang:
sys_val = _lookup_translation(sys_lang, key)
if sys_val:
if not printedsystemfallback:
print(f"[VOLTA] {YELLOW}Falling back to system language {sys_lang}!{RESET}")
printedsystemfallback = True
return sys_val
fallback_val = _lookup_translation(FALLBACK_LOCALE, key)
if fallback_val:
print(f"[VOLTA] {YELLOW}Missing key: '{key}' in '{lang}', falling back to fallback locale '{FALLBACK_LOCALE}'{RESET}")
return fallback_val
return f"[VOLTA] {YELLOW}Missing key: '{key}' in all locales!{RESET}"
def _(key: str) -> str:
return get_translation(LOCALE, key)
load_translations()
watchdog_thread = threading.Thread(target=reload_if_changed, daemon=True)
watchdog_thread.start()
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("LOCALE", help="Locale to validate")
args = parser.parse_args()
print("[VOLTA] Validating all locales....")
check_missing_translations(LOCALE=f"{args.LOCALE}")

51
replace_volta.py Normal file
View file

@ -0,0 +1,51 @@
import os
import re
folder_path = "."
# Real trap regex 😮‍💨 — group(1)=key, group(2)=format args (optional)
pattern = re.compile(
r"""
(?<!\w) # not part of a variable name
\(? # optional opening (
_\(\s*'([a-zA-Z0-9_]+)'\s*\) # k.key()
\)? # optional closing )
(?:\.format\((.*?)\))? # optional .format(...)
""",
re.VERBOSE,
)
def fix_content(content):
def repl(match):
key = match.group(1)
args = match.group(2)
if args:
return f"k.{key}({args})"
else:
return f"k.{key}()"
return pattern.sub(repl, content)
# File types we sweepin 🧹
file_exts = [".py", ".html", ".txt", ".js"]
for subdir, _, files in os.walk(folder_path):
for file in files:
if any(file.endswith(ext) for ext in file_exts):
path = os.path.join(subdir, file)
with open(path, "r", encoding="utf-8") as f:
original = f.read()
updated = fix_content(original)
if original != updated:
print(f"🛠️ Fixed: {path}")
with open(path, "w", encoding="utf-8") as f:
f.write(updated)
print(
"🚀💥 ALL cleaned. No `_('...')` left on road — now its k.dot or nothin fam 😎🔫"
)

View file

@ -4,6 +4,10 @@ spacy
spacytextblob spacytextblob
requests requests
psutil psutil
better_profanity
python-dotenv python-dotenv
dotenv dotenv
pillow pillow
watchdog
py-cpuinfo
websocket-client

View file

@ -0,0 +1,44 @@
{
"bot": {
"prefix": "o.",
"owner_ids": [
642441889181728810
],
"blacklisted_users": [
1391805740716527666
],
"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!",
"positive_gifs": [
"https://tenor.com/view/i-want-a-divorce-dissolution-annulment-seperation-break-up-gif-25753155"
],
"block_profanity": false,
"activity": {
"content": "Rakas - Haloo Helsinki",
"type": "listening"
}
},
"active_memory": "memory.json",
"enabled_cogs": [
"pulse",
"breaking_news"
],
"sync_hub": {
"url": "ws://goober.frii.site",
"enabled": true
}
},
"locale": "en",
"name": "gubert",
"auto_update": true,
"disable_checks": false,
"splash_text_loc": "settings/splash.txt",
"cog_settings": {
"breaking_news": {
"create_from_message_content": true
}
}
}

View file

@ -1,26 +0,0 @@
{
"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

@ -1,2 +1,4 @@
- fix missing translations in some cases
- revamp wiki - revamp wiki
- clean the rest
- alot - alot

16
updater.py Normal file
View file

@ -0,0 +1,16 @@
import subprocess
import time
import sys
import os
import logging
logger = logging.getLogger("goober")
def force_update() -> None:
logger.info("Forcefully updating...")
stash = subprocess.run(["git", "stash"], capture_output=True)
logger.info(stash)
pull = subprocess.run(["git", "pull", "origin", "main"], check=True, capture_output=True)
logger.info(pull)