commit
1b5cb61fe9
@ -0,0 +1,4 @@ |
||||
/venv/* |
||||
gacha_game.db |
||||
__pycache__/ |
||||
bot/client.py |
@ -0,0 +1,63 @@ |
||||
import requests |
||||
from misskey.exceptions import MisskeyAPIException |
||||
from client import client_connection |
||||
from db_utils import get_db_connection |
||||
|
||||
def add_character(name: str, rarity: int, weight: float, image_url: str) -> tuple[int, str]: |
||||
""" |
||||
Adds a character to the database, uploading the image from a public URL to the bot's Misskey Drive. |
||||
|
||||
Args: |
||||
name (str): Character name. |
||||
rarity (int): Character rarity (e.g., 1-5). |
||||
weight (float): Pull weight (e.g., 0.02). |
||||
image_url (str): Public URL of the image from the post (e.g., from note['files'][i]['url']). |
||||
|
||||
Returns: |
||||
tuple[int, str]: Character ID and bot's Drive file_id. |
||||
|
||||
Raises: |
||||
ValueError: If inputs are invalid. |
||||
RuntimeError: If image download/upload or database operation fails. |
||||
""" |
||||
try: |
||||
# Validate inputs |
||||
if not name or not name.strip(): |
||||
raise ValueError("Character name cannot be empty.") |
||||
if not isinstance(rarity, int) or rarity < 1: |
||||
raise ValueError("Rarity must be a positive integer.") |
||||
if not isinstance(weight, (int, float)) or weight <= 0: |
||||
raise ValueError("Weight must be a positive number.") |
||||
if not image_url: |
||||
raise ValueError("Image URL must be provided.") |
||||
|
||||
# Download image |
||||
response = requests.get(image_url, stream=True) |
||||
if response.status_code != 200: |
||||
raise RuntimeError(f"Failed to download image from {image_url}") |
||||
|
||||
# Upload to bot's Drive |
||||
mk = client_connection() |
||||
try: |
||||
media = mk.drive_files_create(response.raw) |
||||
file_id = media["id"] |
||||
except MisskeyAPIException as e: |
||||
raise RuntimeError(f"Failed to upload image to bot's Drive: {e}") from e |
||||
|
||||
# Insert into database |
||||
conn = get_db_connection() |
||||
cur = conn.cursor() |
||||
cur.execute( |
||||
'INSERT INTO characters (name, rarity, weight, file_id) VALUES (?, ?, ?, ?)', |
||||
(name.strip(), rarity, float(weight), file_id) |
||||
) |
||||
conn.commit() |
||||
character_id = cur.lastrowid |
||||
|
||||
return character_id, file_id |
||||
|
||||
except Exception as e: |
||||
raise |
||||
finally: |
||||
if 'conn' in locals(): |
||||
conn.close() |
@ -0,0 +1,51 @@ |
||||
import sqlite3 |
||||
import random |
||||
|
||||
DB_PATH = "./gacha_game.db" # Adjust if needed |
||||
|
||||
def get_db_connection(): |
||||
conn = sqlite3.connect(DB_PATH) |
||||
conn.row_factory = sqlite3.Row |
||||
return conn |
||||
|
||||
def get_or_create_user(username): |
||||
conn = get_db_connection() |
||||
conn.row_factory = sqlite3.Row |
||||
cur = conn.cursor() |
||||
cur.execute('SELECT * FROM users WHERE username = ?', (username,)) |
||||
user = cur.fetchone() |
||||
if user: |
||||
conn.close() |
||||
return user['id'] |
||||
|
||||
# New user starts with has_rolled = False |
||||
cur.execute( |
||||
'INSERT INTO users (username, has_rolled) VALUES (?, ?)', |
||||
(username, False) |
||||
) |
||||
conn.commit() |
||||
user_id = cur.lastrowid |
||||
conn.close() |
||||
return user_id |
||||
|
||||
def add_pull(user_id, character_id): |
||||
conn = get_db_connection() |
||||
cur = conn.cursor() |
||||
cur.execute('INSERT INTO pulls (user_id, character_id) VALUES (?, ?)', (user_id, character_id)) |
||||
conn.commit() |
||||
conn.close() |
||||
|
||||
def get_config(key): |
||||
conn = get_db_connection() |
||||
cur = conn.cursor() |
||||
cur.execute("SELECT value FROM config WHERE key = ?", (key,)) |
||||
row = cur.fetchone() |
||||
conn.close() |
||||
return row[0] if row else None |
||||
|
||||
def set_config(key, value): |
||||
conn = get_db_connection() |
||||
cur = conn.cursor() |
||||
cur.execute("INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)", (key, value)) |
||||
conn.commit() |
||||
conn.close() |
@ -0,0 +1,69 @@ |
||||
import random, re |
||||
from gacha_response import gacha_response |
||||
|
||||
|
||||
def parse_notification(notification,client): |
||||
|
||||
# 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) |
||||
|
||||
visibility = notification["note"]["visibility"] |
||||
if visibility == "specified": |
||||
visibility = "specified" |
||||
else: |
||||
visibility = "home" |
||||
|
||||
|
||||
|
||||
user = notification.get("user", {}) |
||||
username = user.get("username", "unknown") |
||||
host = user.get("host") |
||||
full_user = f"@{username}" if not host else f"@{username}@{host}" |
||||
|
||||
note_obj = notification.get("note", {}) |
||||
note_text = note_obj.get("text") |
||||
note_id = note_obj.get("id") |
||||
|
||||
note = note_text.strip().lower() if note_text else "" |
||||
|
||||
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) |
||||
|
||||
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 |
||||
|
||||
) |
@ -0,0 +1,61 @@ |
||||
import sqlite3 |
||||
|
||||
# Connect to SQLite database (or create it if it doesn't exist) |
||||
conn = sqlite3.connect('gacha_game.db') |
||||
cursor = conn.cursor() |
||||
|
||||
# Create tables |
||||
cursor.execute(''' |
||||
CREATE TABLE IF NOT EXISTS users ( |
||||
id INTEGER PRIMARY KEY AUTOINCREMENT, |
||||
username TEXT UNIQUE NOT NULL, |
||||
has_rolled BOOLEAN NOT NULL DEFAULT 0 |
||||
) |
||||
''') |
||||
|
||||
cursor.execute(''' |
||||
CREATE TABLE IF NOT EXISTS characters ( |
||||
id INTEGER PRIMARY KEY AUTOINCREMENT, |
||||
name TEXT NOT NULL, |
||||
rarity INTEGER NOT NULL, |
||||
weight REAL NOT NULL, |
||||
file_id TEXT NOT NULL |
||||
) |
||||
''') |
||||
|
||||
cursor.execute(''' |
||||
CREATE TABLE IF NOT EXISTS pulls ( |
||||
id INTEGER PRIMARY KEY AUTOINCREMENT, |
||||
user_id INTEGER, |
||||
character_id INTEGER, |
||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, |
||||
FOREIGN KEY (user_id) REFERENCES users(id), |
||||
FOREIGN KEY (character_id) REFERENCES characters(id) |
||||
) |
||||
''') |
||||
|
||||
cursor.execute(""" |
||||
CREATE TABLE IF NOT EXISTS config ( |
||||
key TEXT PRIMARY KEY, |
||||
value TEXT |
||||
) |
||||
""") |
||||
|
||||
""" # Insert example characters into the database if they don't already exist |
||||
characters = [ |
||||
('Murakami-san', 1, 0.35), |
||||
('Mastodon-kun', 2, 0.25), |
||||
('Pleroma-tan', 3, 0.2), |
||||
('Misskey-tan', 4, 0.15), |
||||
('Syuilo-mama', 5, 0.05) |
||||
] |
||||
|
||||
|
||||
cursor.executemany(''' |
||||
INSERT OR IGNORE INTO characters (name, rarity, weight) VALUES (?, ?, ?) |
||||
''', characters) |
||||
""" |
||||
|
||||
# Commit changes and close |
||||
conn.commit() |
||||
conn.close() |
@ -0,0 +1,46 @@ |
||||
import subprocess |
||||
import os |
||||
from watchdog.observers import Observer |
||||
from watchdog.events import FileSystemEventHandler |
||||
import time |
||||
|
||||
class ReloadHandler(FileSystemEventHandler): |
||||
def __init__(self, script_path): |
||||
self.script_path = script_path |
||||
self.process = None |
||||
self.last_modified = 0 |
||||
self.debounce_interval = 1 # Wait 1 second to avoid double triggers |
||||
self.start_process() |
||||
|
||||
def start_process(self): |
||||
if self.process: |
||||
self.process.terminate() |
||||
print(f"🔁 Starting {self.script_path}...") |
||||
self.process = subprocess.Popen(["python3", self.script_path]) |
||||
|
||||
def on_modified(self, event): |
||||
if event.src_path.endswith(".py"): |
||||
current_time = time.time() |
||||
if current_time - self.last_modified > self.debounce_interval: |
||||
print(f"Detected change in {event.src_path}") |
||||
self.start_process() |
||||
self.last_modified = current_time |
||||
|
||||
if __name__ == "__main__": |
||||
script_path = "bot/bot_app.py" |
||||
if not os.path.exists(script_path): |
||||
print(f"Error: {script_path} does not exist") |
||||
exit(1) |
||||
|
||||
event_handler = ReloadHandler(script_path) |
||||
observer = Observer() |
||||
observer.schedule(event_handler, path=".", recursive=True) |
||||
observer.start() |
||||
|
||||
try: |
||||
while True: |
||||
time.sleep(1) |
||||
except KeyboardInterrupt: |
||||
print("Stopping...") |
||||
observer.stop() |
||||
observer.join() |
@ -0,0 +1,55 @@ |
||||
General |
||||
|
||||
```mermaid |
||||
flowchart TD |
||||
|
||||
subgraph Player Interaction |
||||
A1[Misskey bot] |
||||
A2[Web] |
||||
end |
||||
|
||||
subgraph Misskey |
||||
B1[Misskey instance] |
||||
end |
||||
|
||||
subgraph Bot |
||||
C1[Bot core in Python] |
||||
C2[Notification parser] |
||||
C3[Gacha roll logic] |
||||
C4[Database interface] |
||||
C5[Misskey API poster] |
||||
end |
||||
|
||||
subgraph Website |
||||
D1[Flask backend] |
||||
D2[User account system] |
||||
D3[Image gallery] |
||||
end |
||||
|
||||
subgraph Backend |
||||
E1[Shared database] |
||||
E2[Virtual environment] |
||||
E3[Debian Linux server] |
||||
end |
||||
|
||||
A1 <-->|Send or receive mention| B1 |
||||
B1 -->|Send mention| C2 |
||||
C2 -->|Command and information| C3 |
||||
C3 <-->|Ask for command information and confirmed roll setting| C4 |
||||
C4 <--> E1 |
||||
C3 -->|Command result and info| C5 |
||||
C5 -->|Resulting Mention| B1 |
||||
|
||||
A2 --> D1 |
||||
D1 --> D2 |
||||
D1 --> D3 |
||||
D2 --> E1 |
||||
D3 --> E1 |
||||
|
||||
C1 --> E2 |
||||
D1 --> E2 |
||||
|
||||
E1 --> E3 |
||||
B1 --> E3 |
||||
|
||||
``` |
@ -0,0 +1,7 @@ |
||||
blinker==1.9.0 |
||||
click==8.1.8 |
||||
Flask==3.1.0 |
||||
itsdangerous==2.2.0 |
||||
Jinja2==3.1.6 |
||||
MarkupSafe==3.0.2 |
||||
Werkzeug==3.1.3 |
@ -0,0 +1,27 @@ |
||||
#!/bin/bash |
||||
|
||||
# Navigate to the project directory (optional) |
||||
cd "$(dirname "$0")" |
||||
|
||||
# Activate virtual environment |
||||
source venv/bin/activate |
||||
|
||||
# Start the bot |
||||
echo "Starting bot..." |
||||
python3 bot/bot_app.py & |
||||
|
||||
# Save the bot PID so you can stop it later if needed |
||||
BOT_PID=$! |
||||
|
||||
# Start the website |
||||
echo "Starting web server..." |
||||
python3 web/app.py & |
||||
|
||||
# Save the web PID too |
||||
WEB_PID=$! |
||||
|
||||
# On CTRL+C or termination, kill both |
||||
trap "echo 'Stopping...'; kill $BOT_PID $WEB_PID; exit" SIGINT SIGTERM |
||||
|
||||
# Wait for both processes (if you want it to stay attached) |
||||
wait $BOT_PID $WEB_PID |
@ -0,0 +1,59 @@ |
||||
from flask import Flask, render_template |
||||
import sqlite3 |
||||
|
||||
app = Flask(__name__) |
||||
DB_PATH = "./gacha_game.db" # Adjust path if needed |
||||
|
||||
def get_db_connection(): |
||||
conn = sqlite3.connect(DB_PATH) |
||||
conn.row_factory = sqlite3.Row |
||||
return conn |
||||
|
||||
@app.route('/') |
||||
def index(): |
||||
conn = get_db_connection() |
||||
users = conn.execute('SELECT id, username FROM users').fetchall() |
||||
top_users = conn.execute(''' |
||||
SELECT users.id, users.username, COUNT(pulls.id) AS pull_count |
||||
FROM users |
||||
LEFT JOIN pulls ON users.id = pulls.user_id |
||||
GROUP BY users.id |
||||
ORDER BY pull_count DESC |
||||
LIMIT 5 |
||||
''').fetchall() |
||||
|
||||
conn.close() |
||||
return render_template('index.html', users=users, top_users=top_users) |
||||
|
||||
@app.route('/user/<int:user_id>') |
||||
def user_profile(user_id): |
||||
conn = get_db_connection() |
||||
conn.row_factory = sqlite3.Row |
||||
cursor = conn.cursor() |
||||
|
||||
cursor.execute('SELECT * FROM users WHERE id = ?', (user_id,)) |
||||
user = cursor.fetchone() |
||||
|
||||
cursor.execute(''' |
||||
SELECT pulls.timestamp, characters.name as character_name, characters.rarity |
||||
FROM pulls |
||||
JOIN characters ON pulls.character_id = characters.id |
||||
WHERE pulls.user_id = ? |
||||
ORDER BY pulls.timestamp DESC |
||||
''', (user_id,)) |
||||
pulls = cursor.fetchall() |
||||
|
||||
conn.close() |
||||
return render_template('user.html', user=user, pulls=pulls) |
||||
|
||||
@app.route('/about') |
||||
def about(): |
||||
return render_template('about.html') |
||||
|
||||
@app.route('/submit') |
||||
def submit_character(): |
||||
return render_template('submit.html') |
||||
|
||||
|
||||
if __name__ == '__main__': |
||||
app.run(host='0.0.0.0', port=5000, debug=True) |
@ -0,0 +1,13 @@ |
||||
<!DOCTYPE html> |
||||
<html> |
||||
<head> |
||||
<title>About - Misskey Gacha Center</title> |
||||
</head> |
||||
<body> |
||||
<h1>About This Gacha</h1> |
||||
<p>This is a playful Misskey-themed gacha tracker made with Flask and SQLite.</p> |
||||
<p>All rolls are stored, stats are tracked, and characters are added manually for now.</p> |
||||
<p>Built with love, chaos, and way too much caffeine ☕.</p> |
||||
<a href="{{ url_for('index') }}">← Back to Home</a> |
||||
</body> |
||||
</html> |
@ -0,0 +1,12 @@ |
||||
<!DOCTYPE html> |
||||
<html> |
||||
<head> |
||||
<title>Submit a Character - Misskey Gacha Center</title> |
||||
</head> |
||||
<body> |
||||
<h1>Submit a Character</h1> |
||||
<p>Want to add a new character to the gacha pool?</p> |
||||
<p>This feature will be available soon. Stay tuned!</p> |
||||
<a href="{{ url_for('index') }}">← Back to Home</a> |
||||
</body> |
||||
</html> |
@ -0,0 +1,77 @@ |
||||
<!DOCTYPE html> |
||||
<html> |
||||
<head> |
||||
<title>{{ user['username'] }}'s Rolls</title> |
||||
<style> |
||||
body { |
||||
font-family: Arial, sans-serif; |
||||
background-color: #f4f4f8; |
||||
margin: 0; |
||||
padding: 20px; |
||||
} |
||||
.profile, .pulls { |
||||
background-color: white; |
||||
padding: 15px; |
||||
border-radius: 10px; |
||||
box-shadow: 0 0 10px rgba(0,0,0,0.1); |
||||
margin-bottom: 20px; |
||||
} |
||||
h1, h2 { |
||||
margin-top: 0; |
||||
} |
||||
ul { |
||||
list-style-type: none; |
||||
padding: 0; |
||||
} |
||||
li { |
||||
padding: 10px 0; |
||||
border-bottom: 1px solid #eee; |
||||
} |
||||
.rarity { |
||||
color: gold; |
||||
font-weight: bold; |
||||
margin-left: 8px; |
||||
} |
||||
.timestamp { |
||||
color: #888; |
||||
font-size: 0.9em; |
||||
} |
||||
a { |
||||
display: inline-block; |
||||
margin-top: 20px; |
||||
color: #333; |
||||
text-decoration: none; |
||||
background-color: #ddd; |
||||
padding: 8px 12px; |
||||
border-radius: 5px; |
||||
} |
||||
a:hover { |
||||
background-color: #bbb; |
||||
} |
||||
</style> |
||||
</head> |
||||
<body> |
||||
|
||||
<div class="profile"> |
||||
<h1>{{ user['username'] }}'s Gacha Rolls</h1> |
||||
<p>User ID: {{ user['id'] }}</p> |
||||
<p>Total Rolls: {{ pulls | length }}</p> |
||||
</div> |
||||
|
||||
<div class="pulls"> |
||||
<h2>Pull History</h2> |
||||
<ul> |
||||
{% for pull in pulls %} |
||||
<li> |
||||
<span class="timestamp">{{ pull['timestamp'] }}</span><br> |
||||
{{ pull['character_name'] }} |
||||
<span class="rarity">{{ '★' * pull['rarity'] }}</span> |
||||
</li> |
||||
{% endfor %} |
||||
</ul> |
||||
</div> |
||||
|
||||
<a href="{{ url_for('index') }}">← Back to Users</a> |
||||
|
||||
</body> |
||||
</html> |
Loading…
Reference in new issue