Add push notifications for forgotten departures #26
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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'))
 | 
			
		||||
 | 
			
		||||
@ -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/<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, 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
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										86
									
								
								ftracker/notifier.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								ftracker/notifier.py
									
									
									
									
									
										Normal file
									
								
							@ -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()
 | 
			
		||||
		Reference in New Issue
	
	Block a user