From 6da17449906018ad381d23023f1bafca203de3dc Mon Sep 17 00:00:00 2001 From: Kami Date: Fri, 14 Feb 2025 11:42:20 +0100 Subject: [PATCH] Added search feature to `!funfact` - Users can now search for fun facts using `!funfact search [keywords]` without brackets. Returns best match. - Also added some new facts - Minor tweaks to force-sync of Discord slash commands --- bot_discord.py | 44 ++++++++++++++++++------------- cmd_common/common_commands.py | 49 +++++++++++++++++++++++++++++++++-- cmd_discord.py | 7 ++--- cmd_twitch.py | 9 ++++--- config.json | 1 + dictionary/funfacts.json | 28 +++++++++++++++++--- globals.py | 14 +++++++--- 7 files changed, 119 insertions(+), 33 deletions(-) diff --git a/bot_discord.py b/bot_discord.py index e0ceaf7..0c26aa7 100644 --- a/bot_discord.py +++ b/bot_discord.py @@ -12,7 +12,7 @@ import modules.utility from modules.db import log_message, lookup_user, log_bot_event -primary_guild = globals.constants.primary_discord_guild()["object"] +primary_guild = globals.constants.primary_discord_guild() class DiscordBot(commands.Bot): def __init__(self): @@ -86,7 +86,13 @@ class DiscordBot(commands.Bot): await ctx.reply(_result) async def on_message(self, message): - globals.log(f"Message detected by '{message.author.name}' in '{message.author.guild.name}' - #'{message.channel.name}'", "DEBUG") + if message.guild: + guild_name = message.guild.name + channel_name = message.channel.name + 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"Attempting UUI lookup on '{message.author.name}' ...", "DEBUG") @@ -106,15 +112,15 @@ class DiscordBot(commands.Bot): user_is_bot=is_bot ) - globals.log(f"... UUI lookup complete", "DEBUG") - 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-{message.guild.name}" if message.guild else "discord-DM" + platform_str = f"discord-{guild_name}" # The channel name can be message.channel.name or "DM" if it's a private channel - channel_str = message.channel.name if hasattr(message.channel, "name") else "DM" + channel_str = channel_name # If you have attachments, you could gather them as links. try: @@ -183,20 +189,22 @@ class DiscordBot(commands.Bot): # Sync slash commands globally #await self.tree.sync() #globals.log("Discord slash commands synced.") - primary_guild_int = int(self.config["discord_guilds"][0]) num_guilds = len(self.config["discord_guilds"]) - cmd_tree_result = (await self.tree.sync(guild=primary_guild)) + cmd_tree_result = (await self.tree.sync(guild=primary_guild["object"])) command_names = [command.name for command in cmd_tree_result] if cmd_tree_result else None - try: - guild_info = await modules.utility.get_guild_info(self, primary_guild_int) - primary_guild_name = guild_info["name"] - except Exception as e: - primary_guild_name = f"{primary_guild_int} (id)" - globals.log(f"Guild lookup failed: {e}", "ERROR") - - _log_message = f"{num_guilds} guilds (global)" if num_guilds > 1 else f"guild: {primary_guild_name}" - globals.log(f"Discord slash commands force synced to {_log_message}") - globals.log(f"Discord slash commands that got synced: {command_names}") + if primary_guild["id"]: + try: + guild_info = await modules.utility.get_guild_info(self, primary_guild["id"]) + primary_guild_name = guild_info["name"] + except Exception as e: + primary_guild_name = f"{primary_guild["id"]}" + globals.log(f"Guild lookup failed: {e}", "ERROR") + + _log_message = f"{num_guilds} guilds (global)" if num_guilds > 1 else f"guild: {primary_guild_name}" + globals.log(f"Discord slash commands force synced to {_log_message}") + globals.log(f"Discord slash commands that got synced: {command_names}") + else: + globals.log("Discord commands synced globally.") except Exception as e: globals.log(f"Unable to sync Discord slash commands: {e}") diff --git a/cmd_common/common_commands.py b/cmd_common/common_commands.py index 5813ce3..55587e3 100644 --- a/cmd_common/common_commands.py +++ b/cmd_common/common_commands.py @@ -4,6 +4,7 @@ import time from modules import utility import globals import json +import re from modules import db @@ -873,7 +874,51 @@ async def send_message(ctx, text): await ctx.send(text) # Common backend function to get a random fun fact -def get_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) - return random.choice(facts) \ No newline at end of file + + # 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) \ No newline at end of file diff --git a/cmd_discord.py b/cmd_discord.py index 820cd5d..79068ad 100644 --- a/cmd_discord.py +++ b/cmd_discord.py @@ -35,9 +35,10 @@ def setup(bot): config_data = globals.load_config_file() @bot.command(name='funfact', aliases=['fun-fact']) - async def funfact_command(ctx): - fact = cc.get_fun_fact() - # Replies to the invoking user + async def funfact_command(ctx, *keywords): + # keywords is a tuple of strings from the command arguments. + fact = cc.get_fun_fact(list(keywords)) + # Reply to the invoking user. await ctx.reply(fact) diff --git a/cmd_twitch.py b/cmd_twitch.py index 1a26c31..327f6a6 100644 --- a/cmd_twitch.py +++ b/cmd_twitch.py @@ -15,10 +15,11 @@ def setup(bot, db_conn=None): """ @commands.command(name='funfact', aliases=['fun-fact']) - async def funfact_command(self, ctx: commands.Context): - fact = cc.get_fun_fact() - # Replies to the invoking user by prefixing their name - await ctx.reply(fact) + async def funfact_command(self, ctx: commands.Context, *keywords: str): + # Convert keywords tuple to list and pass to get_fun_fact. + fact = cc.get_fun_fact(list(keywords)) + # Reply to the invoking user by prefixing their name. + await ctx.reply(f"@{ctx.author.name} {fact}") @bot.command(name="greet") async def cmd_greet(ctx: commands.Context): diff --git a/config.json b/config.json index d6fa04b..d79befc 100644 --- a/config.json +++ b/config.json @@ -1,5 +1,6 @@ { "discord_guilds": [896713616089309184], + "sync_commands_globally": true, "twitch_channels": ["OokamiKunTV", "ookamipup"], "command_modules": ["cmd_discord", "cmd_twitch", "cmd_common"], "logging": { diff --git a/dictionary/funfacts.json b/dictionary/funfacts.json index 511cea1..9b62c9d 100644 --- a/dictionary/funfacts.json +++ b/dictionary/funfacts.json @@ -506,6 +506,28 @@ "The historical miniaturization of transistors has been a key driver in the rapid evolution of computing hardware.", "Advances in image processing now enable computers to interpret and analyze visual data with remarkable precision.", "Some nuclear power plants use naturally sourced water for cooling, challenging the notion that all require artificial systems.", - "Webster Lake, aka 'Chargoggagoggmanchauggagoggchaubunagungamaugg', can be translated to 'You fish on your side, I'll fish on my side, and no one shall fish in the middle.'" - ] - \ No newline at end of file + "Webster Lake, aka 'Chargoggagoggmanchauggagoggchaubunagungamaugg', can be translated to 'You fish on your side, I'll fish on my side, and no one shall fish in the middle.'", + "Cows, along with other ruminants like sheep, deer and giraffes, have 4 'stomachs', or compartments within the stomach, called the rumen, the reticulum, the omasum and the abomasum.", + "It is proposed that cheese have existed for around 10.000 years, with the earliest archaelogical record being almost 8.000 years old.", + "Wolves have been observed exhibiting mourning behaviors, lingering near the remains of fallen pack members in a manner that suggests they experience grief.", + "Recent research reveals that the unique patterns in wolf howls serve as a vocal fingerprint, conveying detailed information about an individual's identity and emotional state.", + "Contrary to the classic 'alpha' myth, many wolf packs function as family units where the parents lead the group and even subordinate wolves may reproduce.", + "Wolves display remarkable flexibility in their hunting strategies, adapting their techniques based on the prey available and the terrain, demonstrating problem-solving abilities rarely attributed to wild carnivores.", + "In certain wolf populations, individuals have been seen sharing food with non-pack members, challenging the long-held view of wolves as strictly territorial and competitive.", + "Studies show that a wolf's sense of smell is so acute that it can detect prey from several miles away and even follow scent trails that are days old.", + "Cows can recognize and remember the faces of up to 50 individuals, both other cows and humans, indicating a surprisingly complex memory capacity.", + "Beyond their docile reputation, cows have been found to experience a range of emotions, displaying signs of depression when isolated and apparent joy when in the company of their close companions.", + "Research has demonstrated that cows can solve simple puzzles for a reward, challenging the stereotype that they are only passive grazers.", + "Cows communicate through a rich array of vocalizations and subtle body language, suggesting they convey complex emotional states that scientists are only beginning to decipher.", + "Cheese may have been discovered accidentally when milk was stored in containers lined with rennet-rich animal stomachs, causing it to curdle.", + "There are over 1,800 distinct varieties of cheese worldwide, far more than the few types most people are familiar with.", + "Aged cheeses contain virtually no lactose, which explains why many lactose-intolerant individuals can enjoy them without discomfort.", + "Cheese rolling, an annual event in Gloucestershire, England, is not just a quirky tradition but an ancient competitive sport with a history spanning centuries.", + "The characteristic holes in Swiss cheese, known as 'eyes,' are formed by gas bubbles released by bacteria during the fermentation process.", + "Pule cheese, made from the milk of Balkan donkeys, is one of the world's most expensive cheeses, costing over a thousand euros per kilo due to its rarity and labor-intensive production.", + "Cheese may have addictive qualities: during digestion, casomorphins are released from milk proteins, interacting with brain receptors in a way similar to opiates.", + "Some artisanal cheeses are aged in natural caves, where unique microclimates contribute to complex flavors that are difficult to replicate in modern facilities.", + "While Cheddar cheese is now a global staple, it originally came from the small English village of Cheddar, and its production methods have evolved dramatically over time.", + "Modern cheese production often uses vegetarian rennet, derived from microbial or plant sources, challenging the common belief that all cheese is made with animal-derived enzymes.", + "Cows are related to whales, as they both evolved from land-dwelling, even-toed ungulates, hence why a baby whale is called a 'calf'." + ] \ No newline at end of file diff --git a/globals.py b/globals.py index 51fdad4..f349260 100644 --- a/globals.py +++ b/globals.py @@ -214,10 +214,18 @@ class Constants: guild is defined; otherwise, `None`. - "id": The integer ID of the primary guild if available; otherwise, `None`. """ + # Checks for a True/False value in the config file to determine if commands sync should be global or single-guild + # If this is 'true' in config, it will + sync_commands_globally = config_data["sync_commands_globally"] primary_guild_object = None - primary_guild_int = int(config_data["discord_guilds"][0]) if len(config_data["discord_guilds"]) == 1 else None - if primary_guild_int: - primary_guild_object = discord.Object(id=primary_guild_int) + primary_guild_int = None + + if not sync_commands_globally: + log("Discord commands sync set to single-guild in config") + primary_guild_int = int(config_data["discord_guilds"][0]) if len(config_data["discord_guilds"]) > 0 else None + if primary_guild_int: + primary_guild_object = discord.Object(id=primary_guild_int) + return_dict = {"object": primary_guild_object, "id": primary_guild_int} return return_dict