diff --git a/setup.py b/setup.py index 9fe8b0c..5b34277 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import find_packages, setup setup( name='tlapbot', - version='0.5.3', + version='0.6.0', packages=find_packages(), include_package_data=True, install_requires=[ diff --git a/tlapbot/__init__.py b/tlapbot/__init__.py index bc19be3..c5a9368 100644 --- a/tlapbot/__init__.py +++ b/tlapbot/__init__.py @@ -4,6 +4,7 @@ from apscheduler.schedulers.background import BackgroundScheduler from tlapbot.db import get_db from tlapbot.owncast_helpers import is_stream_live, give_points_to_chat + def create_app(test_config=None): app = Flask(__name__, instance_relative_config=True) @@ -14,20 +15,21 @@ def create_app(test_config=None): pass # Prepare config: set db to instance folder, then load default, then - # overwrite it with config.py + # overwrite it with config.py and redeems.py app.config.from_mapping( DATABASE=os.path.join(app.instance_path, "tlapbot.sqlite") ) app.config.from_object('tlapbot.default_config') + app.config.from_object('tlapbot.default_redeems') app.config.from_pyfile('config.py') - + app.config.from_pyfile('redeems.py') # prepare webhooks and redeem dashboard blueprints from . import owncast_webhooks from . import owncast_redeem_dashboard app.register_blueprint(owncast_webhooks.bp) app.register_blueprint(owncast_redeem_dashboard.bp) - + # add db initialization CLI command from . import db db.init_app(app) @@ -44,12 +46,11 @@ def create_app(test_config=None): # start scheduler that will give points to users points_giver = BackgroundScheduler() - points_giver.add_job(proxy_job, 'interval', seconds=app.config['POINTS_CYCLE_TIME']) # change to 10 minutes out of testing + points_giver.add_job(proxy_job, 'interval', seconds=app.config['POINTS_CYCLE_TIME']) points_giver.start() return app - if __name__ == '__main__': - create_app() \ No newline at end of file + create_app() diff --git a/tlapbot/db.py b/tlapbot/db.py index 74dea73..8bb5b68 100644 --- a/tlapbot/db.py +++ b/tlapbot/db.py @@ -22,12 +22,28 @@ def close_db(e=None): if db is not None: db.close() + +def insert_counters(db): + for redeem, redeem_info in current_app.config['REDEEMS'].items(): + if redeem_info["type"] == "counter": + try: + cursor = db.execute( + "INSERT INTO counters(name, count) VALUES(?, 0)", + (redeem,) + ) + db.commit() + except Error as e: + print("Failed inserting counters to db:", e.args[0]) + + def init_db(): db = get_db() with current_app.open_resource('schema.sql') as f: db.executescript(f.read().decode('utf8')) + insert_counters(db) + @click.command('init-db') @with_appcontext @@ -36,6 +52,7 @@ def init_db_command(): init_db() click.echo('Initialized the database.') + def init_app(app): app.teardown_appcontext(close_db) - app.cli.add_command(init_db_command) \ No newline at end of file + app.cli.add_command(init_db_command) diff --git a/tlapbot/default_redeems.py b/tlapbot/default_redeems.py new file mode 100644 index 0000000..6956e1c --- /dev/null +++ b/tlapbot/default_redeems.py @@ -0,0 +1,6 @@ +REDEEMS={ + "hydrate": {"price": 60, "type": "list"}, + "lurk": {"price": 1, "type": "counter"}, + "react": {"price": 200, "type": "note"}, + "request": {"price": 100, "type": "note"} +} \ No newline at end of file diff --git a/tlapbot/owncast_helpers.py b/tlapbot/owncast_helpers.py index 8826571..f766627 100644 --- a/tlapbot/owncast_helpers.py +++ b/tlapbot/owncast_helpers.py @@ -5,6 +5,7 @@ import click from flask.cli import with_appcontext from tlapbot.db import get_db + # # # requests stuff # # # def is_stream_live(): url = current_app.config['OWNCAST_INSTANCE_URL'] + '/api/status' @@ -12,6 +13,7 @@ def is_stream_live(): print(r.json()["online"]) return r.json()["online"] + def give_points_to_chat(db): url = current_app.config['OWNCAST_INSTANCE_URL'] + '/api/integrations/clients' headers = {"Authorization": "Bearer " + current_app.config['OWNCAST_ACCESS_TOKEN']} @@ -22,6 +24,7 @@ def give_points_to_chat(db): user_id, current_app.config['POINTS_AMOUNT_GIVEN']) + def send_chat(message): url = current_app.config['OWNCAST_INSTANCE_URL'] + '/api/integrations/chat/send' headers = {"Authorization": "Bearer " + current_app.config['OWNCAST_ACCESS_TOKEN']} @@ -31,6 +34,7 @@ def send_chat(message): # # # db stuff # # # def read_users_points(db, user_id): + """Errors out if user doesn't exist.""" try: cursor = db.execute( "SELECT points FROM points WHERE id = ?", @@ -41,22 +45,36 @@ def read_users_points(db, user_id): print("Error occured reading points:", e.args[0]) print("To user:", user_id) + +def read_all_users_with_username(db, username): + try: + cursor = db.execute( + "SELECT name, points FROM points WHERE name = ?", + (username,) + ) + users = cursor.fetchall() + return users + except Error as e: + print("Error occured reading points from username:", e.args[0]) + print("To user:", username) + def give_points_to_user(db, user_id, points): try: db.execute( - "UPDATE points SET points = points + ? WHERE id = ?", - (points, user_id,) + "UPDATE points SET points = points + ? WHERE id = ?", + (points, user_id,) ) db.commit() except Error as e: print("Error occured giving points:", e.args[0]) print("To user:", user_id, " amount of points:", points) + def use_points(db, user_id, points): try: db.execute( - "UPDATE points SET points = points - ? WHERE id = ?", - (points, user_id,) + "UPDATE points SET points = points - ? WHERE id = ?", + (points, user_id,) ) db.commit() return True @@ -65,73 +83,110 @@ def use_points(db, user_id, points): print("From user:", user_id, " amount of points:", points) return False + def user_exists(db, user_id): try: cursor = db.execute( "SELECT points FROM points WHERE id = ?", (user_id,) ) - if cursor.fetchone() == None: + if cursor.fetchone() is None: return False return True except Error as e: print("Error occured checking if user exists:", e.args[0]) print("To user:", user_id) -""" Adds a new user to the database. Does nothing if user is already in.""" + def add_user_to_database(db, user_id, display_name): + """ Adds a new user to the database. Does nothing if user is already in.""" try: cursor = db.execute( - "SELECT points FROM points WHERE id = ?", + "SELECT points, name FROM points WHERE id = ?", (user_id,) ) - if cursor.fetchone() == None: + user = cursor.fetchone() + if user is None: cursor.execute( "INSERT INTO points(id, name, points) VALUES(?, ?, 10)", (user_id, display_name) ) + if user is not None and user[1] == None: + cursor.execute( + """UPDATE points + SET name = ? + WHERE id = ?""", + (display_name, user_id) + ) db.commit() except Error as e: print("Error occured adding user to db:", e.args[0]) print("To user:", user_id, display_name) + def change_display_name(db, user_id, new_name): try: cursor = db.execute( - "UPDATE points SET name = ? WHERE id = ?", - (new_name, user_id) - ) + "UPDATE points SET name = ? WHERE id = ?", + (new_name, user_id) + ) db.commit() except Error as e: print("Error occured changing display name:", e.args[0]) print("To user:", user_id, new_name) - -def add_to_redeem_queue(db, user_id, redeem_name): +def add_to_counter(db, counter_name): try: cursor = db.execute( - "INSERT INTO redeem_queue(redeem, redeemer_id) VALUES(?, ?)", - (redeem_name, user_id) + "UPDATE counters SET count = count + 1 WHERE name = ?", + (counter_name,) + ) + db.commit() + except Error as e: + print("Error occured adding to counter:", e.args[0]) + print("To counter:", counter_name) + + +def add_to_redeem_queue(db, user_id, redeem_name, note=None): + try: + cursor = db.execute( + "INSERT INTO redeem_queue(redeem, redeemer_id, note) VALUES(?, ?, ?)", + (redeem_name, user_id, note) ) db.commit() except Error as e: print("Error occured adding to redeem queue:", e.args[0]) - print("To user:", user_id, " with redeem:", redeem_name) + print("To user:", user_id, " with redeem:", redeem_name, "with note:", note) + def clear_redeem_queue(db): try: cursor = db.execute( - "DELETE FROM redeem_queue" - ) + "DELETE FROM redeem_queue" + ) + cursor.execute( + """UPDATE counters SET count = 0""" + ) db.commit() except Error as e: print("Error occured deleting redeem queue:", e.args[0]) + +def all_counters(db): + try: + cursor = db.execute( + """SELECT counters.name, counters.count FROM counters""" + ) + return cursor.fetchall() + except Error as e: + print("Error occured selecting all counters:", e.args[0]) + + def pretty_redeem_queue(db): try: cursor = db.execute( - """SELECT redeem_queue.created, redeem_queue.redeem, points.name + """SELECT redeem_queue.created, redeem_queue.redeem, redeem_queue.note, points.name FROM redeem_queue INNER JOIN points on redeem_queue.redeemer_id = points.id""" @@ -140,15 +195,30 @@ def pretty_redeem_queue(db): except Error as e: print("Error occured selecting pretty redeem queue:", e.args[0]) + def whole_redeem_queue(db): try: cursor = db.execute( - "SELECT * from redeem_queue" - ) + "SELECT * from redeem_queue" + ) return cursor.fetchall() except Error as e: print("Error occured selecting redeem queue:", e.args[0]) + +def remove_duplicate_usernames(db, user_id, username): + try: + cursor = db.execute( + """UPDATE points + SET name = NULL + WHERE name = ? AND NOT id = ?""", + (username, user_id) + ) + db.commit() + except Error as e: + print("Error occured removing duplicate usernames:", e.args[0]) + + @click.command('clear-queue') @with_appcontext def clear_queue_command(): diff --git a/tlapbot/owncast_redeem_dashboard.py b/tlapbot/owncast_redeem_dashboard.py index 27c7c64..263f301 100644 --- a/tlapbot/owncast_redeem_dashboard.py +++ b/tlapbot/owncast_redeem_dashboard.py @@ -1,20 +1,26 @@ -from flask import render_template, Blueprint +from flask import render_template, Blueprint, request from tlapbot.db import get_db -from tlapbot.owncast_helpers import pretty_redeem_queue +from tlapbot.owncast_helpers import (pretty_redeem_queue, all_counters, + read_all_users_with_username) from datetime import datetime, timezone bp = Blueprint('redeem_dashboard', __name__) -@bp.route('/dashboard',methods=['GET']) + +@bp.route('/dashboard', methods=['GET']) def dashboard(): - queue = pretty_redeem_queue(get_db()) - number_of_drinks = 0 + db = get_db() + queue = pretty_redeem_queue(db) + counters = all_counters(db) + username = request.args.get("username") + if username is not None: + users = read_all_users_with_username(db, username) + else: + users = [] utc_timezone = timezone.utc - if queue is not None: - for row in queue: - if row[1] == "drink": - number_of_drinks += 1 return render_template('dashboard.html', queue=queue, - number_of_drinks=number_of_drinks, - utc_timezone=utc_timezone) \ No newline at end of file + counters=counters, + username=username, + users=users, + utc_timezone=utc_timezone) diff --git a/tlapbot/owncast_webhooks.py b/tlapbot/owncast_webhooks.py index b53ae26..64f8cd2 100644 --- a/tlapbot/owncast_webhooks.py +++ b/tlapbot/owncast_webhooks.py @@ -1,11 +1,14 @@ -from flask import Flask,request,json,Blueprint +from flask import Flask, request, json, Blueprint, current_app from sqlite3 import Error from tlapbot.db import get_db -from tlapbot.owncast_helpers import * +from tlapbot.owncast_helpers import (add_user_to_database, change_display_name, + user_exists, send_chat, read_users_points, remove_duplicate_usernames) +from tlapbot.redeems_handler import handle_redeem bp = Blueprint('owncast_webhooks', __name__) -@bp.route('/owncastWebhook',methods=['POST']) + +@bp.route('/owncastWebhook', methods=['POST']) def owncast_webhook(): data = request.json db = get_db() @@ -14,10 +17,14 @@ def owncast_webhook(): display_name = data["eventData"]["user"]["displayName"] # CONSIDER: join points for joining stream add_user_to_database(db, user_id, display_name) + if data["eventData"]["user"]["authenticated"]: + remove_duplicate_usernames(db, user_id, display_name) elif data["type"] == "NAME_CHANGE": user_id = data["eventData"]["user"]["id"] new_name = data["eventData"]["newName"] change_display_name(db, user_id, new_name) + if data["eventData"]["user"]["authenticated"]: + remove_duplicate_usernames(db, user_id, new_name) elif data["type"] == "CHAT": user_id = data["eventData"]["user"]["id"] display_name = data["eventData"]["user"]["displayName"] @@ -25,28 +32,27 @@ def owncast_webhook(): print(f'{data["eventData"]["body"]}') if "!help" in data["eventData"]["body"]: message = """Tlapbot commands: + !help to see this help message. !points to see your points. - !drink to redeem a pitíčko for 60 points. - That's it for now.""" + !name_update to force name update if tlapbot didn't catch it. + Tlapbot redeems:\n""" + for redeem, redeem_info in current_app.config['REDEEMS'].items(): + message += (f"!{redeem} for {redeem_info['price']} points.\n") + # TODO: also make this customizable send_chat(message) elif "!points" in data["eventData"]["body"]: if not user_exists(db, user_id): add_user_to_database(db, user_id, display_name) points = read_users_points(db, user_id) - message = "{}'s points: {}".format(display_name, points) + message = f"{display_name}'s points: {points}" send_chat(message) - elif "!drink" in data["eventData"]["body"]: - points = read_users_points(db, user_id) - if points is not None and points >= 60: - if use_points(db, user_id, 60): - add_to_redeem_queue(db, user_id, "drink") - send_chat("Pitíčko redeemed for 60 points.") - else: - send_chat("Pitíčko not redeemed because of an error.") - else: - send_chat("Can't redeem pitíčko, you don't have enough points.") elif "!name_update" in data["eventData"]["body"]: # Forces name update in case bot didn't catch the NAME_CHANGE - # event. Theoretically only needed when bot was off. + # event. Also removes saved usernames from users with same name + # if user is authenticated. change_display_name(db, user_id, display_name) - return data \ No newline at end of file + if data["eventData"]["user"]["authenticated"]: + remove_duplicate_usernames(db, user_id, display_name) + elif data["eventData"]["body"].startswith("!"): # TODO: make prefix configurable + handle_redeem(data["eventData"]["body"], user_id) + return data diff --git a/tlapbot/redeems_handler.py b/tlapbot/redeems_handler.py new file mode 100644 index 0000000..2fa717c --- /dev/null +++ b/tlapbot/redeems_handler.py @@ -0,0 +1,41 @@ +from flask import current_app +from tlapbot.db import get_db +from tlapbot.owncast_helpers import (use_points, add_to_redeem_queue, + add_to_counter, read_users_points, send_chat) + + +def handle_redeem(message, user_id): + split_message = message[1:].split(maxsplit=1) + redeem = split_message[0] + if len(split_message) == 1: + note = None + else: + note = split_message[1] + + if redeem in current_app.config['REDEEMS']: + db = get_db() + price = current_app.config['REDEEMS'][redeem]["price"] + redeem_type = current_app.config['REDEEMS'][redeem]["type"] + points = read_users_points(db, user_id) + if points is not None and points >= price: + if redeem_type == "counter": + add_to_counter(db, redeem) + use_points(db, user_id, price) + send_chat(f"{redeem} redeemed for {price} points.") + elif redeem_type == "list": + add_to_redeem_queue(db, user_id, redeem) + use_points(db, user_id, price) + send_chat(f"{redeem} redeemed for {price} points.") + elif redeem_type == "note": + if note is not None: + add_to_redeem_queue(db, user_id, redeem, note) + use_points(db, user_id, price) + send_chat(f"{redeem} redeemed for {price} points.") + else: + send_chat(f"Cannot redeem {redeem}, no note included.") + else: + send_chat(f"{redeem} not redeemed because of an error.") + else: + send_chat(f"Can't redeem {redeem}, you don't have enough points.") + else: + send_chat("Can't redeem, redeem not found.") diff --git a/tlapbot/schema.sql b/tlapbot/schema.sql index 0770ea7..681cc70 100644 --- a/tlapbot/schema.sql +++ b/tlapbot/schema.sql @@ -1,16 +1,23 @@ -DROP TABLE IF EXISTS points; +DROP TABLE IF EXISTS counters; DROP TABLE IF EXISTS redeem_queue; -CREATE TABLE points ( +CREATE TABLE IF NOT EXISTS points ( id TEXT PRIMARY KEY, name TEXT, points INTEGER ); +CREATE TABLE counters ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + count INTEGER NOT NULL +); + CREATE TABLE redeem_queue ( id INTEGER PRIMARY KEY AUTOINCREMENT, created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - redeem TEXT, - redeemer_id TEXT, + redeem TEXT NOT NULL, + redeemer_id TEXT NOT NULL, + note TEXT, FOREIGN KEY (redeemer_id) REFERENCES points (id) ); \ No newline at end of file diff --git a/tlapbot/templates/dashboard.html b/tlapbot/templates/dashboard.html index f4865e2..bebbc1d 100644 --- a/tlapbot/templates/dashboard.html +++ b/tlapbot/templates/dashboard.html @@ -5,12 +5,38 @@ Redeems Dashboard + {% if (username and users ) %} + + + + + + {% for user in users %} - - + + + {% endfor %}
Points balance:
Number of drinks: {{ number_of_drinks }} {{ user[0] }} {{ user[1] }}
+ {% endif %} + + {% if counters %} + + + + + + + {% for counter in counters %} + + + + + {% endfor %} +
Counters
{{ counter[0] }} {{ counter[1] }}
+ {% endif %} + {% if queue %} @@ -18,13 +44,17 @@ + {% for row in queue %} + + {% if row[2] %} + {% endif %} {% endfor %}
time redeem redeemernote
{{ row[0].replace(tzinfo=utc_timezone).astimezone().strftime("%H:%M") }} {{ row[1] }}{{ row[3] }}{{ row[2] }}