From bc0f71d5bea799762651af8f7befa0d32534b64f Mon Sep 17 00:00:00 2001 From: Valkyrie Date: Sat, 17 May 2025 17:49:03 +0100 Subject: [PATCH 1/8] Cleanup parsing.py --- bot/parsing.py | 82 ++++++++++++++++++++++++++------------------------ 1 file changed, 42 insertions(+), 40 deletions(-) diff --git a/bot/parsing.py b/bot/parsing.py index 9c6cdcf..a4895b8 100644 --- a/bot/parsing.py +++ b/bot/parsing.py @@ -1,29 +1,28 @@ import random, re from gacha_response import gacha_response - def parse_notification(notification,client): + '''Oarses any notifications received by the bot and sends any commands to + gacha_response()''' - # We get the type of notification to filter the ones that we actually want to parse + # We get the type of notification to filter the ones that we actually want + # to parse notif_type = notification.get("type") - if not((notif_type == "mention") or (notif_type == "reply")): - - return # Ignora todo lo que no sea una mención - - # We want the visibility to be related to the type that was received (so if people don't want to dump a bunch of notes on home they don't have to) + if not notif_type in ('mention', 'reply'): + return # Ignore anything that isn't a mention + # We want the visibility to be related to the type that was received (so if + # people don't want to dump a bunch of notes on home they don't have to) visibility = notification["note"]["visibility"] - if visibility == "specified": - visibility = "specified" - else: + if visibility != "specified": visibility = "home" - - + # Get the full Activitypub ID of the user user = notification.get("user", {}) username = user.get("username", "unknown") host = user.get("host") + # Local users may not have a hostname attached full_user = f"@{username}" if not host else f"@{username}@{host}" note_obj = notification.get("note", {}) @@ -32,38 +31,41 @@ def parse_notification(notification,client): note = note_text.strip().lower() if note_text else "" + # Will want to move bot name to a config file or find a way to derermine + # AP ID at runtime waifumelon_variants = [ "@waifumelon", # simple mention "@waifumelon@mai.waifuism.life", # fully qualified ] - if any(variant in note for variant in waifumelon_variants): - # Find command and arguments after the mention - # Removes all mentions (regex = mentions that start with @ and may contain @domain) - cleaned_text = re.sub(r"@\w+(?:@\S+)?", "", note).strip() - parts = cleaned_text.split() - print(cleaned_text) - print(parts) + # Make sure the notification text explicitly mentions the bot + if not any(variant in note for variant in waifumelon_variants): + return - command = parts[0].lower() if parts else None - arguments = parts[1:] if len(parts) > 1 else [] - else: - command = None - if command: - response = gacha_response(command.lower(),full_user, arguments, note_obj) - if response: - if type(response) == str: - client.notes_create( - text=response, - reply_id=note_id, - visibility=visibility - ) - else: - client.notes_create( - text=response[0], - reply_id=note_id, - visibility=visibility, - file_ids=response[1] - #visible_user_ids=[] #todo: write actual visible users ids so pleromers can use the bot privately + # Find command and arguments after the mention + # Removes all mentions (regex = mentions that start with @ and may contain @domain) + cleaned_text = re.sub(r"@\w+(?:@\S+)?", "", note).strip() + parts = cleaned_text.split() + + command = parts[0].lower() if parts else None + arguments = parts[1:] if len(parts) > 1 else [] - ) \ No newline at end of file + # TODO: move response generation to a different function + response = gacha_response(command.lower(),full_user, arguments, note_obj) + if not response: + return + + if isinstance(response, str): + client.notes_create( + text=response, + reply_id=note_id, + visibility=visibility + ) + else: + client.notes_create( + text=response[0], + reply_id=note_id, + visibility=visibility, + file_ids=response[1] + #visible_user_ids=[] #todo: write actual visible users ids so pleromers can use the bot privately + ) -- 2.36.2 From 36f0d4a0d1232862ae1f4d49644864a815b1ac07 Mon Sep 17 00:00:00 2001 From: Valkyrie Date: Sat, 17 May 2025 18:01:12 +0100 Subject: [PATCH 2/8] Tidy up gacha_response.py --- bot/gacha_response.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/bot/gacha_response.py b/bot/gacha_response.py index 10f053c..0e12a99 100644 --- a/bot/gacha_response.py +++ b/bot/gacha_response.py @@ -3,6 +3,7 @@ from db_utils import get_or_create_user, add_pull, get_db_connection from add_character import add_character def get_character(): + ''' Gets a random character from the database''' conn = get_db_connection() cur = conn.cursor() cur.execute('SELECT * FROM characters') @@ -10,21 +11,25 @@ def get_character(): conn.close() if not characters: - return None, None + return None, None, None, None weights = [c['weight'] for c in characters] chosen = random.choices(characters, weights=weights, k=1)[0] return chosen['id'], chosen['name'], chosen['file_id'], chosen['rarity'] - +# TODO: See issue #3, separate command parsing from game logic. def gacha_response(command,full_user, arguments,note_obj): + '''Parses a given command with arguments, processes the game state and + returns a response''' 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) @@ -35,12 +40,13 @@ def gacha_response(command,full_user, arguments,note_obj): # 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.") + return "You need an image to create a character, dumbass." 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]]) \ No newline at end of file + ) + return([f"Added {arguments[0]}, ID {character_id}.",[file_id]]) + return None -- 2.36.2 From da0d1742ffd87231828d1e85ba66bf7a9d50563e Mon Sep 17 00:00:00 2001 From: VD15 Date: Sat, 17 May 2025 18:33:49 +0100 Subject: [PATCH 3/8] Import python gitignore from github --- .gitignore | 187 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 184 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 4d3cf3d..8f70349 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,185 @@ -/venv/* -gacha_game.db +# Byte-compiled / optimized / DLL files __pycache__/ -bot/client.py \ No newline at end of file +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Cursor +# Cursor is an AI-powered code editor.`.cursorignore` specifies files/directories to +# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data +# refer to https://docs.cursor.com/context/ignore-files +.cursorignore +.cursorindexingignore + +# Custom stuff +gacha_game.db +bot/client.py -- 2.36.2 From 9a4ed06d79f231b552106a1c880d30f8ef410a16 Mon Sep 17 00:00:00 2001 From: VD15 Date: Sat, 17 May 2025 18:58:12 +0100 Subject: [PATCH 4/8] cleanup db_utils.py --- bot/bot_app.py | 19 ++++++------------- bot/db_utils.py | 13 ++++++++++--- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/bot/bot_app.py b/bot/bot_app.py index 2159260..264c031 100644 --- a/bot/bot_app.py +++ b/bot/bot_app.py @@ -1,25 +1,15 @@ +import time import misskey -import os -import asyncio from parsing import parse_notification from db_utils import get_or_create_user, add_pull, get_config, set_config from client import client_connection # Initialize the Misskey client - -# Function to create a note when a user pulls a character in the gacha game - - -# Set the access token and instance URL (replace with your own values) - -# Initialize the Misskey client - client = client_connection() -import time - # Define your whitelist -whitelisted_instances = [] +# TODO: move to config +whitelisted_instances: list[str] = [] def stream_notifications(): print("Starting filtered notification stream...") @@ -28,6 +18,9 @@ def stream_notifications(): while True: try: + # May be able to mark notifications as read using misskey.py and + # filter them out here. This function also takes a since_id we + # could use as well notifications = client.i_notifications() if notifications: diff --git a/bot/db_utils.py b/bot/db_utils.py index 1da2ab7..c319488 100644 --- a/bot/db_utils.py +++ b/bot/db_utils.py @@ -1,22 +1,26 @@ import sqlite3 import random +# TODO: grab this from a config file instead DB_PATH = "./gacha_game.db" # Adjust if needed def get_db_connection(): + '''Creates a connection to the database''' conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row return conn def get_or_create_user(username): + '''Retrieves an ID for a given user, if the user does not exist, it will be + created.''' conn = get_db_connection() conn.row_factory = sqlite3.Row cur = conn.cursor() - cur.execute('SELECT * FROM users WHERE username = ?', (username,)) + cur.execute('SELECT id FROM users WHERE username = ?', (username,)) user = cur.fetchone() if user: conn.close() - return user['id'] + return user # New user starts with has_rolled = False cur.execute( @@ -29,6 +33,7 @@ def get_or_create_user(username): return user_id def add_pull(user_id, character_id): + '''Creates a pull in the database''' conn = get_db_connection() cur = conn.cursor() cur.execute('INSERT INTO pulls (user_id, character_id) VALUES (?, ?)', (user_id, character_id)) @@ -36,6 +41,7 @@ def add_pull(user_id, character_id): conn.close() def get_config(key): + '''Reads the value for a specified config key from the db''' conn = get_db_connection() cur = conn.cursor() cur.execute("SELECT value FROM config WHERE key = ?", (key,)) @@ -44,8 +50,9 @@ def get_config(key): return row[0] if row else None def set_config(key, value): + '''Writes the value for a specified config key to the db''' conn = get_db_connection() cur = conn.cursor() cur.execute("INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)", (key, value)) conn.commit() - conn.close() \ No newline at end of file + conn.close() -- 2.36.2 From d830b07e60d2a8865d9b6ce50ef43f71ef2b0e98 Mon Sep 17 00:00:00 2001 From: VD15 Date: Sat, 17 May 2025 19:05:34 +0100 Subject: [PATCH 5/8] Whoops, that's a tuple --- bot/db_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/db_utils.py b/bot/db_utils.py index c319488..aefe395 100644 --- a/bot/db_utils.py +++ b/bot/db_utils.py @@ -17,7 +17,7 @@ def get_or_create_user(username): conn.row_factory = sqlite3.Row cur = conn.cursor() cur.execute('SELECT id FROM users WHERE username = ?', (username,)) - user = cur.fetchone() + user = cur.fetchone()[0] if user: conn.close() return user -- 2.36.2 From 5cc2ea22675b08cfe625e2d79ae09116a066f55f Mon Sep 17 00:00:00 2001 From: VD15 Date: Sun, 18 May 2025 10:38:59 +0100 Subject: [PATCH 6/8] Move config to ini file --- .gitignore | 1 + bot/config.py | 27 +++++++++++++++++++-------- example_config.ini | 14 ++++++++++++++ 3 files changed, 34 insertions(+), 8 deletions(-) create mode 100644 example_config.ini diff --git a/.gitignore b/.gitignore index 388bdf0..960a84d 100644 --- a/.gitignore +++ b/.gitignore @@ -182,3 +182,4 @@ cython_debug/ # Custom stuff gacha_game.db +config.ini diff --git a/bot/config.py b/bot/config.py index 408cb53..475aab9 100644 --- a/bot/config.py +++ b/bot/config.py @@ -1,8 +1,19 @@ -# Essential for the bot to function -INSTANCE = "" # Bots Misskey instance's URL **with schema** -KEY = "" # API key for the bot - -# Extra stuff for control of the bot -ADMINS = [] # Fedi handles in the traditional 'user@domain.tld' style, - # allows these users to use extra admin exclusive commands - # with the bot +'''Essentials for the bot to function''' +import configparser +config = configparser.ConfigParser() +config.read('config.ini') + +# Username for the bot +USER = config['application']['BotUser'] + +# API key for the bot +KEY = config['application']['ApiKey'] +# Bot's Misskey instance URL +INSTANCE = config['application']['InstanceUrl'] + +# Extra stuff for control of the bot + +# 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'] diff --git a/example_config.ini b/example_config.ini new file mode 100644 index 0000000..fe18f06 --- /dev/null +++ b/example_config.ini @@ -0,0 +1,14 @@ +; 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'] -- 2.36.2 From d76a965e7508af5caeb7a1ac660751436384bc73 Mon Sep 17 00:00:00 2001 From: VD15 Date: Sun, 18 May 2025 10:39:21 +0100 Subject: [PATCH 7/8] Fix the broken stuff --- bot/bot_app.py | 3 ++- bot/db_utils.py | 4 ++-- bot/gacha_response.py | 19 ++++++++++++++++++- bot/parsing.py | 12 ++++++------ 4 files changed, 28 insertions(+), 10 deletions(-) diff --git a/bot/bot_app.py b/bot/bot_app.py index 264c031..5806478 100644 --- a/bot/bot_app.py +++ b/bot/bot_app.py @@ -1,4 +1,5 @@ import time +import traceback import misskey from parsing import parse_notification from db_utils import get_or_create_user, add_pull, get_config, set_config @@ -68,7 +69,7 @@ def stream_notifications(): time.sleep(5) except Exception as e: - print(f"Error: {e}") + print(f"An exception has occured: {e}\n{traceback.format_exc()}") time.sleep(5) diff --git a/bot/db_utils.py b/bot/db_utils.py index aefe395..a143f4a 100644 --- a/bot/db_utils.py +++ b/bot/db_utils.py @@ -17,10 +17,10 @@ def get_or_create_user(username): conn.row_factory = sqlite3.Row cur = conn.cursor() cur.execute('SELECT id FROM users WHERE username = ?', (username,)) - user = cur.fetchone()[0] + user = cur.fetchone() if user: conn.close() - return user + return user[0] # New user starts with has_rolled = False cur.execute( diff --git a/bot/gacha_response.py b/bot/gacha_response.py index 0e12a99..e703aff 100644 --- a/bot/gacha_response.py +++ b/bot/gacha_response.py @@ -18,6 +18,14 @@ def get_character(): return chosen['id'], chosen['name'], chosen['file_id'], chosen['rarity'] +def is_float(val): + try: + float(val) + return True + except ValueError: + return False + + # TODO: See issue #3, separate command parsing from game logic. def gacha_response(command,full_user, arguments,note_obj): '''Parses a given command with arguments, processes the game state and @@ -34,7 +42,7 @@ def gacha_response(command,full_user, arguments,note_obj): add_pull(user_id,character_id) stars = '⭐️' * rarity - return([f"@{full_user} 🎲 Congrats! You rolled **{character_name}** she's {stars} stars! She's all yours now~ 💖✨",[file_id]]) + 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 @@ -42,6 +50,15 @@ def gacha_response(command,full_user, arguments,note_obj): 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]), diff --git a/bot/parsing.py b/bot/parsing.py index a4895b8..e0cf652 100644 --- a/bot/parsing.py +++ b/bot/parsing.py @@ -1,4 +1,5 @@ import random, re +import config from gacha_response import gacha_response def parse_notification(notification,client): @@ -31,15 +32,14 @@ def parse_notification(notification,client): note = note_text.strip().lower() if note_text else "" - # Will want to move bot name to a config file or find a way to derermine - # AP ID at runtime - waifumelon_variants = [ - "@waifumelon", # simple mention - "@waifumelon@mai.waifuism.life", # fully qualified + # Check for both short and fully-qualified name mentions + username_variants = [ + config.USER, + f'@{config.USER.split('@')[1]}' ] # Make sure the notification text explicitly mentions the bot - if not any(variant in note for variant in waifumelon_variants): + if not any(variant in note for variant in username_variants): return # Find command and arguments after the mention -- 2.36.2 From 7a1c3c4005ca1444f83435e9e294e57b7a0a2b84 Mon Sep 17 00:00:00 2001 From: w Date: Sun, 18 May 2025 17:03:48 -0300 Subject: [PATCH 8/8] user split bugfix --- bot/parsing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/parsing.py b/bot/parsing.py index e0cf652..c17a276 100644 --- a/bot/parsing.py +++ b/bot/parsing.py @@ -35,7 +35,7 @@ def parse_notification(notification,client): # Check for both short and fully-qualified name mentions username_variants = [ config.USER, - f'@{config.USER.split('@')[1]}' + f'@{config.USER.split("@")[1]}' ] # Make sure the notification text explicitly mentions the bot -- 2.36.2