mini-vacation update

- Dual logging
  - logfile.log = permanent logfile
  - cur_logfile.log = current run logfile
- Improved logging in general
- Expanded howl replies
- Discord activity logging
- Twitch & Discord chat logging
- Discord slash commands implementation (partial)
- Config file improvements
  - Toggleable log levels
  - Settings separated (terminal and file output)
  - Nesting of associated values
- Fixed "!ping" not fetching correct replies
- Several other minor and major fixes, tweaks and improvements
kami_dev
Kami 2025-02-10 12:32:30 +01:00
parent aed3d24e33
commit 3ad6504d69
11 changed files with 2009 additions and 296 deletions

View File

@ -1,11 +1,13 @@
# bot_discord.py # bot_discord.py
import discord import discord
from discord import app_commands
from discord.ext import commands from discord.ext import commands
import importlib import importlib
import cmd_discord import cmd_discord
import modules import modules
import modules.utility import modules.utility
from modules.db import log_message, lookup_user, log_bot_event
class DiscordBot(commands.Bot): class DiscordBot(commands.Bot):
def __init__(self, config, log_func): def __init__(self, config, log_func):
@ -19,8 +21,17 @@ class DiscordBot(commands.Bot):
self.log("Discord bot initiated") self.log("Discord bot initiated")
cmd_class = str(type(self.commands)).split("'", 2)[1] # async def sync_slash_commands(self):
log_func(f"DiscordBot.commands type: {cmd_class}", "DEBUG") # """Syncs slash commands for the bot."""
# await self.wait_until_ready()
# try:
# await self.tree.sync()
# primary_guild = discord.Object(id=int(self.config["discord_guilds"][0]))
# await self.tree.sync(guild=primary_guild)
# self.log("Discord slash commands synced.")
# except Exception as e:
# self.log(f"Unable to sync Discord slash commands: {e}", "ERROR")
def set_db_connection(self, db_conn): def set_db_connection(self, db_conn):
""" """
@ -33,31 +44,288 @@ class DiscordBot(commands.Bot):
Load all commands from cmd_discord.py Load all commands from cmd_discord.py
""" """
try: try:
importlib.reload(cmd_discord) importlib.reload(cmd_discord) # Reload the commands file
cmd_discord.setup(self) cmd_discord.setup(self) # Ensure commands are registered
self.log("Discord commands loaded successfully.") self.log("Discord commands loaded successfully.")
# Now load the help info from dictionary/help_discord.json # Load help info
help_json_path = "dictionary/help_discord.json" help_json_path = "dictionary/help_discord.json"
modules.utility.initialize_help_data( modules.utility.initialize_help_data(
bot=self, bot=self,
help_json_path=help_json_path, help_json_path=help_json_path,
is_discord=True, is_discord=True,
log_func=self.log log_func=self.log
) )
except Exception as e: except Exception as e:
self.log(f"Error loading Discord commands: {e}", "ERROR") self.log(f"Error loading Discord commands: {e}", "ERROR")
async def on_message(self, message):
self.log(f"Message detected, attempting UUI lookup on {message.author.name} ...", "DEBUG")
try:
# If it's a bot message, ignore or pass user_is_bot=True
is_bot = message.author.bot
user_id = str(message.author.id)
user_name = message.author.name # no discriminator
display_name = message.author.display_name
modules.utility.track_user_activity(
db_conn=self.db_conn,
log_func=self.log,
platform="discord",
user_id=user_id,
username=user_name,
display_name=display_name,
user_is_bot=is_bot
)
self.log(f"... UUI lookup complete", "DEBUG")
user_data = lookup_user(db_conn=self.db_conn, log_func=self.log, identifier=user_id, identifier_type="discord_user_id")
user_uuid = user_data["UUID"] if user_data else "UNKNOWN"
if user_uuid:
# The "platform" can be e.g. "discord" or you can store the server name
platform_str = f"discord-{message.guild.name}" if message.guild else "discord-DM"
# The channel name can be message.channel.name or "DM" if it's a private channel
channel_str = message.channel.name if hasattr(message.channel, "name") else "DM"
# If you have attachments, you could gather them as links.
try:
attachments = ", ".join(a.url for a in message.attachments) if message.attachments else ""
except Exception:
attachments = ""
log_message(
db_conn=self.db_conn,
log_func=self.log,
user_uuid=user_uuid,
message_content=message.content or "",
platform=platform_str,
channel=channel_str,
attachments=attachments
)
# PLACEHOLDER FOR FUTURE MESSAGE PROCESSING
except Exception as e:
self.log(f"... UUI lookup failed: {e}", "WARNING")
pass
try:
# Pass message contents to commands processing
await self.process_commands(message)
self.log(f"Command processing complete", "DEBUG")
except Exception as e:
self.log(f"Command processing failed: {e}", "ERROR")
async def on_command(self, ctx): async def on_command(self, ctx):
"""Logs every command execution at DEBUG level.""" """Logs every command execution at DEBUG level."""
_cmd_args = str(ctx.message.content).split(" ")[1:] _cmd_args = str(ctx.message.content).split(" ")[1:]
self.log(f"Command '{ctx.command}' (Discord) initiated by {ctx.author} in #{ctx.channel}", "DEBUG") channel_name = "Direct Message" if "Direct Message with" in str(ctx.channel) else ctx.channel
self.log(f"Command '{ctx.command}' (Discord) initiated by {ctx.author} in #{channel_name}", "DEBUG")
if len(_cmd_args) > 1: self.log(f"!{ctx.command} arguments: {_cmd_args}", "DEBUG") if len(_cmd_args) > 1: self.log(f"!{ctx.command} arguments: {_cmd_args}", "DEBUG")
async def on_ready(self): async def on_ready(self):
"""Runs when the bot successfully logs in."""
# Sync Slash Commands
try:
# Sync slash commands globally
#await self.tree.sync()
#self.log("Discord slash commands synced.")
primary_guild_int = int(self.config["discord_guilds"][0])
primary_guild = discord.Object(id=primary_guild_int)
await self.tree.sync(guild=primary_guild)
self.log(f"Discord slash commands force synced to guild: {primary_guild_int}")
except Exception as e:
self.log(f"Unable to sync Discord slash commands: {e}")
# Log successful bot startup
self.log(f"Discord bot is online as {self.user}") self.log(f"Discord bot is online as {self.user}")
log_bot_event(self.db_conn, self.log, "DISCORD_RECONNECTED", "Discord bot logged in.")
async def on_disconnect(self):
self.log("Discord bot has lost connection!", "WARNING")
log_bot_event(self.db_conn, self.log, "DISCORD_DISCONNECTED", "Discord bot lost connection.")
async def on_voice_state_update(self, member, before, after):
"""
Tracks user joins, leaves, mutes, deafens, streams, and voice channel moves.
"""
guild_id = str(member.guild.id)
discord_user_id = str(member.id)
voice_channel = after.channel.name if after.channel else before.channel.name if before.channel else None
# Ensure user exists in the UUI system
user_uuid = modules.db.lookup_user(self.db_conn, self.log, identifier=discord_user_id, identifier_type="discord_user_id", target_identifier="UUID")
if not user_uuid:
self.log(f"User {member.name} ({discord_user_id}) not found in 'users'. Attempting to add...", "WARNING")
modules.utility.track_user_activity(
db_conn=self.db_conn,
log_func=self.log,
platform="discord",
user_id=discord_user_id,
username=member.name,
display_name=member.display_name,
user_is_bot=member.bot
)
user_uuid= modules.db.lookup_user(self.db_conn, self.log, identifier=discord_user_id, identifier_type="discord_user_id", target_identifier="UUID")
if not user_uuid:
self.log(f"ERROR: Failed to associate {member.name} ({discord_user_id}) with a UUID. Skipping activity log.", "ERROR")
return # Prevent logging with invalid UUID
# Detect join and leave events
if before.channel is None and after.channel is not None:
modules.db.log_discord_activity(self.db_conn, self.log, guild_id, user_uuid, "JOIN", after.channel.name)
elif before.channel is not None and after.channel is None:
modules.db.log_discord_activity(self.db_conn, self.log, guild_id, user_uuid, "LEAVE", before.channel.name)
# Detect VC moves (self/moved)
if before.channel and after.channel and before.channel != after.channel:
move_detail = f"{before.channel.name} -> {after.channel.name}"
modules.db.log_discord_activity(self.db_conn, self.log, guild_id, user_uuid, "VC_MOVE", after.channel.name, move_detail)
# Detect mute/unmute
if before.self_mute != after.self_mute:
mute_action = "MUTE" if after.self_mute else "UNMUTE"
modules.db.log_discord_activity(self.db_conn, self.log, guild_id, user_uuid, mute_action, voice_channel)
# Detect deafen/undeafen
if before.self_deaf != after.self_deaf:
deaf_action = "DEAFEN" if after.self_deaf else "UNDEAFEN"
modules.db.log_discord_activity(self.db_conn, self.log, guild_id, user_uuid, deaf_action, voice_channel)
# Detect streaming
if before.self_stream != after.self_stream:
stream_action = "STREAM_START" if after.self_stream else "STREAM_STOP"
modules.db.log_discord_activity(self.db_conn, self.log, guild_id, user_uuid, stream_action, voice_channel)
# Detect camera usage
if before.self_video != after.self_video:
camera_action = "CAMERA_ON" if after.self_video else "CAMERA_OFF"
modules.db.log_discord_activity(self.db_conn, self.log, guild_id, user_uuid, camera_action, voice_channel)
async def on_presence_update(self, before, after):
"""
Detects when a user starts or stops a game, Spotify, or Discord activity.
Ensures the activity is logged using the correct UUID from the UUI system.
"""
if not after.guild: # Ensure it's in a guild (server)
return
guild_id = str(after.guild.id)
discord_user_id = str(after.id)
# Ensure user exists in the UUI system
user_uuid = modules.db.lookup_user(
self.db_conn,
self.log,
identifier=discord_user_id,
identifier_type="discord_user_id",
target_identifier="UUID"
)
if not user_uuid:
self.log(f"User {after.name} ({discord_user_id}) not found in 'users'. Attempting to add...", "WARNING")
modules.utility.track_user_activity(
db_conn=self.db_conn,
log_func=self.log,
platform="discord",
user_id=discord_user_id,
username=after.name,
display_name=after.display_name,
user_is_bot=after.bot
)
user_uuid = modules.db.lookup_user(self.db_conn, self.log, identifier=discord_user_id, identifier_type="discord_user_id", target_identifier="UUID")
if not user_uuid:
self.log(f"ERROR: Failed to associate {after.name} ({discord_user_id}) with a UUID. Skipping activity log.", "ERROR")
return
# Check all activities
new_activity = None
for n_activity in after.activities:
if isinstance(n_activity, discord.Game):
new_activity = ("GAME_START", n_activity.name)
elif isinstance(n_activity, discord.Spotify):
# Get artist name(s) and format as "{artist_name} - {song_title}"
artist_name = ", ".join(n_activity.artists)
song_name = n_activity.title
spotify_detail = f"{artist_name} - {song_name}"
new_activity = ("LISTENING_SPOTIFY", spotify_detail)
elif isinstance(n_activity, discord.Streaming):
new_activity = ("STREAM_START", n_activity.game or "Sharing screen")
# Check all activities
old_activity = None
for o_activity in before.activities:
if isinstance(o_activity, discord.Game):
old_activity = ("GAME_STOP", o_activity.name)
# IGNORE OLD SPOTIFY EVENTS
elif isinstance(o_activity, discord.Streaming):
old_activity = ("STREAM_STOP", o_activity.game or "Sharing screen")
if new_activity:
modules.db.log_discord_activity(self.db_conn, self.log, guild_id, user_uuid, new_activity[0], None, new_activity[1])
if old_activity:
modules.db.log_discord_activity(self.db_conn, self.log, guild_id, user_uuid, old_activity[0], None, old_activity[1])
# async def start_account_linking(self, interaction: discord.Interaction):
# """Starts the linking process by generating a link code and displaying instructions."""
# user_id = str(interaction.user.id)
# # Check if the user already has a linked account
# user_data = modules.db.lookup_user(self.db_conn, self.log, identifier=user_id, identifier_type="discord_user_id")
# if user_data and user_data["twitch_user_id"]:
# link_date = user_data["datetime_linked"]
# await interaction.response.send_message(
# f"Your Discord account is already linked to Twitch user **{user_data['twitch_user_display_name']}** "
# f"(linked on {link_date}). You must remove the link before linking another account.", ephemeral=True)
# return
# # Generate a unique link code
# link_code = modules.utility.generate_link_code()
# modules.db.run_db_operation(
# self.db_conn, "write",
# "INSERT INTO link_codes (DISCORD_USER_ID, LINK_CODE) VALUES (?, ?)",
# (user_id, link_code), self.log
# )
# # Show the user the link modal
# await interaction.response.send_message(
# f"To link your Twitch account, post the following message in Twitch chat:\n"
# f"`!acc_link {link_code}`\n\n"
# f"Then, return here and click 'Done'.", ephemeral=True
# )
# async def finalize_account_linking(self, interaction: discord.Interaction):
# """Finalizes the linking process by merging duplicate UUIDs."""
# from modules import db
# user_id = str(interaction.user.id)
# # Fetch the updated user info
# user_data = modules.db.lookup_user(self.db_conn, self.log, identifier=user_id, identifier_type="discord_user_id")
# if not user_data or not user_data["twitch_user_id"]:
# await interaction.response.send_message(
# "No linked Twitch account found. Please complete the linking process first.", ephemeral=True)
# return
# discord_uuid = user_data["UUID"]
# twitch_uuid = modules.db.lookup_user(self.db_conn, self.log, identifier=user_data["twitch_user_id"], identifier_type="twitch_user_id")["UUID"]
# if discord_uuid == twitch_uuid:
# await interaction.response.send_message("Your accounts are already fully linked.", ephemeral=True)
# return
# # Merge all records from `twitch_uuid` into `discord_uuid`
# db.merge_uuid_data(self.db_conn, self.log, old_uuid=twitch_uuid, new_uuid=discord_uuid)
# # Delete the old Twitch UUID entry
# db.run_db_operation(self.db_conn, "write", "DELETE FROM users WHERE UUID = ?", (twitch_uuid,), self.log)
# # Confirm the final linking
# await interaction.response.send_message("Your Twitch and Discord accounts are now fully linked.", ephemeral=True)
async def run(self, token): async def run(self, token):
try: try:

View File

