OokamiPupV2/cmd_common/common_commands.py

503 lines
18 KiB
Python

# cmd_common/common_commands.py
import random
import time
from modules import utility
import globals
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.
"""
# 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
return handle_howl_stats(ctx, platform, target_name)
else:
# normal usage => random generation
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).
"""
# 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 []
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.
"""
db_conn = ctx.bot.db_conn
log_func = ctx.bot.log
# random logic
howl_val = random.randint(0, 100)
# round to nearest 10 except 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
)
# find user in DB by ID
user_data = db.lookup_user(db_conn, log_func, identifier=author_id, identifier_type=platform)
if user_data:
db.insert_howl(db_conn, log_func, user_data["UUID"], howl_val)
else:
log_func(f"Could not find user by ID={author_id} on {platform}. Not storing howl.", "WARNING")
return reply
def handle_howl_stats(ctx, platform, target_name) -> str:
db_conn = ctx.bot.db_conn
log_func = ctx.bot.log
# Check if requesting global stats
if target_name in ("_COMMUNITY_", "all", "global", "community"):
stats = db.get_global_howl_stats(db_conn, log_func)
if not stats:
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"]
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 = lookup_user_by_name(db_conn, log_func, platform, target_name)
if not user_data:
return f"I don't know that user: {target_name}"
stats = db.get_howl_stats(db_conn, log_func, user_data["UUID"])
if not stats:
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"]
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, log_func, platform, name_str):
"""
Attempt to find a user by name on that platform, e.g. 'discord_username' or 'twitch_username'.
"""
# same logic as before
if platform == "discord":
ud = db.lookup_user(db_conn, log_func, name_str, "discord_user_display_name")
if ud:
return ud
ud = db.lookup_user(db_conn, log_func, name_str, "discord_username")
return ud
elif platform == "twitch":
ud = db.lookup_user(db_conn, log_func, name_str, "twitch_user_display_name")
if ud:
return ud
ud = db.lookup_user(db_conn, log_func, name_str, "twitch_username")
return ud
else:
log_func(f"Unknown platform {platform} in lookup_user_by_name", "WARNING")
return None
def ping() -> str:
"""
Returns a dynamic, randomized uptime response.
"""
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}")
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, log_func):
"""
Creates the 'quotes' table if it does not exist, with the columns:
ID, QUOTE_TEXT, QUOTEE, QUOTE_CHANNEL, QUOTE_DATETIME, QUOTE_GAME, QUOTE_REMOVED
Uses a slightly different CREATE statement depending on MariaDB vs SQLite.
"""
if not db_conn:
log_func("No database connection available to create quotes table!", "FATAL")
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
)
"""
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
)
"""
db.run_db_operation(db_conn, "write", create_table_sql, log_func=log_func)
async def handle_quote_command(db_conn, log_func, is_discord: bool, ctx, args, get_twitch_game_for_channel=None):
"""
Core logic for !quote command, shared by both Discord and Twitch.
- `db_conn`: your active DB connection
- `log_func`: your log(...) function
- `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.)
- `get_twitch_game_for_channel`: 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 N`
-> Retrieve quote #N, if not removed.
4) `!quote` (no args)
-> Retrieve a random (not-removed) quote.
"""
# If no subcommand, treat as "random"
if len(args) == 0:
return await retrieve_random_quote(db_conn, log_func, is_discord, ctx)
sub = args[0].lower()
if sub == "add":
# everything after "add" is the quote text
quote_text = " ".join(args[1:]).strip()
if not quote_text:
return await send_message(ctx, "Please provide the quote text after 'add'.")
await add_new_quote(db_conn, log_func, is_discord, ctx, quote_text, get_twitch_game_for_channel)
elif sub == "remove":
if len(args) < 2:
return await send_message(ctx, "Please specify which quote ID to remove.")
await remove_quote(db_conn, log_func, is_discord, ctx, quote_id_str=args[1])
else:
# Possibly a quote ID
if sub.isdigit():
quote_id = int(sub)
await retrieve_specific_quote(db_conn, log_func, ctx, quote_id, is_discord)
else:
# unrecognized subcommand => fallback to random
await retrieve_random_quote(db_conn, log_func, is_discord, ctx)
async def add_new_quote(db_conn, log_func, is_discord, ctx, quote_text, get_twitch_game_for_channel):
"""
Inserts a new quote with UUID instead of username.
"""
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, log_func, identifier=user_id, identifier_type=f"{platform}_user_id")
if not user_data:
log_func(f"ERROR: Could not find UUID for user {ctx.author.name} ({user_id}) on {platform}. Quote not saved.", "ERROR")
await ctx.send("Could not save quote. Your user data is missing from the system.")
return
user_uuid = user_data["UUID"]
channel_name = "Discord" if is_discord else ctx.channel.name
game_name = None
if not is_discord and get_twitch_game_for_channel:
game_name = get_twitch_game_for_channel(channel_name) # Retrieve game if Twitch
# Insert quote
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, log_func=log_func)
if result is not None:
await ctx.send("Quote added successfully!")
else:
await ctx.send("Failed to add quote.")
async def remove_quote(db_conn, log_func, is_discord: bool, ctx, quote_id_str):
"""
Mark quote #ID as removed (QUOTE_REMOVED=1).
"""
if not quote_id_str.isdigit():
return await send_message(ctx, 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, log_func, identifier=user_id, identifier_type=f"{platform}_user_id")
if not user_data:
log_func(f"ERROR: Could not find UUID for user {ctx.author.name} ({user_id}) on {platform}. Quote not removed.", "ERROR")
await ctx.send("Could not remove quote. Your user data is missing from the system.")
return
user_uuid = user_data["UUID"]
quote_id = int(quote_id_str)
remover_user = str(user_uuid)
# Mark as removed
update_sql = """
UPDATE quotes
SET QUOTE_REMOVED = 1,
QUOTE_REMOVED_BY = ?
WHERE ID = ?
AND QUOTE_REMOVED = 0
"""
params = (remover_user, quote_id)
rowcount = db.run_db_operation(db_conn, "update", update_sql, params, log_func=log_func)
if rowcount and rowcount > 0:
await send_message(ctx, f"Removed quote #{quote_id}.")
else:
await send_message(ctx, "Could not remove that quote (maybe it's already removed or doesn't exist).")
async def retrieve_specific_quote(db_conn, log_func, 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."
"""
# First, see if we have any quotes at all
max_id = get_max_quote_id(db_conn, log_func)
if max_id < 1:
return await send_message(ctx, "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
FROM quotes
WHERE ID = ?
"""
rows = db.run_db_operation(db_conn, "read", select_sql, (quote_id,), log_func=log_func)
if not rows:
# no match
return await send_message(ctx, 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"
platform = "discord" if is_discord else "twitch"
# Lookup UUID from users table
user_data = db.lookup_user(db_conn, log_func, identifier=quotee, identifier_type="UUID")
if not user_data:
log_func(f"ERROR: Could not find platform name for remover UUID {quote_removed_by} on UUI. Default to 'Unknown'", "ERROR")
quote_removed_by = "Unknown"
else:
quote_removed_by = user_data[f"{platform}_user_display_name"]
if quote_removed == 1:
# It's removed
await send_message(ctx, f"Quote {quote_number}: [REMOVED by {quote_removed_by}]")
else:
# It's not removed
await send_message(ctx, f"Quote {quote_number}: {quote_text}")
async def retrieve_random_quote(db_conn, log_func, is_discord, ctx):
"""
Grab a random quote (QUOTE_REMOVED=0).
If no quotes exist or all removed, respond with "No quotes are created yet."
"""
# First check if we have any quotes
max_id = get_max_quote_id(db_conn, log_func)
if max_id < 1:
return await send_message(ctx, "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, log_func=log_func)
if not rows:
return await send_message(ctx, "No quotes are created yet.")
quote_number, quote_text = rows[0]
await send_message(ctx, f"Quote {quote_number}: {quote_text}")
def get_max_quote_id(db_conn, log_func):
"""
Return the highest ID in the quotes table, or 0 if empty.
"""
sql = "SELECT MAX(ID) FROM quotes"
rows = db.run_db_operation(db_conn, "read", sql, log_func=log_func)
if rows and rows[0] and rows[0][0] is not None:
return rows[0][0]
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.
"""
if is_discord:
return str(ctx.author.display_name)
else:
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)