Compare commits

...

3 Commits

Author SHA1 Message Date
3a872bceb2
Enable Docker container to generate its own VAPID credentials 2021-06-11 01:06:59 +02:00
4ee4869f82
Minor fixes & improvements
- Move push subscription handling to Notifier class
- Allow duplicate options in config
- Only save subscription in frontend if sub call was successful
2021-06-11 01:04:44 +02:00
618f00a09a
Send notifications N hours after arrival, polling once per hour 2021-06-10 23:14:15 +02:00
8 changed files with 168 additions and 37 deletions

View File

@ -1,7 +1,9 @@
FROM alpine:latest
RUN apk add --update --no-cache \
bash python3 py3-pip nginx uwsgi uwsgi-python3 certbot certbot-nginx
bash python3 py3-pip nginx uwsgi uwsgi-python3 certbot certbot-nginx npm jq
RUN npm install -g web-push

View File

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

View File

@ -19,7 +19,7 @@ class Config:
configfile = findConfigFile()
if configfile:
self.config = ConfigParser()
self.config = ConfigParser(strict=False)
self.config.read(configfile)
else:
raise Exception("No config file found")
@ -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'))

View File

@ -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
@ -174,44 +180,32 @@ def post_pushsub():
except ValueError as e:
return 'Error: JSON decode error:\n' + str(e), 400
name = slugify(data['name'])
data['name'] = name
print(json.dumps(data, indent=SPACES))
Entry = Query()
pushsubs.upsert(data, Entry.name == name)
notifier.subscribe_user(data)
return 'OK', 201
@app.route('/testpush/<name>')
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, 404
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

97
ftracker/notifier.py Normal file
View File

@ -0,0 +1,97 @@
import json
from slugify import slugify
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 subscribe_user(self, data):
pushsubs = self.db.table('pushsubs')
name = slugify(data['name'])
data['name'] = name
Entry = Query()
pushsubs.upsert(data, Entry.name == name)
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()

View File

@ -4,6 +4,9 @@
# Remove or leave empty for temporary (/tmp/ftracker-db.json) storage
db_file = /var/ftracker/db.json
# Delete all information after X days (e.g. for GDPR compliance)
delete_after_days = 28
# List of people to be allowed, in .csv format (comma, no delimiters)
# Col1: First Name(s), Col2: Last Name(s), Col3 (optional): EMail
# Remove or leave empty for no check
@ -21,3 +24,11 @@ guideline_url = https://youtu.be/oHg5SJYRHA0
# JSON indentation for debugging
json_indent = 4
# VAPID credentials for push notifications
# private key: base64url encoded private part of an EC-Prime256v1 keypair. See INSTALL.md
# sender info: usually mailto link to responsible party to contact about issues
push_private_key = abcdefghijklm_NOPQRSTUVWXYZ-0123456789
push_sender_info = mailto:admin@example.com
# when to notify users, in hours after arrival
notify_after_hrs = 10

View File

@ -1,5 +1,25 @@
#!/bin/bash
echo " >>> Checking / Creating & patching VAPID creds <<< "
VAPID_CREDS_FILE=/etc/ftracker/vapid-creds.json
if [[ ! -f $VAPID_CREDS_FILE ]]
then
echo "Generating keypair ..."
web-push generate-vapid-keys --json > $VAPID_CREDS_FILE
echo "Patching public key into frontend ..."
PUB_KEY=`cat $VAPID_CREDS_FILE | jq -r .publicKey`
sed -i "s/pushServerPublicKey = '[a-zA-Z0-9_\-]*'/pushServerPublicKey = '${PUB_KEY}'/" /var/www/html/ftracker/main.js
echo "Patching private key into backend config ..."
PRIV_KEY=`cat $VAPID_CREDS_FILE | jq -r .privateKey`
echo "push_private_key = ${PRIV_KEY}" >> /etc/ftracker/config.ini
fi
echo " >>> Starting nginx <<< "
mkdir /run/nginx # needed because of bug in package

View File

@ -250,7 +250,6 @@ function initPush(name) {
applicationServerKey: pushServerPublicKey
}).then(function(subscription) {
console.log("User is subscribed:", subscription);
localStorage.setItem('pushsub', subscription);
fetch('/pushsubscribe', {
method: "POST",
@ -259,6 +258,9 @@ function initPush(name) {
name: name,
sub: subscription
})
}).then(function(res) {
if (res.ok)
localStorage.setItem('pushsub', subscription);
});
});
});