17 Commits
Author SHA1 Message Date
lili acbc93bedd auto-refresh base
untested
2024-02-04 11:31:42 +01:00
lili e5e88bf520 Merge branch 'master' of github.com:SleepyLili/tlapbot into develop 2024-02-04 11:28:51 +01:00
lili b4a6c4e41c html/jinja formatting improvements
thanks chat
2024-02-03 18:47:42 +01:00
lili 8c19088640 readme: reword milestone "goal" and "price"
should hopefully suggest more clearly that milestones need goal
and everything else needs price
also changed the example to list goal first
2023-07-13 12:44:15 +02:00
lili 5316f7e205 improve default_redeems
list milestone "goal" first in the redeems dictionary
2023-07-13 12:42:46 +02:00
lili 31426fd851 promote to 1.2.1 2023-07-10 11:16:44 +02:00
lili 8d5a3cda42 update readme for 1.2.1 2023-07-10 11:13:31 +02:00
lili e237e7f885 declare alpha version 1.2.1a1 2023-07-03 12:56:03 +02:00
lili 80037593a9 fix redeem handler for redeems with no price
milestone needs to be handled first
2023-07-03 12:27:45 +02:00
lili d484a0a363 fix reset_milestone 2023-07-03 12:18:29 +02:00
lili 348e674f74 make price tab in dashboard blank for milestones 2023-07-03 11:02:09 +02:00
lili ca9aa6d79d add reset and hard-reset command to init 2023-07-03 10:46:35 +02:00
lili ae42a052de add true/false fail checks to click commands
previously click would print stuff like "X done succesfully" after error
now it'll be more clear that a failure has occured
2023-06-26 13:40:26 +02:00
lili d1416d3706 add reset-milestone and hard-reset-milestone
commands to click
2023-06-26 13:28:34 +02:00
lili f66b125ac3 remove unused start_milestone function 2023-06-26 13:12:28 +02:00
lili 6f6fc10d0f remove minimum bid for #15 2023-06-20 14:39:19 +02:00
lili bf68f518da add error handling and response check to requests 2023-06-20 13:41:22 +02:00
12 changed files with 233 additions and 92 deletions
+18 -5
View File
@@ -265,6 +265,16 @@ python -m flask refresh-milestones
```
Running this command shouldn't reset progress on milestones that are already in the database
and are still in the redeems file.
#### reset-milestone
Resets progress on a milestone, but only if the milestone had been completed.
```bash
python -m flask reset-milestone milestone
```
#### hard-reset-milestone
Resets progress on a milestone, regardless of completion status.
```bash
python -m flask hard-reset-milestone milestone
```
## 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.
@@ -313,18 +323,21 @@ REDEEMS={
"lurk": {"price": 1, "type": "counter", "info": "Let us know you're going to lurk."},
"react": {"price": 200, "type": "note", "info": "Attach link to a video for me to react to."},
"request": {"price": 100, "type": "note", "info": "Request a level, gamemode, skin, etc."},
"go_nap": {"price": 1, "type": "milestone", "info": "Streamer will go nap when the goal is reached.", "goal": 1000},
"go_nap": {"goal": 1000, "type": "milestone", "info": "Streamer will go nap when the goal is reached."},
"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 decides the chat command for the redeem. The value is another dictionary that needs to have entries for `"price"`, `"type"` and optionally `"info"` and `"category"`. If the `"type"` is `"milestone"`, there's an additional required `"goal"` field as well.
Each dictionary entry is a redeem, and the dictionary keys are strings that decides the chat command for the redeem.
The value is another dictionary that needs to have an entry for `"type"` and
an entry for `"price"` for non-milestones or `"goal"` for milestones.
Optionally, each redeem can also have `"info"` and `"category"` entries.
- `"price"` value should be an integer that decides how many points the redeem will cost. For milestone redeems, `"price"` determines minimum bid.
- `"type"` value should be either `"list"`, `"counter"`, `"note"` or `"milestone"`. This decided the redeem's type, and whether it will show up as a counter at the top of the dashboard or as an entry in the "recent redeems" chart.
- `"info"` value should be a string that describes what the command does. It's optional, but I recommend writing one for all `"list"` and `"note"` redeems (so that chatters know that they should write a note).
- `"price"` value should be an integer that decides how many points the redeem will cost. Milestone redeems don't use the `"price"` value, they instead need to have a `"goal"`.
- `"goal"` is a required field for milestone goals. It should be an integer, deciding the amount of points required to complete the milestone.
- `"type"` value should be either `"list"`, `"counter"`, `"note"` or `"milestone"`. This decided the redeem's type, and whether it will show up as a counter at the top of the dashboard or as an entry in the "recent redeems" chart.
- `"info"` value should be a string that describes what the command does. It's optional, but I recommend writing one for all `"list"`, `"note"` and `"milestone"` redeems (so that chatters know what they're redeeming and whether they should leave a note).
- `"category"` is an optional list of strings, the categories the redeem is in.
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
+1 -1
View File
@@ -2,7 +2,7 @@ from setuptools import find_packages, setup
setup(
name='tlapbot',
version='1.2.0',
version='1.2.1',
packages=find_packages(),
include_package_data=True,
install_requires=[
+10
View File
@@ -2,9 +2,12 @@ import os
import logging
from flask import Flask
from apscheduler.schedulers.background import BackgroundScheduler
from datetime import datetime
from tlapbot.db import get_db
from tlapbot.owncast_requests import is_stream_live, give_points_to_chat
from tlapbot.redeems import remove_inactive_redeems
from tlapbot.helpers import (get_last_online_time, delete_last_online_time,
save_last_online_time)
def create_app(test_config=None):
app = Flask(__name__, instance_relative_config=True)
@@ -49,14 +52,21 @@ def create_app(test_config=None):
app.cli.add_command(db.refresh_counters_command)
app.cli.add_command(db.refresh_and_clear_command)
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)
# scheduler job for giving points to users
def proxy_job():
with app.app_context():
if is_stream_live():
if get_last_online_time:
delete_last_online_time()
app.logger.info("Stream is LIVE. Giving points to chat.")
give_points_to_chat(get_db())
else:
if not get_last_online_time:
# TODO: error state
save_last_online_time(get_db(), datetime.now(), False)
app.logger.info("Stream is NOT LIVE. (Not giving points to chat.)")
# start scheduler that will give points to users
+63 -13
View File
@@ -4,6 +4,7 @@ import click
from flask import current_app, g
from flask.cli import with_appcontext
from tlapbot.redeems import milestone_complete
def get_db():
if 'db' not in g:
@@ -34,6 +35,8 @@ def insert_counters(db):
db.commit()
except sqlite3.Error as e:
print("Failed inserting counters to db:", e.args[0])
return False
return True
def init_db():
@@ -42,7 +45,8 @@ def init_db():
with current_app.open_resource('schema.sql') as f:
db.executescript(f.read().decode('utf8'))
insert_counters(db)
if insert_counters(db):
return True
def clear_redeem_queue():
@@ -58,6 +62,8 @@ def clear_redeem_queue():
db.commit()
except sqlite3.Error as e:
print("Error occured deleting redeem queue:", e.args[0])
return False
return True
def refresh_counters():
@@ -68,7 +74,9 @@ def refresh_counters():
db.commit()
except sqlite3.Error as e:
print("Error occured deleting old counters:", e.args[0])
insert_counters(db)
return False
if insert_counters(db):
return True
def refresh_milestones():
@@ -89,6 +97,7 @@ def refresh_milestones():
db.commit()
except sqlite3.Error as e:
print("Failed deleting old milestones from db:", e.args[0])
return False
# add new milestones
try:
@@ -113,24 +122,46 @@ def refresh_milestones():
db.commit()
except sqlite3.Error as e:
print("Failed inserting milestones to db:", e.args[0])
return False
return True
def reset_milestone(milestone):
if not milestone in current_app.config['REDEEMS']:
print(f"Failed resetting milestone, {milestone} not in redeems file.")
return False
try:
db = get_db()
db.execute(
"DELETE FROM milestones WHERE name = ?",
(milestone,)
)
db.execute(
"INSERT INTO milestones(name, progress, goal, complete) VALUES(?, ?, ?, FALSE)",
(milestone, 0, current_app.config['REDEEMS'][milestone]['goal'])
)
db.commit()
return True
except sqlite3.Error as e:
current_app.logger.error(f"Error occured adding a milestone: {e.args[0]}")
return False
@click.command('init-db')
@with_appcontext
def init_db_command():
"""Clear the existing data and create new tables."""
init_db()
click.echo('Initialized the database.')
if init_db():
click.echo('Initialized the database.')
@click.command('clear-queue')
@with_appcontext
def clear_queue_command():
"""Remove all redeems from the redeem queue."""
clear_redeem_queue()
click.echo('Cleared redeem queue.')
if clear_redeem_queue():
click.echo('Cleared redeem queue.')
@click.command('refresh-counters')
@@ -138,17 +169,16 @@ def clear_queue_command():
def refresh_counters_command():
"""Refresh counters from current config file.
(Remove old ones, add new ones.)"""
refresh_counters()
click.echo('Counters refreshed.')
if refresh_counters():
click.echo('Counters refreshed.')
@click.command('clear-refresh')
@with_appcontext
def refresh_and_clear_command():
"""Refresh counters and clear queue."""
refresh_counters()
clear_redeem_queue()
click.echo('Counters refreshed and queue cleared.')
if refresh_counters() and clear_redeem_queue():
click.echo('Counters refreshed and queue cleared.')
@click.command('refresh-milestones')
@@ -156,8 +186,28 @@ def refresh_and_clear_command():
def refresh_milestones_command():
"""Initialize all milestones from the redeems file,
delete milestones not in redeem file."""
refresh_milestones()
click.echo('Refreshed milestones.')
if refresh_milestones():
click.echo('Refreshed milestones.')
@click.command('reset-milestone')
@click.argument('milestone')
def reset_milestone_command(milestone):
"""Resets a completed milestone back to zero."""
if milestone_complete(get_db(), milestone):
if reset_milestone(milestone):
click.echo(f"Reset milestone {milestone}.")
else:
click.echo(f"Could not reset milestone {milestone}, milestone not completed.")
click.echo("(You can hard-reset-milestone if you really want to reset it.)")
@click.command('hard-reset-milestone')
@click.argument('milestone')
def hard_reset_milestone_command(milestone):
"""Resets any milestone back to zero."""
if reset_milestone(milestone):
click.echo(f"Hard reset milestone {milestone}.")
def init_app(app):
+1 -1
View File
@@ -3,6 +3,6 @@ REDEEMS={
"lurk": {"price": 1, "type": "counter", "info": "Let us know you're going to lurk."},
"react": {"price": 200, "type": "note", "info": "Attach link to a video for me to react to."},
"request": {"price": 100, "type": "note", "info": "Request a level, gamemode, skin, etc."},
"go_nap": {"price": 1, "type": "milestone", "info": "Streamer will go nap when the goal is reached.", "goal": 1000},
"go_nap": {"goal": 1000, "type": "milestone", "info": "Streamer will go nap when the goal is reached."},
"inactive": {"price": 100, "type": "note", "info": "Example redeem that is inactive by default", "category": ["inactive"]}
}
+31
View File
@@ -95,6 +95,37 @@ def add_user_to_database(db, user_id, display_name):
current_app.logger.error(f"To user id: {user_id}, with display name: {display_name}")
def save_last_online_time(db, timestamp, from_owncast):
try:
db.execute(
"INSERT OVERWRITE last_online_time(id, last_online_time, from_owncast)",
(1, timestamp, from_owncast)
)
db.commit()
except Error as e:
current_app.logger.error(f"Error occured saving last online time: {e.args[0]}")
current_app.logger.error(f"Timestamp: {timestamp}, from_owncast: {from_owncast}")
def get_last_online_time(db):
try:
cursor = db.execute(
"SELECT last_online_time FROM last_online_time WHERE id = 1"
)
last_online_time = cursor.fetchone()
return last_online_time
except Error as e:
current_app.logger.error(f"Error occured reading last online time: {e.args[0]}")
def delete_last_online_time(db):
try:
db.execute("DELETE FROM last_online_time")
db.commit()
except Error as e:
current_app.logger.error(f"Error occured deleting last online time: {e.args[0]}")
def change_display_name(db, user_id, new_name):
try:
cursor = db.execute(
+21 -5
View File
@@ -16,17 +16,33 @@ def is_stream_live():
def give_points_to_chat(db):
url = current_app.config['OWNCAST_INSTANCE_URL'] + '/api/integrations/clients'
headers = {"Authorization": "Bearer " + current_app.config['OWNCAST_ACCESS_TOKEN']}
r = requests.get(url, headers=headers)
try:
r = requests.get(url, headers=headers)
except requests.exceptions.RequestException as e:
current_app.logger.error(f"Error occured getting users to give points to: {e.args[0]}")
return
if r.status_code != 200:
current_app.logger.error(f"Error occured when giving points: Response code not 200.")
current_app.logger.error(f"Response code received: {r.status_code}.")
current_app.logger.error(f"Check owncast instance url and access key.")
return
unique_users = set(map(lambda user_object: user_object["user"]["id"], r.json()))
for user_id in unique_users:
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):
url = current_app.config['OWNCAST_INSTANCE_URL'] + '/api/integrations/chat/send'
headers = {"Authorization": "Bearer " + current_app.config['OWNCAST_ACCESS_TOKEN']}
r = requests.post(url, headers=headers, json={"body": message})
try:
r = requests.post(url, headers=headers, json={"body": message})
except requests.exceptions.RequestException as e:
current_app.logger.error(f"Error occured sending chat message: {e.args[0]}")
return
if r.status_code != 200:
current_app.logger.error(f"Error occured when sending chat: Response code not 200.")
current_app.logger.error(f"Response code received: {r.status_code}.")
current_app.logger.error(f"Check owncast instance url and access key.")
return
return r.json()
+20 -2
View File
@@ -1,11 +1,13 @@
from flask import Flask, request, json, Blueprint, current_app
from tlapbot.db import get_db
from datetime import datetime
from tlapbot.db import get_db, refresh_counters, clear_redeem_queue
from tlapbot.owncast_requests import send_chat
from tlapbot.owncast_helpers import (add_user_to_database, change_display_name,
read_users_points, remove_duplicate_usernames)
read_users_points, remove_duplicate_usernames, get_last_online_time, delete_last_online_time)
from tlapbot.help_message import send_help
from tlapbot.redeems_handler import handle_redeem
# might need datetime timestamp
bp = Blueprint('owncast_webhooks', __name__)
@@ -15,6 +17,22 @@ def owncast_webhook():
data = request.json
db = get_db()
if data["type"] == "STREAM_STARTED":
# TODO: make this a function, import here and in init
delete_last_online_time(db)
last_online = get_last_online_time(db)
if last_online and current_app.config['AUTO_REFRESH']:
time_difference = datetime.now() - last_online
if time_difference.seconds//60 > current_app.config['RECONNECT_TIME']:
if refresh_counters() and clear_redeem_queue():
current_app.logger.debug(f'Counters refreshed, redeem queue cleared.')
else:
current_app.logger.error(
f'Error occured when automatically clearing queue and resetting counters.'
)
elif data["type"] == "STREAM_STOPPED":
save_last_online_time(db, datetime.now(), True)
# Make sure user is in db before doing anything else.
if data["type"] in ["CHAT", "NAME_CHANGED", "USER_JOINED"]:
user_id = data["eventData"]["user"]["id"]
-18
View File
@@ -49,24 +49,6 @@ def add_to_redeem_queue(db, user_id, redeem_name, note=None):
return False
def start_milestone(db, redeem_name):
try:
cursor = db.execute(
"SELECT progress, goal FROM milestones WHERE name = ?",
(redeem_name,)
)
milestone = cursor.fetchone()
current_app.logger.error(f"Milestone: {milestone}")
if milestone is None:
cursor = db.execute(
"INSERT INTO milestones(name, progress, goal) VALUES(?, ?, ?)",
(redeem_name, 0, current_app.config['REDEEMS'][redeem_name]['goal'])
)
db.commit()
except Error as e:
current_app.logger.error(f"Error occured adding a milestone: {e.args[0]}")
def add_to_milestone(db, user_id, redeem_name, points_donated):
try:
cursor = db.execute(
+20 -18
View File
@@ -22,10 +22,29 @@ def handle_redeem(message, user_id):
return
db = get_db()
price = current_app.config['REDEEMS'][redeem]["price"]
redeem_type = current_app.config['REDEEMS'][redeem]["type"]
points = read_users_points(db, user_id)
# handle milestone first because it doesn't have a price
if redeem_type == "milestone":
if milestone_complete(db, redeem):
send_chat(f"Can't redeem {redeem}, that milestone was already completed!")
elif not note:
send_chat(f"Cannot redeem {redeem}, no amount of points specified.")
elif not note.isdigit():
send_chat(f"Cannot redeem {redeem}, amount of points is not an integer.")
elif int(note) > points:
send_chat(f"Can't redeem {redeem}, you're donating more points than you have.")
elif add_to_milestone(db, user_id, redeem, int(note)):
send_chat(f"Succesfully donated to {redeem} milestone!")
if check_apply_milestone_completion(db, redeem):
send_chat(f"Milestone goal {redeem} complete!")
else:
send_chat(f"Redeeming milestone {redeem} failed.")
return
# handle redeems with price argument
price = current_app.config['REDEEMS'][redeem]["price"]
if not points or points < price:
send_chat(f"Can't redeem {redeem}, you don't have enough points.")
return
@@ -50,22 +69,5 @@ def handle_redeem(message, user_id):
send_chat(f"{redeem} redeemed for {price} points.")
else:
send_chat(f"Redeeming {redeem} failed.")
elif redeem_type == "milestone":
if milestone_complete(db, redeem):
send_chat(f"Can't redeem {redeem}, that milestone was already completed!")
elif not note:
send_chat(f"Cannot redeem {redeem}, no amount of points specified.")
elif not note.isdigit():
send_chat(f"Cannot redeem {redeem}, amount of points is not an integer.")
elif int(note) < price:
send_chat(f"Can't redeem {redeem}, your donation is below the minimum bid of {price}.")
elif int(note) > points:
send_chat(f"Can't redeem {redeem}, you're donating more points than you have.")
elif add_to_milestone(db, user_id, redeem, int(note)):
send_chat(f"Succesfully donated to {redeem} milestone!")
if check_apply_milestone_completion(db, redeem):
send_chat(f"Milestone goal {redeem} complete!")
else:
send_chat(f"Redeeming {redeem} failed.")
else:
send_chat(f"{redeem} not redeemed, type of redeem not found.")
+5
View File
@@ -30,3 +30,8 @@ CREATE TABLE redeem_queue (
note TEXT,
FOREIGN KEY (redeemer_id) REFERENCES points (id)
);
CREATE TABLE online_time(
id INTEGER PRIMARY KEY,
online_time TIMESTAMP NOT NULL
);
+43 -29
View File
@@ -30,12 +30,14 @@
<th>Your points balance</th>
</tr>
</thead>
{% for user in users %}
<tbody>
<td> {{ user[0] }} </td>
<td> {{ user[1] }} </td>
</tbody>
{% for user in users %}
<tr>
<td> {{ user[0] }} </td>
<td> {{ user[1] }} </td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
@@ -52,12 +54,14 @@
<th>Active counters</th>
</tr>
</thead>
{% for counter in counters %}
<tbody>
<td> {{ counter[0] }} </td>
<td> {{ counter[1] }} </td>
</tbody>
{% for counter in counters %}
<tr>
<td> {{ counter[0] }} </td>
<td> {{ counter[1] }} </td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% if milestones %}
@@ -68,13 +72,15 @@
<th>Progress</th>
</tr>
</thead>
{% for milestone in milestones %}
<tbody>
<td> {{ milestone[0] }} </td>
<td> <progress id="file" max={{ milestone[2] }} value={{ milestone[1] }}></progress></td>
<td> {{ milestone[1] }} / {{ milestone[2] }}</td>
</tbody>
{% for milestone in milestones %}
<tr>
<td> {{ milestone[0] }} </td>
<td> <progress id="file" max={{ milestone[2] }} value={{ milestone[1] }}></progress></td>
<td> {{ milestone[1] }} / {{ milestone[2] }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% endif %}
@@ -99,16 +105,18 @@
<th>Note</th>
</tr>
</thead>
{% for row in queue %}
<tbody>
<td>{{ row[0].replace(tzinfo=utc_timezone).astimezone().strftime("%H:%M") }}</td>
<td>{{ row[1] }}</td>
<td>{{ row[3] }}</td>
{% if row[2] %}
<td>{{ row[2] }}</td>
{% endif %}
</tbody>
{% for row in queue %}
<tr>
<td>{{ row[0].replace(tzinfo=utc_timezone).astimezone().strftime("%H:%M") }}</td>
<td>{{ row[1] }}</td>
<td>{{ row[3] }}</td>
{% if row[2] %}
<td>{{ row[2] }}</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</body>
@@ -136,16 +144,22 @@
<th>Description</th>
</tr>
</thead>
{% for redeem, redeem_info in redeems.items() %}
<tbody>
<td>{{ prefix }}{{ redeem }}</td>
<td>{{ redeem_info["price"] }}</td>
<td>{{ redeem_info["type"] }}</td>
{% if redeem_info["info"] %}
<td>{{ redeem_info["info"] }}</td>
{% endif %}
</tbody>
{% for redeem, redeem_info in redeems.items() %}
<tr>
<td>{{ prefix }}{{ redeem }}</td>
{% if redeem_info["type"] == "milestone" %}
<td></td>
{% else %}
<td>{{ redeem_info["price"] }}</td>
{% endif %}
<td>{{ redeem_info["type"] }}</td>
{% if redeem_info["info"] %}
<td>{{ redeem_info["info"] }}</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</body>