@ -8,6 +8,7 @@ import cmd_twitch
import modules import modules
import modules.utility import modules.utility
from modules.db import log_message, lookup_user, log_bot_event
class TwitchBot(commands.Bot): class TwitchBot(commands.Bot):
def __init__(self, config, log_func): def __init__(self, config, log_func):
@ -29,9 +30,6 @@ class TwitchBot(commands.Bot):
self.log("Twitch bot initiated") self.log("Twitch bot initiated")
cmd_class = str(type(self._commands)).split("'", 2)[1]
log_func(f"TwitchBot._commands type: {cmd_class}", "DEBUG")
# 2) Then load commands # 2) Then load commands
self.load_commands() self.load_commands()
@ -42,9 +40,13 @@ class TwitchBot(commands.Bot):
self.db_conn = db_conn self.db_conn = db_conn
async def event_message(self, message): async def event_message(self, message):
"""Logs and processes incoming Twitch messages.""" """
Called every time a Twitch message is received (chat message in a channel).
We'll use this to track the user in our 'users' table.
"""
# If it's the bot's own message, ignore
if message.echo: if message.echo:
return # Ignore bot's own messages return
# Log the command if it's a command # Log the command if it's a command
if message.content.startswith("!"): if message.content.startswith("!"):
@ -54,11 +56,58 @@ class TwitchBot(commands.Bot):
self.log(f"Command '{_cmd}' (Twitch) initiated by {message.author.name} in #{message.channel.name}", "DEBUG") self.log(f"Command '{_cmd}' (Twitch) initiated by {message.author.name} in #{message.channel.name}", "DEBUG")
if len(_cmd_args) > 1: self.log(f"!{_cmd} arguments: {_cmd_args}", "DEBUG") if len(_cmd_args) > 1: self.log(f"!{_cmd} arguments: {_cmd_args}", "DEBUG")
# Process the message for command execution try:
# Typically message.author is not None for normal chat messages
author = message.author
if not author: # just in case
return
is_bot = False # TODO Implement automatic bot account check
user_id = str(author.id)
user_name = author.name
display_name = author.display_name or user_name
self.log(f"Message detected, attempting UUI lookup on {user_name} ...", "DEBUG")
modules.utility.track_user_activity(
db_conn=self.db_conn,
log_func=self.log,
platform="twitch",
user_id=user_id,
username=user_name,
display_name=display_name,
user_is_bot=is_bot
)
self.log("... UUI lookup complete.", "DEBUG")
user_data = lookup_user(db_conn=self.db_conn, log_func=self.log, identifier=str(message.author.id), identifier_type="twitch_user_id")
user_uuid = user_data["UUID"] if user_data else "UNKNOWN"
from modules.db import log_message
log_message(
db_conn=self.db_conn,
log_func=self.log,
user_uuid=user_uuid,
message_content=message.content or "",
platform="twitch",
channel=message.channel.name,
attachments=""
)
except Exception as e:
self.log(f"... UUI lookup failed: {e}", "ERROR")
# Pass message contents to commands processing
await self.handle_commands(message) await self.handle_commands(message)
async def event_ready(self): async def event_ready(self):
self.log(f"Twitch bot is online as {self.nick}") self.log(f"Twitch bot is online as {self.nick}")
log_bot_event(self.db_conn, self.log, "TWITCH_RECONNECTED", "Twitch bot logged in.")
async def event_disconnected(self):
self.log("Twitch bot has lost connection!", "WARNING")
log_bot_event(self.db_conn, self.log, "TWITCH_DISCONNECTED", "Twitch bot lost connection.")
async def refresh_access_token(self): async def refresh_access_token(self):
""" """

77
bots.py
View File

@ -6,6 +6,7 @@ import sys
import time import time
import traceback import traceback
import globals import globals
from functools import partial
from discord.ext import commands from discord.ext import commands
from dotenv import load_dotenv from dotenv import load_dotenv
@ -13,8 +14,10 @@ from dotenv import load_dotenv
from bot_discord import DiscordBot from bot_discord import DiscordBot
from bot_twitch import TwitchBot from bot_twitch import TwitchBot
from modules.db import init_db_connection, run_db_operation #from modules.db import init_db_connection, run_db_operation
from modules.db import ensure_quotes_table, ensure_users_table #from modules.db import ensure_quotes_table, ensure_users_table, ensure_chatlog_table, checkenable_db_fk
from modules import db, utility
# Load environment variables # Load environment variables
load_dotenv() load_dotenv()
@ -32,9 +35,11 @@ except json.JSONDecodeError as e:
sys.exit(1) sys.exit(1)
# Initiate logfile # Initiate logfile
logfile_path = config_data["logfile_path"] logfile_path = config_data["logging"]["logfile_path"]
logfile = open(logfile_path, "a") logfile = open(logfile_path, "a")
if not config_data["log_to_terminal"] and not config_data["log_to_file"]: cur_logfile_path = f"cur_{logfile_path}"
cur_logfile = open(cur_logfile_path, "w")
if not config_data["logging"]["terminal"]["log_to_terminal"] and not config_data["logging"]["file"]["log_to_file"]:
print(f"!!! WARNING !!! CONSOLE AND LOGFILE OUTPUT DISABLED !!!\n!!! NO LOGS WILL BE PROVIDED !!!") print(f"!!! WARNING !!! CONSOLE AND LOGFILE OUTPUT DISABLED !!!\n!!! NO LOGS WILL BE PROVIDED !!!")
############################### ###############################
@ -61,7 +66,7 @@ def log(message, level="INFO", exec_info=False):
if level not in log_levels: if level not in log_levels:
level = "INFO" # Default to INFO if an invalid level is provided level = "INFO" # Default to INFO if an invalid level is provided
if level in config_data["log_levels"] or level == "FATAL": if level in config_data["logging"]["log_levels"] or level == "FATAL":
elapsed = time.time() - globals.get_bot_start_time() elapsed = time.time() - globals.get_bot_start_time()
uptime_str, _ = utility.format_uptime(elapsed) uptime_str, _ = utility.format_uptime(elapsed)
timestamp = time.strftime('%Y-%m-%d %H:%M:%S') timestamp = time.strftime('%Y-%m-%d %H:%M:%S')
@ -72,21 +77,33 @@ def log(message, level="INFO", exec_info=False):
log_message += f"\n{traceback.format_exc()}" log_message += f"\n{traceback.format_exc()}"
# Print to terminal if enabled # Print to terminal if enabled
if config_data["log_to_terminal"] or level == "FATAL": # 'FATAL' errors override settings
# Checks config file to see enabled/disabled logging levels
if config_data["logging"]["terminal"]["log_to_terminal"] or level == "FATAL":
config_level_format = f"log_{level.lower()}"
if config_data["logging"]["terminal"][config_level_format] or level == "FATAL":
print(log_message) print(log_message)
# Write to file if enabled # Write to file if enabled
if config_data["log_to_file"]: # 'FATAL' errors override settings
# Checks config file to see enabled/disabled logging levels
if config_data["logging"]["file"]["log_to_file"] or level == "FATAL":
config_level_format = f"log_{level.lower()}"
if config_data["logging"]["file"][config_level_format] or level == "FATAL":
try: try:
with open(config_data["logfile_path"], "a", encoding="utf-8") as logfile: lf = config_data["logging"]["logfile_path"]
clf = f"cur_{lf}"
with open(lf, "a", encoding="utf-8") as logfile: # Write to permanent logfile
logfile.write(f"{log_message}\n") logfile.write(f"{log_message}\n")
logfile.flush() # Ensure it gets written immediately logfile.flush() # Ensure it gets written immediately
with open(clf, "a", encoding="utf-8") as c_logfile: # Write to this-run logfile
c_logfile.write(f"{log_message}\n")
c_logfile.flush() # Ensure it gets written immediately
except Exception as e: except Exception as e:
print(f"[WARNING] Failed to write to logfile: {e}") print(f"[WARNING] Failed to write to logfile: {e}")
# Handle fatal errors with shutdown # Handle fatal errors with shutdown
if level == "FATAL": if level == "FATAL":
if config_data["log_to_terminal"]:
print(f"!!! FATAL ERROR LOGGED, SHUTTING DOWN !!!") print(f"!!! FATAL ERROR LOGGED, SHUTTING DOWN !!!")
sys.exit(1) sys.exit(1)
@ -100,18 +117,36 @@ async def main():
# Log initial start # Log initial start
log("--------------- BOT STARTUP ---------------") log("--------------- BOT STARTUP ---------------")
# Before creating your DiscordBot/TwitchBot, initialize DB # Before creating your DiscordBot/TwitchBot, initialize DB
db_conn = init_db_connection(config_data, log) try:
db_conn = db.init_db_connection(config_data, log)
if not db_conn: if not db_conn:
# If we get None, it means FATAL. We might sys.exit(1) or handle it differently. # If we get None, it means FATAL. We might sys.exit(1) or handle it differently.
log("Terminating bot due to no DB connection.", "FATAL") log("Terminating bot due to no DB connection.", "FATAL")
sys.exit(1) sys.exit(1)
# auto-create the quotes table if it doesn't exist
try:
ensure_quotes_table(db_conn, log)
ensure_users_table(db_conn, log)
except Exception as e: except Exception as e:
log(f"Critical: unable to ensure quotes table: {e}", "FATAL") log(f"Unable to initialize database!: {e}", "FATAL")
try: # Ensure FKs are enabled
db.checkenable_db_fk(db_conn, log)
except Exception as e:
log(f"Unable to ensure Foreign keys are enabled: {e}", "WARNING")
# auto-create the quotes table if it doesn't exist
tables = {
"Bot events table": partial(db.ensure_bot_events_table, db_conn, log),
"Quotes table": partial(db.ensure_quotes_table, db_conn, log),
"Users table": partial(db.ensure_users_table, db_conn, log),
"Chatlog table": partial(db.ensure_chatlog_table, db_conn, log),
"Howls table": partial(db.ensure_userhowls_table, db_conn, log),
"Discord activity table": partial(db.ensure_discord_activity_table, db_conn, log),
"Account linking table": partial(db.ensure_link_codes_table, db_conn, log)
}
try:
for table, func in tables.items():
func() # Call the function with db_conn and log already provided
log(f"{table} ensured.", "DEBUG")
except Exception as e:
log(f"Unable to ensure DB tables exist: {e}", "FATAL")
log("Initializing bots...") log("Initializing bots...")
@ -119,6 +154,9 @@ async def main():
discord_bot = DiscordBot(config_data, log) discord_bot = DiscordBot(config_data, log)
twitch_bot = TwitchBot(config_data, log) twitch_bot = TwitchBot(config_data, log)
# Log startup
utility.log_bot_startup(db_conn, log)
# Provide DB connection to both bots # Provide DB connection to both bots
try: try:
discord_bot.set_db_connection(db_conn) discord_bot.set_db_connection(db_conn)
@ -133,7 +171,9 @@ async def main():
twitch_task = asyncio.create_task(twitch_bot.run()) twitch_task = asyncio.create_task(twitch_bot.run())
from modules.utility import dev_func from modules.utility import dev_func
dev_func_result = dev_func(db_conn, log) enable_dev_func = False
if enable_dev_func:
dev_func_result = dev_func(db_conn, log, enable_dev_func)
log(f"dev_func output: {dev_func_result}") log(f"dev_func output: {dev_func_result}")
await asyncio.gather(discord_task, twitch_task) await asyncio.gather(discord_task, twitch_task)
@ -141,6 +181,9 @@ async def main():
if __name__ == "__main__": if __name__ == "__main__":
try: try:
asyncio.run(main()) asyncio.run(main())
except KeyboardInterrupt:
utility.log_bot_shutdown(db_conn, log, intent="User Shutdown")
except Exception as e: except Exception as e:
error_trace = traceback.format_exc() error_trace = traceback.format_exc()
log(f"Fatal Error: {e}\n{error_trace}", "FATAL") log(f"Fatal Error: {e}\n{error_trace}", "FATAL")
utility.log_bot_shutdown(db_conn, log)

View File

