diff --git a/config.ini b/config.ini index bb1ace1..76bc5fb 100644 --- a/config.ini +++ b/config.ini @@ -30,3 +30,5 @@ json_indent = 4 # sender info: usually mailto link to responsible party to contact about issues push_private_key = abcdefghijklm_NOPQRSTUVWXYZ-0123456789 push_sender_info = mailto:it@fasttube.de +# when to notify users, in hours after arrival +notify_after_hrs = 10 diff --git a/ftracker/config.py b/ftracker/config.py index a2dc075..19cdf74 100644 --- a/ftracker/config.py +++ b/ftracker/config.py @@ -30,3 +30,6 @@ class Config: def __getitem__(self, key): return self.config['global'].get(key) + + def __repr__(self): + return repr(self.config.items('global')) diff --git a/ftracker/core.py b/ftracker/core.py index 1c8e3a7..a2e824c 100644 --- a/ftracker/core.py +++ b/ftracker/core.py @@ -22,14 +22,19 @@ from flask import Flask, request, redirect app = Flask(__name__, static_folder='../web') -from pywebpush import webpush, WebPushException -pushsubs = db.table('pushsubs') - - if config['delete_after_days']: from .deleter import Deleter deleter = Deleter(db, int(config['delete_after_days'])) + +if config['notify_after_hrs']: + from .notifier import Notifier + notifier = Notifier(db, int(config['notify_after_hrs']), { + 'private_key': config['push_private_key'], + 'claims': {'sub': config['push_sender_info']} + }) + + def shutdown(): print('\rReceived stop signal, stopping threads...') deleter.stop() @@ -37,6 +42,7 @@ def shutdown(): atexit.register(shutdown) + @app.route('/guidelines') def get_guidelines(): dest = config['guideline_url'] or None @@ -186,32 +192,25 @@ def post_pushsub(): @app.route('/testpush/') def testpush(name): + if not 'Authorization' in request.headers: + return 'Error: No Authorization', 401, {'WWW-Authenticate': 'Basic'} + + if request.authorization.username != config['admin_user']: + return "Wrong username", 403 + + if request.authorization.password != config['admin_pass']: + return "Wrong password", 403 + Entry = Query() - ps = pushsubs.search(Entry.name == name)[0] + arrivals = db.search((Entry.name == name) & (Entry.departure == None)) - arr = db.search((Entry.name == name) & (Entry.departure == None)) - arr = arr if len(arr) else None + if len(arrivals) == 0: + print("User is not logged in :(") + return "Error: User is not logged in :(", 409 - print(ps) + error = notifier.notify_user(arrivals[0]) - subscription = ps['sub'] - notification = { - 'title': "Forgot to sign out?", - 'body': "You didn't sign out of ftracker yet", - 'arr': arr - } + if error: + return 'Error: ' + error, 201 - try: - webpush( - subscription, - json.dumps(notification, indent=SPACES), - vapid_private_key = config['push_private_key'], - vapid_claims = { - 'sub': config['push_sender_info'] - }, - verbose=True - ) - return 'OK', 201 - except WebPushException as exc: - print(exc) - return 'Error', 500 + return 'OK', 201 diff --git a/ftracker/notifier.py b/ftracker/notifier.py new file mode 100644 index 0000000..5cb7286 --- /dev/null +++ b/ftracker/notifier.py @@ -0,0 +1,86 @@ +import json +from threading import Thread, Event +from datetime import datetime, timedelta + +from tinydb import Query + +from pywebpush import webpush, WebPushException + +ONE_HOUR_IN_S = 60 * 60 + +class Notifier(Thread): + + def notify_user(self, arrival): + + pushsubs = self.db.table('pushsubs') + + Entry = Query() + ps = pushsubs.search(Entry.name == arrival['name']) + if len(ps) == 0: + print("User is not subscribed to notifications :(") + return "User is not subscribed to notifications :(" + + ps = ps[0] + + print("Sending notification", arrival, ps) + + subscription = ps['sub'] + notification = { + 'title': "Forgot to sign out?", + 'body': "You didn't sign out of ftracker yet", + 'arr': arrival + } + + try: + webpush( + subscription, + json.dumps(notification), + vapid_private_key = self.vapid_creds['private_key'], + vapid_claims = self.vapid_creds['claims'] + ) + print("Notification sent") + return None + except WebPushException as exc: + print("Notification failed", exc) + return repr(exc) + + + def notify_logged_in_users(self): + + print("Notifying users that aren't signed out yet...") + + td = timedelta(hours=self.hours) + + threshold = datetime.utcnow() - td + + iso = threshold.isoformat() + 'Z' + + Entry = Query() + arrivals = self.db.search( + (Entry.arrival < iso) & (Entry.departure == None) + ) + + for arrival in arrivals: + self.notify_user(arrival) + + print("Notified everything until UTC", iso) + + def __init__(self, db, hours, vapid_creds): + + self.db = db + self.hours = hours + self.vapid_creds = vapid_creds + + self.notify_logged_in_users() + + Thread.__init__(self, daemon=True) + self.stopped = Event() + self.start() + + def run(self): + + while not self.stopped.wait(ONE_HOUR_IN_S): + self.notify_logged_in_users() + + def stop(self): + self.stopped.set()