From 11c9cf58a3fcd16cc8d6aa6da4f1f625eaef5891 Mon Sep 17 00:00:00 2001 From: w Date: Thu, 22 May 2025 23:26:03 -0300 Subject: [PATCH 01/13] Added table and general functions for stats system --- bot/db_utils.py | 106 ++++++++++++++++++++++++++++++++++++++++++++++++ db.py | 9 ++++ 2 files changed, 115 insertions(+) diff --git a/bot/db_utils.py b/bot/db_utils.py index bb80d98..62e7f15 100644 --- a/bot/db_utils.py +++ b/bot/db_utils.py @@ -4,12 +4,17 @@ import config DB_PATH = config.DB_PATH +# Database functions + def get_db_connection(): '''Creates a connection to the database''' conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row return conn + +# User functions + def get_or_create_user(username): '''Retrieves an ID for a given user, if the user does not exist, it will be created.''' @@ -32,6 +37,9 @@ def get_or_create_user(username): conn.close() return user_id + +# Gameplay functions + def add_pull(user_id, character_id): '''Creates a pull in the database''' conn = get_db_connection() @@ -40,6 +48,9 @@ def add_pull(user_id, character_id): conn.commit() conn.close() + +# Configuration + def get_config(key): '''Reads the value for a specified config key from the db''' conn = get_db_connection() @@ -56,3 +67,98 @@ def set_config(key, value): cur.execute("INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)", (key, value)) conn.commit() conn.close() + + +# Character stat functions + +def add_character_stats(character_id, stats): + ''' + Adds or updates character stats in the character_stats table. + `stats` should be a dictionary like {'power': 5, 'charm': 3} + ''' + if not stats: + return + + conn = get_db_connection() + cur = conn.cursor() + + columns = ', '.join(stats.keys()) + placeholders = ', '.join(['?'] * len(stats)) + updates = ', '.join([f"{col}=excluded.{col}" for col in stats.keys()]) + + values = list(stats.values()) + + sql = f''' + INSERT INTO character_stats (character_id, {columns}) + VALUES (?, {placeholders}) + ON CONFLICT(character_id) DO UPDATE SET {updates} + ''' + cur.execute(sql, [character_id] + values) + conn.commit() + conn.close() + + +def update_character_stat(character_id, stat_name, value): + '''Updates a single stat field for a character''' + conn = get_db_connection() + cur = conn.cursor() + cur.execute(f''' + UPDATE character_stats SET {stat_name} = ? WHERE character_id = ? + ''', (value, character_id)) + conn.commit() + conn.close() + +def get_character_stats(character_id): + '''Retrieves all stats for a single character dynamically''' + conn = get_db_connection() + conn.row_factory = sqlite3.Row # Enables dict-style access to rows + cur = conn.cursor() + cur.execute('SELECT * FROM character_stats WHERE character_id = ?', (character_id,)) + row = cur.fetchone() + conn.close() + + if row: + return {key: row[key] for key in row.keys() if key != 'character_id'} + else: + return {} + +def get_character_stat(character_id, stat_name): + '''Retrieves a single stat value for a character''' + if stat_name not in ('power', 'charm'): + raise ValueError("Invalid stat name") + conn = get_db_connection() + cur = conn.cursor() + cur.execute(f'SELECT {stat_name} FROM character_stats WHERE character_id = ?', (character_id,)) + row = cur.fetchone() + conn.close() + return row[0] if row else 0 + +def get_stats_for_multiple_characters(character_ids): + ''' + Retrieves stats for a list of character IDs. + Returns a dictionary of character_id -> {stat_name: value, ...} + ''' + if not character_ids: + return {} + + placeholders = ','.join('?' for _ in character_ids) + query = f''' + SELECT * + FROM character_stats + WHERE character_id IN ({placeholders}) + ''' + + conn = get_db_connection() + cur = conn.cursor() + cur.execute(query, character_ids) + rows = cur.fetchall() + col_names = [desc[0] for desc in cur.description] + conn.close() + + result = {} + for row in rows: + character_id = row[0] + stats = dict(zip(col_names[1:], row[1:])) # Skip character_id + result[character_id] = stats + + return result diff --git a/db.py b/db.py index 63d0b43..93d96ea 100644 --- a/db.py +++ b/db.py @@ -41,6 +41,15 @@ cursor.execute(""" ) """) +cursor.execute(''' +CREATE TABLE IF NOT EXISTS character_stats ( + character_id INTEGER PRIMARY KEY, + power INTEGER NOT NULL, + charm INTEGER NOT NULL, + FOREIGN KEY(character_id) REFERENCES characters(id) +) +''') + """ # Insert example characters into the database if they don't already exist characters = [ ('Murakami-san', 1, 0.35), From 237f17f40d6d6b2e2c5be0c59699ea1d71a9b2d1 Mon Sep 17 00:00:00 2001 From: VD15 Date: Fri, 23 May 2025 23:44:03 +0100 Subject: [PATCH 02/13] Add a roll timeout to the bot --- bot/add_character.py | 7 +-- bot/config.py | 24 ++++---- bot/db_utils.py | 10 +++- bot/response.py | 130 ++++++++++++++++++++++++++++++------------- example_config.ini | 36 ++++++------ 5 files changed, 133 insertions(+), 74 deletions(-) diff --git a/bot/add_character.py b/bot/add_character.py index 68528c3..aae3fb3 100644 --- a/bot/add_character.py +++ b/bot/add_character.py @@ -32,7 +32,7 @@ def add_character(name: str, rarity: int, weight: float, image_url: str) -> tupl raise ValueError("Image URL must be provided.") # Download image - response = requests.get(image_url, stream=True) + response = requests.get(image_url, stream=True, timeout=30) if response.status_code != 200: raise RuntimeError(f"Failed to download image from {image_url}") @@ -55,9 +55,6 @@ def add_character(name: str, rarity: int, weight: float, image_url: str) -> tupl character_id = cur.lastrowid return character_id, file_id - - except Exception as e: - raise finally: if 'conn' in locals(): - conn.close() \ No newline at end of file + conn.close() diff --git a/bot/config.py b/bot/config.py index 5b73661..643aeb1 100644 --- a/bot/config.py +++ b/bot/config.py @@ -4,22 +4,20 @@ config = configparser.ConfigParser() config.read('config.ini') # Username for the bot -USER = config['application']['BotUser'] - +USER = config['credentials']['User'] # API key for the bot -KEY = config['application']['ApiKey'] +KEY = config['credentials']['Token'] # Bot's Misskey instance URL -INSTANCE = config['application']['InstanceUrl'] - -# SQLite Database location -DB_PATH = config['application']['DatabaseLocation'] - -# Extra stuff for control of the bot +INSTANCE = config['credentials']['Instance'] # TODO: move this to db # Fedi handles in the traditional 'user@domain.tld' style, allows these users -# to use extra admin exclusive commands with the bot''' -ADMINS = config['application']['DefaultAdmins'] +# to use extra admin exclusive commands with the bot +ADMINS = config['application']['DefaultAdmins'] +# SQLite Database location +DB_PATH = config['application']['DatabaseLocation'] -NOTIFICATION_POLL_INTERVAL = int(config['application']['NotificationPollInterval']) -NOTIFICATION_BATCH_SIZE = int(config['application']['NotificationBatchSize']) +NOTIFICATION_POLL_INTERVAL = int(config['notification']['PollInterval']) +NOTIFICATION_BATCH_SIZE = int(config['notification']['BatchSize']) + +GACHA_ROLL_INTERVAL = int(config['gacha']['RollInterval']) diff --git a/bot/db_utils.py b/bot/db_utils.py index 62e7f15..584856b 100644 --- a/bot/db_utils.py +++ b/bot/db_utils.py @@ -1,5 +1,4 @@ import sqlite3 -import random import config DB_PATH = config.DB_PATH @@ -48,6 +47,15 @@ def add_pull(user_id, character_id): conn.commit() conn.close() +def get_last_rolled_at(user_id): + '''Gets the timestamp when the user last rolled''' + conn = get_db_connection() + cur = conn.cursor() + cur.execute("SELECT timestamp FROM pulls WHERE user_id = ? ORDER BY timestamp DESC", \ + (user_id,)) + row = cur.fetchone() + conn.close() + return row[0] if row else None # Configuration diff --git a/bot/response.py b/bot/response.py index 2c58c43..b009344 100644 --- a/bot/response.py +++ b/bot/response.py @@ -1,6 +1,8 @@ import random -from db_utils import get_or_create_user, add_pull, get_db_connection +from datetime import datetime, timedelta, timezone +from db_utils import get_or_create_user, add_pull, get_db_connection, get_last_rolled_at from add_character import add_character +from config import GACHA_ROLL_INTERVAL def get_character(): ''' Gets a random character from the database''' @@ -18,54 +20,104 @@ def get_character(): return chosen['id'], chosen['name'], chosen['file_id'], chosen['rarity'] +def do_roll(full_user): + '''Determines whether the user can roll, then pulls a random character''' + user_id = get_or_create_user(full_user) + + # Get date of user's last roll + date = get_last_rolled_at(user_id) + + # No date means it's users first roll + if date: + # SQLite timestamps returned by the DB are always in UTC + # Below timestamps are to be converted to UTC + prev = datetime.strptime(date + '+0000', '%Y-%m-%d %H:%M:%S%z') + now = datetime.now(timezone.utc) + + time_since_last_roll = now - prev + roll_interval = timedelta(seconds=GACHA_ROLL_INTERVAL) + duration = roll_interval - time_since_last_roll + + # User needs to wait before they can roll again + if time_since_last_roll < roll_interval: + remaining_duration = None + if duration.seconds > 3600: + remaining_duration = f'{-(duration.seconds // -3600)} hours' + elif duration.seconds > 60: + remaining_duration = f'{-(duration.seconds // -60)} minutes' + else: + remaining_duration = f'{duration.seconds} seconds' + + return f'{full_user} ⏱️ Please wait another {remaining_duration} before rolling again.' + + character_id, character_name, file_id, rarity = get_character() + + if not character_id: + return f'{full_user} Uwaaa... something went wrong! No characters found. 😿' + + add_pull(user_id,character_id) + stars = '⭐️' * rarity + return([f"@{full_user} 🎲 Congrats! You rolled {stars} **{character_name}**\n\ + She's all yours now~ 💖✨",[file_id]]) + def is_float(val): + '''Returns true if `val` can be converted to a float''' try: float(val) return True except ValueError: return False +def do_create(full_user, arguments, note_obj): + '''Creates a character''' + # Example call from bot logic + image_url = note_obj.get('files', [{}])[0].get('url') if note_obj.get('files') else None + if not image_url: + return f'{full_user}{full_user} You need an image to create a character, dumbass.' + + if len(arguments) != 3: + return '{full_user}Please specify the following attributes in order: \ + name, rarity, drop weighting' + + if not (arguments[1].isnumeric() and 1 <= int(arguments[1]) <= 5): + return f'{full_user}Invalid rarity: \'{arguments[1]}\' must be a number between 1 and 5' + + if not (is_float(arguments[2]) and 0.0 < float(arguments[2]) <= 1.0): + return f'{full_user}Invalid drop weight: \'{arguments[2]}\' \ + must be a decimal value between 0.0 and 1.0' + + character_id, file_id = add_character( + name=arguments[0], + rarity=int(arguments[1]), + weight=float(arguments[2]), + image_url=image_url + ) + return([f'{full_user}Added {arguments[0]}, ID {character_id}.',[file_id]]) + + +def do_help(full_user): + '''Provides a list of commands that the bot can do.''' + return f'{full_user} Here\'s what I can do:\n \ + - `roll` Pulls a random character.\ + - `create ` Creates a character using a given image.\ + - `help` Shows this message' + +def do_invalid_command(command, full_user): + '''Generic response when an unknown or invalid command is sent''' + return f'{full_user} Unrecognised command: {command}\n\ + Message \'help\' to get a list of valid commands' def generate_response(parsed_command): - '''Given a command with arguments, processes the game state and returns a response''' command, full_user, arguments, note_obj = parsed_command - - if command == "roll": - user_id = get_or_create_user(full_user) - character_id, character_name, file_id, rarity = get_character() - - if not character_id: - #TODO: Can't have tuples of a single element - # Return these as a dict or object instead. - return(f"@{full_user} Uwaaa... something went wrong! No characters found. 😿") - - add_pull(user_id,character_id) - stars = '⭐️' * rarity - return([f"@{full_user} 🎲 Congrats! You rolled {stars} **{character_name}**\nShe's all yours now~ 💖✨",[file_id]]) - - if command == "create": - # Example call from bot logic - image_url = note_obj.get("files", [{}])[0].get("url") if note_obj.get("files") else None - if not image_url: - return "You need an image to create a character, dumbass." - - if len(arguments) != 3: - return "Please specify the following attributes in order: name, rarity, drop weighting" - - if not (arguments[1].isnumeric() and 1 <= int(arguments[1]) <= 5): - return f"Invalid rarity: '{arguments[1]}' must be a number between 1 and 5" - - if not (is_float(arguments[2]) and 0.0 < float(arguments[2]) <= 1.0): - return f"Invalid drop weight: '{arguments[2]}' must be a decimal value between 0.0 and 1.0" - - character_id, file_id = add_character( - name=arguments[0], - rarity=int(arguments[1]), - weight=float(arguments[2]), - image_url=image_url - ) - return([f"Added {arguments[0]}, ID {character_id}.",[file_id]]) - return None + match command: + case 'roll': + return do_roll(full_user) + case 'create': + return do_create(full_user, arguments, note_obj) + case 'help': + return do_help(command) + case _: + return do_invalid_command(command, full_user) diff --git a/example_config.ini b/example_config.ini index 44cc2f9..d7f1c14 100644 --- a/example_config.ini +++ b/example_config.ini @@ -1,23 +1,27 @@ ; Rename me to config.ini and put your values in here [application] -; Full fedi handle of the bot user -BotUser = @bot@example.tld - -; API key for the bot -; Generate one by going to Settings > API > Generate access token -ApiKey = abcdefghijklmnopqrstuvwxyz012345 - -; Fully qualified URL of the instance hosting the bot -InstanceUrl = http://example.tld - ; Comma separated list of fedi handles for any administrator users -DefaultAdmins = ['admin@example.tld'] - +; More can be added through the application +DefaultAdmins = ['admin@example.tld'] ; SQLite Database location DatabaseLocation = ./gacha_game.db -; Number of seconds to sleep while awaiting new notifications -NotificationPollInterval = 5 +[gacha] +; Number of seconds players have to wait between rolls +RollInterval = 72000 + +[notification] +; Number of seconds to sleep while awaiting new notifications +PollInterval = 5 +; Number of notifications to process at once (max 100) +BatchSize = 10 + +[credentials] +; Fully qualified URL of the instance hosting the bot +Instance = http://example.tld +; Full fedi handle of the bot user +User = @bot@example.tld +; API key for the bot +; Generate one by going to Settings > API > Generate access token +Token = abcdefghijklmnopqrstuvwxyz012345 -; Number of notifications to process at once (limit 100) -NotificationBatchSize = 10 From 74e4c86d02f5a2ee83c0f116e21784abbc661f5d Mon Sep 17 00:00:00 2001 From: w Date: Thu, 22 May 2025 23:26:03 -0300 Subject: [PATCH 03/13] Added table and general functions for stats system --- bot/db_utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bot/db_utils.py b/bot/db_utils.py index 584856b..402d0af 100644 --- a/bot/db_utils.py +++ b/bot/db_utils.py @@ -59,6 +59,8 @@ def get_last_rolled_at(user_id): # Configuration +# Configuration + def get_config(key): '''Reads the value for a specified config key from the db''' conn = get_db_connection() From 3a2033a0254e9c7c80f5ff23938bc6370b736cb8 Mon Sep 17 00:00:00 2001 From: w Date: Sat, 24 May 2025 17:54:15 -0300 Subject: [PATCH 04/13] Add a roll timeout to the bot --- bot/db_utils.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/bot/db_utils.py b/bot/db_utils.py index 402d0af..584856b 100644 --- a/bot/db_utils.py +++ b/bot/db_utils.py @@ -59,8 +59,6 @@ def get_last_rolled_at(user_id): # Configuration -# Configuration - def get_config(key): '''Reads the value for a specified config key from the db''' conn = get_db_connection() From 3b7f2006ef3dea991f6910e49a487453fe5bacae Mon Sep 17 00:00:00 2001 From: w Date: Wed, 28 May 2025 23:44:59 -0300 Subject: [PATCH 05/13] unifying character and stats insertion, generalize get_character --- bot/db_utils.py | 86 +++++++++++-------------------------------------- 1 file changed, 18 insertions(+), 68 deletions(-) diff --git a/bot/db_utils.py b/bot/db_utils.py index 2d030cb..a236980 100644 --- a/bot/db_utils.py +++ b/bot/db_utils.py @@ -47,13 +47,27 @@ def get_or_create_user(username): user_id = CURSOR.lastrowid return user_id -def insert_character(name: str, rarity: int, weight: float, file_id: str) -> int: +def insert_character(name: str, rarity: int, weight: float, file_id: str, stats: dict) -> int: '''Inserts a character''' CURSOR.execute( 'INSERT INTO characters (name, rarity, weight, file_id) VALUES (?, ?, ?, ?)', (name, rarity, weight, file_id) ) character_id = CURSOR.lastrowid + + # Insert stats + columns = ', '.join(stats.keys()) + placeholders = ', '.join(['?'] * len(stats)) + updates = ', '.join([f"{col}=excluded.{col}" for col in stats.keys()]) + values = list(stats.values()) + + sql = f''' + INSERT INTO character_stats (character_id, {columns}) + VALUES (?, {placeholders}) + ON CONFLICT(character_id) DO UPDATE SET {updates} + ''' + CURSOR.execute(sql, [character_id] + values) + return character_id if character_id else 0 def insert_pull(user_id, character_id): @@ -85,69 +99,7 @@ def set_config(key, value): # Character stat functions -def add_character_stats(character_id, stats): - ''' - Adds or updates character stats in the character_stats table. - `stats` should be a dictionary like {'power': 5, 'charm': 3} - ''' - if not stats: - return - - conn = get_db_connection() - cur = conn.cursor() - - columns = ', '.join(stats.keys()) - placeholders = ', '.join(['?'] * len(stats)) - updates = ', '.join([f"{col}=excluded.{col}" for col in stats.keys()]) - - values = list(stats.values()) - - sql = f''' - INSERT INTO character_stats (character_id, {columns}) - VALUES (?, {placeholders}) - ON CONFLICT(character_id) DO UPDATE SET {updates} - ''' - cur.execute(sql, [character_id] + values) - conn.commit() - conn.close() - - -def update_character_stat(character_id, stat_name, value): - '''Updates a single stat field for a character''' - conn = get_db_connection() - cur = conn.cursor() - cur.execute(f''' - UPDATE character_stats SET {stat_name} = ? WHERE character_id = ? - ''', (value, character_id)) - conn.commit() - conn.close() - -def get_character_stats(character_id): - '''Retrieves all stats for a single character dynamically''' - conn = get_db_connection() - conn.row_factory = sqlite3.Row # Enables dict-style access to rows - cur = conn.cursor() - cur.execute('SELECT * FROM character_stats WHERE character_id = ?', (character_id,)) - row = cur.fetchone() - conn.close() - - if row: - return {key: row[key] for key in row.keys() if key != 'character_id'} - else: - return {} - -def get_character_stat(character_id, stat_name): - '''Retrieves a single stat value for a character''' - if stat_name not in ('power', 'charm'): - raise ValueError("Invalid stat name") - conn = get_db_connection() - cur = conn.cursor() - cur.execute(f'SELECT {stat_name} FROM character_stats WHERE character_id = ?', (character_id,)) - row = cur.fetchone() - conn.close() - return row[0] if row else 0 - -def get_stats_for_multiple_characters(character_ids): +def get_characters(character_ids): ''' Retrieves stats for a list of character IDs. Returns a dictionary of character_id -> {stat_name: value, ...} @@ -162,12 +114,10 @@ def get_stats_for_multiple_characters(character_ids): WHERE character_id IN ({placeholders}) ''' - conn = get_db_connection() - cur = conn.cursor() - cur.execute(query, character_ids) + + CURSOR.execute(query, character_ids) rows = cur.fetchall() col_names = [desc[0] for desc in cur.description] - conn.close() result = {} for row in rows: From 9d98299264ebfc0f1632938cb4ef5fdefc80abf4 Mon Sep 17 00:00:00 2001 From: w Date: Wed, 28 May 2025 23:52:04 -0300 Subject: [PATCH 06/13] expanding character_stats --- db.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/db.py b/db.py index 52596c4..3849481 100644 --- a/db.py +++ b/db.py @@ -44,8 +44,8 @@ cursor.execute(""" cursor.execute(''' CREATE TABLE IF NOT EXISTS character_stats ( character_id INTEGER PRIMARY KEY, - power INTEGER NOT NULL, - charm INTEGER NOT NULL, + power INTEGER NOT NULL DEFAULT abs(random() % 9999), + charm INTEGER NOT NULL DEFAULT abs(random() % 9999),, FOREIGN KEY(character_id) REFERENCES characters(id) ) ''') From 7b75a7eea9ac8f992369334b39a5cf0a917c0059 Mon Sep 17 00:00:00 2001 From: w Date: Sun, 22 Jun 2025 01:14:32 -0300 Subject: [PATCH 07/13] Refactor card_stats migration to add power, charm, and wit columns to the card table --- migrations/0006_card_stats.sql | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/migrations/0006_card_stats.sql b/migrations/0006_card_stats.sql index 5a67de8..7d6b4bd 100644 --- a/migrations/0006_card_stats.sql +++ b/migrations/0006_card_stats.sql @@ -16,9 +16,11 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see https://www.gnu.org/licenses/. */ -CREATE TABLE IF NOT EXISTS card_stats ( - card_id INTEGER PRIMARY KEY, - power INTEGER NOT NULL DEFAULT abs(random() % 9999), - charm INTEGER NOT NULL DEFAULT abs(random() % 9999),, - FOREIGN KEY(card_id) REFERENCES card(id) -) \ No newline at end of file +ALTER TABLE card + ADD COLUMN power INTEGER NOT NULL DEFAULT (abs(random() % 9999)); + +ALTER TABLE card + ADD COLUMN charm INTEGER NOT NULL DEFAULT (abs(random() % 9999)); + +ALTER TABLE card + ADD COLUMN wit INTEGER NOT NULL DEFAULT (abs(random() % 9999)); \ No newline at end of file From cce941471fe9c96f910998c50294350ea032c328 Mon Sep 17 00:00:00 2001 From: w Date: Sun, 22 Jun 2025 01:42:08 -0300 Subject: [PATCH 08/13] Enhance insert_card function to support optional power, charm, and wit parameters --- bot/db_utils.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/bot/db_utils.py b/bot/db_utils.py index e87def9..b8d00b1 100644 --- a/bot/db_utils.py +++ b/bot/db_utils.py @@ -166,12 +166,25 @@ def is_player_administrator(username: str) -> bool: def insert_card( - name: str, rarity: int, file_id: str) -> int: + name: str, rarity: int, file_id: str, + power: int =None, charm: int = None, wit: int = None) -> int: '''Inserts a card''' - CURSOR.execute( - 'INSERT INTO cards (name, rarity, file_id) VALUES (?, ?, ?)', - (name, rarity, file_id) - ) + if power is not None and charm is not None and wit is not None: + CURSOR.execute( + ''' + INSERT INTO card (name, rarity, file_id, power, charm, wit) + VALUES (?, ?, ?, ?, ?, ?) + ''', + (name, rarity, file_id, power, charm, wit) + ) + else: + CURSOR.execute( + ''' + INSERT INTO card (name, rarity, file_id) + VALUES (?, ?, ?) + ''', + (name, rarity, file_id) + ) card_id = CURSOR.lastrowid return card_id if card_id else 0 From 8c5c860ef6d77ea6c5748497f9b4d807cb4e066f Mon Sep 17 00:00:00 2001 From: w Date: Wed, 25 Jun 2025 00:21:37 -0300 Subject: [PATCH 09/13] get cards function --- bot/db_utils.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/bot/db_utils.py b/bot/db_utils.py index b8d00b1..933ec48 100644 --- a/bot/db_utils.py +++ b/bot/db_utils.py @@ -1,5 +1,5 @@ # Kemoverse - a gacha-style bot for the Fediverse. -# Copyright © 2025 Waifu VD15, Moon, and contributors. +# Copyright © 2025 Waifu, VD15, Moon, and contributors. # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -75,6 +75,19 @@ def get_random_card() -> Card | None: 'image_url': chosen['file_id'] } +def get_cards(card_ids: list[int]) -> list[tuple]: + ''' + Retrieves stats for a list of card IDs. + Returns a list of tuples: (id, name, rarity, file_id, power, charm, wit, ...) + ''' + if not card_ids: + return [] + + placeholders = ','.join('?' for _ in card_ids) + query = f'SELECT * FROM cards WHERE id IN ({placeholders})' + + CURSOR.execute(query, card_ids) + return CURSOR.fetchall() def get_player(username: str) -> int: '''Retrieve a player ID by username, or return None if not found.''' From ad17d33660e1d15f52081d1332abbff52a2108d6 Mon Sep 17 00:00:00 2001 From: w Date: Wed, 25 Jun 2025 00:26:11 -0300 Subject: [PATCH 10/13] Update card stats migration to set default values for power, charm, and wit columns to 0 --- migrations/0006_card_stats.sql | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/migrations/0006_card_stats.sql b/migrations/0006_card_stats.sql index 7d6b4bd..66e2d9b 100644 --- a/migrations/0006_card_stats.sql +++ b/migrations/0006_card_stats.sql @@ -17,10 +17,10 @@ along with this program. If not, see https://www.gnu.org/licenses/. */ ALTER TABLE card - ADD COLUMN power INTEGER NOT NULL DEFAULT (abs(random() % 9999)); + ADD COLUMN power INTEGER NOT NULL DEFAULT 0; ALTER TABLE card - ADD COLUMN charm INTEGER NOT NULL DEFAULT (abs(random() % 9999)); + ADD COLUMN charm INTEGER NOT NULL DEFAULT 0; ALTER TABLE card - ADD COLUMN wit INTEGER NOT NULL DEFAULT (abs(random() % 9999)); \ No newline at end of file + ADD COLUMN wit INTEGER NOT NULL DEFAULT 0; \ No newline at end of file From 23ffd55b7b30ab198c5de91d257fb65f21e4bdfe Mon Sep 17 00:00:00 2001 From: w Date: Wed, 25 Jun 2025 00:59:39 -0300 Subject: [PATCH 11/13] typo --- migrations/0006_card_stats.sql | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/migrations/0006_card_stats.sql b/migrations/0006_card_stats.sql index 66e2d9b..780cd75 100644 --- a/migrations/0006_card_stats.sql +++ b/migrations/0006_card_stats.sql @@ -16,11 +16,11 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see https://www.gnu.org/licenses/. */ -ALTER TABLE card +ALTER TABLE cards ADD COLUMN power INTEGER NOT NULL DEFAULT 0; -ALTER TABLE card +ALTER TABLE cards ADD COLUMN charm INTEGER NOT NULL DEFAULT 0; -ALTER TABLE card +ALTER TABLE cards ADD COLUMN wit INTEGER NOT NULL DEFAULT 0; \ No newline at end of file From 7c7d299fc4622c4790e0322c0e69d9e2508f581f Mon Sep 17 00:00:00 2001 From: w Date: Wed, 25 Jun 2025 01:11:37 -0300 Subject: [PATCH 12/13] Fix SQL table name in insert_card function and update argument validation for optional attributes --- bot/db_utils.py | 4 ++-- bot/response.py | 25 +++++++++++++++++++++++-- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/bot/db_utils.py b/bot/db_utils.py index 933ec48..45cac82 100644 --- a/bot/db_utils.py +++ b/bot/db_utils.py @@ -185,7 +185,7 @@ def insert_card( if power is not None and charm is not None and wit is not None: CURSOR.execute( ''' - INSERT INTO card (name, rarity, file_id, power, charm, wit) + INSERT INTO cards (name, rarity, file_id, power, charm, wit) VALUES (?, ?, ?, ?, ?, ?) ''', (name, rarity, file_id, power, charm, wit) @@ -193,7 +193,7 @@ def insert_card( else: CURSOR.execute( ''' - INSERT INTO card (name, rarity, file_id) + INSERT INTO cards (name, rarity, file_id) VALUES (?, ?, ?) ''', (name, rarity, file_id) diff --git a/bot/response.py b/bot/response.py index 93cf614..63b5d0c 100644 --- a/bot/response.py +++ b/bot/response.py @@ -123,10 +123,10 @@ dumbass.', 'attachment_urls': None } - if len(arguments) != 2: + if not(len(arguments) in (2,5)): return { 'message': f'{author} Please specify the following attributes \ -in order: name, rarity', +in order: name, rarity. Optionally add [power, charm, wit].', 'attachment_urls': None } @@ -137,6 +137,27 @@ be a number between 1 and 5', 'attachment_urls': None } + if len(arguments) == 2: + pass + else: + if not all(is_float(arg) for arg in arguments[2:]): + return { + 'message': f'{author} Invalid attributes: power, charm and \ +wit must be numbers.', + 'attachment_urls': None + } + card_id, file_id = add_card( + name=arguments[0], + rarity=int(arguments[1]), + image_url=image_url, + power=int(arguments[2]), + charm=int(arguments[3]), + wit=int(arguments[4]) + ) + return { + 'message': f'{author} Added {arguments[0]}, ID {card_id}.', + 'attachment_urls': [file_id] + } card_id, file_id = add_card( name=arguments[0], rarity=int(arguments[1]), From 91dfe77ffa6935dc3dabac8863a0cf94c755d01d Mon Sep 17 00:00:00 2001 From: w Date: Thu, 26 Jun 2025 00:37:47 -0300 Subject: [PATCH 13/13] Enhance add_card function to include power, charm, and wit parameters with validation checks --- bot/add_card.py | 13 ++++++++++--- bot/response.py | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/bot/add_card.py b/bot/add_card.py index def64f6..a914b2e 100644 --- a/bot/add_card.py +++ b/bot/add_card.py @@ -19,7 +19,7 @@ import config from fediverse_factory import get_fediverse_service import db_utils -def add_card(name: str, rarity: int, image_url: str) -> tuple[int, str]: +def add_card(name: str, rarity: int, image_url: str, power: int, charm: int, wit: int) -> tuple[int, str]: """ Adds a card to the database, uploading the image from a public URL to the Fediverse instance. @@ -27,6 +27,9 @@ def add_card(name: str, rarity: int, image_url: str) -> tuple[int, str]: name (str): Card name. rarity (int): Card rarity (e.g., 1-5). image_url (str): Public URL of the image from the post. + power (int): Card power value. + charm (int): Card charm value. + wit (int): Card wit value. Returns: tuple[int, str]: Card ID and file_id. @@ -47,7 +50,8 @@ def add_card(name: str, rarity: int, image_url: str) -> tuple[int, str]: raise ValueError(f'Invalid rarity: {rarity}') if not image_url: raise ValueError('Image URL must be provided.') - + if power < 0 or charm < 0 or wit < 0: + raise ValueError('Power, charm, and wit must be non-negative integers.') try: # Download image response = requests.get(image_url, stream=True, timeout=30) @@ -66,7 +70,10 @@ def add_card(name: str, rarity: int, image_url: str) -> tuple[int, str]: card_id = db_utils.insert_card( stripped_name, rarity, - file_id + file_id, + power, + charm, + wit ) return card_id, file_id diff --git a/bot/response.py b/bot/response.py index 63b5d0c..8ec698e 100644 --- a/bot/response.py +++ b/bot/response.py @@ -140,7 +140,7 @@ be a number between 1 and 5', if len(arguments) == 2: pass else: - if not all(is_float(arg) for arg in arguments[2:]): + if not all(arg.isnumeric() for arg in arguments[2:]): return { 'message': f'{author} Invalid attributes: power, charm and \ wit must be numbers.',