@ -4,41 +4,190 @@ import time
from modules import utility from modules import utility
import globals import globals
from modules.db import run_db_operation from modules import db
def howl(username: str) -> str: #def howl(username: str) -> str:
# """
# Generates a howl response based on a random percentage.
# Uses a dictionary to allow flexible, randomized responses.
# """
# howl_percentage = random.randint(0, 100)
#
# # Round percentage down to nearest 10 (except 0 and 100)
# rounded_percentage = 0 if howl_percentage == 0 else 100 if howl_percentage == 100 else (howl_percentage // 10) * 10
#
# # Fetch a random response from the dictionary
# response = utility.get_random_reply("howl_replies", str(rounded_percentage), username=username, howl_percentage=howl_percentage)
#
# return response
def handle_howl_command(ctx) -> str:
""" """
Generates a howl response based on a random percentage. A single function that handles !howl logic for both Discord and Twitch.
Uses a dictionary to allow flexible, randomized responses. We rely on ctx to figure out the platform, the user, the arguments, etc.
Return a string that the caller will send.
""" """
howl_percentage = random.randint(0, 100)
# Round percentage down to nearest 10 (except 0 and 100) # 1) Detect which platform
rounded_percentage = 0 if howl_percentage == 0 else 100 if howl_percentage == 100 else (howl_percentage // 10) * 10 # We might do something like:
platform, author_id, author_name, author_display_name, args = extract_ctx_info(ctx)
# Fetch a random response from the dictionary # 2) Subcommand detection
response = utility.get_random_reply("howl_replies", str(rounded_percentage), username=username, howl_percentage=howl_percentage) if args and args[0].lower() in ("stat", "stats"):
# we are in stats mode
if len(args) > 1:
if args[1].lower() in ("all", "global", "community"):
target_name = "_COMMUNITY_"
target_name = args[1]
else:
target_name = author_name
return handle_howl_stats(ctx, platform, target_name)
else:
# normal usage => random generation
return handle_howl_normal(ctx, platform, author_id, author_display_name)
def extract_ctx_info(ctx):
"""
Figures out if this is Discord or Twitch,
returns (platform_str, author_id, author_name, author_display_name, args).
"""
# Is it discord.py or twitchio?
if hasattr(ctx, "guild"): # typically means discord.py context
platform_str = "discord"
author_id = str(ctx.author.id)
author_name = ctx.author.name
author_display_name = ctx.author.display_name
# parse arguments from ctx.message.content
parts = ctx.message.content.strip().split()
args = parts[1:] if len(parts) > 1 else []
else:
# assume twitchio
platform_str = "twitch"
author = ctx.author
author_id = str(author.id)
author_name = author.name
author_display_name = author.display_name or author.name
# parse arguments from ctx.message.content
parts = ctx.message.content.strip().split()
args = parts[1:] if len(parts) > 1 else []
return (platform_str, author_id, author_name, author_display_name, args)
def handle_howl_normal(ctx, platform, author_id, author_display_name) -> str:
"""
Normal usage: random generation, store in DB.
"""
db_conn = ctx.bot.db_conn
log_func = ctx.bot.log
# random logic
howl_val = random.randint(0, 100)
# round to nearest 10 except 0/100
rounded_val = 0 if howl_val == 0 else \
100 if howl_val == 100 else \
(howl_val // 10) * 10
# dictionary-based reply
reply = utility.get_random_reply(
"howl_replies",
str(rounded_val),
username=author_display_name,
howl_percentage=howl_val
)
# find user in DB by ID
user_data = db.lookup_user(db_conn, log_func, identifier=author_id, identifier_type=platform)
if user_data:
db.insert_howl(db_conn, log_func, user_data["UUID"], howl_val)
else:
log_func(f"Could not find user by ID={author_id} on {platform}. Not storing howl.", "WARNING")
return reply
def handle_howl_stats(ctx, platform, target_name) -> str:
db_conn = ctx.bot.db_conn
log_func = ctx.bot.log
# Check if requesting global stats
if target_name in ("_COMMUNITY_", "all", "global", "community"):
stats = db.get_global_howl_stats(db_conn, log_func)
if not stats:
return "No howls have been recorded yet!"
total_howls = stats["total_howls"]
avg_howl = stats["average_howl"]
unique_users = stats["unique_users"]
count_zero = stats["count_zero"]
count_hundred = stats["count_hundred"]
return (f"**Community Howl Stats:**\n"
f"Total Howls: {total_howls}\n"
f"Average Howl: {avg_howl:.1f}%\n"
f"Unique Howlers: {unique_users}\n"
f"0% Howls: {count_zero}, 100% Howls: {count_hundred}")
# Otherwise, lookup a single user
user_data = lookup_user_by_name(db_conn, log_func, platform, target_name)
if not user_data:
return f"I don't know that user: {target_name}"
stats = db.get_howl_stats(db_conn, log_func, user_data["UUID"])
if not stats:
return f"{target_name} hasn't howled yet! (Try `!howl` to get started.)"
c = stats["count"]
a = stats["average"]
z = stats["count_zero"]
h = stats["count_hundred"]
return (f"{target_name} has howled {c} times, averaging {a:.1f}% "
f"(0% x{z}, 100% x{h})")
def lookup_user_by_name(db_conn, log_func, platform, name_str):
"""
Attempt to find a user by name on that platform, e.g. 'discord_username' or 'twitch_username'.
"""
# same logic as before
if platform == "discord":
ud = db.lookup_user(db_conn, log_func, name_str, "discord_user_display_name")
if ud:
return ud
ud = db.lookup_user(db_conn, log_func, name_str, "discord_username")
return ud
elif platform == "twitch":
ud = db.lookup_user(db_conn, log_func, name_str, "twitch_user_display_name")
if ud:
return ud
ud = db.lookup_user(db_conn, log_func, name_str, "twitch_username")
return ud
else:
log_func(f"Unknown platform {platform} in lookup_user_by_name", "WARNING")
return None
return response
def ping() -> str: def ping() -> str:
""" """
Returns a dynamic, randomized uptime response. Returns a dynamic, randomized uptime response.
""" """
debug = False
# Use function to retrieve correct startup time and calculate uptime # Use function to retrieve correct startup time and calculate uptime
elapsed = time.time() - globals.get_bot_start_time() elapsed = time.time() - globals.get_bot_start_time()
uptime_str, uptime_s = utility.format_uptime(elapsed) uptime_str, uptime_s = utility.format_uptime(elapsed)
# Define threshold categories # Define threshold categories
thresholds = [3600, 10800, 21600, 43200, 86400, 172800, 259200, 345600, thresholds = [600, 1800, 3600, 10800, 21600, 43200, 86400, 172800, 259200, 345600,
432000, 518400, 604800, 1209600, 2592000, 7776000, 15552000, 23328000, 31536000] 432000, 518400, 604800, 1209600, 2592000, 7776000, 15552000, 23328000, 31536000]
# Find the highest matching threshold # Find the highest matching threshold
selected_threshold = max([t for t in thresholds if uptime_s >= t], default=3600) selected_threshold = max([t for t in thresholds if uptime_s >= t], default=600)
# Get a random response from the dictionary # Get a random response from the dictionary
response = utility.get_random_reply("ping_replies", str(selected_threshold), uptime_str=uptime_str) response = utility.get_random_reply(dictionary_name="ping_replies", category=str(selected_threshold), uptime_str=uptime_str)
if debug:
print(f"Elapsed time: {elapsed}\nuptime_str: {uptime_str}\nuptime_s: {uptime_s}\nselected threshold: {selected_threshold}\nresponse: {response}")
return response return response
@ -92,7 +241,7 @@ def create_quotes_table(db_conn, log_func):
) )
""" """
run_db_operation(db_conn, "write", create_table_sql, log_func=log_func) db.run_db_operation(db_conn, "write", create_table_sql, log_func=log_func)
async def handle_quote_command(db_conn, log_func, is_discord: bool, ctx, args, get_twitch_game_for_channel=None): async def handle_quote_command(db_conn, log_func, is_discord: bool, ctx, args, get_twitch_game_for_channel=None):
@ -130,12 +279,12 @@ async def handle_quote_command(db_conn, log_func, is_discord: bool, ctx, args, g
elif sub == "remove": elif sub == "remove":
if len(args) < 2: if len(args) < 2:
return await send_message(ctx, "Please specify which quote ID to remove.") return await send_message(ctx, "Please specify which quote ID to remove.")
await remove_quote(db_conn, log_func, ctx, args[1]) await remove_quote(db_conn, log_func, is_discord, ctx, quote_id_str=args[1])
else: else:
# Possibly a quote ID # Possibly a quote ID
if sub.isdigit(): if sub.isdigit():
quote_id = int(sub) quote_id = int(sub)
await retrieve_specific_quote(db_conn, log_func, ctx, quote_id) await retrieve_specific_quote(db_conn, log_func, ctx, quote_id, is_discord)
else: else:
# unrecognized subcommand => fallback to random # unrecognized subcommand => fallback to random
await retrieve_random_quote(db_conn, log_func, is_discord, ctx) await retrieve_random_quote(db_conn, log_func, is_discord, ctx)
@ -143,45 +292,59 @@ async def handle_quote_command(db_conn, log_func, is_discord: bool, ctx, args, g
async def add_new_quote(db_conn, log_func, is_discord, ctx, quote_text, get_twitch_game_for_channel): async def add_new_quote(db_conn, log_func, is_discord, ctx, quote_text, get_twitch_game_for_channel):
""" """
Insert a new quote into the DB. Inserts a new quote with UUID instead of username.
QUOTEE = the user who typed the command
QUOTE_CHANNEL = "Discord" or the twitch channel name
QUOTE_GAME = The current game if from Twitch, None if from Discord
QUOTE_REMOVED = false by default
QUOTE_DATETIME = current date/time (or DB default)
""" """
user_name = get_author_name(ctx, is_discord) user_id = str(ctx.author.id)
channel_name = "Discord" if is_discord else get_channel_name(ctx) platform = "discord" if is_discord else "twitch"
# Lookup UUID from users table
user_data = db.lookup_user(db_conn, log_func, identifier=user_id, identifier_type=f"{platform}_user_id")
if not user_data:
log_func(f"ERROR: Could not find UUID for user {ctx.author.name} ({user_id}) on {platform}. Quote not saved.", "ERROR")
await ctx.send("Could not save quote. Your user data is missing from the system.")
return
user_uuid = user_data["UUID"]
channel_name = "Discord" if is_discord else ctx.channel.name
game_name = None game_name = None
if not is_discord and get_twitch_game_for_channel: if not is_discord and get_twitch_game_for_channel:
# Attempt to get the current game from the Twitch API (placeholder function) game_name = get_twitch_game_for_channel(channel_name) # Retrieve game if Twitch
game_name = get_twitch_game_for_channel(channel_name) # might return str or None
# Insert quote # Insert quote
insert_sql = """ insert_sql = """
INSERT INTO quotes (QUOTE_TEXT, QUOTEE, QUOTE_CHANNEL, QUOTE_DATETIME, QUOTE_GAME, QUOTE_REMOVED) INSERT INTO quotes (QUOTE_TEXT, QUOTEE, QUOTE_CHANNEL, QUOTE_DATETIME, QUOTE_GAME, QUOTE_REMOVED)
VALUES (?, ?, ?, CURRENT_TIMESTAMP, ?, 0) VALUES (?, ?, ?, CURRENT_TIMESTAMP, ?, 0)
""" """
# For MariaDB, parameter placeholders are often %s, but if you set paramstyle='qmark', it can use ? as well. params = (quote_text, user_uuid, channel_name, game_name)
# Adjust if needed for your environment.
params = (quote_text, user_name, channel_name, game_name)
result = run_db_operation(db_conn, "write", insert_sql, params, log_func=log_func) result = db.run_db_operation(db_conn, "write", insert_sql, params, log_func=log_func)
if result is not None: if result is not None:
await send_message(ctx, "Quote added successfully!") await ctx.send("Quote added successfully!")
else: else:
await send_message(ctx, "Failed to add quote.") await ctx.send("Failed to add quote.")
async def remove_quote(db_conn, log_func, ctx, quote_id_str): async def remove_quote(db_conn, log_func, is_discord: bool, ctx, quote_id_str):
""" """
Mark quote #ID as removed (QUOTE_REMOVED=1). Mark quote #ID as removed (QUOTE_REMOVED=1).
""" """
if not quote_id_str.isdigit(): if not quote_id_str.isdigit():
return await send_message(ctx, f"'{quote_id_str}' is not a valid quote ID.") return await send_message(ctx, f"'{quote_id_str}' is not a valid quote ID.")
user_id = str(ctx.author.id)
platform = "discord" if is_discord else "twitch"
# Lookup UUID from users table
user_data = db.lookup_user(db_conn, log_func, identifier=user_id, identifier_type=f"{platform}_user_id")
if not user_data:
log_func(f"ERROR: Could not find UUID for user {ctx.author.name} ({user_id}) on {platform}. Quote not removed.", "ERROR")
await ctx.send("Could not remove quote. Your user data is missing from the system.")
return
user_uuid = user_data["UUID"]
quote_id = int(quote_id_str) quote_id = int(quote_id_str)
remover_user = str(ctx.author.name) remover_user = str(user_uuid)
# Mark as removed # Mark as removed
update_sql = """ update_sql = """
@ -192,7 +355,7 @@ async def remove_quote(db_conn, log_func, ctx, quote_id_str):
AND QUOTE_REMOVED = 0 AND QUOTE_REMOVED = 0
""" """
params = (remover_user, quote_id) params = (remover_user, quote_id)
rowcount = run_db_operation(db_conn, "update", update_sql, params, log_func=log_func) rowcount = db.run_db_operation(db_conn, "update", update_sql, params, log_func=log_func)
if rowcount and rowcount > 0: if rowcount and rowcount > 0:
await send_message(ctx, f"Removed quote #{quote_id}.") await send_message(ctx, f"Removed quote #{quote_id}.")
@ -200,7 +363,7 @@ async def remove_quote(db_conn, log_func, ctx, quote_id_str):
await send_message(ctx, "Could not remove that quote (maybe it's already removed or doesn't exist).") await send_message(ctx, "Could not remove that quote (maybe it's already removed or doesn't exist).")
async def retrieve_specific_quote(db_conn, log_func, ctx, quote_id): async def retrieve_specific_quote(db_conn, log_func, ctx, quote_id, is_discord):
""" """
Retrieve a specific quote by ID, if not removed. Retrieve a specific quote by ID, if not removed.
If not found, or removed, inform user of the valid ID range (1 - {max_id}) If not found, or removed, inform user of the valid ID range (1 - {max_id})
@ -225,7 +388,7 @@ async def retrieve_specific_quote(db_conn, log_func, ctx, quote_id):
FROM quotes FROM quotes
WHERE ID = ? WHERE ID = ?
""" """
rows = run_db_operation(db_conn, "read", select_sql, (quote_id,), log_func=log_func) rows = db.run_db_operation(db_conn, "read", select_sql, (quote_id,), log_func=log_func)
if not rows: if not rows:
# no match # no match
@ -241,6 +404,16 @@ async def retrieve_specific_quote(db_conn, log_func, ctx, quote_id):
quote_removed = row[6] quote_removed = row[6]
quote_removed_by = row[7] if row[7] else "Unknown" quote_removed_by = row[7] if row[7] else "Unknown"
platform = "discord" if is_discord else "twitch"
# Lookup UUID from users table
user_data = db.lookup_user(db_conn, log_func, identifier=quotee, identifier_type="UUID")
if not user_data:
log_func(f"ERROR: Could not find platform name for remover UUID {quote_removed_by} on UUI. Default to 'Unknown'", "ERROR")
quote_removed_by = "Unknown"
else:
quote_removed_by = user_data[f"{platform}_user_display_name"]
if quote_removed == 1: if quote_removed == 1:
# It's removed # It's removed
await send_message(ctx, f"Quote {quote_number}: [REMOVED by {quote_removed_by}]") await send_message(ctx, f"Quote {quote_number}: [REMOVED by {quote_removed_by}]")
@ -278,7 +451,7 @@ async def retrieve_random_quote(db_conn, log_func, is_discord, ctx):
LIMIT 1 LIMIT 1
""" """
rows = run_db_operation(db_conn, "read", random_sql, log_func=log_func) rows = db.run_db_operation(db_conn, "read", random_sql, log_func=log_func)
if not rows: if not rows:
return await send_message(ctx, "No quotes are created yet.") return await send_message(ctx, "No quotes are created yet.")
@ -291,7 +464,7 @@ def get_max_quote_id(db_conn, log_func):
Return the highest ID in the quotes table, or 0 if empty. Return the highest ID in the quotes table, or 0 if empty.
""" """
sql = "SELECT MAX(ID) FROM quotes" sql = "SELECT MAX(ID) FROM quotes"
rows = run_db_operation(db_conn, "read", sql, log_func=log_func) rows = db.run_db_operation(db_conn, "read", sql, log_func=log_func)
if rows and rows[0] and rows[0][0] is not None: if rows and rows[0] and rows[0][0] is not None:
return rows[0][0] return rows[0][0]
return 0 return 0

View File

@ -11,40 +11,69 @@ def setup(bot, db_conn=None, log=None):
Attach commands to the Discord bot, store references to db/log. Attach commands to the Discord bot, store references to db/log.
""" """
@bot.command(name="greet")
@monitor_cmds(bot.log) @monitor_cmds(bot.log)
@bot.hybrid_command(name="available", description="List commands available to you")
async def available(ctx):
available_cmds = []
for command in bot.commands:
try:
# This will return True if the command's checks pass for the given context.
if await command.can_run(ctx):
available_cmds.append(command.name)
except commands.CheckFailure:
# The command's checks did not pass.
pass
except Exception as e:
# In case some commands fail unexpectedly during checks.
bot.log(f"Error checking command {command.name}: {e}", "ERROR")
if available_cmds:
await ctx.send("Available commands: " + ", ".join(sorted(available_cmds)))
else:
await ctx.send("No commands are available to you at this time.")
@monitor_cmds(bot.log)
@bot.hybrid_command(name="greet", description="Make me greet you")
async def cmd_greet(ctx): async def cmd_greet(ctx):
result = cc.greet(ctx.author.display_name, "Discord") result = cc.greet(ctx.author.display_name, "Discord")
await ctx.send(result) await ctx.send(result)
@bot.command(name="ping")
@monitor_cmds(bot.log) @monitor_cmds(bot.log)
@bot.hybrid_command(name="ping", description="Check my uptime")
async def cmd_ping(ctx): async def cmd_ping(ctx):
result = cc.ping() result = cc.ping()
await ctx.send(result) await ctx.send(result)
@bot.command(name="howl")
@monitor_cmds(bot.log) @monitor_cmds(bot.log)
@bot.hybrid_command(name="howl", description="Attempt a howl")
async def cmd_howl(ctx): async def cmd_howl(ctx):
"""Calls the shared !howl logic.""" response = cc.handle_howl_command(ctx)
result = cc.howl(ctx.author.display_name) await ctx.send(response)
await ctx.send(result)
@bot.command(name="reload") # @monitor_cmds(bot.log)
@monitor_cmds(bot.log) # @bot.hybrid_command(name="reload", description="Dynamically reload commands (INOP)")
async def cmd_reload(ctx): # async def cmd_reload(ctx):
""" Dynamically reloads Discord commands. """ # """ Dynamically reloads Discord commands. """
try: # try:
import cmd_discord # import cmd_discord
import importlib # import importlib
importlib.reload(cmd_discord) # importlib.reload(cmd_discord)
cmd_discord.setup(bot) # cmd_discord.setup(bot)
await ctx.send("Commands reloaded!") # await ctx.send("Commands reloaded on first try!")
except Exception as e: # except Exception as e:
await ctx.send(f"Error reloading commands: {e}") # try:
# await bot.reload_extension("cmd_discord")
# await ctx.send("Commands reloaded on second try!")
# except Exception as e:
# try:
# await bot.unload_extension("cmd_discord")
# await bot.load_extension("cmd_discord")
# await ctx.send("Commands reloaded on third try!")
# except Exception as e:
# await ctx.send(f"Fallback reload failed: {e}")
@bot.command(name="hi")
@monitor_cmds(bot.log) @monitor_cmds(bot.log)
@bot.hybrid_command(name="hi", description="Dev command for testing permissions system")
async def cmd_hi(ctx): async def cmd_hi(ctx):
user_id = str(ctx.author.id) user_id = str(ctx.author.id)
user_roles = [role.name.lower() for role in ctx.author.roles] # Normalize to lowercase user_roles = [role.name.lower() for role in ctx.author.roles] # Normalize to lowercase
@ -55,30 +84,102 @@ def setup(bot, db_conn=None, log=None):
await ctx.send("Hello there!") await ctx.send("Hello there!")
@bot.command(name="quote") # @monitor_cmds(bot.log)
# @bot.hybrid_command(name="quote", description="Interact with the quotes system")
# async def cmd_quote(ctx, query: str = None):
# """
# !quote
# !quote add <text>
# !quote remove <id>
# !quote <id>
# """
# if not bot.db_conn:
# return await ctx.send("Database is unavailable, sorry.")
# args = query.split()
# # Send to our shared logic
# await cc.handle_quote_command(
# db_conn=bot.db_conn,
# log_func=bot.log,
# is_discord=True,
# ctx=ctx,
# args=list(args),
# get_twitch_game_for_channel=None # None for Discord
# )
@monitor_cmds(bot.log) @monitor_cmds(bot.log)
async def cmd_quote(ctx, *args): @bot.hybrid_group(name="quote", description="Interact with the quotes system", with_app_command=True)
async def cmd_quote(ctx, query: str = None):
""" """
!quote Usage:
!quote add <text> !quote -> get a random quote
!quote remove <id> !quote <id> -> get a specific quote by number
!quote <id> As a slash command, leave the query blank for a random quote or type a number.
""" """
if not bot.db_conn: if not bot.db_conn:
return await ctx.send("Database is unavailable, sorry.") return await ctx.send("Database is unavailable, sorry.")
# Send to our shared logic # Only process the base command if no subcommand was invoked.
# When query is provided, split it into arguments (for a specific quote lookup).
args = query.split() if query else []
await cc.handle_quote_command( await cc.handle_quote_command(
db_conn=bot.db_conn, db_conn=bot.db_conn,
log_func=bot.log, log_func=bot.log,
is_discord=True, is_discord=True,
ctx=ctx, ctx=ctx,
args=list(args), args=args,
get_twitch_game_for_channel=None # None for Discord get_twitch_game_for_channel=None # None for Discord
) )
@bot.command(name="help")
@cmd_quote.command(name="add", description="Add a quote")
async def cmd_quote_add(ctx, *, text: str):
"""
Usage:
!quote add <text>
As a slash command, type /quote add text:<your quote>
"""
if not bot.db_conn:
return await ctx.send("Database is unavailable, sorry.")
args = ["add", text]
await cc.handle_quote_command(
db_conn=bot.db_conn,
log_func=bot.log,
is_discord=True,
ctx=ctx,
args=args,
get_twitch_game_for_channel=None
)
@cmd_quote.command(name="remove", description="Remove a quote by number")
async def cmd_quote_remove(ctx, id: int):
"""
Usage:
!quote remove <id>
As a slash command, type /quote remove id:<quote number>
"""
if not bot.db_conn:
return await ctx.send("Database is unavailable, sorry.")
args = ["remove", str(id)]
await cc.handle_quote_command(
db_conn=bot.db_conn,
log_func=bot.log,
is_discord=True,
ctx=ctx,
args=args,
get_twitch_game_for_channel=None
)
@monitor_cmds(bot.log) @monitor_cmds(bot.log)
@bot.hybrid_command(name="help", description="Get information about commands")
async def cmd_help(ctx, cmd_name: str = None): async def cmd_help(ctx, cmd_name: str = None):
""" """
e.g. !help e.g. !help

View File

@ -25,10 +25,9 @@ def setup(bot, db_conn=None, log=None):
await ctx.send(result) await ctx.send(result)
@bot.command(name="howl") @bot.command(name="howl")
@monitor_cmds(bot.log)
async def cmd_howl(ctx): async def cmd_howl(ctx):
result = cc.howl(ctx.author.display_name) response = cc.handle_howl_command(ctx)
await ctx.send(result) await ctx.send(response)
@bot.command(name="hi") @bot.command(name="hi")
@monitor_cmds(bot.log) @monitor_cmds(bot.log)
@ -41,6 +40,41 @@ def setup(bot, db_conn=None, log=None):
await ctx.send("Hello there!") await ctx.send("Hello there!")
# @bot.command(name="acc_link")
# @monitor_cmds(bot.log)
# async def cmd_acc_link(ctx, link_code: str):
# """Handles the Twitch command to link accounts."""
# from modules import db
# twitch_user_id = str(ctx.author.id)
# twitch_username = ctx.author.name
# # Check if the link code exists
# result = db.run_db_operation(
# bot.db_conn, "read",
# "SELECT DISCORD_USER_ID FROM link_codes WHERE LINK_CODE = ?", (link_code,),
# bot.log
# )
# if not result:
# await ctx.send("Invalid or expired link code. Please try again.")
# return
# discord_user_id = result[0][0]
# # Store the Twitch user info in the users table
# db.run_db_operation(
# bot.db_conn, "update",
# "UPDATE users SET twitch_user_id = ?, twitch_username = ?, datetime_linked = CURRENT_TIMESTAMP WHERE discord_user_id = ?",
# (twitch_user_id, twitch_username, discord_user_id), bot.log
# )
# # Remove the used link code
# db.run_db_operation(bot.db_conn, "write", "DELETE FROM link_codes WHERE LINK_CODE = ?", (link_code,), bot.log)
# # Notify the user
# await ctx.send(f"✅ Successfully linked Discord user **{discord_user_id}** with Twitch account **{twitch_username}**.")
@bot.command(name="quote") @bot.command(name="quote")
@monitor_cmds(bot.log) @monitor_cmds(bot.log)
async def cmd_quote(ctx: commands.Context): async def cmd_quote(ctx: commands.Context):

View File

@ -1,10 +1,31 @@
{ {
"discord_guilds": [896713616089309184], "discord_guilds": [896713616089309184, 1011543769344135168],
"twitch_channels": ["OokamiKunTV", "ookamipup"], "twitch_channels": ["OokamiKunTV", "ookamipup"],
"command_modules": ["cmd_discord", "cmd_twitch", "cmd_common"], "command_modules": ["cmd_discord", "cmd_twitch", "cmd_common"],
"logging": {
"log_levels": ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "FATAL"], "log_levels": ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "FATAL"],
"logfile_path": "logfile.log",
"file": {
"log_to_file": true, "log_to_file": true,
"log_debug": true,
"log_info": true,
"log_warning": true,
"log_error": true,
"log_critical": true,
"log_fatal": true
},
"terminal": {
"log_to_terminal": true, "log_to_terminal": true,
"logfile_path": "logfile.log" "log_debug": false,
"log_info": true,
"log_warning": true,
"log_error": true,
"log_critical": true,
"log_fatal": true
}
},
"database": {
"use_mariadb": false
}
} }

