# cmd_common/common_commands.py import random import time from modules import utility import globals import json import re from globals import logger from modules import db #def howl(username: str) -> str: # """ # Generates a howl response based on a random percentage. # Uses a dictionary to allow flexible, randomized responses. # """ # howl_percentage = random.randint(0, 100) # # # Round percentage down to nearest 10 (except 0 and 100) # rounded_percentage = 0 if howl_percentage == 0 else 100 if howl_percentage == 100 else (howl_percentage // 10) * 10 # # # Fetch a random response from the dictionary # response = utility.get_random_reply("howl_replies", str(rounded_percentage), username=username, howl_percentage=howl_percentage) # # return response def handle_howl_command(ctx) -> str: """ A single function that handles !howl logic for both Discord and Twitch. We rely on ctx to figure out the platform, the user, the arguments, etc. Return a string that the caller will send. """ utility.wfstl() # 1) Detect which platform # We might do something like: platform, author_id, author_name, author_display_name, args = extract_ctx_info(ctx) # 2) Subcommand detection if args and args[0].lower() in ("stat", "stats"): # we are in stats mode if len(args) > 1: if args[1].lower() in ("all", "global", "community"): target_name = "_COMMUNITY_" target_name = args[1] else: target_name = author_name utility.wfetl() return handle_howl_stats(ctx, platform, target_name) else: # normal usage => random generation utility.wfetl() return handle_howl_normal(ctx, platform, author_id, author_display_name) def extract_ctx_info(ctx): """ Figures out if this is Discord or Twitch, returns (platform_str, author_id, author_name, author_display_name, args). """ utility.wfstl() # Is it discord.py or twitchio? if hasattr(ctx, "guild"): # typically means discord.py context platform_str = "discord" author_id = str(ctx.author.id) author_name = ctx.author.name author_display_name = ctx.author.display_name # parse arguments from ctx.message.content parts = ctx.message.content.strip().split() args = parts[1:] if len(parts) > 1 else [] else: # assume twitchio platform_str = "twitch" author = ctx.author author_id = str(author.id) author_name = author.name author_display_name = author.display_name or author.name # parse arguments from ctx.message.content parts = ctx.message.content.strip().split() args = parts[1:] if len(parts) > 1 else [] utility.wfetl() return (platform_str, author_id, author_name, author_display_name, args) def handle_howl_normal(ctx, platform, author_id, author_display_name) -> str: """ Normal usage: random generation, store in DB. """ utility.wfstl() db_conn = ctx.bot.db_conn # Random logic for howl percentage howl_val = random.randint(0, 100) rounded_val = 0 if howl_val == 0 else 100 if howl_val == 100 else (howl_val // 10) * 10 # Dictionary-based reply reply = utility.get_random_reply( "howl_replies", str(rounded_val), username=author_display_name, howl_percentage=howl_val ) # Consistent UUID lookup user_data = db.lookup_user(db_conn, identifier=author_id, identifier_type=f"{platform}_user_id") if user_data: user_uuid = user_data["uuid"] db.insert_howl(db_conn, user_uuid, howl_val) else: logger.warning(f"Could not find user by ID={author_id} on {platform}. Not storing howl.") 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 # Check if requesting global stats if target_name in ("_COMMUNITY_", "all", "global", "community"): stats = db.get_global_howl_stats(db_conn) 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"] count_zero = stats["count_zero"] count_hundred = stats["count_hundred"] utility.wfetl() return (f"**Community Howl Stats:**\n" f"Total Howls: {total_howls}\n" f"Average Howl: {avg_howl:.1f}%\n" f"Unique Howlers: {unique_users}\n" f"0% Howls: {count_zero}, 100% Howls: {count_hundred}") # Otherwise, lookup a single user user_data = db.lookup_user(db_conn, identifier=target_name, identifier_type=f"{platform}_username") if not user_data: user_data = db.lookup_user(db_conn, identifier=target_name, identifier_type=f"{platform}_displayname") if not user_data: user_data = db.lookup_user(db_conn, identifier=target_name, identifier_type=f"{platform}_user_id") if not user_data: utility.wfetl() return f"I don't know that user: {target_name}" 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.)" c = stats["count"] a = stats["average"] z = stats["count_zero"] h = stats["count_hundred"] utility.wfetl() return (f"{target_name} has howled {c} times, averaging {a:.1f}% " f"(0% x{z}, 100% x{h})") def lookup_user_by_name(db_conn, platform, name_str): """ Consistent UUID resolution for usernames across platforms. """ utility.wfstl() if platform == "discord": ud = db.lookup_user(db_conn, name_str, "discord_display_name") if ud: utility.wfetl() return ud 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_display_name") if ud: utility.wfetl() return ud ud = db.lookup_user(db_conn, name_str, "twitch_username") utility.wfetl() return ud else: logger.warning(f"Unknown platform '{platform}' in lookup_user_by_name") utility.wfetl() return None def ping() -> str: """ Returns a dynamic, randomized uptime response. """ utility.wfstl() debug = False # Use function to retrieve correct startup time and calculate uptime elapsed = time.time() - globals.get_bot_start_time() uptime_str, uptime_s = utility.format_uptime(elapsed) # Define threshold categories thresholds = [600, 1800, 3600, 10800, 21600, 43200, 86400, 172800, 259200, 345600, 432000, 518400, 604800, 1209600, 2592000, 7776000, 15552000, 23328000, 31536000] # Find the highest matching threshold selected_threshold = max([t for t in thresholds if uptime_s >= t], default=600) # Get a random response from the dictionary response = utility.get_random_reply(dictionary_name="ping_replies", category=str(selected_threshold), uptime_str=uptime_str) if debug: print(f"Elapsed time: {elapsed}\nuptime_str: {uptime_str}\nuptime_s: {uptime_s}\nselected threshold: {selected_threshold}\nresponse: {response}") utility.wfetl() return response def greet(target_display_name: str, platform_name: str) -> str: """ Returns a greeting string for the given user displayname on a given platform. """ return f"Hello {target_display_name}, welcome to {platform_name}!" ###################### # Quotes ###################### def create_quotes_table(db_conn): """ Creates the 'quotes' table if it does not exist, with the columns: ID, QUOTE_TEXT, QUOTEE, QUOTE_CHANNEL, QUOTE_DATETIME, QUOTE_GAME, QUOTE_REMOVED, QUOTE_REMOVED_DATETIME Uses a slightly different CREATE statement depending on MariaDB vs SQLite. """ utility.wfstl() if not db_conn: logger.fatal("No database connection available to create quotes table!") utility.wfetl() return # Detect if this is SQLite or MariaDB db_name = str(type(db_conn)).lower() if 'sqlite3' in db_name: # SQLite create_table_sql = """ CREATE TABLE IF NOT EXISTS quotes ( ID INTEGER PRIMARY KEY AUTOINCREMENT, QUOTE_TEXT TEXT, QUOTEE TEXT, QUOTE_CHANNEL TEXT, QUOTE_DATETIME TEXT, QUOTE_GAME TEXT, QUOTE_REMOVED BOOLEAN DEFAULT 0, QUOTE_REMOVED_DATETIME TEXT ) """ else: # Assume MariaDB # Adjust column types as appropriate for your setup create_table_sql = """ CREATE TABLE IF NOT EXISTS quotes ( ID INT PRIMARY KEY AUTO_INCREMENT, QUOTE_TEXT TEXT, QUOTEE VARCHAR(100), QUOTE_CHANNEL VARCHAR(100), QUOTE_DATETIME DATETIME DEFAULT CURRENT_TIMESTAMP, QUOTE_GAME VARCHAR(200), QUOTE_REMOVED BOOLEAN DEFAULT FALSE, QUOTE_REMOVED_DATETIME DATETIME DEFAULT NULL ) """ db.run_db_operation(db_conn, "write", create_table_sql) utility.wfetl() def is_sqlite(db_conn): """ Helper function to determine if the database connection is SQLite. """ return 'sqlite3' in str(type(db_conn)).lower() async def handle_quote_command(db_conn, is_discord: bool, ctx, args, game_name=None): """ Core logic for !quote command, shared by both Discord and Twitch. - `db_conn`: your active DB connection - `is_discord`: True if this command is being called from Discord, False if from Twitch - `ctx`: the context object (discord.py ctx or twitchio context) - `args`: a list of arguments (e.g. ["add", "some quote text..."], ["remove", "3"], ["info", "3"], ["search", "foo", "bar"], or ["2"] etc.) - `game_name`: function(channel_name) -> str or None Behavior: 1) `!quote add some text here` -> Adds a new quote, stores channel=Discord or twitch channel name, game if twitch. 2) `!quote remove N` -> Mark quote #N as removed. 3) `!quote info N` -> Returns stored info about quote #N (with a Discord embed if applicable). 4) `!quote search [keywords]` -> Searches for the best matching non-removed quote based on the given keywords. 5) `!quote N` -> Retrieve quote #N, if not removed. 6) `!quote` (no args) -> Retrieve a random (not-removed) quote. 7) `!quote last/latest/newest` -> Retrieve the latest (most recent) non-removed quote. """ utility.wfstl() if callable(db_conn): db_conn = db_conn() # If no subcommand, treat as "random" if len(args) == 0: try: utility.wfetl() return await retrieve_random_quote(db_conn) except Exception as e: logger.error(f"handle_quote_command() failed to retrieve a random quote: {e}", exec_info=True) sub = args[0].lower() if sub == "add": # everything after "add" is the quote text quote_text = " ".join(args[1:]).strip() if not quote_text: utility.wfetl() return "Please provide the quote text after 'add'." try: utility.wfetl() return await add_new_quote(db_conn, is_discord, ctx, quote_text, game_name) except Exception as e: logger.error(f"handle_quote_command() failed to add a new quote: {e}", exec_info=True) elif sub == "remove": if len(args) < 2: utility.wfetl() return "Please specify which quote ID to remove." try: utility.wfetl() return await remove_quote(db_conn, is_discord, ctx, quote_id_str=args[1]) except Exception as e: logger.error(f"handle_quote_command() failed to remove a quote: {e}", exec_info=True) elif sub == "restore": if len(args) < 2: utility.wfetl() return "Please specify which quote ID to restore." try: utility.wfetl() return await restore_quote(db_conn, is_discord, ctx, quote_id_str=args[1]) except Exception as e: logger.error(f"handle_quote_command() failed to restore a quote: {e}", exec_info=True) elif sub == "info": if len(args) < 2: utility.wfetl() return "Please specify which quote ID to get info for." if not args[1].isdigit(): utility.wfetl() return f"'{args[1]}' is not a valid quote ID." quote_id = int(args[1]) try: utility.wfetl() return await retrieve_quote_info(db_conn, ctx, quote_id, is_discord) except Exception as e: logger.error(f"handle_quote_command() failed to retrieve quote info: {e}", exec_info=True) elif sub == "search": if len(args) < 2: utility.wfetl() return "Please provide keywords to search for." keywords = args[1:] try: utility.wfetl() return await search_quote(db_conn, keywords, is_discord, ctx) except Exception as e: logger.error(f"handle_quote_command() failed to process quote search: {e}", exec_info=True) elif sub in ["last", "latest", "newest"]: try: utility.wfetl() return await retrieve_latest_quote(db_conn) except Exception as e: logger.error(f"handle_quote_command() failed to retrieve latest quote: {e}", exec_info=True) else: # Possibly a quote ID if sub.isdigit(): quote_id = int(sub) try: utility.wfetl() return await retrieve_specific_quote(db_conn, ctx, quote_id, is_discord) except Exception as e: logger.error(f"handle_quote_command() failed to retrieve a specific quote: {e}", exec_info=True) else: # unrecognized subcommand => fallback to random try: utility.wfetl() return await retrieve_random_quote(db_conn) except Exception as e: logger.error(f"handle_quote_command() failed to retrieve a random quote: {e}", exec_info=True) async def add_new_quote(db_conn, is_discord, ctx, quote_text, game_name: str = None): """ Inserts a new quote with UUID instead of username. """ utility.wfstl() user_id = str(ctx.author.id) platform = "discord" if is_discord else "twitch" # Lookup UUID from users table user_data = db.lookup_user(db_conn, identifier=user_id, identifier_type=f"{platform}_user_id") if not user_data: logger.error(f"Could not find UUID for user {ctx.author.name} ({user_id}) on {platform}. Quote not saved.") utility.wfetl() return "Could not save quote. Your user data is missing from the system." user_uuid = user_data["uuid"] channel_name = "Discord" if is_discord else ctx.channel.name if is_discord or not game_name: game_name = None # 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) """ params = (quote_text, user_uuid, channel_name, game_name) result = db.run_db_operation(db_conn, "write", insert_sql, params) if result is not None: quote_id = get_max_quote_id(db_conn) logger.info(f"New quote added: {quote_text} ({quote_id})") utility.wfetl() return f"Successfully added quote #{quote_id}" else: utility.wfetl() 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. """ utility.wfstl() if not quote_id_str.isdigit(): utility.wfetl() return f"'{quote_id_str}' is not a valid quote ID." user_id = str(ctx.author.id) platform = "discord" if is_discord else "twitch" # Lookup UUID from users table user_data = db.lookup_user(db_conn, identifier=user_id, identifier_type=f"{platform}_user_id") if not user_data: logger.error(f"Could not find UUID for user {ctx.author.name} ({user_id}) on {platform}. Quote not removed.") utility.wfetl() return "Could not remove quote. Your user data is missing from the system." user_uuid = user_data["uuid"] quote_id = int(quote_id_str) remover_user = str(user_uuid) # Mark as removed and record removal datetime update_sql = """ UPDATE quotes SET QUOTE_REMOVED = 1, QUOTE_REMOVED_BY = ?, QUOTE_REMOVED_DATETIME = CURRENT_TIMESTAMP WHERE ID = ? AND QUOTE_REMOVED = 0 """ params = (remover_user, quote_id) rowcount = db.run_db_operation(db_conn, "update", update_sql, params) if rowcount and rowcount > 0: utility.wfetl() return f"Removed quote #{quote_id}." else: utility.wfetl() return "Could not remove that quote (maybe it's already removed or doesn't exist)." async def restore_quote(db_conn, is_discord: bool, ctx, quote_id_str): """ Marks a previously removed quote as unremoved. Updates the quote so that QUOTE_REMOVED is set to 0 and clears the QUOTE_REMOVED_BY and QUOTE_REMOVED_DATETIME fields. """ if not quote_id_str.isdigit(): return f"'{quote_id_str}' is not a valid quote ID." quote_id = int(quote_id_str) # Attempt to restore the quote by clearing its removal flags update_sql = """ UPDATE quotes SET QUOTE_REMOVED = 0, QUOTE_REMOVED_BY = NULL, QUOTE_REMOVED_DATETIME = NULL WHERE ID = ? AND QUOTE_REMOVED = 1 """ params = (quote_id,) rowcount = db.run_db_operation(db_conn, "update", update_sql, params) if rowcount and rowcount > 0: return f"Quote #{quote_id} has been restored." else: return "Could not restore that quote (perhaps it is not marked as removed or does not exist)." async def retrieve_specific_quote(db_conn, ctx, quote_id, is_discord): """ Retrieve a specific quote by ID, if not removed. If not found, or removed, inform user of the valid ID range (1 - {max_id}) If no quotes exist at all, say "No quotes are created yet." """ utility.wfstl() # First, see if we have any quotes at all max_id = get_max_quote_id(db_conn) if max_id < 1: utility.wfetl() return "No quotes are created yet." # Query for that specific quote select_sql = """ SELECT ID, QUOTE_TEXT, QUOTEE, QUOTE_CHANNEL, QUOTE_DATETIME, QUOTE_GAME, QUOTE_REMOVED, QUOTE_REMOVED_BY, QUOTE_REMOVED_DATETIME FROM quotes WHERE ID = ? """ rows = db.run_db_operation(db_conn, "read", select_sql, (quote_id,)) if not rows: # no match utility.wfetl() return f"I couldn't find that quote (1-{max_id})." row = rows[0] quote_number = row[0] quote_text = row[1] quotee = row[2] quote_channel = row[3] quote_datetime = row[4] quote_game = row[5] quote_removed = row[6] quote_removed_by = row[7] if row[7] else "Unknown" quote_removed_datetime = row[8] if row[8] else "Unknown" platform = "discord" if is_discord else "twitch" # Lookup UUID from users table for the quoter user_data = db.lookup_user(db_conn, identifier=quotee, identifier_type="UUID") if not user_data: logger.info(f"Could not find platform name for quotee UUID {quotee}. Default to 'Unknown'") quotee_display = "Unknown" else: quotee_display = user_data[f"{platform}_user_display_name"] if quote_removed == 1: # 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: logger.warning(f"Could not find platform name for remover UUID {quote_removed_by}. Default to 'Unknown'") quote_removed_by_display = "Unknown" else: quote_removed_by_display = removed_user_data[f"{platform}_user_display_name"] utility.wfetl() return f"Quote #{quote_number}: [REMOVED by {quote_removed_by_display} on {quote_removed_datetime}]" else: utility.wfetl() return f"Quote #{quote_number}: {quote_text}" async def retrieve_random_quote(db_conn): """ Grab a random quote (QUOTE_REMOVED=0). If no quotes exist or all removed, respond with "No quotes are created yet." """ utility.wfstl() # First check if we have any quotes max_id = get_max_quote_id(db_conn) if max_id < 1: utility.wfetl() return "No quotes are created yet." # We have quotes, try selecting a random one from the not-removed set if is_sqlite(db_conn): random_sql = """ SELECT ID, QUOTE_TEXT FROM quotes WHERE QUOTE_REMOVED = 0 ORDER BY RANDOM() LIMIT 1 """ else: # MariaDB uses RAND() random_sql = """ SELECT ID, QUOTE_TEXT FROM quotes WHERE QUOTE_REMOVED = 0 ORDER BY RAND() LIMIT 1 """ rows = db.run_db_operation(db_conn, "read", random_sql) if not rows: utility.wfetl() return "No quotes are created yet." quote_number, quote_text = rows[0] utility.wfetl() return f"Quote #{quote_number}: {quote_text}" async def retrieve_latest_quote(db_conn): """ Retrieve the latest (most recent) non-removed quote based on QUOTE_DATETIME. """ utility.wfstl() max_id = get_max_quote_id(db_conn) if max_id < 1: utility.wfetl() return "No quotes are created yet." latest_sql = """ SELECT ID, QUOTE_TEXT FROM quotes WHERE QUOTE_REMOVED = 0 ORDER BY QUOTE_DATETIME DESC LIMIT 1 """ rows = db.run_db_operation(db_conn, "read", latest_sql) if not rows: utility.wfetl() return "No quotes are created yet." quote_number, quote_text = rows[0] utility.wfetl() return f"Quote #{quote_number}: {quote_text}" async def retrieve_quote_info(db_conn, ctx, quote_id, is_discord): """ Retrieve the stored information about a specific quote (excluding the quote text itself). If called from Discord, returns a discord.Embed object with nicely formatted information. If not found, returns an appropriate error message. """ utility.wfstl() # First, check if any quotes exist max_id = get_max_quote_id(db_conn) if max_id < 1: utility.wfetl() return "No quotes are created yet." # Query for the specific quote by ID select_sql = """ SELECT ID, QUOTE_TEXT, QUOTEE, QUOTE_CHANNEL, QUOTE_DATETIME, QUOTE_GAME, QUOTE_REMOVED, QUOTE_REMOVED_BY, QUOTE_REMOVED_DATETIME FROM quotes WHERE ID = ? """ rows = db.run_db_operation(db_conn, "read", select_sql, (quote_id,)) if not rows: utility.wfetl() return f"I couldn't find that quote (1-{max_id})." row = rows[0] quote_number = row[0] quote_text = row[1] quotee = row[2] quote_channel = row[3] quote_datetime = row[4] quote_game = row[5] quote_removed = row[6] quote_removed_by = row[7] if row[7] else None quote_removed_datetime = row[8] if row[8] else None platform = "discord" if is_discord else "twitch" # Lookup display name for the quoter user_data = db.lookup_user(db_conn, identifier=quotee, identifier_type="UUID") if not user_data: logger.info(f"Could not find display name for quotee UUID {quotee}.") quotee_display = "Unknown" else: # 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}.") if quote_channel: info_lines.append(f"Channel: {quote_channel}") if quote_game: info_lines.append(f"Game: {quote_game}") if quote_removed == 1: # Lookup display name for the remover if quote_removed_by: removed_user_data = db.lookup_user(db_conn, identifier=quote_removed_by, identifier_type="UUID") if not removed_user_data: logger.info(f"Could not find display name for remover UUID {quote_removed_by}.") quote_removed_by_display = "Unknown" else: # 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}" if quote_removed_datetime: removed_info += f" on {quote_removed_datetime}" info_lines.append(removed_info) info_text = "\n ".join(info_lines) if is_discord: # Create a Discord embed try: from discord import Embed, Color except ImportError: # If discord.py is not available, fallback to plain text utility.wfetl() return info_text if quote_removed == 1: embed_color = Color.red() embed_title = f"Quote #{quote_number} Info [REMOVED]" else: embed_color = Color.blue() embed_title = f"Quote #{quote_number} Info" embed = Embed(title=embed_title, color=embed_color) embed.add_field(name="Quote", value=quote_text, inline=False) embed.add_field(name="Quoted by", value=quotee_display, inline=True) embed.add_field(name="Quoted on", value=quote_datetime, inline=True) if quote_channel: embed.add_field(name="Channel", value=quote_channel, inline=True) if quote_game: embed.add_field(name="Game", value=quote_game, inline=True) if quote_removed == 1: embed.add_field(name="Removed Info", value=removed_info, inline=False) utility.wfetl() return embed else: utility.wfetl() return info_text async def search_quote(db_conn, keywords, is_discord, ctx): """ Searches for the best matching non-removed quote based on the given keywords. The search compares keywords (case-insensitive) to words in the quote text, game name, quotee's display name, and channel. For each keyword that matches any of these fields, the quote receives +1 point, and for each keyword that does not match, it loses 1 point. A whole word match counts more than a partial match. The quote with the highest score is returned. In case of several equally good matches, one is chosen at random. """ import re utility.wfstl() func_start = time.time() sql = "SELECT ID, QUOTE_TEXT, QUOTE_GAME, QUOTEE, QUOTE_CHANNEL FROM quotes WHERE QUOTE_REMOVED = 0" rows = db.run_db_operation(db_conn, "read", sql) if not rows: func_end = time.time() func_elapsed = utility.time_since(func_start, func_end, "s") dbg_lvl = "DEBUG" if func_elapsed[2] < 5 else "WARNING" logger.info(f"search_quote took {func_elapsed[0]}, {func_elapsed[1]} to complete", dbg_lvl) utility.wfetl() return "No quotes are created yet." best_score = None best_quotes = [] # Normalize keywords to lowercase for case-insensitive matching. lower_keywords = [kw.lower() for kw in keywords] for row in rows: quote_id = row[0] quote_text = row[1] or "" quote_game = row[2] or "" quotee = row[3] or "" quote_channel = row[4] or "" # Lookup display name for quotee using UUID. user_data = db.lookup_user(db_conn, identifier=quotee, identifier_type="UUID") if user_data: # Use display name or fallback to platform username quotee_display = user_data.get("platform_display_name", user_data.get("platform_username", "Unknown")) else: logger.info(f"Could not find display name for quotee UUID {quotee}.") 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 for kw in lower_keywords: keyword_score = 0 for field in (quote_text, quote_game, quotee_display, quote_channel): field_str = field or "" field_lower = field_str.lower() # Check for whole word match using regex word boundaries. if re.search(r'\b' + re.escape(kw) + r'\b', field_lower): keyword_score = 2 break # Whole word match found, no need to check further fields. elif kw in field_lower: keyword_score = max(keyword_score, 1) score_total += keyword_score # Apply penalty: subtract the number of keywords. final_score = score_total - len(lower_keywords) if best_score is None or final_score > best_score: best_score = final_score best_quotes = [(quote_id, quote_text)] elif final_score == best_score: best_quotes.append((quote_id, quote_text)) if not best_quotes: func_end = time.time() func_elapsed = utility.time_since(func_start, func_end) dbg_lvl = "DEBUG" if func_elapsed[2] < 5 else "WARNING" logger.info(f"search_quote took {func_elapsed[0]}, {func_elapsed[1]} to complete", dbg_lvl) utility.wfetl() return "No matching quotes found." chosen = random.choice(best_quotes) func_end = time.time() func_elapsed = utility.time_since(func_start, func_end, "s") dbg_lvl = "DEBUG" if func_elapsed[2] < 5 else "WARNING" logger.info(f"search_quote took {func_elapsed[0]}, {func_elapsed[1]} to complete", dbg_lvl) utility.wfetl() return f"Quote {chosen[0]}: {chosen[1]}" def get_max_quote_id(db_conn): """ Return the highest ID in the quotes table, or 0 if empty. """ utility.wfstl() sql = "SELECT MAX(ID) FROM quotes" rows = db.run_db_operation(db_conn, "read", sql) if rows and rows[0] and rows[0][0] is not None: utility.wfetl() return rows[0][0] utility.wfetl() return 0 def get_author_name(ctx, is_discord): """ Return the name/username of the command author. For Discord, it's ctx.author.display_name (or ctx.author.name). For Twitch (twitchio), it's ctx.author.name. """ utility.wfstl() if is_discord: utility.wfetl() return str(ctx.author.display_name) else: utility.wfetl() return str(ctx.author.name) def get_channel_name(ctx): """ Return the channel name for Twitch. For example, ctx.channel.name in twitchio. """ # In twitchio, ctx.channel has .name return str(ctx.channel.name) async def send_message(ctx, text): """ Minimal helper to send a message to either Discord or Twitch. For discord.py: await ctx.send(text) For twitchio: await ctx.send(text) """ await ctx.send(text) # Common backend function to get a random fun fact def get_fun_fact(keywords=None): """ If keywords is None or empty, returns a random fun fact. Otherwise, searches for the best matching fun fact in dictionary/funfacts.json. For each fun fact: - Awards 2 points for each keyword found as a whole word. - Awards 1 point for each keyword found as a partial match. - Subtracts 1 point for each keyword provided. In the event of a tie, one fun fact is chosen at random. """ with open('dictionary/funfacts.json', 'r') as f: facts = json.load(f) # If no keywords provided, return a random fact. if not keywords: return random.choice(facts) if len(keywords) < 2: return "If you want to search, please append the command with `search [keywords]` without brackets." keywords = keywords[1:] lower_keywords = [kw.lower() for kw in keywords] best_score = None best_facts = [] for fact in facts: score_total = 0 fact_lower = fact.lower() # For each keyword, check for whole word and partial matches. for kw in lower_keywords: if re.search(r'\b' + re.escape(kw) + r'\b', fact_lower): score_total += 2 elif kw in fact_lower: score_total += 1 # Apply penalty for each keyword. final_score = score_total - len(lower_keywords) if best_score is None or final_score > best_score: best_score = final_score best_facts = [fact] elif final_score == best_score: best_facts.append(fact) if not best_facts: return "No matching fun facts found." return random.choice(best_facts)