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 fixes
kami_dev
Kami 2025-03-01 01:54:51 +01:00
parent 766c3ab690
commit d0313a6a92
7 changed files with 393 additions and 404 deletions

View File

@ -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")
if user_uuid:
# The "platform" can be e.g. "discord" or you can store the server name
# Let log_message() handle UUID lookup internally
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
# 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,
identifier=user_id,
identifier_type="discord_user_id",
message_content=message.content or "",
platform=platform_str,
channel=channel_str,
attachments=attachments,
username=message.author.name
attachments=attachments
)
# PLACEHOLDER FOR FUTURE MESSAGE PROCESSING
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:

View File

@ -145,7 +145,10 @@ class TwitchBot(commands.Bot):
globals.log("Twitch token refreshed successfully. Restarting bot...")
# Restart the TwitchIO 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
View File

@ -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:

View File

@ -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

View File

@ -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,57 +85,72 @@ 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"}
if level not in log_levels:
level = "INFO" # Default to INFO if an invalid level is provided
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"]:
# 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()}"
# Print to terminal if enabled
# 'FATAL' errors override settings
# Checks config file to see enabled/disabled logging levels
# 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":
# 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 !!!")
print("!!! FATAL ERROR LOGGED, SHUTTING DOWN !!!")
sys.exit(1)
def reset_curlogfile():

View File

@ -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'
AND name='Users'
"""
else:
check_sql = """
SELECT table_name
FROM information_schema.tables
WHERE table_name = 'users'
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:
# Define platform type and column mappings
platform_map = {
"discord": "Discord",
"discord_user_id": "Discord",
"twitch": "Twitch",
"twitch_user_id": "Twitch"
}
# 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
# 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"
# Get the platform type (Discord or Twitch)
platform_type = platform_map[identifier_type.lower()]
# 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"""
# Use platform_user_id to lookup the UUID
query = """
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} = ?
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)
# Execute the database operation. Adjust run_db_operation() as needed.
rows = run_db_operation(db_conn, "read", query, params=(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:
if globals.log:
globals.log(f"lookup_user: No user found for {identifier_type}='{identifier}'", "DEBUG")
globals.log(f"lookup_user: No user found for {identifier_type}='{identifier}'", "WARNING")
return None
# Since we have a single row, convert it to a dictionary.
# Convert the row 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],
"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]
}
# If the caller requested a specific target column, return that value.
# 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:
# 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)"
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:
if globals.log:
globals.log(f"lookup_user error: target_identifier '{target_identifier}' not present in user data", "WARNING")
globals.log(f"lookup_user error: target_identifier '{target_identifier}' not found in user_data. Available keys: {list(user_data.keys())}", "WARNING")
return None
# Otherwise, return the full user record.
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 its 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 its an object with a name, use that).
channel_val = voice_channel.name if (voice_channel and hasattr(voice_channel, "name")) else voice_channel
# Insert the new event.
sql = """
# Insert 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 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, ?)
"""
else:
insert_sql = """
""" 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"])

View File

@ -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 (?, ?, ?, ?, ?)