Browse Source

run the notification check frequently throughout the day, and allow different times for users

pull/5/head
Trent Larson 8 months ago
parent
commit
2a9b1fcf75
  1. 2
      Dockerfile
  2. 126
      app.py
  3. BIN
      data/webpush.db.empty
  4. 10
      models.py

2
Dockerfile

@ -42,4 +42,4 @@ USER myuser
# Start gunicorn with the appropriate options
# Without "2>&1" the gunicorn internal logging shows in 'docker logs' but doesn't go to stdout like our 'printf' commands.
CMD ["gunicorn", "-b", "0.0.0.0:3000", "--log-level=debug", "--workers=1", "app:app", "2>&1"]
CMD ["sh", "-c", "gunicorn -b 0.0.0.0:3000 --log-level=debug --workers=1 app:app 2>&1"]

126
app.py

@ -4,13 +4,14 @@ Environment variables:
- ADMIN_PASSWORD: password for admin user for sensitive endpoints, defaults to 'admin'
"""
from typing import Dict, Tuple, Union, Optional
from flask import Flask, request, jsonify, Response
from models import db, VAPIDKey, Subscription
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import ec
from flask import Flask, request, jsonify, Response
from models import db, VAPIDKey, Settings, Subscription
from pywebpush import webpush, WebPushException
from sqlalchemy import and_
from typing import Dict, Tuple
import base64
import datetime
@ -141,8 +142,10 @@ class WebPushService():
- message (Dict): The actual message content to be sent as the push notification.
- vapid_key (VAPIDKey): The VAPID key model instance containing the private key used for sending the notification.
Returns:
- request.Response https://requests.readthedocs.io/en/latest/api.html#requests.Response if the push notification was sent successfully
Returns Dict with the following keys
- success: True if the push notification was sent successfully, False otherwise
- message: a string message with the resulting text, usually nothing on success
- result: request.Response https://requests.readthedocs.io/en/latest/api.html#requests.Response if the push notification was sent successfully
or False if there was an exception
Notes:
@ -205,41 +208,108 @@ class WebPushService():
while True:
now = datetime.datetime.now().isoformat()
now = datetime.datetime.now()
print(f"{now} - Starting to send subscriptions...", flush=True)
# Creating a context for the application to enable database operations
with self.app.app_context():
# Retrieving all subscription data from the database
all_subscriptions = Subscription.query.all()
# Retrieving the VAPID key from the database
# Retrieve the VAPID key from the database
vapid_key = VAPIDKey.query.first()
# Constructing the push notification message
# Construct the push notification message
# The title value is a key, triggering the device to apply logic and customize both title and message.
# See https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/src/commit/1e6159869fc28ca6e6b5b3d186617d75705100b4/sw_scripts/additional-scripts.js#L65
UPDATE_TITLE = "DAILY_CHECK"
message = {"title": UPDATE_TITLE, "message": f"Update for {now}"}
# Sending a push notification to each subscribed client
for subscription in all_subscriptions:
subscription_info = {
"endpoint": subscription.endpoint,
"keys": {
"p256dh": subscription.p256dh,
"auth": subscription.auth
}
}
result = WebPushService._send_push_notification(subscription_info, message, vapid_key)
print(f"Result from sub {subscription.id}: success={result['success']} text={result['message']}", flush=True)
print(f"{now} - Finished sending {len(all_subscriptions)} subscriptions.", flush=True)
self.latest_subscription_run = now
# Sleeping for 24 hours before sending the next set of notifications
time.sleep(24 * 60 * 60)
# Determine the beginning and end time to check for subscriptions
settings = Settings.query.first()
if settings.running_notify_end_time is None:
# set all subscriptions without a time to the current time
# (these won't be picked-up in the current run, since thie current minute is the end minute)
Subscription.query.filter_by(notify_time=None).update({Subscription.notify_time: now.strftime('%H:%M')})
"""
Storing the HH:MM for the desired notification time isn't a bad idea.
However, the logic that compares directly to it is a bit complicated.
It would be more straightforward to save the next notification time in each record
and then update that every time this process runs. It would require raw SQL with
some calculations and many DB updates each day, but the logic would be more clear.
"""
# get the previous notify end time from the DB as a datetime
prev_notify_end_time = datetime.datetime.fromisoformat(settings.prev_notify_end_time)
# if it's before midnight this morning, catch us up to the beginning of today:
if prev_notify_end_time < now.replace(hour=0, minute=0, second=0, microsecond=0):
# make the start time the later of: prevNotifyEndTime or yesterday at this time
start_time = max(
prev_notify_end_time,
# if the current time is later in the day, use that
# because the next run will pick up everything from midnight until now today
now.replace(second=0, microsecond=0) - datetime.timedelta(days=1)
)
# make the end time right at midnight at the beginning of today
end_time = now.replace(hour=0, minute=0, second=0, microsecond=0)
start_minute = start_time.strftime('%H:%M')
# (we'd never catch anything if we used a non-zero start_minute and "00:00" as the end_minute)
end_minute = '23:60' # gotta catch "23:59", too
else:
# the start time is OK
start_time = prev_notify_end_time
# the end time is now
end_time = now.replace(second=0, microsecond=0)
start_minute = start_time.strftime('%H:%M')
end_minute = end_time.strftime('%H:%M')
# This really should update & continue only if the running_notify_end_time is still None,
# just in case another thread started.
settings.running_notify_end_time = end_time.isoformat()
db.session.commit()
# this check was generated by Copilot; it's probably unnecessary
if settings.running_notify_end_time == end_time.isoformat():
# Now get the 'HH:MM' value for start & end so we can compare to the notify_time field
# get all the subscriptions that have a notify_time between start_minute inclusive and end_minute exclusive
all_subscriptions = Subscription.query.filter(
and_(Subscription.notify_time >= start_minute, Subscription.notify_time < end_minute)
)
# Send a push notification to each subscribed client
num_subscriptions = all_subscriptions.count()
for subscription in all_subscriptions:
subscription_info = {
"endpoint": subscription.endpoint,
"keys": {
"p256dh": subscription.p256dh,
"auth": subscription.auth
}
}
result = WebPushService._send_push_notification(subscription_info, message, vapid_key)
print(
f"Result from sub {subscription.id}: success={result['success']} text={result['message']}",
flush=True
)
settings.prev_notify_end_time = end_time.isoformat()
settings.running_notify_end_time = None
db.session.commit()
print(f"{now} - Finished sending {num_subscriptions} subscriptions.", flush=True)
else:
print(f"{now} - Failed to update running_notify_end_time", flush=True)
else:
print(f"{now} - Stopped because we're already running a notification check.", flush=True)
self.latest_subscription_run = now.isoformat()
# Sleep before repeating
time.sleep(5 * 60)
# This is an endpoint, routed in __init__

BIN
data/webpush.db.empty

Binary file not shown.

10
models.py

@ -8,11 +8,15 @@ class VAPIDKey(db.Model):
private_key = db.Column(db.String(255), nullable=False)
subscriptions = db.relationship('Subscription', backref='vapid_key', lazy=True)
class Settings(db.Model):
id = db.Column(db.Integer, primary_key=True)
prev_notify_end_time = db.Column(db.String(29), nullable=False)
running_notify_end_time = db.Column(db.String(29), nullable=True)
class Subscription(db.Model):
id = db.Column(db.Integer, primary_key=True)
auth = db.Column(db.String(255), nullable=False)
endpoint = db.Column(db.String(500), nullable=False)
notify_time = db.Column(db.String(5), nullable=True) # HH:MM
p256dh = db.Column(db.String(255), nullable=False)
auth = db.Column(db.String(255), nullable=False)
vapid_key_id = db.Column(db.Integer, db.ForeignKey('vapid_key.id'), nullable=False)

Loading…
Cancel
Save