From aed3d24e333f532aafc3448f54f98310b7bcc099 Mon Sep 17 00:00:00 2001 From: Kami Date: Wed, 5 Feb 2025 00:33:02 +0100 Subject: [PATCH] Started implementing Universal User Identification (UUI) - added database table "users" - table allows for assigning individual users a UUID for universal processing - will support account linking in the future - fixed a bug with reporting Discord commands in logs --- bots.py | 10 ++- cmd_discord.py | 3 +- modules/db.py | 162 +++++++++++++++++++++++++++++++++++++++++++++ modules/utility.py | 15 ++++- 4 files changed, 185 insertions(+), 5 deletions(-) diff --git a/bots.py b/bots.py index c58f30e..726a214 100644 --- a/bots.py +++ b/bots.py @@ -14,7 +14,7 @@ from bot_discord import DiscordBot from bot_twitch import TwitchBot from modules.db import init_db_connection, run_db_operation -from modules.db import ensure_quotes_table +from modules.db import ensure_quotes_table, ensure_users_table # Load environment variables load_dotenv() @@ -56,7 +56,7 @@ def log(message, level="INFO", exec_info=False): See 'config.json' for disabling/enabling logging levels """ from modules import utility - log_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "FATAL"] + 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 @@ -99,7 +99,6 @@ async def main(): # Log initial start log("--------------- BOT STARTUP ---------------") - # Before creating your DiscordBot/TwitchBot, initialize DB db_conn = init_db_connection(config_data, log) if not db_conn: @@ -110,6 +109,7 @@ async def main(): # auto-create the quotes table if it doesn't exist try: ensure_quotes_table(db_conn, log) + ensure_users_table(db_conn, log) except Exception as e: log(f"Critical: unable to ensure quotes table: {e}", "FATAL") @@ -132,6 +132,10 @@ async def main(): discord_task = asyncio.create_task(discord_bot.run(os.getenv("DISCORD_BOT_TOKEN"))) twitch_task = asyncio.create_task(twitch_bot.run()) + from modules.utility import dev_func + dev_func_result = dev_func(db_conn, log) + log(f"dev_func output: {dev_func_result}") + await asyncio.gather(discord_task, twitch_task) if __name__ == "__main__": diff --git a/cmd_discord.py b/cmd_discord.py index 7f7dfa2..7a2d834 100644 --- a/cmd_discord.py +++ b/cmd_discord.py @@ -91,6 +91,7 @@ def setup(bot, db_conn=None, log=None): ###################### # Debug: Print that commands are being registered try: - bot.log(f"Registering commands for Discord: {list(bot.commands.keys())}", "DEBUG") + command_names = [cmd.name for cmd in bot.commands] # Extract command names + bot.log(f"Registering commands for Discord: {command_names}", "DEBUG") except Exception as e: bot.log(f"An error occured while printing registered commands for Discord: {e}", "WARNING") \ No newline at end of file diff --git a/modules/db.py b/modules/db.py index d34f2b1..b81b703 100644 --- a/modules/db.py +++ b/modules/db.py @@ -212,3 +212,165 @@ def ensure_quotes_table(db_conn, log_func): raise RuntimeError(error_msg) log_func("Successfully created table 'quotes'.") + +####################### +# Ensure 'users' table +####################### + +def ensure_users_table(db_conn, log_func): + """ + 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. + """ + 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' + """ + else: + check_sql = """ + SELECT table_name + FROM information_schema.tables + WHERE table_name = 'users' + AND table_schema = DATABASE() + """ + + rows = run_db_operation(db_conn, "read", check_sql, log_func=log_func) + if rows and rows[0] and rows[0][0]: + log_func("Table 'users' already exists, skipping creation.", "DEBUG") + return + + # 2) Table does NOT exist => create it + log_func("Table 'users' does not exist; creating now...") + + if is_sqlite: + create_table_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, + datetime_linked TEXT DEFAULT CURRENT_TIMESTAMP, + user_is_banned BOOLEAN DEFAULT 0, + user_is_bot BOOLEAN DEFAULT 0 + ) + """ + else: + create_table_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), + datetime_linked DATETIME DEFAULT CURRENT_TIMESTAMP, + user_is_banned BOOLEAN DEFAULT FALSE, + user_is_bot BOOLEAN DEFAULT FALSE + ) + """ + + result = run_db_operation(db_conn, "write", create_table_sql, log_func=log_func) + if result is None: + error_msg = "Failed to create 'users' table!" + log_func(error_msg, "ERROR") + raise RuntimeError(error_msg) + + log_func("Successfully created table 'users'.") + + +######################## +# Lookup user function +######################## + +def lookup_user(db_conn, log_func, identifier, identifier_type="discord_user_id"): + """ + Looks up a user in the 'users' table based on the given identifier_type: + - "uuid" + - "discord_user_id" + - "discord_username" + - "twitch_user_id" + - "twitch_username" + You can add more if needed. + + Returns a dictionary with all columns: + { + "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 in MariaDB), + "user_is_banned": bool or int, + "user_is_bot": bool or int + } + + If not found, returns None. + """ + + valid_cols = ["uuid", "discord_user_id", "discord_username", + "twitch_user_id", "twitch_username"] + + if identifier_type.lower() not in valid_cols: + if log_func: + log_func(f"lookup_user error: invalid identifier_type={identifier_type}", "WARNING") + return None + + # Build the query + 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 + """ + + rows = run_db_operation(db_conn, "read", query, params=(identifier,), log_func=log_func) + if not rows: + return None + + # We have at least one row + row = rows[0] # single row + # Build a dictionary + user_data = { + "UUID": row[0], + "discord_user_id": row[1], + "discord_username": row[2], + "discord_user_display_name": row[3], + "twitch_user_id": row[4], + "twitch_username": row[5], + "twitch_user_display_name": row[6], + "datetime_linked": row[7], + "user_is_banned": row[8], + "user_is_bot": row[9], + } + return user_data \ No newline at end of file diff --git a/modules/utility.py b/modules/utility.py index f7ca3a3..a269c2b 100644 --- a/modules/utility.py +++ b/modules/utility.py @@ -420,4 +420,17 @@ async def send_message(ctx, text): """ Minimal helper to send a message to either Discord or Twitch. """ - await ctx.send(text) \ No newline at end of file + await ctx.send(text) + +############################################### +# Development Test Function (called upon start) +############################################### +def dev_func(db_conn, log): + from modules.db import lookup_user + id = "203190147582394369" + id_type = "discord_user_id" + uui_info = lookup_user(db_conn, log, identifier=id, identifier_type=id_type) + if uui_info: + return list(uui_info.values()) + else: + return f"User with identifier '{id}' ({id_type}) not found" \ No newline at end of file