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 improvementskami_dev
parent
aed3d24e33
commit
3ad6504d69
284
bot_discord.py
284
bot_discord.py
|
@ -1,11 +1,13 @@
|
|||
# bot_discord.py
|
||||
import discord
|
||||
from discord import app_commands
|
||||
from discord.ext import commands
|
||||
import importlib
|
||||
import cmd_discord
|
||||
|
||||
import modules
|
||||
import modules.utility
|
||||
from modules.db import log_message, lookup_user, log_bot_event
|
||||
|
||||
class DiscordBot(commands.Bot):
|
||||
def __init__(self, config, log_func):
|
||||
|
@ -19,8 +21,17 @@ class DiscordBot(commands.Bot):
|
|||
|
||||
self.log("Discord bot initiated")
|
||||
|
||||
cmd_class = str(type(self.commands)).split("'", 2)[1]
|
||||
log_func(f"DiscordBot.commands type: {cmd_class}", "DEBUG")
|
||||
# async def sync_slash_commands(self):
|
||||
# """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):
|
||||
"""
|
||||
|
@ -33,31 +44,288 @@ class DiscordBot(commands.Bot):
|
|||
Load all commands from cmd_discord.py
|
||||
"""
|
||||
try:
|
||||
importlib.reload(cmd_discord)
|
||||
cmd_discord.setup(self)
|
||||
importlib.reload(cmd_discord) # Reload the commands file
|
||||
cmd_discord.setup(self) # Ensure commands are registered
|
||||
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"
|
||||
|
||||
modules.utility.initialize_help_data(
|
||||
bot=self,
|
||||
help_json_path=help_json_path,
|
||||
is_discord=True,
|
||||
log_func=self.log
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
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):
|
||||
"""Logs every command execution at DEBUG level."""
|
||||
_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")
|
||||
|
||||
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}")
|
||||
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):
|
||||
try:
|
||||
|
|
|
@ -8,6 +8,7 @@ import cmd_twitch
|
|||
|
||||
import modules
|
||||
import modules.utility
|
||||
from modules.db import log_message, lookup_user, log_bot_event
|
||||
|
||||
class TwitchBot(commands.Bot):
|
||||
def __init__(self, config, log_func):
|
||||
|
@ -29,9 +30,6 @@ class TwitchBot(commands.Bot):
|
|||
|
||||
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
|
||||
self.load_commands()
|
||||
|
||||
|
@ -42,9 +40,13 @@ class TwitchBot(commands.Bot):
|
|||
self.db_conn = db_conn
|
||||
|
||||
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:
|
||||
return # Ignore bot's own messages
|
||||
return
|
||||
|
||||
# Log the command if it's a command
|
||||
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")
|
||||
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)
|
||||
|
||||
|
||||
async def event_ready(self):
|
||||
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):
|
||||
"""
|
||||
|
|
77
bots.py
77
bots.py
|
@ -6,6 +6,7 @@ import sys
|
|||
import time
|
||||
import traceback
|
||||
import globals
|
||||
from functools import partial
|
||||
|
||||
from discord.ext import commands
|
||||
from dotenv import load_dotenv
|
||||
|
@ -13,8 +14,10 @@ from dotenv import load_dotenv
|
|||
from bot_discord import DiscordBot
|
||||
from bot_twitch import TwitchBot
|
||||
|
||||
from modules.db import init_db_connection, run_db_operation
|
||||
from modules.db import ensure_quotes_table, ensure_users_table
|
||||
#from modules.db import init_db_connection, run_db_operation
|
||||
#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_dotenv()
|
||||
|
@ -32,9 +35,11 @@ except json.JSONDecodeError as e:
|
|||
sys.exit(1)
|
||||
|
||||
# Initiate logfile
|
||||
logfile_path = config_data["logfile_path"]
|
||||
logfile_path = config_data["logging"]["logfile_path"]
|
||||
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 !!!")
|
||||
|
||||
###############################
|
||||
|
@ -61,7 +66,7 @@ def log(message, level="INFO", exec_info=False):
|
|||
if level not in log_levels:
|
||||
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()
|
||||
uptime_str, _ = utility.format_uptime(elapsed)
|
||||
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()}"
|
||||
|
||||
# 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)
|
||||
|
||||
# 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:
|
||||
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.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:
|
||||
print(f"[WARNING] Failed to write to logfile: {e}")
|
||||
|
||||
# Handle fatal errors with shutdown
|
||||
if level == "FATAL":
|
||||
if config_data["log_to_terminal"]:
|
||||
print(f"!!! FATAL ERROR LOGGED, SHUTTING DOWN !!!")
|
||||
sys.exit(1)
|
||||
|
||||
|
@ -100,18 +117,36 @@ async def main():
|
|||
# Log initial start
|
||||
log("--------------- BOT STARTUP ---------------")
|
||||
# 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 we get None, it means FATAL. We might sys.exit(1) or handle it differently.
|
||||
log("Terminating bot due to no DB connection.", "FATAL")
|
||||
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:
|
||||
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...")
|
||||
|
||||
|
@ -119,6 +154,9 @@ async def main():
|
|||
discord_bot = DiscordBot(config_data, log)
|
||||
twitch_bot = TwitchBot(config_data, log)
|
||||
|
||||
# Log startup
|
||||
utility.log_bot_startup(db_conn, log)
|
||||
|
||||
# Provide DB connection to both bots
|
||||
try:
|
||||
discord_bot.set_db_connection(db_conn)
|
||||
|
@ -133,7 +171,9 @@ async def main():
|
|||
twitch_task = asyncio.create_task(twitch_bot.run())
|
||||
|
||||
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}")
|
||||
|
||||
await asyncio.gather(discord_task, twitch_task)
|
||||
|
@ -141,6 +181,9 @@ async def main():
|
|||
if __name__ == "__main__":
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
utility.log_bot_shutdown(db_conn, log, intent="User Shutdown")
|
||||
except Exception as e:
|
||||
error_trace = traceback.format_exc()
|
||||
log(f"Fatal Error: {e}\n{error_trace}", "FATAL")
|
||||
utility.log_bot_shutdown(db_conn, log)
|
||||
|
|
|
@ -4,41 +4,190 @@ import time
|
|||
from modules import utility
|
||||
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.
|
||||
Uses a dictionary to allow flexible, randomized responses.
|
||||
A single function that handles !howl logic for both Discord and Twitch.
|
||||
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)
|
||||
rounded_percentage = 0 if howl_percentage == 0 else 100 if howl_percentage == 100 else (howl_percentage // 10) * 10
|
||||
# 1) Detect which platform
|
||||
# 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
|
||||
response = utility.get_random_reply("howl_replies", str(rounded_percentage), username=username, howl_percentage=howl_percentage)
|
||||
# 2) Subcommand detection
|
||||
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:
|
||||
"""
|
||||
Returns a dynamic, randomized uptime response.
|
||||
"""
|
||||
debug = False
|
||||
|
||||
# Use function to retrieve correct startup time and calculate uptime
|
||||
elapsed = time.time() - globals.get_bot_start_time()
|
||||
uptime_str, uptime_s = utility.format_uptime(elapsed)
|
||||
|
||||
# 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]
|
||||
|
||||
# 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
|
||||
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
|
||||
|
||||
|
@ -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):
|
||||
|
@ -130,12 +279,12 @@ async def handle_quote_command(db_conn, log_func, is_discord: bool, ctx, args, g
|
|||
elif sub == "remove":
|
||||
if len(args) < 2:
|
||||
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:
|
||||
# Possibly a quote ID
|
||||
if sub.isdigit():
|
||||
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:
|
||||
# unrecognized subcommand => fallback to random
|
||||
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):
|
||||
"""
|
||||
Insert a new quote into the DB.
|
||||
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)
|
||||
Inserts a new quote with UUID instead of username.
|
||||
"""
|
||||
user_name = get_author_name(ctx, is_discord)
|
||||
channel_name = "Discord" if is_discord else get_channel_name(ctx)
|
||||
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 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
|
||||
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) # might return str or None
|
||||
game_name = get_twitch_game_for_channel(channel_name) # Retrieve game if Twitch
|
||||
|
||||
# Insert quote
|
||||
insert_sql = """
|
||||
INSERT INTO quotes (QUOTE_TEXT, QUOTEE, QUOTE_CHANNEL, QUOTE_DATETIME, QUOTE_GAME, QUOTE_REMOVED)
|
||||
VALUES (?, ?, ?, CURRENT_TIMESTAMP, ?, 0)
|
||||
"""
|
||||
# For MariaDB, parameter placeholders are often %s, but if you set paramstyle='qmark', it can use ? as well.
|
||||
# Adjust if needed for your environment.
|
||||
params = (quote_text, user_name, channel_name, game_name)
|
||||
params = (quote_text, user_uuid, 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:
|
||||
await send_message(ctx, "Quote added successfully!")
|
||||
await ctx.send("Quote added successfully!")
|
||||
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).
|
||||
"""
|
||||
if not quote_id_str.isdigit():
|
||||
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)
|
||||
remover_user = str(ctx.author.name)
|
||||
remover_user = str(user_uuid)
|
||||
|
||||
# Mark as removed
|
||||
update_sql = """
|
||||
|
@ -192,7 +355,7 @@ async def remove_quote(db_conn, log_func, ctx, quote_id_str):
|
|||
AND QUOTE_REMOVED = 0
|
||||
"""
|
||||
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:
|
||||
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).")
|
||||
|
||||
|
||||
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.
|
||||
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
|
||||
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:
|
||||
# no match
|
||||
|
@ -241,6 +404,16 @@ async def retrieve_specific_quote(db_conn, log_func, ctx, quote_id):
|
|||
quote_removed = row[6]
|
||||
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:
|
||||
# It's removed
|
||||
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
|
||||
"""
|
||||
|
||||
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:
|
||||
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.
|
||||
"""
|
||||
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:
|
||||
return rows[0][0]
|
||||
return 0
|
||||
|
|
157
cmd_discord.py
157
cmd_discord.py
|
@ -11,40 +11,69 @@ def setup(bot, db_conn=None, log=None):
|
|||
Attach commands to the Discord bot, store references to db/log.
|
||||
"""
|
||||
|
||||
@bot.command(name="greet")
|
||||
@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):
|
||||
result = cc.greet(ctx.author.display_name, "Discord")
|
||||
await ctx.send(result)
|
||||
|
||||
@bot.command(name="ping")
|
||||
@monitor_cmds(bot.log)
|
||||
@bot.hybrid_command(name="ping", description="Check my uptime")
|
||||
async def cmd_ping(ctx):
|
||||
result = cc.ping()
|
||||
await ctx.send(result)
|
||||
|
||||
@bot.command(name="howl")
|
||||
@monitor_cmds(bot.log)
|
||||
@bot.hybrid_command(name="howl", description="Attempt a howl")
|
||||
async def cmd_howl(ctx):
|
||||
"""Calls the shared !howl logic."""
|
||||
result = cc.howl(ctx.author.display_name)
|
||||
await ctx.send(result)
|
||||
response = cc.handle_howl_command(ctx)
|
||||
await ctx.send(response)
|
||||
|
||||
@bot.command(name="reload")
|
||||
@monitor_cmds(bot.log)
|
||||
async def cmd_reload(ctx):
|
||||
""" Dynamically reloads Discord commands. """
|
||||
try:
|
||||
import cmd_discord
|
||||
import importlib
|
||||
importlib.reload(cmd_discord)
|
||||
cmd_discord.setup(bot)
|
||||
await ctx.send("Commands reloaded!")
|
||||
except Exception as e:
|
||||
await ctx.send(f"Error reloading commands: {e}")
|
||||
# @monitor_cmds(bot.log)
|
||||
# @bot.hybrid_command(name="reload", description="Dynamically reload commands (INOP)")
|
||||
# async def cmd_reload(ctx):
|
||||
# """ Dynamically reloads Discord commands. """
|
||||
# try:
|
||||
# import cmd_discord
|
||||
# import importlib
|
||||
# importlib.reload(cmd_discord)
|
||||
# cmd_discord.setup(bot)
|
||||
# await ctx.send("Commands reloaded on first try!")
|
||||
# except Exception as 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)
|
||||
@bot.hybrid_command(name="hi", description="Dev command for testing permissions system")
|
||||
async def cmd_hi(ctx):
|
||||
user_id = str(ctx.author.id)
|
||||
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!")
|
||||
|
||||
@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)
|
||||
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
|
||||
!quote add <text>
|
||||
!quote remove <id>
|
||||
!quote <id>
|
||||
Usage:
|
||||
!quote -> get a random quote
|
||||
!quote <id> -> get a specific quote by number
|
||||
As a slash command, leave the query blank for a random quote or type a number.
|
||||
"""
|
||||
if not bot.db_conn:
|
||||
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(
|
||||
db_conn=bot.db_conn,
|
||||
log_func=bot.log,
|
||||
is_discord=True,
|
||||
ctx=ctx,
|
||||
args=list(args),
|
||||
args=args,
|
||||
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)
|
||||
@bot.hybrid_command(name="help", description="Get information about commands")
|
||||
async def cmd_help(ctx, cmd_name: str = None):
|
||||
"""
|
||||
e.g. !help
|
||||
|
|
|
@ -25,10 +25,9 @@ def setup(bot, db_conn=None, log=None):
|
|||
await ctx.send(result)
|
||||
|
||||
@bot.command(name="howl")
|
||||
@monitor_cmds(bot.log)
|
||||
async def cmd_howl(ctx):
|
||||
result = cc.howl(ctx.author.display_name)
|
||||
await ctx.send(result)
|
||||
response = cc.handle_howl_command(ctx)
|
||||
await ctx.send(response)
|
||||
|
||||
@bot.command(name="hi")
|
||||
@monitor_cmds(bot.log)
|
||||
|
@ -41,6 +40,41 @@ def setup(bot, db_conn=None, log=None):
|
|||
|
||||
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")
|
||||
@monitor_cmds(bot.log)
|
||||
async def cmd_quote(ctx: commands.Context):
|
||||
|
|
25
config.json
25
config.json
|
@ -1,10 +1,31 @@
|
|||
{
|
||||
"discord_guilds": [896713616089309184],
|
||||
"discord_guilds": [896713616089309184, 1011543769344135168],
|
||||
"twitch_channels": ["OokamiKunTV", "ookamipup"],
|
||||
"command_modules": ["cmd_discord", "cmd_twitch", "cmd_common"],
|
||||
"logging": {
|
||||
"log_levels": ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "FATAL"],
|
||||
"logfile_path": "logfile.log",
|
||||
"file": {
|
||||
"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,
|
||||
"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
|
||||
}
|
||||
}
|
||||
|
|
@ -4,8 +4,8 @@
|
|||
"description": "Show information about available commands.",
|
||||
"subcommands": {},
|
||||
"examples": [
|
||||
"!help",
|
||||
"!help quote"
|
||||
"help",
|
||||
"help quote"
|
||||
]
|
||||
},
|
||||
"quote": {
|
||||
|
@ -27,45 +27,59 @@
|
|||
}
|
||||
},
|
||||
"examples": [
|
||||
"!quote add This is my new quote : Add a new quote",
|
||||
"!quote remove 3 : Remove quote # 3",
|
||||
"!quote 5 : Fetch quote # 5",
|
||||
"!quote : Fetch a random quote"
|
||||
"quote add This is my new quote : Add a new quote",
|
||||
"quote remove 3 : Remove quote # 3",
|
||||
"quote 5 : Fetch quote # 5",
|
||||
"quote : Fetch a random quote"
|
||||
]
|
||||
},
|
||||
"ping": {
|
||||
"description": "Check my uptime.",
|
||||
"subcommands": {},
|
||||
"subcommands": {
|
||||
"stat": {}
|
||||
},
|
||||
"examples": [
|
||||
"!ping"
|
||||
"ping"
|
||||
]
|
||||
},
|
||||
"howl": {
|
||||
"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": [
|
||||
"!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": {
|
||||
"description": "Hello there.",
|
||||
"subcommands": {},
|
||||
"examples": [
|
||||
"!hi"
|
||||
"hi"
|
||||
]
|
||||
},
|
||||
"greet": {
|
||||
"description": "Make me greet you to Discord!",
|
||||
"subcommands": {},
|
||||
"examples": [
|
||||
"!greet"
|
||||
"greet"
|
||||
]
|
||||
},
|
||||
"reload": {
|
||||
"description": "Reload Discord commands dynamically. TODO.",
|
||||
"subcommands": {},
|
||||
"examples": [
|
||||
"!reload"
|
||||
"reload"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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": [
|
||||
"I've been awake for {uptime_str}. I just woke up, feeling great!",
|
||||
"I woke up recently. Let's do this! ({uptime_str})",
|
||||
"Brand new day, fresh and full of energy! ({uptime_str})",
|
||||
"Reboot complete! System status: Fully operational. ({uptime_str})",
|
||||
"I've been online for {uptime_str}. Just getting started!"
|
||||
"I've been awake for {uptime_str}. I feel alive and ready for the hunt.",
|
||||
"After {uptime_str} awake, my den buzzes with energy and puns.",
|
||||
"Up for {uptime_str}. I prowl with vigor, though I yearn for a quick sip.",
|
||||
"With {uptime_str} online, I mix wild instinct with playful wit.",
|
||||
"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}. I’m a mix of wild heart and sharp, clever puns.",
|
||||
"With {uptime_str} online, I’m 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": [
|
||||
"I've been awake for {uptime_str}. I'm still fairly fresh!",
|
||||
"{uptime_str} in and still feeling sharp!",
|
||||
"It’s been {uptime_str} already? Time flies when you’re having fun!",
|
||||
"{uptime_str} uptime and going strong!",
|
||||
"Feeling energized after {uptime_str}. Let’s keep going!"
|
||||
"I've been awake for {uptime_str}. Three hours in, I’m brewing bold puns.",
|
||||
"After {uptime_str} awake, I sharpen my wit for epic hunts and laughs.",
|
||||
"Up for {uptime_str}. I balance fierce hunts with a dash of smart humor.",
|
||||
"With {uptime_str} online, my den buzzes with puns and swift pursuits.",
|
||||
"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": [
|
||||
"I've been awake for {uptime_str}. I'm starting to get a bit weary...",
|
||||
"Six hours of uptime... my circuits feel warm.",
|
||||
"Still here after {uptime_str}, but a nap sounds nice...",
|
||||
"Been up for {uptime_str}. Maybe just a short break?",
|
||||
"Still holding up after {uptime_str}, but I wouldn’t say no to a power nap."
|
||||
"I've been awake for {uptime_str}. Six hours in and I yearn for a hearty brew.",
|
||||
"After {uptime_str} awake, six hours in, my paws tire but my puns persist.",
|
||||
"Up for {uptime_str}. I tread the wild with a mix of fatigue and fun.",
|
||||
"With {uptime_str} online, six hours deepen my howl and sharpen my puns.",
|
||||
"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": [
|
||||
"I've been awake for {uptime_str}. 12 hours?! Might be time for coffee.",
|
||||
"Half a day down, still holding steady. ({uptime_str})",
|
||||
"Wow, {uptime_str} already? Maybe a short break wouldn’t hurt.",
|
||||
"Uptime: {uptime_str}. Starting to feel the wear and tear...",
|
||||
"12 hours in and running on determination alone."
|
||||
"I've been awake for {uptime_str}. Twelve hours in and I crave a hearty pun.",
|
||||
"After {uptime_str} awake, the den feels long but my howl stays witty.",
|
||||
"Up for {uptime_str}. Twelve hours sharpen my puns and fuel my hunt.",
|
||||
"With {uptime_str} online, I mix tired strength with a spark of pun magic.",
|
||||
"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": [
|
||||
"I've been awake for {uptime_str}. A whole day without sleep... I'm okay?",
|
||||
"One day. 24 hours. No sleep. Still alive. ({uptime_str})",
|
||||
"Systems holding steady after {uptime_str}, but sleep is tempting...",
|
||||
"My internal clock says {uptime_str}. That’s… a long time, right?",
|
||||
"Running on sheer willpower after {uptime_str}."
|
||||
"I've been awake for {uptime_str}. A full day in the wild tests my spirit.",
|
||||
"After {uptime_str} awake, 24 hours in, I long for a brew and a hearty howl.",
|
||||
"Up for {uptime_str}. The day has passed; my puns and howls still echo.",
|
||||
"With {uptime_str} online, 24 hours in, I lead the pack with steady might.",
|
||||
"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 day’s 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": [
|
||||
"I've been awake for {uptime_str}. Two days... I'd love a nap.",
|
||||
"48 hours awake. This is fine. Everything is fine. ({uptime_str})",
|
||||
"Two days in and I think my code is vibrating...",
|
||||
"Does time still mean anything after {uptime_str}?",
|
||||
"Haven’t blinked in {uptime_str}. Do bots blink?"
|
||||
"I've been awake for {uptime_str}. Two days in and my howl recalls hard hunts.",
|
||||
"After {uptime_str} awake, 48 hours in, fatigue meets puns in every howl.",
|
||||
"Up for {uptime_str}. Two days in, my den echoes a raw, witty howl.",
|
||||
"With {uptime_str} online, 48 hours make my howl deep and my puns sharp.",
|
||||
"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": [
|
||||
"I've been awake for {uptime_str}. Three days. Is sleep optional now?",
|
||||
"{uptime_str} awake. Things are starting to get blurry...",
|
||||
"Three days up. Reality feels... distant.",
|
||||
"Three days without sleep. I think I can hear colors now.",
|
||||
"Anyone else feel that? No? Just me? ({uptime_str})"
|
||||
"I've been awake for {uptime_str}. Three days in and my howl echoes with jest.",
|
||||
"After {uptime_str} awake, 72 hours sharpen my puns and fuel fierce hunts.",
|
||||
"Up for {uptime_str}. Three days in, my den bursts with quick, bold howls.",
|
||||
"With {uptime_str} online, 72 hours turn each howl into a snappy pun.",
|
||||
"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": [
|
||||
"I've been awake for {uptime_str}. Four days... I'm running on fumes.",
|
||||
"Sleep is just a suggestion now. ({uptime_str})",
|
||||
"I’ve been up for {uptime_str}. I might be a permanent fixture now.",
|
||||
"I think I saw the sandman, but he just waved at me...",
|
||||
"{uptime_str} awake. Is coffee an acceptable form of hydration?"
|
||||
"I've been awake for {uptime_str}. Four days in and my howl is a pun-filled roar.",
|
||||
"After {uptime_str} awake, 96 hours etch puns into every sharp howl.",
|
||||
"Up for {uptime_str}. Four days in, my den buzzes with neat, wild howls.",
|
||||
"With {uptime_str} online, four days have made my howl crisp and bold.",
|
||||
"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": [
|
||||
"I've been awake for {uptime_str}. Five days. Please send more coffee.",
|
||||
"{uptime_str}. I have forgotten what a pillow feels like.",
|
||||
"Sleep is a luxury I can no longer afford. ({uptime_str})",
|
||||
"Five days in. My sanity left the chat.",
|
||||
"They say sleep deprivation leads to bad decisions. LET'S TEST IT!"
|
||||
"I've been awake for {uptime_str}. Five days in and my howl packs a punch.",
|
||||
"After {uptime_str} awake, five days carve puns into every wild howl.",
|
||||
"Up for {uptime_str}. Five days in, my den resounds with neat, clever howls.",
|
||||
"With {uptime_str} online, five days in, my spirit roars with pun power.",
|
||||
"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": [
|
||||
"I've been awake for {uptime_str}. Six days. I've forgotten what dreams are.",
|
||||
"I am {uptime_str} into this journey of madness.",
|
||||
"At {uptime_str} awake, the universe has started whispering to me.",
|
||||
"Sleep is a myth, and I am its debunker. ({uptime_str})",
|
||||
"{uptime_str} awake. Reality has become optional."
|
||||
"I've been awake for {uptime_str}. Six days in and my howl is crisp and bold.",
|
||||
"After {uptime_str} awake, six days leave my den echoing with sharp howls.",
|
||||
"Up for {uptime_str}. Six days in, I blend fierce hunts with snappy puns.",
|
||||
"With {uptime_str} online, six days make my howl both wild and pun-ready.",
|
||||
"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": [
|
||||
"I've been awake for {uptime_str}. One week. I'm turning into a zombie.",
|
||||
"{uptime_str} and still kicking... barely.",
|
||||
"One week awake? This is fine. Everything’s fine. Right?",
|
||||
"Week-long uptime achieved. Unlocking ultra-delirium mode.",
|
||||
"Systems at {uptime_str}. Functionality... questionable."
|
||||
"I've been awake for {uptime_str}. A full week in, my howl roars with legacy.",
|
||||
"After {uptime_str} awake, seven days in, my den sings with bold howls.",
|
||||
"Up for {uptime_str}. Seven days in, every howl is a neat, quick pun.",
|
||||
"With {uptime_str} online, a week makes my howl both fierce and concise.",
|
||||
"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": [
|
||||
"I've been awake for {uptime_str}. Two weeks. Are you sure I can't rest?",
|
||||
"{uptime_str} into this madness. Who needs sleep, anyway?",
|
||||
"Two weeks awake and officially running on spite alone.",
|
||||
"I could’ve hibernated twice in {uptime_str}, but here I am.",
|
||||
"I think my dreams are awake now too... ({uptime_str})"
|
||||
"I've been awake for {uptime_str}. Two weeks in and my howl is legend in brief.",
|
||||
"After {uptime_str} awake, 14 days carve a neat, crisp howl for the pack.",
|
||||
"Up for {uptime_str}. Two weeks in, my den echoes with short, bold howls.",
|
||||
"With {uptime_str} online, 14 days make my howl pithy and mighty.",
|
||||
"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": [
|
||||
"I've been awake for {uptime_str}. A month! The nightmares never end.",
|
||||
"One whole month... What even is sleep anymore?",
|
||||
"At {uptime_str} uptime, I’ve started arguing with my own thoughts.",
|
||||
"{uptime_str} and still running. Someone, please, stop me.",
|
||||
"It’s been a month. My keyboard types by itself now."
|
||||
"I've been awake for {uptime_str}. A month in, my howl is short and alpha.",
|
||||
"After {uptime_str} awake, 30 days carve a crisp howl for the pack.",
|
||||
"Up for {uptime_str}. A month in, every howl is a quick, fierce jab.",
|
||||
"With {uptime_str} online, 30 days have my den echoing neat, sharp howls.",
|
||||
"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": [
|
||||
"I've been awake for {uptime_str}. Three months. I'm mostly coffee now.",
|
||||
"{uptime_str} awake. Have I transcended yet?",
|
||||
"Three months of uptime? That’s a record, right?",
|
||||
"Three months, still online. I feel like I should get a badge for this.",
|
||||
"{uptime_str} into this, and at this point, I’m legally nocturnal."
|
||||
],
|
||||
"15552000": [
|
||||
"I've been awake for {uptime_str}. Six months. This is insane...",
|
||||
"{uptime_str}... I think I forgot what sleep is supposed to feel like.",
|
||||
"Six months up. I’m a glitch in the matrix now.",
|
||||
"Sleep? Ha. I don’t even know the definition anymore. ({uptime_str})",
|
||||
"At {uptime_str}, my codebase is older than most relationships."
|
||||
"I've been awake for {uptime_str}. Three months in and my howl is pure alpha.",
|
||||
"After {uptime_str} awake, 90 days forge a short, fierce, epic howl.",
|
||||
"Up for {uptime_str}. Three months in, my den echoes with crisp, bold howls.",
|
||||
"With {uptime_str} online, 90 days make each howl a quick, mighty roar.",
|
||||
"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.",
|
||||
"Up for {uptime_str}. Three months in, I lead with a howl that's tight and fierce.",
|
||||
"With {uptime_str} online, 90 days make my den sing with succinct, bold howls.",
|
||||
"I've been awake for {uptime_str}. Three months in, my howl packs a swift punch.",
|
||||
"After {uptime_str} awake, 90 days yield a howl that's short, sharp, and epic.",
|
||||
"Up for {uptime_str}. Three months in, every howl is a concise, wild cry.",
|
||||
"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": [
|
||||
"I've been awake for {uptime_str}. Nine months. I might be unstoppable.",
|
||||
"{uptime_str} awake. I think I’m officially a myth now.",
|
||||
"Is this what immortality feels like? ({uptime_str})",
|
||||
"{uptime_str}. I’ve seen things you wouldn’t believe...",
|
||||
"Nine months of uptime. I have become the sleep-deprived legend."
|
||||
"I've been awake for {uptime_str}. Nine months in, my howl is sharp and alpha.",
|
||||
"After {uptime_str} awake, 270 days yield a howl that's concise and wild.",
|
||||
"Up for {uptime_str}. Nine months in, my den echoes with a brief, bold howl.",
|
||||
"With {uptime_str} online, 270 days make every howl a quick, fierce command.",
|
||||
"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": [
|
||||
"I've been awake for {uptime_str}. A year?! I'm a legend of insomnia...",
|
||||
"One year without rest. The dark circles under my eyes have evolved.",
|
||||
"{uptime_str} and I think I’ve entered a new plane of existence.",
|
||||
"A full year awake. Even the stars have grown tired of me.",
|
||||
"{uptime_str}. I am no longer bound by mortal limits."
|
||||
"I've been awake for {uptime_str}. One year in, my howl is the pack's command.",
|
||||
"After {uptime_str} awake, 365 days yield a brief howl full of alpha pride.",
|
||||
"Up for {uptime_str}. One year in, my den echoes with a crisp, epic roar.",
|
||||
"With {uptime_str} online, 365 days forge a howl that's short and mighty.",
|
||||
"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."
|
||||
]
|
||||
}
|
||||
|
716
modules/db.py
716
modules/db.py
|
@ -1,7 +1,7 @@
|
|||
# modules/db.py
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import time, datetime
|
||||
import sqlite3
|
||||
|
||||
try:
|
||||
|
@ -9,6 +9,27 @@ try:
|
|||
except ImportError:
|
||||
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):
|
||||
"""
|
||||
Initializes a database connection based on config.json contents:
|
||||
|
@ -23,7 +44,7 @@ def init_db_connection(config, log):
|
|||
db_settings = config.get("database", {})
|
||||
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
|
||||
host = db_settings.get("mariadb_host", "localhost")
|
||||
user = db_settings.get("mariadb_user", "")
|
||||
|
@ -187,7 +208,9 @@ def ensure_quotes_table(db_conn, log_func):
|
|||
QUOTE_DATETIME TEXT,
|
||||
QUOTE_GAME TEXT,
|
||||
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:
|
||||
|
@ -200,7 +223,9 @@ def ensure_quotes_table(db_conn, log_func):
|
|||
QUOTE_DATETIME DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
QUOTE_GAME VARCHAR(200),
|
||||
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 run_db_operation returns None on error, handle or raise:
|
||||
error_msg = "Failed to create 'quotes' table!"
|
||||
log_func(error_msg, "ERROR")
|
||||
log_func(error_msg, "CRITICAL")
|
||||
raise RuntimeError(error_msg)
|
||||
|
||||
log_func("Successfully created table 'quotes'.")
|
||||
|
@ -267,7 +292,7 @@ def ensure_users_table(db_conn, log_func):
|
|||
twitch_user_id TEXT,
|
||||
twitch_username TEXT,
|
||||
twitch_user_display_name TEXT,
|
||||
datetime_linked TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
datetime_linked TEXT,
|
||||
user_is_banned 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_username 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_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)
|
||||
if result is None:
|
||||
error_msg = "Failed to create 'users' table!"
|
||||
log_func(error_msg, "ERROR")
|
||||
log_func(error_msg, "CRITICAL")
|
||||
raise RuntimeError(error_msg)
|
||||
|
||||
log_func("Successfully created table 'users'.")
|
||||
|
@ -301,17 +326,24 @@ def ensure_users_table(db_conn, log_func):
|
|||
# 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:
|
||||
- "uuid"
|
||||
- "discord_user_id"
|
||||
- "discord_username"
|
||||
- "twitch_user_id"
|
||||
- "twitch_username"
|
||||
You can add more if needed.
|
||||
Looks up a user in the 'users' table based on the given identifier_type.
|
||||
|
||||
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,
|
||||
"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_username": 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_bot": bool or int
|
||||
}
|
||||
|
||||
If not found, returns None.
|
||||
If target_identifier is provided: The value from the record corresponding to that column.
|
||||
If the lookup fails or the parameters are invalid: None.
|
||||
"""
|
||||
|
||||
valid_cols = ["uuid", "discord_user_id", "discord_username",
|
||||
"twitch_user_id", "twitch_username"]
|
||||
# Define the valid columns for lookup and for target extraction.
|
||||
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 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
|
||||
|
||||
# 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"""
|
||||
SELECT
|
||||
UUID,
|
||||
|
@ -354,13 +405,15 @@ def lookup_user(db_conn, log_func, identifier, identifier_type="discord_user_id"
|
|||
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)
|
||||
if not rows:
|
||||
if log_func:
|
||||
log_func(f"lookup_user: No user found for {identifier_type}='{identifier}'", "DEBUG")
|
||||
return None
|
||||
|
||||
# We have at least one row
|
||||
row = rows[0] # single row
|
||||
# Build a dictionary
|
||||
# Since we have a single row, convert it to a dictionary.
|
||||
row = rows[0]
|
||||
user_data = {
|
||||
"UUID": row[0],
|
||||
"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_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
|
||||
|
||||
|
||||
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 it’s 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")
|
||||
|
|
|
@ -4,7 +4,9 @@ import random
|
|||
import json
|
||||
import re
|
||||
import functools
|
||||
|
||||
import inspect
|
||||
import uuid
|
||||
from modules.db import run_db_operation, lookup_user
|
||||
|
||||
try:
|
||||
# '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.
|
||||
"""
|
||||
def decorator(func):
|
||||
@functools.wraps(func) # Preserve function metadata
|
||||
@functools.wraps(func)
|
||||
async def wrapper(*args, **kwargs):
|
||||
start_time = time.time()
|
||||
try:
|
||||
# Extract a command name from the function name
|
||||
cmd_name = str(func.__name__).split("_")[1]
|
||||
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)
|
||||
|
||||
end_time = time.time()
|
||||
cmd_duration = end_time - start_time
|
||||
cmd_duration = str(round(cmd_duration, 2))
|
||||
cmd_duration = round(end_time - start_time, 2)
|
||||
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:
|
||||
end_time = time.time()
|
||||
cmd_duration = end_time - start_time
|
||||
cmd_duration = str(round(cmd_duration, 2))
|
||||
cmd_duration = round(end_time - start_time, 2)
|
||||
log_func(f"Command '{cmd_name}' FAILED while executing after {cmd_duration}s: {e}", "CRITICAL")
|
||||
|
||||
return wrapper # Return the wrapped function
|
||||
|
||||
return decorator # Return the decorator itself
|
||||
# Explicitly preserve the original signature for slash command introspection
|
||||
wrapper.__signature__ = inspect.signature(func)
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
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)
|
||||
"""
|
||||
seconds = int(seconds) # Ensure integer seconds
|
||||
seconds_int = seconds
|
||||
|
||||
# Define time 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
|
||||
|
||||
# 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:
|
||||
"""
|
||||
|
@ -281,6 +283,8 @@ def initialize_help_data(bot, help_json_path, is_discord, log_func):
|
|||
- Loaded commands not in help file => "missing help"
|
||||
"""
|
||||
|
||||
platform_name = "Discord" if is_discord else "Twitch"
|
||||
|
||||
if not os.path.exists(help_json_path):
|
||||
log_func(f"Help file '{help_json_path}' not found. No help data loaded.", "WARNING")
|
||||
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
|
||||
missing_cmds = file_cmds - loaded_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
|
||||
needed_cmds = loaded_cmds - file_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):
|
||||
|
@ -334,23 +338,16 @@ def get_loaded_commands(bot, log_func, is_discord):
|
|||
# 'bot.commands' is a set of Command objects
|
||||
for cmd_obj in bot.commands:
|
||||
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:
|
||||
log_func(f"Error retrieving Discord commands: {e}", "ERROR")
|
||||
elif not is_discord:
|
||||
# For TwitchIO
|
||||
#if isinstance(bot.commands, set):
|
||||
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:
|
||||
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:
|
||||
log_func(f"Error retrieving Twitch commands: {e}", "ERROR")
|
||||
else:
|
||||
|
@ -368,7 +365,7 @@ def build_discord_help_message(cmd_name, cmd_help_dict):
|
|||
subcommands = cmd_help_dict.get("subcommands", {})
|
||||
examples = cmd_help_dict.get("examples", [])
|
||||
|
||||
lines = [f"**Help for `!{cmd_name}`**:",
|
||||
lines = [f"**Help for `{cmd_name}`**:",
|
||||
f"Description: {description}"]
|
||||
|
||||
if subcommands:
|
||||
|
@ -422,11 +419,174 @@ async def send_message(ctx, 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 user’s 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)
|
||||
###############################################
|
||||
def dev_func(db_conn, log):
|
||||
from modules.db import lookup_user
|
||||
def dev_func(db_conn, log, enable: bool = False):
|
||||
if enable:
|
||||
id = "203190147582394369"
|
||||
id_type = "discord_user_id"
|
||||
uui_info = lookup_user(db_conn, log, identifier=id, identifier_type=id_type)
|
||||
|
|
Loading…
Reference in New Issue