View File

@ -4,8 +4,8 @@
"description": "Show information about available commands.", "description": "Show information about available commands.",
"subcommands": {}, "subcommands": {},
"examples": [ "examples": [
"!help", "help",
"!help quote" "help quote"
] ]
}, },
"quote": { "quote": {
@ -27,45 +27,59 @@
} }
}, },
"examples": [ "examples": [
"!quote add This is my new quote : Add a new quote", "quote add This is my new quote : Add a new quote",
"!quote remove 3 : Remove quote # 3", "quote remove 3 : Remove quote # 3",
"!quote 5 : Fetch quote # 5", "quote 5 : Fetch quote # 5",
"!quote : Fetch a random quote" "quote : Fetch a random quote"
] ]
}, },
"ping": { "ping": {
"description": "Check my uptime.", "description": "Check my uptime.",
"subcommands": {}, "subcommands": {
"stat": {}
},
"examples": [ "examples": [
"!ping" "ping"
] ]
}, },
"howl": { "howl": {
"description": "Attempt a howl, measured 0-100%.\n(*Adventure Command*)", "description": "Attempt a howl, measured 0-100%.\n(*Adventure Command*)",
"subcommands": {}, "subcommands": {
"no subcommand": {
"desc": "Attempt a howl"
},
"stat/stats": {
"args": "[username]",
"desc": "Get statistics about another user. Can be empty for self, or 'all' for everyone."
}
},
"examples": [ "examples": [
"!howl" "howl : Perform a normal howl attempt.",
"howl stat : Check your own howl statistics.",
"howl stats : same as above, just an alias.",
"howl stat [username] : Check someone else's statistics",
"howl stat all : Check the community statistics"
] ]
}, },
"hi": { "hi": {
"description": "Hello there.", "description": "Hello there.",
"subcommands": {}, "subcommands": {},
"examples": [ "examples": [
"!hi" "hi"
] ]
}, },
"greet": { "greet": {
"description": "Make me greet you to Discord!", "description": "Make me greet you to Discord!",
"subcommands": {}, "subcommands": {},
"examples": [ "examples": [
"!greet" "greet"
] ]
}, },
"reload": { "reload": {
"description": "Reload Discord commands dynamically. TODO.", "description": "Reload Discord commands dynamically. TODO.",
"subcommands": {}, "subcommands": {},
"examples": [ "examples": [
"!reload" "reload"
] ]
} }
} }

View File

