Compare commits

..

29 commits
main ... dev

Author SHA1 Message Date
WhatDidYouExpect
b860d0e271 a 2025-07-22 00:25:56 +02:00
WhatDidYouExpect
7d6c5aae51 ts fuckass minigames file will be the death of me 2025-07-22 00:19:55 +02:00
WhatDidYouExpect
f17db0d22c canadian french added i dont know any normal french and the guy i know is canadian 2025-07-21 22:13:42 +02:00
WhatDidYouExpect
ed53089d4a :p 2025-07-21 22:10:00 +02:00
WhatDidYouExpect
221f2370e4 oh yeah 2025-07-21 21:12:31 +02:00
WhatDidYouExpect
3c71e9f54e bundled goober 0.0.5 in with default goober incase i dunno they want a small version of it 2025-07-21 16:58:35 +02:00
WhatDidYouExpect
bf421e4fd0 swear to your hearts content 2025-07-19 00:34:38 +02:00
WhatDidYouExpect
fcb1a9782a Linux openmediavault 6.12.32+bpo-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.12.32-1~bpo12+1 (2025-06-21) x86_64 GNU/Linux 2025-07-19 00:30:57 +02:00
WhatDidYouExpect
59ce219183 fuckass interactions.... 2025-07-18 16:15:34 +02:00
WhatDidYouExpect
2e4576ba4b do NOT clone this ✌️😭 2025-07-18 16:09:14 +02:00
WhatDidYouExpect
a92439b352 up 2 date w/volta 2025-07-18 14:20:24 +02:00
WhatDidYouExpect
39f2c26fec updated volta 2025-07-18 14:08:36 +02:00
WhatDidYouExpect
954014d1a4 fuck 2025-07-18 14:03:35 +02:00
WhatDidYouExpect
52d9b058cf half assed translations not doing this today 2025-07-18 14:02:57 +02:00
WhatDidYouExpect
8666c83565 prolly gonna move command to their own modules soon soo 2025-07-18 13:57:58 +02:00
WhatDidYouExpect
9a4d486e25 Please enter the commit message for your changes. Lines starting 2025-07-18 01:40:40 +02:00
WhatDidYouExpect
f5d7121dfb changed from github to git cause i self host it now (i use arch btw) 2025-07-18 01:36:16 +02:00
WhatDidYouExpect
01b40f8b58 added python version jst incase 2025-07-18 01:33:01 +02:00
WhatDidYouExpect
cbe7fe201f dumbass forgot to add () 2025-07-18 01:31:46 +02:00
WhatDidYouExpect
4a695a7bac added os why not fuck it amirite 2025-07-18 01:30:57 +02:00
WhatDidYouExpect
ae2d565004 dynamic git URL 2025-07-18 01:29:31 +02:00
WhatDidYouExpect
280df4f5e0 fuck it one liner 2025-07-18 01:26:55 +02:00
WhatDidYouExpect
dec83f1513 why did they call the song rule #34 if it has nothing to do with rule34 2025-07-18 00:49:04 +02:00
WhatDidYouExpect
54b8bf4c59 change submodule path 2025-07-17 23:48:41 +02:00
WhatDidYouExpect
53e0eb5289 bump it to 2.3.4 2025-07-17 23:35:59 +02:00
WhatDidYouExpect
016e907d39 probably wont break anything (foreshadowing) 2025-07-17 17:42:21 +02:00
WhatDidYouExpect
cb45a9fffc
Update todo.txt 2025-07-17 15:33:32 +02:00
WhatDidYouExpect
7c6be59dc9 yeah no lastfm gets the status change so fix later i guess 2025-07-17 15:22:39 +02:00
WhatDidYouExpect
c565f962c5 sigh 2025-07-16 20:11:08 +02:00
57 changed files with 1848 additions and 5105 deletions

View file

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

5
.gitignore vendored
View file

