run the notification check frequently throughout the day, and allow different times for users
This commit is contained in:
@@ -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"]
|
||||
|
||||
122
app.py
122
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)
|
||||
# Determine the beginning and end time to check for subscriptions
|
||||
settings = Settings.query.first()
|
||||
if settings.running_notify_end_time is None:
|
||||
|
||||
self.latest_subscription_run = now
|
||||
# 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')})
|
||||
|
||||
# Sleeping for 24 hours before sending the next set of notifications
|
||||
time.sleep(24 * 60 * 60)
|
||||
"""
|
||||
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__
|
||||
|
||||
Binary file not shown.
12
models.py
12
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)
|
||||
endpoint = db.Column(db.String(500), nullable=False)
|
||||
p256dh = db.Column(db.String(255), nullable=False)
|
||||
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)
|
||||
vapid_key_id = db.Column(db.Integer, db.ForeignKey('vapid_key.id'), nullable=False)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user