From d0313a6a92fa058e3f7244a14fa131cb27fcbae9 Mon Sep 17 00:00:00 2001 From: Kami Date: Sat, 1 Mar 2025 01:54:51 +0100 Subject: [PATCH] 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 --- bot_discord.py | 47 ++- bot_twitch.py | 5 +- bots.py | 10 +- cmd_common/common_commands.py | 65 ++-- globals.py | 89 +++--- modules/db.py | 557 ++++++++++++++++------------------ modules/utility.py | 24 +- 7 files changed, 393 insertions(+), 404 deletions(-) diff --git a/bot_discord.py b/bot_discord.py index eec3a07..82c93e6 100644 --- a/bot_discord.py +++ b/bot_discord.py @@ -93,17 +93,19 @@ 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 user_id = str(message.author.id) 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 - 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 + # Let log_message() handle UUID lookup internally + platform_str = f"discord-{guild_name}" + 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 = "" + attachments = ", ".join(a.url for a in message.attachments) if message.attachments else "" - 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: diff --git a/bot_twitch.py b/bot_twitch.py index 7d497e3..d45fe14 100644 --- a/bot_twitch.py +++ b/bot_twitch.py @@ -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 diff --git a/bots.py b/bots.py index 17a54c7..a7c58fe 100644 --- a/bots.py +++ b/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: diff --git a/cmd_common/common_commands.py b/cmd_common/common_commands.py index a72c595..88f08c8 100644 --- a/cmd_common/common_commands.py +++ b/cmd_common/common_commands.py @@ -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 @@ -122,7 +124,7 @@ def handle_howl_stats(ctx, platform, target_name) -> str: if not stats: utility.wfetl() return "No howls have been recorded yet!" - + total_howls = stats["total_howls"] avg_howl = stats["average_howl"] unique_users = stats["unique_users"] @@ -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 diff --git a/globals.py b/globals.py index 2a5ddd1..7b3c532 100644 --- a/globals.py +++ b/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(): """ diff --git a/modules/db.py b/modules/db.py index 1d80021..86e3283 100644 --- a/modules/db.py +++ b/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. + + identifier_type can be: + - "discord_user_id" or "discord" + - "twitch_user_id" or "twitch" + - "UUID" (to lookup by UUID directly) - The accepted identifier_type values are: - - "uuid" - - "discord_user_id" or alias "discord" - - "discord_username" - - "discord_user_display_name" - - "twitch_user_id" or alias "twitch" - - "twitch_username" - - "twitch_user_display_name" - - Optionally, if target_identifier is provided (must be one of the accepted columns), - only that column's value will be returned instead of the full user record. - Returns: If target_identifier is None: A dictionary with the following keys: { "UUID": str, - "discord_user_id": str or None, - "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" - ] - - # 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 + # Debug: Log the inputs + globals.log(f"lookup_user() called with: identifier='{identifier}', identifier_type='{identifier_type}', target_identifier='{target_identifier}'", "DEBUG") - # 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" + # Define platform type and column mappings + platform_map = { + "discord": "Discord", + "discord_user_id": "Discord", + "twitch": "Twitch", + "twitch_user_id": "Twitch" + } - # 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") + # 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 - # 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 - """ + # Get the platform type (Discord or Twitch) + platform_type = platform_map[identifier_type.lower()] - # Execute the database operation. Adjust run_db_operation() as needed. - rows = run_db_operation(db_conn, "read", query, params=(identifier,)) + # 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: - 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 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_details [|| event_extras]] - -> Logs a new event. - - info - -> Retrieves detailed information for a given event. - - list [limit] - -> Lists recent events (default limit 5 if not specified). - - search - -> 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_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"]) + diff --git a/modules/utility.py b/modules/utility.py index 232340d..f3a8c3b 100644 --- a/modules/utility.py +++ b/modules/utility.py @@ -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 (?, ?, ?, ?, ?)