Compare commits
36 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
d6b51c787a | ||
![]() |
4e111b410d | ||
![]() |
b0ba03f97d | ||
![]() |
10ab18bc51 | ||
![]() |
b01bd5b80b | ||
![]() |
d300a3c812 | ||
![]() |
8021d17d27 | ||
![]() |
b860d0e271 | ||
![]() |
7d6c5aae51 | ||
![]() |
f17db0d22c | ||
![]() |
ed53089d4a | ||
![]() |
221f2370e4 | ||
![]() |
3c71e9f54e | ||
![]() |
bf421e4fd0 | ||
![]() |
fcb1a9782a | ||
![]() |
59ce219183 | ||
![]() |
2e4576ba4b | ||
![]() |
a92439b352 | ||
![]() |
39f2c26fec | ||
![]() |
954014d1a4 | ||
![]() |
52d9b058cf | ||
![]() |
8666c83565 | ||
![]() |
9a4d486e25 | ||
![]() |
f5d7121dfb | ||
![]() |
01b40f8b58 | ||
![]() |
cbe7fe201f | ||
![]() |
4a695a7bac | ||
![]() |
ae2d565004 | ||
![]() |
280df4f5e0 | ||
![]() |
dec83f1513 | ||
![]() |
54b8bf4c59 | ||
![]() |
53e0eb5289 | ||
![]() |
016e907d39 | ||
![]() |
cb45a9fffc | ||
![]() |
7c6be59dc9 | ||
![]() |
c565f962c5 |
4
.gitignore
vendored
|
@ -12,4 +12,6 @@ received_memory.json
|
||||||
translation_report.txt
|
translation_report.txt
|
||||||
translationcompleteness.py
|
translationcompleteness.py
|
||||||
modules/volta
|
modules/volta
|
||||||
log.txt
|
log.txt
|
||||||
|
settings/settings.json
|
||||||
|
settings/splash.txt
|
||||||
|
|
2
.gitmodules
vendored
|
@ -1,3 +1,3 @@
|
||||||
[submodule "modules/volta"]
|
[submodule "modules/volta"]
|
||||||
path = modules/volta
|
path = modules/volta
|
||||||
url = https://github.com/gooberinc/volta
|
url = https://forgejo.expect.ovh/gooberinc/volta
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
knockoff of genai basically :p
|
knockoff of genai basically :p
|
||||||
|
THIS!! IS THE ACTUAL REPO!!!! NOT THE OTHER ONE!!! THIS ONE!!!
|
||||||
|
|
||||||
Special thanks to [Charlie's Computers](https://github.com/PowerPCFan) for being the only one I know of that's hosting Goober 24/7
|
Special thanks to [Charlie's Computers](https://github.com/PowerPCFan) for being the only one I know of that's hosting Goober 24/7
|
||||||
|
|
||||||
[Goober Central](https://github.com/whatdidyouexpect/goober-central)
|
[Goober Central](https://github.com/whatdidyouexpect/goober-central)
|
||||||
|
|
||||||

|
[Another mirror](https://forgejo.expect.ovh/gooberinc/goober)
|
||||||
|
no promises that it'll be stable
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
# goobers custom commands
|
|
||||||
[Hello World!](https://github.com/WhatDidYouExpect/goober/blob/main/cogs/hello.py)
|
|
||||||
by expect
|
|
||||||
|
|
||||||
[WhoAmI (lists username and nickname)](https://github.com/WhatDidYouExpect/goober/blob/main/cogs/whoami.py)
|
|
||||||
by PowerPCFan
|
|
||||||
|
|
||||||
[Cog Manager](https://github.com/WhatDidYouExpect/goober/blob/main/cogs/cogmanager.py)
|
|
||||||
by expect
|
|
||||||
|
|
||||||
[TensorFlow integration](https://github.com/WhatDidYouExpect/goober/blob/main/cogs/tf.py)
|
|
||||||
by SuperSilly2 (requires Python 3.7 - 3.10, tensorflow-metal/tensorflow-gpu and tensorflow/tensorflow-macos)
|
|
||||||
|
|
||||||
[Web Scraper](https://raw.githubusercontent.com/WhatDidYouExpect/goober/refs/heads/main/cogs/webscraper.py)
|
|
||||||
by expect (requires goober version 0.11.7.2 or higher)
|
|
||||||
|
|
||||||
[Status Changer](https://raw.githubusercontent.com/WhatDidYouExpect/goober/refs/heads/main/cogs/songchanger.py)
|
|
||||||
by expect (requires goober version 0.11.8 or higher)
|
|
||||||
|
|
||||||
[Status Changer](https://raw.githubusercontent.com/WhatDidYouExpect/goober/refs/heads/main/cogs/songchanger.py)
|
|
||||||
by expect (requires goober version 0.11.8 or higher)
|
|
||||||
|
|
||||||
[webUI](https://raw.githubusercontent.com/WhatDidYouExpect/goober/refs/heads/main/cogs/webserver.py)
|
|
||||||
by expect (requires goober version 0.11.8 or higher)
|
|
||||||
|
|
||||||
[LastFM](https://raw.githubusercontent.com/WhatDidYouExpect/goober/refs/heads/main/cogs/webserver.py)
|
|
||||||
by expect (no idea what version it needs i've only tried it on 1.0.3)
|
|
||||||
- you have to add LASTFM_USERNAME and LASTFM_API_KEY to your .env
|
|
|
@ -1,20 +1,17 @@
|
||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
from modules.globalvars import ownerid
|
from modules.permission import requires_admin
|
||||||
class FileSync(commands.Cog):
|
class FileSync(commands.Cog):
|
||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.mode = None
|
self.mode = None
|
||||||
self.peer_id = None
|
self.peer_id = None
|
||||||
self.awaiting_file = False
|
self.awaiting_file = False
|
||||||
|
@requires_admin()
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def syncfile(self, ctx, mode: str, peer: discord.User):
|
async def syncfile(self, ctx, mode: str, peer: discord.User):
|
||||||
self.mode = mode.lower()
|
self.mode = mode.lower()
|
||||||
self.peer_id = peer.id
|
self.peer_id = peer.id
|
||||||
if ctx.author.id != ownerid:
|
|
||||||
await ctx.send("You don't have permission to execute this command.")
|
|
||||||
return
|
|
||||||
if self.mode == "s":
|
if self.mode == "s":
|
||||||
await ctx.send(f"<@{self.peer_id}> FILE_TRANSFER_REQUEST")
|
await ctx.send(f"<@{self.peer_id}> FILE_TRANSFER_REQUEST")
|
||||||
await ctx.send(file=discord.File("memory.json"))
|
await ctx.send(file=discord.File("memory.json"))
|
||||||
|
|
|
@ -1,98 +0,0 @@
|
||||||
import discord
|
|
||||||
from discord.ext import commands
|
|
||||||
from modules.image import *
|
|
||||||
from modules.volta.main import _
|
|
||||||
from PIL import Image, ImageEnhance, ImageFilter, ImageOps, ImageChops, ImageColor
|
|
||||||
import os, random, shutil, tempfile
|
|
||||||
|
|
||||||
async def deepfryimage(path):
|
|
||||||
with Image.open(path).convert("RGB") as im:
|
|
||||||
# make it burn
|
|
||||||
for _ in range(3):
|
|
||||||
im = im.resize((int(im.width * 0.7), int(im.height * 0.7)))
|
|
||||||
im = im.resize((int(im.width * 1.5), int(im.height * 1.5)))
|
|
||||||
im = ImageEnhance.Contrast(im).enhance(random.uniform(5, 10))
|
|
||||||
im = ImageEnhance.Sharpness(im).enhance(random.uniform(10, 50))
|
|
||||||
im = ImageEnhance.Brightness(im).enhance(random.uniform(1.5, 3))
|
|
||||||
r, g, b = im.split()
|
|
||||||
r = r.point(lambda i: min(255, i * random.uniform(1.2, 2.0)))
|
|
||||||
g = g.point(lambda i: min(255, i * random.uniform(0.5, 1.5)))
|
|
||||||
b = b.point(lambda i: min(255, i * random.uniform(0.5, 2.0)))
|
|
||||||
channels = [r, g, b]
|
|
||||||
random.shuffle(channels)
|
|
||||||
im = Image.merge("RGB", tuple(channels))
|
|
||||||
overlay_color = tuple(random.randint(0, 255) for _ in range(3))
|
|
||||||
overlay = Image.new("RGB", im.size, overlay_color)
|
|
||||||
im = ImageChops.add(im, overlay, scale=2.0, offset=random.randint(-64, 64))
|
|
||||||
|
|
||||||
im = im.filter(ImageFilter.EDGE_ENHANCE_MORE)
|
|
||||||
im = im.filter(ImageFilter.GaussianBlur(radius=random.uniform(0.5, 2)))
|
|
||||||
for _ in range(3):
|
|
||||||
tmp_path = tempfile.mktemp(suffix=".jpg")
|
|
||||||
im.save(tmp_path, format="JPEG", quality=random.randint(5, 15))
|
|
||||||
im = Image.open(tmp_path)
|
|
||||||
if random.random() < 0.3:
|
|
||||||
im = ImageOps.posterize(im, bits=random.choice([2, 3, 4]))
|
|
||||||
if random.random() < 0.2:
|
|
||||||
im = ImageOps.invert(im)
|
|
||||||
out_path = tempfile.mktemp(suffix=".jpg")
|
|
||||||
im.save(out_path, format="JPEG", quality=5)
|
|
||||||
return out_path
|
|
||||||
|
|
||||||
|
|
||||||
class whami(commands.Cog):
|
|
||||||
def __init__(self, bot):
|
|
||||||
self.bot = bot
|
|
||||||
|
|
||||||
|
|
||||||
@commands.command()
|
|
||||||
async def fuckup(self, ctx):
|
|
||||||
assets_folder = "assets/images"
|
|
||||||
temp_input = None
|
|
||||||
|
|
||||||
def get_random_asset_image():
|
|
||||||
files = [f for f in os.listdir(assets_folder) if f.lower().endswith(('.png', '.jpg', '.jpeg', '.webp'))]
|
|
||||||
if not files:
|
|
||||||
return None
|
|
||||||
return os.path.join(assets_folder, random.choice(files))
|
|
||||||
|
|
||||||
if ctx.message.attachments:
|
|
||||||
attachment = ctx.message.attachments[0]
|
|
||||||
if attachment.content_type and attachment.content_type.startswith("image/"):
|
|
||||||
ext = os.path.splitext(attachment.filename)[1]
|
|
||||||
temp_input = f"tempy{ext}"
|
|
||||||
await attachment.save(temp_input)
|
|
||||||
input_path = temp_input
|
|
||||||
else:
|
|
||||||
fallback_image = get_random_asset_image()
|
|
||||||
if fallback_image is None:
|
|
||||||
await ctx.reply(_('no_image_available'))
|
|
||||||
return
|
|
||||||
temp_input = tempfile.mktemp(suffix=os.path.splitext(fallback_image)[1])
|
|
||||||
shutil.copy(fallback_image, temp_input)
|
|
||||||
input_path = temp_input
|
|
||||||
else:
|
|
||||||
fallback_image = get_random_asset_image()
|
|
||||||
if fallback_image is None:
|
|
||||||
await ctx.reply(_('no_image_available'))
|
|
||||||
return
|
|
||||||
temp_input = tempfile.mktemp(suffix=os.path.splitext(fallback_image)[1])
|
|
||||||
shutil.copy(fallback_image, temp_input)
|
|
||||||
input_path = temp_input
|
|
||||||
|
|
||||||
output_path = await gen_meme(input_path)
|
|
||||||
|
|
||||||
if output_path is None or not os.path.isfile(output_path):
|
|
||||||
if temp_input and os.path.exists(temp_input):
|
|
||||||
os.remove(temp_input)
|
|
||||||
await ctx.reply(_('failed_generate_image'))
|
|
||||||
return
|
|
||||||
|
|
||||||
deepfried_path = await deepfryimage(output_path)
|
|
||||||
await ctx.send(file=discord.File(deepfried_path))
|
|
||||||
|
|
||||||
if temp_input and os.path.exists(temp_input):
|
|
||||||
os.remove(temp_input)
|
|
||||||
|
|
||||||
async def setup(bot):
|
|
||||||
await bot.add_cog(whami(bot))
|
|
196
assets/cogs/internal/base_commands.py
Normal file
|
@ -0,0 +1,196 @@
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
import subprocess
|
||||||
|
from typing import Dict, List
|
||||||
|
import discord
|
||||||
|
from discord import Colour
|
||||||
|
from discord.ext import commands
|
||||||
|
import discord.ext
|
||||||
|
import discord.ext.commands
|
||||||
|
from modules.globalvars import local_version
|
||||||
|
from modules.volta.main import _ , set_language
|
||||||
|
from modules.permission import requires_admin
|
||||||
|
from modules.sentenceprocessing import send_message
|
||||||
|
from modules.settings import instance as settings_manager
|
||||||
|
from modules.version import check_for_update
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
settings = settings_manager.settings
|
||||||
|
|
||||||
|
def get_git_origin_raw():
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["git", "config", "--get", "remote.origin.url"],
|
||||||
|
capture_output=True, text=True, check=True
|
||||||
|
)
|
||||||
|
return result.stdout.strip()
|
||||||
|
except Exception:
|
||||||
|
return "Failed to get git origin"
|
||||||
|
|
||||||
|
class BaseCommands(commands.Cog):
|
||||||
|
def __init__(self, bot):
|
||||||
|
self.bot: discord.ext.commands.Bot = bot
|
||||||
|
|
||||||
|
def get_git_origin_raw():
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["git", "config", "--get", "remote.origin.url"],
|
||||||
|
capture_output=True, text=True, check=True
|
||||||
|
)
|
||||||
|
return result.stdout.strip()
|
||||||
|
except Exception:
|
||||||
|
return "https://forgejo.expect.ovh/gooberinc/goober" # fallback if git fails
|
||||||
|
|
||||||
|
@commands.command()
|
||||||
|
async def help(self, ctx: commands.Context) -> None:
|
||||||
|
embed: discord.Embed = discord.Embed(
|
||||||
|
title=f"{_('command_help_embed_title')}",
|
||||||
|
description=f"{_('command_help_embed_desc')}",
|
||||||
|
color=discord.Colour(0x000000),
|
||||||
|
)
|
||||||
|
|
||||||
|
command_categories = {
|
||||||
|
f"{_('command_help_categories_general')}": [
|
||||||
|
"mem",
|
||||||
|
"talk",
|
||||||
|
"about",
|
||||||
|
"ping",
|
||||||
|
"impact",
|
||||||
|
"demotivator",
|
||||||
|
"help",
|
||||||
|
],
|
||||||
|
f"{_('command_help_categories_admin')}": ["stats", "retrain", "setlanguage"],
|
||||||
|
}
|
||||||
|
|
||||||
|
custom_commands: List[str] = []
|
||||||
|
for cog_name, cog in self.bot.cogs.items():
|
||||||
|
for command in cog.get_commands():
|
||||||
|
if (
|
||||||
|
command.name
|
||||||
|
not in command_categories[f"{_('command_help_categories_general')}"]
|
||||||
|
and command.name
|
||||||
|
not in command_categories[f"{_('command_help_categories_admin')}"]
|
||||||
|
):
|
||||||
|
custom_commands.append(command.name)
|
||||||
|
|
||||||
|
if custom_commands:
|
||||||
|
embed.add_field(
|
||||||
|
name=_('command_help_categories_custom'),
|
||||||
|
value="\n".join(
|
||||||
|
[
|
||||||
|
f"{settings['bot']['prefix']}{command}"
|
||||||
|
for command in custom_commands
|
||||||
|
]
|
||||||
|
),
|
||||||
|
inline=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
for category, commands_list in command_categories.items():
|
||||||
|
commands_in_category: str = "\n".join(
|
||||||
|
[f"{settings['bot']['prefix']}{command}" for command in commands_list]
|
||||||
|
)
|
||||||
|
embed.add_field(name=category, value=commands_in_category, inline=False)
|
||||||
|
|
||||||
|
|
||||||
|
await send_message(ctx, embed=embed)
|
||||||
|
|
||||||
|
@requires_admin()
|
||||||
|
@commands.command()
|
||||||
|
async def setlanguage(self, ctx: commands.Context, locale: str) -> None:
|
||||||
|
await ctx.defer()
|
||||||
|
set_language(locale)
|
||||||
|
await ctx.send(":thumbsup:")
|
||||||
|
|
||||||
|
@commands.command()
|
||||||
|
async def ping(self, ctx: commands.Context) -> None:
|
||||||
|
await ctx.defer()
|
||||||
|
latency: int = round(self.bot.latency * 1000)
|
||||||
|
|
||||||
|
embed: discord.Embed = discord.Embed(
|
||||||
|
title="Pong!!",
|
||||||
|
description=(
|
||||||
|
settings["bot"]["misc"]["ping_line"],
|
||||||
|
f"`{_('command_ping_embed_desc')}: {latency}ms`\n",
|
||||||
|
),
|
||||||
|
color=discord.Colour(0x000000),
|
||||||
|
)
|
||||||
|
embed.set_footer(
|
||||||
|
text=f"{_('command_ping_footer')} {ctx.author.name}",
|
||||||
|
icon_url=ctx.author.display_avatar.url,
|
||||||
|
)
|
||||||
|
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
@commands.command()
|
||||||
|
async def about(self, ctx: commands.Context) -> None:
|
||||||
|
latest_version: str = check_for_update(slient=True)
|
||||||
|
embed: discord.Embed = discord.Embed(title=f"{(_('command_about_embed_title'))}", description="", color=Colour(0x000000))
|
||||||
|
embed.add_field(name=f"{(_('command_about_embed_field1'))}", value=f"{settings['name']}", inline=False)
|
||||||
|
embed.add_field(name=f"{(_('command_about_embed_field2name'))}", value=f"{(_('command_about_embed_field2value')).format(local_version=local_version, latest_version=latest_version)}", inline=False)
|
||||||
|
embed.add_field(name=f"Git", value=get_git_origin_raw())
|
||||||
|
embed.add_field(name=f"OS", value=platform.platform())
|
||||||
|
|
||||||
|
await send_message(ctx, embed=embed)
|
||||||
|
|
||||||
|
@commands.command()
|
||||||
|
async def stats(self, ctx: commands.Context) -> None:
|
||||||
|
memory_file: str = "memory.json"
|
||||||
|
file_size: int = os.path.getsize(memory_file)
|
||||||
|
|
||||||
|
with open(memory_file, "r") as file:
|
||||||
|
line_count: int = sum(1 for _ in file)
|
||||||
|
|
||||||
|
embed: discord.Embed = discord.Embed(
|
||||||
|
title=f"{_('command_stats_embed_title')}",
|
||||||
|
description=f"{_('command_stats_embed_desc')}",
|
||||||
|
color=discord.Colour(0x000000),
|
||||||
|
)
|
||||||
|
embed.add_field(
|
||||||
|
name=f"{_('command_stats_embed_field1name')}",
|
||||||
|
value=f"placeholder",
|
||||||
|
inline=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(settings["splash_text_loc"], "r") as f:
|
||||||
|
splash_text = "".join(f.readlines())
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name=_('command_stats_embed_field3name'),
|
||||||
|
value=_('command_stats_embed_field3value').format(
|
||||||
|
NAME=settings["name"],
|
||||||
|
PREFIX=settings["bot"]["prefix"],
|
||||||
|
ownerid=settings["bot"]["owner_ids"][0],
|
||||||
|
PING_LINE=settings["bot"]["misc"]["ping_line"],
|
||||||
|
showmemenabled=settings["bot"]["allow_show_mem_command"],
|
||||||
|
USERTRAIN_ENABLED=settings["bot"]["user_training"],
|
||||||
|
song=settings["bot"]["misc"]["active_song"],
|
||||||
|
splashtext=splash_text
|
||||||
|
),
|
||||||
|
inline=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
await send_message(ctx, embed=embed)
|
||||||
|
|
||||||
|
@commands.command()
|
||||||
|
async def mem(self, ctx: commands.Context) -> None:
|
||||||
|
if not settings["bot"]["allow_show_mem_command"]:
|
||||||
|
return
|
||||||
|
|
||||||
|
with open(settings["bot"]["active_memory"], "rb") as f:
|
||||||
|
data: bytes = f.read()
|
||||||
|
|
||||||
|
response = requests.post(
|
||||||
|
"https://litterbox.catbox.moe/resources/internals/api.php",
|
||||||
|
data={"reqtype": "fileupload", "time": "1h"},
|
||||||
|
files={"fileToUpload": data},
|
||||||
|
)
|
||||||
|
|
||||||
|
await send_message(ctx, response.text)
|
||||||
|
|
||||||
|
|
||||||
|
async def setup(bot: discord.ext.commands.Bot):
|
||||||
|
print("Setting up base_commands")
|
||||||
|
bot.remove_command("help")
|
||||||
|
await bot.add_cog(BaseCommands(bot))
|
|
@ -1,18 +1,15 @@
|
||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
from modules.globalvars import ownerid
|
from modules.permission import requires_admin
|
||||||
|
|
||||||
COG_PREFIX = "assets.cogs."
|
COG_PREFIX = "assets.cogs."
|
||||||
|
|
||||||
class CogManager(commands.Cog):
|
class CogManager(commands.Cog):
|
||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
@requires_admin()
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def load(self, ctx, cog_name: str = None):
|
async def load(self, ctx, cog_name: str = None):
|
||||||
if ctx.author.id != ownerid:
|
|
||||||
await ctx.send("You do not have permission to use this command.")
|
|
||||||
return
|
|
||||||
if cog_name is None:
|
if cog_name is None:
|
||||||
await ctx.send("Please provide the cog name to load.")
|
await ctx.send("Please provide the cog name to load.")
|
||||||
return
|
return
|
||||||
|
@ -21,12 +18,9 @@ class CogManager(commands.Cog):
|
||||||
await ctx.send(f"Loaded cog `{cog_name}` successfully.")
|
await ctx.send(f"Loaded cog `{cog_name}` successfully.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await ctx.send(f"Error loading cog `{cog_name}`: {e}")
|
await ctx.send(f"Error loading cog `{cog_name}`: {e}")
|
||||||
|
@requires_admin()
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def unload(self, ctx, cog_name: str = None):
|
async def unload(self, ctx, cog_name: str = None):
|
||||||
if ctx.author.id != ownerid:
|
|
||||||
await ctx.send("You do not have permission to use this command.")
|
|
||||||
return
|
|
||||||
if cog_name is None:
|
if cog_name is None:
|
||||||
await ctx.send("Please provide the cog name to unload.")
|
await ctx.send("Please provide the cog name to unload.")
|
||||||
return
|
return
|
||||||
|
@ -35,12 +29,9 @@ class CogManager(commands.Cog):
|
||||||
await ctx.send(f"Unloaded cog `{cog_name}` successfully.")
|
await ctx.send(f"Unloaded cog `{cog_name}` successfully.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await ctx.send(f"Error unloading cog `{cog_name}`: {e}")
|
await ctx.send(f"Error unloading cog `{cog_name}`: {e}")
|
||||||
|
@requires_admin()
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def reload(self, ctx, cog_name: str = None):
|
async def reload(self, ctx, cog_name: str = None):
|
||||||
if ctx.author.id != ownerid:
|
|
||||||
await ctx.send("You do not have permission to use this command.")
|
|
||||||
return
|
|
||||||
if cog_name is None:
|
if cog_name is None:
|
||||||
await ctx.send("Please provide the cog name to reload.")
|
await ctx.send("Please provide the cog name to reload.")
|
||||||
return
|
return
|
112
assets/cogs/internal/markov.py
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
import discord
|
||||||
|
from discord.ext import commands
|
||||||
|
|
||||||
|
import discord.ext
|
||||||
|
import discord.ext.commands
|
||||||
|
|
||||||
|
from modules.markovmemory import save_markov_model, train_markov_model, load_markov_model
|
||||||
|
from modules.permission import requires_admin
|
||||||
|
from modules.sentenceprocessing import (
|
||||||
|
improve_sentence_coherence,
|
||||||
|
is_positive,
|
||||||
|
rephrase_for_coherence,
|
||||||
|
send_message,
|
||||||
|
)
|
||||||
|
from modules.volta.main import _
|
||||||
|
import logging
|
||||||
|
from typing import List, Optional, Set
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import markovify
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger("goober")
|
||||||
|
from modules.settings import instance as settings_manager
|
||||||
|
|
||||||
|
settings = settings_manager.settings
|
||||||
|
|
||||||
|
|
||||||
|
class Markov(commands.Cog):
|
||||||
|
def __init__(self, bot):
|
||||||
|
self.bot: discord.ext.commands.Bot = bot
|
||||||
|
|
||||||
|
|
||||||
|
@requires_admin()
|
||||||
|
@commands.command()
|
||||||
|
async def retrain(self, ctx: discord.ext.commands.Context):
|
||||||
|
markov_model: Optional[markovify.Text] = load_markov_model()
|
||||||
|
message_ref: discord.Message | None = await send_message(
|
||||||
|
ctx, f"{_('command_markov_retrain')}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if message_ref is None:
|
||||||
|
logger.error("Failed to send message!")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(settings["bot"]["active_memory"], "r") as f:
|
||||||
|
memory: List[str] = json.load(f)
|
||||||
|
except FileNotFoundError:
|
||||||
|
await send_message(ctx, f"{_('command_markov_memory_not_found')}")
|
||||||
|
return
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
await send_message(ctx, f"{_('command_markov_memory_is_corrupt')}")
|
||||||
|
return
|
||||||
|
|
||||||
|
data_size: int = len(memory)
|
||||||
|
|
||||||
|
processing_message_ref: discord.Message | None = await send_message(
|
||||||
|
ctx, f"{(_('command_markov_retraining').format(data_size=data_size))}"
|
||||||
|
)
|
||||||
|
if processing_message_ref is None:
|
||||||
|
logger.error("Couldnt find message processing message!")
|
||||||
|
|
||||||
|
start_time: float = time.time()
|
||||||
|
|
||||||
|
model = train_markov_model(memory)
|
||||||
|
if not model:
|
||||||
|
logger.error("Failed to train markov model")
|
||||||
|
await ctx.send("Failed to retrain!")
|
||||||
|
return False
|
||||||
|
|
||||||
|
markov_model = model
|
||||||
|
save_markov_model(markov_model)
|
||||||
|
|
||||||
|
logger.debug(f"Completed retraining in {round(time.time() - start_time,3)}s")
|
||||||
|
|
||||||
|
await send_message(
|
||||||
|
ctx,
|
||||||
|
_('command_markov_retraining').format(data_size=data_size),
|
||||||
|
edit=True,
|
||||||
|
message_reference=processing_message_ref,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@commands.command()
|
||||||
|
async def talk(self, ctx: commands.Context, sentence_size: int = 5) -> None:
|
||||||
|
markov_model: Optional[markovify.Text] = load_markov_model()
|
||||||
|
if markov_model is None:
|
||||||
|
await send_message(ctx, _("command_markovcommand_talk_insufficent_text"))
|
||||||
|
return
|
||||||
|
|
||||||
|
raw_sentence = None
|
||||||
|
if sentence_size == 1:
|
||||||
|
raw_sentence = markov_model.make_short_sentence(max_chars=100, tries=100)
|
||||||
|
else:
|
||||||
|
raw_sentence = markov_model.make_sentence(tries=100, max_words=sentence_size)
|
||||||
|
print(raw_sentence)
|
||||||
|
|
||||||
|
if random.random() < 0.9 and is_positive(raw_sentence):
|
||||||
|
gif_url = random.choice(settings["bot"]["misc"]["positive_gifs"])
|
||||||
|
raw_sentence = f"{raw_sentence}\n[jif]({gif_url})"
|
||||||
|
|
||||||
|
os.environ["gooberlatestgen"] = raw_sentence
|
||||||
|
await send_message(ctx, raw_sentence)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async def setup(bot):
|
||||||
|
await bot.add_cog(Markov(bot))
|
89
assets/cogs/internal/permission.py
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
import discord
|
||||||
|
from discord.ext import commands
|
||||||
|
|
||||||
|
from modules.permission import requires_admin
|
||||||
|
from modules.settings import instance as settings_manager
|
||||||
|
|
||||||
|
settings = settings_manager.settings
|
||||||
|
|
||||||
|
|
||||||
|
class PermissionManager(commands.Cog):
|
||||||
|
def __init__(self, bot):
|
||||||
|
self.bot = bot
|
||||||
|
|
||||||
|
@requires_admin()
|
||||||
|
@commands.command()
|
||||||
|
async def add_owner(self, ctx: commands.Context, member: discord.Member):
|
||||||
|
settings["bot"]["owner_ids"].append(member.id)
|
||||||
|
settings_manager.commit()
|
||||||
|
embed = discord.Embed(
|
||||||
|
title="Permissions",
|
||||||
|
description=f"Set {member.name} as an owner",
|
||||||
|
color=discord.Color.blue(),
|
||||||
|
)
|
||||||
|
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
@requires_admin()
|
||||||
|
@commands.command()
|
||||||
|
async def remove_owner(self, ctx: commands.Context, member: discord.Member):
|
||||||
|
try:
|
||||||
|
settings["bot"]["owner_ids"].remove(member.id)
|
||||||
|
settings_manager.commit()
|
||||||
|
except ValueError:
|
||||||
|
await ctx.send("User is not an owner!")
|
||||||
|
return
|
||||||
|
|
||||||
|
embed = discord.Embed(
|
||||||
|
title="Permissions",
|
||||||
|
description=f"Removed {member.name} from being an owner",
|
||||||
|
color=discord.Color.blue(),
|
||||||
|
)
|
||||||
|
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
@requires_admin()
|
||||||
|
@commands.command()
|
||||||
|
async def blacklist_user(self, ctx: commands.Context, member: discord.Member):
|
||||||
|
settings["bot"]["blacklisted_users"].append(member.id)
|
||||||
|
settings_manager.add_admin_log_event(
|
||||||
|
{
|
||||||
|
"action": "add",
|
||||||
|
"author": ctx.author.id,
|
||||||
|
"change": "blacklisted_users",
|
||||||
|
"messageId": ctx.message.id,
|
||||||
|
"target": member.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
settings_manager.commit()
|
||||||
|
|
||||||
|
embed = discord.Embed(
|
||||||
|
title="Blacklist",
|
||||||
|
description=f"Added {member.name} to the blacklist",
|
||||||
|
color=discord.Color.blue(),
|
||||||
|
)
|
||||||
|
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
@requires_admin()
|
||||||
|
@commands.command()
|
||||||
|
async def unblacklist_user(self, ctx: commands.Context, member: discord.Member):
|
||||||
|
try:
|
||||||
|
settings["bot"]["blacklisted_users"].remove(member.id)
|
||||||
|
settings_manager.commit()
|
||||||
|
|
||||||
|
except ValueError:
|
||||||
|
await ctx.send("User is not on the blacklist!")
|
||||||
|
return
|
||||||
|
|
||||||
|
embed = discord.Embed(
|
||||||
|
title="Blacklist",
|
||||||
|
description=f"Removed {member.name} from blacklist",
|
||||||
|
color=discord.Color.blue(),
|
||||||
|
)
|
||||||
|
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
|
||||||
|
async def setup(bot):
|
||||||
|
await bot.add_cog(PermissionManager(bot))
|
|
@ -1,84 +0,0 @@
|
||||||
import os
|
|
||||||
import discord
|
|
||||||
from discord.ext import commands, tasks
|
|
||||||
import aiohttp
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
|
|
||||||
load_dotenv()
|
|
||||||
|
|
||||||
#stole most of this code from my old expect bot so dont be suprised if its poorly made
|
|
||||||
|
|
||||||
LASTFM_API_KEY = os.getenv("LASTFM_API_KEY")
|
|
||||||
LASTFM_USERNAME = os.getenv("LASTFM_USERNAME")
|
|
||||||
|
|
||||||
class LastFmCog(commands.Cog):
|
|
||||||
def __init__(self, bot):
|
|
||||||
self.bot = bot
|
|
||||||
self.current_track = None
|
|
||||||
self.update_presence_task = None
|
|
||||||
self.ready = False
|
|
||||||
bot.loop.create_task(self.wait_until_ready())
|
|
||||||
|
|
||||||
async def wait_until_ready(self):
|
|
||||||
await self.bot.wait_until_ready()
|
|
||||||
self.ready = True
|
|
||||||
self.update_presence.start()
|
|
||||||
|
|
||||||
@tasks.loop(seconds=60)
|
|
||||||
async def update_presence(self):
|
|
||||||
print("Looped!")
|
|
||||||
if not self.ready:
|
|
||||||
return
|
|
||||||
track = await self.fetch_current_track()
|
|
||||||
if track and track != self.current_track:
|
|
||||||
self.current_track = track
|
|
||||||
artist, song = track
|
|
||||||
activity_name = f"{artist} - {song}"
|
|
||||||
await self.bot.change_presence(activity=discord.Activity(type=discord.ActivityType.listening, name=activity_name))
|
|
||||||
print(f"Updated song to {artist} - {song}")
|
|
||||||
else:
|
|
||||||
print("LastFM gave me the same track! not updating...")
|
|
||||||
|
|
||||||
@update_presence.before_loop
|
|
||||||
async def before_update_presence(self):
|
|
||||||
await self.bot.wait_until_ready()
|
|
||||||
|
|
||||||
@commands.command(name="lastfm")
|
|
||||||
async def lastfm_command(self, ctx):
|
|
||||||
track = await self.fetch_current_track()
|
|
||||||
if not track:
|
|
||||||
await ctx.send("No track currently playing or could not fetch data")
|
|
||||||
return
|
|
||||||
self.current_track = track
|
|
||||||
artist, song = track
|
|
||||||
activity_name = f"{artist} - {song}"
|
|
||||||
await self.bot.change_presence(activity=discord.Activity(type=discord.ActivityType.listening, name=activity_name))
|
|
||||||
await ctx.send(f"Updated presence to: Listening to {activity_name}")
|
|
||||||
|
|
||||||
async def fetch_current_track(self):
|
|
||||||
url = (
|
|
||||||
f"http://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks"
|
|
||||||
f"&user={LASTFM_USERNAME}&api_key={LASTFM_API_KEY}&format=json&limit=1"
|
|
||||||
)
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
async with session.get(url) as resp:
|
|
||||||
if resp.status != 200:
|
|
||||||
return None
|
|
||||||
data = await resp.json()
|
|
||||||
|
|
||||||
recenttracks = data.get("recenttracks", {}).get("track", [])
|
|
||||||
if not recenttracks:
|
|
||||||
return None
|
|
||||||
|
|
||||||
track = recenttracks[0]
|
|
||||||
if '@attr' in track and track['@attr'].get('nowplaying') == 'true':
|
|
||||||
artist = track.get('artist', {}).get('#text', 'Unknown Artist')
|
|
||||||
song = track.get('name', 'Unknown Song')
|
|
||||||
return artist, song
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def setup(bot):
|
|
||||||
if not LASTFM_API_KEY or not LASTFM_USERNAME:
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
await bot.add_cog(LastFmCog(bot))
|
|
123
assets/cogs/lyrics.py
Normal 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))
|
|
@ -1,33 +0,0 @@
|
||||||
import discord
|
|
||||||
from discord.ext import commands
|
|
||||||
from modules.globalvars import RED, GREEN, RESET, LOCAL_VERSION_FILE
|
|
||||||
import os
|
|
||||||
|
|
||||||
class songchange(commands.Cog):
|
|
||||||
def __init__(self, bot):
|
|
||||||
self.bot = bot
|
|
||||||
|
|
||||||
def get_local_version():
|
|
||||||
if os.path.exists(LOCAL_VERSION_FILE):
|
|
||||||
with open(LOCAL_VERSION_FILE, "r") as f:
|
|
||||||
return f.read().strip()
|
|
||||||
return "0.0.0"
|
|
||||||
|
|
||||||
global local_version
|
|
||||||
local_version = get_local_version()
|
|
||||||
|
|
||||||
@commands.command()
|
|
||||||
async def changesong(self, ctx):
|
|
||||||
if LOCAL_VERSION_FILE > "0.11.8":
|
|
||||||
await ctx.send(f"Goober is too old! you must have version 0.11.8 you have {local_version}")
|
|
||||||
return
|
|
||||||
await ctx.send("Check the terminal! (this does not persist across restarts)")
|
|
||||||
song = input("\nEnter a song:\n")
|
|
||||||
try:
|
|
||||||
await self.bot.change_presence(activity=discord.Activity(type=discord.ActivityType.listening, name=f"{song}"))
|
|
||||||
print(f"{GREEN}Changed song to {song}{RESET}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"{RED}An error occurred while changing songs..: {str(e)}{RESET}")
|
|
||||||
|
|
||||||
async def setup(bot):
|
|
||||||
await bot.add_cog(songchange(bot))
|
|
|
@ -1,155 +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
|
|
||||||
from tensorflow import keras
|
|
||||||
from tensorflow.keras.preprocessing.text import Tokenizer
|
|
||||||
from tensorflow.keras.preprocessing.sequence import pad_sequences
|
|
||||||
from tensorflow.keras.models import Sequential, load_model
|
|
||||||
from tensorflow.keras.layers import Embedding, LSTM, Dense
|
|
||||||
from tensorflow.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))
|
|
|
@ -1,113 +0,0 @@
|
||||||
import discord
|
|
||||||
from discord.ext import commands
|
|
||||||
import aiohttp
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
import json
|
|
||||||
import asyncio
|
|
||||||
from urllib.parse import urljoin
|
|
||||||
from modules.globalvars import ownerid
|
|
||||||
class WebScraper(commands.Cog):
|
|
||||||
def __init__(self, bot):
|
|
||||||
self.bot = bot
|
|
||||||
self.visited_urls = set()
|
|
||||||
|
|
||||||
async def fetch(self, session, url):
|
|
||||||
"""Fetch the HTML content of a URL."""
|
|
||||||
try:
|
|
||||||
async with session.get(url, timeout=10) as response:
|
|
||||||
return await response.text()
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Failed to fetch {url}: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def extract_sentences(self, text):
|
|
||||||
"""Extract sentences from text."""
|
|
||||||
sentences = text.split('.')
|
|
||||||
return [sentence.strip() for sentence in sentences if sentence.strip()]
|
|
||||||
|
|
||||||
def save_to_json(self, sentences):
|
|
||||||
"""Save sentences to memory.json."""
|
|
||||||
try:
|
|
||||||
try:
|
|
||||||
with open("memory.json", "r") as file:
|
|
||||||
data = json.load(file)
|
|
||||||
except (FileNotFoundError, json.JSONDecodeError):
|
|
||||||
data = []
|
|
||||||
data.extend(sentences)
|
|
||||||
with open("memory.json", "w") as file:
|
|
||||||
json.dump(data, file, indent=4)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Failed to save to JSON: {e}")
|
|
||||||
|
|
||||||
def undo_last_scrape(self):
|
|
||||||
"""Undo the last scrape by removing the most recent sentences."""
|
|
||||||
try:
|
|
||||||
with open("memory.json", "r") as file:
|
|
||||||
data = json.load(file)
|
|
||||||
|
|
||||||
if not data:
|
|
||||||
print("No data to undo.")
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
data = data[:-1]
|
|
||||||
|
|
||||||
with open("memory.json", "w") as file:
|
|
||||||
json.dump(data, file, indent=4)
|
|
||||||
|
|
||||||
return True
|
|
||||||
except (FileNotFoundError, json.JSONDecodeError):
|
|
||||||
print("No data to undo or failed to load JSON.")
|
|
||||||
return False
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Failed to undo last scrape: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def scrape_links(self, session, url, depth=2):
|
|
||||||
print(f"Scraping: {url}")
|
|
||||||
self.visited_urls.add(url)
|
|
||||||
|
|
||||||
html = await self.fetch(session, url)
|
|
||||||
if not html:
|
|
||||||
return
|
|
||||||
|
|
||||||
soup = BeautifulSoup(html, "html.parser")
|
|
||||||
|
|
||||||
for paragraph in soup.find_all('p'):
|
|
||||||
sentences = self.extract_sentences(paragraph.get_text())
|
|
||||||
self.save_to_json(sentences)
|
|
||||||
|
|
||||||
|
|
||||||
@commands.command()
|
|
||||||
async def start_scrape(self, ctx, start_url: str):
|
|
||||||
"""Command to start the scraping process."""
|
|
||||||
if ctx.author.id != ownerid:
|
|
||||||
await ctx.send("You do not have permission to use this command.")
|
|
||||||
return
|
|
||||||
|
|
||||||
if not start_url.startswith("http"):
|
|
||||||
await ctx.send("Please provide a valid URL.")
|
|
||||||
return
|
|
||||||
|
|
||||||
await ctx.send(f"Starting scrape from {start_url}... This may take a while!")
|
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
await self.scrape_links(session, start_url)
|
|
||||||
|
|
||||||
await ctx.send("Scraping complete! Sentences saved to memory.json.")
|
|
||||||
|
|
||||||
@commands.command()
|
|
||||||
async def undo_scrape(self, ctx):
|
|
||||||
"""Command to undo the last scrape."""
|
|
||||||
if ctx.author.id != ownerid:
|
|
||||||
await ctx.send("You do not have permission to use this command.")
|
|
||||||
return
|
|
||||||
|
|
||||||
success = self.undo_last_scrape()
|
|
||||||
if success:
|
|
||||||
await ctx.send("Last scrape undone successfully.")
|
|
||||||
else:
|
|
||||||
await ctx.send("No data to undo or an error occurred.")
|
|
||||||
|
|
||||||
async def setup(bot):
|
|
||||||
await bot.add_cog(WebScraper(bot))
|
|
|
@ -1,880 +0,0 @@
|
||||||
import discord
|
|
||||||
from discord.ext import commands, tasks
|
|
||||||
import asyncio
|
|
||||||
from aiohttp import web
|
|
||||||
import psutil
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
from datetime import datetime
|
|
||||||
import time
|
|
||||||
import aiohttp
|
|
||||||
import re
|
|
||||||
from aiohttp import WSMsgType
|
|
||||||
from modules.globalvars import VERSION_URL
|
|
||||||
import sys
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
class GooberWeb(commands.Cog):
|
|
||||||
def __init__(self, bot):
|
|
||||||
self.bot = bot
|
|
||||||
self.app = web.Application()
|
|
||||||
self.runner = None
|
|
||||||
self.site = None
|
|
||||||
self.last_command = "No commands executed yet"
|
|
||||||
self.last_command_time = "Never"
|
|
||||||
self.start_time = time.time()
|
|
||||||
self.websockets = set()
|
|
||||||
|
|
||||||
self.app.add_routes([
|
|
||||||
web.get('/', self.handle_index),
|
|
||||||
web.get('/changesong', self.handle_changesong),
|
|
||||||
web.get('/stats', self.handle_stats),
|
|
||||||
web.get('/data', self.handle_json_data),
|
|
||||||
web.get('/ws', self.handle_websocket),
|
|
||||||
web.get('/styles.css', self.handle_css),
|
|
||||||
web.get('/settings', self.handle_settings),
|
|
||||||
web.post('/update_settings', self.handle_update_settings),
|
|
||||||
web.post('/restart_bot', self.handle_restart_bot),
|
|
||||||
])
|
|
||||||
|
|
||||||
self.bot.loop.create_task(self.start_web_server())
|
|
||||||
self.update_clients.start()
|
|
||||||
|
|
||||||
async def restart_bot(self):
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
python = sys.executable
|
|
||||||
os.execl(python, python, *sys.argv)
|
|
||||||
|
|
||||||
async def handle_restart_bot(self, request):
|
|
||||||
asyncio.create_task(self.restart_bot())
|
|
||||||
return web.Response(text="Bot is restarting...")
|
|
||||||
|
|
||||||
async def get_blacklisted_users(self):
|
|
||||||
blacklisted_ids = os.getenv("BLACKLISTED_USERS", "").split(",")
|
|
||||||
blacklisted_users = []
|
|
||||||
|
|
||||||
for user_id in blacklisted_ids:
|
|
||||||
if not user_id.strip():
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
user = await self.bot.fetch_user(int(user_id))
|
|
||||||
blacklisted_users.append({
|
|
||||||
"name": f"{user.name}",
|
|
||||||
"avatar_url": str(user.avatar.url) if user.avatar else str(user.default_avatar.url),
|
|
||||||
"id": user.id
|
|
||||||
})
|
|
||||||
except discord.NotFound:
|
|
||||||
blacklisted_users.append({
|
|
||||||
"name": f"Unknown User ({user_id})",
|
|
||||||
"avatar_url": "",
|
|
||||||
"id": user_id
|
|
||||||
})
|
|
||||||
except discord.HTTPException as e:
|
|
||||||
print(f"Error fetching user {user_id}: {e}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
return blacklisted_users
|
|
||||||
|
|
||||||
async def get_enhanced_guild_info(self):
|
|
||||||
guilds = sorted(self.bot.guilds, key=lambda g: g.member_count, reverse=True)
|
|
||||||
guild_info = []
|
|
||||||
|
|
||||||
for guild in guilds:
|
|
||||||
icon_url = str(guild.icon.url) if guild.icon else ""
|
|
||||||
guild_info.append({
|
|
||||||
"name": guild.name,
|
|
||||||
"member_count": guild.member_count,
|
|
||||||
"icon_url": icon_url,
|
|
||||||
"id": guild.id
|
|
||||||
})
|
|
||||||
|
|
||||||
return guild_info
|
|
||||||
|
|
||||||
async def start_web_server(self):
|
|
||||||
self.runner = web.AppRunner(self.app)
|
|
||||||
await self.runner.setup()
|
|
||||||
self.site = web.TCPSite(self.runner, '0.0.0.0', 8080)
|
|
||||||
await self.site.start()
|
|
||||||
print("Goober web server started on port 8080")
|
|
||||||
|
|
||||||
async def stop_web_server(self):
|
|
||||||
await self.site.stop()
|
|
||||||
await self.runner.cleanup()
|
|
||||||
print("Web server stopped")
|
|
||||||
|
|
||||||
def cog_unload(self):
|
|
||||||
self.update_clients.cancel()
|
|
||||||
self.bot.loop.create_task(self.stop_web_server())
|
|
||||||
|
|
||||||
@tasks.loop(seconds=5)
|
|
||||||
async def update_clients(self):
|
|
||||||
if not self.websockets:
|
|
||||||
return
|
|
||||||
|
|
||||||
stats = await self.get_bot_stats()
|
|
||||||
message = json.dumps(stats)
|
|
||||||
|
|
||||||
for ws in set(self.websockets):
|
|
||||||
try:
|
|
||||||
await ws.send_str(message)
|
|
||||||
except ConnectionResetError:
|
|
||||||
self.websockets.remove(ws)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error sending to websocket: {e}")
|
|
||||||
self.websockets.remove(ws)
|
|
||||||
|
|
||||||
async def handle_websocket(self, request):
|
|
||||||
ws = web.WebSocketResponse()
|
|
||||||
await ws.prepare(request)
|
|
||||||
self.websockets.add(ws)
|
|
||||||
|
|
||||||
try:
|
|
||||||
async for msg in ws:
|
|
||||||
if msg.type == WSMsgType.ERROR:
|
|
||||||
print(f"WebSocket error: {ws.exception()}")
|
|
||||||
finally:
|
|
||||||
self.websockets.remove(ws)
|
|
||||||
|
|
||||||
return ws
|
|
||||||
|
|
||||||
async def handle_css(self, request):
|
|
||||||
css_path = os.path.join(os.path.dirname(__file__), 'styles.css')
|
|
||||||
if os.path.exists(css_path):
|
|
||||||
return web.FileResponse(css_path)
|
|
||||||
return web.Response(text="CSS file not found", status=404)
|
|
||||||
|
|
||||||
@commands.Cog.listener()
|
|
||||||
async def on_message(self, message):
|
|
||||||
if message.author.bot:
|
|
||||||
return
|
|
||||||
|
|
||||||
ctx = await self.bot.get_context(message)
|
|
||||||
if ctx.valid and ctx.command:
|
|
||||||
self._update_command_stats(ctx.command.name, ctx.author)
|
|
||||||
|
|
||||||
@commands.Cog.listener()
|
|
||||||
async def on_app_command_completion(self, interaction, command):
|
|
||||||
self._update_command_stats(command.name, interaction.user)
|
|
||||||
|
|
||||||
def _update_command_stats(self, command_name, user):
|
|
||||||
self.last_command = f"{command_name} (by {user.name})"
|
|
||||||
self.last_command_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
if self.websockets:
|
|
||||||
asyncio.create_task(self.update_clients())
|
|
||||||
|
|
||||||
async def get_bot_stats(self):
|
|
||||||
process = psutil.Process(os.getpid())
|
|
||||||
mem_info = process.memory_full_info()
|
|
||||||
cpu_percent = psutil.cpu_percent()
|
|
||||||
process_cpu = process.cpu_percent()
|
|
||||||
|
|
||||||
memory_json_size = "N/A"
|
|
||||||
if os.path.exists("memory.json"):
|
|
||||||
memory_json_size = f"{os.path.getsize('memory.json') / 1024:.2f} KB"
|
|
||||||
|
|
||||||
guild_info = await self.get_enhanced_guild_info()
|
|
||||||
blacklisted_users = await self.get_blacklisted_users()
|
|
||||||
|
|
||||||
uptime_seconds = int(time.time() - self.start_time)
|
|
||||||
uptime_str = f"{uptime_seconds // 86400}d {(uptime_seconds % 86400) // 3600}h {(uptime_seconds % 3600) // 60}m {uptime_seconds % 60}s"
|
|
||||||
|
|
||||||
return {
|
|
||||||
"ram_usage": f"{mem_info.rss / 1024 / 1024:.2f} MB",
|
|
||||||
"cpu_usage": f"{process_cpu}%",
|
|
||||||
"system_cpu": f"{cpu_percent}%",
|
|
||||||
"memory_json_size": memory_json_size,
|
|
||||||
"guild_count": len(guild_info),
|
|
||||||
"bl_count": len(blacklisted_users),
|
|
||||||
"guilds": guild_info,
|
|
||||||
"blacklisted_users": blacklisted_users,
|
|
||||||
"last_command": self.last_command,
|
|
||||||
"last_command_time": self.last_command_time,
|
|
||||||
"bot_uptime": uptime_str,
|
|
||||||
"latency": f"{self.bot.latency * 1000:.2f} ms",
|
|
||||||
"bot_name": self.bot.user.name,
|
|
||||||
"bot_avatar_url": str(self.bot.user.avatar.url) if self.bot.user.avatar else "",
|
|
||||||
"authenticated": os.getenv("gooberauthenticated"),
|
|
||||||
"lastmsg": os.getenv("gooberlatestgen"),
|
|
||||||
"localversion": os.getenv("gooberlocal_version"),
|
|
||||||
"latestversion": os.getenv("gooberlatest_version"),
|
|
||||||
"owner": os.getenv("ownerid")
|
|
||||||
}
|
|
||||||
|
|
||||||
async def handle_update(self, request):
|
|
||||||
if os.path.exists("goob/update.py"):
|
|
||||||
return web.FileResponse("goob/update.py")
|
|
||||||
return web.Response(text="Update file not found", status=404)
|
|
||||||
|
|
||||||
async def handle_changesong(self, request):
|
|
||||||
song = request.query.get('song', '')
|
|
||||||
if song:
|
|
||||||
await self.bot.change_presence(activity=discord.Activity(type=discord.ActivityType.listening, name=song))
|
|
||||||
return web.Response(text=f"Changed song to: {song}")
|
|
||||||
return web.Response(text="Please provide a song parameter", status=400)
|
|
||||||
|
|
||||||
async def handle_changes(self, request):
|
|
||||||
if os.path.exists("goob/changes.txt"):
|
|
||||||
return web.FileResponse("goob/changes.txt")
|
|
||||||
return web.Response(text="Changelog not found", status=404)
|
|
||||||
|
|
||||||
async def read_env_file(self):
|
|
||||||
env_vars = {}
|
|
||||||
try:
|
|
||||||
with open('.env', 'r') as f:
|
|
||||||
for line in f:
|
|
||||||
line = line.strip()
|
|
||||||
if not line or line.startswith('#') or '=' not in line:
|
|
||||||
continue
|
|
||||||
|
|
||||||
key, value = line.split('=', 1)
|
|
||||||
key = key.strip()
|
|
||||||
if key in ['splashtext', 'DISCORD_BOT_TOKEN']:
|
|
||||||
continue
|
|
||||||
|
|
||||||
env_vars[key] = value.strip('"\'')
|
|
||||||
except FileNotFoundError:
|
|
||||||
print(".env file not found")
|
|
||||||
return env_vars
|
|
||||||
|
|
||||||
|
|
||||||
async def handle_settings(self, request):
|
|
||||||
env_vars = await self.read_env_file()
|
|
||||||
|
|
||||||
# Get config.py variables
|
|
||||||
config_vars = {}
|
|
||||||
try:
|
|
||||||
with open('config.py', 'r') as f:
|
|
||||||
for line in f:
|
|
||||||
if line.startswith('VERSION_URL'):
|
|
||||||
config_vars['VERSION_URL'] = line.split('=', 1)[1].strip().strip('"')
|
|
||||||
except FileNotFoundError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
settings_html = """
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Goober Settings</title>
|
|
||||||
<style>
|
|
||||||
body { background-color: #121212; color: #ffffff; font-family: 'Segoe UI', sans-serif; }
|
|
||||||
h1 { color: #ff5555; text-align: center; }
|
|
||||||
.settings-container { max-width: 800px; margin: auto; background-color: #1e1e1e; padding: 20px; border-radius: 8px; }
|
|
||||||
.form-group { margin-bottom: 15px; }
|
|
||||||
label { display: block; margin-bottom: 5px; color: #ff9999; }
|
|
||||||
input { width: 100%; padding: 8px; background-color: #252525; color: white; border: 1px solid #444; border-radius: 4px; }
|
|
||||||
button { background-color: #5f1b1b; color: white; border: none; padding: 10px; border-radius: 4px; cursor: pointer; }
|
|
||||||
button:hover { background-color: #7a2323; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class='settings-container'>
|
|
||||||
<h1>Goober Settings</h1>
|
|
||||||
<form id='settingsForm' action='/update_settings' method='post'>
|
|
||||||
"""
|
|
||||||
|
|
||||||
for key, value in env_vars.items():
|
|
||||||
settings_html += f"""
|
|
||||||
<div class='form-group'>
|
|
||||||
<label for='{key}'>{key}</label>
|
|
||||||
<input type='text' id='{key}' name='{key}' value='{value}'>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
|
|
||||||
for key, value in config_vars.items():
|
|
||||||
settings_html += f"""
|
|
||||||
<div class='form-group'>
|
|
||||||
<label for='{key}'>{key}</label>
|
|
||||||
<input type='text' id='{key}' name='{key}' value='{value}'>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
|
|
||||||
settings_html += """
|
|
||||||
<button type='submit'>Save Settings</button>
|
|
||||||
</form>
|
|
||||||
<form action="/restart_bot" method="POST">
|
|
||||||
<button type="submit">Restart</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
"""
|
|
||||||
|
|
||||||
return web.Response(text=settings_html, content_type='text/html')
|
|
||||||
|
|
||||||
async def handle_update_settings(self, request):
|
|
||||||
data = await request.post()
|
|
||||||
env_text = ""
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open('.env', 'r') as f:
|
|
||||||
env_text = f.read()
|
|
||||||
except FileNotFoundError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def replace_match(match):
|
|
||||||
key = match.group(1)
|
|
||||||
value = match.group(2)
|
|
||||||
if key in ['splashtext', 'DISCORD_BOT_TOKEN']:
|
|
||||||
return match.group(0)
|
|
||||||
if key in data:
|
|
||||||
new_value = data[key]
|
|
||||||
if not (new_value.startswith('"') and new_value.endswith('"')):
|
|
||||||
new_value = f'"{new_value}"'
|
|
||||||
return f'{key}={new_value}'
|
|
||||||
return match.group(0)
|
|
||||||
|
|
||||||
env_text = re.sub(r'^(\w+)=([\s\S]+?)(?=\n\w+=|\Z)', replace_match, env_text, flags=re.MULTILINE)
|
|
||||||
|
|
||||||
with open('.env', 'w') as f:
|
|
||||||
f.write(env_text.strip() + '\n')
|
|
||||||
|
|
||||||
if 'VERSION_URL' in data:
|
|
||||||
config_text = ""
|
|
||||||
try:
|
|
||||||
with open('config.py', 'r') as f:
|
|
||||||
config_text = f.read()
|
|
||||||
except FileNotFoundError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
config_text = re.sub(r'^(VERSION_URL\s*=\s*").+?"', f'\\1{data["VERSION_URL"]}"', config_text, flags=re.MULTILINE)
|
|
||||||
|
|
||||||
with open('config.py', 'w') as f:
|
|
||||||
f.write(config_text.strip() + '\n')
|
|
||||||
|
|
||||||
return aiohttp.web.Response(text="Settings updated successfully!")
|
|
||||||
|
|
||||||
async def handle_index(self, request):
|
|
||||||
stats = await self.get_bot_stats()
|
|
||||||
|
|
||||||
guild_list_html = ""
|
|
||||||
for guild in stats['guilds']:
|
|
||||||
icon_html = f'<img src="{guild["icon_url"]}" alt="guild icon" class="guild-icon">' if guild["icon_url"] else '<div class="guild-icon-placeholder"></div>'
|
|
||||||
guild_list_html += f"""
|
|
||||||
<div class="guild-item">
|
|
||||||
{icon_html}
|
|
||||||
<div class="guild-info">
|
|
||||||
<div class="guild-name">{guild["name"]}</div>
|
|
||||||
<div class="guild-members">{guild["member_count"]} members</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
blacklisted_users_html = ""
|
|
||||||
for user in stats['blacklisted_users']:
|
|
||||||
avatar_html = f'<img src="{user["avatar_url"]}" alt="user avatar" class="user-avatar">' if user["avatar_url"] else '<div class="user-avatar-placeholder"></div>'
|
|
||||||
blacklisted_users_html += f"""
|
|
||||||
<div class="blacklisted-user">
|
|
||||||
{avatar_html}
|
|
||||||
<div class="user-info">
|
|
||||||
<div class="user-name">{user["name"]}</div>
|
|
||||||
<div class="user-id">ID: {user["id"]}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
|
|
||||||
owner_id = stats.get('owner')
|
|
||||||
owner = None
|
|
||||||
owner_username = "Owner"
|
|
||||||
owner_pfp = ""
|
|
||||||
|
|
||||||
if owner_id:
|
|
||||||
try:
|
|
||||||
owner = await self.bot.fetch_user(int(owner_id))
|
|
||||||
owner_username = f"{owner.name}"
|
|
||||||
owner_pfp = str(owner.avatar.url) if owner and owner.avatar else ""
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
html_content = f"""
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>goobs central</title>
|
|
||||||
<style>
|
|
||||||
#loading-screen {{
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background-color: #000;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 9999;
|
|
||||||
transition: opacity 1.5s ease-out;
|
|
||||||
}}
|
|
||||||
|
|
||||||
#loading-screen.fade-out {{
|
|
||||||
opacity: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
}}
|
|
||||||
|
|
||||||
#welcome-message {{
|
|
||||||
color: #fff;
|
|
||||||
font-size: 2em;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
text-align: center;
|
|
||||||
text-shadow: 0 0 10px #ff5555;
|
|
||||||
}}
|
|
||||||
|
|
||||||
#owner-avatar {{
|
|
||||||
width: 100px;
|
|
||||||
height: 100px;
|
|
||||||
border-radius: 50%;
|
|
||||||
object-fit: cover;
|
|
||||||
border: 3px solid #5f1b1b;
|
|
||||||
box-shadow: 0 0 20px #ff5555;
|
|
||||||
}}
|
|
||||||
body {{
|
|
||||||
background-color: #121212;
|
|
||||||
color: #ffffff;
|
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
line-height: 1.6;
|
|
||||||
}}
|
|
||||||
|
|
||||||
.topnav {{
|
|
||||||
background-color: #2a0a0a;
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 10px;
|
|
||||||
gap: 15px;
|
|
||||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
|
|
||||||
}}
|
|
||||||
|
|
||||||
.stat-item {{
|
|
||||||
gap: 5px;
|
|
||||||
color: white;
|
|
||||||
font-size: 14px;
|
|
||||||
background-color: #1a1a1a;
|
|
||||||
padding: 8px 15px;
|
|
||||||
border-radius: 6px;
|
|
||||||
align-items: center;
|
|
||||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
|
|
||||||
position: relative;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}}
|
|
||||||
|
|
||||||
.stat-item:hover {{
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4);
|
|
||||||
}}
|
|
||||||
|
|
||||||
.stat-item::after {{
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
bottom: -5px;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
width: 60%;
|
|
||||||
height: 3px;
|
|
||||||
background: linear-gradient(90deg, transparent, #ff5555, transparent);
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.3s ease;
|
|
||||||
}}
|
|
||||||
|
|
||||||
.stat-item:hover::after {{
|
|
||||||
opacity: 1;
|
|
||||||
}}
|
|
||||||
|
|
||||||
.stat-title {{
|
|
||||||
font-weight: bold;
|
|
||||||
color: #ff9999;
|
|
||||||
}}
|
|
||||||
|
|
||||||
.stat-item span:not(.stat-title) {{
|
|
||||||
font-weight: bold;
|
|
||||||
color: #ffffff;
|
|
||||||
}}
|
|
||||||
|
|
||||||
.center {{
|
|
||||||
text-align: center;
|
|
||||||
max-width: 800px;
|
|
||||||
margin: 20px auto;
|
|
||||||
padding: 0 20px;
|
|
||||||
}}
|
|
||||||
|
|
||||||
.bot-info {{
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 15px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}}
|
|
||||||
|
|
||||||
.bot-avatar {{
|
|
||||||
width: 80px;
|
|
||||||
height: 80px;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 3px solid #5f1b1b;
|
|
||||||
object-fit: cover;
|
|
||||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
|
|
||||||
}}
|
|
||||||
|
|
||||||
hr {{
|
|
||||||
border: 0;
|
|
||||||
height: 1px;
|
|
||||||
background-image: linear-gradient(to right, transparent, #5f1b1b, transparent);
|
|
||||||
margin: 20px 0;
|
|
||||||
}}
|
|
||||||
|
|
||||||
.stat-container-row {{
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 30px;
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0 20px;
|
|
||||||
}}
|
|
||||||
|
|
||||||
.stat-container {{
|
|
||||||
flex: 1;
|
|
||||||
background-color: #1e1e1e;
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
|
||||||
min-width: 0;
|
|
||||||
}}
|
|
||||||
|
|
||||||
.stat-title {{
|
|
||||||
color: #ff5555;
|
|
||||||
font-size: 1.1em;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
border-bottom: 1px solid #333;
|
|
||||||
padding-bottom: 5px;
|
|
||||||
}}
|
|
||||||
|
|
||||||
.guild-item {{
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 15px;
|
|
||||||
padding: 10px;
|
|
||||||
margin: 5px 0;
|
|
||||||
background-color: #252525;
|
|
||||||
border-radius: 5px;
|
|
||||||
transition: background-color 0.2s;
|
|
||||||
}}
|
|
||||||
|
|
||||||
.guild-item:hover {{
|
|
||||||
background-color: #333;
|
|
||||||
}}
|
|
||||||
|
|
||||||
.guild-icon {{
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
border-radius: 50%;
|
|
||||||
object-fit: cover;
|
|
||||||
}}
|
|
||||||
|
|
||||||
.guild-icon-placeholder {{
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: #7289da;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: white;
|
|
||||||
font-weight: bold;
|
|
||||||
}}
|
|
||||||
|
|
||||||
.guild-info {{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex-grow: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}}
|
|
||||||
|
|
||||||
.guild-name {{
|
|
||||||
font-weight: bold;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}}
|
|
||||||
|
|
||||||
.guild-members {{
|
|
||||||
font-size: 0.8em;
|
|
||||||
color: #99aab5;
|
|
||||||
}}
|
|
||||||
|
|
||||||
.blacklisted-user {{
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 15px;
|
|
||||||
padding: 10px;
|
|
||||||
margin: 5px 0;
|
|
||||||
background-color: #2a1a1a;
|
|
||||||
border-radius: 5px;
|
|
||||||
border-left: 3px solid #ff5555;
|
|
||||||
}}
|
|
||||||
|
|
||||||
.user-avatar {{
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
border-radius: 50%;
|
|
||||||
object-fit: cover;
|
|
||||||
}}
|
|
||||||
|
|
||||||
.user-avatar-placeholder {{
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: #7289da;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: white;
|
|
||||||
font-weight: bold;
|
|
||||||
}}
|
|
||||||
|
|
||||||
.user-info {{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}}
|
|
||||||
|
|
||||||
.user-name {{
|
|
||||||
font-weight: bold;
|
|
||||||
color: #ff5555;
|
|
||||||
}}
|
|
||||||
|
|
||||||
.user-id {{
|
|
||||||
font-size: 0.8em;
|
|
||||||
color: #99aab5;
|
|
||||||
}}
|
|
||||||
|
|
||||||
input[type="text"] {{
|
|
||||||
background-color: #252525;
|
|
||||||
color: white;
|
|
||||||
border: 1px solid #444;
|
|
||||||
padding: 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
width: 200px;
|
|
||||||
margin-right: 10px;
|
|
||||||
}}
|
|
||||||
|
|
||||||
button {{
|
|
||||||
background-color: #5f1b1b;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 8px 15px;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.2s;
|
|
||||||
}}
|
|
||||||
|
|
||||||
button:hover {{
|
|
||||||
background-color: #7a2323;
|
|
||||||
}}
|
|
||||||
|
|
||||||
#guild-list, #blacklisted-users {{
|
|
||||||
max-height: 400px;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding-right: 5px;
|
|
||||||
}}
|
|
||||||
|
|
||||||
#guild-list::-webkit-scrollbar, #blacklisted-users::-webkit-scrollbar {{
|
|
||||||
width: 6px;
|
|
||||||
}}
|
|
||||||
|
|
||||||
#guild-list::-webkit-scrollbar-track, #blacklisted-users::-webkit-scrollbar-track {{
|
|
||||||
background: #1a1a1a;
|
|
||||||
}}
|
|
||||||
|
|
||||||
#guild-list::-webkit-scrollbar-thumb, #blacklisted-users::-webkit-scrollbar-thumb {{
|
|
||||||
background-color: #5f1b1b;
|
|
||||||
border-radius: 3px;
|
|
||||||
}}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {{
|
|
||||||
.stat-container-row {{
|
|
||||||
flex-direction: column;
|
|
||||||
}}
|
|
||||||
|
|
||||||
.topnav {{
|
|
||||||
gap: 10px;
|
|
||||||
padding: 10px 5px;
|
|
||||||
}}
|
|
||||||
|
|
||||||
.stat-item {{
|
|
||||||
font-size: 12px;
|
|
||||||
padding: 8px 12px;
|
|
||||||
}}
|
|
||||||
}}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="loading-screen">
|
|
||||||
<img id="owner-avatar" src="{owner_pfp}" onerror="this.style.display='none'">
|
|
||||||
<div id="welcome-message"><b>Welcome, {owner_username}</b></div>
|
|
||||||
</div>
|
|
||||||
<div class="topnav">
|
|
||||||
<div class="stat-item" id="ram-usage">
|
|
||||||
<span class="stat-title">RAM:</span>
|
|
||||||
<span>{stats['ram_usage']}</span>
|
|
||||||
</div>
|
|
||||||
<div class="stat-item" id="system-cpu">
|
|
||||||
<span class="stat-title">CPU:</span>
|
|
||||||
<span>{stats['system_cpu']}</span>
|
|
||||||
</div>
|
|
||||||
<div class="stat-item" id="latency">
|
|
||||||
<span class="stat-title">Latency:</span>
|
|
||||||
<span>{stats['latency']}</span>
|
|
||||||
</div>
|
|
||||||
<div class="stat-item" id="json-size">
|
|
||||||
<span class="stat-title">JSON Size:</span>
|
|
||||||
<span>{stats['memory_json_size']}</span>
|
|
||||||
</div>
|
|
||||||
<div class="stat-item" id="uptime">
|
|
||||||
<span class="stat-title">Uptime:</span>
|
|
||||||
<span>{stats['bot_uptime']}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="center">
|
|
||||||
<div class="bot-info">
|
|
||||||
<img src="{stats['bot_avatar_url']}" alt="botvatar" class="bot-avatar" id="bot-avatar">
|
|
||||||
<h1 id="bot-name">{stats['bot_name']}</h1>
|
|
||||||
</div>
|
|
||||||
<hr>
|
|
||||||
<p>your stupid little goober that learns off other people's messages</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat-container-row">
|
|
||||||
<div class="stat-container">
|
|
||||||
<div class="stat-title">Last Command</div>
|
|
||||||
<div id="last-command">{stats['last_command']}</div>
|
|
||||||
<div style="font-size: 0.9em; color: #999;" id="last-command-time">at {stats['last_command_time']}</div>
|
|
||||||
<br>
|
|
||||||
<div class="stat-title">Logged into goober central</div>
|
|
||||||
<div id="last-command">{stats['authenticated']}</div>
|
|
||||||
<br>
|
|
||||||
<div class="stat-title">Last generated message</div>
|
|
||||||
<div id="last-command">{stats['lastmsg']}</div>
|
|
||||||
<br>
|
|
||||||
<div class="stat-title">Version</div>
|
|
||||||
<div id="last-command">Installed Version: {stats['localversion']}</div>
|
|
||||||
<div id="last-command">Latest Version: {stats['latestversion']}</div>
|
|
||||||
<br>
|
|
||||||
<div class="stat-title">goober-central URL</div>
|
|
||||||
<div id="last-command">{VERSION_URL}</div>
|
|
||||||
<br>
|
|
||||||
<div class="stat-title">Change song</div>
|
|
||||||
<form action="/changesong" method="get">
|
|
||||||
<input type="text" name="song" placeholder="Enter song name...">
|
|
||||||
<button type="submit">
|
|
||||||
change song
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat-container">
|
|
||||||
<div class="stat-title">Servers (<span id="guild-count">{stats['guild_count']}</span>)</div>
|
|
||||||
<div id="guild-list">
|
|
||||||
{guild_list_html}
|
|
||||||
</div>
|
|
||||||
<br>
|
|
||||||
<div class="stat-title">Blacklisted Users (<span id="guild-count">{stats['bl_count']})</div>
|
|
||||||
<div id="blacklisted-users">
|
|
||||||
{blacklisted_users_html if stats['blacklisted_users'] else "<div>No blacklisted users</div>"}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
window.addEventListener('load', function() {{
|
|
||||||
setTimeout(function() {{
|
|
||||||
const loadingScreen = document.getElementById('loading-screen');
|
|
||||||
loadingScreen.classList.add('fade-out');
|
|
||||||
setTimeout(function() {{
|
|
||||||
loadingScreen.remove();
|
|
||||||
}}, 1500);
|
|
||||||
}}, 1500);
|
|
||||||
}});
|
|
||||||
const ws = new WebSocket('ws://' + window.location.host + '/ws');
|
|
||||||
|
|
||||||
ws.onmessage = function(event) {{
|
|
||||||
const data = JSON.parse(event.data);
|
|
||||||
|
|
||||||
document.getElementById('ram-usage').innerHTML = `<span class="stat-title">RAM:</span> <span>${{data.ram_usage}}</span>`;
|
|
||||||
document.getElementById('cpu-usage').innerHTML = `<span class="stat-title">CPU:</span> <span>${{data.cpu_usage}}</span>`;
|
|
||||||
document.getElementById('system-cpu').innerHTML = `<span class="stat-title">System CPU:</span> <span>${{data.system_cpu}}</span>`;
|
|
||||||
document.getElementById('latency').innerHTML = `<span class="stat-title">Latency:</span> <span>${{data.latency}}</span>`;
|
|
||||||
document.getElementById('json-size').innerHTML = `<span class="stat-title">JSON Size:</span> <span>${{data.memory_json_size}}</span>`;
|
|
||||||
document.getElementById('uptime').innerHTML = `<span class="stat-title">Uptime:</span> <span>${{data.bot_uptime}}</span>`;
|
|
||||||
|
|
||||||
document.getElementById('bot-name').textContent = data.bot_name;
|
|
||||||
const botAvatar = document.getElementById('bot-avatar');
|
|
||||||
if (botAvatar.src !== data.bot_avatar_url) {{
|
|
||||||
botAvatar.src = data.bot_avatar_url;
|
|
||||||
}}
|
|
||||||
|
|
||||||
|
|
||||||
document.getElementById('last-command').textContent = data.last_command;
|
|
||||||
document.getElementById('last-command-time').textContent = `at ${{data.last_command_time}}`;
|
|
||||||
|
|
||||||
document.getElementById('guild-count').textContent = data.guild_count;
|
|
||||||
|
|
||||||
|
|
||||||
let guildListHtml = '';
|
|
||||||
data.guilds.forEach(guild => {{
|
|
||||||
const iconHtml = guild.icon_url
|
|
||||||
? `<img src="${{guild.icon_url}}" alt="guild icon" class="guild-icon">`
|
|
||||||
: '<div class="guild-icon-placeholder"></div>';
|
|
||||||
guildListHtml += `
|
|
||||||
<div class="guild-item">
|
|
||||||
${{iconHtml}}
|
|
||||||
<div class="guild-info">
|
|
||||||
<div class="guild-name">${{guild.name}}</div>
|
|
||||||
<div class="guild-members">${{guild.member_count}} members</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}});
|
|
||||||
document.getElementById('guild-list').innerHTML = guildListHtml;
|
|
||||||
|
|
||||||
let blacklistedUsersHtml = '';
|
|
||||||
if (data.blacklisted_users && data.blacklisted_users.length > 0) {{
|
|
||||||
data.blacklisted_users.forEach(user => {{
|
|
||||||
const avatarHtml = user.avatar_url
|
|
||||||
? `<img src="${{user.avatar_url}}" alt="user avatar" class="user-avatar">`
|
|
||||||
: '<div class="user-avatar-placeholder"></div>';
|
|
||||||
blacklistedUsersHtml += `
|
|
||||||
<div class="blacklisted-user">
|
|
||||||
${{avatarHtml}}
|
|
||||||
<div class="user-info">
|
|
||||||
<div class="user-name">${{user.name}}</div>
|
|
||||||
<div class="user-id">ID: ${{user.id}}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}});
|
|
||||||
}} else {{
|
|
||||||
blacklistedUsersHtml = '<div>No blacklisted users</div>';
|
|
||||||
}}
|
|
||||||
document.getElementById('blacklisted-users').innerHTML = blacklistedUsersHtml;
|
|
||||||
}};
|
|
||||||
|
|
||||||
ws.onclose = function() {{
|
|
||||||
console.log('WebSocket disconnected');
|
|
||||||
}};
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
"""
|
|
||||||
|
|
||||||
return web.Response(text=html_content, content_type='text/html')
|
|
||||||
|
|
||||||
async def handle_stats(self, request):
|
|
||||||
return await self.handle_index(request)
|
|
||||||
|
|
||||||
async def handle_json_data(self, request):
|
|
||||||
stats = await self.get_bot_stats()
|
|
||||||
return web.json_response(stats)
|
|
||||||
|
|
||||||
async def setup(bot):
|
|
||||||
await bot.add_cog(GooberWeb(bot))
|
|
Before Width: | Height: | Size: 114 KiB |
Before Width: | Height: | Size: 295 KiB |
Before Width: | Height: | Size: 155 KiB |
Before Width: | Height: | Size: 746 KiB |
Before Width: | Height: | Size: 355 KiB |
Before Width: | Height: | Size: 275 KiB |
Before Width: | Height: | Size: 1.7 MiB |
|
@ -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!",
|
"memory_file_valid": "The memory.json file is valid!",
|
||||||
"file_aint_uft8": "File is not valid UTF-8 text. Might be binary or corrupted.",
|
"file_aint_uft8": "File is not valid UTF-8 text. Might be binary or corrupted.",
|
||||||
"psutil_not_installed": "Memory check skipped.",
|
"psutil_not_installed": "Memory check skipped.",
|
||||||
"not_cloned": "Goober is not cloned! Please clone it from GitHub.",
|
"not_cloned": "Goober is not cloned! Please clone it from Git.",
|
||||||
"checks_disabled": "Checks are disabled!",
|
"checks_disabled": "Checks are disabled!",
|
||||||
"unhandled_exception": "An unhandled exception occurred. Please report this issue on GitHub.",
|
"unhandled_exception": "An unhandled exception occurred. Please report this issue on GitHub.",
|
||||||
"active_users:": "Active users:",
|
"active_users:": "Active users:",
|
||||||
|
|
|
@ -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 l’envoi du ping actif : {error}",
|
|
||||||
"already_started": "J’ai 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 n’est 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": "n’est 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 d’analyser 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 l’usage 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 l’espace.",
|
|
||||||
"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 d’encodage 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
|
@ -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}```"
|
||||||
|
}
|
|
@ -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!",
|
"memory_file_valid": "Il file JSON è valido!",
|
||||||
"file_aint_utf8": "Il file non è un UTF-8 valido. Forse è binario?",
|
"file_aint_utf8": "Il file non è un UTF-8 valido. Forse è binario?",
|
||||||
"psutil_not_installed": "Controllo memoria saltato.",
|
"psutil_not_installed": "Controllo memoria saltato.",
|
||||||
"not_cloned": "Goober non è stato clonato! Clonalo da GitHub.",
|
"not_cloned": "Goober non è stato clonato! Clonalo da Git.",
|
||||||
"checks_disabled": "I controlli sono disabilitati!",
|
"checks_disabled": "I controlli sono disabilitati!",
|
||||||
"unhandled_exception": "Si è verificata un'eccezione non gestita. Segnala questo problema su GitHub, per favore.",
|
"unhandled_exception": "Si è verificata un'eccezione non gestita. Segnala questo problema su GitHub, per favore.",
|
||||||
"active_users:": "Utenti attivi:",
|
"active_users:": "Utenti attivi:",
|
||||||
|
|
513
bot.py
|
@ -1,513 +0,0 @@
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
import random
|
|
||||||
import traceback
|
|
||||||
import subprocess
|
|
||||||
import tempfile
|
|
||||||
import shutil
|
|
||||||
import uuid
|
|
||||||
import asyncio
|
|
||||||
import sys
|
|
||||||
from typing import List, Dict, Set, Optional, Tuple, Any, Union, Callable, Coroutine, TypeVar, Type
|
|
||||||
import logging
|
|
||||||
from modules.globalvars import *
|
|
||||||
from modules.prestartchecks import start_checks
|
|
||||||
from modules.logger import GooberFormatter
|
|
||||||
import logging
|
|
||||||
|
|
||||||
logger = logging.getLogger("goober")
|
|
||||||
logger.setLevel(logging.DEBUG)
|
|
||||||
|
|
||||||
console_handler = logging.StreamHandler()
|
|
||||||
console_handler.setLevel(logging.DEBUG)
|
|
||||||
console_handler.setFormatter(GooberFormatter())
|
|
||||||
|
|
||||||
file_handler = logging.FileHandler("log.txt", mode="w+", encoding="UTF-8")
|
|
||||||
file_handler.setLevel(logging.DEBUG)
|
|
||||||
file_handler.setFormatter(GooberFormatter(colors=False))
|
|
||||||
|
|
||||||
logger.addHandler(console_handler)
|
|
||||||
logger.addHandler(file_handler)
|
|
||||||
|
|
||||||
# Print splash text and check for updates
|
|
||||||
print(splashtext) # Print splash text (from modules/globalvars.py)
|
|
||||||
start_checks()
|
|
||||||
|
|
||||||
import requests
|
|
||||||
import discord
|
|
||||||
from discord.ext import commands
|
|
||||||
from discord import app_commands
|
|
||||||
from discord import Colour, Embed, File, Interaction, Message
|
|
||||||
from discord.abc import Messageable
|
|
||||||
|
|
||||||
from better_profanity import profanity
|
|
||||||
from discord.ext import commands
|
|
||||||
|
|
||||||
from modules.volta.main import _, set_language
|
|
||||||
from modules.markovmemory import *
|
|
||||||
from modules.version import *
|
|
||||||
from modules.sentenceprocessing import *
|
|
||||||
from modules.unhandledexception import handle_exception
|
|
||||||
from modules.image import gen_meme, gen_demotivator
|
|
||||||
|
|
||||||
sys.excepthook = handle_exception
|
|
||||||
check_for_update() # Check for updates (from modules/version.py)
|
|
||||||
|
|
||||||
# Type aliases
|
|
||||||
T = TypeVar('T')
|
|
||||||
MessageContext = Union[commands.Context, discord.Interaction]
|
|
||||||
MessageReference = Union[Message, discord.WebhookMessage]
|
|
||||||
|
|
||||||
# Constants with type hints
|
|
||||||
positive_gifs: List[str] = os.getenv("POSITIVE_GIFS", "").split(',')
|
|
||||||
currenthash: str = ""
|
|
||||||
launched: bool = False
|
|
||||||
slash_commands_enabled: bool = False
|
|
||||||
|
|
||||||
# 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=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] = load_memory()
|
|
||||||
markov_model: Optional[markovify.Text] = load_markov_model()
|
|
||||||
if not markov_model:
|
|
||||||
logger.error(_('markov_model_not_found'))
|
|
||||||
memory = load_memory()
|
|
||||||
markov_model = train_markov_model(memory)
|
|
||||||
|
|
||||||
generated_sentences: Set[str] = set()
|
|
||||||
used_words: Set[str] = set()
|
|
||||||
|
|
||||||
async def load_cogs_from_folder(bot, folder_name="assets/cogs"):
|
|
||||||
for filename in os.listdir(folder_name):
|
|
||||||
if filename.endswith(".py") and not filename.startswith("_"):
|
|
||||||
cog_name = filename[:-3]
|
|
||||||
module_path = folder_name.replace("/", ".").replace("\\", ".") + f".{cog_name}"
|
|
||||||
try:
|
|
||||||
await bot.load_extension(module_path)
|
|
||||||
logger.info(f"{(_('loaded_cog'))} {cog_name}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"{(_('cog_fail'))} {cog_name} {e}")
|
|
||||||
traceback.print_exc()
|
|
||||||
|
|
||||||
async def send_alive_ping_periodically() -> None:
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
requests.post(f"{VERSION_URL}/aliveping", json={"name": NAME})
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"{(_('error_sending_alive_ping'))}{RESET} {e}")
|
|
||||||
await asyncio.sleep(60)
|
|
||||||
|
|
||||||
# Event: Called when the bot is ready
|
|
||||||
@bot.event
|
|
||||||
async def on_ready() -> None:
|
|
||||||
global launched
|
|
||||||
global slash_commands_enabled
|
|
||||||
global NAME
|
|
||||||
|
|
||||||
folder_name: str = "cogs"
|
|
||||||
if launched:
|
|
||||||
return
|
|
||||||
|
|
||||||
await load_cogs_from_folder(bot)
|
|
||||||
try:
|
|
||||||
synced: List[discord.app_commands.AppCommand] = await bot.tree.sync()
|
|
||||||
logger.info(f"{_('synced_commands')} {len(synced)} {(_('synced_commands2'))}")
|
|
||||||
slash_commands_enabled = True
|
|
||||||
logger.info(f"{(_('started')).format(name=NAME)}")
|
|
||||||
|
|
||||||
bot.loop.create_task(send_alive_ping_periodically())
|
|
||||||
except discord.errors.Forbidden as perm_error:
|
|
||||||
logger.error(f"Permission error while syncing commands: {perm_error}")
|
|
||||||
logger.error("Make sure the bot has the 'applications.commands' scope and is invited with the correct permissions.")
|
|
||||||
quit()
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"{_('fail_commands_sync')} {e}")
|
|
||||||
traceback.print_exc()
|
|
||||||
quit()
|
|
||||||
|
|
||||||
if not song:
|
|
||||||
return
|
|
||||||
await bot.change_presence(activity=discord.Activity(type=discord.ActivityType.listening, name=f"{song}"))
|
|
||||||
launched = True
|
|
||||||
|
|
||||||
@bot.event
|
|
||||||
async def on_command_error(ctx: commands.Context, error: commands.CommandError) -> None:
|
|
||||||
from modules.unhandledexception import handle_exception
|
|
||||||
|
|
||||||
if isinstance(error, commands.CommandInvokeError):
|
|
||||||
original: Exception = error.original
|
|
||||||
handle_exception(
|
|
||||||
type(original), original, original.__traceback__,
|
|
||||||
context=f"Command: {ctx.command} | User: {ctx.author}"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
handle_exception(
|
|
||||||
type(error), error, error.__traceback__,
|
|
||||||
context=f"Command: {ctx.command} | User: {ctx.author}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Command: Retrain the Markov model from memory
|
|
||||||
@bot.hybrid_command(description=f"{(_('command_desc_retrain'))}")
|
|
||||||
async def retrain(ctx: commands.Context) -> None:
|
|
||||||
if ctx.author.id != ownerid:
|
|
||||||
return
|
|
||||||
|
|
||||||
message_ref: MessageReference = await send_message(ctx, f"{(_('command_markov_retrain'))}")
|
|
||||||
try:
|
|
||||||
with open(MEMORY_FILE, 'r') as f:
|
|
||||||
memory: List[str] = json.load(f)
|
|
||||||
except FileNotFoundError:
|
|
||||||
await send_message(ctx, f"{(_('command_markov_memory_not_found'))}")
|
|
||||||
return
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
await send_message(ctx, f"{(_('command_markov_memory_is_corrupt'))}")
|
|
||||||
return
|
|
||||||
|
|
||||||
data_size: int = len(memory)
|
|
||||||
processed_data: int = 0
|
|
||||||
processing_message_ref: MessageReference = await send_message(ctx, f"{(_('command_markov_retraining')).format(processed_data=processed_data, data_size=data_size)}")
|
|
||||||
start_time: float = time.time()
|
|
||||||
|
|
||||||
for i, data in enumerate(memory):
|
|
||||||
processed_data += 1
|
|
||||||
|
|
||||||
global markov_model
|
|
||||||
markov_model = train_markov_model(memory)
|
|
||||||
save_markov_model(markov_model)
|
|
||||||
|
|
||||||
await send_message(ctx, f"{_('command_markov_retrain_successful').format(data_size=data_size)}", edit=True, message_reference=processing_message_ref)
|
|
||||||
|
|
||||||
# Command: Generate a sentence using the Markov model
|
|
||||||
@bot.hybrid_command(description=f"{(_('command_desc_talk'))}")
|
|
||||||
async def talk(ctx: commands.Context, sentence_size: int = 5) -> None:
|
|
||||||
if not markov_model:
|
|
||||||
await send_message(ctx, f"{(_('command_talk_insufficent_text'))}")
|
|
||||||
return
|
|
||||||
|
|
||||||
response: Optional[str] = None
|
|
||||||
for _ in range(20):
|
|
||||||
if sentence_size == 1:
|
|
||||||
response = markov_model.make_short_sentence(max_chars=100, tries=100)
|
|
||||||
if response:
|
|
||||||
response = response.split()[0]
|
|
||||||
else:
|
|
||||||
response = markov_model.make_sentence(tries=100, max_words=sentence_size)
|
|
||||||
|
|
||||||
if response and response not in generated_sentences:
|
|
||||||
if sentence_size > 1:
|
|
||||||
response = improve_sentence_coherence(response)
|
|
||||||
generated_sentences.add(response)
|
|
||||||
break
|
|
||||||
|
|
||||||
if response:
|
|
||||||
cleaned_response: str = re.sub(r'[^\w\s]', '', response).lower()
|
|
||||||
coherent_response: str = rephrase_for_coherence(cleaned_response)
|
|
||||||
if random.random() < 0.9 and is_positive(coherent_response):
|
|
||||||
gif_url: str = random.choice(positive_gifs)
|
|
||||||
combined_message: str = f"{coherent_response}\n[jif]({gif_url})"
|
|
||||||
else:
|
|
||||||
combined_message: str = coherent_response
|
|
||||||
logger.info(combined_message)
|
|
||||||
os.environ['gooberlatestgen'] = combined_message
|
|
||||||
await send_message(ctx, combined_message)
|
|
||||||
else:
|
|
||||||
await send_message(ctx, f"{(_('command_talk_generation_fail'))}")
|
|
||||||
|
|
||||||
# Command: Generate an image
|
|
||||||
@bot.hybrid_command(description=f"{(_('command_desc_help'))}")
|
|
||||||
async def impact(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_meme(input_path)
|
|
||||||
|
|
||||||
if output_path is None or not os.path.isfile(output_path):
|
|
||||||
if temp_input and os.path.exists(temp_input):
|
|
||||||
os.remove(temp_input)
|
|
||||||
await ctx.reply(_('failed_generate_image'))
|
|
||||||
return
|
|
||||||
|
|
||||||
await ctx.send(file=discord.File(output_path))
|
|
||||||
|
|
||||||
if temp_input and os.path.exists(temp_input):
|
|
||||||
os.remove(temp_input)
|
|
||||||
|
|
||||||
# New demotivator command
|
|
||||||
@bot.hybrid_command(description="Generate a demotivator poster with two lines of text")
|
|
||||||
async def demotivator(ctx: commands.Context) -> None:
|
|
||||||
assets_folder: str = "assets/images"
|
|
||||||
temp_input: Optional[str] = None
|
|
||||||
|
|
||||||
def get_random_asset_image() -> Optional[str]:
|
|
||||||
files: List[str] = [f for f in os.listdir(assets_folder) if f.lower().endswith(('.png', '.jpg', '.jpeg', '.webp'))]
|
|
||||||
if not files:
|
|
||||||
return None
|
|
||||||
return os.path.join(assets_folder, random.choice(files))
|
|
||||||
|
|
||||||
if ctx.message.attachments:
|
|
||||||
attachment: discord.Attachment = ctx.message.attachments[0]
|
|
||||||
if attachment.content_type and attachment.content_type.startswith("image/"):
|
|
||||||
ext: str = os.path.splitext(attachment.filename)[1]
|
|
||||||
temp_input = f"tempy{ext}"
|
|
||||||
await attachment.save(temp_input)
|
|
||||||
input_path: str = temp_input
|
|
||||||
else:
|
|
||||||
fallback_image: Optional[str] = get_random_asset_image()
|
|
||||||
if fallback_image is None:
|
|
||||||
await ctx.reply(_('no_image_available'))
|
|
||||||
return
|
|
||||||
temp_input = tempfile.mktemp(suffix=os.path.splitext(fallback_image)[1])
|
|
||||||
shutil.copy(fallback_image, temp_input)
|
|
||||||
input_path = temp_input
|
|
||||||
else:
|
|
||||||
fallback_image = get_random_asset_image()
|
|
||||||
if fallback_image is None:
|
|
||||||
await ctx.reply(_('no_image_available'))
|
|
||||||
return
|
|
||||||
temp_input = tempfile.mktemp(suffix=os.path.splitext(fallback_image)[1])
|
|
||||||
shutil.copy(fallback_image, temp_input)
|
|
||||||
input_path = temp_input
|
|
||||||
|
|
||||||
output_path: Optional[str] = await gen_demotivator(input_path)
|
|
||||||
|
|
||||||
if output_path is None or not os.path.isfile(output_path):
|
|
||||||
if temp_input and os.path.exists(temp_input):
|
|
||||||
os.remove(temp_input)
|
|
||||||
await ctx.reply("Failed to generate demotivator.")
|
|
||||||
return
|
|
||||||
|
|
||||||
await ctx.send(file=discord.File(output_path))
|
|
||||||
|
|
||||||
if temp_input and os.path.exists(temp_input):
|
|
||||||
os.remove(temp_input)
|
|
||||||
|
|
||||||
bot.remove_command('help')
|
|
||||||
# Command: Show help information
|
|
||||||
@bot.hybrid_command(description=f"{(_('command_desc_help'))}")
|
|
||||||
async def help(ctx: commands.Context) -> None:
|
|
||||||
embed: discord.Embed = discord.Embed(
|
|
||||||
title=f"{(_('command_help_embed_title'))}",
|
|
||||||
description=f"{(_('command_help_embed_desc'))}",
|
|
||||||
color=Colour(0x000000)
|
|
||||||
)
|
|
||||||
|
|
||||||
command_categories: Dict[str, List[str]] = {
|
|
||||||
f"{(_('command_help_categories_general'))}": ["mem", "talk", "about", "ping", "impact", "demotivator", "help"],
|
|
||||||
f"{(_('command_help_categories_admin'))}": ["stats", "retrain", "setlanguage"]
|
|
||||||
}
|
|
||||||
|
|
||||||
custom_commands: List[str] = []
|
|
||||||
for cog_name, cog in bot.cogs.items():
|
|
||||||
for command in cog.get_commands():
|
|
||||||
if command.name not in command_categories[f"{(_('command_help_categories_general'))}"] and command.name not in command_categories[f"{(_('command_help_categories_admin'))}"]:
|
|
||||||
custom_commands.append(command.name)
|
|
||||||
|
|
||||||
if custom_commands:
|
|
||||||
embed.add_field(name=f"{(_('command_help_categories_custom'))}", value="\n".join([f"{PREFIX}{command}" for command in custom_commands]), inline=False)
|
|
||||||
|
|
||||||
for category, commands_list in command_categories.items():
|
|
||||||
commands_in_category: str = "\n".join([f"{PREFIX}{command}" for command in commands_list])
|
|
||||||
embed.add_field(name=category, value=commands_in_category, inline=False)
|
|
||||||
|
|
||||||
await send_message(ctx, embed=embed)
|
|
||||||
|
|
||||||
@bot.hybrid_command(description=f"{(_('command_desc_setlang'))}")
|
|
||||||
@app_commands.describe(locale="Choose your language")
|
|
||||||
async def setlanguage(ctx: commands.Context, locale: str) -> None:
|
|
||||||
if ctx.author.id != ownerid:
|
|
||||||
await ctx.send(":thumbsdown:")
|
|
||||||
return
|
|
||||||
await ctx.defer()
|
|
||||||
set_language(locale)
|
|
||||||
await ctx.send(":thumbsup:")
|
|
||||||
|
|
||||||
# Event: Called on every message
|
|
||||||
@bot.event
|
|
||||||
async def on_message(message: discord.Message) -> None:
|
|
||||||
global memory, markov_model
|
|
||||||
EMOJIS = ["\U0001F604", "\U0001F44D", "\U0001F525", "\U0001F4AF", "\U0001F389", "\U0001F60E"] # originally was emojis but it would probably shit itself on systems without unicode so....
|
|
||||||
if message.author.bot:
|
|
||||||
return
|
|
||||||
|
|
||||||
if str(message.author.id) in BLACKLISTED_USERS:
|
|
||||||
return
|
|
||||||
|
|
||||||
if message.content.startswith((f"{PREFIX}talk", f"{PREFIX}mem", f"{PREFIX}help", f"{PREFIX}stats", f"{PREFIX}")):
|
|
||||||
logger.info(f"{(_('command_ran')).format(message=message)}")
|
|
||||||
await bot.process_commands(message)
|
|
||||||
return
|
|
||||||
|
|
||||||
if profanity.contains_profanity(message.content):
|
|
||||||
return
|
|
||||||
|
|
||||||
if message.content:
|
|
||||||
if not USERTRAIN_ENABLED:
|
|
||||||
return
|
|
||||||
formatted_message: str = append_mentions_to_18digit_integer(message.content)
|
|
||||||
cleaned_message: str = preprocess_message(formatted_message)
|
|
||||||
if cleaned_message:
|
|
||||||
memory.append(cleaned_message)
|
|
||||||
message_metadata = {
|
|
||||||
"user_id": str(message.author.id),
|
|
||||||
"user_name": str(message.author),
|
|
||||||
"guild_id": str(message.guild.id) if message.guild else "DM",
|
|
||||||
"guild_name": str(message.guild.name) if message.guild else "DM",
|
|
||||||
"channel_id": str(message.channel.id),
|
|
||||||
"channel_name": str(message.channel),
|
|
||||||
"message": message.content,
|
|
||||||
"timestamp": time.time()
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
if isinstance(memory, list):
|
|
||||||
memory.append({"_meta": message_metadata})
|
|
||||||
else:
|
|
||||||
logger.warning("Memory is not a list; can't append metadata")
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Failed to append metadata to memory: {e}")
|
|
||||||
|
|
||||||
save_memory(memory)
|
|
||||||
|
|
||||||
sentiment_score = is_positive(message.content) # doesnt work but im scared to change the logic now please ignore
|
|
||||||
if sentiment_score > 0.8:
|
|
||||||
if REACT != "True":
|
|
||||||
return
|
|
||||||
emoji = random.choice(EMOJIS)
|
|
||||||
try:
|
|
||||||
await message.add_reaction(emoji)
|
|
||||||
except Exception as e:
|
|
||||||
logger.info(f"Failed to react with emoji: {e}")
|
|
||||||
|
|
||||||
await bot.process_commands(message)
|
|
||||||
|
|
||||||
# Event: Called on every interaction (slash command, etc.)
|
|
||||||
@bot.event
|
|
||||||
async def on_interaction(interaction: discord.Interaction) -> None:
|
|
||||||
logger.info(f"{(_('command_ran_s')).format(interaction=interaction)}{interaction.data['name']}")
|
|
||||||
|
|
||||||
# Global check: Block blacklisted users from running commands
|
|
||||||
@bot.check
|
|
||||||
async def block_blacklisted(ctx: commands.Context) -> bool:
|
|
||||||
if str(ctx.author.id) in BLACKLISTED_USERS:
|
|
||||||
try:
|
|
||||||
if isinstance(ctx, discord.Interaction):
|
|
||||||
if not ctx.response.is_done():
|
|
||||||
await ctx.response.send_message(_('blacklisted'), ephemeral=True)
|
|
||||||
else:
|
|
||||||
await ctx.followup.send(_('blacklisted'), ephemeral=True)
|
|
||||||
else:
|
|
||||||
await ctx.send(_('blacklisted_user'), ephemeral=True)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Command: Show bot latency
|
|
||||||
@bot.hybrid_command(description=f"{(_('command_desc_ping'))}")
|
|
||||||
async def ping(ctx: commands.Context) -> None:
|
|
||||||
await ctx.defer()
|
|
||||||
latency: int = round(bot.latency * 1000)
|
|
||||||
|
|
||||||
LOLembed: discord.Embed = discord.Embed(
|
|
||||||
title="Pong!!",
|
|
||||||
description=(
|
|
||||||
f"{PING_LINE}\n"
|
|
||||||
f"`{(_('command_ping_embed_desc'))}: {latency}ms`\n"
|
|
||||||
),
|
|
||||||
color=Colour(0x000000)
|
|
||||||
)
|
|
||||||
LOLembed.set_footer(text=f"{(_('command_ping_footer'))} {ctx.author.name}", icon_url=ctx.author.avatar.url)
|
|
||||||
|
|
||||||
await ctx.send(embed=LOLembed)
|
|
||||||
|
|
||||||
# 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"Github", value=f"https://github.com/gooberinc/goober")
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
await send_message(ctx, embed=embed)
|
|
||||||
|
|
||||||
# Command: Upload memory.json to litterbox.catbox.moe and return the link
|
|
||||||
@bot.hybrid_command()
|
|
||||||
async def mem(ctx: commands.Context) -> None:
|
|
||||||
if showmemenabled != "true":
|
|
||||||
return
|
|
||||||
command: str = """curl -F "reqtype=fileupload" -F "time=1h" -F "fileToUpload=@memory.json" https://litterbox.catbox.moe/resources/internals/api.php"""
|
|
||||||
memorylitter: subprocess.CompletedProcess = subprocess.run(command, shell=True, capture_output=True, text=True)
|
|
||||||
logger.debug(memorylitter)
|
|
||||||
await send_message(ctx, memorylitter.stdout.strip())
|
|
||||||
|
|
||||||
# Helper: Improve sentence coherence (simple capitalization fix)
|
|
||||||
def improve_sentence_coherence(sentence: str) -> str:
|
|
||||||
# Capitalizes "i" to "I" in the sentence
|
|
||||||
sentence = sentence.replace(" i ", " I ")
|
|
||||||
return sentence
|
|
||||||
|
|
||||||
# Start the bot
|
|
||||||
bot.run(TOKEN)
|
|
29
example.env
|
@ -1,28 +1 @@
|
||||||
DISCORDBOTTOKEN=
|
DISCORDBOTTOKEN=
|
||||||
BOTPREFIX="g."
|
|
||||||
PINGLINE="The Beretta fires fast and won't make you feel any better!"
|
|
||||||
BLACKLISTEDUSERS=
|
|
||||||
OWNERID=
|
|
||||||
USERTRAINENABLED="true"
|
|
||||||
SHOWMEMENABLED="true"
|
|
||||||
LOCALE=fi
|
|
||||||
NAME=goober
|
|
||||||
AUTOUPDATE="True"
|
|
||||||
SONG="Basket Case - Green Day"
|
|
||||||
CHECKSDISABLED="Frue"
|
|
||||||
REACT="True"
|
|
||||||
POSITIVEGIFS="https://media.discordapp.net/attachments/821047460151427135/1181371808566493184/jjpQGeno.gif, https://tenor.com/view/chill-guy-my-new-character-gif-2777893510283028272,https://tenor.com/view/goodnight-goodnight-friends-weezer-weezer-goodnight-gif-7322052181075806988"
|
|
||||||
SPLASHTEXT="
|
|
||||||
|
|
||||||
SS\
|
|
||||||
SS |
|
|
||||||
SSSSSS\ SSSSSS\ SSSSSS\ SSSSSSS\ SSSSSS\ SSSSSS\
|
|
||||||
SS __SS\ SS __SS\ SS __SS\ SS __SS\ SS __SS\ SS __SS\
|
|
||||||
SS / SS |SS / SS |SS / SS |SS | SS |SSSSSSSS |SS | \__|
|
|
||||||
SS | SS |SS | SS |SS | SS |SS | SS |SS ____|SS |
|
|
||||||
\SSSSSSS |\SSSSSS |\SSSSSS |SSSSSSS |\SSSSSSS\ SS |
|
|
||||||
\____SS | \______/ \______/ \_______/ \_______|\__|
|
|
||||||
SS\ SS |
|
|
||||||
\SSSSSS |
|
|
||||||
\______/
|
|
||||||
"
|
|
302
main.py
Normal file
|
@ -0,0 +1,302 @@
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import random
|
||||||
|
import traceback
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
from typing import (
|
||||||
|
List,
|
||||||
|
Dict,
|
||||||
|
Literal,
|
||||||
|
Set,
|
||||||
|
Optional,
|
||||||
|
Tuple,
|
||||||
|
Any,
|
||||||
|
TypedDict,
|
||||||
|
Union,
|
||||||
|
Callable,
|
||||||
|
Coroutine,
|
||||||
|
TypeVar,
|
||||||
|
Type,
|
||||||
|
)
|
||||||
|
import logging
|
||||||
|
from modules.prestartchecks import start_checks
|
||||||
|
from modules.logger import GooberFormatter
|
||||||
|
from modules.volta.main import *
|
||||||
|
import logging
|
||||||
|
from modules.settings import Settings as SettingsManager
|
||||||
|
from modules.permission import requires_admin
|
||||||
|
from modules.volta.main import _
|
||||||
|
from modules.version import check_for_update
|
||||||
|
check_for_update()
|
||||||
|
logger = logging.getLogger("goober")
|
||||||
|
logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
console_handler = logging.StreamHandler()
|
||||||
|
console_handler.setLevel(logging.DEBUG)
|
||||||
|
console_handler.setFormatter(GooberFormatter())
|
||||||
|
|
||||||
|
file_handler = logging.FileHandler("log.txt", mode="w+", encoding="UTF-8")
|
||||||
|
file_handler.setLevel(logging.DEBUG)
|
||||||
|
file_handler.setFormatter(GooberFormatter(colors=False))
|
||||||
|
|
||||||
|
logger.addHandler(console_handler)
|
||||||
|
logger.addHandler(file_handler)
|
||||||
|
|
||||||
|
settings_manager = SettingsManager()
|
||||||
|
settings = settings_manager.settings
|
||||||
|
|
||||||
|
splash_text: str = ""
|
||||||
|
|
||||||
|
with open(settings["splash_text_loc"], "r", encoding="UTF-8") as f:
|
||||||
|
splash_text = "".join(f.readlines())
|
||||||
|
print(splash_text)
|
||||||
|
|
||||||
|
start_checks()
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord.ext import commands
|
||||||
|
from discord import app_commands
|
||||||
|
from discord import Colour, Message
|
||||||
|
|
||||||
|
from better_profanity import profanity
|
||||||
|
from discord.ext import commands
|
||||||
|
|
||||||
|
from modules.markovmemory import *
|
||||||
|
from modules.sentenceprocessing import *
|
||||||
|
from modules.unhandledexception import handle_exception
|
||||||
|
|
||||||
|
sys.excepthook = handle_exception
|
||||||
|
|
||||||
|
|
||||||
|
class MessageMetadata(TypedDict):
|
||||||
|
user_id: str
|
||||||
|
user_name: str
|
||||||
|
guild_id: str | Literal["DM"]
|
||||||
|
guild_name: str | Literal["DM"]
|
||||||
|
channel_id: str
|
||||||
|
channel_name: str
|
||||||
|
message: str
|
||||||
|
timestamp: float
|
||||||
|
|
||||||
|
|
||||||
|
# Constants with type hints
|
||||||
|
positive_gifs: List[str] = settings["bot"]["misc"]["positive_gifs"]
|
||||||
|
name = settings["name"]
|
||||||
|
currenthash: str = ""
|
||||||
|
launched: bool = False
|
||||||
|
slash_commands_enabled: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
# Set up Discord bot intents and create bot instance
|
||||||
|
intents: discord.Intents = discord.Intents.default()
|
||||||
|
intents.messages = True
|
||||||
|
intents.presences = True
|
||||||
|
intents.members = True
|
||||||
|
intents.message_content = True
|
||||||
|
bot: commands.Bot = commands.Bot(
|
||||||
|
command_prefix=settings["bot"]["prefix"],
|
||||||
|
intents=intents,
|
||||||
|
allowed_mentions=discord.AllowedMentions(
|
||||||
|
everyone=False, roles=False, users=False, replied_user=True
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Load memory and Markov model for text generation
|
||||||
|
memory: List[str | Dict[Literal["_meta"], MessageMetadata]] = load_memory()
|
||||||
|
|
||||||
|
generated_sentences: Set[str] = set()
|
||||||
|
used_words: Set[str] = set()
|
||||||
|
|
||||||
|
|
||||||
|
async def load_cogs_from_folder(bot: commands.Bot, folder_name="assets/cogs"):
|
||||||
|
for filename in [file for file in os.listdir(folder_name) if file.endswith(".py")]:
|
||||||
|
cog_name: str = filename[:-3]
|
||||||
|
|
||||||
|
if (
|
||||||
|
"internal" not in folder_name
|
||||||
|
and cog_name not in settings["bot"]["enabled_cogs"]
|
||||||
|
):
|
||||||
|
logger.debug(f"Skipping cog {cog_name} (not in enabled cogs)")
|
||||||
|
continue
|
||||||
|
|
||||||
|
module_path = folder_name.replace("/", ".").replace("\\", ".") + f".{cog_name}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
await bot.load_extension(module_path)
|
||||||
|
logger.info(f"{_('loaded_cog')} {cog_name}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"{_('cog_fail')} {cog_name} {e}")
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
|
||||||
|
# Event: Called when the bot is ready
|
||||||
|
@bot.event
|
||||||
|
async def on_ready() -> None:
|
||||||
|
global launched
|
||||||
|
|
||||||
|
folder_name: str = "cogs"
|
||||||
|
if launched:
|
||||||
|
return
|
||||||
|
|
||||||
|
await load_cogs_from_folder(bot)
|
||||||
|
await load_cogs_from_folder(bot, "assets/cogs/internal")
|
||||||
|
try:
|
||||||
|
synced: List[discord.app_commands.AppCommand] = await bot.tree.sync()
|
||||||
|
|
||||||
|
logger.info(f"{_('synced_commands')} {len(synced)} {_('synced_commands2')}")
|
||||||
|
logger.info(_('started').format(name=name))
|
||||||
|
|
||||||
|
except discord.errors.Forbidden as perm_error:
|
||||||
|
logger.error(f"Permission error while syncing commands: {perm_error}")
|
||||||
|
logger.error(
|
||||||
|
"Make sure the bot has the 'applications.commands' scope and is invited with the correct permissions."
|
||||||
|
)
|
||||||
|
quit()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"{_('fail_commands_sync')} {e}")
|
||||||
|
traceback.print_exc()
|
||||||
|
quit()
|
||||||
|
|
||||||
|
if not settings["bot"]["misc"]["active_song"]:
|
||||||
|
return
|
||||||
|
await bot.change_presence(
|
||||||
|
activity=discord.Activity(
|
||||||
|
type=discord.ActivityType.listening,
|
||||||
|
name=settings["bot"]["misc"]["active_song"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
launched = True
|
||||||
|
|
||||||
|
bot.remove_command('help')
|
||||||
|
|
||||||
|
@bot.event
|
||||||
|
async def on_command_error(ctx: commands.Context, error: commands.CommandError) -> None:
|
||||||
|
from modules.unhandledexception import handle_exception
|
||||||
|
|
||||||
|
if isinstance(error, commands.CommandInvokeError):
|
||||||
|
original: Exception = error.original
|
||||||
|
handle_exception(
|
||||||
|
type(original),
|
||||||
|
original,
|
||||||
|
original.__traceback__,
|
||||||
|
context=f"Command: {ctx.command} | User: {ctx.author}",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
handle_exception(
|
||||||
|
type(error),
|
||||||
|
error,
|
||||||
|
error.__traceback__,
|
||||||
|
context=f"Command: {ctx.command} | User: {ctx.author}",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Event: Called on every message
|
||||||
|
@bot.event
|
||||||
|
async def on_message(message: discord.Message) -> None:
|
||||||
|
global memory
|
||||||
|
EMOJIS = [
|
||||||
|
"\U0001f604",
|
||||||
|
"\U0001f44d",
|
||||||
|
"\U0001f525",
|
||||||
|
"\U0001f4af",
|
||||||
|
"\U0001f389",
|
||||||
|
"\U0001f60e",
|
||||||
|
] # originally was emojis but it would probably shit itself on systems without unicode so....
|
||||||
|
if message.author.bot:
|
||||||
|
return
|
||||||
|
|
||||||
|
if str(message.author.id) in settings["bot"]["blacklisted_users"]:
|
||||||
|
return
|
||||||
|
|
||||||
|
commands = [
|
||||||
|
settings["bot"]["prefix"] + command.name for command in bot.tree.get_commands()
|
||||||
|
]
|
||||||
|
|
||||||
|
if message.content.startswith(tuple(commands)):
|
||||||
|
logger.info(f"{(_('command_ran')).format(message=message)}")
|
||||||
|
await bot.process_commands(message)
|
||||||
|
return
|
||||||
|
|
||||||
|
if message.content:
|
||||||
|
if not settings["bot"]["user_training"]:
|
||||||
|
return
|
||||||
|
|
||||||
|
formatted_message: str = message.content
|
||||||
|
cleaned_message: str = preprocess_message(formatted_message)
|
||||||
|
if cleaned_message:
|
||||||
|
memory.append(cleaned_message)
|
||||||
|
|
||||||
|
message_metadata: MessageMetadata = {
|
||||||
|
"user_id": str(message.author.id),
|
||||||
|
"user_name": str(message.author),
|
||||||
|
"guild_id": str(message.guild.id) if message.guild else "DM",
|
||||||
|
"guild_name": str(message.guild.name) if message.guild else "DM",
|
||||||
|
"channel_id": str(message.channel.id),
|
||||||
|
"channel_name": str(message.channel),
|
||||||
|
"message": message.content,
|
||||||
|
"timestamp": time.time(),
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
if isinstance(memory, list):
|
||||||
|
memory.append({"_meta": message_metadata})
|
||||||
|
else:
|
||||||
|
logger.warning("Memory is not a list; can't append metadata")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to append metadata to memory: {e}")
|
||||||
|
|
||||||
|
save_memory(memory)
|
||||||
|
|
||||||
|
sentiment_score = is_positive(
|
||||||
|
message.content
|
||||||
|
) # doesnt work but im scared to change the logic now please ignore
|
||||||
|
if sentiment_score > 0.8:
|
||||||
|
if not settings["bot"]["react_to_messages"]:
|
||||||
|
return
|
||||||
|
emoji = random.choice(EMOJIS)
|
||||||
|
try:
|
||||||
|
await message.add_reaction(emoji)
|
||||||
|
except Exception as e:
|
||||||
|
logger.info(f"Failed to react with emoji: {e}")
|
||||||
|
|
||||||
|
await bot.process_commands(message)
|
||||||
|
|
||||||
|
|
||||||
|
# Event: Called on every interaction (slash command, etc.)
|
||||||
|
@bot.event
|
||||||
|
async def on_interaction(interaction: discord.Interaction) -> None:
|
||||||
|
logger.info(f"{(_('command_ran_s')).format(interaction=interaction)}{name}")
|
||||||
|
|
||||||
|
|
||||||
|
# Global check: Block blacklisted users from running commands
|
||||||
|
@bot.check
|
||||||
|
async def block_blacklisted(ctx: commands.Context) -> bool:
|
||||||
|
if ctx.author.id not in settings["bot"]["blacklisted_users"]:
|
||||||
|
return True
|
||||||
|
|
||||||
|
try:
|
||||||
|
if isinstance(ctx, discord.Interaction):
|
||||||
|
if not ctx.response.is_done():
|
||||||
|
await ctx.response.send_message(_('blacklisted'), ephemeral=True)
|
||||||
|
else:
|
||||||
|
await ctx.followup.send(_('blacklisted'), ephemeral=True)
|
||||||
|
else:
|
||||||
|
await ctx.send(_('blacklisted_user'), ephemeral=True)
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
# Helper: Improve sentence coherence (simple capitalization fix)
|
||||||
|
def improve_sentence_coherence(sentence: str) -> str:
|
||||||
|
# Capitalizes "i" to "I" in the sentence
|
||||||
|
sentence = sentence.replace(" i ", " I ")
|
||||||
|
return sentence
|
||||||
|
|
||||||
|
# Start the bot
|
||||||
|
if __name__ == "__main__":
|
||||||
|
bot.run(os.environ.get("DISCORDBOTTOKEN", ""))
|
|
@ -1,59 +1,71 @@
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
|
from typing import Callable, List
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
import pathlib
|
import pathlib
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
|
|
||||||
def get_git_branch():
|
def get_git_branch():
|
||||||
try:
|
try:
|
||||||
branch = subprocess.check_output(
|
branch = (
|
||||||
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
subprocess.check_output(
|
||||||
stderr=subprocess.DEVNULL
|
["git", "rev-parse", "--abbrev-ref", "HEAD"], stderr=subprocess.DEVNULL
|
||||||
).decode('utf-8').strip()
|
)
|
||||||
|
.decode("utf-8")
|
||||||
|
.strip()
|
||||||
|
)
|
||||||
return branch
|
return branch
|
||||||
except subprocess.CalledProcessError:
|
except subprocess.CalledProcessError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
env_path = pathlib.Path(__file__).parent.parent / '.env'
|
|
||||||
|
env_path = pathlib.Path(__file__).parent.parent / ".env"
|
||||||
load_dotenv(dotenv_path=env_path)
|
load_dotenv(dotenv_path=env_path)
|
||||||
|
|
||||||
|
available_cogs: Callable[[], List[str]] = lambda: [
|
||||||
|
file[:-3] for file in os.listdir("assets/cogs") if file.endswith(".py")
|
||||||
|
]
|
||||||
|
|
||||||
ANSI = "\033["
|
ANSI = "\033["
|
||||||
RED = f"{ANSI}31m"
|
RED = f"{ANSI}31m"
|
||||||
GREEN = f"{ANSI}32m"
|
GREEN = f"{ANSI}32m"
|
||||||
YELLOW = f"{ANSI}33m"
|
YELLOW = f"{ANSI}33m"
|
||||||
PURPLE = f"{ANSI}35m"
|
PURPLE = f"{ANSI}35m"
|
||||||
DEBUG = f"{ANSI}1;30m"
|
DEBUG = f"{ANSI}90m"
|
||||||
RESET = f"{ANSI}0m"
|
RESET = f"{ANSI}0m"
|
||||||
|
|
||||||
VERSION_URL = "https://raw.githubusercontent.com/gooberinc/version/main"
|
VERSION_URL = "https://raw.githubusercontent.com/gooberinc/version/main"
|
||||||
UPDATE_URL = VERSION_URL+"/latest_version.json"
|
UPDATE_URL = VERSION_URL + "/latest_version.json"
|
||||||
print(UPDATE_URL)
|
print(UPDATE_URL)
|
||||||
LOCAL_VERSION_FILE = "current_version.txt"
|
LOCAL_VERSION_FILE = "current_version.txt"
|
||||||
TOKEN = os.getenv("DISCORDBOTTOKEN", "0")
|
|
||||||
PREFIX = os.getenv("BOTPREFIX", "g.")
|
# TOKEN = os.getenv("DISCORDBOTTOKEN", "0")
|
||||||
PING_LINE = os.getenv("PINGLINE")
|
# PREFIX = os.getenv("BOTPREFIX", "g.")
|
||||||
CHECKS_DISABLED = os.getenv("CHECKSDISABLED")
|
# PING_LINE = os.getenv("PINGLINE")
|
||||||
LOCALE = os.getenv("LOCALE", "en")
|
# CHECKS_DISABLED = os.getenv("CHECKSDISABLED")
|
||||||
gooberTOKEN = os.getenv("GOOBERTOKEN")
|
# LOCALE = os.getenv("LOCALE", "en")
|
||||||
splashtext = os.getenv("SPLASHTEXT")
|
# BLACKLISTED_USERS = os.getenv("BLACKLISTEDUSERS", "").split(",")
|
||||||
ownerid = int(os.getenv("OWNERID", "0"))
|
# USERTRAIN_ENABLED = os.getenv("USERTRAINENABLED", "true").lower() == "true"
|
||||||
showmemenabled = os.getenv("SHOWMEMENABLED")
|
# NAME = os.getenv("NAME")
|
||||||
BLACKLISTED_USERS = os.getenv("BLACKLISTEDUSERS", "").split(",")
|
# MEMORY_FILE = "memory.json"
|
||||||
USERTRAIN_ENABLED = os.getenv("USERTRAINENABLED", "true").lower() == "true"
|
# MEMORY_LOADED_FILE = "MEMORY_LOADED" # is this still even used?? okay just checked its used in the markov module
|
||||||
NAME = os.getenv("NAME")
|
# ALIVEPING = os.getenv("ALIVEPING")
|
||||||
MEMORY_FILE = "memory.json"
|
# AUTOUPDATE = os.getenv("AUTOUPDATE")
|
||||||
MEMORY_LOADED_FILE = "MEMORY_LOADED" # is this still even used?? okay just checked its used in the markov module
|
# REACT = os.getenv("REACT")
|
||||||
ALIVEPING = os.getenv("ALIVEPING")
|
|
||||||
AUTOUPDATE = os.getenv("AUTOUPDATE")
|
# 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?
|
# IGNOREWARNING = False # is this either??? i don't think so?
|
||||||
song = os.getenv("song")
|
# song = os.getenv("song")
|
||||||
arch = platform.machine()
|
arch = platform.machine()
|
||||||
slash_commands_enabled = True # 100% broken, its a newer enough version so its probably enabled by default.... fix this at somepoint or hard code it in goober central code
|
slash_commands_enabled = True # 100% broken, its a newer enough version so its probably enabled by default.... fix this at somepoint or hard code it in goober central code
|
||||||
launched = False
|
launched = False
|
||||||
latest_version = "0.0.0"
|
latest_version = "0.0.0"
|
||||||
local_version = "2.3.3"
|
local_version = "3.0.0"
|
||||||
os.environ['gooberlocal_version'] = local_version
|
os.environ["gooberlocal_version"] = local_version
|
||||||
REACT = os.getenv("REACT")
|
beta = get_git_branch() == "dev"
|
||||||
if get_git_branch() == "dev":
|
|
||||||
beta = True
|
|
||||||
# this makes goober think its a beta version, so it will not update to the latest stable version or run any version checks
|
|
||||||
else:
|
|
||||||
beta = False
|
|
174
modules/image.py
|
@ -1,174 +0,0 @@
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import random
|
|
||||||
import shutil
|
|
||||||
import tempfile
|
|
||||||
from typing import Optional, List
|
|
||||||
from PIL import Image, ImageDraw, ImageFont, ImageOps
|
|
||||||
from modules.markovmemory import load_markov_model
|
|
||||||
from modules.sentenceprocessing import improve_sentence_coherence, rephrase_for_coherence
|
|
||||||
|
|
||||||
generated_sentences = set()
|
|
||||||
|
|
||||||
def load_font(size):
|
|
||||||
return ImageFont.truetype("assets/fonts/Impact.ttf", size=size)
|
|
||||||
|
|
||||||
def load_tnr(size):
|
|
||||||
return ImageFont.truetype("assets/fonts/TNR.ttf", size=size)
|
|
||||||
|
|
||||||
def draw_text_with_outline(draw, text, x, y, font):
|
|
||||||
outline_offsets = [(-2, -2), (-2, 2), (2, -2), (2, 2), (0, -2), (0, 2), (-2, 0), (2, 0)]
|
|
||||||
for ox, oy in outline_offsets:
|
|
||||||
draw.text((x + ox, y + oy), text, font=font, fill="black")
|
|
||||||
draw.text((x, y), text, font=font, fill="white")
|
|
||||||
|
|
||||||
def fits_in_width(text, font, max_width, draw):
|
|
||||||
bbox = draw.textbbox((0, 0), text, font=font)
|
|
||||||
text_width = bbox[2] - bbox[0]
|
|
||||||
return text_width <= max_width
|
|
||||||
|
|
||||||
def split_text_to_fit(text, font, max_width, draw):
|
|
||||||
words = text.split()
|
|
||||||
for i in range(len(words), 0, -1):
|
|
||||||
top_text = " ".join(words[:i])
|
|
||||||
bottom_text = " ".join(words[i:])
|
|
||||||
if fits_in_width(top_text, font, max_width, draw) and fits_in_width(bottom_text, font, max_width, draw):
|
|
||||||
return top_text, bottom_text
|
|
||||||
midpoint = len(words) // 2
|
|
||||||
return " ".join(words[:midpoint]), " ".join(words[midpoint:])
|
|
||||||
|
|
||||||
async def gen_meme(input_image_path, sentence_size=5, max_attempts=10):
|
|
||||||
markov_model = load_markov_model()
|
|
||||||
if not markov_model or not os.path.isfile(input_image_path):
|
|
||||||
return None
|
|
||||||
|
|
||||||
attempt = 0
|
|
||||||
while attempt < max_attempts:
|
|
||||||
with Image.open(input_image_path).convert("RGBA") as img:
|
|
||||||
draw = ImageDraw.Draw(img)
|
|
||||||
width, height = img.size
|
|
||||||
|
|
||||||
font_size = int(height / 10)
|
|
||||||
font = load_font(font_size)
|
|
||||||
|
|
||||||
response = None
|
|
||||||
for _ in range(20):
|
|
||||||
if sentence_size == 1:
|
|
||||||
candidate = markov_model.make_short_sentence(max_chars=100, tries=100)
|
|
||||||
if candidate:
|
|
||||||
candidate = candidate.split()[0]
|
|
||||||
else:
|
|
||||||
candidate = markov_model.make_sentence(tries=100, max_words=sentence_size)
|
|
||||||
|
|
||||||
if candidate and candidate not in generated_sentences:
|
|
||||||
if sentence_size > 1:
|
|
||||||
candidate = improve_sentence_coherence(candidate)
|
|
||||||
generated_sentences.add(candidate)
|
|
||||||
response = candidate
|
|
||||||
break
|
|
||||||
|
|
||||||
if not response:
|
|
||||||
response = "NO TEXT GENERATED"
|
|
||||||
|
|
||||||
cleaned_response = re.sub(r'[^\w\s]', '', response).lower()
|
|
||||||
coherent_response = rephrase_for_coherence(cleaned_response).upper()
|
|
||||||
|
|
||||||
bbox = draw.textbbox((0, 0), coherent_response, font=font)
|
|
||||||
text_width = bbox[2] - bbox[0]
|
|
||||||
text_height_px = bbox[3] - bbox[1]
|
|
||||||
max_text_height = height // 4
|
|
||||||
|
|
||||||
if text_width <= width and text_height_px <= max_text_height:
|
|
||||||
draw_text_with_outline(draw, coherent_response, (width - text_width) / 2, 0, font)
|
|
||||||
img.save(input_image_path)
|
|
||||||
return input_image_path
|
|
||||||
else:
|
|
||||||
top_text, bottom_text = split_text_to_fit(coherent_response, font, width, draw)
|
|
||||||
|
|
||||||
top_bbox = draw.textbbox((0, 0), top_text, font=font)
|
|
||||||
bottom_bbox = draw.textbbox((0, 0), bottom_text, font=font)
|
|
||||||
|
|
||||||
top_height = top_bbox[3] - top_bbox[1]
|
|
||||||
bottom_height = bottom_bbox[3] - bottom_bbox[1]
|
|
||||||
|
|
||||||
if top_height <= max_text_height and bottom_height <= max_text_height:
|
|
||||||
draw_text_with_outline(draw, top_text, (width - (top_bbox[2] - top_bbox[0])) / 2, 0, font)
|
|
||||||
y_bottom = height - bottom_height - int(height * 0.04)
|
|
||||||
draw_text_with_outline(draw, bottom_text, (width - (bottom_bbox[2] - bottom_bbox[0])) / 2, y_bottom, font)
|
|
||||||
img.save(input_image_path)
|
|
||||||
return input_image_path
|
|
||||||
|
|
||||||
attempt += 1
|
|
||||||
|
|
||||||
with Image.open(input_image_path).convert("RGBA") as img:
|
|
||||||
draw = ImageDraw.Draw(img)
|
|
||||||
width, height = img.size
|
|
||||||
font_size = int(height / 10)
|
|
||||||
font = load_font(font_size)
|
|
||||||
|
|
||||||
truncated = coherent_response[:100]
|
|
||||||
bbox = draw.textbbox((0, 0), truncated, font=font)
|
|
||||||
text_width = bbox[2] - bbox[0]
|
|
||||||
draw_text_with_outline(draw, truncated, (width - text_width) / 2, 0, font)
|
|
||||||
img.save(input_image_path)
|
|
||||||
return input_image_path
|
|
||||||
|
|
||||||
async def gen_demotivator(input_image_path, max_attempts=5):
|
|
||||||
markov_model = load_markov_model()
|
|
||||||
if not markov_model or not os.path.isfile(input_image_path):
|
|
||||||
return None
|
|
||||||
|
|
||||||
attempt = 0
|
|
||||||
while attempt < max_attempts:
|
|
||||||
with Image.open(input_image_path).convert("RGB") as img:
|
|
||||||
size = max(img.width, img.height)
|
|
||||||
frame_thick = int(size * 0.0054)
|
|
||||||
inner_size = size - 2 * frame_thick
|
|
||||||
resized_img = img.resize((inner_size, inner_size), Image.LANCZOS)
|
|
||||||
framed = Image.new("RGB", (size, size), "white")
|
|
||||||
framed.paste(resized_img, (frame_thick, frame_thick))
|
|
||||||
landscape_w = int(size * 1.5)
|
|
||||||
caption_h = int(size * 0.3)
|
|
||||||
canvas_h = framed.height + caption_h
|
|
||||||
canvas = Image.new("RGB", (landscape_w, canvas_h), "black")
|
|
||||||
# the above logic didnt even work, fml
|
|
||||||
fx = (landscape_w - framed.width) // 2
|
|
||||||
canvas.paste(framed, (fx, 0))
|
|
||||||
|
|
||||||
draw = ImageDraw.Draw(canvas)
|
|
||||||
|
|
||||||
title = subtitle = None
|
|
||||||
for _ in range(20):
|
|
||||||
t = markov_model.make_sentence(tries=100, max_words=4)
|
|
||||||
s = markov_model.make_sentence(tries=100, max_words=5)
|
|
||||||
if t and s and t != s:
|
|
||||||
title = t.upper()
|
|
||||||
subtitle = s.capitalize()
|
|
||||||
break
|
|
||||||
if not title: title = "DEMOTIVATOR"
|
|
||||||
if not subtitle: subtitle = "no text generated"
|
|
||||||
|
|
||||||
title_sz = int(caption_h * 0.4)
|
|
||||||
sub_sz = int(caption_h * 0.25)
|
|
||||||
title_font = load_tnr(title_sz)
|
|
||||||
sub_font = load_tnr(sub_sz)
|
|
||||||
|
|
||||||
bbox = draw.textbbox((0, 0), title, font=title_font)
|
|
||||||
txw, txh = bbox[2] - bbox[0], bbox[3] - bbox[1]
|
|
||||||
tx = (landscape_w - txw) // 2
|
|
||||||
ty = framed.height + int(caption_h * 0.1)
|
|
||||||
draw_text_with_outline(draw, title, tx, ty, title_font)
|
|
||||||
|
|
||||||
bbox = draw.textbbox((0, 0), subtitle, font=sub_font)
|
|
||||||
sxw, sxh = bbox[2] - bbox[0], bbox[3] - bbox[1]
|
|
||||||
sx = (landscape_w - sxw) // 2
|
|
||||||
sy = ty + txh + int(caption_h * 0.05)
|
|
||||||
for ox, oy in [(-1, -1), (1, -1), (-1, 1), (1, 1)]:
|
|
||||||
draw.text((sx + ox, sy + oy), subtitle, font=sub_font, fill="black")
|
|
||||||
draw.text((sx, sy), subtitle, font=sub_font, fill="#AAAAAA")
|
|
||||||
|
|
||||||
canvas.save(input_image_path)
|
|
||||||
return input_image_path
|
|
||||||
|
|
||||||
attempt += 1
|
|
||||||
return None
|
|
|
@ -1,4 +1,5 @@
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
from modules.globalvars import *
|
from modules.globalvars import *
|
||||||
|
|
||||||
class GooberFormatter(logging.Formatter):
|
class GooberFormatter(logging.Formatter):
|
||||||
|
@ -16,10 +17,14 @@ class GooberFormatter(logging.Formatter):
|
||||||
}
|
}
|
||||||
|
|
||||||
def format(self, record: logging.LogRecord):
|
def format(self, record: logging.LogRecord):
|
||||||
|
ansiescape = re.compile(r'\x1B[@-_][0-?]*[ -/]*[@-~]')
|
||||||
if self.colors:
|
if self.colors:
|
||||||
log_fmt = self.FORMATS.get(record.levelno) # Add colors
|
log_fmt = self.FORMATS.get(record.levelno) # Add colors
|
||||||
else:
|
else:
|
||||||
log_fmt = self._format # Just use the default format
|
log_fmt = self._format # Just use the default format
|
||||||
|
|
||||||
formatter = logging.Formatter(log_fmt, datefmt="%m/%d/%y %H:%M:%S")
|
formatter = logging.Formatter(log_fmt, datefmt="%m/%d/%y %H:%M:%S")
|
||||||
return formatter.format(record)
|
formatted = formatter.format(record)
|
||||||
|
if not self.colors:
|
||||||
|
formatted = ansiescape.sub('', formatted)
|
||||||
|
return formatted
|
||||||
|
|
|
@ -3,9 +3,16 @@ import json
|
||||||
import markovify
|
import markovify
|
||||||
import pickle
|
import pickle
|
||||||
from modules.globalvars import *
|
from modules.globalvars import *
|
||||||
from modules.volta.main import _
|
|
||||||
import logging
|
import logging
|
||||||
|
from modules.volta.main import _
|
||||||
|
from modules.settings import instance as settings_manager
|
||||||
|
|
||||||
|
settings = settings_manager.settings
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger("goober")
|
logger = logging.getLogger("goober")
|
||||||
|
|
||||||
|
|
||||||
# Get file size and line count for a given file path
|
# Get file size and line count for a given file path
|
||||||
def get_file_info(file_path):
|
def get_file_info(file_path):
|
||||||
try:
|
try:
|
||||||
|
@ -16,46 +23,52 @@ def get_file_info(file_path):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {"error": str(e)}
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
# Load memory data from file, or use default dataset if not loaded yet
|
# Load memory data from file, or use default dataset if not loaded yet
|
||||||
def load_memory():
|
def load_memory():
|
||||||
data = []
|
data = []
|
||||||
|
|
||||||
# Try to load data from MEMORY_FILE
|
# Try to load data from MEMORY_FILE
|
||||||
try:
|
try:
|
||||||
with open(MEMORY_FILE, "r") as f:
|
with open(settings["bot"]["active_memory"], "r") as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
# Save memory data to MEMORY_FILE
|
# Save memory data to MEMORY_FILE
|
||||||
def save_memory(memory):
|
def save_memory(memory):
|
||||||
with open(MEMORY_FILE, "w") as f:
|
with open(settings["bot"]["active_memory"], "w") as f:
|
||||||
json.dump(memory, f, indent=4)
|
json.dump(memory, f, indent=4)
|
||||||
|
|
||||||
def train_markov_model(memory, additional_data=None):
|
|
||||||
|
def train_markov_model(memory, additional_data=None) -> markovify.NewlineText | None:
|
||||||
if not memory:
|
if not memory:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
filtered_memory = [line for line in memory if isinstance(line, str)]
|
filtered_memory = [line for line in memory if isinstance(line, str)]
|
||||||
if additional_data:
|
if additional_data:
|
||||||
filtered_memory.extend(line for line in additional_data if isinstance(line, str))
|
filtered_memory.extend(
|
||||||
|
line for line in additional_data if isinstance(line, str)
|
||||||
|
)
|
||||||
|
|
||||||
if not filtered_memory:
|
if not filtered_memory:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
text = "\n".join(filtered_memory)
|
text = "\n".join(filtered_memory)
|
||||||
model = markovify.NewlineText(text, state_size=2)
|
model = markovify.NewlineText(text, state_size=2)
|
||||||
return model
|
return model
|
||||||
|
|
||||||
# Save the Markov model to a pickle file
|
def save_markov_model(model, filename="markov_model.pkl"):
|
||||||
def save_markov_model(model, filename='markov_model.pkl'):
|
with open(filename, "wb") as f:
|
||||||
with open(filename, 'wb') as f:
|
|
||||||
pickle.dump(model, f)
|
pickle.dump(model, f)
|
||||||
logger.info(f"Markov model saved to {filename}.")
|
logger.info(f"Markov model saved to {filename}.")
|
||||||
|
|
||||||
# Load the Markov model from a pickle file
|
def load_markov_model(filename="markov_model.pkl"):
|
||||||
def load_markov_model(filename='markov_model.pkl'):
|
|
||||||
try:
|
try:
|
||||||
with open(filename, 'rb') as f:
|
with open(filename, "rb") as f:
|
||||||
model = pickle.load(f)
|
model = pickle.load(f)
|
||||||
logger.info(f"{_('model_loaded')} {filename}.{RESET}")
|
logger.info(f"{_('model_loaded')} {filename}.{RESET}")
|
||||||
return model
|
return model
|
||||||
|
|
37
modules/permission.py
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
from functools import wraps
|
||||||
|
import discord
|
||||||
|
|
||||||
|
import discord.ext
|
||||||
|
import discord.ext.commands
|
||||||
|
|
||||||
|
from modules.settings import Settings as SettingsManager
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger("goober")
|
||||||
|
|
||||||
|
settings_manager = SettingsManager()
|
||||||
|
settings = settings_manager.settings
|
||||||
|
|
||||||
|
|
||||||
|
class PermissionError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def requires_admin():
|
||||||
|
async def wrapper(ctx: discord.ext.commands.Context):
|
||||||
|
print(ctx.author.id)
|
||||||
|
if ctx.author.id not in settings["bot"]["owner_ids"]:
|
||||||
|
await ctx.send("You don't have the necessary permissions to run this command!")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
command = ctx.command
|
||||||
|
if not command:
|
||||||
|
logger.info(f"Unknown command ran {ctx.message}")
|
||||||
|
else:
|
||||||
|
logger.info(
|
||||||
|
f'Command {settings["bot"]["prefix"]}{command.name} @{ctx.author.name}'
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
return discord.ext.commands.check(wrapper)
|
|
@ -1,4 +1,5 @@
|
||||||
from modules.globalvars import *
|
from modules.globalvars import *
|
||||||
|
from modules.settings import Settings as SettingsManager
|
||||||
from modules.volta.main import _, check_missing_translations
|
from modules.volta.main import _, check_missing_translations
|
||||||
import time
|
import time
|
||||||
import os
|
import os
|
||||||
|
@ -14,6 +15,13 @@ import logging
|
||||||
|
|
||||||
logger = logging.getLogger("goober")
|
logger = logging.getLogger("goober")
|
||||||
|
|
||||||
|
settings_manager = SettingsManager()
|
||||||
|
settings = settings_manager.settings
|
||||||
|
MEMORY_FILE = settings["bot"]["active_memory"]
|
||||||
|
with open(settings["splash_text_loc"], "r", encoding="UTF-8") as f:
|
||||||
|
splash_text = "".join(f.readlines())
|
||||||
|
|
||||||
|
|
||||||
# import shutil
|
# import shutil
|
||||||
psutilavaliable = True
|
psutilavaliable = True
|
||||||
try:
|
try:
|
||||||
|
@ -28,8 +36,7 @@ def check_for_model():
|
||||||
logger.info("Model is installed.")
|
logger.info("Model is installed.")
|
||||||
else:
|
else:
|
||||||
logger.info("Model is not installed.")
|
logger.info("Model is not installed.")
|
||||||
|
|
||||||
|
|
||||||
def iscloned():
|
def iscloned():
|
||||||
if os.path.exists(".git"):
|
if os.path.exists(".git"):
|
||||||
return True
|
return True
|
||||||
|
@ -38,130 +45,112 @@ def iscloned():
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
def get_stdlib_modules():
|
def get_stdlib_modules():
|
||||||
stdlib_path = pathlib.Path(sysconfig.get_paths()['stdlib'])
|
stdlib = pathlib.Path(sysconfig.get_paths()['stdlib'])
|
||||||
modules = set()
|
modules = set(sys.builtin_module_names)
|
||||||
if hasattr(sys, 'builtin_module_names'):
|
|
||||||
modules.update(sys.builtin_module_names)
|
modules.update(
|
||||||
for file in stdlib_path.glob('*.py'):
|
f.stem for f in stdlib.glob('*.py') if f.stem != '__init__'
|
||||||
if file.stem != '__init__':
|
)
|
||||||
modules.add(file.stem)
|
modules.update(
|
||||||
for folder in stdlib_path.iterdir():
|
d.name for d in stdlib.iterdir() if (d / '__init__.py').exists()
|
||||||
if folder.is_dir() and (folder / '__init__.py').exists():
|
)
|
||||||
modules.add(folder.name)
|
modules.update(
|
||||||
for file in stdlib_path.glob('*.*'):
|
f.stem for f in stdlib.glob('*') if f.suffix in ('.so', '.pyd')
|
||||||
if file.suffix in ('.so', '.pyd'):
|
)
|
||||||
modules.add(file.stem)
|
|
||||||
|
|
||||||
return modules
|
return modules
|
||||||
|
|
||||||
def check_requirements():
|
def check_requirements():
|
||||||
STD_LIB_MODULES = get_stdlib_modules()
|
stdlib = get_stdlib_modules()
|
||||||
PACKAGE_ALIASES = {
|
aliases = {
|
||||||
"discord": "discord.py",
|
"discord": "discord.py",
|
||||||
"better_profanity": "better-profanity",
|
"better_profanity": "better-profanity",
|
||||||
"dotenv": "python-dotenv",
|
"dotenv": "python-dotenv",
|
||||||
"pil": "pillow"
|
"pil": "pillow"
|
||||||
}
|
}
|
||||||
|
|
||||||
parent_dir = os.path.dirname(os.path.abspath(__file__))
|
req_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'requirements.txt'))
|
||||||
requirements_path = os.path.abspath(os.path.join(parent_dir, '..', 'requirements.txt'))
|
if not os.path.exists(req_path):
|
||||||
|
logger.error(_('requirements_not_found').format(path=req_path))
|
||||||
if not os.path.exists(requirements_path):
|
|
||||||
logger.error(f"{(_('requirements_not_found')).format(path=requirements_path)}")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
with open(requirements_path, 'r') as f:
|
with open(req_path) as f:
|
||||||
lines = f.readlines()
|
requirements = {
|
||||||
requirements = set()
|
aliases.get(line.split('==')[0].strip().lower(), line.split('==')[0].strip().lower())
|
||||||
for line in lines:
|
for line in f if line.strip() and not line.startswith('#')
|
||||||
line = line.strip()
|
}
|
||||||
if line and not line.startswith('#'):
|
|
||||||
base_pkg = line.split('==')[0].lower()
|
|
||||||
aliased_pkg = PACKAGE_ALIASES.get(base_pkg, base_pkg)
|
|
||||||
requirements.add(aliased_pkg)
|
|
||||||
|
|
||||||
installed_packages = {dist.metadata['Name'].lower() for dist in importlib.metadata.distributions()}
|
installed = {d.metadata['Name'].lower() for d in importlib.metadata.distributions()}
|
||||||
missing = []
|
missing = []
|
||||||
|
|
||||||
for req in sorted(requirements):
|
for pkg in sorted(requirements):
|
||||||
if req in STD_LIB_MODULES or req == 'modules':
|
if pkg in stdlib or pkg == 'modules':
|
||||||
print((_('std_lib_local_skipped')).format(package=req))
|
print(_('std_lib_local_skipped').format(package=pkg))
|
||||||
continue
|
continue
|
||||||
|
if pkg in installed:
|
||||||
check_name = req.lower()
|
logger.info(_('ok_installed').format(package=pkg))
|
||||||
|
|
||||||
if check_name in installed_packages:
|
|
||||||
logger.info(f"{_('ok_installed').format(package=check_name)} {check_name}")
|
|
||||||
else:
|
else:
|
||||||
logger.error(f"{(_('missing_package')).format(package=check_name)} {check_name} {(_('missing_package2'))}")
|
logger.error(f"{_('missing_package').format(package=pkg)} {pkg} {_('missing_package2')}")
|
||||||
missing.append(check_name)
|
missing.append(pkg)
|
||||||
|
|
||||||
if missing:
|
if missing:
|
||||||
logger.error(_('missing_packages_detected'))
|
logger.error(_('missing_packages_detected'))
|
||||||
for pkg in missing:
|
for pkg in missing:
|
||||||
print(f" - {pkg}")
|
print(f" - {pkg}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
else:
|
|
||||||
logger.info(_('all_requirements_satisfied'))
|
logger.info(_('all_requirements_satisfied'))
|
||||||
|
|
||||||
|
|
||||||
def check_latency():
|
def check_latency():
|
||||||
host = "1.1.1.1"
|
host = "1.1.1.1"
|
||||||
system = platform.system()
|
system = platform.system()
|
||||||
|
|
||||||
if system == "Windows":
|
cmd, pattern = {
|
||||||
cmd = ["ping", "-n", "1", "-w", "1000", host]
|
"Windows": (["ping", "-n", "1", "-w", "1000", host], r"Average = (\d+)ms"),
|
||||||
latency_pattern = r"Average = (\d+)ms"
|
"Darwin": (["ping", "-c", "1", host], r"time=([\d\.]+) ms")
|
||||||
|
}.get(system, (["ping", "-c", "1", "-W", "1", host], r"time=([\d\.]+) ms"))
|
||||||
elif system == "Darwin":
|
|
||||||
cmd = ["ping", "-c", "1", host]
|
|
||||||
latency_pattern = r"time=([\d\.]+) ms"
|
|
||||||
|
|
||||||
else:
|
|
||||||
cmd = ["ping", "-c", "1", "-W", "1", host]
|
|
||||||
latency_pattern = r"time=([\d\.]+) ms"
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||||
cmd,
|
if result.returncode != 0:
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
stderr=subprocess.PIPE,
|
|
||||||
text=True
|
|
||||||
)
|
|
||||||
|
|
||||||
if result.returncode == 0:
|
|
||||||
match = re.search(latency_pattern, result.stdout)
|
|
||||||
if match:
|
|
||||||
latency_ms = float(match.group(1))
|
|
||||||
logger.info((_('ping_to')).format(host=host, latency=latency_ms))
|
|
||||||
if latency_ms > 300:
|
|
||||||
logger.warning(f"{(_('high_latency'))}")
|
|
||||||
else:
|
|
||||||
logger.warning((_('could_not_parse_latency')))
|
|
||||||
else:
|
|
||||||
print(result.stderr)
|
print(result.stderr)
|
||||||
logger.error(f"{(_('ping_failed')).format(host=host)}{RESET}")
|
return logger.error(_('ping_failed').format(host=host) + RESET)
|
||||||
|
|
||||||
|
match = re.search(pattern, result.stdout)
|
||||||
|
if not match:
|
||||||
|
return logger.warning(_('could_not_parse_latency'))
|
||||||
|
|
||||||
|
latency = float(match.group(1))
|
||||||
|
logger.info(_('ping_to').format(host=host, latency=latency))
|
||||||
|
if latency > 300:
|
||||||
|
logger.warning(_('high_latency'))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error((_('error_running_ping')).format(error=e))
|
logger.error(_('error_running_ping').format(error=e))
|
||||||
|
|
||||||
def check_memory():
|
def check_memory():
|
||||||
if psutilavaliable == False:
|
if not psutilavaliable:
|
||||||
return
|
return
|
||||||
try:
|
|
||||||
memory_info = psutil.virtual_memory() # type: ignore
|
|
||||||
total_memory = memory_info.total / (1024 ** 3)
|
|
||||||
used_memory = memory_info.used / (1024 ** 3)
|
|
||||||
free_memory = memory_info.available / (1024 ** 3)
|
|
||||||
|
|
||||||
logger.info((_('memory_usage')).format(used=used_memory, total=total_memory, percent=(used_memory / total_memory) * 100))
|
try:
|
||||||
if used_memory > total_memory * 0.9:
|
mem = psutil.virtual_memory() # type: ignore
|
||||||
print(f"{YELLOW}{(_('memory_above_90')).format(percent=(used_memory / total_memory) * 100)}{RESET}")
|
total = mem.total / 1e9
|
||||||
logger.info((_('total_memory')).format(total=total_memory))
|
used = mem.used / 1e9
|
||||||
logger.info((_('used_memory')).format(used=used_memory))
|
free = mem.available / 1e9
|
||||||
if free_memory < 1:
|
percent_used = (used / total) * 100
|
||||||
logger.warning(f"{(_('low_free_memory')).format(free=free_memory)}")
|
|
||||||
|
logger.info(_('memory_usage').format(used=used, total=total, percent=percent_used))
|
||||||
|
if percent_used > 90:
|
||||||
|
print(f"{YELLOW}{_('memory_above_90').format(percent=percent_used)}{RESET}")
|
||||||
|
logger.info(_('total_memory').format(total=total))
|
||||||
|
logger.info(_('used_memory').format(used=used))
|
||||||
|
if free < 1:
|
||||||
|
logger.warning(_('low_free_memory').format(free=free))
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
logger.error(_('psutil_not_installed')) # todo: translate this into italian and put it in the translations "psutil is not installed. Memory check skipped."
|
logger.error(_('psutil_not_installed'))
|
||||||
|
|
||||||
def check_cpu():
|
def check_cpu():
|
||||||
if psutilavaliable == False:
|
if psutilavaliable == False:
|
||||||
|
@ -178,22 +167,23 @@ def check_cpu():
|
||||||
|
|
||||||
def check_memoryjson():
|
def check_memoryjson():
|
||||||
try:
|
try:
|
||||||
logger.info((_('memory_file')).format(size=os.path.getsize(MEMORY_FILE) / (1024 ** 2)))
|
size_mb = os.path.getsize(MEMORY_FILE) / (1024 ** 2)
|
||||||
if os.path.getsize(MEMORY_FILE) > 1_073_741_824:
|
logger.info(_('memory_file').format(size=size_mb))
|
||||||
logger.warning(f"{(_('memory_file_large'))}")
|
if size_mb > 1024:
|
||||||
|
logger.warning(_('memory_file_large'))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(MEMORY_FILE, 'r', encoding='utf-8') as f:
|
with open(MEMORY_FILE, 'r', encoding='utf-8') as f:
|
||||||
json.load(f)
|
json.load(f)
|
||||||
except json.JSONDecodeError as e:
|
except (json.JSONDecodeError, UnicodeDecodeError) as e:
|
||||||
logger.error(f"{(_('memory_file_corrupted')).format(error=e)}")
|
msg = _('memory_file_corrupted') if isinstance(e, json.JSONDecodeError) else _('memory_file_encoding')
|
||||||
logger.warning(f"{(_('consider_backup_memory'))}")
|
logger.error(msg.format(error=e))
|
||||||
except UnicodeDecodeError as e:
|
logger.warning(_('consider_backup_memory'))
|
||||||
logger.error(f"{(_('memory_file_encoding')).format(error=e)}")
|
|
||||||
logger.warning(f"{(_('consider_backup_memory'))}")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"{(_('error_reading_memory')).format(error=e)}")
|
logger.error(_('error_reading_memory').format(error=e))
|
||||||
|
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
logger(f"{(_('memory_file_not_found'))}")
|
logger.error(_('memory_file_not_found'))
|
||||||
|
|
||||||
def presskey2skip(timeout):
|
def presskey2skip(timeout):
|
||||||
if os.name == 'nt':
|
if os.name == 'nt':
|
||||||
|
@ -228,8 +218,8 @@ def presskey2skip(timeout):
|
||||||
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
||||||
beta = beta
|
beta = beta
|
||||||
def start_checks():
|
def start_checks():
|
||||||
if CHECKS_DISABLED == "True":
|
if settings["disable_checks"]:
|
||||||
logger.warning(f"{(_('checks_disabled'))}")
|
logger.warning(f"{_('checks_disabled')}")
|
||||||
return
|
return
|
||||||
logger.info(_('running_prestart_checks'))
|
logger.info(_('running_prestart_checks'))
|
||||||
check_for_model()
|
check_for_model()
|
||||||
|
@ -251,5 +241,4 @@ def start_checks():
|
||||||
pass
|
pass
|
||||||
logger.info(_('continuing_in_seconds').format(seconds=5))
|
logger.info(_('continuing_in_seconds').format(seconds=5))
|
||||||
presskey2skip(timeout=5)
|
presskey2skip(timeout=5)
|
||||||
os.system('cls' if os.name == 'nt' else 'clear')
|
os.system('cls' if os.name == 'nt' else 'clear')
|
||||||
print(splashtext)
|
|
|
@ -37,44 +37,37 @@ def is_positive(sentence):
|
||||||
return sentiment_score > 0.6 # had to raise the bar because it kept saying "death to jews" was fine and it kept reacting to them
|
return sentiment_score > 0.6 # had to raise the bar because it kept saying "death to jews" was fine and it kept reacting to them
|
||||||
|
|
||||||
async def send_message(ctx, message=None, embed=None, file=None, edit=False, message_reference=None):
|
async def send_message(ctx, message=None, embed=None, file=None, edit=False, message_reference=None):
|
||||||
if edit and message_reference:
|
try:
|
||||||
try:
|
if edit and message_reference:
|
||||||
await message_reference.edit(content=message, embed=embed)
|
await message_reference.edit(content=message, embed=embed)
|
||||||
except Exception as e:
|
return message_reference
|
||||||
await ctx.send(f"{RED}{(_('edit_fail'))} {e}{RESET}")
|
|
||||||
else:
|
send_kwargs = {}
|
||||||
if hasattr(ctx, "respond"):
|
if message:
|
||||||
sent_message = None
|
send_kwargs['content'] = message
|
||||||
if embed:
|
if embed:
|
||||||
sent_message = await ctx.respond(embed=embed, ephemeral=False)
|
send_kwargs['embed'] = embed
|
||||||
elif message:
|
if file:
|
||||||
sent_message = await ctx.respond(message, ephemeral=False)
|
send_kwargs['file'] = file
|
||||||
if file:
|
|
||||||
sent_message = await ctx.respond(file=file, ephemeral=False)
|
if hasattr(ctx, "respond"):
|
||||||
else:
|
return await ctx.respond(**send_kwargs, ephemeral=False)
|
||||||
sent_message = None
|
else:
|
||||||
if embed:
|
return await ctx.send(**send_kwargs)
|
||||||
sent_message = await ctx.send(embed=embed)
|
|
||||||
elif message:
|
except Exception as e:
|
||||||
sent_message = await ctx.send(message)
|
await ctx.send(f"{RED}{(_('edit_fail'))} {e}{RESET}")
|
||||||
if file:
|
|
||||||
sent_message = await ctx.send(file=file)
|
|
||||||
return sent_message
|
|
||||||
|
|
||||||
def append_mentions_to_18digit_integer(message):
|
|
||||||
pattern = r'\b\d{18}\b'
|
|
||||||
return re.sub(pattern, lambda match: "", message)
|
|
||||||
|
|
||||||
def preprocess_message(message):
|
def preprocess_message(message):
|
||||||
message = append_mentions_to_18digit_integer(message)
|
message = message
|
||||||
doc = nlp(message)
|
doc = nlp(message)
|
||||||
tokens = [token.text for token in doc if token.is_alpha or token.is_digit]
|
tokens = [token.text for token in doc if token.is_alpha or token.is_digit]
|
||||||
return " ".join(tokens)
|
return " ".join(tokens)
|
||||||
|
|
||||||
def improve_sentence_coherence(sentence):
|
def improve_sentence_coherence(sentence):
|
||||||
return re.sub(r'\bi\b', 'I', sentence)
|
return ""
|
||||||
|
|
||||||
def rephrase_for_coherence(sentence):
|
def rephrase_for_coherence(sentence):
|
||||||
words = sentence.split()
|
coherent_sentence = sentence
|
||||||
coherent_sentence = " ".join(words)
|
|
||||||
return coherent_sentence
|
return coherent_sentence
|
||||||
|
|
58
modules/settings.py
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from typing import List, TypedDict
|
||||||
|
import copy
|
||||||
|
|
||||||
|
class MiscBotOptions(TypedDict):
|
||||||
|
ping_line: str
|
||||||
|
active_song: str
|
||||||
|
positive_gifs: List[str]
|
||||||
|
block_profanity: bool
|
||||||
|
|
||||||
|
class BotSettings(TypedDict):
|
||||||
|
prefix: str
|
||||||
|
owner_ids: List[int]
|
||||||
|
blacklisted_users: List[int]
|
||||||
|
user_training: bool
|
||||||
|
allow_show_mem_command: bool
|
||||||
|
react_to_messages: bool
|
||||||
|
misc: MiscBotOptions
|
||||||
|
enabled_cogs: List[str]
|
||||||
|
active_memory: str
|
||||||
|
|
||||||
|
class SettingsType(TypedDict):
|
||||||
|
bot: BotSettings
|
||||||
|
locale: str
|
||||||
|
name: str
|
||||||
|
auto_update: bool
|
||||||
|
disable_checks: bool
|
||||||
|
splash_text_loc: str
|
||||||
|
|
||||||
|
class Settings:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.path = os.path.join(".", "settings", "settings.json")
|
||||||
|
if not os.path.exists(self.path):
|
||||||
|
raise FileNotFoundError("settings.json file does not exist!")
|
||||||
|
|
||||||
|
with open(self.path, "r") as f:
|
||||||
|
self._kv_store = json.load(f)
|
||||||
|
|
||||||
|
self.settings: SettingsType = self._kv_store # type: ignore
|
||||||
|
self.original_settings = copy.deepcopy(self.settings)
|
||||||
|
|
||||||
|
def get_locale(self) -> str:
|
||||||
|
# Return locale or None if missing
|
||||||
|
return self.settings.get("locale", None)
|
||||||
|
|
||||||
|
def commit(self) -> None:
|
||||||
|
with open(self.path, "w") as f:
|
||||||
|
json.dump(self.settings, f, indent=4)
|
||||||
|
self.original_settings = copy.deepcopy(self.settings)
|
||||||
|
|
||||||
|
def discard(self) -> None:
|
||||||
|
self.settings = copy.deepcopy(self.original_settings)
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
instance = Settings()
|
||||||
|
locale = instance.get_locale()
|
||||||
|
print("Locale:", locale)
|
|
@ -1,23 +1,16 @@
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
import os
|
from modules.globalvars import RED, RESET
|
||||||
from modules.globalvars import RED, RESET, splashtext
|
|
||||||
from modules.volta.main import _
|
from modules.volta.main import _
|
||||||
|
|
||||||
def handle_exception(exc_type, exc_value, exc_traceback, *, context=None):
|
def handle_exception(exc_type, exc_value, exc_traceback, *, context=None):
|
||||||
os.system('cls' if os.name == 'nt' else 'clear')
|
|
||||||
|
|
||||||
if issubclass(exc_type, KeyboardInterrupt):
|
if issubclass(exc_type, KeyboardInterrupt):
|
||||||
sys.__excepthook__(exc_type, exc_value, exc_traceback)
|
sys.__excepthook__(exc_type, exc_value, exc_traceback)
|
||||||
return
|
return
|
||||||
|
|
||||||
print(splashtext)
|
|
||||||
print(f"{RED}=====BEGINNING OF TRACEBACK====={RESET}")
|
print(f"{RED}=====BEGINNING OF TRACEBACK====={RESET}")
|
||||||
traceback.print_exception(exc_type, exc_value, exc_traceback)
|
traceback.print_exception(exc_type, exc_value, exc_traceback)
|
||||||
print(f"{RED}========END OF TRACEBACK========{RESET}")
|
print(f"{RED}========END OF TRACEBACK========{RESET}")
|
||||||
print(f"{RED}{_('unhandled_exception')}{RESET}")
|
print(f"{RED}{_('unhandled_exception')}{RESET}")
|
||||||
|
|
||||||
|
|
||||||
if context:
|
if context:
|
||||||
print(f"{RED}Context: {context}{RESET}")
|
print(f"{RED}Context: {context}{RESET}")
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,6 @@ def is_remote_ahead(branch='main', remote='origin'):
|
||||||
count = run_cmd(f'git rev-list --count HEAD..{remote}/{branch}')
|
count = run_cmd(f'git rev-list --count HEAD..{remote}/{branch}')
|
||||||
return int(count) > 0
|
return int(count) > 0
|
||||||
|
|
||||||
# Automatically update the local repository if the remote is ahead
|
|
||||||
def auto_update(branch='main', remote='origin'):
|
def auto_update(branch='main', remote='origin'):
|
||||||
if launched == True:
|
if launched == True:
|
||||||
print(_("already_started"))
|
print(_("already_started"))
|
||||||
|
@ -70,7 +69,7 @@ def get_latest_version_info():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Check if an update is available and perform update if needed
|
# Check if an update is available and perform update if needed
|
||||||
def check_for_update():
|
def check_for_update(slient=False):
|
||||||
global latest_version, local_version, launched
|
global latest_version, local_version, launched
|
||||||
|
|
||||||
latest_version_info = get_latest_version_info()
|
latest_version_info = get_latest_version_info()
|
||||||
|
@ -90,16 +89,17 @@ def check_for_update():
|
||||||
logger.error(f"{RED}{_('cant_find_local_version')}{RESET}")
|
logger.error(f"{RED}{_('cant_find_local_version')}{RESET}")
|
||||||
return
|
return
|
||||||
# Compare local and latest versions
|
# Compare local and latest versions
|
||||||
if local_version < latest_version:
|
if slient != True:
|
||||||
logger.info(f"{YELLOW}{_('new_version').format(latest_version=latest_version, local_version=local_version)}{RESET}")
|
if local_version < latest_version:
|
||||||
logger.info(f"{YELLOW}{_('changelog').format(VERSION_URL=VERSION_URL)}{RESET}")
|
logger.info(f"{YELLOW}{_('new_version').format(latest_version=latest_version, local_version=local_version)}{RESET}")
|
||||||
auto_update()
|
logger.info(f"{YELLOW}{_('changelog').format(VERSION_URL=VERSION_URL)}{RESET}")
|
||||||
elif beta == True:
|
auto_update()
|
||||||
logger.warning(f"You are running an \"unstable\" version of Goober, do not expect it to work properly.\nVersion {local_version}\nServer: {latest_version}{RESET}")
|
elif beta == True:
|
||||||
elif local_version > latest_version:
|
logger.warning(f"You are running an \"unstable\" version of Goober, do not expect it to work properly.\nVersion {local_version}\nServer: {latest_version}{RESET}")
|
||||||
logger.warning(f"{_('modification_warning')}")
|
elif local_version > latest_version:
|
||||||
elif local_version == latest_version:
|
logger.warning(f"{_('modification_warning')}")
|
||||||
logger.info(f"{_('latest_version')} {local_version}")
|
elif local_version == latest_version:
|
||||||
logger.info(f"{_('latest_version2').format(VERSION_URL=VERSION_URL)}\n\n")
|
logger.info(f"{_('latest_version')} {local_version}")
|
||||||
launched = True
|
logger.info(f"{_('latest_version2').format(VERSION_URL=VERSION_URL)}\n\n")
|
||||||
|
launched = True
|
||||||
return latest_version
|
return latest_version
|
|
@ -7,8 +7,11 @@ import locale
|
||||||
import json
|
import json
|
||||||
import pathlib
|
import pathlib
|
||||||
import threading
|
import threading
|
||||||
|
import platform
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
from functools import lru_cache
|
||||||
|
|
||||||
ANSI = "\033["
|
ANSI = "\033["
|
||||||
RED = f"{ANSI}31m"
|
RED = f"{ANSI}31m"
|
||||||
|
@ -47,12 +50,50 @@ def find_dotenv(start_path: pathlib.Path) -> pathlib.Path | None:
|
||||||
current = current.parent
|
current = current.parent
|
||||||
return None
|
return None
|
||||||
|
|
||||||
env_path = find_dotenv(pathlib.Path(__file__).parent)
|
def load_settings_json() -> dict | None:
|
||||||
if env_path:
|
start_path = working_dir.resolve()
|
||||||
load_dotenv(dotenv_path=env_path)
|
current = start_path.resolve()
|
||||||
print(f"[VOLTA] {GREEN}Loaded .env from {env_path}{RESET}")
|
while current != current.parent:
|
||||||
|
candidate = current / "settings.json"
|
||||||
|
if candidate.exists():
|
||||||
|
try:
|
||||||
|
with open(candidate, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
print(f"[VOLTA] {GREEN}Loaded settings.json locale '{data.get('locale')}' from {candidate}{RESET}")
|
||||||
|
return data
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[VOLTA] {RED}Failed to load settings.json at {candidate}: {e}{RESET}")
|
||||||
|
return None
|
||||||
|
current = current.parent
|
||||||
|
|
||||||
|
for root, dirs, files in os.walk(start_path):
|
||||||
|
if "settings.json" in files:
|
||||||
|
candidate = pathlib.Path(root) / "settings.json"
|
||||||
|
try:
|
||||||
|
with open(candidate, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
print(f"[VOLTA] {GREEN}Loaded settings.json locale '{data.get('locale')}' from {candidate}{RESET}")
|
||||||
|
return data
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[VOLTA] {RED}Failed to load settings.json at {candidate}: {e}{RESET}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
print(f"[VOLTA] {YELLOW}No settings.json found scanning up or down from {start_path}{RESET}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
settings = load_settings_json()
|
||||||
|
if settings and "locale" in settings:
|
||||||
|
LOCALE = settings["locale"]
|
||||||
else:
|
else:
|
||||||
print(f"[VOLTA] {YELLOW}No .env file found from {__file__} upwards.{RESET}")
|
env_path = find_dotenv(pathlib.Path(__file__).parent)
|
||||||
|
if env_path:
|
||||||
|
load_dotenv(dotenv_path=env_path)
|
||||||
|
print(f"[VOLTA] {GREEN}Loaded .env from {env_path}{RESET}")
|
||||||
|
else:
|
||||||
|
print(f"[VOLTA] {YELLOW}No .env file found from {__file__} upwards.{RESET}")
|
||||||
|
|
||||||
|
LOCALE = os.getenv("LOCALE") or None
|
||||||
|
|
||||||
|
|
||||||
locales_dirs.extend(find_locales_dirs(module_dir))
|
locales_dirs.extend(find_locales_dirs(module_dir))
|
||||||
if working_dir != module_dir:
|
if working_dir != module_dir:
|
||||||
|
@ -61,10 +102,6 @@ if working_dir != module_dir:
|
||||||
translations = {}
|
translations = {}
|
||||||
_file_mod_times = {}
|
_file_mod_times = {}
|
||||||
|
|
||||||
import locale
|
|
||||||
import platform
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
def get_system_locale():
|
def get_system_locale():
|
||||||
system = platform.system() # fallback incase locale isnt set
|
system = platform.system() # fallback incase locale isnt set
|
||||||
|
@ -119,6 +156,7 @@ def reload_if_changed():
|
||||||
current_mtime = file_path.stat().st_mtime
|
current_mtime = file_path.stat().st_mtime
|
||||||
if current_mtime != last_mtime:
|
if current_mtime != last_mtime:
|
||||||
print(f"[VOLTA] {RED}Translation file changed: {file_path}, reloading...{RESET}")
|
print(f"[VOLTA] {RED}Translation file changed: {file_path}, reloading...{RESET}")
|
||||||
|
_lookup_translation.cache_clear()
|
||||||
load_translations()
|
load_translations()
|
||||||
break
|
break
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
|
@ -140,9 +178,10 @@ def set_language(lang: str):
|
||||||
else:
|
else:
|
||||||
print(f"[VOLTA] {RED}The fallback translations cannot be found! No fallback available.{RESET}")
|
print(f"[VOLTA] {RED}The fallback translations cannot be found! No fallback available.{RESET}")
|
||||||
ENGLISH_MISSING = True
|
ENGLISH_MISSING = True
|
||||||
|
_lookup_translation.cache_clear()
|
||||||
|
|
||||||
def check_missing_translations():
|
def check_missing_translations(LOCALE=LOCALE):
|
||||||
global LOCALE, ENGLISH_MISSING
|
global ENGLISH_MISSING
|
||||||
load_translations()
|
load_translations()
|
||||||
if FALLBACK_LOCALE not in translations:
|
if FALLBACK_LOCALE not in translations:
|
||||||
print(f"[VOLTA] {RED}Fallback translations ({FALLBACK_LOCALE}.json) missing from assets/locales.{RESET}")
|
print(f"[VOLTA] {RED}Fallback translations ({FALLBACK_LOCALE}.json) missing from assets/locales.{RESET}")
|
||||||
|
@ -175,26 +214,33 @@ def check_missing_translations():
|
||||||
|
|
||||||
printedsystemfallback = False
|
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):
|
def get_translation(lang: str, key: str):
|
||||||
global printedsystemfallback
|
global printedsystemfallback
|
||||||
if ENGLISH_MISSING:
|
if ENGLISH_MISSING:
|
||||||
return f"[VOLTA] {RED}No fallback available!{RESET}"
|
return f"[VOLTA] {RED}No fallback available!{RESET}"
|
||||||
fallback_translations = translations.get(FALLBACK_LOCALE, {})
|
|
||||||
sys_lang = get_system_locale().split("_")[0] if get_system_locale() else None
|
|
||||||
sys_translations = translations.get(sys_lang, {}) if sys_lang else {}
|
|
||||||
lang_translations = translations.get(lang, {})
|
|
||||||
if key in lang_translations:
|
|
||||||
return lang_translations[key]
|
|
||||||
if sys_lang and sys_lang != lang and key in sys_translations:
|
|
||||||
if not printedsystemfallback:
|
|
||||||
print(f"[VOLTA] {YELLOW}Falling back to system language {sys_lang}!{RESET}")
|
|
||||||
printedsystemfallback = True
|
|
||||||
return sys_translations[key]
|
|
||||||
if key in fallback_translations:
|
|
||||||
print(f"[VOLTA] {YELLOW}Missing key: '{key}' in '{lang}', falling back to fallback locale '{FALLBACK_LOCALE}'{RESET}")
|
|
||||||
return fallback_translations[key]
|
|
||||||
return f"[VOLTA] {YELLOW}Missing key: '{key}' in all locales!{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:
|
def _(key: str) -> str:
|
||||||
return get_translation(LOCALE, key)
|
return get_translation(LOCALE, key)
|
||||||
|
|
||||||
|
@ -204,4 +250,9 @@ watchdog_thread = threading.Thread(target=reload_if_changed, daemon=True)
|
||||||
watchdog_thread.start()
|
watchdog_thread.start()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
print("Volta should not be run directly! Please use it as a module..")
|
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}")
|
||||||
|
|
|
@ -4,7 +4,6 @@ spacy
|
||||||
spacytextblob
|
spacytextblob
|
||||||
requests
|
requests
|
||||||
psutil
|
psutil
|
||||||
better_profanity
|
|
||||||
python-dotenv
|
python-dotenv
|
||||||
dotenv
|
dotenv
|
||||||
pillow
|
pillow
|
26
settings/settingsexample.json
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"bot": {
|
||||||
|
"prefix": "g.",
|
||||||
|
"owner_ids": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"blacklisted_users": [],
|
||||||
|
"user_training": true,
|
||||||
|
"allow_show_mem_command": true,
|
||||||
|
"react_to_messages": true,
|
||||||
|
"misc": {
|
||||||
|
"ping_line": "The Beretta fires fast and won't make you feel any better!",
|
||||||
|
"active_song": "Basket Case - Green Day",
|
||||||
|
"positive_gifs": [],
|
||||||
|
"block_profanity": false
|
||||||
|
},
|
||||||
|
"active_memory": "memory.json",
|
||||||
|
"enabled_cogs": [
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locale": "fi",
|
||||||
|
"name": "goober",
|
||||||
|
"auto_update": true,
|
||||||
|
"disable_checks": false,
|
||||||
|
"splash_text_loc": "settings/splash.txt"
|
||||||
|
}
|
11
settings/splashexample.txt
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
SS\
|
||||||
|
SS |
|
||||||
|
SSSSSS\ SSSSSS\ SSSSSS\ SSSSSSS\ SSSSSS\ SSSSSS\
|
||||||
|
SS __SS\ SS __SS\ SS __SS\ SS __SS\ SS __SS\ SS __SS\
|
||||||
|
SS / SS |SS / SS |SS / SS |SS | SS |SSSSSSSS |SS | \__|
|
||||||
|
SS | SS |SS | SS |SS | SS |SS | SS |SS ____|SS |
|
||||||
|
\SSSSSSS |\SSSSSS |\SSSSSS |SSSSSSS |\SSSSSSS\ SS |
|
||||||
|
\____SS | \______/ \______/ \_______/ \_______|\__|
|
||||||
|
SS\ SS |
|
||||||
|
\SSSSSS |
|
||||||
|
\______/
|
2
todo.txt
|
@ -1,4 +1,2 @@
|
||||||
- fix missing translations in some cases
|
|
||||||
- revamp wiki
|
- revamp wiki
|
||||||
- clean the rest
|
|
||||||
- alot
|
- alot
|
||||||
|
|