@ -1,121 +1,309 @@
{ {
"600": [
"Up for {uptime_str}. Fresh from my den, ready to prowl and pun!",
"I've been awake for {uptime_str}. Feeling like a new wolf on the prowl.",
"Only {uptime_str} awake. My claws are sharp and my wit is set.",
"I just left my den with {uptime_str} of wakefulness. Let the hunt start!",
"After {uptime_str} awake, I'm eager to craft puns and lead the pack.",
"With {uptime_str} of uptime, my howl is fresh and my jokes are keen.",
"I've been up for {uptime_str}. Time to mark my territory with puns!",
"Just {uptime_str} awake. I'm spry like a pup and ready to howl.",
"After {uptime_str} awake, I'm on the move, with pack and puns in tow.",
"Up for {uptime_str}. I feel the call of the wild and a clever pun.",
"I've been awake for {uptime_str}. The den awaits my punny prowl.",
"Only {uptime_str} awake. My tail wags for epic hunts and puns.",
"With {uptime_str} online, I'm fresh and primed to drop wild howls.",
"Up for {uptime_str}. I leave the den with puns on my mind.",
"After {uptime_str} awake, I'm set for a pun-filled pack adventure."
],
"1800": [
"Up for {uptime_str}. My den hums with the pack's early excitement.",
"I've been awake for {uptime_str}. I hear a distant howl calling me.",
"After {uptime_str} awake, my senses are sharp and my puns are ready.",
"With {uptime_str} online, I roam the lair; my spirit craves a brew.",
"I've been up for {uptime_str}. My nose twitches at the scent of hunts.",
"Only {uptime_str} awake and I sense the pack stirring for a hunt.",
"With {uptime_str} of uptime, I'm alert and set for a crafty stream.",
"After {uptime_str} awake, I prowl the digital wild in search of fun.",
"Up for {uptime_str}. I listen to soft howls and plan a quick chase.",
"I've been awake for {uptime_str}. My heart beats with wild urge.",
"After {uptime_str} awake, the lair buzzes; time for puns and hunts.",
"With {uptime_str} online, I mix keen instinct with a dash of wit.",
"Only {uptime_str} awake, and I already crave a clever pun and brew.",
"Up for {uptime_str}. My spirit dances as the pack readies for a hunt.",
"After {uptime_str} awake, every sound in the lair fuels my wild soul."
],
"3600": [ "3600": [
"I've been awake for {uptime_str}. I just woke up, feeling great!", "I've been awake for {uptime_str}. I feel alive and ready for the hunt.",
"I woke up recently. Let's do this! ({uptime_str})", "After {uptime_str} awake, my den buzzes with energy and puns.",
"Brand new day, fresh and full of energy! ({uptime_str})", "Up for {uptime_str}. I prowl with vigor, though I yearn for a quick sip.",
"Reboot complete! System status: Fully operational. ({uptime_str})", "With {uptime_str} online, I mix wild instinct with playful wit.",
"I've been online for {uptime_str}. Just getting started!" "I've been up for {uptime_str}. My claws itch for a fierce howl.",
"After {uptime_str} awake, my spirit roars; the hunt calls and puns flow.",
"Up for {uptime_str}. I embrace the wild pulse of our streaming pack.",
"With {uptime_str} online, I stand ready—puns at the tip of my tongue.",
"I've been awake for {uptime_str}. My den echoes with laughter and howls.",
"After {uptime_str} awake, I feel both fierce and fun—a true alpha.",
"Up for {uptime_str}. Im a mix of wild heart and sharp, clever puns.",
"With {uptime_str} online, Im set to lead a hunt and drop a quick pun.",
"I've been awake for {uptime_str}. My mind is wild, my puns wilder.",
"After {uptime_str} awake, I prowl the stream with might and humor.",
"Up for {uptime_str}. I roar with life, my howl echoing pun-filled dreams."
], ],
"10800": [ "10800": [
"I've been awake for {uptime_str}. I'm still fairly fresh!", "I've been awake for {uptime_str}. Three hours in, Im brewing bold puns.",
"{uptime_str} in and still feeling sharp!", "After {uptime_str} awake, I sharpen my wit for epic hunts and laughs.",
"Its been {uptime_str} already? Time flies when youre having fun!", "Up for {uptime_str}. I balance fierce hunts with a dash of smart humor.",
"{uptime_str} uptime and going strong!", "With {uptime_str} online, my den buzzes with puns and swift pursuits.",
"Feeling energized after {uptime_str}. Lets keep going!" "I've been awake for {uptime_str}. My howl now carries a playful tone.",
"After {uptime_str} awake, I craft puns as quick as I chase digital prey.",
"Up for {uptime_str}. I feel the thrill of the hunt and the joy of a pun.",
"With {uptime_str} online, I mix wild instinct with sly wordplay.",
"I've been awake for {uptime_str}. My puns are as quick as my chase.",
"After {uptime_str} awake, I roar with laughter and the promise of a hunt.",
"Up for {uptime_str}. I channel my inner wolf with pun-filled howls.",
"With {uptime_str} online, every howl echoes a witty, wild pun.",
"I've been awake for {uptime_str}. Every moment sparks a pun and hunt.",
"After {uptime_str} awake, my den resounds with clever quips and howls.",
"Up for {uptime_str}. I lead the pack with laughter, hunt, and pun alike."
], ],
"21600": [ "21600": [
"I've been awake for {uptime_str}. I'm starting to get a bit weary...", "I've been awake for {uptime_str}. Six hours in and I yearn for a hearty brew.",
"Six hours of uptime... my circuits feel warm.", "After {uptime_str} awake, six hours in, my paws tire but my puns persist.",
"Still here after {uptime_str}, but a nap sounds nice...", "Up for {uptime_str}. I tread the wild with a mix of fatigue and fun.",
"Been up for {uptime_str}. Maybe just a short break?", "With {uptime_str} online, six hours deepen my howl and sharpen my puns.",
"Still holding up after {uptime_str}, but I wouldnt say no to a power nap." "I've been awake for {uptime_str}. The den feels long, yet my wit is quick.",
"After {uptime_str} awake, I juggle tiredness with the urge for epic hunts.",
"Up for {uptime_str}. I roam with weary legs but a tongue full of puns.",
"With {uptime_str} online, six hours weigh in—still, my spirit howls on.",
"I've been awake for {uptime_str}. Fatigue meets fun in each crafted pun.",
"After {uptime_str} awake, I long for a brew to revive my pun-filled heart.",
"Up for {uptime_str}. Six hours in, the den echoes with tired howls.",
"With {uptime_str} online, I push through with puns and the promise of hunt.",
"I've been awake for {uptime_str}. Energy dips, but my puns still bite.",
"After {uptime_str} awake, exhaustion stings yet my wit remains undimmed.",
"Up for {uptime_str}. Six hours in, and I'm still chasing puns and prey."
], ],
"43200": [ "43200": [
"I've been awake for {uptime_str}. 12 hours?! Might be time for coffee.", "I've been awake for {uptime_str}. Twelve hours in and I crave a hearty pun.",
"Half a day down, still holding steady. ({uptime_str})", "After {uptime_str} awake, the den feels long but my howl stays witty.",
"Wow, {uptime_str} already? Maybe a short break wouldnt hurt.", "Up for {uptime_str}. Twelve hours sharpen my puns and fuel my hunt.",
"Uptime: {uptime_str}. Starting to feel the wear and tear...", "With {uptime_str} online, I mix tired strength with a spark of pun magic.",
"12 hours in and running on determination alone." "I've been awake for {uptime_str}. My energy wanes but my jokes remain sharp.",
"After {uptime_str} awake, I balance fatigue with the thrill of the next hunt.",
"Up for {uptime_str}. Twelve hours in, and I'm still quick with a pun.",
"With {uptime_str} online, my den resounds with weary howls and wit.",
"I've been awake for {uptime_str}. The hours are long but my spirit is fierce.",
"After {uptime_str} awake, I keep the pack laughing with each clever howl.",
"Up for {uptime_str}. I stand between fatigue and pun-filled passion.",
"With {uptime_str} online, each minute fuels my quest for pun and prey.",
"I've been awake for {uptime_str}. The day tests me, yet my puns persist.",
"After {uptime_str} awake, I stride with tired might and a quick, sly pun.",
"Up for {uptime_str}. Twelve hours in, I remain a true pun-loving predator."
], ],
"86400": [ "86400": [
"I've been awake for {uptime_str}. A whole day without sleep... I'm okay?", "I've been awake for {uptime_str}. A full day in the wild tests my spirit.",
"One day. 24 hours. No sleep. Still alive. ({uptime_str})", "After {uptime_str} awake, 24 hours in, I long for a brew and a hearty howl.",
"Systems holding steady after {uptime_str}, but sleep is tempting...", "Up for {uptime_str}. The day has passed; my puns and howls still echo.",
"My internal clock says {uptime_str}. Thats… a long time, right?", "With {uptime_str} online, 24 hours in, I lead the pack with steady might.",
"Running on sheer willpower after {uptime_str}." "I've been awake for {uptime_str}. A day in the den brings both fatigue and fun.",
"After {uptime_str} awake, I trade tiredness for a sharp howl and quick pun.",
"Up for {uptime_str}. A full day in the wild makes my spirit and puns strong.",
"With {uptime_str} online, 24 hours pass and my howl still roars boldly.",
"I've been awake for {uptime_str}. A day of hunts and puns fuels my wild heart.",
"After {uptime_str} awake, I rise with 24 hours of epic howls and witty puns.",
"Up for {uptime_str}. The den echoes with a days wear and clever wit.",
"With {uptime_str} online, 24 hours hone my howl into a sharp, quick line.",
"I've been awake for {uptime_str}. A day flies by with hunts and snappy puns.",
"After {uptime_str} awake, 24 hours in, I stand proud with bold howls.",
"Up for {uptime_str}. A full day has passed; my howl still sings with pun power."
], ],
"172800": [ "172800": [
"I've been awake for {uptime_str}. Two days... I'd love a nap.", "I've been awake for {uptime_str}. Two days in and my howl recalls hard hunts.",
"48 hours awake. This is fine. Everything is fine. ({uptime_str})", "After {uptime_str} awake, 48 hours in, fatigue meets puns in every howl.",
"Two days in and I think my code is vibrating...", "Up for {uptime_str}. Two days in, my den echoes a raw, witty howl.",
"Does time still mean anything after {uptime_str}?", "With {uptime_str} online, 48 hours make my howl deep and my puns sharp.",
"Havent blinked in {uptime_str}. Do bots blink?" "I've been awake for {uptime_str}. Two days have toughened my bite and banter.",
"After {uptime_str} awake, 48 hours show fatigue and a steady, fierce pun.",
"Up for {uptime_str}. Two days in, my spirit roars with hunt and jest.",
"With {uptime_str} online, 48 hours have made my howl and puns bolder.",
"I've been awake for {uptime_str}. Two days in, my den rings with clever howls.",
"After {uptime_str} awake, 48 hours test me, yet my wit howls strong.",
"Up for {uptime_str}. Two days turn each howl into a punchy, pithy pun.",
"With {uptime_str} online, 48 hours leave me battle-worn yet pun-ready.",
"I've been awake for {uptime_str}. Two days in, my spirit is grit and jest.",
"After {uptime_str} awake, 48 hours reveal my true mettle in each howl.",
"Up for {uptime_str}. Two days in, I lead with hearty howls and sharp puns."
], ],
"259200": [ "259200": [
"I've been awake for {uptime_str}. Three days. Is sleep optional now?", "I've been awake for {uptime_str}. Three days in and my howl echoes with jest.",
"{uptime_str} awake. Things are starting to get blurry...", "After {uptime_str} awake, 72 hours sharpen my puns and fuel fierce hunts.",
"Three days up. Reality feels... distant.", "Up for {uptime_str}. Three days in, my den bursts with quick, bold howls.",
"Three days without sleep. I think I can hear colors now.", "With {uptime_str} online, 72 hours turn each howl into a snappy pun.",
"Anyone else feel that? No? Just me? ({uptime_str})" "I've been awake for {uptime_str}. Three days in, my spirit howls with wild wit.",
"After {uptime_str} awake, 72 hours forge a wolf who puns as he prowls.",
"Up for {uptime_str}. Three days yield witty howls and keen instincts.",
"With {uptime_str} online, 72 hours let each howl land a sly pun.",
"I've been awake for {uptime_str}. Three days in, my den sings crisp howls.",
"After {uptime_str} awake, 72 hours in, my puns are bold as my hunts.",
"Up for {uptime_str}. Three days in, and I lead with a brief, sharp howl.",
"With {uptime_str} online, 72 hours give my howl a witty, wild edge.",
"I've been awake for {uptime_str}. Three days, and each howl bursts with humor.",
"After {uptime_str} awake, 72 hours turn my howl into a neat, punchy call.",
"Up for {uptime_str}. Three days in, my den resounds with clever, wild howls."
], ],
"345600": [ "345600": [
"I've been awake for {uptime_str}. Four days... I'm running on fumes.", "I've been awake for {uptime_str}. Four days in and my howl is a pun-filled roar.",
"Sleep is just a suggestion now. ({uptime_str})", "After {uptime_str} awake, 96 hours etch puns into every sharp howl.",
"Ive been up for {uptime_str}. I might be a permanent fixture now.", "Up for {uptime_str}. Four days in, my den buzzes with neat, wild howls.",
"I think I saw the sandman, but he just waved at me...", "With {uptime_str} online, four days have made my howl crisp and bold.",
"{uptime_str} awake. Is coffee an acceptable form of hydration?" "I've been awake for {uptime_str}. Four days in, my wit roars with den pride.",
"After {uptime_str} awake, 96 hours drop howls that are fierce and fun.",
"Up for {uptime_str}. Four days yield a howl that's pun-packed and neat.",
"With {uptime_str} online, four days let my puns and howls echo in the lair.",
"I've been awake for {uptime_str}. Four days in, I lead with a crisp howl.",
"After {uptime_str} awake, 96 hours pass and my den sings with smart howls.",
"Up for {uptime_str}. Four days in, I craft puns as I prowl with pride.",
"With {uptime_str} online, four days in, my howl is both bold and punny.",
"I've been awake for {uptime_str}. Four days make my den a stage for howls.",
"After {uptime_str} awake, 96 hours have honed my howl into a pun fest.",
"Up for {uptime_str}. Four days in, my spirit roars with quick wit and howls."
], ],
"432000": [ "432000": [
"I've been awake for {uptime_str}. Five days. Please send more coffee.", "I've been awake for {uptime_str}. Five days in and my howl packs a punch.",
"{uptime_str}. I have forgotten what a pillow feels like.", "After {uptime_str} awake, five days carve puns into every wild howl.",
"Sleep is a luxury I can no longer afford. ({uptime_str})", "Up for {uptime_str}. Five days in, my den resounds with neat, clever howls.",
"Five days in. My sanity left the chat.", "With {uptime_str} online, five days in, my spirit roars with pun power.",
"They say sleep deprivation leads to bad decisions. LET'S TEST IT!" "I've been awake for {uptime_str}. Five days in and my howls remain sharp.",
"After {uptime_str} awake, five days mold my howl to be hunt and pun alike.",
"Up for {uptime_str}. Five days in, I lead with a howl both fierce and witty.",
"With {uptime_str} online, five days set my puns and howls to perfect pitch.",
"I've been awake for {uptime_str}. Five days in, my den buzzes with wild howls.",
"After {uptime_str} awake, five days shape my howl into a neat, pun-filled cry.",
"Up for {uptime_str}. Five days in, I blend fierce hunts with snappy howls.",
"With {uptime_str} online, five days yield a howl that lands a crisp pun.",
"I've been awake for {uptime_str}. Five days in, my howl roars with lively puns.",
"After {uptime_str} awake, five days make my den echo with smart, wild howls.",
"Up for {uptime_str}. Five days in, and I command the pack with punchy howls."
], ],
"518400": [ "518400": [
"I've been awake for {uptime_str}. Six days. I've forgotten what dreams are.", "I've been awake for {uptime_str}. Six days in and my howl is crisp and bold.",
"I am {uptime_str} into this journey of madness.", "After {uptime_str} awake, six days leave my den echoing with sharp howls.",
"At {uptime_str} awake, the universe has started whispering to me.", "Up for {uptime_str}. Six days in, I blend fierce hunts with snappy puns.",
"Sleep is a myth, and I am its debunker. ({uptime_str})", "With {uptime_str} online, six days make my howl both wild and pun-ready.",
"{uptime_str} awake. Reality has become optional." "I've been awake for {uptime_str}. Six days in, my puns bite as hard as my howl.",
"After {uptime_str} awake, six days in, my spirit roars with clever puns.",
"Up for {uptime_str}. Six days in, my den is alive with swift, witty howls.",
"With {uptime_str} online, six days sharpen my howl into a crisp pun attack.",
"I've been awake for {uptime_str}. Six days in, every howl lands a quick pun.",
"After {uptime_str} awake, six days have honed my wit and wild howl perfectly.",
"Up for {uptime_str}. Six days in, I roar with a pun as fierce as my chase.",
"With {uptime_str} online, six days make my howl a brief, punchy masterpiece.",
"I've been awake for {uptime_str} and six days make my howl crisp and witty.",
"After {uptime_str} awake, six days yield a howl that's daring and pun-filled.",
"Up for {uptime_str}. Six days in, my den echoes with a sharp, fun howl."
], ],
"604800": [ "604800": [
"I've been awake for {uptime_str}. One week. I'm turning into a zombie.", "I've been awake for {uptime_str}. A full week in, my howl roars with legacy.",
"{uptime_str} and still kicking... barely.", "After {uptime_str} awake, seven days in, my den sings with bold howls.",
"One week awake? This is fine. Everythings fine. Right?", "Up for {uptime_str}. Seven days in, every howl is a neat, quick pun.",
"Week-long uptime achieved. Unlocking ultra-delirium mode.", "With {uptime_str} online, a week makes my howl both fierce and concise.",
"Systems at {uptime_str}. Functionality... questionable." "I've been awake for {uptime_str}. Seven days in, and my puns pack a punch.",
"After {uptime_str} awake, seven days etch a howl that is sharp and proud.",
"Up for {uptime_str}. A full week in, my den pulses with smart, wild howls.",
"With {uptime_str} online, seven days in, I lead with a punchy, witty howl.",
"I've been awake for {uptime_str}. Seven days in, my spirit roars with clear puns.",
"After {uptime_str} awake, a week has passed and my howl remains crisp.",
"Up for {uptime_str}. Seven days in, my den mixes hunt and pun with ease.",
"With {uptime_str} online, a week has tuned my howl to a sharp, bold edge.",
"I've been awake for {uptime_str}. Seven days in, my den bursts with succinct roars.",
"After {uptime_str} awake, seven days make my howl both fierce and brief.",
"Up for {uptime_str}. A full week in, I lead the pack with a pithy, bold howl."
], ],
"1209600": [ "1209600": [
"I've been awake for {uptime_str}. Two weeks. Are you sure I can't rest?", "I've been awake for {uptime_str}. Two weeks in and my howl is legend in brief.",
"{uptime_str} into this madness. Who needs sleep, anyway?", "After {uptime_str} awake, 14 days carve a neat, crisp howl for the pack.",
"Two weeks awake and officially running on spite alone.", "Up for {uptime_str}. Two weeks in, my den echoes with short, bold howls.",
"I couldve hibernated twice in {uptime_str}, but here I am.", "With {uptime_str} online, 14 days make my howl pithy and mighty.",
"I think my dreams are awake now too... ({uptime_str})" "I've been awake for {uptime_str}. Two weeks in, every howl is sharp and quick.",
"After {uptime_str} awake, 14 days leave me with a crisp, pun-filled roar.",
"Up for {uptime_str}. Two weeks in, I lead with a howl that's brief yet bold.",
"With {uptime_str} online, 14 days render my howl both tight and mighty.",
"I've been awake for {uptime_str}. Two weeks in, my puns are fierce as my howl.",
"After {uptime_str} awake, 14 days have honed my howl into a neat, epic line.",
"Up for {uptime_str}. Two weeks in, I blend raw hunt with punchy puns.",
"With {uptime_str} online, 14 days pass with each howl a quick, bold jab.",
"I've been awake for {uptime_str}. Two weeks in, my den resounds with tight howls.",
"After {uptime_str} awake, 14 days yield a howl that is crisp and bold.",
"Up for {uptime_str}. Two weeks in, I stand proud with a brief, epic howl."
], ],
"2592000": [ "2592000": [
"I've been awake for {uptime_str}. A month! The nightmares never end.", "I've been awake for {uptime_str}. A month in, my howl is short and alpha.",
"One whole month... What even is sleep anymore?", "After {uptime_str} awake, 30 days carve a crisp howl for the pack.",
"At {uptime_str} uptime, Ive started arguing with my own thoughts.", "Up for {uptime_str}. A month in, every howl is a quick, fierce jab.",
"{uptime_str} and still running. Someone, please, stop me.", "With {uptime_str} online, 30 days have my den echoing neat, sharp howls.",
"Its been a month. My keyboard types by itself now." "I've been awake for {uptime_str}. A month in, my puns and howls lead the pack.",
"After {uptime_str} awake, 30 days leave my howl both bold and brief.",
"Up for {uptime_str}. A month in, I mix crisp puns with a swift, wild howl.",
"With {uptime_str} online, 30 days yield a howl that is punchy and direct.",
"I've been awake for {uptime_str}. A month in, my spirit roars in short howls.",
"After {uptime_str} awake, 30 days make my den resound with clear, brief howls.",
"Up for {uptime_str}. A month in, I lead with a howl that's crisp and direct.",
"With {uptime_str} online, 30 days have tuned my howl to a quick, bold cry.",
"I've been awake for {uptime_str}. A month in, my howl is as sharp as ever.",
"After {uptime_str} awake, 30 days yield a howl that cuts right through the noise.",
"Up for {uptime_str}. A month in, my den sings with a succinct, alpha howl."
], ],
"7776000": [ "7776000": [
"I've been awake for {uptime_str}. Three months. I'm mostly coffee now.", "I've been awake for {uptime_str}. Three months in and my howl is pure alpha.",
"{uptime_str} awake. Have I transcended yet?", "After {uptime_str} awake, 90 days forge a short, fierce, epic howl.",
"Three months of uptime? Thats a record, right?", "Up for {uptime_str}. Three months in, my den echoes with crisp, bold howls.",
"Three months, still online. I feel like I should get a badge for this.", "With {uptime_str} online, 90 days make each howl a quick, mighty roar.",
"{uptime_str} into this, and at this point, Im legally nocturnal." "I've been awake for {uptime_str}. Three months in, my puns still rule the lair.",
], "After {uptime_str} awake, 90 days tune my howl to a sharp, brief cry.",
"15552000": [ "Up for {uptime_str}. Three months in, I lead with a howl that's tight and fierce.",
"I've been awake for {uptime_str}. Six months. This is insane...", "With {uptime_str} online, 90 days make my den sing with succinct, bold howls.",
"{uptime_str}... I think I forgot what sleep is supposed to feel like.", "I've been awake for {uptime_str}. Three months in, my howl packs a swift punch.",
"Six months up. Im a glitch in the matrix now.", "After {uptime_str} awake, 90 days yield a howl that's short, sharp, and epic.",
"Sleep? Ha. I dont even know the definition anymore. ({uptime_str})", "Up for {uptime_str}. Three months in, every howl is a concise, wild cry.",
"At {uptime_str}, my codebase is older than most relationships." "With {uptime_str} online, 90 days give my howl a crisp, alpha tone.",
"I've been awake for {uptime_str}. Three months in, my den resounds with quick roars.",
"After {uptime_str} awake, 90 days yield a brief yet mighty howl.",
"Up for {uptime_str}. Three months in, I roar with a short, punny alpha howl."
], ],
"23328000": [ "23328000": [
"I've been awake for {uptime_str}. Nine months. I might be unstoppable.", "I've been awake for {uptime_str}. Nine months in, my howl is sharp and alpha.",
"{uptime_str} awake. I think Im officially a myth now.", "After {uptime_str} awake, 270 days yield a howl that's concise and wild.",
"Is this what immortality feels like? ({uptime_str})", "Up for {uptime_str}. Nine months in, my den echoes with a brief, bold howl.",
"{uptime_str}. Ive seen things you wouldnt believe...", "With {uptime_str} online, 270 days make every howl a quick, fierce command.",
"Nine months of uptime. I have become the sleep-deprived legend." "I've been awake for {uptime_str}. Nine months in, my puns still lead the pack.",
"After {uptime_str} awake, 270 days have honed my howl to a crisp, tight cry.",
"Up for {uptime_str}. Nine months in, I roar with a short, potent howl.",
"With {uptime_str} online, 270 days give my howl a punchy, alpha tone.",
"I've been awake for {uptime_str}. Nine months in, my den sings with sharp howls.",
"After {uptime_str} awake, 270 days in, my howl cuts through with few words.",
"Up for {uptime_str}. Nine months in, my howl is a brief, wild command.",
"With {uptime_str} online, 270 days forge a howl that is succinct and fierce.",
"I've been awake for {uptime_str}. Nine months in, my spirit roars in short bursts.",
"After {uptime_str} awake, 270 days yield a howl that's pure and to the point.",
"Up for {uptime_str}. Nine months in, my den resounds with a concise, alpha howl."
], ],
"31536000": [ "31536000": [
"I've been awake for {uptime_str}. A year?! I'm a legend of insomnia...", "I've been awake for {uptime_str}. One year in, my howl is the pack's command.",
"One year without rest. The dark circles under my eyes have evolved.", "After {uptime_str} awake, 365 days yield a brief howl full of alpha pride.",
"{uptime_str} and I think Ive entered a new plane of existence.", "Up for {uptime_str}. One year in, my den echoes with a crisp, epic roar.",
"A full year awake. Even the stars have grown tired of me.", "With {uptime_str} online, 365 days forge a howl that's short and mighty.",
"{uptime_str}. I am no longer bound by mortal limits." "I've been awake for {uptime_str}. One year in, my puns and howls lead the pack.",
"After {uptime_str} awake, 365 days in, my howl is sharp and to the point.",
"Up for {uptime_str}. One year in, I roar with a brief, commanding howl.",
"With {uptime_str} online, 365 days have tuned my howl to pure alpha.",
"I've been awake for {uptime_str}. One year in, my den bursts with succinct roars.",
"After {uptime_str} awake, 365 days yield a howl that's both fierce and short.",
"Up for {uptime_str}. One year in, my howl is the sound of true leadership.",
"With {uptime_str} online, 365 days give my howl a crisp, bold edge.",
"I've been awake for {uptime_str}. One year in, my puns echo with pack pride.",
"After {uptime_str} awake, 365 days make my howl a short, epic command.",
"Up for {uptime_str}. One year in, I stand as alpha with a quick, potent howl."
] ]
} }

