Merge pull request #2 from SleepyLili/configurable-redeems

configurable redeems
This commit is contained in:
Lin (Lili) Pavelů 2022-11-07 12:59:07 +01:00 committed by GitHub
commit e3cc9286af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 248 additions and 64 deletions

View File

@ -2,7 +2,7 @@ from setuptools import find_packages, setup
setup( setup(
name='tlapbot', name='tlapbot',
version='0.5.3', version='0.6.0',
packages=find_packages(), packages=find_packages(),
include_package_data=True, include_package_data=True,
install_requires=[ install_requires=[

View File

@ -4,6 +4,7 @@ from apscheduler.schedulers.background import BackgroundScheduler
from tlapbot.db import get_db from tlapbot.db import get_db
from tlapbot.owncast_helpers import is_stream_live, give_points_to_chat from tlapbot.owncast_helpers import is_stream_live, give_points_to_chat
def create_app(test_config=None): def create_app(test_config=None):
app = Flask(__name__, instance_relative_config=True) app = Flask(__name__, instance_relative_config=True)
@ -14,13 +15,14 @@ def create_app(test_config=None):
pass pass
# Prepare config: set db to instance folder, then load default, then # 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( app.config.from_mapping(
DATABASE=os.path.join(app.instance_path, "tlapbot.sqlite") DATABASE=os.path.join(app.instance_path, "tlapbot.sqlite")
) )
app.config.from_object('tlapbot.default_config') app.config.from_object('tlapbot.default_config')
app.config.from_object('tlapbot.default_redeems')
app.config.from_pyfile('config.py') app.config.from_pyfile('config.py')
app.config.from_pyfile('redeems.py')
# prepare webhooks and redeem dashboard blueprints # prepare webhooks and redeem dashboard blueprints
from . import owncast_webhooks from . import owncast_webhooks
@ -44,12 +46,11 @@ def create_app(test_config=None):
# start scheduler that will give points to users # start scheduler that will give points to users
points_giver = BackgroundScheduler() 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() points_giver.start()
return app return app
if __name__ == '__main__': if __name__ == '__main__':
create_app() create_app()

View File

@ -22,12 +22,28 @@ def close_db(e=None):
if db is not None: if db is not None:
db.close() 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(): def init_db():
db = get_db() db = get_db()
with current_app.open_resource('schema.sql') as f: with current_app.open_resource('schema.sql') as f:
db.executescript(f.read().decode('utf8')) db.executescript(f.read().decode('utf8'))
insert_counters(db)
@click.command('init-db') @click.command('init-db')
@with_appcontext @with_appcontext
@ -36,6 +52,7 @@ def init_db_command():
init_db() init_db()
click.echo('Initialized the database.') click.echo('Initialized the database.')
def init_app(app): def init_app(app):
app.teardown_appcontext(close_db) app.teardown_appcontext(close_db)
app.cli.add_command(init_db_command) app.cli.add_command(init_db_command)

View File

@ -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"}
}

View File

@ -5,6 +5,7 @@ import click
from flask.cli import with_appcontext from flask.cli import with_appcontext
from tlapbot.db import get_db from tlapbot.db import get_db
# # # requests stuff # # # # # # requests stuff # # #
def is_stream_live(): def is_stream_live():
url = current_app.config['OWNCAST_INSTANCE_URL'] + '/api/status' url = current_app.config['OWNCAST_INSTANCE_URL'] + '/api/status'
@ -12,6 +13,7 @@ def is_stream_live():
print(r.json()["online"]) print(r.json()["online"])
return r.json()["online"] return r.json()["online"]
def give_points_to_chat(db): def give_points_to_chat(db):
url = current_app.config['OWNCAST_INSTANCE_URL'] + '/api/integrations/clients' url = current_app.config['OWNCAST_INSTANCE_URL'] + '/api/integrations/clients'
headers = {"Authorization": "Bearer " + current_app.config['OWNCAST_ACCESS_TOKEN']} headers = {"Authorization": "Bearer " + current_app.config['OWNCAST_ACCESS_TOKEN']}
@ -22,6 +24,7 @@ def give_points_to_chat(db):
user_id, user_id,
current_app.config['POINTS_AMOUNT_GIVEN']) current_app.config['POINTS_AMOUNT_GIVEN'])
def send_chat(message): def send_chat(message):
url = current_app.config['OWNCAST_INSTANCE_URL'] + '/api/integrations/chat/send' url = current_app.config['OWNCAST_INSTANCE_URL'] + '/api/integrations/chat/send'
headers = {"Authorization": "Bearer " + current_app.config['OWNCAST_ACCESS_TOKEN']} headers = {"Authorization": "Bearer " + current_app.config['OWNCAST_ACCESS_TOKEN']}
@ -31,6 +34,7 @@ def send_chat(message):
# # # db stuff # # # # # # db stuff # # #
def read_users_points(db, user_id): def read_users_points(db, user_id):
"""Errors out if user doesn't exist."""
try: try:
cursor = db.execute( cursor = db.execute(
"SELECT points FROM points WHERE id = ?", "SELECT points FROM points WHERE id = ?",
@ -41,6 +45,19 @@ def read_users_points(db, user_id):
print("Error occured reading points:", e.args[0]) print("Error occured reading points:", e.args[0])
print("To user:", user_id) 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): def give_points_to_user(db, user_id, points):
try: try:
db.execute( db.execute(
@ -52,6 +69,7 @@ def give_points_to_user(db, user_id, points):
print("Error occured giving points:", e.args[0]) print("Error occured giving points:", e.args[0])
print("To user:", user_id, " amount of points:", points) print("To user:", user_id, " amount of points:", points)
def use_points(db, user_id, points): def use_points(db, user_id, points):
try: try:
db.execute( db.execute(
@ -65,36 +83,47 @@ def use_points(db, user_id, points):
print("From user:", user_id, " amount of points:", points) print("From user:", user_id, " amount of points:", points)
return False return False
def user_exists(db, user_id): def user_exists(db, user_id):
try: try:
cursor = db.execute( cursor = db.execute(
"SELECT points FROM points WHERE id = ?", "SELECT points FROM points WHERE id = ?",
(user_id,) (user_id,)
) )
if cursor.fetchone() == None: if cursor.fetchone() is None:
return False return False
return True return True
except Error as e: except Error as e:
print("Error occured checking if user exists:", e.args[0]) print("Error occured checking if user exists:", e.args[0])
print("To user:", user_id) 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): 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: try:
cursor = db.execute( cursor = db.execute(
"SELECT points FROM points WHERE id = ?", "SELECT points, name FROM points WHERE id = ?",
(user_id,) (user_id,)
) )
if cursor.fetchone() == None: user = cursor.fetchone()
if user is None:
cursor.execute( cursor.execute(
"INSERT INTO points(id, name, points) VALUES(?, ?, 10)", "INSERT INTO points(id, name, points) VALUES(?, ?, 10)",
(user_id, display_name) (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() db.commit()
except Error as e: except Error as e:
print("Error occured adding user to db:", e.args[0]) print("Error occured adding user to db:", e.args[0])
print("To user:", user_id, display_name) print("To user:", user_id, display_name)
def change_display_name(db, user_id, new_name): def change_display_name(db, user_id, new_name):
try: try:
cursor = db.execute( cursor = db.execute(
@ -107,31 +136,57 @@ def change_display_name(db, user_id, new_name):
print("To user:", user_id, new_name) print("To user:", user_id, new_name)
def add_to_counter(db, counter_name):
def add_to_redeem_queue(db, user_id, redeem_name):
try: try:
cursor = db.execute( cursor = db.execute(
"INSERT INTO redeem_queue(redeem, redeemer_id) VALUES(?, ?)", "UPDATE counters SET count = count + 1 WHERE name = ?",
(redeem_name, user_id) (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() db.commit()
except Error as e: except Error as e:
print("Error occured adding to redeem queue:", e.args[0]) 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): def clear_redeem_queue(db):
try: try:
cursor = db.execute( cursor = db.execute(
"DELETE FROM redeem_queue" "DELETE FROM redeem_queue"
) )
cursor.execute(
"""UPDATE counters SET count = 0"""
)
db.commit() db.commit()
except Error as e: except Error as e:
print("Error occured deleting redeem queue:", e.args[0]) 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): def pretty_redeem_queue(db):
try: try:
cursor = db.execute( 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 FROM redeem_queue
INNER JOIN points INNER JOIN points
on redeem_queue.redeemer_id = points.id""" on redeem_queue.redeemer_id = points.id"""
@ -140,6 +195,7 @@ def pretty_redeem_queue(db):
except Error as e: except Error as e:
print("Error occured selecting pretty redeem queue:", e.args[0]) print("Error occured selecting pretty redeem queue:", e.args[0])
def whole_redeem_queue(db): def whole_redeem_queue(db):
try: try:
cursor = db.execute( cursor = db.execute(
@ -149,6 +205,20 @@ def whole_redeem_queue(db):
except Error as e: except Error as e:
print("Error occured selecting redeem queue:", e.args[0]) 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') @click.command('clear-queue')
@with_appcontext @with_appcontext
def clear_queue_command(): def clear_queue_command():

View File

@ -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.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 from datetime import datetime, timezone
bp = Blueprint('redeem_dashboard', __name__) bp = Blueprint('redeem_dashboard', __name__)
@bp.route('/dashboard',methods=['GET'])
@bp.route('/dashboard', methods=['GET'])
def dashboard(): def dashboard():
queue = pretty_redeem_queue(get_db()) db = get_db()
number_of_drinks = 0 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 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', return render_template('dashboard.html',
queue=queue, queue=queue,
number_of_drinks=number_of_drinks, counters=counters,
username=username,
users=users,
utc_timezone=utc_timezone) utc_timezone=utc_timezone)

View File

@ -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 sqlite3 import Error
from tlapbot.db import get_db 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 = Blueprint('owncast_webhooks', __name__)
@bp.route('/owncastWebhook',methods=['POST'])
@bp.route('/owncastWebhook', methods=['POST'])
def owncast_webhook(): def owncast_webhook():
data = request.json data = request.json
db = get_db() db = get_db()
@ -14,10 +17,14 @@ def owncast_webhook():
display_name = data["eventData"]["user"]["displayName"] display_name = data["eventData"]["user"]["displayName"]
# CONSIDER: join points for joining stream # CONSIDER: join points for joining stream
add_user_to_database(db, user_id, display_name) 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": elif data["type"] == "NAME_CHANGE":
user_id = data["eventData"]["user"]["id"] user_id = data["eventData"]["user"]["id"]
new_name = data["eventData"]["newName"] new_name = data["eventData"]["newName"]
change_display_name(db, user_id, new_name) 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": elif data["type"] == "CHAT":
user_id = data["eventData"]["user"]["id"] user_id = data["eventData"]["user"]["id"]
display_name = data["eventData"]["user"]["displayName"] display_name = data["eventData"]["user"]["displayName"]
@ -25,28 +32,27 @@ def owncast_webhook():
print(f'{data["eventData"]["body"]}') print(f'{data["eventData"]["body"]}')
if "!help" in data["eventData"]["body"]: if "!help" in data["eventData"]["body"]:
message = """Tlapbot commands: message = """Tlapbot commands:
!help to see this help message.
!points to see your points. !points to see your points.
!drink to redeem a pitíčko for 60 points. !name_update to force name update if tlapbot didn't catch it.
That's it for now.""" 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) send_chat(message)
elif "!points" in data["eventData"]["body"]: elif "!points" in data["eventData"]["body"]:
if not user_exists(db, user_id): if not user_exists(db, user_id):
add_user_to_database(db, user_id, display_name) add_user_to_database(db, user_id, display_name)
points = read_users_points(db, user_id) points = read_users_points(db, user_id)
message = "{}'s points: {}".format(display_name, points) message = f"{display_name}'s points: {points}"
send_chat(message) 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"]: elif "!name_update" in data["eventData"]["body"]:
# Forces name update in case bot didn't catch the NAME_CHANGE # 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) change_display_name(db, user_id, display_name)
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 return data

View File

@ -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.")

View File

@ -1,16 +1,23 @@
DROP TABLE IF EXISTS points; DROP TABLE IF EXISTS counters;
DROP TABLE IF EXISTS redeem_queue; DROP TABLE IF EXISTS redeem_queue;
CREATE TABLE points ( CREATE TABLE IF NOT EXISTS points (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
name TEXT, name TEXT,
points INTEGER points INTEGER
); );
CREATE TABLE counters (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
count INTEGER NOT NULL
);
CREATE TABLE redeem_queue ( CREATE TABLE redeem_queue (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
redeem TEXT, redeem TEXT NOT NULL,
redeemer_id TEXT, redeemer_id TEXT NOT NULL,
note TEXT,
FOREIGN KEY (redeemer_id) REFERENCES points (id) FOREIGN KEY (redeemer_id) REFERENCES points (id)
); );

View File

@ -5,12 +5,38 @@
<title>Redeems Dashboard</title> <title>Redeems Dashboard</title>
</head> </head>
<body> <body>
{% if (username and users ) %}
<table> <table>
<thead>
<tr>
<th>Points balance:</th>
</tr>
</thead>
{% for user in users %}
<tbody> <tbody>
<td> Number of drinks: </td> <td> {{ user[0] }} </td>
<td> {{ number_of_drinks }} </td> <td> {{ user[1] }} </td>
</tbody> </tbody>
{% endfor %}
</table> </table>
{% endif %}
{% if counters %}
<table>
<thead>
<tr>
<th>Counters</th>
</tr>
</thead>
{% for counter in counters %}
<tbody>
<td> {{ counter[0] }} </td>
<td> {{ counter[1] }} </td>
</tbody>
{% endfor %}
</table>
{% endif %}
{% if queue %} {% if queue %}
<table> <table>
<thead> <thead>
@ -18,13 +44,17 @@
<th>time</th> <th>time</th>
<th>redeem</th> <th>redeem</th>
<th>redeemer</th> <th>redeemer</th>
<th>note</th>
</tr> </tr>
</thead> </thead>
{% for row in queue %} {% for row in queue %}
<tbody> <tbody>
<td>{{ row[0].replace(tzinfo=utc_timezone).astimezone().strftime("%H:%M") }}</td> <td>{{ row[0].replace(tzinfo=utc_timezone).astimezone().strftime("%H:%M") }}</td>
<td>{{ row[1] }}</td> <td>{{ row[1] }}</td>
<td>{{ row[3] }}</td>
{% if row[2] %}
<td>{{ row[2] }}</td> <td>{{ row[2] }}</td>
{% endif %}
</tbody> </tbody>
{% endfor %} {% endfor %}
</table> </table>