Compare commits
13
Commits
56cb94e1a0
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0bae46e852 | ||
|
|
a52db9f6ae | ||
|
|
924c4c547f | ||
|
|
d7fdc1e183 | ||
|
|
bd6f94a976 | ||
|
|
32596ab4e5 | ||
|
|
6f4c6b33f2 | ||
|
|
ecdd399e27 | ||
|
|
3bbfc140ef | ||
|
|
b8fb30a2ae | ||
|
|
70285958d1 | ||
|
|
9592e2daf0 | ||
|
|
d0ff884dec |
@@ -97,6 +97,16 @@ Milestone redeems can be used as long-term community challenges, to start stream
|
||||
challenges, decide new games to play, etc.
|
||||
|
||||
|
||||
#### Polls
|
||||
Poll redeems are viewer votes, choices or answers. A poll can have multiple options,
|
||||
and viewers choose which option they're voting for. The dashboard then shows all options, their number of points (=number of votes), and a progress bar indicating which poll option is leading.
|
||||
|
||||
Poll redeems can be used to decide what to do next, what game to play, or just a fun viewer interaction where people vote if they like pineapple on pizza.
|
||||
|
||||
There is no "limit" or official closure of poll redeems.
|
||||
|
||||
Polls only work if `POLLS_ENABLED` is `True` in the config.
|
||||
|
||||
## License & Contributions
|
||||
Tlapbot as it currently is does not come with a license. If you're a content creator, streamer, vtuber, etc. I'll be happy to give you permission to use Tlapbot, or make changes that'd fit your stream.
|
||||
|
||||
@@ -122,7 +132,7 @@ as a package in editable more.
|
||||
python -m flask init-db
|
||||
```
|
||||
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
|
||||
by default.)
|
||||
|
||||
@@ -133,7 +143,7 @@ by default.)
|
||||
OWNCAST_INSTANCE_URL # default points to localhost owncast on default port
|
||||
```
|
||||
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.
|
||||
7. If you've added any new counters or milestones, run `refresh-counters` or `refresh-milestones` commands to initialize them into the database.
|
||||
|
||||
@@ -282,6 +292,19 @@ Resets progress on a milestone, regardless of completion status.
|
||||
```bash
|
||||
python -m flask hard-reset-milestone milestone
|
||||
```
|
||||
|
||||
#### refresh-polls
|
||||
Resets progress on all polls, removes old polls not in config, adds new polls from config.
|
||||
```bash
|
||||
python -m flask refresh-polls
|
||||
```
|
||||
|
||||
#### reset-poll
|
||||
Resets progress on a single poll. (Can be used to add a single poll from config while not resetting the other polls.)
|
||||
```bash
|
||||
python -m flask reset-poll poll-name
|
||||
```
|
||||
|
||||
## Configuration files
|
||||
Configuration files should be in the instance folder. For folder installation of tlapbot,
|
||||
that's `instance/` from the root of the Github repository.
|
||||
@@ -299,7 +322,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_INSTANCE_URL` is the full URL of your owncast instance, like `"http://MyTlapbotServer.example"`
|
||||
#### 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,
|
||||
in seconds.
|
||||
- `POINTS_AMOUNT_GIVEN` decides how many channel points users receive.
|
||||
@@ -309,6 +332,7 @@ This makes the !help output quite long, so it's `False` by default.
|
||||
- `GUNICORN` if `True`, sets logging to use gunicorn's logger. Only set this to True if you're using Gunicorn to run tlapbot.
|
||||
- `ACTIVE_CATEGORIES` can be an empty list `[]`, or a list of strings of activated categories (i.e. `["chatting", "singing"]`). Redeems with a category included in the list will be active, redeems from other categories will not be active. Redeems with no category are always active.
|
||||
- `PREFIX` is a *single character string* that decides what character gets used as a prefix for tlapbot commands. (i.e. you can use `?` instead of `!`). Symbols are recommended. Prefix cannot be longer than one character.
|
||||
- `POLLS_ENABLED` decides if polls are enabled. `True` by default. You can disable polls by setting it to `False`.
|
||||
#### Example config:
|
||||
An example to show what your config like could look like
|
||||
```python
|
||||
@@ -320,7 +344,7 @@ LIST_REDEEMS=True
|
||||
ACTIVE_CATEGORIES=["gaming"]
|
||||
```
|
||||
### 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.)
|
||||
#### `default_redeems.py`:
|
||||
@@ -334,6 +358,7 @@ REDEEMS={
|
||||
"inactive": {"price": 100, "type": "note", "info": "Example redeem that is inactive by default", "category": ["inactive"]}
|
||||
}
|
||||
```
|
||||
|
||||
#### File format
|
||||
`redeems.py` is a config file with just a `REDEEMS` key, that assigns a dictionary of redeems to it.
|
||||
Each dictionary entry is a redeem, and the dictionary keys are strings that decide the chat command for the redeem.
|
||||
@@ -350,3 +375,14 @@ Optionally, each redeem can also have `"info"` and `"category"` entries.
|
||||
If a category from the list is in `ACTIVE_CATEGORIES` from `config.py`,
|
||||
then the redeem will be active. It will not be active if none of the categories
|
||||
are in `ACTIVE_CATEGORIES`. Redeems with no category are always active.
|
||||
|
||||
### polls.py
|
||||
`polls.py` is where you define your custom polls. Tlapbot will work without it, and you don't need to supply your file if you turned off polls.
|
||||
|
||||
(`polls.py` should be in the instance folder: `instance/polls.py` for folder install.)
|
||||
```python
|
||||
POLLS={
|
||||
"favourite_food2": {"info": "Vote for your favourite food.", "options": ["pizza", "soup", "curry", "schnitzel"]},
|
||||
"test_other_thing": {"info": "thing", "options": ["no", "yes"]}
|
||||
}
|
||||
```
|
||||
+14
-2
@@ -20,11 +20,21 @@ def create_app(test_config: None = None) -> Flask:
|
||||
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_object('tlapbot.defaults.default_config')
|
||||
app.config.from_object('tlapbot.defaults.default_redeems')
|
||||
app.config.from_pyfile('config.py', silent=True)
|
||||
app.config.from_pyfile('redeems.py', silent=True)
|
||||
|
||||
# set up polls if they're enabled
|
||||
if app.config['POLLS_ENABLED']:
|
||||
app.config.from_object('tlapbot.defaults.default_polls')
|
||||
app.config.from_pyfile('polls.py', silent=True)
|
||||
|
||||
for poll in app.config['POLLS']:
|
||||
if ' ' in poll:
|
||||
app.logger.warning(f"Poll '{poll}' has spaces in its name.")
|
||||
app.logger.warning("Poll with spaces are impossible to redeem.")
|
||||
|
||||
# Make logging work for gunicorn-ran instances of tlapbot.
|
||||
if app.config['GUNICORN']:
|
||||
gunicorn_logger = logging.getLogger('gunicorn.error')
|
||||
@@ -57,6 +67,8 @@ def create_app(test_config: None = None) -> Flask:
|
||||
app.cli.add_command(db.refresh_milestones_command)
|
||||
app.cli.add_command(db.reset_milestone_command)
|
||||
app.cli.add_command(db.hard_reset_milestone_command)
|
||||
app.cli.add_command(db.refresh_polls_command)
|
||||
app.cli.add_command(db.reset_poll_command)
|
||||
|
||||
# scheduler job for giving points to users
|
||||
def proxy_job() -> None:
|
||||
|
||||
+76
-2
@@ -4,7 +4,7 @@ import click
|
||||
from flask import current_app, g, Flask
|
||||
from flask.cli import with_appcontext
|
||||
|
||||
from tlapbot.redeems import milestone_complete
|
||||
from tlapbot.redeems.milestones import milestone_complete
|
||||
|
||||
|
||||
def get_db() -> sqlite3.Connection:
|
||||
@@ -18,7 +18,7 @@ def get_db() -> sqlite3.Connection:
|
||||
return g.db
|
||||
|
||||
|
||||
def close_db() -> None:
|
||||
def close_db(*_args) -> None:
|
||||
db: sqlite3.Connection = g.pop('db', None)
|
||||
|
||||
if db is not None:
|
||||
@@ -40,6 +40,20 @@ def insert_counters(db: sqlite3.Connection) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
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()
|
||||
|
||||
@@ -78,6 +92,47 @@ def refresh_counters() -> bool:
|
||||
return insert_counters(db)
|
||||
|
||||
|
||||
def populate_poll_options(db: sqlite3.Connection, poll: str) -> bool:
|
||||
for option in current_app.config['POLLS'][poll]['options']:
|
||||
db.execute(
|
||||
"INSERT INTO polls(poll_name, points, option) VALUES(?, ?, ?)",
|
||||
(poll, 0, option)
|
||||
)
|
||||
db.commit()
|
||||
return True
|
||||
|
||||
|
||||
def refresh_polls() -> bool:
|
||||
db = get_db()
|
||||
try:
|
||||
db.execute("DELETE FROM polls")
|
||||
db.commit()
|
||||
for poll in current_app.config['POLLS']:
|
||||
populate_poll_options(db, poll)
|
||||
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:
|
||||
if poll not in current_app.config['POLLS']:
|
||||
print(f"Failed resetting poll, {poll} not in polls file.")
|
||||
return False
|
||||
try:
|
||||
db = get_db()
|
||||
db.execute(
|
||||
"DELETE FROM polls WHERE poll_name = ?",
|
||||
(poll,)
|
||||
)
|
||||
populate_poll_options(db, poll)
|
||||
return True
|
||||
except sqlite3.Error as e:
|
||||
current_app.logger.error(f"Error occurred resetting poll: {e.args[0]}")
|
||||
return False
|
||||
|
||||
|
||||
def refresh_milestones() -> bool:
|
||||
db = get_db()
|
||||
# delete old milestones
|
||||
@@ -208,6 +263,25 @@ def hard_reset_milestone_command(milestone: str) -> None:
|
||||
click.echo(f"Hard reset milestone {milestone}.")
|
||||
|
||||
|
||||
@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-poll')
|
||||
@click.argument('poll')
|
||||
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.cli.add_command(init_db_command)
|
||||
|
||||
@@ -8,3 +8,4 @@ LIST_REDEEMS=False
|
||||
ACTIVE_CATEGORIES=[]
|
||||
GUNICORN=False
|
||||
PREFIX='!'
|
||||
POLLS_ENABLED=True
|
||||
@@ -0,0 +1,3 @@
|
||||
POLLS={
|
||||
"favourite_food": {"info": "Vote for your favourite food.", "options": ["pizza", "soup", "curry", "schnitzel"]}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ from tlapbot.owncast_helpers import (add_user_to_database, change_display_name,
|
||||
read_users_points, remove_duplicate_usernames)
|
||||
from tlapbot.help_message import send_help
|
||||
from tlapbot.redeems_handler import handle_redeem
|
||||
from tlapbot.poll_handler import handle_poll_vote
|
||||
|
||||
|
||||
bp = Blueprint('owncast_webhooks', __name__)
|
||||
@@ -57,6 +58,9 @@ def owncast_webhook() -> Any | None:
|
||||
change_display_name(db, user_id, display_name)
|
||||
if data["eventData"]["user"]["authenticated"]:
|
||||
remove_duplicate_usernames(db, user_id, display_name)
|
||||
elif data["eventData"]["rawBody"].startswith(f"{prefix}vote"):
|
||||
if current_app.config['POLLS_ENABLED']:
|
||||
handle_poll_vote(data["eventData"]["rawBody"], user_id)
|
||||
elif data["eventData"]["rawBody"].startswith(prefix):
|
||||
handle_redeem(data["eventData"]["rawBody"], user_id)
|
||||
return data
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
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=2)
|
||||
if len(split_message) < 3:
|
||||
send_chat("Can't vote for poll, not enough arguments in message.")
|
||||
return
|
||||
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
|
||||
poll_points = int(poll_points)
|
||||
if user_points < poll_points:
|
||||
send_chat(f"Can't vote in {poll_name} poll, you're voting with more points than you have.")
|
||||
return
|
||||
if (vote_in_poll(db, poll_name, option, poll_points) and
|
||||
use_points(db, user_id, poll_points)):
|
||||
send_chat(f"{poll_points} points donated to {option} in poll {poll_name}")
|
||||
@@ -1,207 +0,0 @@
|
||||
from flask import current_app
|
||||
from sqlite3 import Error, Connection
|
||||
from typing import Tuple, Any
|
||||
from tlapbot.owncast_helpers import use_points
|
||||
from tlapbot.tlapbot_types import Redeems
|
||||
|
||||
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
|
||||
|
||||
# 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 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
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
@@ -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
|
||||
|
||||
@@ -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.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
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
from flask import current_app
|
||||
from sqlite3 import Error, Connection
|
||||
|
||||
|
||||
def get_poll_votes(db: Connection, poll_name: str) -> dict[str, int] | None:
|
||||
"""Get all poll votes and point values for a given `poll_name`"""
|
||||
try:
|
||||
options = {}
|
||||
cursor = db.execute(
|
||||
"SELECT option, points from polls WHERE poll_name = ?",
|
||||
(poll_name,)
|
||||
)
|
||||
for row in cursor:
|
||||
options[row[0]] = row[1]
|
||||
return options
|
||||
except Error as e:
|
||||
current_app.logger.error(f"Error occurred getting poll votes: {e.args[0]}")
|
||||
current_app.logger.error(f"For poll {poll_name}")
|
||||
|
||||
|
||||
def all_active_polls(db: Connection) -> dict[str, dict[str, int]] | None:
|
||||
"""Returns a dict where the keys are poll names, values is a dict of options and points.
|
||||
Returns None if polls aren't enabled."""
|
||||
if not current_app.config['POLLS_ENABLED']:
|
||||
return None
|
||||
polls = current_app.config['POLLS']
|
||||
poll_votes = {}
|
||||
for poll_name in polls.keys():
|
||||
poll_votes[poll_name] = get_poll_votes(db, poll_name)
|
||||
return poll_votes
|
||||
|
||||
|
||||
def max_poll_votes(poll_dict: dict[str, int]) -> int:
|
||||
"""Returns the maximum number of poll votes/points. Input is the output of `get_poll_votes`"""
|
||||
highest_points = 0
|
||||
for _, points in poll_dict.items():
|
||||
if points > highest_points:
|
||||
highest_points = points
|
||||
return highest_points
|
||||
|
||||
|
||||
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 points = points + ? 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 poll: {e.args[0]}")
|
||||
return False
|
||||
@@ -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,8 +1,9 @@
|
||||
from flask import current_app
|
||||
from tlapbot.db import get_db
|
||||
from tlapbot.owncast_requests import send_chat
|
||||
from tlapbot.redeems import (add_to_redeem_queue, add_to_counter, add_to_milestone,
|
||||
milestone_complete, is_redeem_active)
|
||||
from tlapbot.redeems.redeems import add_to_redeem_queue, 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
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
DROP TABLE IF EXISTS counters;
|
||||
DROP TABLE IF EXISTS redeem_queue;
|
||||
DROP TABLE IF EXISTS milestones;
|
||||
DROP TABLE IF EXISTS polls;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS points (
|
||||
id TEXT PRIMARY KEY,
|
||||
@@ -29,3 +30,10 @@ CREATE TABLE redeem_queue (
|
||||
note TEXT,
|
||||
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
|
||||
);
|
||||
@@ -47,6 +47,31 @@
|
||||
{% endif %}
|
||||
|
||||
{% if not passive %}
|
||||
{% if polls %}
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">Active polls</th>
|
||||
<th colspan="2">Points</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for poll_name, poll_dict in polls.items() %}
|
||||
{% for option in poll_dict.keys() %}
|
||||
{% set max_vote = max_poll_votes(poll_dict) %}
|
||||
<tr>
|
||||
<td> {{ poll_name }} </td>
|
||||
<td> {{ option }} </td>
|
||||
<td> {{ poll_dict[option] }} </td>
|
||||
<td> <progress id="file" max={{ max_vote }} value={{ poll_dict[option] }}></progress></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% endif %}
|
||||
{% if counters %}
|
||||
<table>
|
||||
<thead>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
from flask import render_template, Blueprint, request, current_app
|
||||
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.redeems.polls import all_active_polls, max_poll_votes
|
||||
from tlapbot.owncast_helpers import read_all_users_with_username
|
||||
from datetime import timezone
|
||||
|
||||
@@ -20,7 +23,9 @@ def dashboard() -> str:
|
||||
queue=pretty_redeem_queue(db),
|
||||
counters=all_active_counters(db),
|
||||
milestones=all_active_milestones(db),
|
||||
polls=all_active_polls(db),
|
||||
redeems=all_active_redeems(),
|
||||
max_poll_votes=max_poll_votes,
|
||||
prefix=current_app.config['PREFIX'],
|
||||
passive=current_app.config['PASSIVE'],
|
||||
username=username,
|
||||
|
||||
Reference in New Issue
Block a user