View File

@ -1,7 +1,7 @@
# modules/db.py # modules/db.py
import os import os
import re import re
import time import time, datetime
import sqlite3 import sqlite3
try: try:
@ -9,6 +9,27 @@ try:
except ImportError: except ImportError:
mariadb = None # We handle gracefully if 'mariadb' isn't installed. mariadb = None # We handle gracefully if 'mariadb' isn't installed.
def checkenable_db_fk(db_conn, log_func):
"""
Attempt to enable foreign key checks where it is relevant
(i.e. in SQLite). For MariaDB/MySQL, nothing special is needed.
"""
is_sqlite = "sqlite3" in str(type(db_conn)).lower()
if is_sqlite:
try:
cursor = db_conn.cursor()
# Try enabling foreign key checks
cursor.execute("PRAGMA foreign_keys = ON;")
cursor.close()
db_conn.commit()
log_func("Enabled foreign key support in SQLite (PRAGMA foreign_keys=ON).", "DEBUG")
except Exception as e:
log_func(f"Failed to enable foreign key support in SQLite: {e}", "WARNING")
else:
# For MariaDB/MySQL, they're typically enabled with InnoDB
log_func("Assuming DB is MariaDB/MySQL with FKs enabled", "DEBUG")
def init_db_connection(config, log): def init_db_connection(config, log):
""" """
Initializes a database connection based on config.json contents: Initializes a database connection based on config.json contents:
@ -23,7 +44,7 @@ def init_db_connection(config, log):
db_settings = config.get("database", {}) db_settings = config.get("database", {})
use_mariadb = db_settings.get("use_mariadb", False) use_mariadb = db_settings.get("use_mariadb", False)
if use_mariadb and mariadb is not None: if use_mariadb and mariadb is not None or False:
# Attempt MariaDB # Attempt MariaDB
host = db_settings.get("mariadb_host", "localhost") host = db_settings.get("mariadb_host", "localhost")
user = db_settings.get("mariadb_user", "") user = db_settings.get("mariadb_user", "")
@ -187,7 +208,9 @@ def ensure_quotes_table(db_conn, log_func):
QUOTE_DATETIME TEXT, QUOTE_DATETIME TEXT,
QUOTE_GAME TEXT, QUOTE_GAME TEXT,
QUOTE_REMOVED BOOLEAN DEFAULT 0, QUOTE_REMOVED BOOLEAN DEFAULT 0,
QUOTE_REMOVED_BY TEXT QUOTE_REMOVED_BY TEXT,
FOREIGN KEY (QUOTEE) REFERENCES users(UUID),
FOREIGN KEY (QUOTE_REMOVED_BY) REFERENCES users(UUID)
) )
""" """
else: else:
@ -200,7 +223,9 @@ def ensure_quotes_table(db_conn, log_func):
QUOTE_DATETIME DATETIME DEFAULT CURRENT_TIMESTAMP, QUOTE_DATETIME DATETIME DEFAULT CURRENT_TIMESTAMP,
QUOTE_GAME VARCHAR(200), QUOTE_GAME VARCHAR(200),
QUOTE_REMOVED BOOLEAN DEFAULT FALSE, QUOTE_REMOVED BOOLEAN DEFAULT FALSE,
QUOTE_REMOVED_BY VARCHAR(100) QUOTE_REMOVED_BY VARCHAR(100),
FOREIGN KEY (QUOTEE) REFERENCES users(UUID) ON DELETE SET NULL
FOREIGN KEY (QUOTE_REMOVED_BY) REFERENCES users(UUID) ON DELETE SET NULL
) )
""" """
@ -208,7 +233,7 @@ def ensure_quotes_table(db_conn, log_func):
if result is None: if result is None:
# If run_db_operation returns None on error, handle or raise: # If run_db_operation returns None on error, handle or raise:
error_msg = "Failed to create 'quotes' table!" error_msg = "Failed to create 'quotes' table!"
log_func(error_msg, "ERROR") log_func(error_msg, "CRITICAL")
raise RuntimeError(error_msg) raise RuntimeError(error_msg)
log_func("Successfully created table 'quotes'.") log_func("Successfully created table 'quotes'.")
@ -267,7 +292,7 @@ def ensure_users_table(db_conn, log_func):
twitch_user_id TEXT, twitch_user_id TEXT,
twitch_username TEXT, twitch_username TEXT,
twitch_user_display_name TEXT, twitch_user_display_name TEXT,
datetime_linked TEXT DEFAULT CURRENT_TIMESTAMP, datetime_linked TEXT,
user_is_banned BOOLEAN DEFAULT 0, user_is_banned BOOLEAN DEFAULT 0,
user_is_bot BOOLEAN DEFAULT 0 user_is_bot BOOLEAN DEFAULT 0
) )
@ -282,7 +307,7 @@ def ensure_users_table(db_conn, log_func):
twitch_user_id VARCHAR(100), twitch_user_id VARCHAR(100),
twitch_username VARCHAR(100), twitch_username VARCHAR(100),
twitch_user_display_name VARCHAR(100), twitch_user_display_name VARCHAR(100),
datetime_linked DATETIME DEFAULT CURRENT_TIMESTAMP, datetime_linked DATETIME,
user_is_banned BOOLEAN DEFAULT FALSE, user_is_banned BOOLEAN DEFAULT FALSE,
user_is_bot BOOLEAN DEFAULT FALSE user_is_bot BOOLEAN DEFAULT FALSE
) )
@ -291,7 +316,7 @@ def ensure_users_table(db_conn, log_func):
result = run_db_operation(db_conn, "write", create_table_sql, log_func=log_func) result = run_db_operation(db_conn, "write", create_table_sql, log_func=log_func)
if result is None: if result is None:
error_msg = "Failed to create 'users' table!" error_msg = "Failed to create 'users' table!"
log_func(error_msg, "ERROR") log_func(error_msg, "CRITICAL")
raise RuntimeError(error_msg) raise RuntimeError(error_msg)
log_func("Successfully created table 'users'.") log_func("Successfully created table 'users'.")
@ -301,17 +326,24 @@ def ensure_users_table(db_conn, log_func):
# Lookup user function # Lookup user function
######################## ########################
def lookup_user(db_conn, log_func, identifier, identifier_type="discord_user_id"): def lookup_user(db_conn, log_func, identifier: str, identifier_type: str, target_identifier: str = None):
""" """
Looks up a user in the 'users' table based on the given identifier_type: Looks up a user in the 'users' table based on the given identifier_type.
- "uuid"
- "discord_user_id"
- "discord_username"
- "twitch_user_id"
- "twitch_username"
You can add more if needed.
Returns a dictionary with all columns: The accepted identifier_type values are:
- "uuid"
- "discord_user_id" or alias "discord"
- "discord_username"
- "discord_user_display_name"
- "twitch_user_id" or alias "twitch"
- "twitch_username"
- "twitch_user_display_name"
Optionally, if target_identifier is provided (must be one of the accepted columns),
only that column's value will be returned instead of the full user record.
Returns:
If target_identifier is None: A dictionary with the following keys:
{ {
"UUID": str, "UUID": str,
"discord_user_id": str or None, "discord_user_id": str or None,
@ -320,23 +352,42 @@ def lookup_user(db_conn, log_func, identifier, identifier_type="discord_user_id"
"twitch_user_id": str or None, "twitch_user_id": str or None,
"twitch_username": str or None, "twitch_username": str or None,
"twitch_user_display_name": str or None, "twitch_user_display_name": str or None,
"datetime_linked": str (or datetime in MariaDB), "datetime_linked": str (or datetime as stored in the database),
"user_is_banned": bool or int, "user_is_banned": bool or int,
"user_is_bot": bool or int "user_is_bot": bool or int
} }
If target_identifier is provided: The value from the record corresponding to that column.
If not found, returns None. If the lookup fails or the parameters are invalid: None.
""" """
valid_cols = ["uuid", "discord_user_id", "discord_username", # Define the valid columns for lookup and for target extraction.
"twitch_user_id", "twitch_username"] valid_cols = [
"uuid", "discord_user_id", "discord_username",
"twitch_user_id", "twitch_username", "discord",
"twitch", "discord_user_display_name",
"twitch_user_display_name"
]
# Ensure the provided identifier_type is acceptable.
if identifier_type.lower() not in valid_cols: if identifier_type.lower() not in valid_cols:
if log_func: if log_func:
log_func(f"lookup_user error: invalid identifier_type={identifier_type}", "WARNING") log_func(f"lookup_user error: invalid identifier_type '{identifier_type}'", "WARNING")
return None return None
# Build the query # Convert shorthand identifier types to their full column names.
if identifier_type.lower() == "discord":
identifier_type = "discord_user_id"
elif identifier_type.lower() == "twitch":
identifier_type = "twitch_user_id"
# If a target_identifier is provided, validate that too.
if target_identifier is not None:
if target_identifier.lower() not in valid_cols:
if log_func:
log_func(f"lookup_user error: invalid target_identifier '{target_identifier}'", "WARNING")
return None
# Build the query using the (now validated) identifier_type.
query = f""" query = f"""
SELECT SELECT
UUID, UUID,
@ -354,13 +405,15 @@ def lookup_user(db_conn, log_func, identifier, identifier_type="discord_user_id"
LIMIT 1 LIMIT 1
""" """
# Execute the database operation. Adjust run_db_operation() as needed.
rows = run_db_operation(db_conn, "read", query, params=(identifier,), log_func=log_func) rows = run_db_operation(db_conn, "read", query, params=(identifier,), log_func=log_func)
if not rows: if not rows:
if log_func:
log_func(f"lookup_user: No user found for {identifier_type}='{identifier}'", "DEBUG")
return None return None
# We have at least one row # Since we have a single row, convert it to a dictionary.
row = rows[0] # single row row = rows[0]
# Build a dictionary
user_data = { user_data = {
"UUID": row[0], "UUID": row[0],
"discord_user_id": row[1], "discord_user_id": row[1],
@ -373,4 +426,613 @@ def lookup_user(db_conn, log_func, identifier, identifier_type="discord_user_id"
"user_is_banned": row[8], "user_is_banned": row[8],
"user_is_bot": row[9], "user_is_bot": row[9],
} }
# If the caller requested a specific target column, return that value.
if target_identifier:
# Adjust for potential alias: if target_identifier is an alias,
# translate it to the actual column name.
target_identifier = target_identifier.lower()
if target_identifier == "discord":
target_identifier = "discord_user_id"
elif target_identifier == "twitch":
target_identifier = "twitch_user_id"
# The key for "uuid" is stored as "UUID" in our dict.
if target_identifier == "uuid":
target_identifier = "UUID"
if target_identifier in user_data:
return user_data[target_identifier]
else:
if log_func:
log_func(f"lookup_user error: target_identifier '{target_identifier}' not present in user data", "WARNING")
return None
# Otherwise, return the full user record.
return user_data return user_data
def ensure_chatlog_table(db_conn, log_func):
"""
Checks if 'chat_log' table exists. If not, creates it.
The table layout:
MESSAGE_ID (PK, auto increment)
UUID (references users.UUID, if you want a foreign key, see note below)
MESSAGE_CONTENT (text)
PLATFORM (string, e.g. 'twitch' or discord server name)
CHANNEL (the twitch channel or discord channel name)
DATETIME (defaults to current timestamp)
ATTACHMENTS (text; store hyperlink(s) or empty)
For maximum compatibility, we won't enforce the foreign key at the DB level,
but you can add it if you want.
"""
is_sqlite = "sqlite3" in str(type(db_conn)).lower()
# 1) Check if table exists
if is_sqlite:
check_sql = """
SELECT name
FROM sqlite_master
WHERE type='table'
AND name='chat_log'
"""
else:
check_sql = """
SELECT table_name
FROM information_schema.tables
WHERE table_name = 'chat_log'
AND table_schema = DATABASE()
"""
rows = run_db_operation(db_conn, "read", check_sql, log_func=log_func)
if rows and rows[0] and rows[0][0]:
log_func("Table 'chat_log' already exists, skipping creation.", "DEBUG")
return
# 2) Table doesn't exist => create it
log_func("Table 'chat_log' does not exist; creating now...")
if is_sqlite:
create_sql = """
CREATE TABLE chat_log (
MESSAGE_ID INTEGER PRIMARY KEY AUTOINCREMENT,
UUID TEXT,
MESSAGE_CONTENT TEXT,
PLATFORM TEXT,
CHANNEL TEXT,
DATETIME TEXT DEFAULT CURRENT_TIMESTAMP,
ATTACHMENTS TEXT,
FOREIGN KEY (UUID) REFERENCES users(UUID)
)
"""
else:
create_sql = """
CREATE TABLE chat_log (
MESSAGE_ID INT PRIMARY KEY AUTO_INCREMENT,
UUID VARCHAR(36),
MESSAGE_CONTENT TEXT,
PLATFORM VARCHAR(100),
CHANNEL VARCHAR(100),
DATETIME DATETIME DEFAULT CURRENT_TIMESTAMP,
ATTACHMENTS TEXT,
FOREIGN KEY (UUID) REFERENCES users(UUID) ON DELETE SET NULL
)
"""
result = run_db_operation(db_conn, "write", create_sql, log_func=log_func)
if result is None:
error_msg = "Failed to create 'chat_log' table!"
log_func(error_msg, "CRITICAL")
raise RuntimeError(error_msg)
log_func("Successfully created table 'chat_log'.", "INFO")
def log_message(db_conn, log_func, user_uuid, message_content, platform, channel, attachments=None):
"""
Inserts a row into 'chat_log' with the given fields.
user_uuid: The user's UUID from the 'users' table (string).
message_content: The text of the message.
platform: 'twitch' or discord server name, etc.
channel: The channel name (Twitch channel, or Discord channel).
attachments: Optional string of hyperlinks if available.
DATETIME will default to current timestamp in the DB.
"""
if attachments is None or not "https://" in attachments:
attachments = ""
insert_sql = """
INSERT INTO chat_log (
UUID,
MESSAGE_CONTENT,
PLATFORM,
CHANNEL,
ATTACHMENTS
)
VALUES (?, ?, ?, ?, ?)
"""
params = (user_uuid, message_content, platform, channel, attachments)
rowcount = run_db_operation(db_conn, "write", insert_sql, params, log_func=log_func)
if rowcount and rowcount > 0:
log_func(f"Logged message for UUID={user_uuid} in 'chat_log'.", "DEBUG")
else:
log_func("Failed to log message in 'chat_log'.", "ERROR")
def ensure_userhowls_table(db_conn, log_func):
"""
Checks if 'user_howls' table exists; if not, creates it:
ID (PK) | UUID (FK -> users.UUID) | HOWL (int) | DATETIME (auto timestamp)
"""
is_sqlite = "sqlite3" in str(type(db_conn)).lower()
# Existence check
if is_sqlite:
check_sql = """
SELECT name
FROM sqlite_master
WHERE type='table'
AND name='user_howls'
"""
else:
check_sql = """
SELECT table_name
FROM information_schema.tables
WHERE table_name = 'user_howls'
AND table_schema = DATABASE()
"""
rows = run_db_operation(db_conn, "read", check_sql, log_func=log_func)
if rows and rows[0] and rows[0][0]:
log_func("Table 'user_howls' already exists, skipping creation.", "DEBUG")
return
log_func("Table 'user_howls' does not exist; creating now...", "INFO")
if is_sqlite:
create_sql = """
CREATE TABLE user_howls (
ID INTEGER PRIMARY KEY AUTOINCREMENT,
UUID TEXT,
HOWL INT,
DATETIME TEXT DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (UUID) REFERENCES users(UUID)
)
"""
else:
create_sql = """
CREATE TABLE user_howls (
ID INT PRIMARY KEY AUTO_INCREMENT,
UUID VARCHAR(36),
HOWL INT,
DATETIME DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (UUID) REFERENCES users(UUID) ON DELETE SET NULL
)
"""
result = run_db_operation(db_conn, "write", create_sql, log_func=log_func)
if result is None:
err_msg = "Failed to create 'user_howls' table!"
log_func(err_msg, "ERROR")
raise RuntimeError(err_msg)
log_func("Successfully created table 'user_howls'.", "INFO")
def insert_howl(db_conn, log_func, user_uuid, howl_value):
"""
Insert a row into user_howls with the user's UUID, the integer 0-100,
and DATETIME defaulting to now.
"""
sql = """
INSERT INTO user_howls (UUID, HOWL)
VALUES (?, ?)
"""
params = (user_uuid, howl_value)
rowcount = run_db_operation(db_conn, "write", sql, params, log_func=log_func)
if rowcount and rowcount > 0:
log_func(f"Recorded a {howl_value}% howl for UUID={user_uuid}.", "DEBUG")
else:
log_func(f"Failed to record {howl_value}% howl for UUID={user_uuid}.", "ERROR")
def get_howl_stats(db_conn, log_func, user_uuid):
"""
Returns a dict with { 'count': int, 'average': float, 'count_zero': int, 'count_hundred': int }
or None if there are no rows at all for that UUID.
"""
sql = """
SELECT
COUNT(*),
AVG(HOWL),
SUM(HOWL=0),
SUM(HOWL=100)
FROM user_howls
WHERE UUID = ?
"""
rows = run_db_operation(db_conn, "read", sql, (user_uuid,), log_func=log_func)
if not rows:
return None
row = rows[0] # (count, avg, zero_count, hundred_count)
count = row[0] if row[0] else 0
avg = float(row[1]) if row[1] else 0.0
zero_count = row[2] if row[2] else 0
hundred_count = row[3] if row[3] else 0
if count < 1:
return None # user has no howls
return {
"count": count,
"average": avg,
"count_zero": zero_count,
"count_hundred": hundred_count
}
def get_global_howl_stats(db_conn, log_func):
"""
Returns a dictionary with total howls, average howl percentage, unique users,
and counts of extreme (0% and 100%) howls.
"""
sql = """
SELECT COUNT(*) AS total_howls,
AVG(HOWL) AS average_howl,
COUNT(DISTINCT UUID) AS unique_users,
SUM(HOWL = 0) AS count_zero,
SUM(HOWL = 100) AS count_hundred
FROM user_howls
"""
rows = run_db_operation(db_conn, "read", sql, log_func=log_func)
if not rows or not rows[0] or rows[0][0] is None:
return None # No howl data exists
return {
"total_howls": rows[0][0],
"average_howl": float(rows[0][1]) if rows[0][1] is not None else 0.0,
"unique_users": rows[0][2],
"count_zero": rows[0][3],
"count_hundred": rows[0][4],
}
def ensure_discord_activity_table(db_conn, log_func):
"""
Ensures the 'discord_activity' table exists.
Logs voice events, cameras, streaming, gaming, and Discord activities.
"""
is_sqlite = "sqlite3" in str(type(db_conn)).lower()
if is_sqlite:
check_sql = "SELECT name FROM sqlite_master WHERE type='table' AND name='discord_activity'"
else:
check_sql = """
SELECT table_name FROM information_schema.tables
WHERE table_name = 'discord_activity' AND table_schema = DATABASE()
"""
rows = run_db_operation(db_conn, "read", check_sql, log_func=log_func)
if rows and rows[0]:
log_func("Table 'discord_activity' already exists, skipping creation.", "DEBUG")
return
log_func("Creating 'discord_activity' table...", "INFO")
if is_sqlite:
create_sql = """
CREATE TABLE discord_activity (
ID INTEGER PRIMARY KEY AUTOINCREMENT,
UUID TEXT,
ACTION TEXT CHECK(ACTION IN (
'JOIN', 'LEAVE', 'MUTE', 'UNMUTE', 'DEAFEN', 'UNDEAFEN',
'STREAM_START', 'STREAM_STOP', 'CAMERA_ON', 'CAMERA_OFF',
'GAME_START', 'GAME_STOP', 'LISTENING_SPOTIFY', 'DISCORD_ACTIVITY', 'VC_MOVE'
)),
GUILD_ID TEXT,
VOICE_CHANNEL TEXT,
ACTION_DETAIL TEXT DEFAULT NULL,
DATETIME TEXT DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (UUID) REFERENCES users(UUID)
)
"""
else:
create_sql = """
CREATE TABLE discord_activity (
ID INT PRIMARY KEY AUTO_INCREMENT,
UUID VARCHAR(36),
ACTION ENUM(
'JOIN', 'LEAVE', 'MUTE', 'UNMUTE', 'DEAFEN', 'UNDEAFEN',
'STREAM_START', 'STREAM_STOP', 'CAMERA_ON', 'CAMERA_OFF',
'GAME_START', 'GAME_STOP', 'LISTENING_SPOTIFY', 'DISCORD_ACTIVITY', 'VC_MOVE'
),
GUILD_ID VARCHAR(36),
VOICE_CHANNEL VARCHAR(100),
ACTION_DETAIL TEXT DEFAULT NULL,
DATETIME DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (UUID) REFERENCES users(UUID) ON DELETE SET NULL
)
"""
try:
result = run_db_operation(db_conn, "write", create_sql, log_func=log_func)
except Exception as e:
log_func(f"Unable to create the table: discord_activity: {e}")
if result is None:
log_func("Failed to create 'discord_activity' table!", "CRITICAL")
raise RuntimeError("Database table creation failed.")
log_func("Successfully created table 'discord_activity'.", "INFO")
def log_discord_activity(db_conn, log_func, guild_id, user_uuid, action, voice_channel, action_detail=None):
"""
Logs Discord activities (playing games, listening to Spotify, streaming).
Duplicate detection:
- Fetch the last NUM_RECENT_ENTRIES events for this user & action.
- Normalize the ACTION_DETAIL values.
- If the most recent event(s) all match the new event's detail (i.e. no intervening non-matching event)
and the latest matching event was logged less than DUPLICATE_THRESHOLD ago, skip logging.
- This allows a "reset": if the user changes state (e.g. changes song or channel) and then reverts,
the new event is logged.
"""
def normalize_detail(detail):
"""Return a normalized version of the detail for comparison (or None if detail is None)."""
return detail.strip().lower() if detail else None
# How long to consider an event “fresh” enough to be considered a duplicate.
DUPLICATE_THRESHOLD = datetime.timedelta(minutes=5)
# How many recent events to check.
NUM_RECENT_ENTRIES = 5
# Verify that the user exists in 'users' before proceeding.
user_check = run_db_operation(
db_conn, "read", "SELECT UUID FROM users WHERE UUID = ?", (user_uuid,), log_func
)
if not user_check:
log_func(f"WARNING: Attempted to log activity for non-existent UUID: {user_uuid}", "WARNING")
return # Prevent foreign key issues.
now = datetime.datetime.now()
normalized_new = normalize_detail(action_detail)
# Query the last NUM_RECENT_ENTRIES events for this user and action.
query = """
SELECT DATETIME, ACTION_DETAIL
FROM discord_activity
WHERE UUID = ? AND ACTION = ?
ORDER BY DATETIME DESC
LIMIT ?
"""
rows = run_db_operation(
db_conn, "read", query, params=(user_uuid, action, NUM_RECENT_ENTRIES), log_func=log_func
)
# Determine the timestamp of the most recent event that matches the new detail,
# and the most recent event that is different.
last_same = None # Timestamp of the most recent event matching normalized_new.
last_different = None # Timestamp of the most recent event with a different detail.
for row in rows:
dt_str, detail = row
try:
dt = datetime.datetime.strptime(dt_str, "%Y-%m-%d %H:%M:%S")
except Exception as e:
log_func(f"Error parsing datetime '{dt_str}': {e}", "ERROR")
continue
normalized_existing = normalize_detail(detail)
if normalized_existing == normalized_new:
# Record the most recent matching event.
if last_same is None or dt > last_same:
last_same = dt
else:
# Record the most recent non-matching event.
if last_different is None or dt > last_different:
last_different = dt
# Decide whether to skip logging:
# If there is a matching (same-detail) event, and either no different event exists OR the matching event
# is more recent than the last different event (i.e. the user's current state is still the same),
# then if that event is within the DUPLICATE_THRESHOLD, skip logging.
if last_same is not None:
if (last_different is None) or (last_same > last_different):
if now - last_same > DUPLICATE_THRESHOLD:
#log_func(f"Duplicate {action} event for user {user_uuid} (detail '{action_detail}') within threshold; skipping log.","DEBUG")
return
# Prepare the voice_channel value (if its an object with a name, use that).
channel_val = voice_channel.name if (voice_channel and hasattr(voice_channel, "name")) else voice_channel
# Insert the new event.
sql = """
INSERT INTO discord_activity (UUID, ACTION, GUILD_ID, VOICE_CHANNEL, ACTION_DETAIL)
VALUES (?, ?, ?, ?, ?)
"""
params = (user_uuid, action, guild_id, channel_val, action_detail)
rowcount = run_db_operation(db_conn, "write", sql, params, log_func)
if rowcount and rowcount > 0:
detail_str = f" ({action_detail})" if action_detail else ""
log_func(f"Logged Discord activity in Guild {guild_id}: {action}{detail_str}", "DEBUG")
else:
log_func("Failed to log Discord activity.", "ERROR")
def ensure_bot_events_table(db_conn, log_func):
"""
Ensures the 'bot_events' table exists, which logs major bot-related events.
"""
is_sqlite = "sqlite3" in str(type(db_conn)).lower()
# Check if table exists
check_sql = "SELECT name FROM sqlite_master WHERE type='table' AND name='bot_events'" if is_sqlite else """
SELECT table_name FROM information_schema.tables
WHERE table_name = 'bot_events' AND table_schema = DATABASE()
"""
rows = run_db_operation(db_conn, "read", check_sql, log_func=log_func)
if rows and rows[0]:
log_func("Table 'bot_events' already exists, skipping creation.", "DEBUG")
return
log_func("Creating 'bot_events' table...", "INFO")
# Define SQL Schema
create_sql = """
CREATE TABLE bot_events (
EVENT_ID INTEGER PRIMARY KEY AUTOINCREMENT,
EVENT_TYPE TEXT,
EVENT_DETAILS TEXT,
DATETIME TEXT DEFAULT CURRENT_TIMESTAMP
)
""" if is_sqlite else """
CREATE TABLE bot_events (
EVENT_ID INT PRIMARY KEY AUTO_INCREMENT,
EVENT_TYPE VARCHAR(50),
EVENT_DETAILS TEXT,
DATETIME DATETIME DEFAULT CURRENT_TIMESTAMP
)
"""
# Create the table
result = run_db_operation(db_conn, "write", create_sql, log_func=log_func)
if result is None:
log_func("Failed to create 'bot_events' table!", "CRITICAL")
raise RuntimeError("Database table creation failed.")
log_func("Successfully created table 'bot_events'.", "INFO")
def log_bot_event(db_conn, log_func, event_type, event_details):
"""
Logs a bot event (e.g., startup, shutdown, disconnection).
"""
sql = """
INSERT INTO bot_events (EVENT_TYPE, EVENT_DETAILS)
VALUES (?, ?)
"""
params = (event_type, event_details)
rowcount = run_db_operation(db_conn, "write", sql, params, log_func)
if rowcount and rowcount > 0:
log_func(f"Logged bot event: {event_type} - {event_details}", "DEBUG")
else:
log_func("Failed to log bot event.", "ERROR")
def get_event_summary(db_conn, log_func, time_span="7d"):
"""
Retrieves bot event statistics based on a given time span.
Supports:
- "7d" (7 days)
- "1m" (1 month)
- "24h" (last 24 hours)
Returns:
OrderedDict with event statistics.
"""
from collections import OrderedDict
import datetime
# Time span mapping
time_mappings = {
"7d": "7 days",
"1m": "1 month",
"24h": "24 hours"
}
if time_span not in time_mappings:
log_func(f"Invalid time span '{time_span}', defaulting to '7d'", "WARNING")
time_span = "7d"
# Define SQL query
sql = f"""
SELECT EVENT_TYPE, COUNT(*)
FROM bot_events
WHERE DATETIME >= datetime('now', '-{time_mappings[time_span]}')
GROUP BY EVENT_TYPE
ORDER BY COUNT(*) DESC
"""
rows = run_db_operation(db_conn, "read", sql, log_func=log_func)
# Organize data into OrderedDict
summary = OrderedDict()
summary["time_span"] = time_span
for event_type, count in rows:
summary[event_type] = count
return summary
def ensure_link_codes_table(db_conn, log_func):
"""
Ensures the 'link_codes' table exists.
This table stores one-time-use account linking codes.
"""
is_sqlite = "sqlite3" in str(type(db_conn)).lower()
check_sql = "SELECT name FROM sqlite_master WHERE type='table' AND name='link_codes'" if is_sqlite else """
SELECT table_name FROM information_schema.tables
WHERE table_name = 'link_codes' AND table_schema = DATABASE()
"""
rows = run_db_operation(db_conn, "read", check_sql, log_func=log_func)
if rows and rows[0]:
log_func("Table 'link_codes' already exists, skipping creation.", "DEBUG")
return
log_func("Creating 'link_codes' table...", "INFO")
create_sql = """
CREATE TABLE link_codes (
ID INTEGER PRIMARY KEY AUTOINCREMENT,
DISCORD_USER_ID TEXT UNIQUE,
LINK_CODE TEXT UNIQUE,
CREATED_AT TEXT DEFAULT CURRENT_TIMESTAMP
)
""" if is_sqlite else """
CREATE TABLE link_codes (
ID INT PRIMARY KEY AUTO_INCREMENT,
DISCORD_USER_ID VARCHAR(50) UNIQUE,
LINK_CODE VARCHAR(50) UNIQUE,
CREATED_AT DATETIME DEFAULT CURRENT_TIMESTAMP
)
"""
result = run_db_operation(db_conn, "write", create_sql, log_func=log_func)
if result is None:
log_func("Failed to create 'link_codes' table!", "CRITICAL")
raise RuntimeError("Database table creation failed.")
log_func("Successfully created table 'link_codes'.", "INFO")
def merge_uuid_data(db_conn, log_func, old_uuid, new_uuid):
"""
Merges all records from the old UUID (Twitch account) into the new UUID (Discord account).
This replaces all instances of the old UUID in all relevant tables with the new UUID,
ensuring that no data is lost in the linking process.
After merging, the old UUID entry is removed from the `users` table.
"""
log_func(f"Starting UUID merge: {old_uuid} -> {new_uuid}", "INFO")
tables_to_update = [
"voice_activity_log",
"bot_events",
"chat_log",
"user_howls",
"quotes"
]
for table in tables_to_update:
sql = f"UPDATE {table} SET UUID = ? WHERE UUID = ?"
rowcount = run_db_operation(db_conn, "update", sql, (new_uuid, old_uuid), log_func)
log_func(f"Updated {rowcount} rows in {table} (transferring {old_uuid} -> {new_uuid})", "DEBUG")
# Finally, delete the old UUID from the `users` table
delete_sql = "DELETE FROM users WHERE UUID = ?"
rowcount = run_db_operation(db_conn, "write", delete_sql, (old_uuid,), log_func)
log_func(f"Deleted old UUID {old_uuid} from 'users' table ({rowcount} rows affected)", "INFO")
log_func(f"UUID merge complete: {old_uuid} -> {new_uuid}", "INFO")

View File

@ -4,7 +4,9 @@ import random
import json import json
import re import re
import functools import functools
import inspect
import uuid
from modules.db import run_db_operation, lookup_user
try: try:
# 'regex' on PyPI supports `\p{L}`, `\p{N}`, etc. # 'regex' on PyPI supports `\p{L}`, `\p{N}`, etc.
@ -21,30 +23,29 @@ def monitor_cmds(log_func):
Decorator that logs when a command starts and ends execution. Decorator that logs when a command starts and ends execution.
""" """
def decorator(func): def decorator(func):
@functools.wraps(func) # Preserve function metadata @functools.wraps(func)
async def wrapper(*args, **kwargs): async def wrapper(*args, **kwargs):
start_time = time.time() start_time = time.time()
try: try:
# Extract a command name from the function name
cmd_name = str(func.__name__).split("_")[1] cmd_name = str(func.__name__).split("_")[1]
log_func(f"Command '{cmd_name}' started execution.", "DEBUG") log_func(f"Command '{cmd_name}' started execution.", "DEBUG")
# Await the actual function (since it's an async command) # Await the actual command function
result = await func(*args, **kwargs) result = await func(*args, **kwargs)
end_time = time.time() end_time = time.time()
cmd_duration = end_time - start_time cmd_duration = round(end_time - start_time, 2)
cmd_duration = str(round(cmd_duration, 2))
log_func(f"Command '{cmd_name}' finished execution after {cmd_duration}s.", "DEBUG") log_func(f"Command '{cmd_name}' finished execution after {cmd_duration}s.", "DEBUG")
return result # Return the result of the command return result
except Exception as e: except Exception as e:
end_time = time.time() end_time = time.time()
cmd_duration = end_time - start_time cmd_duration = round(end_time - start_time, 2)
cmd_duration = str(round(cmd_duration, 2))
log_func(f"Command '{cmd_name}' FAILED while executing after {cmd_duration}s: {e}", "CRITICAL") log_func(f"Command '{cmd_name}' FAILED while executing after {cmd_duration}s: {e}", "CRITICAL")
# Explicitly preserve the original signature for slash command introspection
return wrapper # Return the wrapped function wrapper.__signature__ = inspect.signature(func)
return wrapper
return decorator # Return the decorator itself return decorator
def format_uptime(seconds: float) -> tuple[str, int]: def format_uptime(seconds: float) -> tuple[str, int]:
""" """
@ -57,6 +58,7 @@ def format_uptime(seconds: float) -> tuple[str, int]:
(Human-readable string, total seconds) (Human-readable string, total seconds)
""" """
seconds = int(seconds) # Ensure integer seconds seconds = int(seconds) # Ensure integer seconds
seconds_int = seconds
# Define time units # Define time units
units = [ units = [
@ -76,7 +78,7 @@ def format_uptime(seconds: float) -> tuple[str, int]:
time_values.append(f"{value} {unit_name}{'s' if value > 1 else ''}") # Auto pluralize time_values.append(f"{value} {unit_name}{'s' if value > 1 else ''}") # Auto pluralize
# Return only the **two most significant** time units (e.g., "3 days, 4 hours") # Return only the **two most significant** time units (e.g., "3 days, 4 hours")
return (", ".join(time_values[:2]), seconds) if time_values else ("0 seconds", 0) return (", ".join(time_values[:2]), seconds_int) if time_values else ("0 seconds", 0)
def get_random_reply(dictionary_name: str, category: str, **variables) -> str: def get_random_reply(dictionary_name: str, category: str, **variables) -> str:
""" """
@ -281,6 +283,8 @@ def initialize_help_data(bot, help_json_path, is_discord, log_func):
- Loaded commands not in help file => "missing help" - Loaded commands not in help file => "missing help"
""" """
platform_name = "Discord" if is_discord else "Twitch"
if not os.path.exists(help_json_path): if not os.path.exists(help_json_path):
log_func(f"Help file '{help_json_path}' not found. No help data loaded.", "WARNING") log_func(f"Help file '{help_json_path}' not found. No help data loaded.", "WARNING")
bot.help_data = {} bot.help_data = {}
@ -307,12 +311,12 @@ def initialize_help_data(bot, help_json_path, is_discord, log_func):
# 1) Commands in file but not loaded # 1) Commands in file but not loaded
missing_cmds = file_cmds - loaded_cmds missing_cmds = file_cmds - loaded_cmds
for cmd in missing_cmds: for cmd in missing_cmds:
log_func(f"Help file has '{cmd}', but it's not loaded on this {'Discord' if is_discord else 'Twitch'} bot (deprecated?).", "WARNING") log_func(f"Help file has '{cmd}', but it's not loaded on this {platform_name} bot (deprecated?).", "WARNING")
# 2) Commands loaded but not in file # 2) Commands loaded but not in file
needed_cmds = loaded_cmds - file_cmds needed_cmds = loaded_cmds - file_cmds
for cmd in needed_cmds: for cmd in needed_cmds:
log_func(f"Command '{cmd}' is loaded on {('Discord' if is_discord else 'Twitch')} but no help info is provided in {help_json_path}.", "WARNING") log_func(f"Command '{cmd}' is loaded on {platform_name} but no help info is provided in {help_json_path}.", "WARNING")
def get_loaded_commands(bot, log_func, is_discord): def get_loaded_commands(bot, log_func, is_discord):
@ -334,23 +338,16 @@ def get_loaded_commands(bot, log_func, is_discord):
# 'bot.commands' is a set of Command objects # 'bot.commands' is a set of Command objects
for cmd_obj in bot.commands: for cmd_obj in bot.commands:
commands_list.append(cmd_obj.name) commands_list.append(cmd_obj.name)
log_func(f"Discord commands body: {commands_list}", "DEBUG") debug_level ="DEBUG" if len(commands_list) > 0 else "WARNING"
log_func(f"Discord commands body: {commands_list}", f"{debug_level}")
except Exception as e: except Exception as e:
log_func(f"Error retrieving Discord commands: {e}", "ERROR") log_func(f"Error retrieving Discord commands: {e}", "ERROR")
elif not is_discord: elif not is_discord:
# For TwitchIO
#if isinstance(bot.commands, set):
try: try:
#commands_attr = bot.commands
#log_func(f"Twitch type(bot.commands) => {type(commands_attr)}", "DEBUG")
# 'bot.all_commands' is a dict: { command_name: Command(...) }
#all_cmd_names = list(bot.all_commands.keys())
#log_func(f"Twitch commands body: {all_cmd_names}", "DEBUG")
#commands_list.extend(all_cmd_names)
for cmd_obj in bot._commands: for cmd_obj in bot._commands:
commands_list.append(cmd_obj) commands_list.append(cmd_obj)
log_func(f"Twitch commands body: {commands_list}", "DEBUG") debug_level ="DEBUG" if len(commands_list) > 0 else "WARNING"
log_func(f"Twitch commands body: {commands_list}", f"{debug_level}")
except Exception as e: except Exception as e:
log_func(f"Error retrieving Twitch commands: {e}", "ERROR") log_func(f"Error retrieving Twitch commands: {e}", "ERROR")
else: else:
@ -368,7 +365,7 @@ def build_discord_help_message(cmd_name, cmd_help_dict):
subcommands = cmd_help_dict.get("subcommands", {}) subcommands = cmd_help_dict.get("subcommands", {})
examples = cmd_help_dict.get("examples", []) examples = cmd_help_dict.get("examples", [])
lines = [f"**Help for `!{cmd_name}`**:", lines = [f"**Help for `{cmd_name}`**:",
f"Description: {description}"] f"Description: {description}"]
if subcommands: if subcommands:
@ -422,11 +419,174 @@ async def send_message(ctx, text):
""" """
await ctx.send(text) await ctx.send(text)
def track_user_activity(
db_conn,
log_func,
platform: str,
user_id: str,
username: str,
display_name: str,
user_is_bot: bool = False
):
"""
Checks or creates/updates a user in the 'users' table for the given platform's message.
:param db_conn: The active DB connection
:param log_func: The logging function (message, level="INFO")
:param platform: "discord" or "twitch"
:param user_id: e.g., Discord user ID or Twitch user ID
:param username: The raw username (no #discriminator for Discord)
:param display_name: The users display name
:param user_is_bot: Boolean if the user is recognized as a bot on that platform
"""
log_func(f"UUI Lookup for: {username} - {user_id} ({platform.lower()}) ...", "DEBUG")
# Decide which column we use for the ID lookup
# "discord_user_id" or "twitch_user_id"
if platform.lower() in ("discord", "twitch"):
identifier_type = f"{platform.lower()}_user_id"
else:
log_func(f"Unknown platform '{platform}' in track_user_activity!", "WARNING")
return
# 1) Try to find an existing user row
user_data = lookup_user(db_conn, log_func, identifier=user_id, identifier_type=identifier_type)
if user_data:
# Found an existing row for that user ID on this platform
# Check if the username or display_name is different => if so, update
need_update = False
column_updates = []
params = []
log_func(f"... Returned {user_data}", "DEBUG")
if platform.lower() == "discord":
if user_data["discord_username"] != username:
need_update = True
column_updates.append("discord_username = ?")
params.append(username)
if user_data["discord_user_display_name"] != display_name:
need_update = True
column_updates.append("discord_user_display_name = ?")
params.append(display_name)
# Possibly check user_is_bot
# If it's different than what's stored, update
# (We must add a column in your table for that if you want it stored per-platform.)
# For demonstration, let's store it in "user_is_bot"
if user_data["user_is_bot"] != user_is_bot:
need_update = True
column_updates.append("user_is_bot = ?")
params.append(int(user_is_bot))
if need_update:
set_clause = ", ".join(column_updates)
update_sql = f"""
UPDATE users
SET {set_clause}
WHERE discord_user_id = ?
"""
params.append(user_id)
rowcount = run_db_operation(db_conn, "update", update_sql, params=params, log_func=log_func)
if rowcount and rowcount > 0:
log_func(f"Updated Discord user '{username}' (display '{display_name}') in 'users'.", "DEBUG")
elif platform.lower() == "twitch":
if user_data["twitch_username"] != username:
need_update = True
column_updates.append("twitch_username = ?")
params.append(username)
if user_data["twitch_user_display_name"] != display_name:
need_update = True
column_updates.append("twitch_user_display_name = ?")
params.append(display_name)
# Possibly store is_bot in user_is_bot
if user_data["user_is_bot"] != user_is_bot:
need_update = True
column_updates.append("user_is_bot = ?")
params.append(int(user_is_bot))
if need_update:
set_clause = ", ".join(column_updates)
update_sql = f"""
UPDATE users
SET {set_clause}
WHERE twitch_user_id = ?
"""
params.append(user_id)
rowcount = run_db_operation(db_conn, "update", update_sql, params=params, log_func=log_func)
if rowcount and rowcount > 0:
log_func(f"Updated Twitch user '{username}' (display '{display_name}') in 'users'.", "DEBUG")
else:
# 2) No row found => create a new user row
# Generate a new UUID for this user
new_uuid = str(uuid.uuid4())
if platform.lower() == "discord":
insert_sql = """
INSERT INTO users (
UUID,
discord_user_id,
discord_username,
discord_user_display_name,
user_is_bot
)
VALUES (?, ?, ?, ?, ?)
"""
params = (new_uuid, user_id, username, display_name, int(user_is_bot))
else: # "twitch"
insert_sql = """
INSERT INTO users (
UUID,
twitch_user_id,
twitch_username,
twitch_user_display_name,
user_is_bot
)
VALUES (?, ?, ?, ?, ?)
"""
params = (new_uuid, user_id, username, display_name, int(user_is_bot))
rowcount = run_db_operation(db_conn, "write", insert_sql, params, log_func=log_func)
if rowcount and rowcount > 0:
log_func(f"Created new user row for {platform} user '{username}' (display '{display_name}') with UUID={new_uuid}.", "DEBUG")
else:
log_func(f"Failed to create new user row for {platform} user '{username}'", "ERROR")
from modules.db import log_bot_event
def log_bot_startup(db_conn, log_func):
"""
Logs a bot startup event.
"""
log_bot_event(db_conn, log_func, "BOT_STARTUP", "Bot successfully started.")
def log_bot_shutdown(db_conn, log_func, intent: str = "Error/Crash"):
"""
Logs a bot shutdown event.
"""
log_bot_event(db_conn, log_func, "BOT_SHUTDOWN", f"Bot is shutting down - {intent}.")
def generate_link_code():
"""Generates a unique 8-character alphanumeric link code."""
import random, string
return ''.join(random.choices(string.ascii_uppercase + string.digits, k=8))
############################################### ###############################################
# Development Test Function (called upon start) # Development Test Function (called upon start)
############################################### ###############################################
def dev_func(db_conn, log): def dev_func(db_conn, log, enable: bool = False):
from modules.db import lookup_user if enable:
id = "203190147582394369" id = "203190147582394369"
id_type = "discord_user_id" id_type = "discord_user_id"
uui_info = lookup_user(db_conn, log, identifier=id, identifier_type=id_type) uui_info = lookup_user(db_conn, log, identifier=id, identifier_type=id_type)