938 lines
34 KiB
Python
938 lines
34 KiB
Python
# cmd_common/common_commands.py
|
|
import random
|
|
import time
|
|
from modules import utility
|
|
import globals
|
|
import json
|
|
import re
|
|
|
|
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:
|
|
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
|
|
|
|
# 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:
|
|
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.
|
|
"""
|
|
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:
|
|
globals.log("No database connection available to create quotes table!", "FATAL")
|
|
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:
|
|
globals.log(f"handle_quote_command() failed to retrieve a random quote: {e}", "ERROR", 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:
|
|
globals.log(f"handle_quote_command() failed to add a new quote: {e}", "ERROR", 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:
|
|
globals.log(f"handle_quote_command() failed to remove a quote: {e}", "ERROR", 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:
|
|
globals.log(f"handle_quote_command() failed to restore a quote: {e}", "ERROR", 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:
|
|
globals.log(f"handle_quote_command() failed to retrieve quote info: {e}", "ERROR", 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:
|
|
globals.log(f"handle_quote_command() failed to process quote search: {e}", "ERROR", exec_info=True)
|
|
elif sub in ["last", "latest", "newest"]:
|
|
try:
|
|
utility.wfetl()
|
|
return await retrieve_latest_quote(db_conn)
|
|
except Exception as e:
|
|
globals.log(f"handle_quote_command() failed to retrieve latest quote: {e}", "ERROR", 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:
|
|
globals.log(f"handle_quote_command() failed to retrieve a specific quote: {e}", "ERROR", exec_info=True)
|
|
else:
|
|
# unrecognized subcommand => fallback to random
|
|
try:
|
|
utility.wfetl()
|
|
return await retrieve_random_quote(db_conn)
|
|
except Exception as e:
|
|
globals.log(f"handle_quote_command() failed to retrieve a random quote: {e}", "ERROR", 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:
|
|
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."
|
|
|
|
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)
|
|
globals.log(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:
|
|
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."
|
|
|
|
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:
|
|
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"]
|
|
|
|
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:
|
|
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"]
|
|
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:
|
|
globals.log(f"Could not find display name for quotee UUID {quotee}.", "INFO")
|
|
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:
|
|
globals.log(f"Could not find display name for remover UUID {quote_removed_by}.", "INFO")
|
|
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"
|
|
globals.log(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:
|
|
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
|
|
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"
|
|
globals.log(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"
|
|
globals.log(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) |