Big quote system overhaul
- Returned "quote" commands to textual commands type - Rewritten quote system backend to ensure stability - Added new quote subcommands: - `!quote search [keywords]` allow users to search for a quote using keywords, and returns the best match. If several equally good matches are found, returns one of them at random - `!quote info [quote_id]` allows users to see more info about a given quote. Grants more information on Discord - `!quote restore [quote_id]` allows users to restore a previously removed quote - `!quote last/latest/newest` allows users to get the newest quote - Added new quote features to Discord helpfile - Moved database init to globals.init_db_conn - Associated unlinked usernames now default to that of the other platform, appended with "({platform} unlinked)". This should ensure consistensy despite users not linking accounts in the UAL system - Added time_since(start, end, format) function to easily get formatted time differences, eg. for execution time reporting - Added "wfstl" and "wfetl" helper function under utility. Useful for debugging when a function starts and ends manually if needed. - Minor tweaks, corrections, bugfixeskami_dev
parent
71505b4de1
commit
1b141c10fb
|
@ -8,3 +8,4 @@ Test ?/
|
|||
permissions.json
|
||||
local_database.sqlite
|
||||
logo.*
|
||||
_SQL_PREFILL_QUERIES_
|
9
bots.py
9
bots.py
|
@ -38,14 +38,7 @@ async def main():
|
|||
# Log initial start
|
||||
globals.log("--------------- BOT STARTUP ---------------")
|
||||
# Before creating your DiscordBot/TwitchBot, initialize DB
|
||||
try:
|
||||
db_conn = db.init_db_connection(config_data)
|
||||
if not db_conn:
|
||||
# If we get None, it means FATAL. We might sys.exit(1) or handle it differently.
|
||||
globals.log("Terminating bot due to no DB connection.", "FATAL")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
globals.log(f"Unable to initialize database!: {e}", "FATAL")
|
||||
db_conn = globals.init_db_conn()
|
||||
|
||||
try: # Ensure FKs are enabled
|
||||
db.checkenable_db_fk(db_conn)
|
||||
|
|
|
@ -27,7 +27,7 @@ def handle_howl_command(ctx) -> str:
|
|||
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)
|
||||
|
@ -41,10 +41,12 @@ def handle_howl_command(ctx) -> str:
|
|||
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):
|
||||
|
@ -52,6 +54,7 @@ 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"
|
||||
|
@ -72,12 +75,14 @@ def extract_ctx_info(ctx):
|
|||
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
|
||||
|
@ -102,15 +107,18 @@ def handle_howl_normal(ctx, platform, author_id, author_display_name) -> str:
|
|||
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:
|
||||
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"]
|
||||
|
@ -119,6 +127,7 @@ def handle_howl_stats(ctx, platform, target_name) -> str:
|
|||
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"
|
||||
|
@ -128,16 +137,19 @@ def handle_howl_stats(ctx, platform, target_name) -> str:
|
|||
# Otherwise, lookup a single user
|
||||
user_data = lookup_user_by_name(db_conn, platform, target_name)
|
||||
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"])
|
||||
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})")
|
||||
|
||||
|
@ -146,21 +158,27 @@ 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'.
|
||||
"""
|
||||
utility.wfstl()
|
||||
# same logic as before
|
||||
if platform == "discord":
|
||||
ud = db.lookup_user(db_conn, name_str, "discord_user_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_user_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
|
||||
|
||||
|
||||
|
@ -168,6 +186,7 @@ def ping() -> str:
|
|||
"""
|
||||
Returns a dynamic, randomized uptime response.
|
||||
"""
|
||||
utility.wfstl()
|
||||
debug = False
|
||||
|
||||
# Use function to retrieve correct startup time and calculate uptime
|
||||
|
@ -187,6 +206,7 @@ def ping() -> 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:
|
||||
|
@ -202,11 +222,13 @@ def greet(target_display_name: str, platform_name: str) -> str:
|
|||
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
|
||||
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
|
||||
|
@ -221,7 +243,8 @@ def create_quotes_table(db_conn):
|
|||
QUOTE_CHANNEL TEXT,
|
||||
QUOTE_DATETIME TEXT,
|
||||
QUOTE_GAME TEXT,
|
||||
QUOTE_REMOVED BOOLEAN DEFAULT 0
|
||||
QUOTE_REMOVED BOOLEAN DEFAULT 0,
|
||||
QUOTE_REMOVED_DATETIME TEXT
|
||||
)
|
||||
"""
|
||||
else:
|
||||
|
@ -235,11 +258,20 @@ def create_quotes_table(db_conn):
|
|||
QUOTE_CHANNEL VARCHAR(100),
|
||||
QUOTE_DATETIME DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
QUOTE_GAME VARCHAR(200),
|
||||
QUOTE_REMOVED BOOLEAN DEFAULT FALSE
|
||||
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, get_twitch_game_for_channel=None):
|
||||
|
@ -248,7 +280,7 @@ async def handle_quote_command(db_conn, is_discord: bool, ctx, args, get_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..."] or ["remove", "3"] or ["2"] etc.)
|
||||
- `args`: a list of arguments (e.g. ["add", "some quote text..."], ["remove", "3"], ["info", "3"], ["search", "foo", "bar"], or ["2"] etc.)
|
||||
- `get_twitch_game_for_channel`: function(channel_name) -> str or None
|
||||
|
||||
Behavior:
|
||||
|
@ -256,14 +288,27 @@ async def handle_quote_command(db_conn, is_discord: bool, ctx, args, get_twitch_
|
|||
-> 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 N`
|
||||
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.
|
||||
4) `!quote` (no args)
|
||||
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:
|
||||
return await retrieve_random_quote(db_conn, is_discord, ctx)
|
||||
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()
|
||||
|
||||
|
@ -271,26 +316,83 @@ async def handle_quote_command(db_conn, is_discord: bool, ctx, args, get_twitch_
|
|||
# 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, get_twitch_game_for_channel)
|
||||
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
|
||||
return await retrieve_random_quote(db_conn, is_discord, ctx)
|
||||
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, get_twitch_game_for_channel):
|
||||
"""
|
||||
Inserts a new quote with UUID instead of username.
|
||||
"""
|
||||
utility.wfstl()
|
||||
user_id = str(ctx.author.id)
|
||||
platform = "discord" if is_discord else "twitch"
|
||||
|
||||
|
@ -298,6 +400,7 @@ async def add_new_quote(db_conn, is_discord, ctx, quote_text, get_twitch_game_fo
|
|||
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")
|
||||
utility.wfetl()
|
||||
return "Could not save quote. Your user data is missing from the system."
|
||||
|
||||
user_uuid = user_data["UUID"]
|
||||
|
@ -315,16 +418,22 @@ async def add_new_quote(db_conn, is_discord, ctx, quote_text, get_twitch_game_fo
|
|||
|
||||
result = db.run_db_operation(db_conn, "write", insert_sql, params)
|
||||
if result is not None:
|
||||
return "Quote added successfully!"
|
||||
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).
|
||||
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)
|
||||
|
@ -334,6 +443,7 @@ async def remove_quote(db_conn, is_discord: bool, ctx, quote_id_str):
|
|||
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")
|
||||
utility.wfetl()
|
||||
return "Could not remove quote. Your user data is missing from the system."
|
||||
|
||||
user_uuid = user_data["UUID"]
|
||||
|
@ -341,11 +451,12 @@ async def remove_quote(db_conn, is_discord: bool, ctx, quote_id_str):
|
|||
quote_id = int(quote_id_str)
|
||||
remover_user = str(user_uuid)
|
||||
|
||||
# Mark as removed
|
||||
# Mark as removed and record removal datetime
|
||||
update_sql = """
|
||||
UPDATE quotes
|
||||
SET QUOTE_REMOVED = 1,
|
||||
QUOTE_REMOVED_BY = ?
|
||||
QUOTE_REMOVED_BY = ?,
|
||||
QUOTE_REMOVED_DATETIME = CURRENT_TIMESTAMP
|
||||
WHERE ID = ?
|
||||
AND QUOTE_REMOVED = 0
|
||||
"""
|
||||
|
@ -353,10 +464,39 @@ async def remove_quote(db_conn, is_discord: bool, ctx, quote_id_str):
|
|||
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):
|
||||
"""
|
||||
|
@ -364,9 +504,11 @@ async def retrieve_specific_quote(db_conn, ctx, quote_id, is_discord):
|
|||
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
|
||||
|
@ -379,7 +521,8 @@ async def retrieve_specific_quote(db_conn, ctx, quote_id, is_discord):
|
|||
QUOTE_DATETIME,
|
||||
QUOTE_GAME,
|
||||
QUOTE_REMOVED,
|
||||
QUOTE_REMOVED_BY
|
||||
QUOTE_REMOVED_BY,
|
||||
QUOTE_REMOVED_DATETIME
|
||||
FROM quotes
|
||||
WHERE ID = ?
|
||||
"""
|
||||
|
@ -387,6 +530,7 @@ async def retrieve_specific_quote(db_conn, ctx, quote_id, is_discord):
|
|||
|
||||
if not rows:
|
||||
# no match
|
||||
utility.wfetl()
|
||||
return f"I couldn't find that quote (1-{max_id})."
|
||||
|
||||
row = rows[0]
|
||||
|
@ -398,33 +542,43 @@ async def retrieve_specific_quote(db_conn, ctx, quote_id, is_discord):
|
|||
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
|
||||
# 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 remover UUID {quote_removed_by} on UUI. Default to 'Unknown'", "ERROR")
|
||||
quote_removed_by = "Unknown"
|
||||
globals.log(f"ERROR: Could not find platform name for quotee UUID {quotee}. Default to 'Unknown'", "ERROR")
|
||||
quotee_display = "Unknown"
|
||||
else:
|
||||
quote_removed_by = user_data[f"{platform}_user_display_name"]
|
||||
quotee_display = user_data[f"{platform}_user_display_name"]
|
||||
|
||||
if quote_removed == 1:
|
||||
# It's removed
|
||||
return f"Quote {quote_number}: [REMOVED by {quote_removed_by}]"
|
||||
# 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")
|
||||
quote_removed_by_display = "Unknown"
|
||||
else:
|
||||
# It's not removed
|
||||
return f"Quote {quote_number}: {quote_text}"
|
||||
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, is_discord, ctx):
|
||||
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
|
||||
|
@ -448,36 +602,256 @@ async def retrieve_random_quote(db_conn, is_discord, ctx):
|
|||
|
||||
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]
|
||||
await f"Quote {quote_number}: {quote_text}"
|
||||
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"ERROR: Could not find display name for quotee UUID {quotee}.", "ERROR")
|
||||
quotee_display = "Unknown"
|
||||
else:
|
||||
quotee_display = user_data.get(f"{platform}_user_display_name", "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"ERROR: Could not find display name for remover UUID {quote_removed_by}.", "ERROR")
|
||||
quote_removed_by_display = "Unknown"
|
||||
else:
|
||||
quote_removed_by_display = removed_user_data.get(f"{platform}_user_display_name", "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:
|
||||
quotee_display = user_data.get(f"{'discord' if is_discord else 'twitch'}_user_display_name", "")
|
||||
else:
|
||||
quotee_display = ""
|
||||
|
||||
# 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 is_sqlite(db_conn):
|
||||
return 'sqlite3' in str(type(db_conn)).lower()
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ from modules.permissions import has_permission
|
|||
from modules.utility import handle_help_command
|
||||
import globals
|
||||
|
||||
def setup(bot, db_conn=None):
|
||||
def setup(bot):
|
||||
"""
|
||||
Attach commands to the Discord bot, store references to db/log.
|
||||
"""
|
||||
|
@ -168,84 +168,53 @@ def setup(bot, db_conn=None):
|
|||
# get_twitch_game_for_channel=None # None for Discord
|
||||
# )
|
||||
|
||||
@bot.hybrid_group(name="quote", description="Interact with the quotes system", with_app_command=True)
|
||||
async def cmd_quote(ctx, *, id: Optional[int] = None):
|
||||
@bot.command(name="quote")
|
||||
async def cmd_quote(ctx, *, arg_str: str = ""):
|
||||
"""
|
||||
Handles base quote commands.
|
||||
- `!quote` -> Fetch a random quote
|
||||
- `!quote <id>` -> Fetch a specific quote by ID
|
||||
- `/quote` or `/quote <id>` -> Works for slash commands
|
||||
Handles the !quote command with multiple subcommands.
|
||||
|
||||
Usage:
|
||||
- `!quote`
|
||||
-> Retrieves a random (non-removed) quote.
|
||||
- `!quote <number>`
|
||||
-> Retrieves the specific quote by ID.
|
||||
- `!quote add <quote text>`
|
||||
-> Adds a new quote and replies with its quote number.
|
||||
- `!quote remove <number>`
|
||||
-> Removes the specified quote.
|
||||
- `!quote restore <number>`
|
||||
-> Restores a previously removed quote.
|
||||
- `!quote info <number>`
|
||||
-> Displays stored information about the quote (as an embed on Discord).
|
||||
- `!quote search [keywords]`
|
||||
-> Searches for the best matching quote based on the provided keywords.
|
||||
- `!quote last/latest/newest`
|
||||
-> Retrieves the latest (most recent) non-removed quote.
|
||||
"""
|
||||
if not bot.db_conn:
|
||||
return await ctx.send("Database is unavailable, sorry.")
|
||||
|
||||
# If no query is provided, fetch a random quote.
|
||||
if not id:
|
||||
args = []
|
||||
else:
|
||||
args = id.split() # Split query into arguments
|
||||
if not globals.init_db_conn:
|
||||
await ctx.send("Database is unavailable, sorry.")
|
||||
return
|
||||
|
||||
# Parse the arguments from the message text
|
||||
args = arg_str.split() if arg_str else []
|
||||
globals.log(f"'quote' command initiated with arguments: {args}", "DEBUG")
|
||||
|
||||
result = await cc.handle_quote_command(
|
||||
db_conn=bot.db_conn,
|
||||
db_conn=globals.init_db_conn,
|
||||
is_discord=True,
|
||||
ctx=ctx,
|
||||
args=args,
|
||||
get_twitch_game_for_channel=None
|
||||
)
|
||||
|
||||
globals.log(f"'quote' result: {result}")
|
||||
globals.log(f"'quote' result: {result}", "DEBUG")
|
||||
|
||||
# If the result is a discord.Embed, send it as an embed; otherwise, send plain text.
|
||||
if hasattr(result, "to_dict"):
|
||||
await ctx.send(embed=result)
|
||||
else:
|
||||
await ctx.send(result)
|
||||
|
||||
@cmd_quote.command(name="add", description="Add a quote")
|
||||
async def cmd_quote_add(ctx, *, text: str):
|
||||
"""
|
||||
Usage:
|
||||
- `!quote add <text>`
|
||||
- `/quote add text:<your quote>` for slash commands.
|
||||
"""
|
||||
if not bot.db_conn:
|
||||
return await ctx.send("Database is unavailable, sorry.")
|
||||
|
||||
# Ensure text isn't empty
|
||||
if not text.strip():
|
||||
return await ctx.send("You must provide a quote text.")
|
||||
|
||||
args = ["add", text] # Properly format arguments
|
||||
|
||||
result = await cc.handle_quote_command(
|
||||
db_conn=bot.db_conn,
|
||||
is_discord=True,
|
||||
ctx=ctx,
|
||||
args=args,
|
||||
get_twitch_game_for_channel=None
|
||||
)
|
||||
|
||||
await ctx.send(result)
|
||||
|
||||
@cmd_quote.command(name="remove", description="Remove a quote by number")
|
||||
async def cmd_quote_remove(ctx, id: int):
|
||||
"""
|
||||
Usage:
|
||||
- `!quote remove <id>`
|
||||
- `/quote remove id:<quote number>`
|
||||
"""
|
||||
if not bot.db_conn:
|
||||
return await ctx.send("Database is unavailable, sorry.")
|
||||
|
||||
args = ["remove", str(id)] # Properly pass the ID as an argument
|
||||
|
||||
result = await cc.handle_quote_command(
|
||||
db_conn=bot.db_conn,
|
||||
is_discord=True,
|
||||
ctx=ctx,
|
||||
args=args,
|
||||
get_twitch_game_for_channel=None
|
||||
)
|
||||
|
||||
await ctx.send(result)
|
||||
|
||||
######################
|
||||
# The following log entry must be last in the file to verify commands loading as they should
|
||||
|
|
|
@ -14,29 +14,29 @@ def setup(bot, db_conn=None):
|
|||
We also attach the db_conn and log so the commands can use them.
|
||||
"""
|
||||
@bot.command(name="greet")
|
||||
async def cmd_greet(ctx):
|
||||
async def cmd_greet(ctx: commands.Context):
|
||||
result = cc.greet(ctx.author.display_name, "Twitch")
|
||||
await ctx.send(result)
|
||||
await ctx.reply(result)
|
||||
|
||||
@bot.command(name="ping")
|
||||
async def cmd_ping(ctx):
|
||||
async def cmd_ping(ctx: commands.Context):
|
||||
result = cc.ping()
|
||||
await ctx.send(result)
|
||||
await ctx.reply(result)
|
||||
|
||||
@bot.command(name="howl")
|
||||
async def cmd_howl(ctx):
|
||||
async def cmd_howl(ctx: commands.Context):
|
||||
response = cc.handle_howl_command(ctx)
|
||||
await ctx.send(response)
|
||||
await ctx.reply(response)
|
||||
|
||||
@bot.command(name="hi")
|
||||
async def cmd_hi(ctx):
|
||||
async def cmd_hi(ctx: commands.Context):
|
||||
user_id = str(ctx.author.id) # Twitch user ID
|
||||
user_roles = [role.lower() for role in ctx.author.badges.keys()] # "roles" from Twitch badges
|
||||
|
||||
if not has_permission("hi", user_id, user_roles, "twitch"):
|
||||
return await ctx.send("You don't have permission to use this command.")
|
||||
|
||||
await ctx.send("Hello there!")
|
||||
await ctx.reply("Hello there!")
|
||||
|
||||
# @bot.command(name="acc_link")
|
||||
# @monitor_cmds(bot.log)
|
||||
|
@ -73,26 +73,94 @@ def setup(bot, db_conn=None):
|
|||
# await ctx.send(f"✅ Successfully linked Discord user **{discord_user_id}** with Twitch account **{twitch_username}**.")
|
||||
|
||||
|
||||
# @bot.command(name="quote")
|
||||
# async def cmd_quote(ctx: commands.Context):
|
||||
# """
|
||||
# Handles the !quote command with multiple subcommands.
|
||||
|
||||
# Usage:
|
||||
# - !quote
|
||||
# -> Retrieves a random (non-removed) quote.
|
||||
# - !quote <number>
|
||||
# -> Retrieves the specific quote by ID.
|
||||
# - !quote add <quote text>
|
||||
# -> Adds a new quote and replies with its quote number.
|
||||
# - !quote remove <number>
|
||||
# -> Removes the specified quote.
|
||||
# - !quote info <number>
|
||||
# -> Displays stored information about the quote (as an embed on Discord).
|
||||
# - !quote search [keywords]
|
||||
# -> Searches for the best matching quote based on the provided keywords.
|
||||
# - !quote last/latest/newest
|
||||
# -> Retrieves the latest (most recent) non-removed quote.
|
||||
# """
|
||||
# if not bot.db_conn:
|
||||
# return await ctx.send("Database is unavailable, sorry.")
|
||||
|
||||
# parts = ctx.message.content.strip().split()
|
||||
# args = parts[1:] if len(parts) > 1 else []
|
||||
|
||||
# def get_twitch_game_for_channel(chan_name):
|
||||
# # Placeholder for your actual logic to fetch the current game
|
||||
# return "SomeGame"
|
||||
|
||||
# result = await cc.handle_quote_command(
|
||||
# db_conn=bot.db_conn,
|
||||
# is_discord=False,
|
||||
# ctx=ctx,
|
||||
# args=args,
|
||||
# get_twitch_game_for_channel=get_twitch_game_for_channel
|
||||
# )
|
||||
# await ctx.send(result)
|
||||
|
||||
@bot.command(name="quote")
|
||||
async def cmd_quote(ctx: commands.Context):
|
||||
if not bot.db_conn:
|
||||
return await ctx.send("Database is unavailable, sorry.")
|
||||
"""
|
||||
Handles the !quote command with multiple subcommands.
|
||||
|
||||
parts = ctx.message.content.strip().split()
|
||||
args = parts[1:] if len(parts) > 1 else []
|
||||
Usage:
|
||||
- !quote
|
||||
-> Retrieves a random (non-removed) quote.
|
||||
- !quote <number>
|
||||
-> Retrieves the specific quote by ID.
|
||||
- !quote add <quote text>
|
||||
-> Adds a new quote and replies with its quote number.
|
||||
- !quote remove <number>
|
||||
-> Removes the specified quote.
|
||||
- `!quote restore <number>`
|
||||
-> Restores a previously removed quote.
|
||||
- !quote info <number>
|
||||
-> Displays stored information about the quote (as an embed on Discord).
|
||||
- !quote search [keywords]
|
||||
-> Searches for the best matching quote based on the provided keywords.
|
||||
- !quote last/latest/newest
|
||||
-> Retrieves the latest (most recent) non-removed quote.
|
||||
"""
|
||||
if not globals.init_db_conn:
|
||||
await ctx.reply("Database is unavailable, sorry.")
|
||||
return
|
||||
|
||||
# Parse the arguments from the message text
|
||||
args = ctx.message.content.strip().split()
|
||||
args = args[1:] if args else []
|
||||
globals.log(f"'quote' command initiated with arguments: {args}", "DEBUG")
|
||||
globals.log(f"'quote' command message content: {ctx.message.content}", "DEBUG")
|
||||
|
||||
def get_twitch_game_for_channel(chan_name):
|
||||
# Placeholder for your actual logic to fetch the current game
|
||||
return "SomeGame"
|
||||
|
||||
result = await cc.handle_quote_command(
|
||||
db_conn=bot.db_conn,
|
||||
db_conn=globals.init_db_conn,
|
||||
is_discord=False,
|
||||
ctx=ctx,
|
||||
args=args,
|
||||
get_twitch_game_for_channel=get_twitch_game_for_channel
|
||||
)
|
||||
await ctx.send(result)
|
||||
|
||||
globals.log(f"'quote' result: {result}", "DEBUG")
|
||||
|
||||
await ctx.reply(result)
|
||||
|
||||
@bot.command(name="help")
|
||||
async def cmd_help(ctx):
|
||||
|
|
|
@ -9,8 +9,22 @@
|
|||
]
|
||||
},
|
||||
"quote": {
|
||||
"description": "Manage quotes (add, remove, fetch).",
|
||||
"description": "Manage quotes (add, remove, restore, fetch, info).",
|
||||
"subcommands": {
|
||||
"no subcommand": {
|
||||
"desc": "Fetch a random quote."
|
||||
},
|
||||
"[quote_number]": {
|
||||
"args": "[quote_number]",
|
||||
"desc": "Fetch a specific quote by ID."
|
||||
},
|
||||
"search": {
|
||||
"args": "[keywords]",
|
||||
"desc": "Search for a quote with keywords."
|
||||
},
|
||||
"last/latest/newest": {
|
||||
"desc": "Returns the newest quote."
|
||||
},
|
||||
"add": {
|
||||
"args": "[quote_text]",
|
||||
"desc": "Adds a new quote."
|
||||
|
@ -19,18 +33,23 @@
|
|||
"args": "[quote_number]",
|
||||
"desc": "Removes the specified quote by ID."
|
||||
},
|
||||
"[quote_number]": {
|
||||
"desc": "Fetch a specific quote by ID."
|
||||
"restore": {
|
||||
"args": "[quote_number]",
|
||||
"desc": "Restores the specified quote by ID."
|
||||
},
|
||||
"no subcommand": {
|
||||
"desc": "Fetch a random quote."
|
||||
"info": {
|
||||
"args": "[quote_number]",
|
||||
"desc": "Retrieves info about the specified quote."
|
||||
}
|
||||
},
|
||||
"examples": [
|
||||
"quote : Fetch a random quote",
|
||||
"quote 3 : Fetch quote #3",
|
||||
"quote search ookamikuntv : Search for quote containing 'ookamikuntv'",
|
||||
"quote add This is my new quote : Add a new quote",
|
||||
"quote remove 3 : Remove quote # 3",
|
||||
"quote 5 : Fetch quote # 5",
|
||||
"quote : Fetch a random quote"
|
||||
"quote remove 3 : Remove quote #3",
|
||||
"quote restore 3 : Restores quote #3",
|
||||
"quote info 3 : Gets info about quote #3"
|
||||
]
|
||||
},
|
||||
"ping": {
|
||||
|
|
22
globals.py
22
globals.py
|
@ -23,6 +23,9 @@ def load_config_file():
|
|||
print(f"Error parsing config.json: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Load configuration file
|
||||
config_data = load_config_file()
|
||||
|
||||
###############################
|
||||
# Simple Logging System
|
||||
###############################
|
||||
|
@ -42,9 +45,6 @@ def log(message, level="INFO", exec_info=False):
|
|||
See 'config.json' for disabling/enabling logging levels
|
||||
"""
|
||||
|
||||
# Load configuration file
|
||||
config_data = load_config_file()
|
||||
|
||||
# Initiate logfile
|
||||
lfp = config_data["logging"]["logfile_path"] # Log File Path
|
||||
clfp = f"cur_{lfp}" # Current Log File Path
|
||||
|
@ -99,9 +99,6 @@ def log(message, level="INFO", exec_info=False):
|
|||
sys.exit(1)
|
||||
|
||||
def reset_curlogfile():
|
||||
# Load configuration file
|
||||
config_data = load_config_file()
|
||||
|
||||
# Initiate logfile
|
||||
lfp = config_data["logging"]["logfile_path"] # Log File Path
|
||||
clfp = f"cur_{lfp}" # Current Log File Path
|
||||
|
@ -112,3 +109,16 @@ def reset_curlogfile():
|
|||
except Exception as e:
|
||||
#log(f"Failed to clear current-run logfile: {e}")
|
||||
pass
|
||||
|
||||
def init_db_conn():
|
||||
try:
|
||||
import modules.db
|
||||
db_conn = modules.db.init_db_connection(config_data)
|
||||
if not db_conn:
|
||||
# If we get None, it means FATAL. We might sys.exit(1) or handle it differently.
|
||||
log("Terminating bot due to no DB connection.", "FATAL")
|
||||
sys.exit(1)
|
||||
return db_conn
|
||||
except Exception as e:
|
||||
log(f"Unable to initialize database!: {e}", "FATAL")
|
||||
return None
|
|
@ -141,6 +141,16 @@ def run_db_operation(conn, operation, query, params=None):
|
|||
conn.commit()
|
||||
if globals.log:
|
||||
globals.log(f"DB operation '{operation}' committed.", "DEBUG")
|
||||
# If the query is an INSERT, return the last inserted row ID
|
||||
if query.strip().lower().startswith("insert"):
|
||||
try:
|
||||
return cursor.lastrowid
|
||||
except Exception as e:
|
||||
if globals.log:
|
||||
globals.log(f"Error retrieving lastrowid: {e}", "ERROR")
|
||||
return cursor.rowcount
|
||||
else:
|
||||
return cursor.rowcount
|
||||
|
||||
# If it's read/lookup, fetch results
|
||||
read_ops = ("read", "lookup", "select")
|
||||
|
@ -210,6 +220,7 @@ def ensure_quotes_table(db_conn):
|
|||
QUOTE_GAME TEXT,
|
||||
QUOTE_REMOVED BOOLEAN DEFAULT 0,
|
||||
QUOTE_REMOVED_BY TEXT,
|
||||
QUOTE_REMOVED_DATETIME TEXT DEFAULT NULL,
|
||||
FOREIGN KEY (QUOTEE) REFERENCES users(UUID),
|
||||
FOREIGN KEY (QUOTE_REMOVED_BY) REFERENCES users(UUID)
|
||||
)
|
||||
|
@ -225,6 +236,7 @@ def ensure_quotes_table(db_conn):
|
|||
QUOTE_GAME VARCHAR(200),
|
||||
QUOTE_REMOVED BOOLEAN DEFAULT FALSE,
|
||||
QUOTE_REMOVED_BY VARCHAR(100),
|
||||
QUOTE_REMOVED_DATETIME DATETIME DEFAULT NULL,
|
||||
FOREIGN KEY (QUOTEE) REFERENCES users(UUID) ON DELETE SET NULL
|
||||
FOREIGN KEY (QUOTE_REMOVED_BY) REFERENCES users(UUID) ON DELETE SET NULL
|
||||
)
|
||||
|
@ -418,11 +430,11 @@ def lookup_user(db_conn, identifier: str, identifier_type: str, target_identifie
|
|||
user_data = {
|
||||
"UUID": row[0],
|
||||
"discord_user_id": row[1],
|
||||
"discord_username": row[2],
|
||||
"discord_user_display_name": row[3],
|
||||
"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],
|
||||
"twitch_user_display_name": row[6],
|
||||
"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],
|
||||
|
@ -442,6 +454,17 @@ def lookup_user(db_conn, identifier: str, identifier_type: str, target_identifie
|
|||
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)"
|
||||
|
||||
if target_identifier in user_data:
|
||||
return user_data[target_identifier]
|
||||
else:
|
||||
|
|
|
@ -679,6 +679,83 @@ def generate_link_code():
|
|||
import random, string
|
||||
return ''.join(random.choices(string.ascii_uppercase + string.digits, k=8))
|
||||
|
||||
def time_since(start, end=None, format=None):
|
||||
"""
|
||||
Returns the epoch time since the start value.
|
||||
|
||||
:param start: The epoch time to check against.
|
||||
:param end: The epoch time to compare with.
|
||||
Defaults to current time.
|
||||
:param format: One of 's', 'm', 'h', or 'd' corresponding to seconds, minutes, hours, or days.
|
||||
Defaults to "s" (seconds).
|
||||
:return: A tuple (x, y, total_elapsed) where:
|
||||
- For "s": x is whole seconds and y is the remaining milliseconds.
|
||||
- For "m": x is whole minutes and y is the remaining seconds.
|
||||
- For "h": x is whole hours and y is the remaining minutes.
|
||||
- For "d": x is whole days and y is the remaining hours.
|
||||
- total_elapsed is the complete elapsed time in seconds.
|
||||
"""
|
||||
# Ensure a start time is provided.
|
||||
if start is None:
|
||||
globals.log("time_since() lacks a start value!", "ERROR")
|
||||
return None
|
||||
|
||||
# If no end time is provided, use the current time.
|
||||
if end is None:
|
||||
end = time.time()
|
||||
|
||||
# Default to seconds if format is not valid.
|
||||
if format not in ["s", "m", "h", "d"]:
|
||||
globals.log("time_since() has incorrect format string. Defaulting to 's'", "WARNING")
|
||||
format = "s"
|
||||
|
||||
# Compute the total elapsed time in seconds.
|
||||
since = end - start
|
||||
|
||||
# Break down the elapsed time according to the requested format.
|
||||
if format == "s":
|
||||
# Format as seconds: whole seconds and remainder in milliseconds.
|
||||
x = int(since)
|
||||
y = int((since - x) * 1000)
|
||||
x = str(x) + "s"
|
||||
y = str(y) + "ms"
|
||||
elif format == "m":
|
||||
# Format as minutes: whole minutes and remainder in seconds.
|
||||
x = int(since // 60)
|
||||
y = int(since % 60)
|
||||
x = str(x) + "m"
|
||||
y = str(y) + "s"
|
||||
elif format == "h":
|
||||
# Format as hours: whole hours and remainder in minutes.
|
||||
x = int(since // 3600)
|
||||
y = int((since % 3600) // 60)
|
||||
x = str(x) + "h"
|
||||
y = str(y) + "m"
|
||||
elif format == "d":
|
||||
# Format as days: whole days and remainder in hours.
|
||||
x = int(since // 86400)
|
||||
y = int((since % 86400) // 3600)
|
||||
x = str(x) + "d"
|
||||
y = str(y) + "h"
|
||||
|
||||
return (x, y, since)
|
||||
|
||||
def wfstl():
|
||||
"""
|
||||
Write Function Start To Log (debug)
|
||||
Writes the calling function to log under the DEBUG category.
|
||||
"""
|
||||
caller_function_name = inspect.currentframe().f_back.f_code.co_name
|
||||
globals.log(f"Function {caller_function_name} started processing")
|
||||
|
||||
def wfetl():
|
||||
"""
|
||||
Write Function End To Log (debug)
|
||||
Writes the calling function to log under the DEBUG category.
|
||||
"""
|
||||
caller_function_name = inspect.currentframe().f_back.f_code.co_name
|
||||
globals.log(f"Function {caller_function_name} finished processing")
|
||||
|
||||
|
||||
###############################################
|
||||
# Development Test Function (called upon start)
|
||||
|
|
Loading…
Reference in New Issue