@ -12,7 +12,4 @@ received_memory.json
translation_report.txt
translationcompleteness.py
modules/volta
log.txt
settings/admin_logs.json
settings/settings.json
assets/images/cached/*
log.txt

2
.gitmodules vendored
View file

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

View file

@ -1,2 +1,9 @@
Real repo: https://forgejo.expect.ovh/gooberinc/goober
This is just a fork that was made the upstream
knockoff of genai basically :p
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

View file

@ -8,9 +8,6 @@ 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)

View file

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

67
assets/cogs/cogmanager.py Normal file
View file

@ -0,0 +1,67 @@
import discord
from discord.ext import commands
from modules.globalvars import ownerid
COG_PREFIX = "assets.cogs."
class CogManager(commands.Cog):
def __init__(self, bot):
self.bot = bot
@commands.command()
async def load(self, ctx, cog_name: str = None):
if ctx.author.id != ownerid:
await ctx.send("You do not have permission to use this command.")
return
if cog_name is None:
await ctx.send("Please provide the cog name to load.")
return
try:
await self.bot.load_extension(COG_PREFIX + cog_name)
await ctx.send(f"Loaded cog `{cog_name}` successfully.")
except Exception as e:
await ctx.send(f"Error loading cog `{cog_name}`: {e}")
@commands.command()
async def unload(self, ctx, cog_name: str = None):
if ctx.author.id != ownerid:
await ctx.send("You do not have permission to use this command.")
return
if cog_name is None:
await ctx.send("Please provide the cog name to unload.")
return
try:
await self.bot.unload_extension(COG_PREFIX + cog_name)
await ctx.send(f"Unloaded cog `{cog_name}` successfully.")
except Exception as e:
await ctx.send(f"Error unloading cog `{cog_name}`: {e}")
@commands.command()
async def reload(self, ctx, cog_name: str = None):
if ctx.author.id != ownerid:
await ctx.send("You do not have permission to use this command.")
return
if cog_name is None:
await ctx.send("Please provide the cog name to reload.")
return
try:
await self.bot.unload_extension(COG_PREFIX + cog_name)
await self.bot.load_extension(COG_PREFIX + cog_name)
await ctx.send(f"Reloaded cog `{cog_name}` successfully.")
except Exception as e:
await ctx.send(f"Error reloading cog `{cog_name}`: {e}")
@commands.command()
async def listcogs(self, ctx):
"""Lists all currently loaded cogs in an embed."""
cogs = list(self.bot.cogs.keys())
if not cogs:
await ctx.send("No cogs are currently loaded.")
return
embed = discord.Embed(title="Loaded Cogs", description="Here is a list of all currently loaded cogs:")
embed.add_field(name="Cogs", value="\n".join(cogs), inline=False)
await ctx.send(embed=embed)
async def setup(bot):
await bot.add_cog(CogManager(bot))

View file

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

View file

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

View file

@ -1,10 +1,9 @@
import discord
from discord.ext import commands
from modules.image import *
from modules.volta.main import _
from PIL import Image, ImageEnhance, ImageFilter, ImageOps, ImageChops, ImageColor
import os, random, shutil, tempfile
import modules.keys as k
async def deepfryimage(path):
with Image.open(path).convert("RGB") as im:
@ -45,17 +44,14 @@ 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"))
]
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))
@ -70,7 +66,7 @@ class whami(commands.Cog):
else:
fallback_image = get_random_asset_image()
if fallback_image is None:
await ctx.reply(k.no_image_available())
await ctx.reply(_('no_image_available'))
return
temp_input = tempfile.mktemp(suffix=os.path.splitext(fallback_image)[1])
shutil.copy(fallback_image, temp_input)
@ -78,7 +74,7 @@ class whami(commands.Cog):
else:
fallback_image = get_random_asset_image()
if fallback_image is None:
await ctx.reply(k.no_image_available())
await ctx.reply(_('no_image_available'))
return
temp_input = tempfile.mktemp(suffix=os.path.splitext(fallback_image)[1])
shutil.copy(fallback_image, temp_input)
@ -89,7 +85,7 @@ class whami(commands.Cog):
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())
await ctx.reply(_('failed_generate_image'))
return
deepfried_path = await deepfryimage(output_path)
@ -98,6 +94,5 @@ class whami(commands.Cog):
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,231 +0,0 @@
import os
from typing import Dict, List
import discord
from discord.ext import commands
import discord.ext
import discord.ext.commands
import modules.keys as k
from modules.permission import requires_admin
from modules.sentenceprocessing import send_message
from modules.settings import instance as settings_manager
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
class BaseCommands(commands.Cog):
def __init__(self, bot):
self.bot: discord.ext.commands.Bot = bot
@commands.command()
async def help(self, ctx: commands.Context) -> None:
embed: discord.Embed = discord.Embed(
title=f"{k.command_help_embed_title()}",
description=f"{k.command_help_embed_desc()}",
color=discord.Colour(0x000000),
)
command_categories = {
f"{k.command_help_categories_general()}": [
"mem",
"talk",
"about",
"ping",
"impact",
"demotivator",
"help",
],
f"{k.command_help_categories_admin()}": ["stats", "retrain", "setlanguage"],
}
custom_commands: List[str] = []
for cog_name, cog in self.bot.cogs.items():
for command in cog.get_commands():
if (
command.name
not in command_categories[f"{k.command_help_categories_general()}"]
and command.name
not in command_categories[f"{k.command_help_categories_admin()}"]
):
custom_commands.append(command.name)
if custom_commands:
embed.add_field(
name=f"{k.command_help_categories_custom()}",
value="\n".join(
[
f"{settings['bot']['prefix']}{command}"
for command in custom_commands
]
),
inline=False,
)
for category, commands_list in command_categories.items():
commands_in_category: str = "\n".join(
[f"{settings['bot']['prefix']}{command}" for command in commands_list]
)
embed.add_field(name=category, value=commands_in_category, inline=False)
await send_message(ctx, embed=embed)
@requires_admin()
@commands.command()
async def setlanguage(self, ctx: commands.Context, locale: str) -> None:
await ctx.defer()
k.change_language(locale)
settings["locale"] = locale # type: ignore
settings_manager.commit()
await ctx.send(":thumbsup:")
@commands.command()
async def ping(self, ctx: commands.Context) -> None:
await ctx.defer()
latency: int = round(self.bot.latency * 1000)
embed: discord.Embed = discord.Embed(
title="Pong!!",
description=(
settings["bot"]["misc"]["ping_line"],
f"`{k.command_ping_embed_desc()}: {latency}ms`\n",
),
color=discord.Colour(0x000000),
)
embed.set_footer(
text=f"{k.command_ping_footer()} {ctx.author.name}",
icon_url=ctx.author.display_avatar.url,
)
await ctx.send(embed=embed)
@commands.command()
async def about(self, ctx: commands.Context) -> None:
embed: discord.Embed = discord.Embed(
title=k.command_about_embed_title(),
description="",
color=discord.Colour(0x000000),
)
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)
@commands.command()
async def stats(self, ctx: commands.Context) -> None:
memory_file: str = settings["bot"]["active_memory"]
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:
line_count: int = sum(1 for _ in file)
embed: discord.Embed = discord.Embed(
title=f"{k.command_stats_embed_title()}",
description=f"{k.command_stats_embed_desc()}",
color=discord.Colour(0x000000),
)
embed.add_field(
name=f"{k.command_stats_embed_field1name()}",
value=f"{k.command_stats_embed_field1value(file_size=file_size, line_count=line_count)}",
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:
splash_text = "".join(f.readlines())
embed.add_field(
name=f"{k.command_stats_embed_field3name()}",
value=f"""{k.command_stats_embed_field3value(
NAME=settings["name"], PREFIX=settings["bot"]["prefix"], ownerid=settings["bot"]["owner_ids"][0],
PING_LINE=settings["bot"]["misc"]["ping_line"], showmemenabled=settings["bot"]["allow_show_mem_command"],
USERTRAIN_ENABLED=settings["bot"]["user_training"], song=settings["bot"]["misc"]["activity"]["content"],
splashtext=splash_text
)}""",
inline=False,
)
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()
async def mem(self, ctx: commands.Context) -> None:
if not settings["bot"]["allow_show_mem_command"]:
return
with open(settings["bot"]["active_memory"], "rb") as f:
data: bytes = f.read()
response = requests.post(
"https://litterbox.catbox.moe/resources/internals/api.php",
data={"reqtype": "fileupload", "time": "1h"},
files={"fileToUpload": data},
)
await send_message(ctx, response.text)
@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):
print("Setting up base_commands")
bot.remove_command("help")
await bot.add_cog(BaseCommands(bot))

View file

@ -1,129 +0,0 @@
import discord
from discord.ext import commands
import discord.ext
import discord.ext.commands
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."
class CogManager(commands.Cog):
def __init__(self, bot):
self.bot = bot
@requires_admin()
@commands.command()
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:
await ctx.send("Please provide the cog name to load.")
return
try:
await self.bot.load_extension(COG_PREFIX + cog_name)
await ctx.send(f"Loaded cog `{cog_name}` successfully.")
except Exception as e:
await ctx.send(f"Error loading cog `{cog_name}`: {e}")
@requires_admin()
@commands.command()
async def unload(self, ctx, cog_name: str | None = None):
if cog_name is None:
await ctx.send("Please provide the cog name to unload.")
return
try:
await self.bot.unload_extension(COG_PREFIX + cog_name)
await ctx.send(f"Unloaded cog `{cog_name}` successfully.")
except Exception as e:
await ctx.send(f"Error unloading cog `{cog_name}`: {e}")
@requires_admin()
@commands.command()
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:
await ctx.send("Please provide the cog name to reload.")
return
if cog_name[:-3] not in settings["bot"]["enabled_cogs"]:
await ctx.send("Please enable the cog first!")
return
try:
await self.bot.unload_extension(COG_PREFIX + cog_name)
await self.bot.load_extension(COG_PREFIX + cog_name)
await ctx.send(f"Reloaded cog `{cog_name}` successfully.")
except Exception as e:
await ctx.send(f"Error reloading cog `{cog_name}`: {e}")
@commands.command()
async def listcogs(self, ctx):
"""Lists all currently loaded cogs in an embed."""
cogs = list(self.bot.cogs.keys())
if not cogs:
await ctx.send("No cogs are currently loaded.")
return
embed = discord.Embed(
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)
async def setup(bot):
await bot.add_cog(CogManager(bot))

View file

@ -1,123 +0,0 @@
import os
import random
import re
import discord
from discord.ext import commands
import discord.ext
import discord.ext.commands
from modules.markovmemory import (
load_markov_model,
save_markov_model,
train_markov_model,
)
from modules.permission import requires_admin
from modules.sentenceprocessing import (
improve_sentence_coherence,
is_positive,
rephrase_for_coherence,
send_message,
)
import modules.keys as k
import logging
from typing import List, Optional, Set
import json
import time
import markovify
logger = logging.getLogger("goober")
from modules.settings import instance as settings_manager
settings = settings_manager.settings
class Markov(commands.Cog):
def __init__(self, bot):
self.bot: discord.ext.commands.Bot = bot
self.model: markovify.NewlineText | None = load_markov_model()
@requires_admin()
@commands.command()
async def retrain(self, ctx: discord.ext.commands.Context):
message_ref: discord.Message | None = await send_message(
ctx, f"{k.command_markov_retrain()}"
)
if message_ref is None:
logger.error("Failed to send message!")
return
try:
with open(settings["bot"]["active_memory"], "r") as f:
memory: List[str] = json.load(f)
except FileNotFoundError:
await send_message(ctx, f"{k.command_markov_memory_not_found()}")
return
except json.JSONDecodeError:
await send_message(ctx, f"{k.command_markov_memory_is_corrupt()}")
return
data_size: int = len(memory)
processing_message_ref: discord.Message | None = await send_message(
ctx, f"{k.command_markov_retraining(data_size)}"
)
if processing_message_ref is None:
logger.error("Couldnt find message processing message!")
start_time: float = time.time()
model = train_markov_model(memory)
if not model:
logger.error("Failed to train markov model")
await ctx.send("Failed to retrain!")
return False
self.model = model
save_markov_model(self.model)
logger.debug(f"Completed retraining in {round(time.time() - start_time,3)}s")
await send_message(
ctx,
f"{k.command_markov_retrain_successful(data_size)}",
edit=True,
message_reference=processing_message_ref,
)
@commands.command()
async def talk(self, ctx: commands.Context, sentence_size: int = 5) -> None:
if not self.model:
await send_message(ctx, f"{k.command_talk_insufficent_text()}")
return
response: str = ""
if sentence_size == 1:
response = (
self.model.make_short_sentence(max_chars=200, tries=700)
or k.command_talk_generation_fail()
)
else:
response = improve_sentence_coherence(
self.model.make_sentence(tries=100, max_words=sentence_size)
or k.command_talk_generation_fail()
)
cleaned_response: str = re.sub(r"[^\w\s]", "", response).lower()
coherent_response: str = rephrase_for_coherence(cleaned_response)
if random.random() < 0.9 and is_positive(coherent_response):
gif_url: str = random.choice(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):
await bot.add_cog(Markov(bot))

View file

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

View file

@ -6,12 +6,11 @@ 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
#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
@ -35,11 +34,7 @@ class LastFmCog(commands.Cog):
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 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...")
@ -57,11 +52,7 @@ class LastFmCog(commands.Cog):
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 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):
@ -80,13 +71,12 @@ class LastFmCog(commands.Cog):
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")
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

123
assets/cogs/lyrics.py Normal file
View file

@ -0,0 +1,123 @@
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,12 +2,7 @@ from discord.ext import commands
import discord
from collections import defaultdict, Counter
import datetime
from modules.permission import requires_admin
from modules.settings import instance as settings_manager
settings = settings_manager.settings
from modules.globalvars import ownerid
class StatsCog(commands.Cog):
def __init__(self, bot):
self.bot = bot
@ -34,54 +29,38 @@ class StatsCog(commands.Cog):
async def on_command(self, ctx):
self.command_usage[ctx.command.qualified_name] += 1
@requires_admin()
@commands.command()
async def spyware(self, ctx):
if ctx.author.id != ownerid:
return
uptime = datetime.datetime.utcnow() - self.start_time
hours_elapsed = max((uptime.total_seconds() / 3600), 1)
avg_per_hour = self.total_messages / hours_elapsed
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:
peak_hour, peak_count = "N/A", 0
top_users = self.user_message_counts.most_common(5)
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="Total Messages", value=str(self.total_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,
)
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(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(f"<@{user_id}>: {count} messages" for user_id, count in top_users)
or "No data"
)
top_str = "\n".join(
f"<@{user_id}>: {count} messages" for user_id, count in top_users
) or "No data"
embed.add_field(name="Top Chatters", value=top_str, inline=False)
cmd_str = (
"\n".join(
f"{cmd}: {count}" for cmd, count in self.command_usage.most_common(5)
)
or "No commands used yet"
)
cmd_str = "\n".join(
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)
await ctx.send(embed=embed)
async def setup(bot):
await bot.add_cog(StatsCog(bot))

View file

@ -0,0 +1,22 @@
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

@ -3,9 +3,6 @@ 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
@ -19,20 +16,18 @@ class songchange(commands.Cog):
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}")
async def changesong(self, ctx):
if LOCAL_VERSION_FILE > "0.11.8":
await ctx.send(f"Goober is too old! you must have version 0.11.8 you have {local_version}")
return
await ctx.send("Check the terminal! (this does not persist across restarts)")
song = input("\nEnter a song:\n")
try:
await self.bot.change_presence(
activity=discord.Activity(
type=discord.ActivityType.listening, name=f"{song}"
)
)
await self.bot.change_presence(activity=discord.Activity(type=discord.ActivityType.listening, name=f"{song}"))
print(f"{GREEN}Changed song to {song}{RESET}")
except Exception as e:
print(f"{RED}An error occurred while changing songs..: {str(e)}{RESET}")
async def setup(bot):
await bot.add_cog(songchange(bot))

View file

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

View file

@ -5,12 +5,7 @@ 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
from modules.globalvars import ownerid
class WebScraper(commands.Cog):
def __init__(self, bot):
self.bot = bot
@ -27,7 +22,7 @@ class WebScraper(commands.Cog):
def extract_sentences(self, text):
"""Extract sentences from text."""
sentences = text.split(".")
sentences = text.split('.')
return [sentence.strip() for sentence in sentences if sentence.strip()]
def save_to_json(self, sentences):
@ -54,6 +49,7 @@ class WebScraper(commands.Cog):
print("No data to undo.")
return False
data = data[:-1]
with open("memory.json", "w") as file:
@ -77,14 +73,18 @@ class WebScraper(commands.Cog):
soup = BeautifulSoup(html, "html.parser")
for paragraph in soup.find_all("p"):
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 ctx.author.id != ownerid:
await ctx.send("You do not have permission to use this command.")
return
if not start_url.startswith("http"):
await ctx.send("Please provide a valid URL.")
return
@ -96,16 +96,18 @@ class WebScraper(commands.Cog):
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."""
if ctx.author.id != ownerid:
await ctx.send("You do not have permission to use this command.")
return
success = self.undo_last_scrape()
if success:
await ctx.send("Last scrape undone successfully.")
else:
await ctx.send("No data to undo or an error occurred.")
async def setup(bot):
await bot.add_cog(WebScraper(bot))

View file

@ -14,7 +14,6 @@ from modules.globalvars import VERSION_URL
import sys
import subprocess
class GooberWeb(commands.Cog):
def __init__(self, bot):
self.bot = bot
@ -25,20 +24,18 @@ class GooberWeb(commands.Cog):
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.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()
@ -55,82 +52,69 @@ class GooberWeb(commands.Cog):
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,
}
)
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,
}
)
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,
}
)
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)
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)
@ -139,62 +123,62 @@ class GooberWeb(commands.Cog):
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")
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}%",
@ -209,29 +193,23 @@ class GooberWeb(commands.Cog):
"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 ""
),
"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"),
"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", "")
song = request.query.get('song', '')
if song:
await self.bot.change_presence(
activity=discord.Activity(
type=discord.ActivityType.listening, name=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)
@ -243,37 +221,36 @@ class GooberWeb(commands.Cog):
async def read_env_file(self):
env_vars = {}
try:
with open(".env", "r") as f:
with open('.env', 'r') as f:
for line in f:
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
if not line or line.startswith('#') or '=' not in line:
continue
key, value = line.split("=", 1)
key, value = line.split('=', 1)
key = key.strip()
if key in ["splashtext", "DISCORD_BOT_TOKEN"]:
if key in ['splashtext', 'DISCORD_BOT_TOKEN']:
continue
env_vars[key] = value.strip("\"'")
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:
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('"')
)
if line.startswith('VERSION_URL'):
config_vars['VERSION_URL'] = line.split('=', 1)[1].strip().strip('"')
except FileNotFoundError:
pass
settings_html = """
<!DOCTYPE html>
<html>
@ -295,7 +272,7 @@ class GooberWeb(commands.Cog):
<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'>
@ -303,7 +280,7 @@ class GooberWeb(commands.Cog):
<input type='text' id='{key}' name='{key}' value='{value}'>
</div>
"""
for key, value in config_vars.items():
settings_html += f"""
<div class='form-group'>
@ -311,7 +288,7 @@ class GooberWeb(commands.Cog):
<input type='text' id='{key}' name='{key}' value='{value}'>
</div>
"""
settings_html += """
<button type='submit'>Save Settings</button>
</form>
@ -322,15 +299,15 @@ class GooberWeb(commands.Cog):
</body>
</html>
"""
return web.Response(text=settings_html, content_type="text/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:
with open('.env', 'r') as f:
env_text = f.read()
except FileNotFoundError:
pass
@ -338,42 +315,32 @@ class GooberWeb(commands.Cog):
def replace_match(match):
key = match.group(1)
value = match.group(2)
if key in ["splashtext", "DISCORD_BOT_TOKEN"]:
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 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,
)
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")
with open('.env', 'w') as f:
f.write(env_text.strip() + '\n')
if "VERSION_URL" in data:
if 'VERSION_URL' in data:
config_text = ""
try:
with open("config.py", "r") as f:
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,
)
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")
with open('config.py', 'w') as f:
f.write(config_text.strip() + '\n')
return aiohttp.web.Response(text="Settings updated successfully!")
@ -381,12 +348,8 @@ class GooberWeb(commands.Cog):
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>'
)
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}
@ -397,12 +360,8 @@ class GooberWeb(commands.Cog):
</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>'
)
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}
@ -413,11 +372,11 @@ class GooberWeb(commands.Cog):
</div>
"""
owner_id = stats.get("owner")
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))
@ -426,6 +385,7 @@ class GooberWeb(commands.Cog):
except:
pass
html_content = f"""
<!DOCTYPE html>
<html>
@ -906,16 +866,15 @@ class GooberWeb(commands.Cog):
</body>
</html>
"""
return web.Response(text=html_content, content_type="text/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,7 +1,6 @@
import discord
from discord.ext import commands
class whoami(commands.Cog):
def __init__(self, bot):
self.bot = bot
@ -14,13 +13,12 @@ class whoami(commands.Cog):
embed = discord.Embed(
title="User Information",
description=f"Your User ID is: {user_id}\n"
f"Your username is: {username}\n"
f"Your nickname in this server is: <@{user_id}>",
color=discord.Color.blue(),
f"Your username is: {username}\n"
f"Your nickname in this server is: <@{user_id}>",
color=discord.Color.blue()
)
await ctx.send(embed=embed)
async def setup(bot):
await bot.add_cog(whoami(bot))

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

View file

@ -1,8 +1,23 @@
{
"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!",
"file_aint_uft8": "File is not valid UTF-8 text. Might be binary or corrupted.",
"psutil_not_installed": "Memory check skipped.",
"not_cloned": "Goober is not cloned! Please clone it from GitHub.",
"not_cloned": "Goober is not cloned! Please clone it from Git.",
"checks_disabled": "Checks are disabled!",
"unhandled_exception": "An unhandled exception occurred. Please report this issue on GitHub.",
"active_users:": "Active users:",
@ -131,14 +146,6 @@
"command_stats_embed_field2name": "Version",
"command_stats_embed_field2value": "Local: {local_version} \nLatest: {latest_version}",
"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}```",
"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}"
"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}```"
}

View file

@ -73,5 +73,5 @@
"command_stats_embed_field2name": "Version",
"command_stats_embed_field2value": "Version local: {local_version} \nUltima version: {latest_version}",
"command_stats_embed_field3name": "informacion sobre las variables",
"command_stats_embed_field3value": "Nombre: {NAME} \nPrefijo: {PREFIX} \nID del propietario: {ownerid}\nLinea de ping: {PING_LINE} \nCompartir memoria habilitada: {showmemenabled} \nEntrenamiento de usuario habilitado: {USERTRAIN_ENABLED} \nCancion: {song} \nTexto de bienvenida: ```{splashtext}```"
"command_stats_embed_field3value": "Nombre: {NAME} \nPrefijo: {PREFIX} \nID del propietario: {ownerid}\nLinea de ping: {PING_LINE} \nCompartir memoria habilitada: {showmemenabled} \nEntrenamiento de usuario habilitado: {USERTRAIN_ENABLED} \nCancion: {song} \nTexto de bienvenida: ```{splashtext}```"
}

View file

@ -102,7 +102,7 @@
"command_markov_retrain": "Uudelleenkoulutetaan markov-mallia... Odota.",
"command_markov_memory_not_found": "Virhe: muistitiedostoa ei löytynyt!",
"command_markov_memory_is_corrupt": "Virhe: muistitiedosto on korruptoitu!",
"command_markov_retraining": "Käsitellään {processed_data}/{data_size} datapistettä...",
"command_markov_retraining": "Käsitellään {processed_data}/{data_size} datapisteestä...",
"command_markov_retrain_successful": "Markov-malli koulutettiin uudestaan {data_size} datapisteellä!",
"command_desc_talk":"puhuu ja sillei",
"command_talk_insufficent_text": "Minun pitää oppia lisää viesteistä ennen kun puhun.",
@ -131,6 +131,5 @@
"command_stats_embed_field2value": "Paikallinen: {local_version} \nUusin: {latest_version}",
"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}```"
}

View file

@ -1,130 +0,0 @@
{
"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}```"
}

149
assets/locales/fr_ca.json Normal file
View file

@ -0,0 +1,149 @@
{
"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,8 +1,23 @@
{
"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!",
"file_aint_utf8": "Il file non è un UTF-8 valido. Forse è binario?",
"psutil_not_installed": "Controllo memoria saltato.",
"not_cloned": "Goober non è stato clonato! Clonalo da GitHub.",
"not_cloned": "Goober non è stato clonato! Clonalo da Git.",
"checks_disabled": "I controlli sono disabilitati!",
"unhandled_exception": "Si è verificata un'eccezione non gestita. Segnala questo problema su GitHub, per favore.",
"active_users:": "Utenti attivi:",

615
bot.py
View file

@ -1,5 +1,22 @@
import os
import re
import json
import time
import random
import traceback
import subprocess
import tempfile
import shutil
import psutil
import asyncio
import platform
import sys
from typing import List, Dict, Set, Optional, Tuple, Any, Union, Callable, Coroutine, TypeVar, Type
import logging
from modules.globalvars import *
from modules.prestartchecks import start_checks
from modules.logger import GooberFormatter
import logging
logger = logging.getLogger("goober")
logger.setLevel(logging.DEBUG)
@ -15,238 +32,205 @@ file_handler.setFormatter(GooberFormatter(colors=False))
logger.addHandler(console_handler)
logger.addHandler(file_handler)
import os
import re
import json
import time
import random
import traceback
import subprocess
import tempfile
import shutil
import sys
from typing import (
List,
Dict,
Literal,
Set,
Optional,
Tuple,
Any,
TypedDict,
Union,
Callable,
Coroutine,
TypeVar,
Type,
)
import logging
from modules.prestartchecks import start_checks
import modules.keys as k
from modules import key_compiler
import logging
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.sync_conenctor import instance as sync_connector
import threading
def build_keys():
key_compiler.build_result(
"en",
"assets/locales",
types=True,
output_path="modules/keys.py",
generate_comments=True,
)
build_keys()
settings = settings_manager.settings
splash_text: str = ""
k.change_language(settings["locale"])
with open(settings["splash_text_loc"], "r", encoding="UTF-8") as f:
splash_text = "".join(f.readlines())
print(splash_text)
# Print splash text and check for updates
print(splashtext) # Print splash text (from modules/globalvars.py)
start_checks()
import requests
import discord
from discord.ext import commands
from discord import app_commands
from discord import Colour, Message
from better_profanity import profanity
from discord import Colour, Embed, File, Interaction, Message
from discord.abc import Messageable
from discord.ext import commands
from modules.volta.main import _, set_language
from modules.markovmemory import *
from modules.version import *
from modules.sentenceprocessing import *
from modules.unhandledexception import handle_exception
from modules.image import gen_demotivator
from modules.image import gen_meme, gen_demotivator
from modules.minigames import guessthenumber, hangman
sys.excepthook = handle_exception
class MessageMetadata(TypedDict):
user_id: str
user_name: str
guild_id: str | Literal["DM"]
guild_name: str | Literal["DM"]
channel_id: str
channel_name: str
message: str
timestamp: float
check_for_update() # Check for updates (from modules/version.py)
# Type aliases
T = TypeVar('T')
MessageContext = Union[commands.Context, discord.Interaction]
MessageReference = Union[Message, discord.WebhookMessage]
# Constants with type hints
positive_gifs: List[str] = settings["bot"]["misc"]["positive_gifs"]
positive_gifs: List[str] = os.getenv("POSITIVE_GIFS", "").split(',')
currenthash: str = ""
launched: bool = False
slash_commands_enabled: bool = False
# Set up Discord bot intents and create bot instance
intents: discord.Intents = discord.Intents.default()
intents.messages = True
intents.message_content = True
bot: commands.Bot = commands.Bot(
command_prefix=settings["bot"]["prefix"],
intents=intents,
allowed_mentions=discord.AllowedMentions(
everyone=False, roles=False, users=False, replied_user=True
),
)
# Load memory and Markov model for text generation
memory: List[str | Dict[Literal["_meta"], MessageMetadata]] = load_memory()
memory: List[str] = load_memory()
markov_model: Optional[markovify.Text] = load_markov_model()
if not markov_model:
logger.error(k.markov_model_not_found())
logger.error(_('markov_model_not_found'))
memory = load_memory()
markov_model = train_markov_model(memory)
generated_sentences: Set[str] = set()
used_words: Set[str] = set()
async def load_cogs_from_folder(bot, folder_name="assets/cogs"):
for filename in os.listdir(folder_name):
if filename.endswith(".py") and not filename.startswith("_"):
cog_name = filename[:-3]
module_path = folder_name.replace("/", ".").replace("\\", ".") + f".{cog_name}"
try:
await bot.load_extension(module_path)
logger.info(f"{(_('loaded_cog'))} {cog_name}")
except Exception as e:
logger.error(f"{(_('cog_fail'))} {cog_name} {e}")
traceback.print_exc()
async def load_cogs_from_folder(bot: commands.Bot, folder_name="assets/cogs"):
for filename in [file for file in os.listdir(folder_name) if file.endswith(".py")]:
cog_name: str = filename[:-3]
if (
"internal" not in folder_name
and cog_name not in settings["bot"]["enabled_cogs"]
):
logger.debug(f"Skipping cog {cog_name} (not in enabled cogs)")
continue
module_path = folder_name.replace("/", ".").replace("\\", ".") + f".{cog_name}"
async def send_alive_ping_periodically() -> None:
while True:
try:
await bot.load_extension(module_path)
logger.info(f"{k.loaded_cog()} {cog_name}")
requests.post(f"{VERSION_URL}/aliveping", json={"name": NAME})
except Exception as e:
logger.error(f"{k.cog_fail()} {cog_name} {e}")
traceback.print_exc()
logger.error(f"{(_('error_sending_alive_ping'))}{RESET} {e}")
await asyncio.sleep(60)
# Event: Called when the bot is ready
@bot.event
async def on_ready() -> None:
global launched
global slash_commands_enabled
global NAME
global status
folder_name: str = "cogs"
if launched:
return
await load_cogs_from_folder(bot, "assets/cogs/internal")
await load_cogs_from_folder(bot)
try:
synced: List[discord.app_commands.AppCommand] = await bot.tree.sync()
logger.info(f"{k.synced_commands()} {len(synced)} {k.synced_commands2()}")
logger.info(k.started(settings["name"]))
logger.info(f"{_('synced_commands')} {len(synced)} {(_('synced_commands2'))}")
slash_commands_enabled = True
logger.info(f"{(_('started')).format(name=NAME)}")
bot.loop.create_task(send_alive_ping_periodically())
except discord.errors.Forbidden as perm_error:
logger.error(f"Permission error while syncing commands: {perm_error}")
logger.error(
"Make sure the bot has the 'applications.commands' scope and is invited with the correct permissions."
)
logger.error(f"Permission error while syncing commands: {perm_error}")
logger.error("Make sure the bot has the 'applications.commands' scope and is invited with the correct permissions.")
quit()
except Exception as e:
logger.error(f"{k.fail_commands_sync()} {e}")
logger.error(f"{_('fail_commands_sync')} {e}")
traceback.print_exc()
quit()
if not settings["bot"]["misc"]["activity"]["content"]:
if not song:
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(
activity=discord.Activity(
type=activities.get(
settings["bot"]["misc"]["activity"]["type"],
discord.ActivityType.unknown,
),
name=settings["bot"]["misc"]["activity"]["content"],
)
)
status = {
"idle": discord.Status.idle,
"dnd": discord.Status.dnd,
"invisible": discord.Status.invisible,
"online": discord.Status.online
}.get(status.lower(), discord.Status.online)
await bot.change_presence(status=status, activity=discord.Activity(type=discord.ActivityType.listening, name=f"{song}"))
launched = True
@bot.event
async def on_command_error(ctx: commands.Context, error: commands.CommandError) -> None:
from modules.unhandledexception import handle_exception
if isinstance(error, commands.CommandInvokeError):
original: Exception = error.original
handle_exception(
type(original),
original,
original.__traceback__,
context=f"Command: {ctx.command} | User: {ctx.author}",
type(original), original, original.__traceback__,
context=f"Command: {ctx.command} | User: {ctx.author}"
)
else:
handle_exception(
type(error),
error,
error.__traceback__,
context=f"Command: {ctx.command} | User: {ctx.author}",
type(error), error, error.__traceback__,
context=f"Command: {ctx.command} | User: {ctx.author}"
)
# Command: Retrain the Markov model from memory
@bot.hybrid_command(description=f"{(_('command_desc_retrain'))}")
async def retrain(ctx: commands.Context) -> None:
if ctx.author.id != ownerid:
return
# New demotivator command
@bot.hybrid_command(description="Generate a demotivator poster with two lines of text")
async def demotivator(ctx: commands.Context) -> None:
message_ref: MessageReference = await send_message(ctx, f"{(_('command_markov_retrain'))}")
try:
with open(MEMORY_FILE, 'r') as f:
memory: List[str] = json.load(f)
except FileNotFoundError:
await send_message(ctx, f"{(_('command_markov_memory_not_found'))}")
return
except json.JSONDecodeError:
await send_message(ctx, f"{(_('command_markov_memory_is_corrupt'))}")
return
data_size: int = len(memory)
processed_data: int = 0
processing_message_ref: MessageReference = await send_message(ctx, f"{(_('command_markov_retraining')).format(processed_data=processed_data, data_size=data_size)}")
start_time: float = time.time()
for i, data in enumerate(memory):
processed_data += 1
global markov_model
markov_model = train_markov_model(memory)
save_markov_model(markov_model)
await send_message(ctx, f"{_('command_markov_retrain_successful').format(data_size=data_size)}", edit=True, message_reference=processing_message_ref)
# Command: Generate a sentence using the Markov model
@bot.hybrid_command(description=f"{(_('command_desc_talk'))}")
async def talk(ctx: commands.Context, sentence_size: int = 5) -> None:
if not markov_model:
await send_message(ctx, f"{(_('command_talk_insufficent_text'))}")
return
response: Optional[str] = None
for _ in range(20):
if sentence_size == 1:
response = markov_model.make_short_sentence(max_chars=100, tries=100)
if response:
response = response.split()[0]
else:
response = markov_model.make_sentence(tries=100, max_words=sentence_size)
if response and response not in generated_sentences:
if sentence_size > 1:
response = improve_sentence_coherence(response)
generated_sentences.add(response)
break
if response:
cleaned_response: str = re.sub(r'[^\w\s]', '', response).lower()
coherent_response: str = rephrase_for_coherence(cleaned_response)
if random.random() < 0.9 and is_positive(coherent_response):
gif_url: str = random.choice(positive_gifs)
combined_message: str = f"{coherent_response}\n[jif]({gif_url})"
else:
combined_message: str = coherent_response
logger.info(combined_message)
os.environ['gooberlatestgen'] = combined_message
await send_message(ctx, combined_message)
else:
await send_message(ctx, f"{(_('command_talk_generation_fail'))}")
@bot.hybrid_command(description=f"RAM")
async def ramusage(ctx):
process = psutil.Process(os.getpid())
mem = process.memory_info().rss
await send_message(ctx, f"Total memory used: {mem / 1024 / 1024:.2f} MB")
# Command: Generate an image
@bot.hybrid_command(description=f"{(_('command_desc_help'))}")
async def impact(ctx: commands.Context, text: Optional[str] = None) -> None:
assets_folder: str = "assets/images"
temp_input: str | None = None
temp_input: Optional[str] = None
def get_random_asset_image() -> Optional[str]:
files: List[str] = [
f
for f in os.listdir(assets_folder)
if f.lower().endswith((".png", ".jpg", ".jpeg", ".webp"))
]
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))
@ -256,13 +240,12 @@ async def demotivator(ctx: commands.Context) -> None:
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)
await attachment.save(temp_input)
input_path: str = temp_input
else:
fallback_image: Optional[str] = get_random_asset_image()
if fallback_image is None:
await ctx.reply(k.no_image_available())
await ctx.reply(_('no_image_available'))
return
temp_input = tempfile.mktemp(suffix=os.path.splitext(fallback_image)[1])
shutil.copy(fallback_image, temp_input)
@ -270,13 +253,63 @@ async def demotivator(ctx: commands.Context) -> None:
else:
fallback_image = get_random_asset_image()
if fallback_image is None:
await ctx.reply(k.no_image_available())
await ctx.reply(_('no_image_available'))
return
temp_input = tempfile.mktemp(suffix=os.path.splitext(fallback_image)[1])
shutil.copy(fallback_image, temp_input)
input_path = temp_input
output_path: Optional[str] = await gen_demotivator(input_path) # type: ignore
output_path: Optional[str] = await gen_meme(input_path, custom_text=text)
if output_path is None or not os.path.isfile(output_path):
if temp_input and os.path.exists(temp_input):
os.remove(temp_input)
await ctx.reply(_('failed_generate_image'))
return
await ctx.send(file=discord.File(output_path))
if temp_input and os.path.exists(temp_input):
os.remove(temp_input)
# New demotivator command
@bot.hybrid_command(description="Generate a demotivator poster with two lines of text")
async def demotivator(ctx: commands.Context) -> None:
assets_folder: str = "assets/images"
temp_input: Optional[str] = None
def get_random_asset_image() -> Optional[str]:
files: List[str] = [f for f in os.listdir(assets_folder) if f.lower().endswith(('.png', '.jpg', '.jpeg', '.webp'))]
if not files:
return None
return os.path.join(assets_folder, random.choice(files))
if ctx.message.attachments:
attachment: discord.Attachment = ctx.message.attachments[0]
if attachment.content_type and attachment.content_type.startswith("image/"):
ext: str = os.path.splitext(attachment.filename)[1]
temp_input = f"tempy{ext}"
await attachment.save(temp_input)
input_path: str = temp_input
else:
fallback_image: Optional[str] = get_random_asset_image()
if fallback_image is None:
await ctx.reply(_('no_image_available'))
return
temp_input = tempfile.mktemp(suffix=os.path.splitext(fallback_image)[1])
shutil.copy(fallback_image, temp_input)
input_path = temp_input
else:
fallback_image = get_random_asset_image()
if fallback_image is None:
await ctx.reply(_('no_image_available'))
return
temp_input = tempfile.mktemp(suffix=os.path.splitext(fallback_image)[1])
shutil.copy(fallback_image, temp_input)
input_path = temp_input
output_path: Optional[str] = await gen_demotivator(input_path)
if output_path is None or not os.path.isfile(output_path):
if temp_input and os.path.exists(temp_input):
@ -289,50 +322,70 @@ async def demotivator(ctx: commands.Context) -> None:
if temp_input and os.path.exists(temp_input):
os.remove(temp_input)
bot.remove_command('help')
# Command: Show help information
@bot.hybrid_command(description=f"{(_('command_desc_help'))}")
async def help(ctx: commands.Context) -> None:
embed: discord.Embed = discord.Embed(
title=f"{(_('command_help_embed_title'))}",
description=f"{(_('command_help_embed_desc'))}",
color=Colour(0x000000)
)
command_categories: Dict[str, List[str]] = {
f"{(_('command_help_categories_general'))}": ["mem", "talk", "about", "ping", "impact", "demotivator", "help"],
f"{(_('command_help_categories_admin'))}": ["stats", "retrain", "setlanguage"]
}
custom_commands: List[str] = []
for cog_name, cog in bot.cogs.items():
for command in cog.get_commands():
if command.name not in command_categories[f"{(_('command_help_categories_general'))}"] and command.name not in command_categories[f"{(_('command_help_categories_admin'))}"]:
custom_commands.append(command.name)
if custom_commands:
embed.add_field(name=f"{(_('command_help_categories_custom'))}", value="\n".join([f"{PREFIX}{command}" for command in custom_commands]), inline=False)
for category, commands_list in command_categories.items():
commands_in_category: str = "\n".join([f"{PREFIX}{command}" for command in commands_list])
embed.add_field(name=category, value=commands_in_category, inline=False)
await send_message(ctx, embed=embed)
@bot.hybrid_command(description=f"{(_('command_desc_setlang'))}")
@app_commands.describe(locale="Choose your language")
async def setlanguage(ctx: commands.Context, locale: str) -> None:
if ctx.author.id != ownerid:
await ctx.send(":thumbsdown:")
return
await ctx.defer()
set_language(locale)
await ctx.send(":thumbsup:")
# Event: Called on every message
@bot.event
async def on_message(message: discord.Message) -> None:
global memory, markov_model
EMOJIS = [
"\U0001f604",
"\U0001f44d",
"\U0001f525",
"\U0001f4af",
"\U0001f389",
"\U0001f60e",
] # originally was emojis but it would probably shit itself on systems without unicode so....
EMOJIS = ["\U0001F604", "\U0001F44D", "\U0001F525", "\U0001F4AF", "\U0001F389", "\U0001F60E"] # originally was emojis but it would probably shit itself on systems without unicode so....
if message.author.bot:
return
if message.author.id in settings["bot"]["blacklisted_users"]:
if str(message.author.id) in BLACKLISTED_USERS:
return
commands = [
settings["bot"]["prefix"] + command.name for command in bot.tree.get_commands()
]
if message.content.startswith(tuple(commands)):
logger.info(f"{k.command_ran(message.author.name, message.content)}")
if message.content.startswith((f"{PREFIX}talk", f"{PREFIX}mem", f"{PREFIX}help", f"{PREFIX}stats", f"{PREFIX}")):
logger.info(f"{(_('command_ran')).format(message=message)}")
await bot.process_commands(message)
return
if (
profanity.contains_profanity(message.content)
and settings["bot"]["misc"]["block_profanity"]
):
return
if message.content:
if not settings["bot"]["user_training"]:
if not USERTRAIN_ENABLED:
return
formatted_message: str = append_mentions_to_18digit_integer(message.content)
cleaned_message: str = preprocess_message(formatted_message)
formatted_message: str = message.content
cleaned_message: str = formatted_message
if cleaned_message:
memory.append(cleaned_message)
message_metadata: MessageMetadata = {
message_metadata = {
"user_id": str(message.author.id),
"user_name": str(message.author),
"guild_id": str(message.guild.id) if message.guild else "DM",
@ -340,7 +393,7 @@ async def on_message(message: discord.Message) -> None:
"channel_id": str(message.channel.id),
"channel_name": str(message.channel),
"message": message.content,
"timestamp": time.time(),
"timestamp": time.time()
}
try:
if isinstance(memory, list):
@ -352,17 +405,10 @@ async def on_message(message: discord.Message) -> None:
save_memory(memory)
sentiment_score = is_positive(
message.content
) # doesnt work but im scared to change the logic now please ignore
sentiment_score = is_positive(message.content) # doesnt work but im scared to change the logic now please ignore
if sentiment_score > 0.8:
if not settings["bot"]["react_to_messages"]:
if REACT != "True":
return
if not sync_connector.can_react(message.id):
logger.info("Sync hub determined that this instance cannot react")
return
emoji = random.choice(EMOJIS)
try:
await message.add_reaction(emoji)
@ -371,32 +417,106 @@ async def on_message(message: discord.Message) -> None:
await bot.process_commands(message)
# Event: Called on every interaction (slash command, etc.)
@bot.event
async def on_interaction(interaction: discord.Interaction) -> None:
logger.info(f"{k.command_ran_s(interaction.user.name)} {interaction.user.name}")
name = None
if interaction.data.get('name') is None:
name = "Unknown"
else:
name = interaction.data['name']
logger.info(f"{(_('command_ran_s')).format(interaction=interaction)}{name}")
# Global check: Block blacklisted users from running commands
@bot.check
async def block_blacklisted(ctx: commands.Context) -> bool:
if ctx.author.id not in settings["bot"]["blacklisted_users"]:
return True
try:
if isinstance(ctx, discord.Interaction):
if not ctx.response.is_done():
await ctx.response.send_message(k.blacklisted(), ephemeral=True)
if str(ctx.author.id) in BLACKLISTED_USERS:
try:
if isinstance(ctx, discord.Interaction):
if not ctx.response.is_done():
await ctx.response.send_message(_('blacklisted'), ephemeral=True)
else:
await ctx.followup.send(_('blacklisted'), ephemeral=True)
else:
await ctx.followup.send(k.blacklisted(), ephemeral=True)
else:
await ctx.send(k.blacklisted_user(), ephemeral=True)
except:
await ctx.send(_('blacklisted_user'), ephemeral=True)
except:
pass
return False
return True
# Command: Show bot latency
@bot.hybrid_command(description=f"{(_('command_desc_ping'))}")
async def ping(ctx: commands.Context) -> None:
await ctx.defer()
latency: int = round(bot.latency * 1000)
LOLembed: discord.Embed = discord.Embed(
title="Pong!!",
description=(
f"{PING_LINE}\n"
f"`{(_('command_ping_embed_desc'))}: {latency}ms`\n"
),
color=Colour(0x000000)
)
LOLembed.set_footer(text=f"{(_('command_ping_footer'))} {ctx.author.name}", icon_url=ctx.author.avatar.url)
await ctx.send(embed=LOLembed)
def get_git_remote_url():
try:
url = subprocess.check_output(
["git", "config", "--get", "remote.origin.url"],
text=True,
stderr=subprocess.DEVNULL,
).strip()
return url
except subprocess.CalledProcessError:
return "Unknown"
# Command: Show about information
@bot.hybrid_command(description=f"{(_('command_about_desc'))}")
async def about(ctx: commands.Context) -> None:
print("-----------------------------------\n\n")
latest_version: str = check_for_update()
print("-----------------------------------")
embed: discord.Embed = discord.Embed(title=f"{(_('command_about_embed_title'))}", description="", color=Colour(0x000000))
embed.add_field(name=f"{(_('command_about_embed_field1'))}", value=f"{NAME}", inline=False)
embed.add_field(name=f"{(_('command_about_embed_field2name'))}", value=f"{(_('command_about_embed_field2value')).format(local_version=local_version, latest_version=latest_version)}", inline=False)
embed.add_field(name=f"Git", value=get_git_remote_url())
embed.add_field(name=f"OS", value=platform.platform())
await send_message(ctx, embed=embed)
# Command: Show bot statistics (admin only)
@bot.hybrid_command(description="stats")
async def stats(ctx: commands.Context) -> None:
if ctx.author.id != ownerid:
return
print("-----------------------------------\n\n")
latest_version: str = check_for_update()
print("-----------------------------------")
memory_file: str = 'memory.json'
file_size: int = os.path.getsize(memory_file)
with open(memory_file, 'r') as file:
line_count: int = sum(1 for _ in file)
embed: discord.Embed = discord.Embed(title=f"{(_('command_stats_embed_title'))}", description=f"{(_('command_stats_embed_desc'))}", color=Colour(0x000000))
embed.add_field(name=f"{(_('command_stats_embed_field1name'))}", value=f"{(_('command_stats_embed_field1value')).format(file_size=file_size, line_count=line_count)}", inline=False)
embed.add_field(name=f"{(_('command_stats_embed_field2name'))}", value=f"{(_('command_stats_embed_field2value')).format(local_version=local_version, latest_version=latest_version)}", inline=False)
embed.add_field(name=f"{(_('command_stats_embed_field3name'))}", value=f"{(_('command_stats_embed_field3value')).format(NAME=NAME, PREFIX=PREFIX, ownerid=ownerid, PING_LINE=PING_LINE, showmemenabled=showmemenabled, USERTRAIN_ENABLED=USERTRAIN_ENABLED, song=song, splashtext=splashtext)}", inline=False)
embed.add_field(name=f"OS", value=platform.platform())
embed.add_field(name="Python Version", value=platform.python_version())
await send_message(ctx, embed=embed)
# Command: Upload memory.json to litterbox.catbox.moe and return the link
@bot.hybrid_command()
async def mem(ctx: commands.Context) -> None:
if showmemenabled != "true":
return
command: str = """curl -F "reqtype=fileupload" -F "time=1h" -F "fileToUpload=@memory.json" https://litterbox.catbox.moe/resources/internals/api.php"""
memorylitter: subprocess.CompletedProcess = subprocess.run(command, shell=True, capture_output=True, text=True)
logger.debug(memorylitter)
await send_message(ctx, memorylitter.stdout.strip())
# Helper: Improve sentence coherence (simple capitalization fix)
def improve_sentence_coherence(sentence: str) -> str:
@ -404,40 +524,5 @@ def improve_sentence_coherence(sentence: str) -> str:
sentence = sentence.replace(" i ", " I ")
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
if __name__ == "__main__":
bot.run(os.environ.get("DISCORD_BOT_TOKEN", ""))
bot.run(TOKEN)

245
botminimal.py Normal file
View file

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

29
example.env Normal file
View file

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

View file

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

View file

@ -6,58 +6,38 @@ 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,
)
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),
]
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
):
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):
async def gen_meme(input_image_path, sentence_size=5, max_attempts=10, custom_text=None):
markov_model = load_markov_model()
if not markov_model or not os.path.isfile(input_image_path):
return None
@ -72,29 +52,28 @@ async def gen_meme(input_image_path, sentence_size=5, max_attempts=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 custom_text:
response = custom_text
else:
for _ in range(20):
if sentence_size == 1:
candidate = markov_model.make_short_sentence(max_chars=100, tries=100)
if candidate:
candidate = candidate.split()[0]
else:
candidate = markov_model.make_sentence(tries=100, max_words=sentence_size)
if candidate and candidate not in generated_sentences:
if sentence_size > 1:
candidate = improve_sentence_coherence(candidate)
generated_sentences.add(candidate)
response = candidate
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()
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)
@ -103,15 +82,11 @@ async def gen_meme(input_image_path, sentence_size=5, max_attempts=10):
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
)
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_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)
@ -120,21 +95,9 @@ async def gen_meme(input_image_path, sentence_size=5, max_attempts=10):
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,
)
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,
)
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
@ -153,7 +116,6 @@ async def gen_meme(input_image_path, sentence_size=5, max_attempts=10):
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):
@ -165,7 +127,7 @@ async def gen_demotivator(input_image_path, max_attempts=5):
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)
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)
@ -186,10 +148,8 @@ async def gen_demotivator(input_image_path, max_attempts=5):
title = t.upper()
subtitle = s.capitalize()
break
if not title:
title = "DEMOTIVATOR"
if not subtitle:
subtitle = "no text generated"
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)

View file

@ -1,220 +0,0 @@
# 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")

File diff suppressed because it is too large Load diff

View file

@ -1,28 +1,30 @@
import logging
import re
from modules.globalvars import *
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._format = f"[ %(levelname)-8s ]: %(message)s {DEBUG} [%(asctime)s.%(msecs)03d] (%(filename)s:%(funcName)s) {RESET}"
self.FORMATS = {
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.ERROR: RED + self._format + RESET,
logging.CRITICAL: PURPLE + self._format + RESET,
logging.CRITICAL: PURPLE + self._format + RESET
}
def format(self, record: logging.LogRecord):
ansiescape = re.compile(r'\x1B[@-_][0-?]*[ -/]*[@-~]')
if self.colors:
log_fmt = self.FORMATS.get(record.levelno) # Add colors
log_fmt = self.FORMATS.get(record.levelno) # Add colors
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")
return formatter.format(record)
formatted = formatter.format(record)
if not self.colors:
formatted = ansiescape.sub('', formatted)
return formatted

View file

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

71
modules/minigames.py Normal file
View file

@ -0,0 +1,71 @@
import random
import discord
from discord import ui, Interaction, TextStyle
from discord.ext import commands
import aiohttp
import asyncio
from modules.globalvars import bot
from modules.volta.main import _
# @bot.hybrid_command(description=_('minigames_guess_the_number'))
async def guessthenumber(ctx: commands.Context):
number = random.randint(1, 10)
class GuessModal(ui.Modal, title=_('minigames_guess_the_number')):
guess = ui.TextInput(label=_('minigames_your_guess'), style=TextStyle.short)
async def on_submit(self, interaction: Interaction):
try:
user_guess = int(self.guess.value)
except:
await interaction.response.send_message(_('minigames_invalid_number'), ephemeral=True)
return
if user_guess == number:
await interaction.response.send_message(_('minigames_correct'), ephemeral=True)
else:
await interaction.response.send_message(f"{_('minigames_wrong_number')} {number}.", ephemeral=True)
async def button_callback(interaction: Interaction):
await interaction.response.send_modal(GuessModal())
button = ui.Button(label=_('minigames_guess_button'), style=discord.ButtonStyle.primary)
button.callback = button_callback
view = ui.View()
view.add_item(button)
await ctx.send(_('minigames_click_to_guess'), view=view)
# @bot.hybrid_command(description=_('minigames_hangman')) nope nope nope fuck no nope no thanks no nuh uh not today nope
async def hangman(ctx: commands.Context):
async with aiohttp.ClientSession() as session:
async with session.get("https://random-word-api.herokuapp.com/word?number=1") as resp:
if resp.status != 200:
await ctx.send("Failed to get a random word.")
return
data = await resp.json()
word = data[0].lower()
print(word)
guessed_letters = set()
wrong_guesses = 0
max_wrong = 6
def display_word():
return " ".join([c if c in guessed_letters else "_" for c in word])
class GuessModal(ui.Modal, title=_('minigames_hangman_guess')):
letter = ui.TextInput(label=_('minigames_hangman_user_letter_guess'), style=TextStyle.short, max_length=1)
async def on_submit(self, interaction: Interaction):
nonlocal guessed_letters, wrong_guesses
guess = self.letter.value.lower()
if guess in guessed_letters:
await interaction.response.send_message(f"{_('minigames_hangman_already_guessed')}'{guess}'!", ephemeral=True)
return
guessed_letters.add(guess)
if guess not in word:
wrong_guesses += 1
if all(c in guessed_letters for c in word):
await interaction.response.edit_message(content=f"{_('minigames_hangman_won')} **{word}**", view=None)
elif wrong_guesses >= max_wrong:
await interaction.response.edit_message(content=f"{_('minigames_hangman_lost')} **{word}**", view=None)
else:
await interaction.response.edit_message(content=_('minigames_hangman_game').format(display_word=display_word(),wrong_guesses=wrong_guesses,max_wrong=max_wrong), view=view)
async def button_callback(interaction: Interaction):
await interaction.response.send_modal(GuessModal())
button = ui.Button(label=_('minigames_click_to_guess'), style=discord.ButtonStyle.primary)
button.callback = button_callback
view = ui.View()
view.add_item(button)
await ctx.send(_('minigames_hangman_game').format(display_word=display_word,wrong_guesses=wrong_guesses,max_wrong=max_wrong), view=view)

View file

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

View file

@ -1,4 +1,5 @@
from modules.globalvars import *
from modules.volta.main import _, check_missing_translations
import time
import os
import sys
@ -10,12 +11,6 @@ import re
from spacy.util import is_package
import importlib.metadata
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")
@ -26,8 +21,7 @@ try:
import psutil
except ImportError:
psutilavaliable = False
logger.error(k.missing_requests_psutil())
logger.error(_('missing_requests_psutil'))
def check_for_model():
if is_package("en_core_web_sm"):
@ -35,88 +29,80 @@ def check_for_model():
else:
logger.info("Model is not installed.")
def iscloned():
if os.path.exists(".git"):
return True
else:
logger.error(f"{k.not_cloned()}")
logger.error(f"{_('not_cloned')}")
sys.exit(1)
def get_stdlib_modules():
stdlib_path = pathlib.Path(sysconfig.get_paths()["stdlib"])
stdlib_path = pathlib.Path(sysconfig.get_paths()['stdlib'])
modules = set()
if hasattr(sys, "builtin_module_names"):
if hasattr(sys, 'builtin_module_names'):
modules.update(sys.builtin_module_names)
for file in stdlib_path.glob("*.py"):
if file.stem != "__init__":
for file in stdlib_path.glob('*.py'):
if file.stem != '__init__':
modules.add(file.stem)
for folder in stdlib_path.iterdir():
if folder.is_dir() and (folder / "__init__.py").exists():
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"):
for file in stdlib_path.glob('*.*'):
if file.suffix in ('.so', '.pyd'):
modules.add(file.stem)
return modules
def check_requirements():
STD_LIB_MODULES = get_stdlib_modules()
PACKAGE_ALIASES = {
"discord": "discord.py",
"better_profanity": "better-profanity",
"dotenv": "python-dotenv",
"pil": "pillow",
"websocket": "websocket-client"
"pil": "pillow"
}
parent_dir = os.path.dirname(os.path.abspath(__file__))
requirements_path = os.path.abspath(
os.path.join(parent_dir, "..", "requirements.txt")
)
requirements_path = os.path.abspath(os.path.join(parent_dir, '..', 'requirements.txt'))
if not os.path.exists(requirements_path):
logger.error(f"{k.requirements_not_found(path=requirements_path)}")
logger.error(f"{(_('requirements_not_found')).format(path=requirements_path)}")
return
with open(requirements_path, "r") as f:
with open(requirements_path, 'r') as f:
lines = f.readlines()
requirements = set()
for line in lines:
line = line.strip()
if line and not line.startswith("#"):
base_pkg = line.split("==")[0].lower()
if line and not line.startswith('#'):
base_pkg = line.split('==')[0].lower()
aliased_pkg = PACKAGE_ALIASES.get(base_pkg, base_pkg)
requirements.add(aliased_pkg)
installed_packages = {
dist.metadata["Name"].lower() for dist in importlib.metadata.distributions()
}
installed_packages = {dist.metadata['Name'].lower() for dist in importlib.metadata.distributions()}
missing = []
for req in sorted(requirements):
if req in STD_LIB_MODULES or req == "modules":
print(k.std_lib_local_skipped(package=req))
if req in STD_LIB_MODULES or req == 'modules':
print((_('std_lib_local_skipped')).format(package=req))
continue
check_name = req.lower()
if check_name in installed_packages:
logger.info(f"{k.ok_installed()} {check_name}")
logger.info(f"{_('ok_installed').format(package=check_name)} {check_name}")
else:
logger.error(f"{k.missing_package()} {check_name} {k.missing_package2()}")
logger.error(f"{(_('missing_package')).format(package=check_name)} {check_name} {(_('missing_package2'))}")
missing.append(check_name)
if missing:
logger.error(k.missing_packages_detected())
logger.error(_('missing_packages_detected'))
for pkg in missing:
print(f" - {pkg}")
sys.exit(1)
else:
logger.info(k.all_requirements_satisfied())
logger.info(_('all_requirements_satisfied'))
def check_latency():
host = "1.1.1.1"
@ -136,103 +122,82 @@ def check_latency():
try:
result = subprocess.run(
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
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))
logger.info((_('ping_to')).format(host=host, latency=latency_ms))
if latency_ms > 300:
logger.warning(f"{k.high_latency()}")
logger.warning(f"{(_('high_latency'))}")
else:
logger.warning(k.could_not_parse_latency())
logger.warning((_('could_not_parse_latency')))
else:
print(result.stderr)
logger.error(f"{k.ping_failed(host=host)}{RESET}")
logger.error(f"{(_('ping_failed')).format(host=host)}{RESET}")
except Exception as e:
logger.error(k.error_running_ping(error=e))
logger.error((_('error_running_ping')).format(error=e))
def check_memory():
if psutilavaliable == False:
return
try:
memory_info = psutil.virtual_memory() # type: ignore
total_memory = memory_info.total / (1024**3)
used_memory = memory_info.used / (1024**3)
free_memory = memory_info.available / (1024**3)
total_memory = memory_info.total / (1024 ** 3)
used_memory = memory_info.used / (1024 ** 3)
free_memory = memory_info.available / (1024 ** 3)
logger.info(
k.memory_usage(
used=used_memory,
total=total_memory,
percent=(used_memory / total_memory) * 100,
)
)
logger.info((_('memory_usage')).format(used=used_memory, total=total_memory, percent=(used_memory / total_memory) * 100))
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))
print(f"{YELLOW}{(_('memory_above_90')).format(percent=(used_memory / total_memory) * 100)}{RESET}")
logger.info((_('total_memory')).format(total=total_memory))
logger.info((_('used_memory')).format(used=used_memory))
if free_memory < 1:
logger.warning(f"{k.low_free_memory(free=free_memory)}")
logger.warning(f"{(_('low_free_memory')).format(free=free_memory)}")
sys.exit(1)
except ImportError:
logger.error(
k.psutil_not_installed()
) # todo: translate this into italian and put it in the translations "psutil is not installed. Memory check skipped."
logger.error(_('psutil_not_installed')) # todo: translate this into italian and put it in the translations "psutil is not installed. Memory check skipped."
def check_cpu():
if psutilavaliable == False:
return
logger.info(k.measuring_cpu())
logger.info((_('measuring_cpu')))
cpu_per_core = psutil.cpu_percent(interval=1, percpu=True) # type: ignore
total_cpu = sum(cpu_per_core) / len(cpu_per_core)
logger.info(k.total_cpu_usage(usage=total_cpu))
logger.info((_('total_cpu_usage')).format(usage=total_cpu))
if total_cpu > 85:
logger.warning(f"{k.high_avg_cpu(usage=total_cpu)}")
logger.warning(f"{(_('high_avg_cpu')).format(usage=total_cpu)}")
if total_cpu > 95:
logger.error(k.really_high_cpu())
logger.error(_('really_high_cpu'))
sys.exit(1)
def check_memoryjson():
try:
logger.info(
k.memory_file(
size=os.path.getsize(settings["bot"]["active_memory"]) / (1024**2)
)
)
if os.path.getsize(settings["bot"]["active_memory"]) > 1_073_741_824:
logger.warning(f"{k.memory_file_large()}")
logger.info((_('memory_file')).format(size=os.path.getsize(MEMORY_FILE) / (1024 ** 2)))
if os.path.getsize(MEMORY_FILE) > 1_073_741_824:
logger.warning(f"{(_('memory_file_large'))}")
try:
with open(settings["bot"]["active_memory"], "r", encoding="utf-8") as f:
with open(MEMORY_FILE, 'r', encoding='utf-8') as f:
json.load(f)
except json.JSONDecodeError as e:
logger.error(f"{k.memory_file_corrupted(error=e)}")
logger.warning(f"{k.consider_backup_memory()}")
logger.error(f"{(_('memory_file_corrupted')).format(error=e)}")
logger.warning(f"{(_('consider_backup_memory'))}")
except UnicodeDecodeError as e:
logger.error(f"{k.memory_file_encoding(error=e)}")
logger.warning(f"{k.consider_backup_memory()}")
logger.error(f"{(_('memory_file_encoding')).format(error=e)}")
logger.warning(f"{(_('consider_backup_memory'))}")
except Exception as e:
logger.error(f"{k.error_reading_memory(error=e)}")
logger.error(f"{(_('error_reading_memory')).format(error=e)}")
except FileNotFoundError:
logger.info(f"{k.memory_file_not_found()}")
logger(f"{(_('memory_file_not_found'))}")
def presskey2skip(timeout):
if os.name == "nt":
if os.name == 'nt':
import msvcrt
start_time = time.time()
while True:
if msvcrt.kbhit():
@ -261,44 +226,30 @@ def presskey2skip(timeout):
time.sleep(0.1)
finally:
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
def start_checks():
if settings["disable_checks"]:
logger.warning(f"{k.checks_disabled()}")
if CHECKS_DISABLED == "True":
logger.warning(f"{(_('checks_disabled'))}")
return
logger.info(k.running_prestart_checks())
logger.info(_('running_prestart_checks'))
check_for_model()
iscloned()
check_missing_translations()
check_requirements()
check_latency()
check_memory()
check_memoryjson()
check_cpu()
check_synchub()
if os.path.exists(".env"):
pass
else:
logger.warning(f"{k.env_file_not_found()}")
logger.warning(f"{(_('env_file_not_found'))}")
sys.exit(1)
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:
pass
logger.info(k.continuing_in_seconds(seconds=5))
logger.info(_('continuing_in_seconds').format(seconds=5))
presskey2skip(timeout=5)
os.system("cls" if os.name == "nt" else "clear")
with open(settings["splash_text_loc"], "r") as f:
print("".join(f.readlines()))
os.system('cls' if os.name == 'nt' else 'clear')
print(splashtext)

View file

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

View file

@ -1,155 +0,0 @@
import json
import os
from typing import Dict, List, Literal, Mapping, Any, TypedDict
from modules.keys import Language
import logging
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):
ping_line: str
activity: Activity
positive_gifs: List[str]
block_profanity: bool
class BotSettings(TypedDict):
prefix: str
owner_ids: List[int]
blacklisted_users: List[int]
user_training: bool
allow_show_mem_command: bool
react_to_messages: bool
misc: MiscBotOptions
enabled_cogs: List[str]
active_memory: str
sync_hub: SyncHub
class SettingsType(TypedDict):
bot: BotSettings
locale: Language
name: str
auto_update: bool
disable_checks: bool
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:
def __init__(self) -> None:
global instance
instance = self
self.path: str = os.path.join(".", "settings", "settings.json")
if not os.path.exists(self.path):
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:
self.__kv_store: dict = json.load(f)
self.settings = SettingsType(self.__kv_store) # type: ignore
self.original_settings = copy.deepcopy(self.settings)
self.log_path: str = os.path.join(".", "settings", "admin_logs.json")
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:
with open(self.path, "w") as f:
json.dump(self.settings, f, ensure_ascii=False, indent=4)
self.original_settings = self.settings
def discard(self) -> None:
self.settings = self.original_settings
def get_plugin_settings(
self, plugin_name: str, default: Mapping[Any, Any]
) -> Mapping[Any, Any]:
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()

View file

@ -1,94 +0,0 @@
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,29 +1,25 @@
import sys
import traceback
import os
from modules.settings import instance as settings_manager
import logging
from modules.globalvars import RED, RESET
import modules.keys as k
settings = settings_manager.settings
logger = logging.getLogger("goober")
from modules.globalvars import RED, RESET, splashtext
from modules.volta.main import _
def handle_exception(exc_type, exc_value, exc_traceback, *, context=None):
os.system("cls" if os.name == "nt" else "clear")
os.system('cls' if os.name == 'nt' else 'clear')
if issubclass(exc_type, KeyboardInterrupt):
sys.__excepthook__(exc_type, exc_value, exc_traceback)
return
with open(settings["splash_text_loc"], "r") as f:
print("".join(f.readlines()))
print(splashtext)
print(f"{RED}=====BEGINNING OF TRACEBACK====={RESET}")
traceback.print_exception(exc_type, exc_value, exc_traceback)
print(f"{RED}========END OF TRACEBACK========{RESET}")
print(f"{RED}{k.unhandled_exception()}{RESET}")
print(f"{RED}{_('unhandled_exception')}{RESET}")
if context:
print(f"{RED}Context: {context}{RESET}")

105
modules/version.py Normal file
View file

@ -0,0 +1,105 @@
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
# Automatically update the local repository if the remote is ahead
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():
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 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

220
modules/volta/main.py Normal file
View file

@ -0,0 +1,220 @@
# 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
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}")
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}")

View file

@ -1,51 +0,0 @@
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,10 +4,6 @@ spacy
spacytextblob
requests
psutil
better_profanity
python-dotenv
dotenv
pillow
watchdog
py-cpuinfo
websocket-client
pillow

View file

@ -1,44 +0,0 @@
{
"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,11 +0,0 @@
SS\
SS |
SSSSSS\ SSSSSS\ SSSSSS\ SSSSSSS\ SSSSSS\ SSSSSS\
SS __SS\ SS __SS\ SS __SS\ SS __SS\ SS __SS\ SS __SS\
SS / SS |SS / SS |SS / SS |SS | SS |SSSSSSSS |SS | \__|
SS | SS |SS | SS |SS | SS |SS | SS |SS ____|SS |
\SSSSSSS |\SSSSSS |\SSSSSS |SSSSSSS |\SSSSSSS\ SS |
\____SS | \______/ \______/ \_______/ \_______|\__|
SS\ SS |
\SSSSSS |
\______/

View file

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

View file

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