rewrote env parts and fixed some frigging issues

This commit is contained in:
ctih1 2025-07-22 19:32:19 +03:00
parent 99ab5d334e
commit f7042ed8a7
23 changed files with 3070 additions and 280 deletions

324
bot.py
View file

@ -10,12 +10,22 @@ import shutil
import uuid
import asyncio
import sys
from typing import List, Dict, Set, Optional, Tuple, Any, Union, Callable, Coroutine, TypeVar, Type
from typing import List, Dict, Literal, Set, Optional, Tuple, Any, TypedDict, Union, Callable, Coroutine, TypeVar, Type
import logging
from modules.globalvars import *
from modules.prestartchecks import start_checks
from modules.logger import GooberFormatter
import modules.keys as k
from modules import key_compiler
import logging
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
from modules.settings import Settings as SettingsManager
def build_keys():
key_compiler.build_result("en", "assets/locales", types=True, output_path="modules/keys.py", generate_comments=True)
build_keys()
logger = logging.getLogger("goober")
logger.setLevel(logging.DEBUG)
@ -31,8 +41,15 @@ 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)
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 requests
@ -45,7 +62,6 @@ 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 *
@ -60,8 +76,18 @@ T = TypeVar('T')
MessageContext = Union[commands.Context, discord.Interaction]
MessageReference = Union[Message, discord.WebhookMessage]
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] = os.getenv("POSITIVE_GIFS", "").split(',')
positive_gifs: List[str] = settings["bot"]["misc"]["positive_gifs"]
currenthash: str = ""
launched: bool = False
slash_commands_enabled: bool = False
@ -70,41 +96,48 @@ slash_commands_enabled: bool = False
intents: discord.Intents = discord.Intents.default()
intents.messages = True
intents.message_content = True
bot: commands.Bot = commands.Bot(
command_prefix=PREFIX,
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] = load_memory()
memory: List[str | Dict[Literal["_meta"], MessageMetadata]] = load_memory()
markov_model: Optional[markovify.Text] = load_markov_model()
if not markov_model:
logger.error(_('markov_model_not_found'))
logger.error(k.markov_model_not_found())
memory = load_memory()
markov_model = train_markov_model(memory)
generated_sentences: Set[str] = set()
used_words: Set[str] = set()
async def load_cogs_from_folder(bot, folder_name="assets/cogs"):
for filename in os.listdir(folder_name):
if filename.endswith(".py") and not filename.startswith("_"):
cog_name = filename[:-3]
module_path = folder_name.replace("/", ".").replace("\\", ".") + f".{cog_name}"
try:
await bot.load_extension(module_path)
logger.info(f"{(_('loaded_cog'))} {cog_name}")
except Exception as e:
logger.error(f"{(_('cog_fail'))} {cog_name} {e}")
traceback.print_exc()
async def load_cogs_from_folder(bot: commands.Bot, folder_name="assets/cogs"):
for filename in [file for file in os.listdir(folder_name) if file.endswith(".py")]:
cog_name:str = filename[:-3]
print(cog_name)
if 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"{k.loaded_cog()} {cog_name}")
except Exception as e:
logger.error(f"{k.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})
requests.post(f"{VERSION_URL}/aliveping", json={"name": settings["name"]})
except Exception as e:
logger.error(f"{(_('error_sending_alive_ping'))}{RESET} {e}")
logger.error(f"{k.error_sending_alive_ping(e)}{RESET} {e}")
await asyncio.sleep(60)
# Event: Called when the bot is ready
@ -121,9 +154,9 @@ async def on_ready() -> None:
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'))}")
logger.info(f"{k.synced_commands()} {len(synced)} {k.synced_commands2()}")
slash_commands_enabled = True
logger.info(f"{(_('started')).format(name=NAME)}")
logger.info(k.started(settings["name"]))
bot.loop.create_task(send_alive_ping_periodically())
except discord.errors.Forbidden as perm_error:
@ -131,13 +164,13 @@ async def on_ready() -> None:
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}")
logger.error(f"{k.fail_commands_sync()} {e}")
traceback.print_exc()
quit()
if not song:
if not settings["bot"]["misc"]["active_song"]:
return
await bot.change_presence(activity=discord.Activity(type=discord.ActivityType.listening, name=f"{song}"))
await bot.change_presence(activity=discord.Activity(type=discord.ActivityType.listening, name=settings["bot"]["misc"]["active_song"]))
launched = True
@bot.event
@ -157,41 +190,48 @@ async def on_command_error(ctx: commands.Context, error: commands.CommandError)
)
# Command: Retrain the Markov model from memory
@bot.hybrid_command(description=f"{(_('command_desc_retrain'))}")
@bot.hybrid_command(description=f"{k.command_desc_retrain()}")
async def retrain(ctx: commands.Context) -> None:
if ctx.author.id != ownerid:
global markov_model
if ctx.author.id not in settings["bot"]["owner_ids"]:
return
message_ref: discord.Message | None = await send_message(ctx, f"{k.command_markov_retrain()}")
if message_ref is None:
logger.error("Failed to send message!")
return
message_ref: MessageReference = await send_message(ctx, f"{(_('command_markov_retrain'))}")
try:
with open(MEMORY_FILE, 'r') as f:
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'))}")
await send_message(ctx, f"{k.command_markov_memory_not_found()}")
return
except json.JSONDecodeError:
await send_message(ctx, f"{(_('command_markov_memory_is_corrupt'))}")
await send_message(ctx, f"{k.command_markov_memory_is_corrupt()}")
return
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
data_size: int = len(memory)
processing_message_ref: discord.Message | None = await send_message(ctx, f"{k.command_markov_retraining(data_size)}")
if processing_message_ref is None:
logger.error("Couldnt find message processing message!")
start_time: float = time.time()
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)
logger.debug(f"Completed retraining in {round(time.time() - start_time,3)}s")
await send_message(ctx, f"{k.command_markov_retrain_successful(data_size)}", edit=True, message_reference=processing_message_ref)
# Command: Generate a sentence using the Markov model
@bot.hybrid_command(description=f"{(_('command_desc_talk'))}")
@bot.hybrid_command(description=f"{k.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'))}")
await send_message(ctx, f"{k.command_talk_insufficent_text()}")
return
response: Optional[str] = None
@ -218,65 +258,18 @@ async def talk(ctx: commands.Context, sentence_size: int = 5) -> None:
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'))}")
await send_message(ctx, f"{k.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
temp_input: str | None = None
def get_random_asset_image() -> Optional[str]:
files: List[str] = [f for f in os.listdir(assets_folder) if f.lower().endswith(('.png', '.jpg', '.jpeg', '.webp'))]
@ -289,12 +282,13 @@ async def demotivator(ctx: commands.Context) -> None:
if attachment.content_type and attachment.content_type.startswith("image/"):
ext: str = os.path.splitext(attachment.filename)[1]
temp_input = f"tempy{ext}"
await attachment.save(temp_input)
with open(temp_input, "wb") as f:
await attachment.save(f)
input_path: str = temp_input
else:
fallback_image: Optional[str] = get_random_asset_image()
if fallback_image is None:
await ctx.reply(_('no_image_available'))
await ctx.reply(k.no_image_available())
return
temp_input = tempfile.mktemp(suffix=os.path.splitext(fallback_image)[1])
shutil.copy(fallback_image, temp_input)
@ -302,7 +296,7 @@ async def demotivator(ctx: commands.Context) -> None:
else:
fallback_image = get_random_asset_image()
if fallback_image is None:
await ctx.reply(_('no_image_available'))
await ctx.reply(k.no_image_available())
return
temp_input = tempfile.mktemp(suffix=os.path.splitext(fallback_image)[1])
shutil.copy(fallback_image, temp_input)
@ -323,42 +317,43 @@ async def demotivator(ctx: commands.Context) -> None:
bot.remove_command('help')
# Command: Show help information
@bot.hybrid_command(description=f"{(_('command_desc_help'))}")
@bot.hybrid_command(description=f"{k.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'))}",
title=f"{k.command_help_embed_title()}",
description=f"{k.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"]
f"{k.command_help_categories_general()}": ["mem", "talk", "about", "ping", "impact", "demotivator", "help"],
f"{k.command_help_categories_admin()}": ["stats", "retrain", "setlanguage"]
}
custom_commands: List[str] = []
for cog_name, cog in 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'))}"]:
if command.name not in command_categories[f"{k.command_help_categories_general()}"] and command.name not in command_categories[f"{k.command_help_categories_admin()}"]:
custom_commands.append(command.name)
if custom_commands:
embed.add_field(name=f"{(_('command_help_categories_custom'))}", value="\n".join([f"{PREFIX}{command}" for command in custom_commands]), inline=False)
embed.add_field(name=f"{k.command_help_categories_custom()}", value="\n".join([f"{settings["bot"]["prefix"]}{command}" for command in custom_commands]), inline=False)
for category, commands_list in command_categories.items():
commands_in_category: str = "\n".join([f"{PREFIX}{command}" for command in commands_list])
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)
@bot.hybrid_command(description=f"{(_('command_desc_setlang'))}")
@bot.hybrid_command(description=f"{k.command_desc_setlang()}")
@app_commands.describe(locale="Choose your language")
async def setlanguage(ctx: commands.Context, locale: str) -> None:
if ctx.author.id != ownerid:
if ctx.author.id not in settings["bot"]["owner_ids"]:
await ctx.send(":thumbsdown:")
return
await ctx.defer()
set_language(locale)
k.change_language(locale)
await ctx.send(":thumbsup:")
# Event: Called on every message
@ -369,25 +364,32 @@ async def on_message(message: discord.Message) -> None:
if message.author.bot:
return
if str(message.author.id) in BLACKLISTED_USERS:
if str(message.author.id) in settings["bot"]["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)}")
commands = [settings["bot"]["prefix"] + command.name for command in bot.tree.get_commands()]
if message.content.startswith(tuple(commands)):
logger.info(f"{k.command_ran(message.author.name, message.content)}")
await bot.process_commands(message)
return
if profanity.contains_profanity(message.content):
if profanity.contains_profanity(message.content) and settings["bot"]["misc"]["block_profanity"]:
return
if message.content:
if not USERTRAIN_ENABLED:
if not settings["bot"]["user_training"]:
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 = {
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",
@ -409,7 +411,7 @@ async def on_message(message: discord.Message) -> None:
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":
if not settings["bot"]["react_to_messages"]:
return
emoji = random.choice(EMOJIS)
try:
@ -422,27 +424,27 @@ async def on_message(message: discord.Message) -> None:
# 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']}")
logger.info(f"{k.command_ran_s(interaction.user.name)} {interaction.user.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:
if str(ctx.author.id) in settings["bot"]["blacklisted_users"]:
try:
if isinstance(ctx, discord.Interaction):
if not ctx.response.is_done():
await ctx.response.send_message(_('blacklisted'), ephemeral=True)
await ctx.response.send_message(k.blacklisted(), ephemeral=True)
else:
await ctx.followup.send(_('blacklisted'), ephemeral=True)
await ctx.followup.send(k.blacklisted(), ephemeral=True)
else:
await ctx.send(_('blacklisted_user'), ephemeral=True)
await ctx.send(k.blacklisted_user(), ephemeral=True)
except:
pass
return False
return True
# Command: Show bot latency
@bot.hybrid_command(description=f"{(_('command_desc_ping'))}")
@bot.hybrid_command(description=f"{k.command_desc_ping()}")
async def ping(ctx: commands.Context) -> None:
await ctx.defer()
latency: int = round(bot.latency * 1000)
@ -450,35 +452,44 @@ async def ping(ctx: commands.Context) -> None:
LOLembed: discord.Embed = discord.Embed(
title="Pong!!",
description=(
f"{PING_LINE}\n"
f"`{(_('command_ping_embed_desc'))}: {latency}ms`\n"
settings["bot"]["misc"]["ping_line"],
f"`{k.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)
LOLembed.set_footer(text=f"{k.command_ping_footer()} {ctx.author.name}", icon_url=ctx.author.display_avatar.url)
await ctx.send(embed=LOLembed)
# Command: Show about information
@bot.hybrid_command(description=f"{(_('command_about_desc'))}")
@bot.hybrid_command(description=f"{k.command_about_desc()}")
async def about(ctx: commands.Context) -> None:
print("-----------------------------------\n\n")
latest_version: str = check_for_update()
latest_version: str = 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")
embed: discord.Embed = discord.Embed(title=f"{k.command_about_embed_title()}", description="", color=Colour(0x000000))
embed.add_field(name=k.command_about_embed_field1(), value=f"{settings["name"]}", inline=False)
embed.add_field(
name=k.command_about_embed_field2name(),
value=k.command_about_embed_field2value(
local_version=local_version, latest_version=latest_version
),
inline=False
)
embed.add_field(name="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:
if ctx.author.id not in settings["bot"]["owner_ids"]:
return
print("-----------------------------------\n\n")
latest_version: str = check_for_update()
latest_version: str = str(check_for_update())
print("-----------------------------------")
memory_file: str = 'memory.json'
file_size: int = os.path.getsize(memory_file)
@ -486,18 +497,24 @@ async def stats(ctx: commands.Context) -> None:
with open(memory_file, 'r') as file:
line_count: int = sum(1 for _ in file)
embed: discord.Embed = discord.Embed(title=f"{(_('command_stats_embed_title'))}", description=f"{(_('command_stats_embed_desc'))}", color=Colour(0x000000))
embed.add_field(name=f"{(_('command_stats_embed_field1name'))}", value=f"{(_('command_stats_embed_field1value')).format(file_size=file_size, line_count=line_count)}", inline=False)
embed.add_field(name=f"{(_('command_stats_embed_field2name'))}", value=f"{(_('command_stats_embed_field2value')).format(local_version=local_version, latest_version=latest_version)}", inline=False)
embed.add_field(name=f"{(_('command_stats_embed_field3name'))}", value=f"{(_('command_stats_embed_field3value')).format(NAME=NAME, PREFIX=PREFIX, ownerid=ownerid, PING_LINE=PING_LINE, showmemenabled=showmemenabled, USERTRAIN_ENABLED=USERTRAIN_ENABLED, song=song, splashtext=splashtext)}", inline=False)
embed: discord.Embed = discord.Embed(title=f"{k.command_stats_embed_title()}", description=f"{k.command_stats_embed_desc()}", color=Colour(0x000000))
embed.add_field(name=f"{k.command_stats_embed_field1name()}", value=f"{k.command_stats_embed_field1value(file_size=file_size, line_count=line_count)}", inline=False)
embed.add_field(name=f"{k.command_stats_embed_field2name()}", value=f"{k.command_stats_embed_field2value(local_version=local_version, latest_version=latest_version)}", inline=False)
embed.add_field(name=f"{k.command_stats_embed_field3name()}", value=f"{k.command_stats_embed_field3value(
NAME=settings["name"], PREFIX=settings["bot"]["prefix"], ownerid=settings["bot"]["owner_ids"][0],
PING_LINE=settings["bot"]["misc"]["ping_line"], showmemenabled=settings["bot"]["allow_show_mem_command"],
USERTRAIN_ENABLED=settings["bot"]["user_training"], song=settings["bot"]["misc"]["active_song"],
splashtext=splash_text
)}", 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":
if not settings["bot"]["allow_show_mem_command"]:
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)
@ -509,5 +526,36 @@ def improve_sentence_coherence(sentence: str) -> str:
sentence = sentence.replace(" i ", " I ")
return sentence
class OnMyWatch:
watchDirectory = "assets/locales"
def __init__(self):
self.observer = Observer()
def run(self):
event_handler = Handler()
self.observer.schedule(event_handler, self.watchDirectory, recursive = True)
self.observer.start()
try:
while True:
time.sleep(5)
except:
self.observer.stop()
print("Observer Stopped")
self.observer.join()
class Handler(FileSystemEventHandler):
def on_any_event(self, event):
if event.is_directory:
return None
elif event.event_type == 'modified':
build_keys()
# Start the bot
bot.run(TOKEN)
bot.run(os.environ.get("DISCORD_BOT_TOKEN", ""))