Compare commits
21 Commits
Author | SHA1 | Date | |
---|---|---|---|
b8fb30a2ae | |||
70285958d1 | |||
9592e2daf0 | |||
d0ff884dec | |||
56cb94e1a0 | |||
4e363876e7 | |||
20e3d6d4b2 | |||
b3cbe42450 | |||
68c585405d | |||
e8d8c265a7 | |||
a274ddb89f | |||
22fcea8a96 | |||
0d5d82a23b | |||
50403381e0 | |||
6758829634 | |||
9f89a2a892 | |||
c8d2865383 | |||
09de99234c | |||
fe75fb7acc | |||
477151f423 | |||
c22af69b2a |
@ -122,7 +122,7 @@ as a package in editable more.
|
|||||||
python -m flask init-db
|
python -m flask init-db
|
||||||
```
|
```
|
||||||
5. Create an `instance/config.py` file and fill it in as needed.
|
5. Create an `instance/config.py` file and fill it in as needed.
|
||||||
Default values are included in `tlapbot/default_config.py`, and values in
|
Default values are included in `tlapbot/defaults/default_config.py`, and values in
|
||||||
`config.py` overwrite them. (The database also lives in the instance folder
|
`config.py` overwrite them. (The database also lives in the instance folder
|
||||||
by default.)
|
by default.)
|
||||||
|
|
||||||
@ -133,7 +133,7 @@ by default.)
|
|||||||
OWNCAST_INSTANCE_URL # default points to localhost owncast on default port
|
OWNCAST_INSTANCE_URL # default points to localhost owncast on default port
|
||||||
```
|
```
|
||||||
6. OPTIONAL: Create an `instance/redeems.py` file and add your custom redeems.
|
6. OPTIONAL: Create an `instance/redeems.py` file and add your custom redeems.
|
||||||
If you don't add a redeems file, the bot will initialize the default redeems from `tlapbot/default_redeems.py`.
|
If you don't add a redeems file, the bot will initialize the default redeems from `tlapbot/defaults/default_redeems.py`.
|
||||||
More details on how to write the config and redeems files are written later in the readme.
|
More details on how to write the config and redeems files are written later in the readme.
|
||||||
7. If you've added any new counters or milestones, run `refresh-counters` or `refresh-milestones` commands to initialize them into the database.
|
7. If you've added any new counters or milestones, run `refresh-counters` or `refresh-milestones` commands to initialize them into the database.
|
||||||
|
|
||||||
@ -299,7 +299,7 @@ Including these values is mandatory if you want tlapbot to work.
|
|||||||
- `OWNCAST_ACCESS_TOKEN` is the owncast access token that owncast will use to get list of users in chat. Generate one in your owncast instance.
|
- `OWNCAST_ACCESS_TOKEN` is the owncast access token that owncast will use to get list of users in chat. Generate one in your owncast instance.
|
||||||
- `OWNCAST_INSTANCE_URL` is the full URL of your owncast instance, like `"http://MyTlapbotServer.example"`
|
- `OWNCAST_INSTANCE_URL` is the full URL of your owncast instance, like `"http://MyTlapbotServer.example"`
|
||||||
#### Optional
|
#### Optional
|
||||||
Including these values will overwrite their defaults from `/tlapbot/default_config.py`.
|
Including these values will overwrite their defaults from `/tlapbot/defaults/default_config.py`.
|
||||||
- `POINTS_CYCLE_TIME` decides how often channel points are given to users in chat,
|
- `POINTS_CYCLE_TIME` decides how often channel points are given to users in chat,
|
||||||
in seconds.
|
in seconds.
|
||||||
- `POINTS_AMOUNT_GIVEN` decides how many channel points users receive.
|
- `POINTS_AMOUNT_GIVEN` decides how many channel points users receive.
|
||||||
@ -320,7 +320,7 @@ LIST_REDEEMS=True
|
|||||||
ACTIVE_CATEGORIES=["gaming"]
|
ACTIVE_CATEGORIES=["gaming"]
|
||||||
```
|
```
|
||||||
### redeems.py
|
### redeems.py
|
||||||
`redeems.py` is a file where you define all your custom redeems. Tlapbot will work without it, but it will load a few default, generic redeems from `tlapbot/default_redeems.py`.
|
`redeems.py` is a file where you define all your custom redeems. Tlapbot will work without it, but it will load a few default, generic redeems from `tlapbot/defaults/default_redeems.py`.
|
||||||
|
|
||||||
(`redeems.py` should be in the instance folder: `instance/redeems.py` for folder install.)
|
(`redeems.py` should be in the instance folder: `instance/redeems.py` for folder install.)
|
||||||
#### `default_redeems.py`:
|
#### `default_redeems.py`:
|
||||||
|
@ -6,7 +6,7 @@ from tlapbot.db import get_db
|
|||||||
from tlapbot.owncast_requests import is_stream_live, give_points_to_chat
|
from tlapbot.owncast_requests import is_stream_live, give_points_to_chat
|
||||||
|
|
||||||
|
|
||||||
def create_app(test_config=None):
|
def create_app(test_config: None = None) -> Flask:
|
||||||
app = Flask(__name__, instance_relative_config=True)
|
app = Flask(__name__, instance_relative_config=True)
|
||||||
|
|
||||||
# ensure the instance folder exists
|
# ensure the instance folder exists
|
||||||
@ -20,11 +20,16 @@ def create_app(test_config=None):
|
|||||||
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.defaults.default_config')
|
||||||
app.config.from_object('tlapbot.default_redeems')
|
app.config.from_object('tlapbot.defaults.default_redeems')
|
||||||
app.config.from_pyfile('config.py', silent=True)
|
app.config.from_pyfile('config.py', silent=True)
|
||||||
app.config.from_pyfile('redeems.py', silent=True)
|
app.config.from_pyfile('redeems.py', silent=True)
|
||||||
|
|
||||||
|
# set up polls if they're enabled
|
||||||
|
if app.config['POLLS']:
|
||||||
|
app.config.from_object('tlapbot.defaults.default_polls')
|
||||||
|
app.config.from_pyfile('polls.py', silent=True)
|
||||||
|
|
||||||
# Make logging work for gunicorn-ran instances of tlapbot.
|
# Make logging work for gunicorn-ran instances of tlapbot.
|
||||||
if app.config['GUNICORN']:
|
if app.config['GUNICORN']:
|
||||||
gunicorn_logger = logging.getLogger('gunicorn.error')
|
gunicorn_logger = logging.getLogger('gunicorn.error')
|
||||||
@ -59,7 +64,7 @@ def create_app(test_config=None):
|
|||||||
app.cli.add_command(db.hard_reset_milestone_command)
|
app.cli.add_command(db.hard_reset_milestone_command)
|
||||||
|
|
||||||
# scheduler job for giving points to users
|
# scheduler job for giving points to users
|
||||||
def proxy_job():
|
def proxy_job() -> None:
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
if is_stream_live():
|
if is_stream_live():
|
||||||
app.logger.info("Stream is LIVE. Giving points to chat.")
|
app.logger.info("Stream is LIVE. Giving points to chat.")
|
||||||
|
111
tlapbot/db.py
111
tlapbot/db.py
@ -1,13 +1,13 @@
|
|||||||
import sqlite3
|
import sqlite3
|
||||||
|
|
||||||
import click
|
import click
|
||||||
from flask import current_app, g
|
from flask import current_app, g, Flask
|
||||||
from flask.cli import with_appcontext
|
from flask.cli import with_appcontext
|
||||||
|
|
||||||
from tlapbot.redeems import milestone_complete
|
from tlapbot.redeems.milestones import milestone_complete
|
||||||
|
|
||||||
|
|
||||||
def get_db():
|
def get_db() -> sqlite3.Connection:
|
||||||
if 'db' not in g:
|
if 'db' not in g:
|
||||||
g.db = sqlite3.connect(
|
g.db = sqlite3.connect(
|
||||||
current_app.config['DATABASE'],
|
current_app.config['DATABASE'],
|
||||||
@ -18,14 +18,14 @@ def get_db():
|
|||||||
return g.db
|
return g.db
|
||||||
|
|
||||||
|
|
||||||
def close_db(e=None):
|
def close_db() -> None:
|
||||||
db = g.pop('db', None)
|
db: sqlite3.Connection = g.pop('db', None)
|
||||||
|
|
||||||
if db is not None:
|
if db is not None:
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
def insert_counters(db):
|
def insert_counters(db: sqlite3.Connection) -> bool:
|
||||||
for redeem, redeem_info in current_app.config['REDEEMS'].items():
|
for redeem, redeem_info in current_app.config['REDEEMS'].items():
|
||||||
if redeem_info["type"] == "counter":
|
if redeem_info["type"] == "counter":
|
||||||
try:
|
try:
|
||||||
@ -40,17 +40,30 @@ def insert_counters(db):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def init_db():
|
def insert_polls(db: sqlite3.Connection) -> bool:
|
||||||
|
for poll, poll_info in current_app.config['POLLS'].items():
|
||||||
|
for option in poll_info["options"]:
|
||||||
|
try:
|
||||||
|
db.execute(
|
||||||
|
"INSERT INTO polls(poll_name, option, points) VALUES(?, ?, 0)",
|
||||||
|
(poll, option)
|
||||||
|
)
|
||||||
|
except sqlite3.Error as e:
|
||||||
|
print(f"Failed inserting poll option {poll} {option} to db: {e.args[0]}")
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def init_db() -> bool:
|
||||||
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'))
|
||||||
|
|
||||||
if insert_counters(db):
|
return insert_counters(db)
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def clear_redeem_queue():
|
def clear_redeem_queue() -> bool:
|
||||||
db = get_db()
|
db = get_db()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -62,25 +75,50 @@ def clear_redeem_queue():
|
|||||||
)
|
)
|
||||||
db.commit()
|
db.commit()
|
||||||
except sqlite3.Error as e:
|
except sqlite3.Error as e:
|
||||||
print("Error occured deleting redeem queue:", e.args[0])
|
print("Error occurred deleting redeem queue:", e.args[0])
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def refresh_counters():
|
def refresh_counters() -> bool:
|
||||||
db = get_db()
|
db = get_db()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
db.execute("DELETE FROM counters")
|
db.execute("DELETE FROM counters")
|
||||||
db.commit()
|
db.commit()
|
||||||
except sqlite3.Error as e:
|
except sqlite3.Error as e:
|
||||||
print("Error occured deleting old counters:", e.args[0])
|
print("Error occurred deleting old counters:", e.args[0])
|
||||||
return False
|
return False
|
||||||
if insert_counters(db):
|
return insert_counters(db)
|
||||||
|
|
||||||
|
|
||||||
|
def refresh_polls() -> bool:
|
||||||
|
db = get_db()
|
||||||
|
|
||||||
|
try:
|
||||||
|
db.execute("DELETE FROM polls")
|
||||||
|
db.commit()
|
||||||
|
except sqlite3.Error as e:
|
||||||
|
print("Error occurred deleting old counters:", e.args[0])
|
||||||
|
return False
|
||||||
|
return insert_polls(db)
|
||||||
|
|
||||||
|
|
||||||
|
def reset_poll(poll: str) -> bool:
|
||||||
|
try:
|
||||||
|
db = get_db()
|
||||||
|
db.execute(
|
||||||
|
"UPDATE polls SET points = 0 WHERE poll_name = ?",
|
||||||
|
(poll,)
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
return True
|
return True
|
||||||
|
except sqlite3.Error as e:
|
||||||
|
current_app.logger.error(f"Error occurred adding a milestone: {e.args[0]}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def refresh_milestones():
|
def refresh_milestones() -> bool:
|
||||||
db = get_db()
|
db = get_db()
|
||||||
# delete old milestones
|
# delete old milestones
|
||||||
try:
|
try:
|
||||||
@ -111,7 +149,7 @@ def refresh_milestones():
|
|||||||
result = cursor.fetchone()
|
result = cursor.fetchone()
|
||||||
if result is None:
|
if result is None:
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"INSERT INTO milestones(name, progress, goal, complete) VALUES(?, 0, ?, FALSE)",
|
"INSERT INTO milestones(name, progress, goal) VALUES(?, 0, ?)",
|
||||||
(redeem, redeem_info['goal'])
|
(redeem, redeem_info['goal'])
|
||||||
)
|
)
|
||||||
# update existing milestone to new goal
|
# update existing milestone to new goal
|
||||||
@ -127,7 +165,7 @@ def refresh_milestones():
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def reset_milestone(milestone):
|
def reset_milestone(milestone: str) -> bool:
|
||||||
if milestone not in current_app.config['REDEEMS']:
|
if milestone not in current_app.config['REDEEMS']:
|
||||||
print(f"Failed resetting milestone, {milestone} not in redeems file.")
|
print(f"Failed resetting milestone, {milestone} not in redeems file.")
|
||||||
return False
|
return False
|
||||||
@ -138,19 +176,19 @@ def reset_milestone(milestone):
|
|||||||
(milestone,)
|
(milestone,)
|
||||||
)
|
)
|
||||||
db.execute(
|
db.execute(
|
||||||
"INSERT INTO milestones(name, progress, goal, complete) VALUES(?, ?, ?, FALSE)",
|
"INSERT INTO milestones(name, progress, goal) VALUES(?, ?, ?)",
|
||||||
(milestone, 0, current_app.config['REDEEMS'][milestone]['goal'])
|
(milestone, 0, current_app.config['REDEEMS'][milestone]['goal'])
|
||||||
)
|
)
|
||||||
db.commit()
|
db.commit()
|
||||||
return True
|
return True
|
||||||
except sqlite3.Error as e:
|
except sqlite3.Error as e:
|
||||||
current_app.logger.error(f"Error occured adding a milestone: {e.args[0]}")
|
current_app.logger.error(f"Error occurred adding a milestone: {e.args[0]}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@click.command('init-db')
|
@click.command('init-db')
|
||||||
@with_appcontext
|
@with_appcontext
|
||||||
def init_db_command():
|
def init_db_command() -> None:
|
||||||
"""Clear the existing data and create new tables."""
|
"""Clear the existing data and create new tables."""
|
||||||
if init_db():
|
if init_db():
|
||||||
click.echo('Initialized the database.')
|
click.echo('Initialized the database.')
|
||||||
@ -158,7 +196,7 @@ def init_db_command():
|
|||||||
|
|
||||||
@click.command('clear-queue')
|
@click.command('clear-queue')
|
||||||
@with_appcontext
|
@with_appcontext
|
||||||
def clear_queue_command():
|
def clear_queue_command() -> None:
|
||||||
"""Remove all redeems from the redeem queue."""
|
"""Remove all redeems from the redeem queue."""
|
||||||
if clear_redeem_queue():
|
if clear_redeem_queue():
|
||||||
click.echo('Cleared redeem queue.')
|
click.echo('Cleared redeem queue.')
|
||||||
@ -166,7 +204,7 @@ def clear_queue_command():
|
|||||||
|
|
||||||
@click.command('refresh-counters')
|
@click.command('refresh-counters')
|
||||||
@with_appcontext
|
@with_appcontext
|
||||||
def refresh_counters_command():
|
def refresh_counters_command() -> None:
|
||||||
"""Refresh counters from current config file.
|
"""Refresh counters from current config file.
|
||||||
(Remove old ones, add new ones.)"""
|
(Remove old ones, add new ones.)"""
|
||||||
if refresh_counters():
|
if refresh_counters():
|
||||||
@ -175,7 +213,7 @@ def refresh_counters_command():
|
|||||||
|
|
||||||
@click.command('clear-refresh')
|
@click.command('clear-refresh')
|
||||||
@with_appcontext
|
@with_appcontext
|
||||||
def refresh_and_clear_command():
|
def refresh_and_clear_command() -> None:
|
||||||
"""Refresh counters and clear queue."""
|
"""Refresh counters and clear queue."""
|
||||||
if refresh_counters() and clear_redeem_queue():
|
if refresh_counters() and clear_redeem_queue():
|
||||||
click.echo('Counters refreshed and queue cleared.')
|
click.echo('Counters refreshed and queue cleared.')
|
||||||
@ -183,7 +221,7 @@ def refresh_and_clear_command():
|
|||||||
|
|
||||||
@click.command('refresh-milestones')
|
@click.command('refresh-milestones')
|
||||||
@with_appcontext
|
@with_appcontext
|
||||||
def refresh_milestones_command():
|
def refresh_milestones_command() -> None:
|
||||||
"""Initialize all milestones from the redeems file,
|
"""Initialize all milestones from the redeems file,
|
||||||
delete milestones not in redeem file."""
|
delete milestones not in redeem file."""
|
||||||
if refresh_milestones():
|
if refresh_milestones():
|
||||||
@ -192,7 +230,7 @@ def refresh_milestones_command():
|
|||||||
|
|
||||||
@click.command('reset-milestone')
|
@click.command('reset-milestone')
|
||||||
@click.argument('milestone')
|
@click.argument('milestone')
|
||||||
def reset_milestone_command(milestone):
|
def reset_milestone_command(milestone: str) -> None:
|
||||||
"""Resets a completed milestone back to zero."""
|
"""Resets a completed milestone back to zero."""
|
||||||
if milestone_complete(get_db(), milestone):
|
if milestone_complete(get_db(), milestone):
|
||||||
if reset_milestone(milestone):
|
if reset_milestone(milestone):
|
||||||
@ -204,12 +242,31 @@ def reset_milestone_command(milestone):
|
|||||||
|
|
||||||
@click.command('hard-reset-milestone')
|
@click.command('hard-reset-milestone')
|
||||||
@click.argument('milestone')
|
@click.argument('milestone')
|
||||||
def hard_reset_milestone_command(milestone):
|
def hard_reset_milestone_command(milestone: str) -> None:
|
||||||
"""Resets any milestone back to zero."""
|
"""Resets any milestone back to zero."""
|
||||||
if reset_milestone(milestone):
|
if reset_milestone(milestone):
|
||||||
click.echo(f"Hard reset milestone {milestone}.")
|
click.echo(f"Hard reset milestone {milestone}.")
|
||||||
|
|
||||||
|
|
||||||
def init_app(app):
|
@click.command('refresh-polls')
|
||||||
|
@with_appcontext
|
||||||
|
def refresh_polls_command() -> None:
|
||||||
|
"""Initialize all polls from the polls file,
|
||||||
|
delete polls not in polls file."""
|
||||||
|
if refresh_polls():
|
||||||
|
click.echo('Refreshed polls.')
|
||||||
|
|
||||||
|
|
||||||
|
@click.command('reset-polls')
|
||||||
|
@click.argument('milestone')
|
||||||
|
def reset_poll_command(poll: str) -> None:
|
||||||
|
"""Resets polls progress back to zero."""
|
||||||
|
if reset_poll(poll):
|
||||||
|
click.echo(f"Reset poll {poll}.")
|
||||||
|
else:
|
||||||
|
click.echo(f"Could not reset poll {poll}.")
|
||||||
|
|
||||||
|
|
||||||
|
def init_app(app: Flask) -> None:
|
||||||
app.teardown_appcontext(close_db)
|
app.teardown_appcontext(close_db)
|
||||||
app.cli.add_command(init_db_command)
|
app.cli.add_command(init_db_command)
|
||||||
|
0
tlapbot/defaults/__init__.py
Normal file
0
tlapbot/defaults/__init__.py
Normal file
@ -8,3 +8,4 @@ LIST_REDEEMS=False
|
|||||||
ACTIVE_CATEGORIES=[]
|
ACTIVE_CATEGORIES=[]
|
||||||
GUNICORN=False
|
GUNICORN=False
|
||||||
PREFIX='!'
|
PREFIX='!'
|
||||||
|
POLLS=True
|
3
tlapbot/defaults/default_polls.py
Normal file
3
tlapbot/defaults/default_polls.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
POLLS={
|
||||||
|
"favourite_food": {"info": "Vote for your favourite food.", "options": ["pizza", "soup", "curry", "schnitzel"]}
|
||||||
|
}
|
@ -2,7 +2,7 @@ from flask import current_app
|
|||||||
from tlapbot.owncast_requests import send_chat
|
from tlapbot.owncast_requests import send_chat
|
||||||
|
|
||||||
|
|
||||||
def send_help():
|
def send_help() -> None:
|
||||||
message = []
|
message = []
|
||||||
message.append("Tlapbot gives you points for being in chat, and then allows you to spend those points. <br>")
|
message.append("Tlapbot gives you points for being in chat, and then allows you to spend those points. <br>")
|
||||||
message.append(f"People connected to chat receive {current_app.config['POINTS_AMOUNT_GIVEN']} points every {current_app.config['POINTS_CYCLE_TIME']} seconds. <br>")
|
message.append(f"People connected to chat receive {current_app.config['POINTS_AMOUNT_GIVEN']} points every {current_app.config['POINTS_CYCLE_TIME']} seconds. <br>")
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
from flask import current_app
|
from flask import current_app
|
||||||
from sqlite3 import Error
|
from sqlite3 import Error, Connection
|
||||||
from re import sub
|
from re import sub
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
|
|
||||||
# # # db stuff # # #
|
# # # db stuff # # #
|
||||||
def read_users_points(db, user_id):
|
def read_users_points(db: Connection, user_id: str) -> int | None:
|
||||||
"""Errors out if user doesn't exist."""
|
"""Returns None and logs error in case of error, or 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 = ?",
|
||||||
@ -13,11 +14,12 @@ def read_users_points(db, user_id):
|
|||||||
)
|
)
|
||||||
return cursor.fetchone()[0]
|
return cursor.fetchone()[0]
|
||||||
except Error as e:
|
except Error as e:
|
||||||
current_app.logger.error(f"Error occured reading points: {e.args[0]}")
|
current_app.logger.error(f"Error occurred reading points: {e.args[0]}")
|
||||||
current_app.logger.error(f"To user: {user_id}")
|
current_app.logger.error(f"Of user: {user_id}")
|
||||||
|
|
||||||
|
|
||||||
def read_all_users_with_username(db, username):
|
def read_all_users_with_username(db: Connection, username: str) -> list[Tuple[str, int]] | None:
|
||||||
|
"""Returns None only if Error was logged."""
|
||||||
try:
|
try:
|
||||||
cursor = db.execute(
|
cursor = db.execute(
|
||||||
"SELECT name, points FROM points WHERE name = ?",
|
"SELECT name, points FROM points WHERE name = ?",
|
||||||
@ -26,11 +28,11 @@ def read_all_users_with_username(db, username):
|
|||||||
users = cursor.fetchall()
|
users = cursor.fetchall()
|
||||||
return users
|
return users
|
||||||
except Error as e:
|
except Error as e:
|
||||||
current_app.logger.error(f"Error occured reading points by username: {e.args[0]}")
|
current_app.logger.error(f"Error occurred reading points by username: {e.args[0]}")
|
||||||
current_app.logger.error(f"To everyone with username: {username}")
|
current_app.logger.error(f"Of everyone with username: {username}")
|
||||||
|
|
||||||
|
|
||||||
def give_points_to_user(db, user_id, points):
|
def give_points_to_user(db: Connection, user_id: str, points: int) -> None:
|
||||||
try:
|
try:
|
||||||
db.execute(
|
db.execute(
|
||||||
"UPDATE points SET points = points + ? WHERE id = ?",
|
"UPDATE points SET points = points + ? WHERE id = ?",
|
||||||
@ -38,11 +40,11 @@ def give_points_to_user(db, user_id, points):
|
|||||||
)
|
)
|
||||||
db.commit()
|
db.commit()
|
||||||
except Error as e:
|
except Error as e:
|
||||||
current_app.logger.error(f"Error occured giving points: {e.args[0]}")
|
current_app.logger.error(f"Error occurred giving points: {e.args[0]}")
|
||||||
current_app.logger.error(f"To user: {user_id} amount of points: {points}")
|
current_app.logger.error(f"To user: {user_id} amount of points: {points}")
|
||||||
|
|
||||||
|
|
||||||
def use_points(db, user_id, points):
|
def use_points(db: Connection, user_id: str, points: int) -> bool:
|
||||||
try:
|
try:
|
||||||
db.execute(
|
db.execute(
|
||||||
"UPDATE points SET points = points - ? WHERE id = ?",
|
"UPDATE points SET points = points - ? WHERE id = ?",
|
||||||
@ -51,12 +53,13 @@ def use_points(db, user_id, points):
|
|||||||
db.commit()
|
db.commit()
|
||||||
return True
|
return True
|
||||||
except Error as e:
|
except Error as e:
|
||||||
current_app.logger.error(f"Error occured using points: {e.args[0]}")
|
current_app.logger.error(f"Error occurred using points: {e.args[0]}")
|
||||||
current_app.logger.error(f"From user: {user_id} amount of points: {points}")
|
current_app.logger.error(f"From user: {user_id} amount of points: {points}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def user_exists(db, user_id):
|
def user_exists(db: Connection, user_id: str) -> bool | None:
|
||||||
|
"""Returns None only if an error was logged."""
|
||||||
try:
|
try:
|
||||||
cursor = db.execute(
|
cursor = db.execute(
|
||||||
"SELECT points FROM points WHERE id = ?",
|
"SELECT points FROM points WHERE id = ?",
|
||||||
@ -66,11 +69,11 @@ def user_exists(db, user_id):
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
except Error as e:
|
except Error as e:
|
||||||
current_app.logger.error(f"Error occured checking if user exists: {e.args[0]}")
|
current_app.logger.error(f"Error occurred checking if user exists: {e.args[0]}")
|
||||||
current_app.logger.error(f"To user: {user_id}")
|
current_app.logger.error(f"To user: {user_id}")
|
||||||
|
|
||||||
|
|
||||||
def add_user_to_database(db, user_id, display_name):
|
def add_user_to_database(db: Connection, user_id: str, display_name: str) -> None:
|
||||||
""" Adds a new user to the database. Does nothing if user is already in."""
|
""" Adds a new user to the database. Does nothing if user is already in."""
|
||||||
try:
|
try:
|
||||||
cursor = db.execute(
|
cursor = db.execute(
|
||||||
@ -92,11 +95,11 @@ def add_user_to_database(db, user_id, display_name):
|
|||||||
)
|
)
|
||||||
db.commit()
|
db.commit()
|
||||||
except Error as e:
|
except Error as e:
|
||||||
current_app.logger.error(f"Error occured adding user to db: {e.args[0]}")
|
current_app.logger.error(f"Error occurred adding user to db: {e.args[0]}")
|
||||||
current_app.logger.error(f"To user id: {user_id}, with display name: {display_name}")
|
current_app.logger.error(f"To user id: {user_id}, with display name: {display_name}")
|
||||||
|
|
||||||
|
|
||||||
def change_display_name(db, user_id, new_name):
|
def change_display_name(db: Connection, user_id: str, new_name: str) -> None:
|
||||||
try:
|
try:
|
||||||
db.execute(
|
db.execute(
|
||||||
"UPDATE points SET name = ? WHERE id = ?",
|
"UPDATE points SET name = ? WHERE id = ?",
|
||||||
@ -104,11 +107,11 @@ def change_display_name(db, user_id, new_name):
|
|||||||
)
|
)
|
||||||
db.commit()
|
db.commit()
|
||||||
except Error as e:
|
except Error as e:
|
||||||
current_app.logger.error(f"Error occured changing display name: {e.args[0]}")
|
current_app.logger.error(f"Error occurred changing display name: {e.args[0]}")
|
||||||
current_app.logger.error(f"To user id: {user_id}, with display name: {new_name}")
|
current_app.logger.error(f"To user id: {user_id}, with display name: {new_name}")
|
||||||
|
|
||||||
|
|
||||||
def remove_duplicate_usernames(db, user_id, username):
|
def remove_duplicate_usernames(db: Connection, user_id: str, username: str) -> None:
|
||||||
try:
|
try:
|
||||||
db.execute(
|
db.execute(
|
||||||
"""UPDATE points
|
"""UPDATE points
|
||||||
@ -118,12 +121,12 @@ def remove_duplicate_usernames(db, user_id, username):
|
|||||||
)
|
)
|
||||||
db.commit()
|
db.commit()
|
||||||
except Error as e:
|
except Error as e:
|
||||||
current_app.logger.error(f"Error occured removing duplicate usernames: {e.args[0]}")
|
current_app.logger.error(f"Error occurred removing duplicate usernames: {e.args[0]}")
|
||||||
|
|
||||||
|
|
||||||
# # # misc. stuff # # #
|
# # # misc. stuff # # #
|
||||||
# This is now unused since rawBody attribute of the webhook now returns cleaned-up emotes.
|
# This is now unused since rawBody attribute of the webhook now returns cleaned-up emotes.
|
||||||
def remove_emoji(message):
|
def remove_emoji(message: str) -> str:
|
||||||
return sub(
|
return sub(
|
||||||
r'<img class="emoji" alt="(:.*?:)" title=":.*?:" src="/img/emoji/.*?">',
|
r'<img class="emoji" alt="(:.*?:)" title=":.*?:" src="/img/emoji/.*?">',
|
||||||
r'\1',
|
r'\1',
|
||||||
|
@ -1,28 +1,30 @@
|
|||||||
import requests
|
import requests
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
from tlapbot.owncast_helpers import give_points_to_user
|
from tlapbot.owncast_helpers import give_points_to_user
|
||||||
|
from sqlite3 import Connection
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
def is_stream_live():
|
def is_stream_live() -> bool:
|
||||||
url = current_app.config['OWNCAST_INSTANCE_URL'] + '/api/status'
|
url = current_app.config['OWNCAST_INSTANCE_URL'] + '/api/status'
|
||||||
try:
|
try:
|
||||||
r = requests.get(url)
|
r = requests.get(url)
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
current_app.logger.error(f"Error occured checking if stream is live: {e.args[0]}")
|
current_app.logger.error(f"Error occurred checking if stream is live: {e.args[0]}")
|
||||||
return False
|
return False
|
||||||
return r.json()["online"]
|
return r.json()["online"]
|
||||||
|
|
||||||
|
|
||||||
def give_points_to_chat(db):
|
def give_points_to_chat(db: Connection) -> None:
|
||||||
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']}
|
||||||
try:
|
try:
|
||||||
r = requests.get(url, headers=headers)
|
r = requests.get(url, headers=headers)
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
current_app.logger.error(f"Error occured getting users to give points to: {e.args[0]}")
|
current_app.logger.error(f"Error occurred getting users to give points to: {e.args[0]}")
|
||||||
return
|
return
|
||||||
if r.status_code != 200:
|
if r.status_code != 200:
|
||||||
current_app.logger.error(f"Error occured when giving points: Response code not 200.")
|
current_app.logger.error(f"Error occurred when giving points: Response code not 200.")
|
||||||
current_app.logger.error(f"Response code received: {r.status_code}.")
|
current_app.logger.error(f"Response code received: {r.status_code}.")
|
||||||
current_app.logger.error(f"Check owncast instance url and access key.")
|
current_app.logger.error(f"Check owncast instance url and access key.")
|
||||||
return
|
return
|
||||||
@ -31,16 +33,16 @@ def give_points_to_chat(db):
|
|||||||
give_points_to_user(db, user_id, current_app.config['POINTS_AMOUNT_GIVEN'])
|
give_points_to_user(db, user_id, current_app.config['POINTS_AMOUNT_GIVEN'])
|
||||||
|
|
||||||
|
|
||||||
def send_chat(message):
|
def send_chat(message: str) -> Any:
|
||||||
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']}
|
||||||
try:
|
try:
|
||||||
r = requests.post(url, headers=headers, json={"body": message})
|
r = requests.post(url, headers=headers, json={"body": message})
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
current_app.logger.error(f"Error occured sending chat message: {e.args[0]}")
|
current_app.logger.error(f"Error occurred sending chat message: {e.args[0]}")
|
||||||
return
|
return
|
||||||
if r.status_code != 200:
|
if r.status_code != 200:
|
||||||
current_app.logger.error(f"Error occured when sending chat: Response code not 200.")
|
current_app.logger.error(f"Error occurred when sending chat: Response code not 200.")
|
||||||
current_app.logger.error(f"Response code received: {r.status_code}.")
|
current_app.logger.error(f"Response code received: {r.status_code}.")
|
||||||
current_app.logger.error(f"Check owncast instance url and access key.")
|
current_app.logger.error(f"Check owncast instance url and access key.")
|
||||||
return
|
return
|
||||||
|
@ -1,19 +1,25 @@
|
|||||||
from flask import Flask, request, json, Blueprint, current_app
|
from flask import Flask, request, json, Blueprint, current_app
|
||||||
|
from sqlite3 import Connection
|
||||||
|
from typing import Any
|
||||||
from tlapbot.db import get_db
|
from tlapbot.db import get_db
|
||||||
from tlapbot.owncast_requests import send_chat
|
from tlapbot.owncast_requests import send_chat
|
||||||
from tlapbot.owncast_helpers import (add_user_to_database, change_display_name,
|
from tlapbot.owncast_helpers import (add_user_to_database, change_display_name,
|
||||||
read_users_points, remove_duplicate_usernames)
|
read_users_points, remove_duplicate_usernames)
|
||||||
from tlapbot.help_message import send_help
|
from tlapbot.help_message import send_help
|
||||||
from tlapbot.redeems_handler import handle_redeem
|
from tlapbot.redeems_handler import handle_redeem
|
||||||
|
from tlapbot.poll_handler import handle_poll_vote
|
||||||
|
|
||||||
|
|
||||||
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() -> Any | None:
|
||||||
data = request.json
|
"""Reads webhook json -- adds new users, removes duplicate usernames,
|
||||||
db = get_db()
|
handles name changes and chat messages with commands.
|
||||||
|
Returns the 'data' json from the request."""
|
||||||
|
data: Any | None = request.json
|
||||||
|
db: Connection = get_db()
|
||||||
|
|
||||||
# Make sure user is in db before doing anything else.
|
# Make sure user is in db before doing anything else.
|
||||||
if data["type"] in ["CHAT", "NAME_CHANGED", "USER_JOINED"]:
|
if data["type"] in ["CHAT", "NAME_CHANGED", "USER_JOINED"]:
|
||||||
@ -52,6 +58,8 @@ def owncast_webhook():
|
|||||||
change_display_name(db, user_id, display_name)
|
change_display_name(db, user_id, display_name)
|
||||||
if data["eventData"]["user"]["authenticated"]:
|
if data["eventData"]["user"]["authenticated"]:
|
||||||
remove_duplicate_usernames(db, user_id, display_name)
|
remove_duplicate_usernames(db, user_id, display_name)
|
||||||
|
elif data["eventData"]["rawBody"].startswith(f"{prefix}vote"):
|
||||||
|
handle_poll_vote(data["eventData"]["rawBody"], user_id)
|
||||||
elif data["eventData"]["rawBody"].startswith(prefix):
|
elif data["eventData"]["rawBody"].startswith(prefix):
|
||||||
handle_redeem(data["eventData"]["rawBody"], user_id)
|
handle_redeem(data["eventData"]["rawBody"], user_id)
|
||||||
return data
|
return data
|
||||||
|
31
tlapbot/poll_handler.py
Normal file
31
tlapbot/poll_handler.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
from sqlite3 import Connection
|
||||||
|
from tlapbot.db import get_db
|
||||||
|
from tlapbot.owncast_requests import send_chat
|
||||||
|
from tlapbot.owncast_helpers import read_users_points, use_points
|
||||||
|
from tlapbot.redeems.polls import poll_option_exists, vote_in_poll
|
||||||
|
|
||||||
|
|
||||||
|
def handle_poll_vote(message: str, user_id: str) -> None:
|
||||||
|
split_message = message[5:].split(maxsplit=1)
|
||||||
|
if len(split_message < 3):
|
||||||
|
send_chat("Can't vote for poll, not enough arguments in message.")
|
||||||
|
poll_name = split_message[0]
|
||||||
|
option = split_message[1]
|
||||||
|
poll_points = split_message[2]
|
||||||
|
|
||||||
|
db: Connection = get_db()
|
||||||
|
if not poll_option_exists(db, poll_name, option):
|
||||||
|
send_chat("Can't vote for poll, poll and option combination not found.")
|
||||||
|
return
|
||||||
|
if not poll_points.isdigit():
|
||||||
|
send_chat(f"Can't vote in {poll_name} poll, donation amount is not a number.")
|
||||||
|
return
|
||||||
|
user_points = read_users_points(db, user_id)
|
||||||
|
if not user_points:
|
||||||
|
send_chat(f"Can't vote in {poll_name} poll, failed to read users' points.")
|
||||||
|
return
|
||||||
|
if user_points < poll_points:
|
||||||
|
send_chat(f"Can't vote in {poll_name} poll, you're voting with more points than you have.")
|
||||||
|
if (vote_in_poll(db, poll_name, option, poll_points) and
|
||||||
|
use_points(db, user_id, poll_points)):
|
||||||
|
send_chat(f"{poll_points} donated to {option} in poll {poll_name}")
|
@ -1,227 +0,0 @@
|
|||||||
from flask import current_app
|
|
||||||
from sqlite3 import Error
|
|
||||||
from tlapbot.owncast_helpers import use_points
|
|
||||||
|
|
||||||
def counter_exists(db, counter_name):
|
|
||||||
try:
|
|
||||||
cursor = db.execute(
|
|
||||||
"SELECT count FROM counters WHERE name = ?",
|
|
||||||
(counter_name,)
|
|
||||||
)
|
|
||||||
counter = cursor.fetchone()
|
|
||||||
if counter is None:
|
|
||||||
current_app.logger.warning("Counter not found in database.")
|
|
||||||
current_app.logger.warning("Maybe you forgot to run the refresh-counters CLI command "
|
|
||||||
"after you added a new counter to the config?")
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
except Error as e:
|
|
||||||
current_app.logger.error(f"Error occured checking if counter exists: {e.args[0]}")
|
|
||||||
current_app.logger.error(f"For counter: {counter_name}")
|
|
||||||
|
|
||||||
|
|
||||||
def add_to_counter(db, counter_name):
|
|
||||||
if counter_exists(db, counter_name):
|
|
||||||
try:
|
|
||||||
db.execute(
|
|
||||||
"UPDATE counters SET count = count + 1 WHERE name = ?",
|
|
||||||
(counter_name,)
|
|
||||||
)
|
|
||||||
db.commit()
|
|
||||||
return True
|
|
||||||
except Error as e:
|
|
||||||
current_app.logger.error(f"Error occured adding to counter: {e.args[0]}")
|
|
||||||
current_app.logger.error(f"To counter: {counter_name}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def add_to_redeem_queue(db, user_id, redeem_name, note=None):
|
|
||||||
try:
|
|
||||||
db.execute(
|
|
||||||
"INSERT INTO redeem_queue(redeem, redeemer_id, note) VALUES(?, ?, ?)",
|
|
||||||
(redeem_name, user_id, note)
|
|
||||||
)
|
|
||||||
db.commit()
|
|
||||||
return True
|
|
||||||
except Error as e:
|
|
||||||
current_app.logger.error(f"Error occured adding to redeem queue: {e.args[0]}")
|
|
||||||
current_app.logger.error(f"To user: {user_id} with redeem: {redeem_name} with note: {note}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def add_to_milestone(db, user_id, redeem_name, points_donated):
|
|
||||||
try:
|
|
||||||
cursor = db.execute(
|
|
||||||
"SELECT progress, goal FROM milestones WHERE name = ?",
|
|
||||||
(redeem_name,)
|
|
||||||
)
|
|
||||||
row = cursor.fetchone()
|
|
||||||
if row is None:
|
|
||||||
current_app.logger.warning("Milestone not found in database.")
|
|
||||||
current_app.logger.warning("Maybe you forgot to run the refresh-milestones CLI command "
|
|
||||||
"after you added a new milestone to the config?")
|
|
||||||
return False
|
|
||||||
progress, goal = row
|
|
||||||
if progress + points_donated > goal:
|
|
||||||
points_donated = goal - progress
|
|
||||||
if points_donated < 0:
|
|
||||||
points_donated = 0
|
|
||||||
if use_points(db, user_id, points_donated):
|
|
||||||
cursor = db.execute(
|
|
||||||
"UPDATE milestones SET progress = ? WHERE name = ?",
|
|
||||||
(progress + points_donated, redeem_name)
|
|
||||||
)
|
|
||||||
db.commit()
|
|
||||||
return True
|
|
||||||
except Error as e:
|
|
||||||
current_app.logger.error(f"Error occured updating milestone: {e.args[0]}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def milestone_complete(db, redeem_name):
|
|
||||||
try:
|
|
||||||
cursor = db.execute(
|
|
||||||
"SELECT complete FROM milestones WHERE name = ?",
|
|
||||||
(redeem_name,)
|
|
||||||
)
|
|
||||||
row = cursor.fetchone()
|
|
||||||
if row is None:
|
|
||||||
current_app.logger.warning("Milestone not found in database.")
|
|
||||||
current_app.logger.warning("Maybe you forgot to run the refresh-milestones CLI command "
|
|
||||||
"after you added a new milestone to the config?")
|
|
||||||
else:
|
|
||||||
return row[0]
|
|
||||||
except Error as e:
|
|
||||||
current_app.logger.error(f"Error occured checking if milestone is complete: {e.args[0]}")
|
|
||||||
|
|
||||||
|
|
||||||
def check_apply_milestone_completion(db, redeem_name):
|
|
||||||
try:
|
|
||||||
cursor = db.execute(
|
|
||||||
"SELECT progress, goal FROM milestones WHERE name = ?",
|
|
||||||
(redeem_name,)
|
|
||||||
)
|
|
||||||
row = cursor.fetchone()
|
|
||||||
if row is None:
|
|
||||||
current_app.logger.warning("Milestone not found in database.")
|
|
||||||
current_app.logger.warning("Maybe you forgot to run the refresh-milestones CLI command "
|
|
||||||
"after you added a new milestone to the config?")
|
|
||||||
else:
|
|
||||||
progress, goal = row
|
|
||||||
if progress == goal:
|
|
||||||
cursor = db.execute(
|
|
||||||
"UPDATE milestones SET complete = TRUE WHERE name = ?",
|
|
||||||
(redeem_name,)
|
|
||||||
)
|
|
||||||
db.commit()
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
except Error as e:
|
|
||||||
current_app.logger.error(f"Error occured applying milestone completion: {e.args[0]}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def all_milestones(db):
|
|
||||||
try:
|
|
||||||
cursor = db.execute(
|
|
||||||
"""SELECT name, progress, goal FROM milestones"""
|
|
||||||
)
|
|
||||||
return cursor.fetchall()
|
|
||||||
except Error as e:
|
|
||||||
current_app.logger.error(f"Error occured selecting all milestones: {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:
|
|
||||||
current_app.logger.error(f"Error occured selecting all counters: {e.args[0]}")
|
|
||||||
|
|
||||||
|
|
||||||
def all_active_counters(db):
|
|
||||||
counters = all_counters(db)
|
|
||||||
all_active_counters = []
|
|
||||||
for name, count in counters:
|
|
||||||
if is_redeem_active(name):
|
|
||||||
all_active_counters.append((name, count))
|
|
||||||
return all_active_counters
|
|
||||||
|
|
||||||
|
|
||||||
def all_active_milestones(db):
|
|
||||||
milestones = all_milestones(db)
|
|
||||||
all_active_milestones = []
|
|
||||||
for name, progress, goal in milestones:
|
|
||||||
if is_redeem_active(name):
|
|
||||||
all_active_milestones.append((name, progress, goal))
|
|
||||||
return all_active_milestones
|
|
||||||
|
|
||||||
|
|
||||||
def all_active_redeems():
|
|
||||||
redeems = current_app.config['REDEEMS']
|
|
||||||
all_active_redeems = {}
|
|
||||||
for redeem_name, redeem_dict in redeems.items():
|
|
||||||
if redeem_dict.get('category', None):
|
|
||||||
for category in redeem_dict['category']:
|
|
||||||
if category in current_app.config['ACTIVE_CATEGORIES']:
|
|
||||||
all_active_redeems[redeem_name] = redeem_dict
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
all_active_redeems[redeem_name] = redeem_dict
|
|
||||||
return all_active_redeems
|
|
||||||
|
|
||||||
|
|
||||||
def pretty_redeem_queue(db):
|
|
||||||
try:
|
|
||||||
cursor = db.execute(
|
|
||||||
"""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"""
|
|
||||||
)
|
|
||||||
return cursor.fetchall()
|
|
||||||
except Error as e:
|
|
||||||
current_app.logger.error(f"Error occured selecting pretty redeem queue: {e.args[0]}")
|
|
||||||
|
|
||||||
|
|
||||||
def whole_redeem_queue(db):
|
|
||||||
try:
|
|
||||||
cursor = db.execute(
|
|
||||||
"SELECT * from redeem_queue"
|
|
||||||
)
|
|
||||||
return cursor.fetchall()
|
|
||||||
except Error as e:
|
|
||||||
current_app.logger.error(f"Error occured selecting redeem queue: {e.args[0]}")
|
|
||||||
|
|
||||||
|
|
||||||
def is_redeem_active(redeem_name):
|
|
||||||
"""Checks if redeem is active. Pulls the redeem by name from config."""
|
|
||||||
active_categories = current_app.config['ACTIVE_CATEGORIES']
|
|
||||||
redeem_dict = current_app.config['REDEEMS'].get(redeem_name, None)
|
|
||||||
if redeem_dict:
|
|
||||||
if "category" in redeem_dict:
|
|
||||||
for category in redeem_dict["category"]:
|
|
||||||
if category in active_categories:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
return True
|
|
||||||
return None # redeem does not exist, unknown active state
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def is_redeem_from_config_active(redeem, active_categories):
|
|
||||||
"""Checks if redeem is active. `redeem` is a whole key:value pair from redeems config."""
|
|
||||||
if "category" in redeem[1] and redeem[1]["category"]:
|
|
||||||
for category in redeem[1]["category"]:
|
|
||||||
if category in active_categories:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def remove_inactive_redeems(redeems, active_categories):
|
|
||||||
return dict(filter(lambda redeem: is_redeem_from_config_active(redeem, active_categories),
|
|
||||||
redeems.items()))
|
|
0
tlapbot/redeems/__init__.py
Normal file
0
tlapbot/redeems/__init__.py
Normal file
63
tlapbot/redeems/counters.py
Normal file
63
tlapbot/redeems/counters.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
from flask import current_app
|
||||||
|
from sqlite3 import Error, Connection
|
||||||
|
from typing import Tuple
|
||||||
|
from tlapbot.redeems.redeems import is_redeem_active
|
||||||
|
|
||||||
|
|
||||||
|
def counter_exists(db: Connection, counter_name: str) -> bool | None:
|
||||||
|
"""Returns None only if error was logged."""
|
||||||
|
try:
|
||||||
|
cursor = db.execute(
|
||||||
|
"SELECT count FROM counters WHERE name = ?",
|
||||||
|
(counter_name,)
|
||||||
|
)
|
||||||
|
counter = cursor.fetchone()
|
||||||
|
if counter is None:
|
||||||
|
current_app.logger.warning("Counter not found in database.")
|
||||||
|
current_app.logger.warning("Maybe you forgot to run the refresh-counters CLI command "
|
||||||
|
"after you added a new counter to the config?")
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
except Error as e:
|
||||||
|
current_app.logger.error(f"Error occurred checking if counter exists: {e.args[0]}")
|
||||||
|
current_app.logger.error(f"For counter: {counter_name}")
|
||||||
|
|
||||||
|
|
||||||
|
def add_to_counter(db: Connection, counter_name: str) -> bool:
|
||||||
|
if counter_exists(db, counter_name):
|
||||||
|
try:
|
||||||
|
db.execute(
|
||||||
|
"UPDATE counters SET count = count + 1 WHERE name = ?",
|
||||||
|
(counter_name,)
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
return True
|
||||||
|
except Error as e:
|
||||||
|
current_app.logger.error(f"Error occurred adding to counter: {e.args[0]}")
|
||||||
|
current_app.logger.error(f"To counter: {counter_name}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def all_counters(db: Connection) -> list[Tuple[str, int]] | None:
|
||||||
|
"""Returns list of all (even inactive) counters and their current value.
|
||||||
|
Returns None only if error was logged."""
|
||||||
|
try:
|
||||||
|
cursor = db.execute(
|
||||||
|
"""SELECT name, count FROM counters"""
|
||||||
|
)
|
||||||
|
return cursor.fetchall()
|
||||||
|
except Error as e:
|
||||||
|
current_app.logger.error(f"Error occurred selecting all counters: {e.args[0]}")
|
||||||
|
|
||||||
|
|
||||||
|
def all_active_counters(db: Connection) -> list[Tuple[str, int]] | None:
|
||||||
|
"""Returns list of all active counters, and their current value.
|
||||||
|
Returns None if error was logged."""
|
||||||
|
counters = all_counters(db)
|
||||||
|
if counters is not None:
|
||||||
|
all_active_counters = []
|
||||||
|
for name, count in counters:
|
||||||
|
if is_redeem_active(name):
|
||||||
|
all_active_counters.append((name, count))
|
||||||
|
return all_active_counters
|
||||||
|
|
80
tlapbot/redeems/milestones.py
Normal file
80
tlapbot/redeems/milestones.py
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
from flask import current_app
|
||||||
|
from sqlite3 import Error, Connection
|
||||||
|
from typing import Tuple, Any
|
||||||
|
from tlapbot.owncast_helpers import use_points
|
||||||
|
from tlapbot.redeems import is_redeem_active
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: add a milestone_exists check?
|
||||||
|
def add_to_milestone(db: Connection, user_id: str, redeem_name: str, points_donated: int) -> bool:
|
||||||
|
try:
|
||||||
|
cursor = db.execute(
|
||||||
|
"SELECT progress, goal FROM milestones WHERE name = ?",
|
||||||
|
(redeem_name,)
|
||||||
|
)
|
||||||
|
row = cursor.fetchone()
|
||||||
|
if row is None:
|
||||||
|
current_app.logger.warning("Milestone not found in database.")
|
||||||
|
current_app.logger.warning("Maybe you forgot to run the refresh-milestones CLI command "
|
||||||
|
"after you added a new milestone to the config?")
|
||||||
|
return False
|
||||||
|
progress, goal = row
|
||||||
|
if progress + points_donated > goal:
|
||||||
|
points_donated = goal - progress
|
||||||
|
if points_donated < 0:
|
||||||
|
points_donated = 0
|
||||||
|
if use_points(db, user_id, points_donated):
|
||||||
|
cursor = db.execute(
|
||||||
|
"UPDATE milestones SET progress = ? WHERE name = ?",
|
||||||
|
(progress + points_donated, redeem_name)
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
return True
|
||||||
|
except Error as e:
|
||||||
|
current_app.logger.error(f"Error occurred updating milestone: {e.args[0]}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def milestone_complete(db: Connection, redeem_name: str) -> bool | None:
|
||||||
|
"""Returns None only if error was logged."""
|
||||||
|
try:
|
||||||
|
cursor = db.execute(
|
||||||
|
"SELECT progress, goal FROM milestones WHERE name = ?",
|
||||||
|
(redeem_name,)
|
||||||
|
)
|
||||||
|
row = cursor.fetchone()
|
||||||
|
if row is None:
|
||||||
|
current_app.logger.warning("Milestone not found in database.")
|
||||||
|
current_app.logger.warning("Maybe you forgot to run the refresh-milestones CLI command "
|
||||||
|
"after you added a new milestone to the config?")
|
||||||
|
else:
|
||||||
|
progress, goal = row
|
||||||
|
if progress == goal:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
except Error as e:
|
||||||
|
current_app.logger.error(f"Error occurred checking if milestone is complete: {e.args[0]}")
|
||||||
|
|
||||||
|
|
||||||
|
def all_milestones(db: Connection) -> list[Tuple[str, int, int]] | None:
|
||||||
|
"""Returns list of all (even inactive) milestones, their progress and their goal.
|
||||||
|
Returns None only if error was logged."""
|
||||||
|
try:
|
||||||
|
cursor = db.execute(
|
||||||
|
"""SELECT name, progress, goal FROM milestones"""
|
||||||
|
)
|
||||||
|
return cursor.fetchall()
|
||||||
|
except Error as e:
|
||||||
|
current_app.logger.error(f"Error occurred selecting all milestones: {e.args[0]}")
|
||||||
|
|
||||||
|
|
||||||
|
def all_active_milestones(db: Connection) -> list[Tuple[str, int, int]] | None:
|
||||||
|
"""Returns list of all active milestones, their progress and their goal.
|
||||||
|
Returns None only if error was logged."""
|
||||||
|
milestones = all_milestones(db)
|
||||||
|
if milestones is not None:
|
||||||
|
all_active_milestones = []
|
||||||
|
for name, progress, goal in milestones:
|
||||||
|
if is_redeem_active(name):
|
||||||
|
all_active_milestones.append((name, progress, goal))
|
||||||
|
return all_active_milestones
|
||||||
|
|
38
tlapbot/redeems/polls.py
Normal file
38
tlapbot/redeems/polls.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
from flask import current_app
|
||||||
|
from sqlite3 import Error, Connection
|
||||||
|
|
||||||
|
|
||||||
|
def poll_option_exists(db: Connection, poll_name: str, option: str) -> bool | None:
|
||||||
|
"""Returns None only if error was logged."""
|
||||||
|
try:
|
||||||
|
cursor = db.execute(
|
||||||
|
"SELECT poll_name, option FROM polls WHERE poll_name = ? and option = ?",
|
||||||
|
(poll_name, option)
|
||||||
|
)
|
||||||
|
counter = cursor.fetchone()
|
||||||
|
if counter is None:
|
||||||
|
current_app.logger.warning("Poll and option combination not found in database.")
|
||||||
|
current_app.logger.warning("Maybe you forgot to run the refresh-polls CLI command "
|
||||||
|
"after you added a new poll to the config?")
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
except Error as e:
|
||||||
|
current_app.logger.error(f"Error occurred checking if poll exists: {e.args[0]}")
|
||||||
|
current_app.logger.error(f"For poll&option: {poll_name}:{option}")
|
||||||
|
|
||||||
|
|
||||||
|
def vote_in_poll(db: Connection, poll_name: str, option: str, points: int) -> bool | None:
|
||||||
|
if points <= 0:
|
||||||
|
current_app.logger.warning(f"Vote for {poll_name}:{option} was not a positive integer.")
|
||||||
|
return
|
||||||
|
if poll_option_exists(db, poll_name, option):
|
||||||
|
try:
|
||||||
|
cursor = db.execute(
|
||||||
|
"UPDATE polls SET progress = progress + ? WHERE poll_name = ? and option = ?",
|
||||||
|
(points, poll_name, option)
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
return True
|
||||||
|
except Error as e:
|
||||||
|
current_app.logger.error(f"Error occurred updating milestone: {e.args[0]}")
|
||||||
|
return False
|
74
tlapbot/redeems/redeems.py
Normal file
74
tlapbot/redeems/redeems.py
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
from flask import current_app
|
||||||
|
from sqlite3 import Error, Connection
|
||||||
|
from typing import Tuple, Any
|
||||||
|
from tlapbot.tlapbot_types import Redeems
|
||||||
|
|
||||||
|
# TODO: test if the new default works
|
||||||
|
def add_to_redeem_queue(db: Connection, user_id: str, redeem_name: str, note: str="") -> bool:
|
||||||
|
try:
|
||||||
|
db.execute(
|
||||||
|
"INSERT INTO redeem_queue(redeem, redeemer_id, note) VALUES(?, ?, ?)",
|
||||||
|
(redeem_name, user_id, note)
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
return True
|
||||||
|
except Error as e:
|
||||||
|
current_app.logger.error(f"Error occurred adding to redeem queue: {e.args[0]}")
|
||||||
|
current_app.logger.error(f"To user: {user_id} with redeem: {redeem_name} with note: {note}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def all_active_redeems() -> Redeems:
|
||||||
|
"""Returns list of all active redeems."""
|
||||||
|
redeems = current_app.config['REDEEMS']
|
||||||
|
all_active_redeems = {}
|
||||||
|
for redeem_name, redeem_dict in redeems.items():
|
||||||
|
if redeem_dict.get('category', None):
|
||||||
|
for category in redeem_dict['category']:
|
||||||
|
if category in current_app.config['ACTIVE_CATEGORIES']:
|
||||||
|
all_active_redeems[redeem_name] = redeem_dict
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
all_active_redeems[redeem_name] = redeem_dict
|
||||||
|
return all_active_redeems
|
||||||
|
|
||||||
|
|
||||||
|
def pretty_redeem_queue(db: Connection) -> list[Tuple[str, str, str, str]] | None:
|
||||||
|
"""Returns a 'pretty' redeem queue, with name of the redeemer joined instead of ID.
|
||||||
|
Returns None only if error was logged."""
|
||||||
|
try:
|
||||||
|
cursor = db.execute(
|
||||||
|
"""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"""
|
||||||
|
)
|
||||||
|
return cursor.fetchall()
|
||||||
|
except Error as e:
|
||||||
|
current_app.logger.error(f"Error occurred selecting pretty redeem queue: {e.args[0]}")
|
||||||
|
|
||||||
|
|
||||||
|
def whole_redeem_queue(db: Connection) -> list[Any] | None:
|
||||||
|
"""Returns None if error was logged."""
|
||||||
|
try:
|
||||||
|
cursor = db.execute(
|
||||||
|
"SELECT * from redeem_queue" #TODO: specify columns to fetch
|
||||||
|
)
|
||||||
|
return cursor.fetchall()
|
||||||
|
except Error as e:
|
||||||
|
current_app.logger.error(f"Error occurred selecting redeem queue: {e.args[0]}")
|
||||||
|
|
||||||
|
|
||||||
|
def is_redeem_active(redeem_name: str) -> bool | None:
|
||||||
|
"""Checks if redeem is active. Pulls the redeem by name from config.
|
||||||
|
Returns None if the redeem doesn't exist."""
|
||||||
|
active_categories = current_app.config['ACTIVE_CATEGORIES']
|
||||||
|
redeem_dict = current_app.config['REDEEMS'].get(redeem_name, None)
|
||||||
|
if redeem_dict:
|
||||||
|
if redeem_dict.get('category', None):
|
||||||
|
for category in redeem_dict["category"]:
|
||||||
|
if category in active_categories:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
return None # redeem does not exist, unknown active state
|
@ -1,12 +1,13 @@
|
|||||||
from flask import current_app
|
from flask import current_app
|
||||||
from tlapbot.db import get_db
|
from tlapbot.db import get_db
|
||||||
from tlapbot.owncast_requests import send_chat
|
from tlapbot.owncast_requests import send_chat
|
||||||
from tlapbot.redeems import (add_to_redeem_queue, add_to_counter, add_to_milestone,
|
from tlapbot.redeems.redeems import add_to_redeem_queue, is_redeem_active
|
||||||
check_apply_milestone_completion, milestone_complete, is_redeem_active)
|
from tlapbot.redeems.counters import add_to_counter
|
||||||
|
from tlapbot.redeems.milestones import add_to_milestone, milestone_complete
|
||||||
from tlapbot.owncast_helpers import use_points, read_users_points
|
from tlapbot.owncast_helpers import use_points, read_users_points
|
||||||
|
|
||||||
|
|
||||||
def handle_redeem(message, user_id):
|
def handle_redeem(message: str, user_id: str) -> None:
|
||||||
split_message = message[1:].split(maxsplit=1)
|
split_message = message[1:].split(maxsplit=1)
|
||||||
redeem = split_message[0]
|
redeem = split_message[0]
|
||||||
if len(split_message) == 1:
|
if len(split_message) == 1:
|
||||||
@ -25,6 +26,11 @@ def handle_redeem(message, user_id):
|
|||||||
redeem_type = current_app.config['REDEEMS'][redeem]["type"]
|
redeem_type = current_app.config['REDEEMS'][redeem]["type"]
|
||||||
points = read_users_points(db, user_id)
|
points = read_users_points(db, user_id)
|
||||||
|
|
||||||
|
if points is None:
|
||||||
|
send_chat(f"Can't redeem {redeem}, failed to read users' points.")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
# handle milestone first because it doesn't have a price
|
# handle milestone first because it doesn't have a price
|
||||||
if redeem_type == "milestone":
|
if redeem_type == "milestone":
|
||||||
if milestone_complete(db, redeem):
|
if milestone_complete(db, redeem):
|
||||||
@ -39,7 +45,7 @@ def handle_redeem(message, user_id):
|
|||||||
send_chat(f"Can't donate zero points.")
|
send_chat(f"Can't donate zero points.")
|
||||||
elif add_to_milestone(db, user_id, redeem, int(note)):
|
elif add_to_milestone(db, user_id, redeem, int(note)):
|
||||||
send_chat(f"Succesfully donated to {redeem} milestone!")
|
send_chat(f"Succesfully donated to {redeem} milestone!")
|
||||||
if check_apply_milestone_completion(db, redeem):
|
if milestone_complete(db, redeem):
|
||||||
send_chat(f"Milestone goal {redeem} complete!")
|
send_chat(f"Milestone goal {redeem} complete!")
|
||||||
else:
|
else:
|
||||||
send_chat(f"Redeeming milestone {redeem} failed.")
|
send_chat(f"Redeeming milestone {redeem} failed.")
|
||||||
|
@ -12,8 +12,7 @@ CREATE TABLE milestones (
|
|||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
progress INTEGER NOT NULL,
|
progress INTEGER NOT NULL,
|
||||||
goal INTEGER NOT NULL,
|
goal INTEGER NOT NULL
|
||||||
complete BOOLEAN NOT NULL
|
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE counters (
|
CREATE TABLE counters (
|
||||||
@ -30,3 +29,10 @@ CREATE TABLE redeem_queue (
|
|||||||
note TEXT,
|
note TEXT,
|
||||||
FOREIGN KEY (redeemer_id) REFERENCES points (id)
|
FOREIGN KEY (redeemer_id) REFERENCES points (id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE polls (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
points INTEGER NOT NULL,
|
||||||
|
option TEXT NOT NULL,
|
||||||
|
poll_name TEXT NOT NULL,
|
||||||
|
)
|
@ -1,6 +1,8 @@
|
|||||||
from flask import render_template, Blueprint, request, current_app
|
from flask import render_template, Blueprint, request, current_app
|
||||||
from tlapbot.db import get_db
|
from tlapbot.db import get_db
|
||||||
from tlapbot.redeems import all_active_counters, all_active_milestones, all_active_redeems, pretty_redeem_queue
|
from tlapbot.redeems.redeems import all_active_redeems, pretty_redeem_queue
|
||||||
|
from tlapbot.redeems.counters import all_active_counters
|
||||||
|
from tlapbot.redeems.milestones import all_active_milestones
|
||||||
from tlapbot.owncast_helpers import read_all_users_with_username
|
from tlapbot.owncast_helpers import read_all_users_with_username
|
||||||
from datetime import timezone
|
from datetime import timezone
|
||||||
|
|
||||||
@ -8,7 +10,7 @@ bp = Blueprint('redeem_dashboard', __name__)
|
|||||||
|
|
||||||
|
|
||||||
@bp.route('/dashboard', methods=['GET'])
|
@bp.route('/dashboard', methods=['GET'])
|
||||||
def dashboard():
|
def dashboard() -> str:
|
||||||
db = get_db()
|
db = get_db()
|
||||||
username = request.args.get("username")
|
username = request.args.get("username")
|
||||||
if username is not None:
|
if username is not None:
|
||||||
|
4
tlapbot/tlapbot_types.py
Normal file
4
tlapbot/tlapbot_types.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
from typing import Any, TypeAlias
|
||||||
|
|
||||||
|
Redeems: TypeAlias = dict[str, dict[str, Any]]
|
||||||
|
# at the moment the Any could be specialized to str | int | list[str]
|
Loading…
x
Reference in New Issue
Block a user