Massive UUI system and database overhaul
- Dynamic accounts association - Accounts can now be associated dynamically, allowing multiple accounts on the same platform to be associated to the same UUID. - UUI system now supports any platform, eg. YouTube, TikTok, Kick, Twitter, etc. - More robust user lookups with enhanced fault tolerance and allowance for NULL data. - Optimized database structure with two tables for user association; one for UUID and basic info, another for platform-specific details. - Enhanced logging functionality: logs now prefix the calling function. - Enhanced user lookup debug messages, allowing easy query inspection and data validation. - Other minor fixeskami_dev
parent
766c3ab690
commit
d0313a6a92
|
@ -93,10 +93,11 @@ class DiscordBot(commands.Bot):
|
|||
else:
|
||||
guild_name = "DM"
|
||||
channel_name = "Direct Message"
|
||||
|
||||
globals.log(f"Message detected by '{message.author.name}' in '{guild_name}' - #'{channel_name}'", "DEBUG")
|
||||
#globals.log(f"Message body:\n{message}\nMessage content: {message.content}", "DEBUG") # Full message debug
|
||||
globals.log(f"Message content: '{message.content}'", "DEBUG") # Partial message debug (content only)
|
||||
globals.log(f"Message content: '{message.content}'", "DEBUG")
|
||||
globals.log(f"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
|
||||
|
@ -104,6 +105,7 @@ class DiscordBot(commands.Bot):
|
|||
user_name = message.author.name # no discriminator
|
||||
display_name = message.author.display_name
|
||||
|
||||
# Track user activity first
|
||||
modules.utility.track_user_activity(
|
||||
db_conn=self.db_conn,
|
||||
platform="discord",
|
||||
|
@ -113,33 +115,21 @@ class DiscordBot(commands.Bot):
|
|||
user_is_bot=is_bot
|
||||
)
|
||||
|
||||
user_data = lookup_user(db_conn=self.db_conn, identifier=user_id, identifier_type="discord_user_id")
|
||||
user_uuid = user_data["UUID"] if user_data else "UNKNOWN"
|
||||
globals.log(f"... UUI lookup complete", "DEBUG")
|
||||
# Let log_message() handle UUID lookup internally
|
||||
platform_str = f"discord-{guild_name}"
|
||||
channel_str = channel_name
|
||||
|
||||
if user_uuid:
|
||||
# The "platform" can be e.g. "discord" or you can store the server name
|
||||
platform_str = f"discord-{guild_name}"
|
||||
# The channel name can be message.channel.name or "DM" if it's a private channel
|
||||
channel_str = channel_name
|
||||
attachments = ", ".join(a.url for a in message.attachments) if message.attachments else ""
|
||||
|
||||
# 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,
|
||||
user_uuid=user_uuid,
|
||||
message_content=message.content or "",
|
||||
platform=platform_str,
|
||||
channel=channel_str,
|
||||
attachments=attachments,
|
||||
username=message.author.name
|
||||
)
|
||||
|
||||
# PLACEHOLDER FOR FUTURE MESSAGE PROCESSING
|
||||
log_message(
|
||||
db_conn=self.db_conn,
|
||||
identifier=user_id,
|
||||
identifier_type="discord_user_id",
|
||||
message_content=message.content or "",
|
||||
platform=platform_str,
|
||||
channel=channel_str,
|
||||
attachments=attachments
|
||||
)
|
||||
except Exception as e:
|
||||
globals.log(f"... UUI lookup failed: {e}", "WARNING")
|
||||
pass
|
||||
|
@ -151,6 +141,7 @@ class DiscordBot(commands.Bot):
|
|||
except Exception as e:
|
||||
globals.log(f"Command processing failed: {e}", "ERROR")
|
||||
|
||||
|
||||
def load_bot_settings(self):
|
||||
"""Loads bot activity settings from JSON file."""
|
||||
try:
|
||||
|
|
|
@ -145,7 +145,10 @@ class TwitchBot(commands.Bot):
|
|||
globals.log("Twitch token refreshed successfully. Restarting bot...")
|
||||
|
||||
# Restart the TwitchIO connection
|
||||
await self.close() # Close the old connection
|
||||
try:
|
||||
await self.close() # Close the old connection
|
||||
except Exception as e:
|
||||
globals.log(f"refresh_access_token() failed during close attempt: {e}", "WARNING")
|
||||
await self.start() # Restart with the new token
|
||||
|
||||
return # Exit function after successful refresh
|
||||
|
|
10
bots.py
10
bots.py
|
@ -51,6 +51,7 @@ async def main():
|
|||
"Bot events table": partial(db.ensure_bot_events_table, db_conn),
|
||||
"Quotes table": partial(db.ensure_quotes_table, db_conn),
|
||||
"Users table": partial(db.ensure_users_table, db_conn),
|
||||
"Platform_Mapping table": partial(db.ensure_platform_mapping_table, db_conn),
|
||||
"Chatlog table": partial(db.ensure_chatlog_table, db_conn),
|
||||
"Howls table": partial(db.ensure_userhowls_table, db_conn),
|
||||
"Discord activity table": partial(db.ensure_discord_activity_table, db_conn),
|
||||
|
@ -69,7 +70,7 @@ async def main():
|
|||
|
||||
# Create both bots
|
||||
discord_bot = DiscordBot()
|
||||
twitch_bot = TwitchBot()
|
||||
#twitch_bot = TwitchBot()
|
||||
|
||||
# Log startup
|
||||
utility.log_bot_startup(db_conn)
|
||||
|
@ -77,7 +78,7 @@ async def main():
|
|||
# Provide DB connection to both bots
|
||||
try:
|
||||
discord_bot.set_db_connection(db_conn)
|
||||
twitch_bot.set_db_connection(db_conn)
|
||||
#twitch_bot.set_db_connection(db_conn)
|
||||
globals.log(f"Initialized database connection to both bots")
|
||||
except Exception as e:
|
||||
globals.log(f"Unable to initialize database connection to one or both bots: {e}", "FATAL")
|
||||
|
@ -85,7 +86,7 @@ async def main():
|
|||
globals.log("Starting Discord and Twitch bots...")
|
||||
|
||||
discord_task = asyncio.create_task(discord_bot.run(os.getenv("DISCORD_BOT_TOKEN")))
|
||||
twitch_task = asyncio.create_task(twitch_bot.run())
|
||||
#twitch_task = asyncio.create_task(twitch_bot.run())
|
||||
|
||||
from modules.utility import dev_func
|
||||
enable_dev_func = False
|
||||
|
@ -93,7 +94,8 @@ async def main():
|
|||
dev_func_result = dev_func(db_conn, enable_dev_func)
|
||||
globals.log(f"dev_func output: {dev_func_result}")
|
||||
|
||||
await asyncio.gather(discord_task, twitch_task)
|
||||
#await asyncio.gather(discord_task, twitch_task)
|
||||
await asyncio.gather(discord_task)
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
|
|
|
@ -87,14 +87,11 @@ def handle_howl_normal(ctx, platform, author_id, author_display_name) -> str:
|
|||
utility.wfstl()
|
||||
db_conn = ctx.bot.db_conn
|
||||
|
||||
# random logic
|
||||
# Random logic for howl percentage
|
||||
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
|
||||
rounded_val = 0 if howl_val == 0 else 100 if howl_val == 100 else (howl_val // 10) * 10
|
||||
|
||||
# dictionary-based reply
|
||||
# Dictionary-based reply
|
||||
reply = utility.get_random_reply(
|
||||
"howl_replies",
|
||||
str(rounded_val),
|
||||
|
@ -102,17 +99,22 @@ def handle_howl_normal(ctx, platform, author_id, author_display_name) -> str:
|
|||
howl_percentage=howl_val
|
||||
)
|
||||
|
||||
# find user in DB by ID
|
||||
user_data = db.lookup_user(db_conn, identifier=author_id, identifier_type=platform)
|
||||
# Consistent UUID lookup
|
||||
user_data = db.lookup_user(db_conn, identifier=author_id, identifier_type=f"{platform}_user_id")
|
||||
if user_data:
|
||||
db.insert_howl(db_conn, user_data["UUID"], howl_val)
|
||||
user_uuid = user_data["UUID"]
|
||||
db.insert_howl(db_conn, user_uuid, howl_val)
|
||||
else:
|
||||
globals.log(f"Could not find user by ID={author_id} on {platform}. Not storing howl.", "WARNING")
|
||||
|
||||
utility.wfetl()
|
||||
return reply
|
||||
|
||||
|
||||
def handle_howl_stats(ctx, platform, target_name) -> str:
|
||||
"""
|
||||
Handles !howl stats subcommand for both community and individual users.
|
||||
"""
|
||||
utility.wfstl()
|
||||
db_conn = ctx.bot.db_conn
|
||||
|
||||
|
@ -137,12 +139,13 @@ def handle_howl_stats(ctx, platform, target_name) -> str:
|
|||
f"0% Howls: {count_zero}, 100% Howls: {count_hundred}")
|
||||
|
||||
# Otherwise, lookup a single user
|
||||
user_data = lookup_user_by_name(db_conn, platform, target_name)
|
||||
user_data = db.lookup_user(db_conn, identifier=target_name, identifier_type=f"{platform}_username")
|
||||
if not user_data:
|
||||
utility.wfetl()
|
||||
return f"I don't know that user: {target_name}"
|
||||
|
||||
stats = db.get_howl_stats(db_conn, user_data["UUID"])
|
||||
user_uuid = user_data["UUID"]
|
||||
stats = db.get_howl_stats(db_conn, user_uuid)
|
||||
if not stats:
|
||||
utility.wfetl()
|
||||
return f"{target_name} hasn't howled yet! (Try `!howl` to get started.)"
|
||||
|
@ -156,12 +159,13 @@ def handle_howl_stats(ctx, platform, target_name) -> str:
|
|||
f"(0% x{z}, 100% x{h})")
|
||||
|
||||
|
||||
|
||||
def lookup_user_by_name(db_conn, platform, name_str):
|
||||
"""
|
||||
Attempt to find a user by name on that platform, e.g. 'discord_username' or 'twitch_username'.
|
||||
Consistent UUID resolution for usernames across platforms.
|
||||
"""
|
||||
utility.wfstl()
|
||||
# same logic as before
|
||||
|
||||
if platform == "discord":
|
||||
ud = db.lookup_user(db_conn, name_str, "discord_user_display_name")
|
||||
if ud:
|
||||
|
@ -170,6 +174,7 @@ def lookup_user_by_name(db_conn, platform, name_str):
|
|||
ud = db.lookup_user(db_conn, name_str, "discord_username")
|
||||
utility.wfetl()
|
||||
return ud
|
||||
|
||||
elif platform == "twitch":
|
||||
ud = db.lookup_user(db_conn, name_str, "twitch_user_display_name")
|
||||
if ud:
|
||||
|
@ -178,12 +183,14 @@ def lookup_user_by_name(db_conn, platform, name_str):
|
|||
ud = db.lookup_user(db_conn, name_str, "twitch_username")
|
||||
utility.wfetl()
|
||||
return ud
|
||||
|
||||
else:
|
||||
globals.log(f"Unknown platform {platform} in lookup_user_by_name", "WARNING")
|
||||
globals.log(f"Unknown platform '{platform}' in lookup_user_by_name", "WARNING")
|
||||
utility.wfetl()
|
||||
return None
|
||||
|
||||
|
||||
|
||||
def ping() -> str:
|
||||
"""
|
||||
Returns a dynamic, randomized uptime response.
|
||||
|
@ -401,7 +408,7 @@ async def add_new_quote(db_conn, is_discord, ctx, quote_text, game_name: str = N
|
|||
# Lookup UUID from users table
|
||||
user_data = db.lookup_user(db_conn, identifier=user_id, identifier_type=f"{platform}_user_id")
|
||||
if not user_data:
|
||||
globals.log(f"ERROR: Could not find UUID for user {ctx.author.name} ({user_id}) on {platform}. Quote not saved.", "ERROR")
|
||||
globals.log(f"Could not find UUID for user {ctx.author.name} ({user_id}) on {platform}. Quote not saved.", "ERROR")
|
||||
utility.wfetl()
|
||||
return "Could not save quote. Your user data is missing from the system."
|
||||
|
||||
|
@ -410,7 +417,7 @@ async def add_new_quote(db_conn, is_discord, ctx, quote_text, game_name: str = N
|
|||
if is_discord or not game_name:
|
||||
game_name = None
|
||||
|
||||
# Insert quote
|
||||
# Insert quote using UUID for QUOTEE
|
||||
insert_sql = """
|
||||
INSERT INTO quotes (QUOTE_TEXT, QUOTEE, QUOTE_CHANNEL, QUOTE_DATETIME, QUOTE_GAME, QUOTE_REMOVED)
|
||||
VALUES (?, ?, ?, CURRENT_TIMESTAMP, ?, 0)
|
||||
|
@ -428,6 +435,7 @@ async def add_new_quote(db_conn, is_discord, ctx, quote_text, game_name: str = N
|
|||
return "Failed to add quote."
|
||||
|
||||
|
||||
|
||||
async def remove_quote(db_conn, is_discord: bool, ctx, quote_id_str):
|
||||
"""
|
||||
Mark quote #ID as removed (QUOTE_REMOVED=1) and record removal datetime.
|
||||
|
@ -443,7 +451,7 @@ async def remove_quote(db_conn, is_discord: bool, ctx, quote_id_str):
|
|||
# Lookup UUID from users table
|
||||
user_data = db.lookup_user(db_conn, identifier=user_id, identifier_type=f"{platform}_user_id")
|
||||
if not user_data:
|
||||
globals.log(f"ERROR: Could not find UUID for user {ctx.author.name} ({user_id}) on {platform}. Quote not removed.", "ERROR")
|
||||
globals.log(f"Could not find UUID for user {ctx.author.name} ({user_id}) on {platform}. Quote not removed.", "ERROR")
|
||||
utility.wfetl()
|
||||
return "Could not remove quote. Your user data is missing from the system."
|
||||
|
||||
|
@ -550,7 +558,7 @@ async def retrieve_specific_quote(db_conn, ctx, quote_id, is_discord):
|
|||
# Lookup UUID from users table for the quoter
|
||||
user_data = db.lookup_user(db_conn, identifier=quotee, identifier_type="UUID")
|
||||
if not user_data:
|
||||
globals.log(f"ERROR: Could not find platform name for quotee UUID {quotee}. Default to 'Unknown'", "ERROR")
|
||||
globals.log(f"Could not find platform name for quotee UUID {quotee}. Default to 'Unknown'", "INFO")
|
||||
quotee_display = "Unknown"
|
||||
else:
|
||||
quotee_display = user_data[f"{platform}_user_display_name"]
|
||||
|
@ -559,7 +567,7 @@ async def retrieve_specific_quote(db_conn, ctx, quote_id, is_discord):
|
|||
# Lookup UUID for removed_by if removed
|
||||
removed_user_data = db.lookup_user(db_conn, identifier=quote_removed_by, identifier_type="UUID")
|
||||
if not removed_user_data:
|
||||
globals.log(f"ERROR: Could not find platform name for remover UUID {quote_removed_by}. Default to 'Unknown'", "ERROR")
|
||||
globals.log(f"Could not find platform name for remover UUID {quote_removed_by}. Default to 'Unknown'", "INFO")
|
||||
quote_removed_by_display = "Unknown"
|
||||
else:
|
||||
quote_removed_by_display = removed_user_data[f"{platform}_user_display_name"]
|
||||
|
@ -686,10 +694,11 @@ async def retrieve_quote_info(db_conn, ctx, quote_id, is_discord):
|
|||
# Lookup display name for the quoter
|
||||
user_data = db.lookup_user(db_conn, identifier=quotee, identifier_type="UUID")
|
||||
if not user_data:
|
||||
globals.log(f"ERROR: Could not find display name for quotee UUID {quotee}.", "ERROR")
|
||||
globals.log(f"Could not find display name for quotee UUID {quotee}.", "INFO")
|
||||
quotee_display = "Unknown"
|
||||
else:
|
||||
quotee_display = user_data.get(f"{platform}_user_display_name", "Unknown")
|
||||
# Use display name or fallback to platform username
|
||||
quotee_display = user_data.get(f"platform_display_name", user_data.get("platform_username", "Unknown"))
|
||||
|
||||
info_lines = []
|
||||
info_lines.append(f"Quote #{quote_number} was quoted by {quotee_display} on {quote_datetime}.")
|
||||
|
@ -703,10 +712,11 @@ async def retrieve_quote_info(db_conn, ctx, quote_id, is_discord):
|
|||
if quote_removed_by:
|
||||
removed_user_data = db.lookup_user(db_conn, identifier=quote_removed_by, identifier_type="UUID")
|
||||
if not removed_user_data:
|
||||
globals.log(f"ERROR: Could not find display name for remover UUID {quote_removed_by}.", "ERROR")
|
||||
globals.log(f"Could not find display name for remover UUID {quote_removed_by}.", "INFO")
|
||||
quote_removed_by_display = "Unknown"
|
||||
else:
|
||||
quote_removed_by_display = removed_user_data.get(f"{platform}_user_display_name", "Unknown")
|
||||
# Use display name or fallback to platform username
|
||||
quote_removed_by_display = removed_user_data.get(f"platform_display_name", removed_user_data.get("platform_username", "Unknown"))
|
||||
else:
|
||||
quote_removed_by_display = "Unknown"
|
||||
removed_info = f"Removed by {quote_removed_by_display}"
|
||||
|
@ -784,10 +794,11 @@ async def search_quote(db_conn, keywords, is_discord, ctx):
|
|||
# Lookup display name for quotee using UUID.
|
||||
user_data = db.lookup_user(db_conn, identifier=quotee, identifier_type="UUID")
|
||||
if user_data:
|
||||
quotee_display = user_data.get(f"{'discord' if is_discord else 'twitch'}_user_display_name", "")
|
||||
# Use display name or fallback to platform username
|
||||
quotee_display = user_data.get("platform_display_name", user_data.get("platform_username", "Unknown"))
|
||||
else:
|
||||
quotee_display = ""
|
||||
|
||||
globals.log(f"Could not find display name for quotee UUID {quotee}.", "INFO")
|
||||
quotee_display = "Unknown"
|
||||
# For each keyword, check each field.
|
||||
# Award 2 points for a whole word match and 1 point for a partial match.
|
||||
score_total = 0
|
||||
|
|
89
globals.py
89
globals.py
|
@ -3,6 +3,7 @@ import json
|
|||
import sys
|
||||
import traceback
|
||||
import discord
|
||||
import inspect
|
||||
|
||||
# Store the start time globally.
|
||||
_bot_start_time = time.time()
|
||||
|
@ -60,7 +61,8 @@ def log(message: str, level="INFO", exec_info=False, linebreaks=False):
|
|||
Log a message with the specified log level.
|
||||
|
||||
Capable of logging individual levels to the terminal and/or logfile separately.
|
||||
Can also append traceback information if needed, and is capable of preserving/removing linebreaks from log messages as needed.
|
||||
Can also append traceback information if needed, and is capable of preserving/removing
|
||||
linebreaks from log messages as needed. Also prepends the calling function name.
|
||||
|
||||
Args:
|
||||
message (str): The message to log.
|
||||
|
@ -83,58 +85,73 @@ def log(message: str, level="INFO", exec_info=False, linebreaks=False):
|
|||
log("An error occured during processing", "ERROR", exec_info=True, linebreaks=False)
|
||||
"""
|
||||
|
||||
# Initiate logfile
|
||||
lfp = config_data["logging"]["logfile_path"] # Log File Path
|
||||
clfp = f"cur_{lfp}" # Current Log File Path
|
||||
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 !!!")
|
||||
# Hard-coded options/settings (can be expanded as needed)
|
||||
default_level = "INFO" # Fallback log level
|
||||
allowed_levels = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "FATAL"}
|
||||
|
||||
# Ensure valid level
|
||||
if level not in allowed_levels:
|
||||
level = default_level
|
||||
|
||||
# Capture the calling function's name using inspect
|
||||
try:
|
||||
caller_frame = inspect.stack()[1]
|
||||
caller_name = caller_frame.function
|
||||
except Exception:
|
||||
caller_name = "Unknown"
|
||||
|
||||
# Optionally remove linebreaks if not desired
|
||||
if not linebreaks:
|
||||
message = message.replace("\n", " ")
|
||||
|
||||
# Get current timestamp and uptime
|
||||
elapsed = time.time() - get_bot_start_time() # Assuming this function is defined elsewhere
|
||||
from modules import utility
|
||||
log_levels = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "FATAL"}
|
||||
uptime_str, _ = utility.format_uptime(elapsed)
|
||||
timestamp = time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
if level not in log_levels:
|
||||
level = "INFO" # Default to INFO if an invalid level is provided
|
||||
# Prepend dynamic details including the caller name
|
||||
log_message = f"[{timestamp} - {uptime_str}] [{level}] [Func: {caller_name}] {message}"
|
||||
|
||||
# Append traceback if required or for error levels
|
||||
if exec_info or level in {"ERROR", "CRITICAL", "FATAL"}:
|
||||
log_message += f"\n{traceback.format_exc()}"
|
||||
|
||||
# Read logging settings from the configuration
|
||||
lfp = config_data["logging"]["logfile_path"] # Log File Path
|
||||
clfp = f"cur_{lfp}" # Current Log File Path
|
||||
|
||||
if not (config_data["logging"]["terminal"]["log_to_terminal"] or
|
||||
config_data["logging"]["file"]["log_to_file"]):
|
||||
print("!!! WARNING !!! CONSOLE AND LOGFILE OUTPUT DISABLED !!!\n"
|
||||
"!!! NO LOGS WILL BE PROVIDED !!!")
|
||||
|
||||
# Check if this log level is enabled (or if it's a FATAL message which always prints)
|
||||
if level in config_data["logging"]["log_levels"] or level == "FATAL":
|
||||
elapsed = time.time() - get_bot_start_time()
|
||||
uptime_str, _ = utility.format_uptime(elapsed)
|
||||
timestamp = time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
log_message = f"[{timestamp} - {uptime_str}] [{level}] {message}"
|
||||
|
||||
# Include traceback for certain error levels
|
||||
if exec_info or level in ["ERROR", "CRITICAL", "FATAL"]:
|
||||
log_message += f"\n{traceback.format_exc()}"
|
||||
|
||||
# Print to terminal if enabled
|
||||
# 'FATAL' errors override settings
|
||||
# Checks config file to see enabled/disabled logging levels
|
||||
# Terminal output
|
||||
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":
|
||||
if config_data["logging"]["terminal"].get(config_level_format, False) or level == "FATAL":
|
||||
print(log_message)
|
||||
|
||||
# Write to file if enabled
|
||||
# 'FATAL' errors override settings
|
||||
# Checks config file to see enabled/disabled logging levels
|
||||
# File output
|
||||
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":
|
||||
if config_data["logging"]["file"].get(config_level_format, False) or level == "FATAL":
|
||||
try:
|
||||
lfp = config_data["logging"]["logfile_path"]
|
||||
clfp = f"cur_{lfp}"
|
||||
with open(lfp, "a", encoding="utf-8") as logfile: # Write to permanent logfile
|
||||
with open(lfp, "a", encoding="utf-8") as logfile:
|
||||
logfile.write(f"{log_message}\n")
|
||||
logfile.flush() # Ensure it gets written immediately
|
||||
with open(clfp, "a", encoding="utf-8") as c_logfile: # Write to this-run logfile
|
||||
logfile.flush()
|
||||
with open(clfp, "a", encoding="utf-8") as c_logfile:
|
||||
c_logfile.write(f"{log_message}\n")
|
||||
c_logfile.flush() # Ensure it gets written immediately
|
||||
c_logfile.flush()
|
||||
except Exception as e:
|
||||
print(f"[WARNING] Failed to write to logfile: {e}")
|
||||
|
||||
# Handle fatal errors with shutdown
|
||||
if level == "FATAL":
|
||||
print(f"!!! FATAL ERROR LOGGED, SHUTTING DOWN !!!")
|
||||
sys.exit(1)
|
||||
# Handle fatal errors with shutdown
|
||||
if level == "FATAL":
|
||||
print("!!! FATAL ERROR LOGGED, SHUTTING DOWN !!!")
|
||||
sys.exit(1)
|
||||
|
||||
def reset_curlogfile():
|
||||
"""
|
||||
|
|
583
modules/db.py
583
modules/db.py
|
@ -78,7 +78,7 @@ def init_db_connection(config):
|
|||
sqlite_path = db_settings.get("sqlite_path", "local_database.sqlite")
|
||||
try:
|
||||
conn = sqlite3.connect(sqlite_path)
|
||||
globals.log(f"Database connection established using local SQLite: {sqlite_path}")
|
||||
globals.log(f"Database connection established using local SQLite: {sqlite_path}", "DEBUG")
|
||||
return conn
|
||||
except sqlite3.Error as e:
|
||||
globals.log(f"Could not open local SQLite database '{sqlite_path}': {e}", "WARNING")
|
||||
|
@ -257,82 +257,126 @@ def ensure_quotes_table(db_conn):
|
|||
|
||||
def ensure_users_table(db_conn):
|
||||
"""
|
||||
Checks if 'users' table exists. If not, creates it.
|
||||
|
||||
The 'users' table tracks user linkage across platforms:
|
||||
- UUID: (PK) The universal ID for the user
|
||||
- discord_user_id, discord_username, discord_user_display_name
|
||||
- twitch_user_id, twitch_username, twitch_user_display_name
|
||||
- datetime_linked (DATE/TIME of row creation)
|
||||
- user_is_banned (BOOLEAN)
|
||||
- user_is_bot (BOOLEAN)
|
||||
|
||||
This helps unify data for a single 'person' across Discord & Twitch.
|
||||
Checks if 'Users' table exists. If not, creates it.
|
||||
"""
|
||||
is_sqlite = "sqlite3" in str(type(db_conn)).lower()
|
||||
|
||||
# 1) Check existence
|
||||
if is_sqlite:
|
||||
check_sql = """
|
||||
SELECT name
|
||||
FROM sqlite_master
|
||||
WHERE type='table'
|
||||
AND name='users'
|
||||
FROM sqlite_master
|
||||
WHERE type='table'
|
||||
AND name='Users'
|
||||
"""
|
||||
else:
|
||||
check_sql = """
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_name = 'users'
|
||||
AND table_schema = DATABASE()
|
||||
FROM information_schema.tables
|
||||
WHERE table_name = 'Users'
|
||||
AND table_schema = DATABASE()
|
||||
"""
|
||||
|
||||
rows = run_db_operation(db_conn, "read", check_sql)
|
||||
if rows and rows[0] and rows[0][0]:
|
||||
globals.log("Table 'users' already exists, skipping creation.", "DEBUG")
|
||||
globals.log("Table 'Users' already exists, skipping creation.", "DEBUG")
|
||||
return
|
||||
|
||||
# 2) Table does NOT exist => create it
|
||||
globals.log("Table 'users' does not exist; creating now...")
|
||||
globals.log("Table 'Users' does not exist; creating now...", "INFO")
|
||||
|
||||
if is_sqlite:
|
||||
create_table_sql = """
|
||||
CREATE TABLE users (
|
||||
create_sql = """
|
||||
CREATE TABLE Users (
|
||||
UUID TEXT PRIMARY KEY,
|
||||
discord_user_id TEXT,
|
||||
discord_username TEXT,
|
||||
discord_user_display_name TEXT,
|
||||
twitch_user_id TEXT,
|
||||
twitch_username TEXT,
|
||||
twitch_user_display_name TEXT,
|
||||
Unified_Username TEXT,
|
||||
datetime_linked TEXT,
|
||||
user_is_banned BOOLEAN DEFAULT 0,
|
||||
user_is_bot BOOLEAN DEFAULT 0
|
||||
)
|
||||
"""
|
||||
else:
|
||||
create_table_sql = """
|
||||
CREATE TABLE users (
|
||||
create_sql = """
|
||||
CREATE TABLE Users (
|
||||
UUID VARCHAR(36) PRIMARY KEY,
|
||||
discord_user_id VARCHAR(100),
|
||||
discord_username VARCHAR(100),
|
||||
discord_user_display_name VARCHAR(100),
|
||||
twitch_user_id VARCHAR(100),
|
||||
twitch_username VARCHAR(100),
|
||||
twitch_user_display_name VARCHAR(100),
|
||||
Unified_Username VARCHAR(100),
|
||||
datetime_linked DATETIME,
|
||||
user_is_banned BOOLEAN DEFAULT FALSE,
|
||||
user_is_bot BOOLEAN DEFAULT FALSE
|
||||
)
|
||||
"""
|
||||
|
||||
result = run_db_operation(db_conn, "write", create_table_sql)
|
||||
result = run_db_operation(db_conn, "write", create_sql)
|
||||
if result is None:
|
||||
error_msg = "Failed to create 'users' table!"
|
||||
error_msg = "Failed to create 'Users' table!"
|
||||
globals.log(error_msg, "CRITICAL")
|
||||
raise RuntimeError(error_msg)
|
||||
|
||||
globals.log("Successfully created table 'users'.")
|
||||
globals.log("Successfully created table 'Users'.", "INFO")
|
||||
|
||||
#######################
|
||||
# Ensure 'platform_mapping' table
|
||||
#######################
|
||||
|
||||
def ensure_platform_mapping_table(db_conn):
|
||||
"""
|
||||
Ensures the 'Platform_Mapping' table exists.
|
||||
This table maps platform-specific user IDs to the universal UUID.
|
||||
"""
|
||||
is_sqlite = "sqlite3" in str(type(db_conn)).lower()
|
||||
|
||||
if is_sqlite:
|
||||
check_sql = """
|
||||
SELECT name
|
||||
FROM sqlite_master
|
||||
WHERE type='table'
|
||||
AND name='Platform_Mapping'
|
||||
"""
|
||||
else:
|
||||
check_sql = """
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_name = 'Platform_Mapping'
|
||||
AND table_schema = DATABASE()
|
||||
"""
|
||||
|
||||
rows = run_db_operation(db_conn, "read", check_sql)
|
||||
if rows and rows[0] and rows[0][0]:
|
||||
globals.log("Table 'Platform_Mapping' already exists, skipping creation.", "DEBUG")
|
||||
return
|
||||
|
||||
globals.log("Table 'Platform_Mapping' does not exist; creating now...", "INFO")
|
||||
|
||||
if is_sqlite:
|
||||
create_sql = """
|
||||
CREATE TABLE Platform_Mapping (
|
||||
Platform_User_ID TEXT,
|
||||
Platform_Type TEXT,
|
||||
UUID TEXT,
|
||||
Display_Name TEXT,
|
||||
Username TEXT,
|
||||
PRIMARY KEY (Platform_User_ID, Platform_Type),
|
||||
FOREIGN KEY (UUID) REFERENCES Users(UUID) ON DELETE CASCADE
|
||||
)
|
||||
"""
|
||||
else:
|
||||
create_sql = """
|
||||
CREATE TABLE Platform_Mapping (
|
||||
Platform_User_ID VARCHAR(100),
|
||||
Platform_Type ENUM('Discord', 'Twitch'),
|
||||
UUID VARCHAR(36),
|
||||
Display_Name VARCHAR(100),
|
||||
Username VARCHAR(100),
|
||||
PRIMARY KEY (Platform_User_ID, Platform_Type),
|
||||
FOREIGN KEY (UUID) REFERENCES Users(UUID) ON DELETE CASCADE
|
||||
)
|
||||
"""
|
||||
|
||||
result = run_db_operation(db_conn, "write", create_sql)
|
||||
if result is None:
|
||||
error_msg = "Failed to create 'Platform_Mapping' table!"
|
||||
globals.log(error_msg, "CRITICAL")
|
||||
raise RuntimeError(error_msg)
|
||||
|
||||
globals.log("Successfully created table 'Platform_Mapping'.", "INFO")
|
||||
|
||||
|
||||
########################
|
||||
|
@ -341,141 +385,134 @@ def ensure_users_table(db_conn):
|
|||
|
||||
def lookup_user(db_conn, identifier: str, identifier_type: str, target_identifier: str = None):
|
||||
"""
|
||||
Looks up a user in the 'users' table based on the given identifier_type.
|
||||
Looks up a user in the 'Users' table using 'Platform_Mapping' for platform-specific IDs or UUID.
|
||||
|
||||
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.
|
||||
identifier_type can be:
|
||||
- "discord_user_id" or "discord"
|
||||
- "twitch_user_id" or "twitch"
|
||||
- "UUID" (to lookup by UUID directly)
|
||||
|
||||
Returns:
|
||||
If target_identifier is None: A dictionary with the following keys:
|
||||
{
|
||||
"UUID": str,
|
||||
"discord_user_id": str or None,
|
||||
"discord_username": str or None,
|
||||
"discord_user_display_name": str or None,
|
||||
"twitch_user_id": str or None,
|
||||
"twitch_username": str or None,
|
||||
"twitch_user_display_name": str or None,
|
||||
"datetime_linked": str (or datetime as stored in the database),
|
||||
"Unified_Username": str,
|
||||
"datetime_linked": str,
|
||||
"user_is_banned": bool or int,
|
||||
"user_is_bot": bool or int
|
||||
"user_is_bot": bool or int,
|
||||
"platform_user_id": str,
|
||||
"platform_display_name": str,
|
||||
"platform_username": str,
|
||||
"platform_type": str
|
||||
}
|
||||
If target_identifier is provided: The value from the record corresponding to that column.
|
||||
If the lookup fails or the parameters are invalid: None.
|
||||
"""
|
||||
|
||||
# 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"
|
||||
]
|
||||
# Debug: Log the inputs
|
||||
globals.log(f"lookup_user() called with: identifier='{identifier}', identifier_type='{identifier_type}', target_identifier='{target_identifier}'", "DEBUG")
|
||||
|
||||
# Ensure the provided identifier_type is acceptable.
|
||||
if identifier_type.lower() not in valid_cols:
|
||||
if globals.log:
|
||||
globals.log(f"lookup_user error: invalid identifier_type '{identifier_type}'", "WARNING")
|
||||
return None
|
||||
|
||||
# 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 globals.log:
|
||||
globals.log(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,
|
||||
discord_user_id,
|
||||
discord_username,
|
||||
discord_user_display_name,
|
||||
twitch_user_id,
|
||||
twitch_username,
|
||||
twitch_user_display_name,
|
||||
datetime_linked,
|
||||
user_is_banned,
|
||||
user_is_bot
|
||||
FROM users
|
||||
WHERE {identifier_type} = ?
|
||||
LIMIT 1
|
||||
"""
|
||||
|
||||
# Execute the database operation. Adjust run_db_operation() as needed.
|
||||
rows = run_db_operation(db_conn, "read", query, params=(identifier,))
|
||||
if not rows:
|
||||
if globals.log:
|
||||
globals.log(f"lookup_user: No user found for {identifier_type}='{identifier}'", "DEBUG")
|
||||
return None
|
||||
|
||||
# Since we have a single row, convert it to a dictionary.
|
||||
row = rows[0]
|
||||
user_data = {
|
||||
"UUID": row[0],
|
||||
"discord_user_id": row[1],
|
||||
"discord_username": row[2] if row[2] else f"{row[5]} (Discord unlinked)",
|
||||
"discord_user_display_name": row[3] if row[3] else f"{row[6]} (Discord unlinked)",
|
||||
"twitch_user_id": row[4],
|
||||
"twitch_username": row[5] if row[5] else f"{row[2]} (Twitch unlinked)",
|
||||
"twitch_user_display_name": row[6] if row[6] else f"{row[3]} (Twitch unlinked)",
|
||||
"datetime_linked": row[7],
|
||||
"user_is_banned": row[8],
|
||||
"user_is_bot": row[9],
|
||||
# Define platform type and column mappings
|
||||
platform_map = {
|
||||
"discord": "Discord",
|
||||
"discord_user_id": "Discord",
|
||||
"twitch": "Twitch",
|
||||
"twitch_user_id": "Twitch"
|
||||
}
|
||||
|
||||
# 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 usernames are Null, default to that of the opposite platform
|
||||
# if not user_data['discord_username']:
|
||||
# user_data['discord_username'] = f"{user_data['twitch_username']} (Discord unlinked)"
|
||||
# elif not user_data['twitch_username']:
|
||||
# user_data['twitch_username'] = f"{user_data['discord_username']} (Twitch unlinked)"
|
||||
|
||||
# if not user_data['discord_user_display_name']:
|
||||
# user_data['discord_user_display_name'] = f"{user_data['twitch_user_display_name']} (Discord unlinked)"
|
||||
# elif not user_data['twitch_user_display_name']:
|
||||
# user_data['twitch_user_display_name'] = f"{user_data['discord_user_display_name']} (Twitch unlinked)"
|
||||
|
||||
if target_identifier in user_data:
|
||||
return user_data[target_identifier]
|
||||
else:
|
||||
if globals.log:
|
||||
globals.log(f"lookup_user error: target_identifier '{target_identifier}' not present in user data", "WARNING")
|
||||
# Handle UUID case separately
|
||||
if identifier_type.upper() == "UUID":
|
||||
query = """
|
||||
SELECT
|
||||
u.UUID,
|
||||
u.Unified_Username,
|
||||
u.datetime_linked,
|
||||
u.user_is_banned,
|
||||
u.user_is_bot,
|
||||
pm.Platform_User_ID,
|
||||
pm.Display_Name,
|
||||
pm.Username,
|
||||
pm.Platform_Type
|
||||
FROM Users u
|
||||
LEFT JOIN Platform_Mapping pm ON u.UUID = pm.UUID
|
||||
WHERE u.UUID = ?
|
||||
LIMIT 1
|
||||
"""
|
||||
params = (identifier,)
|
||||
else:
|
||||
# Handle platform-specific lookups
|
||||
if identifier_type.lower() not in platform_map:
|
||||
globals.log(f"lookup_user error: invalid identifier_type '{identifier_type}'", "WARNING")
|
||||
return None
|
||||
|
||||
# Otherwise, return the full user record.
|
||||
# Get the platform type (Discord or Twitch)
|
||||
platform_type = platform_map[identifier_type.lower()]
|
||||
|
||||
# Use platform_user_id to lookup the UUID
|
||||
query = """
|
||||
SELECT
|
||||
u.UUID,
|
||||
u.Unified_Username,
|
||||
u.datetime_linked,
|
||||
u.user_is_banned,
|
||||
u.user_is_bot,
|
||||
pm.Platform_User_ID,
|
||||
pm.Display_Name,
|
||||
pm.Username,
|
||||
pm.Platform_Type
|
||||
FROM Users u
|
||||
INNER JOIN Platform_Mapping pm ON u.UUID = pm.UUID
|
||||
WHERE pm.Platform_Type = ? AND pm.Platform_User_ID = ?
|
||||
LIMIT 1
|
||||
"""
|
||||
params = (platform_type, identifier)
|
||||
|
||||
# Debug: Log the query and parameters
|
||||
globals.log(f"lookup_user() executing query: {query} with params={params}", "DEBUG")
|
||||
|
||||
# Run the query
|
||||
rows = run_db_operation(db_conn, "read", query, params)
|
||||
|
||||
# Debug: Log the result of the query
|
||||
globals.log(f"lookup_user() query result: {rows}", "DEBUG")
|
||||
|
||||
# Handle no result case
|
||||
if not rows:
|
||||
globals.log(f"lookup_user: No user found for {identifier_type}='{identifier}'", "WARNING")
|
||||
return None
|
||||
|
||||
# Convert the row to a dictionary
|
||||
row = rows[0]
|
||||
user_data = {
|
||||
"UUID": row[0], # Make UUID consistently uppercase
|
||||
"unified_username": row[1],
|
||||
"datetime_linked": row[2],
|
||||
"user_is_banned": row[3],
|
||||
"user_is_bot": row[4],
|
||||
"platform_user_id": row[5],
|
||||
"platform_display_name": row[6],
|
||||
"platform_username": row[7],
|
||||
"platform_type": row[8]
|
||||
}
|
||||
|
||||
# Debug: Log the constructed user data
|
||||
globals.log(f"lookup_user() constructed user_data: {user_data}", "DEBUG")
|
||||
|
||||
# If target_identifier is provided, return just that value
|
||||
if target_identifier:
|
||||
target_identifier = target_identifier.upper() # Force uppercase for consistency
|
||||
if target_identifier in user_data:
|
||||
globals.log(f"lookup_user() returning target_identifier='{target_identifier}' with value='{user_data[target_identifier]}'", "DEBUG")
|
||||
return user_data[target_identifier]
|
||||
else:
|
||||
globals.log(f"lookup_user error: target_identifier '{target_identifier}' not found in user_data. Available keys: {list(user_data.keys())}", "WARNING")
|
||||
return None
|
||||
|
||||
globals.log(f"lookup_user() returning full user_data: {user_data}", "DEBUG")
|
||||
return user_data
|
||||
|
||||
|
||||
|
||||
def ensure_chatlog_table(db_conn):
|
||||
"""
|
||||
Checks if 'chat_log' table exists. If not, creates it.
|
||||
|
@ -555,17 +592,17 @@ def ensure_chatlog_table(db_conn):
|
|||
globals.log("Successfully created table 'chat_log'.", "INFO")
|
||||
|
||||
|
||||
def log_message(db_conn, user_uuid, message_content, platform, channel, attachments=None, username: str = "Unknown"):
|
||||
def log_message(db_conn, identifier, identifier_type, 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.
|
||||
Logs a message in 'chat_log' with UUID fetched using the new Platform_Mapping structure.
|
||||
"""
|
||||
# Get UUID using the updated lookup_user
|
||||
user_data = lookup_user(db_conn, identifier, identifier_type)
|
||||
if not user_data:
|
||||
globals.log(f"User not found for {identifier_type}='{identifier}'", "WARNING")
|
||||
return
|
||||
|
||||
DATETIME will default to current timestamp in the DB.
|
||||
"""
|
||||
user_uuid = user_data["UUID"]
|
||||
|
||||
if attachments is None or not "https://" in attachments:
|
||||
attachments = ""
|
||||
|
@ -577,14 +614,13 @@ def log_message(db_conn, user_uuid, message_content, platform, channel, attachme
|
|||
PLATFORM,
|
||||
CHANNEL,
|
||||
ATTACHMENTS
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
) VALUES (?, ?, ?, ?, ?)
|
||||
"""
|
||||
params = (user_uuid, message_content, platform, channel, attachments)
|
||||
rowcount = run_db_operation(db_conn, "write", insert_sql, params)
|
||||
|
||||
if rowcount and rowcount > 0:
|
||||
globals.log(f"Logged message for UUID={user_uuid} ({username}) in 'chat_log'.", "DEBUG")
|
||||
globals.log(f"Logged message for UUID={user_uuid}.", "DEBUG")
|
||||
else:
|
||||
globals.log("Failed to log message in 'chat_log'.", "ERROR")
|
||||
|
||||
|
@ -791,36 +827,29 @@ def ensure_discord_activity_table(db_conn):
|
|||
globals.log("Successfully created table 'discord_activity'.", "INFO")
|
||||
|
||||
|
||||
def log_discord_activity(db_conn, guild_id, user_uuid, action, voice_channel, action_detail=None):
|
||||
def log_discord_activity(db_conn, guild_id, user_identifier, 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.
|
||||
Logs Discord activities with duplicate detection to prevent redundant logs.
|
||||
"""
|
||||
|
||||
# Resolve UUID using the new Platform_Mapping
|
||||
user_data = lookup_user(db_conn, user_identifier, identifier_type="discord_user_id")
|
||||
if not user_data:
|
||||
globals.log(f"User not found for Discord ID: {user_identifier}", "WARNING")
|
||||
return
|
||||
|
||||
user_uuid = user_data["UUID"]
|
||||
|
||||
# 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
|
||||
|
||||
# Duplicate Detection Logic
|
||||
def normalize_detail(detail):
|
||||
"""Return a normalized version of the detail for comparison (or None if detail is None)."""
|
||||
"""Normalize detail for comparison (lowercase, stripped of whitespace)."""
|
||||
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,)
|
||||
)
|
||||
if not user_check:
|
||||
globals.log(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)
|
||||
|
||||
|
@ -832,15 +861,9 @@ def log_discord_activity(db_conn, guild_id, user_uuid, action, voice_channel, ac
|
|||
ORDER BY DATETIME DESC
|
||||
LIMIT ?
|
||||
"""
|
||||
rows = run_db_operation(
|
||||
db_conn, "read", query, params=(user_uuid, action, NUM_RECENT_ENTRIES)
|
||||
)
|
||||
|
||||
# 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.
|
||||
rows = run_db_operation(db_conn, "read", query, params=(user_uuid, action, NUM_RECENT_ENTRIES))
|
||||
|
||||
last_same, last_different = None, None
|
||||
for row in rows:
|
||||
dt_str, detail = row
|
||||
try:
|
||||
|
@ -850,41 +873,37 @@ def log_discord_activity(db_conn, guild_id, user_uuid, action, voice_channel, ac
|
|||
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.
|
||||
# Check duplicate conditions
|
||||
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")
|
||||
if now - last_same < DUPLICATE_THRESHOLD:
|
||||
globals.log(f"Duplicate {action} event for {user_uuid} 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 the new event
|
||||
insert_sql = """
|
||||
INSERT INTO discord_activity (UUID, ACTION, GUILD_ID, VOICE_CHANNEL, ACTION_DETAIL)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""" if "sqlite3" in str(type(db_conn)).lower() else """
|
||||
INSERT INTO discord_activity (UUID, ACTION, GUILD_ID, VOICE_CHANNEL, ACTION_DETAIL)
|
||||
VALUES (%s, %s, %s, %s, %s)
|
||||
"""
|
||||
params = (user_uuid, action, guild_id, channel_val, action_detail)
|
||||
rowcount = run_db_operation(db_conn, "write", sql, params)
|
||||
rowcount = run_db_operation(db_conn, "write", insert_sql, params)
|
||||
|
||||
if rowcount and rowcount > 0:
|
||||
detail_str = f" ({action_detail})" if action_detail else ""
|
||||
globals.log(f"Logged Discord activity in Guild {guild_id}: {action}{detail_str}", "DEBUG")
|
||||
globals.log(f"Logged Discord activity for UUID={user_uuid} in Guild {guild_id}: {action}{detail_str}", "DEBUG")
|
||||
else:
|
||||
globals.log("Failed to log Discord activity.", "ERROR")
|
||||
|
||||
|
||||
def ensure_bot_events_table(db_conn):
|
||||
"""
|
||||
Ensures the 'bot_events' table exists, which logs major bot-related events.
|
||||
|
@ -1033,35 +1052,36 @@ def ensure_link_codes_table(db_conn):
|
|||
|
||||
def merge_uuid_data(db_conn, 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.
|
||||
Merges data from old UUID to new UUID, updating references in Platform_Mapping.
|
||||
"""
|
||||
globals.log(f"Starting UUID merge: {old_uuid} -> {new_uuid}", "INFO")
|
||||
globals.log(f"Merging UUID data: {old_uuid} -> {new_uuid}", "INFO")
|
||||
|
||||
# Update references in Platform_Mapping
|
||||
update_mapping_sql = """
|
||||
UPDATE Platform_Mapping SET UUID = ? WHERE UUID = ?
|
||||
"""
|
||||
run_db_operation(db_conn, "update", update_mapping_sql, (new_uuid, old_uuid))
|
||||
|
||||
tables_to_update = [
|
||||
"voice_activity_log",
|
||||
"bot_events",
|
||||
"chat_log",
|
||||
"user_howls",
|
||||
"quotes"
|
||||
"discord_activity",
|
||||
"community_events"
|
||||
]
|
||||
|
||||
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))
|
||||
globals.log(f"Updated {rowcount} rows in {table} (transferring {old_uuid} -> {new_uuid})", "DEBUG")
|
||||
globals.log(f"Updated {rowcount} rows in {table} (transferred {old_uuid} -> {new_uuid})", "DEBUG")
|
||||
|
||||
# Finally, delete the old UUID from the `users` table
|
||||
delete_sql = "DELETE FROM users WHERE UUID = ?"
|
||||
# Finally, delete the old UUID from Users table
|
||||
delete_sql = "DELETE FROM Users WHERE UUID = ?"
|
||||
rowcount = run_db_operation(db_conn, "write", delete_sql, (old_uuid,))
|
||||
|
||||
globals.log(f"Deleted old UUID {old_uuid} from 'users' table ({rowcount} rows affected)", "INFO")
|
||||
globals.log(f"Deleted old UUID {old_uuid} from 'Users' table ({rowcount} rows affected)", "INFO")
|
||||
|
||||
globals.log(f"UUID merge complete: {old_uuid} -> {new_uuid}", "INFO")
|
||||
|
||||
|
||||
def ensure_community_events_table(db_conn):
|
||||
"""
|
||||
Checks if 'community_events' table exists. If not, attempts to create it.
|
||||
|
@ -1126,21 +1146,9 @@ def ensure_community_events_table(db_conn):
|
|||
|
||||
async def handle_community_event(db_conn, is_discord, ctx, args):
|
||||
"""
|
||||
Handles community event commands.
|
||||
|
||||
Accepted subcommands (args[0] if provided):
|
||||
- add <event_type> [event_details [|| event_extras]]
|
||||
-> Logs a new event.
|
||||
- info <event_id>
|
||||
-> Retrieves detailed information for a given event.
|
||||
- list [limit]
|
||||
-> Lists recent events (default limit 5 if not specified).
|
||||
- search <keyword...>
|
||||
-> Searches events (by EVENT_TYPE or EVENT_DETAILS).
|
||||
|
||||
If no arguments are provided, defaults to listing recent events.
|
||||
Handles community event commands including add, info, list, and search.
|
||||
"""
|
||||
from modules import db # Assumes your db module is available
|
||||
|
||||
if len(args) == 0:
|
||||
args = ["list"]
|
||||
|
||||
|
@ -1149,12 +1157,12 @@ async def handle_community_event(db_conn, is_discord, ctx, args):
|
|||
if sub == "add":
|
||||
if len(args) < 2:
|
||||
return "Please provide the event type after 'add'."
|
||||
event_type = args[1]
|
||||
# Concatenate remaining args as event details (if any)
|
||||
event_details = " ".join(args[2:]).strip() if len(args) > 2 else None
|
||||
|
||||
# Optional: If you want to support extras, you might separate event_details and extras using "||"
|
||||
event_type = args[1]
|
||||
event_details = " ".join(args[2:]).strip() if len(args) > 2 else None
|
||||
event_extras = None
|
||||
|
||||
# Support extras using "||" separator
|
||||
if event_details and "||" in event_details:
|
||||
parts = event_details.split("||", 1)
|
||||
event_details = parts[0].strip()
|
||||
|
@ -1162,28 +1170,28 @@ async def handle_community_event(db_conn, is_discord, ctx, args):
|
|||
|
||||
platform = "Discord" if is_discord else "Twitch"
|
||||
user_id = str(ctx.author.id)
|
||||
# Lookup user data to get UUID (similar to your quote logic)
|
||||
user_data = db.lookup_user(db_conn, identifier=user_id, identifier_type=f"{platform.lower()}_user_id")
|
||||
|
||||
# Get UUID using lookup_user()
|
||||
user_data = lookup_user(db_conn, identifier=user_id, identifier_type=f"{platform.lower()}_user_id")
|
||||
if not user_data:
|
||||
globals.log(f"Could not find UUID for user {ctx.author.name} ({user_id}) on {platform}.", "ERROR")
|
||||
globals.log(f"User not found: {ctx.author.name} ({user_id}) on {platform}", "ERROR")
|
||||
return "Could not log event: user data missing."
|
||||
|
||||
user_uuid = user_data["UUID"]
|
||||
|
||||
# Insert new event. Use appropriate parameter placeholders.
|
||||
if "sqlite3" in str(type(db_conn)).lower():
|
||||
insert_sql = """
|
||||
INSERT INTO community_events
|
||||
(EVENT_PLATFORM, EVENT_TYPE, EVENT_DETAILS, EVENT_USER, DATETIME, EVENT_EXTRAS)
|
||||
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP, ?)
|
||||
"""
|
||||
else:
|
||||
insert_sql = """
|
||||
INSERT INTO community_events
|
||||
(EVENT_PLATFORM, EVENT_TYPE, EVENT_DETAILS, EVENT_USER, DATETIME, EVENT_EXTRAS)
|
||||
VALUES (%s, %s, %s, %s, CURRENT_TIMESTAMP, %s)
|
||||
"""
|
||||
# Insert new event. Adjust for SQLite or MariaDB.
|
||||
insert_sql = """
|
||||
INSERT INTO community_events
|
||||
(EVENT_PLATFORM, EVENT_TYPE, EVENT_DETAILS, EVENT_USER, DATETIME, EVENT_EXTRAS)
|
||||
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP, ?)
|
||||
""" if "sqlite3" in str(type(db_conn)).lower() else """
|
||||
INSERT INTO community_events
|
||||
(EVENT_PLATFORM, EVENT_TYPE, EVENT_DETAILS, EVENT_USER, DATETIME, EVENT_EXTRAS)
|
||||
VALUES (%s, %s, %s, %s, CURRENT_TIMESTAMP, %s)
|
||||
"""
|
||||
params = (platform, event_type, event_details, user_uuid, event_extras)
|
||||
result = db.run_db_operation(db_conn, "write", insert_sql, params)
|
||||
result = run_db_operation(db_conn, "write", insert_sql, params)
|
||||
|
||||
if result is not None:
|
||||
globals.log(f"New event added: {event_type} by {ctx.author.name}", "DEBUG")
|
||||
return f"Successfully logged event: {event_type}"
|
||||
|
@ -1197,13 +1205,11 @@ async def handle_community_event(db_conn, is_discord, ctx, args):
|
|||
select_sql = "SELECT * FROM community_events WHERE EVENT_ID = ?"
|
||||
if "sqlite3" not in str(type(db_conn)).lower():
|
||||
select_sql = "SELECT * FROM community_events WHERE EVENT_ID = %s"
|
||||
rows = db.run_db_operation(db_conn, "read", select_sql, (event_id,))
|
||||
rows = run_db_operation(db_conn, "read", select_sql, (event_id,))
|
||||
if not rows:
|
||||
return f"No event found with ID {event_id}."
|
||||
row = rows[0]
|
||||
# row indices: 0: EVENT_ID, 1: EVENT_PLATFORM, 2: EVENT_TYPE, 3: EVENT_DETAILS,
|
||||
# 4: EVENT_USER, 5: DATETIME, 6: EVENT_EXTRAS
|
||||
resp = (
|
||||
return (
|
||||
f"Event #{row[0]}:\n"
|
||||
f"Platform: {row[1]}\n"
|
||||
f"Type: {row[2]}\n"
|
||||
|
@ -1212,47 +1218,6 @@ async def handle_community_event(db_conn, is_discord, ctx, args):
|
|||
f"Datetime: {row[5]}\n"
|
||||
f"Extras: {row[6] or 'N/A'}"
|
||||
)
|
||||
return resp
|
||||
|
||||
elif sub == "list":
|
||||
# Optional limit argument (default 5)
|
||||
limit = 5
|
||||
if len(args) >= 2 and args[1].isdigit():
|
||||
limit = int(args[1])
|
||||
select_sql = f"SELECT * FROM community_events ORDER BY DATETIME DESC LIMIT {limit}"
|
||||
rows = db.run_db_operation(db_conn, "read", select_sql)
|
||||
if not rows:
|
||||
return "No events logged yet."
|
||||
resp_lines = []
|
||||
for row in rows:
|
||||
# Display basic info: ID, Type, Platform, Datetime
|
||||
resp_lines.append(f"#{row[0]}: {row[2]} on {row[1]} at {row[5]}")
|
||||
return "\n".join(resp_lines)
|
||||
|
||||
elif sub == "search":
|
||||
if len(args) < 2:
|
||||
return "Please provide keywords to search for."
|
||||
keywords = " ".join(args[1:])
|
||||
like_pattern = f"%{keywords}%"
|
||||
search_sql = """
|
||||
SELECT * FROM community_events
|
||||
WHERE EVENT_TYPE LIKE ? OR EVENT_DETAILS LIKE ?
|
||||
ORDER BY DATETIME DESC LIMIT 5
|
||||
"""
|
||||
if "sqlite3" not in str(type(db_conn)).lower():
|
||||
search_sql = """
|
||||
SELECT * FROM community_events
|
||||
WHERE EVENT_TYPE LIKE %s OR EVENT_DETAILS LIKE %s
|
||||
ORDER BY DATETIME DESC LIMIT 5
|
||||
"""
|
||||
rows = db.run_db_operation(db_conn, "read", search_sql, (like_pattern, like_pattern))
|
||||
if not rows:
|
||||
return "No matching events found."
|
||||
resp_lines = []
|
||||
for row in rows:
|
||||
resp_lines.append(f"#{row[0]}: {row[2]} on {row[1]} at {row[5]}")
|
||||
return "\n".join(resp_lines)
|
||||
|
||||
else:
|
||||
# Unknown subcommand; default to listing recent events.
|
||||
return await handle_community_event_command(db_conn, is_discord, ctx, ["list"])
|
||||
return await handle_community_event(db_conn, is_discord, ctx, ["list"])
|
||||
|
||||
|
|
|
@ -723,14 +723,14 @@ def track_user_activity(
|
|||
globals.log(f"... Returned {user_data}", "DEBUG")
|
||||
|
||||
if platform.lower() == "discord":
|
||||
if user_data["discord_username"] != username:
|
||||
if user_data["platform_username"] != username:
|
||||
need_update = True
|
||||
column_updates.append("discord_username = ?")
|
||||
column_updates.append("platform_username = ?")
|
||||
params.append(username)
|
||||
|
||||
if user_data["discord_user_display_name"] != display_name:
|
||||
if user_data["platform_display_name"] != display_name:
|
||||
need_update = True
|
||||
column_updates.append("discord_user_display_name = ?")
|
||||
column_updates.append("platform_display_name = ?")
|
||||
params.append(display_name)
|
||||
|
||||
if user_data["user_is_bot"] != user_is_bot:
|
||||
|
@ -752,14 +752,14 @@ def track_user_activity(
|
|||
globals.log(f"Updated Discord user '{username}' (display '{display_name}') in 'users'.", "DEBUG")
|
||||
|
||||
elif platform.lower() == "twitch":
|
||||
if user_data["twitch_username"] != username:
|
||||
if user_data["platform_username"] != username:
|
||||
need_update = True
|
||||
column_updates.append("twitch_username = ?")
|
||||
column_updates.append("platform_username = ?")
|
||||
params.append(username)
|
||||
|
||||
if user_data["twitch_user_display_name"] != display_name:
|
||||
if user_data["platform_display_name"] != display_name:
|
||||
need_update = True
|
||||
column_updates.append("twitch_user_display_name = ?")
|
||||
column_updates.append("platform_display_name = ?")
|
||||
params.append(display_name)
|
||||
|
||||
if user_data["user_is_bot"] != user_is_bot:
|
||||
|
@ -789,8 +789,8 @@ def track_user_activity(
|
|||
INSERT INTO users (
|
||||
UUID,
|
||||
discord_user_id,
|
||||
discord_username,
|
||||
discord_user_display_name,
|
||||
platform_username,
|
||||
platform_display_name,
|
||||
user_is_bot
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
|
@ -801,8 +801,8 @@ def track_user_activity(
|
|||
INSERT INTO users (
|
||||
UUID,
|
||||
twitch_user_id,
|
||||
twitch_username,
|
||||
twitch_user_display_name,
|
||||
platform_username,
|
||||
platform_display_name,
|
||||
user_is_bot
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
|
|
Loading…
Reference in New Issue