add a customizable message, plus other fixes
This commit is contained in:
13
CHANGELOG.md
13
CHANGELOG.md
@@ -5,7 +5,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
|
||||||
## [0.4.0] - 2024-03-31
|
## [0.5.0] - 2024-11-24
|
||||||
|
### Added
|
||||||
|
- `notifyType` when subscribing, including `DIRECT_NOTIFICATION` with a hard-coded message
|
||||||
|
- `SECONDS_BETWEEN_NOTIFICATIONS` environment variable for length of notification loop
|
||||||
|
### Changed
|
||||||
|
- DB changes:
|
||||||
|
- `alter table subscription add column notify_type varchar(32);`
|
||||||
|
- `update subscription set notify_type = 'DAILY_CHECK';`
|
||||||
|
- `alter table subscription add column created_date datetime;`
|
||||||
|
- `alter table subscription add column message varchar(100);`
|
||||||
|
|
||||||
|
## [0.4.0] - 2024-03-31 - 400d9e6eb0e8508efbb4c870a79c0008d15f2200
|
||||||
### Added
|
### Added
|
||||||
- Different times for users to receive notifications
|
- Different times for users to receive notifications
|
||||||
- Ping endpoint
|
- Ping endpoint
|
||||||
|
|||||||
105
README.md
105
README.md
@@ -1,5 +1,59 @@
|
|||||||
# py-push-server
|
# py-push-server
|
||||||
|
|
||||||
|
|
||||||
|
## Run the server outside Docker
|
||||||
|
|
||||||
|
Run the app:
|
||||||
|
|
||||||
|
```commandline
|
||||||
|
sh <(curl https://pkgx.sh) +python.org +virtualenv.pypa.io sh
|
||||||
|
|
||||||
|
# first time
|
||||||
|
python -m venv .
|
||||||
|
|
||||||
|
source bin/activate
|
||||||
|
|
||||||
|
# first time
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
cp data/webpush.db.empty data/webpush.db
|
||||||
|
# or: python init_db.py
|
||||||
|
|
||||||
|
# For local DB access, you'll have to uncomment the local path for `db_uri`.
|
||||||
|
|
||||||
|
# 3 workers would trigger 3 daily subscription runs
|
||||||
|
gunicorn -b 0.0.0.0:3000 --log-level=debug --workers=1 app:app
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
... and see the results in a browser: http://localhost:3000/web-push/vapid
|
||||||
|
|
||||||
|
See Troubleshooting below if that doesn't work out of the box.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Run a test:
|
||||||
|
|
||||||
|
```commandline
|
||||||
|
python test-webpush.py
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Run haproxy (on a Mac):
|
||||||
|
|
||||||
|
* Create "haproxy-config" directory for those files above, eg. /usr/local/etc/haproxy
|
||||||
|
|
||||||
|
* Comment out the `log rsyslog` and `bind *:443` lines in /usr/local/etc/haproxy/haproxy.cfg and then run:
|
||||||
|
|
||||||
|
`haproxy -f /usr/local/etc/haproxy/haproxy.cfg`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Docker Build & Deploy
|
## Docker Build & Deploy
|
||||||
|
|
||||||
- Update CHANGELOG.md
|
- Update CHANGELOG.md
|
||||||
@@ -40,6 +94,7 @@ Finally, after it's started, generate a new VAPID by hitting the `regenerate-vap
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Docker Compose & HAProxy Setup
|
## Docker Compose & HAProxy Setup
|
||||||
|
|
||||||
On a production server for security (eg /web-push/generate_vapid): set an environment variable `ADMIN_PASSWORD` for permissions; one way is to create a .env file with the value inside before running `docker compose` commands:
|
On a production server for security (eg /web-push/generate_vapid): set an environment variable `ADMIN_PASSWORD` for permissions; one way is to create a .env file with the value inside before running `docker compose` commands:
|
||||||
@@ -237,56 +292,6 @@ timesafari-pwa.anomalistlabs.com/web-push/ web_push_backend
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Run the server outside Docker
|
|
||||||
|
|
||||||
Run the app:
|
|
||||||
|
|
||||||
```commandline
|
|
||||||
sh <(curl https://pkgx.sh) +python.org +virtualenv.pypa.io sh
|
|
||||||
|
|
||||||
# first time
|
|
||||||
python -m venv .
|
|
||||||
|
|
||||||
source bin/activate
|
|
||||||
|
|
||||||
# first time
|
|
||||||
pip install -r requirements.txt
|
|
||||||
|
|
||||||
cp data/webpush.db.empty data/webpush.db
|
|
||||||
|
|
||||||
# For DB access, you'll have to uncomment the local path for `db_uri`.
|
|
||||||
|
|
||||||
# 3 workers would trigger 3 daily subscription runs
|
|
||||||
gunicorn -b 0.0.0.0:3000 --log-level=debug --workers=1 app:app
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
... and see the results in a browser: http://localhost:3000/web-push/vapid
|
|
||||||
|
|
||||||
See Troubleshooting below if that doesn't work out of the box.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Run a test:
|
|
||||||
|
|
||||||
```commandline
|
|
||||||
python webpush.py
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Run haproxy (on a Mac):
|
|
||||||
|
|
||||||
* Create "haproxy-config" directory for those files above, eg. /usr/local/etc/haproxy
|
|
||||||
|
|
||||||
* Comment out the `log rsyslog` and `bind *:443` lines in /usr/local/etc/haproxy/haproxy.cfg and then run:
|
|
||||||
|
|
||||||
`haproxy -f /usr/local/etc/haproxy/haproxy.cfg`
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Troubleshooting
|
Troubleshooting
|
||||||
|
|
||||||
* If you get "no such table: vapid_key" then your file pointers are probably wrong.
|
* If you get "no such table: vapid_key" then your file pointers are probably wrong.
|
||||||
|
|||||||
171
app.py
171
app.py
@@ -2,7 +2,8 @@
|
|||||||
Environment variables:
|
Environment variables:
|
||||||
- ADMIN_PASSWORD: password for admin user for sensitive endpoints, defaults to 'admin'
|
- ADMIN_PASSWORD: password for admin user for sensitive endpoints, defaults to 'admin'
|
||||||
- PUSH_SERVER_VERSION: optional version of server
|
- PUSH_SERVER_VERSION: optional version of server
|
||||||
- SQLALCHEMY_DATABASE_URI: path to sqlite file, starting with "sqlite:////"
|
- SECONDS_BETWEEN_NOTIFICATIONS: optional number of seconds between notifications, defaults to 5 minutes
|
||||||
|
- SQLALCHEMY_DATABASE_URI: absolute path to sqlite file, starting with "sqlite:////"
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from cryptography.hazmat.backends import default_backend
|
from cryptography.hazmat.backends import default_backend
|
||||||
@@ -23,7 +24,14 @@ import time
|
|||||||
|
|
||||||
CONTACT_EMAIL = "mailto:info@timesafari.app"
|
CONTACT_EMAIL = "mailto:info@timesafari.app"
|
||||||
|
|
||||||
|
# On Time Safari, this title bypasses the filters and shows the message directly.
|
||||||
|
TITLE_DIRECT_NOTIFICATION = 'DIRECT_NOTIFICATION'
|
||||||
|
# On Time Safari, this title triggers the API check for the user's latest data.
|
||||||
|
TITLE_DAILY_INDIVIDUAL_CHECK = 'DAILY_CHECK'
|
||||||
|
|
||||||
PUSH_SERVER_VERSION = os.getenv('PUSH_SERVER_VERSION')
|
PUSH_SERVER_VERSION = os.getenv('PUSH_SERVER_VERSION')
|
||||||
|
SECONDS_BETWEEN_NOTIFICATIONS_STR = os.getenv('SECONDS_BETWEEN_NOTIFICATIONS', '60')
|
||||||
|
SECONDS_BETWEEN_NOTIFICATIONS = int(SECONDS_BETWEEN_NOTIFICATIONS_STR)
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
@@ -53,11 +61,11 @@ class WebPushService():
|
|||||||
self.app.add_url_rule('/web-push/regenerate-vapid', view_func=self.regenerate_vapid, methods=['POST'])
|
self.app.add_url_rule('/web-push/regenerate-vapid', view_func=self.regenerate_vapid, methods=['POST'])
|
||||||
self.app.add_url_rule('/web-push/ping', view_func=self.ping, methods=['GET'])
|
self.app.add_url_rule('/web-push/ping', view_func=self.ping, methods=['GET'])
|
||||||
|
|
||||||
# Setting the database URI for the application
|
# Setting the database URI for the application, with an absolute path
|
||||||
db_uri = os.getenv('SQLALCHEMY_DATABASE_URI', 'sqlite:////app/instance/data/webpush.db')
|
db_uri = os.getenv('SQLALCHEMY_DATABASE_URI', 'sqlite:////app/instance/data/webpush.db')
|
||||||
# This relative path works in docker-compose
|
# This relative path works in docker-compose
|
||||||
# This relative path works on a local run if you link to this dir from "var/app-instance"
|
# This relative path works on a local run if you link to the dir with app.py from "var/app-instance"
|
||||||
#db_uri = os.getenv('SQLALCHEMY_DATABASE_URI', 'sqlite:///data/webpush.db')
|
db_uri = os.getenv('SQLALCHEMY_DATABASE_URI', 'sqlite:///data/webpush.db')
|
||||||
self.app.config['SQLALCHEMY_DATABASE_URI'] = db_uri
|
self.app.config['SQLALCHEMY_DATABASE_URI'] = db_uri
|
||||||
|
|
||||||
# Initializing the database with the application
|
# Initializing the database with the application
|
||||||
@@ -135,14 +143,14 @@ class WebPushService():
|
|||||||
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _send_push_notification(subscription_info: Dict, message: Dict, vapid_key: VAPIDKey) -> Dict[str, any]:
|
def _send_push_notification(subscription_info: Dict, message: Dict, vapid_private_key: str) -> Dict[str, any]:
|
||||||
"""
|
"""
|
||||||
Sends a push notification using the provided subscription information, message, and VAPID key.
|
Sends a push notification using the provided subscription information, message, and VAPID key.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
- subscription_info (Dict): The information required to send the push notification to a specific client.
|
- subscription_info (Dict): The information required to send the push notification to a specific client.
|
||||||
- message (Dict): The actual message content to be sent as the push notification.
|
- 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.
|
- vapid_private_key (str): The private key used for sending the notification.
|
||||||
|
|
||||||
Returns Dict with the following keys
|
Returns Dict with the following keys
|
||||||
- success: True if the push notification was sent successfully, False otherwise
|
- success: True if the push notification was sent successfully, False otherwise
|
||||||
@@ -160,7 +168,7 @@ class WebPushService():
|
|||||||
result = webpush(
|
result = webpush(
|
||||||
subscription_info=subscription_info,
|
subscription_info=subscription_info,
|
||||||
data=json.dumps(message),
|
data=json.dumps(message),
|
||||||
vapid_private_key=vapid_key.private_key,
|
vapid_private_key=vapid_private_key,
|
||||||
vapid_claims={"sub": CONTACT_EMAIL}
|
vapid_claims={"sub": CONTACT_EMAIL}
|
||||||
)
|
)
|
||||||
# "because sometimes that's what I had to do to make it work!" - Matthew
|
# "because sometimes that's what I had to do to make it work!" - Matthew
|
||||||
@@ -170,7 +178,7 @@ class WebPushService():
|
|||||||
except WebPushException as ex:
|
except WebPushException as ex:
|
||||||
now = datetime.datetime.now().isoformat()
|
now = datetime.datetime.now().isoformat()
|
||||||
endpoint = subscription_info['endpoint']
|
endpoint = subscription_info['endpoint']
|
||||||
print(f"{now}: Failed to send push notification for {endpoint} -- {ex}", flush=True)
|
print(f"{now}: Failed to send push notification for {json.dumps(endpoint)} -- {ex}", flush=True)
|
||||||
|
|
||||||
unsubscribed_msg = '410 Gone'
|
unsubscribed_msg = '410 Gone'
|
||||||
unsubscribed = False
|
unsubscribed = False
|
||||||
@@ -199,7 +207,7 @@ class WebPushService():
|
|||||||
1. Retrieves all subscription data from the database.
|
1. Retrieves all subscription data from the database.
|
||||||
2. Constructs the push notification message.
|
2. Constructs the push notification message.
|
||||||
3. Sends a push notification to each subscribed client.
|
3. Sends a push notification to each subscribed client.
|
||||||
4. Sleeps for 24 hours before repeating the process.
|
4. Sleeps for 5 minutes and repeats the process.
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- The method runs in an infinite loop, meaning it will keep sending notifications until the program is terminated.
|
- The method runs in an infinite loop, meaning it will keep sending notifications until the program is terminated.
|
||||||
@@ -211,7 +219,7 @@ class WebPushService():
|
|||||||
while True:
|
while True:
|
||||||
|
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
print(f"{now} - Starting to send subscriptions...", flush=True)
|
# print(f"{now} - Starting to send subscriptions...", flush=True)
|
||||||
|
|
||||||
# Creating a context for the application to enable database operations
|
# Creating a context for the application to enable database operations
|
||||||
with self.app.app_context():
|
with self.app.app_context():
|
||||||
@@ -219,19 +227,10 @@ class WebPushService():
|
|||||||
# Retrieve the VAPID key from the database
|
# Retrieve the VAPID key from the database
|
||||||
vapid_key = VAPIDKey.query.first()
|
vapid_key = VAPIDKey.query.first()
|
||||||
|
|
||||||
# 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}"}
|
|
||||||
|
|
||||||
# Determine the beginning and end time to check for subscriptions
|
# Determine the beginning and end time to check for subscriptions
|
||||||
settings = Settings.query.first()
|
settings = Settings.query.first()
|
||||||
if settings.running_notify_end_time is None:
|
if settings.running_notify_end_time is None:
|
||||||
|
# only do this if we're not already inside one of these loops
|
||||||
# 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.
|
Storing the HH:MM for the desired notification time isn't a bad idea.
|
||||||
@@ -244,8 +243,9 @@ class WebPushService():
|
|||||||
|
|
||||||
# get the previous notify end time from the DB as a datetime
|
# get the previous notify end time from the DB as a datetime
|
||||||
prev_notify_end_time = datetime.datetime.fromisoformat(settings.prev_notify_end_time)
|
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 it's before midnight this morning
|
||||||
if prev_notify_end_time < now.replace(hour=0, minute=0, second=0, microsecond=0):
|
if prev_notify_end_time < now.replace(hour=0, minute=0, second=0, microsecond=0):
|
||||||
|
# catch us up to the beginning of today
|
||||||
# make the start time the later of: prevNotifyEndTime or yesterday at this time
|
# make the start time the later of: prevNotifyEndTime or yesterday at this time
|
||||||
start_time = max(
|
start_time = max(
|
||||||
prev_notify_end_time,
|
prev_notify_end_time,
|
||||||
@@ -272,7 +272,13 @@ class WebPushService():
|
|||||||
# This really should update & continue only if the running_notify_end_time is still None,
|
# This really should update & continue only if the running_notify_end_time is still None,
|
||||||
# just in case another thread started.
|
# just in case another thread started.
|
||||||
settings.running_notify_end_time = end_time.isoformat()
|
settings.running_notify_end_time = end_time.isoformat()
|
||||||
|
|
||||||
|
# set all subscriptions without a time to the current time
|
||||||
|
# (these won't be picked-up in the current run, since the current minute is the end minute)
|
||||||
|
Subscription.query.filter_by(notify_time=None).update({Subscription.notify_time: now.strftime('%H:%M')})
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
# this check was generated by Copilot; it's probably unnecessary
|
# this check was generated by Copilot; it's probably unnecessary
|
||||||
if settings.running_notify_end_time == end_time.isoformat():
|
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
|
# Now get the 'HH:MM' value for start & end so we can compare to the notify_time field
|
||||||
@@ -292,9 +298,18 @@ class WebPushService():
|
|||||||
"auth": subscription.auth
|
"auth": subscription.auth
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result = WebPushService._send_push_notification(subscription_info, message, vapid_key)
|
# Construct the push notification message
|
||||||
|
# The title value is a key, defaulting to trigger 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
|
||||||
|
payload = {"title": subscription.notify_type}
|
||||||
|
if subscription.message:
|
||||||
|
payload['message'] = subscription.message
|
||||||
|
elif subscription.notify_type == TITLE_DIRECT_NOTIFICATION:
|
||||||
|
# They should get some message.
|
||||||
|
payload['message'] = "Just a friendly reminder: click and share some gratitude with the world."
|
||||||
|
result = WebPushService._send_push_notification(subscription_info, payload, vapid_key.private_key)
|
||||||
print(
|
print(
|
||||||
f"Result from sub {subscription.id}: success={result['success']} text={result['message']}",
|
f"Result from sub {subscription.id}: success={result['success']} message={result['message']}",
|
||||||
flush=True
|
flush=True
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -305,13 +320,12 @@ class WebPushService():
|
|||||||
else:
|
else:
|
||||||
print(f"{now} - Failed to update running_notify_end_time", flush=True)
|
print(f"{now} - Failed to update running_notify_end_time", flush=True)
|
||||||
else:
|
else:
|
||||||
print(f"{now} - Stopped because we're already running a notification check.", flush=True)
|
print(f"{now} - Subscription check stopped because we're already running a notification check.", flush=True)
|
||||||
|
|
||||||
|
|
||||||
self.latest_subscription_run = now.isoformat()
|
self.latest_subscription_run = now.isoformat()
|
||||||
|
|
||||||
# Sleep before repeating
|
# Sleep before repeating
|
||||||
time.sleep(5 * 60)
|
time.sleep(SECONDS_BETWEEN_NOTIFICATIONS)
|
||||||
|
|
||||||
# This is an endpoint, routed in __init__
|
# This is an endpoint, routed in __init__
|
||||||
def ping(self) -> str:
|
def ping(self) -> str:
|
||||||
@@ -415,6 +429,20 @@ class WebPushService():
|
|||||||
URL: /web-push/subscribe
|
URL: /web-push/subscribe
|
||||||
Method: POST
|
Method: POST
|
||||||
|
|
||||||
|
Body:
|
||||||
|
- JSON object with the following keys:
|
||||||
|
- endpoint: the endpoint URL for the push notification
|
||||||
|
- keys: a JSON object with the following keys:
|
||||||
|
- p256dh: the P-256 elliptic curve Diffie-Hellman key pair
|
||||||
|
- auth: the authentication secret for the push subscription
|
||||||
|
- message: an optional string message to send to the subscriber, max 100 characters
|
||||||
|
The message is only used for certain notifyType values like TITLE_DIRECT_NOTIFICATION
|
||||||
|
- notifyTime: a JSON object with the following keys:
|
||||||
|
- utcHour: the hour in UTC
|
||||||
|
- minute: the minute in UTC
|
||||||
|
- notifyType: optional type of notification to send
|
||||||
|
If not type is sent, the internal type is set to TITLE_DAILY_INDIVIDUAL_CHECK
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
- A JSON response with "success" as True or False, "message" as a response message string, and potentially a "result" as a request.Response object from sending a test notification
|
- A JSON response with "success" as True or False, "message" as a response message string, and potentially a "result" as a request.Response object from sending a test notification
|
||||||
|
|
||||||
@@ -426,6 +454,14 @@ class WebPushService():
|
|||||||
|
|
||||||
# Retrieving the content from the incoming request
|
# Retrieving the content from the incoming request
|
||||||
content = request.json
|
content = request.json
|
||||||
|
if (content is None) or ('endpoint' not in content) or ('keys' not in content):
|
||||||
|
return jsonify(success=False, message="Missing subscription information"), 400
|
||||||
|
if ('p256dh' not in content['keys']) or ('auth' not in content['keys']):
|
||||||
|
return jsonify(success=False, message="Missing subscription keys information"), 400
|
||||||
|
if ('notifyTime' not in content) or ('utcHour' not in content['notifyTime']):
|
||||||
|
return jsonify(success=False, message="Missing notifyTime information"), 400
|
||||||
|
if ('notifyType' in content) and (content['notifyType'] not in [TITLE_DIRECT_NOTIFICATION, TITLE_DAILY_INDIVIDUAL_CHECK]):
|
||||||
|
return jsonify(success=False, message="Invalid notifyType"), 400
|
||||||
|
|
||||||
# Retrieving the VAPID key from the database
|
# Retrieving the VAPID key from the database
|
||||||
vapid_key = VAPIDKey.query.first()
|
vapid_key = VAPIDKey.query.first()
|
||||||
@@ -435,19 +471,32 @@ class WebPushService():
|
|||||||
return jsonify(success=False, message="No VAPID keys available"), 500
|
return jsonify(success=False, message="No VAPID keys available"), 500
|
||||||
|
|
||||||
# Constructing the notify_time string
|
# Constructing the notify_time string
|
||||||
notify_time = "13:13" # random time that is in most people's waking hours (server time, typically UTC)
|
notify_hour = content['notifyTime']['utcHour']
|
||||||
if ('notifyTime' in content) and ('utcHour' in content['notifyTime']):
|
if 'minute' in content['notifyTime']:
|
||||||
notify_hour = content['notifyTime']['utcHour']
|
notify_minute = content['notifyTime']['minute']
|
||||||
if 'minute' in content['notifyTime']:
|
else:
|
||||||
notify_minute = content['notifyTime']['minute']
|
notify_minute = 0
|
||||||
|
notify_time = '{:02d}'.format(notify_hour) + ":" + '{:02d}'.format(notify_minute)
|
||||||
|
|
||||||
|
notify_type = TITLE_DAILY_INDIVIDUAL_CHECK
|
||||||
|
if 'notifyType' in content:
|
||||||
|
notify_type = content['notifyType']
|
||||||
|
|
||||||
|
# check that the message is 100 characters or less
|
||||||
|
message = None
|
||||||
|
if 'message' in content:
|
||||||
|
if len(content['message']) > 100:
|
||||||
|
return jsonify(success=False, message="Message is too long. Max 100 characters."), 400
|
||||||
else:
|
else:
|
||||||
notify_minute = 0
|
message = content['message']
|
||||||
notify_time = '{:02d}'.format(notify_hour) + ":" + '{:02d}'.format(notify_minute)
|
|
||||||
|
|
||||||
# Creating a new Subscription instance with the provided data
|
# Creating a new Subscription instance with the provided data
|
||||||
subscription = Subscription(auth=content['keys']['auth'],
|
subscription = Subscription(auth=content['keys']['auth'],
|
||||||
|
created_date=datetime.datetime.now().isoformat(),
|
||||||
endpoint=content['endpoint'],
|
endpoint=content['endpoint'],
|
||||||
|
message=message,
|
||||||
notify_time=notify_time,
|
notify_time=notify_time,
|
||||||
|
notify_type=notify_type,
|
||||||
p256dh=content['keys']['p256dh'],
|
p256dh=content['keys']['p256dh'],
|
||||||
vapid_key_id=vapid_key.id)
|
vapid_key_id=vapid_key.id)
|
||||||
|
|
||||||
@@ -455,10 +504,6 @@ class WebPushService():
|
|||||||
db.session.add(subscription)
|
db.session.add(subscription)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
# Introducing a delay (ensure that gateway endpoint is available)
|
|
||||||
# ... which I'm now commenting out because there's no pending request so it doesn't make sense... we'll see if things still work
|
|
||||||
#time.sleep(10)
|
|
||||||
|
|
||||||
# Constructing the subscription information for the push notification
|
# Constructing the subscription information for the push notification
|
||||||
subscription_info = {
|
subscription_info = {
|
||||||
"endpoint": subscription.endpoint,
|
"endpoint": subscription.endpoint,
|
||||||
@@ -472,7 +517,7 @@ class WebPushService():
|
|||||||
message = {"title": "Subscription Successful", "message": "Thank you for subscribing!"}
|
message = {"title": "Subscription Successful", "message": "Thank you for subscribing!"}
|
||||||
|
|
||||||
# Sending the confirmation push notification
|
# Sending the confirmation push notification
|
||||||
result = WebPushService._send_push_notification(subscription_info, message, vapid_key)
|
result = WebPushService._send_push_notification(subscription_info, message, vapid_key.private_key)
|
||||||
|
|
||||||
# Returning the operation status
|
# Returning the operation status
|
||||||
return jsonify(success=result["success"], message=result["message"]), 200
|
return jsonify(success=result["success"], message=result["message"]), 200
|
||||||
@@ -492,6 +537,14 @@ class WebPushService():
|
|||||||
URL: /web-push/unsubscribe
|
URL: /web-push/unsubscribe
|
||||||
Method: POST
|
Method: POST
|
||||||
|
|
||||||
|
Body:
|
||||||
|
- JSON object with the following keys:
|
||||||
|
- endpoint: the endpoint URL for the push notification
|
||||||
|
- keys: a JSON object with the following keys:
|
||||||
|
- p256dh: the P-256 elliptic curve Diffie-Hellman key pair
|
||||||
|
- auth: the authentication secret for the push subscription
|
||||||
|
- notifyType: "DAILY_CHECK" or "DIRECT_NOTIFICATION" -- if empty, all notifications deleted
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
- Tuple[str, int]: A JSON response indicating the success or failure of the operation, along with the appropriate HTTP status code.
|
- Tuple[str, int]: A JSON response indicating the success or failure of the operation, along with the appropriate HTTP status code.
|
||||||
|
|
||||||
@@ -502,20 +555,22 @@ class WebPushService():
|
|||||||
|
|
||||||
# Retrieving the endpoint from the incoming request
|
# Retrieving the endpoint from the incoming request
|
||||||
content = request.json
|
content = request.json
|
||||||
endpoint = content['endpoint']
|
endpoint = content.get('endpoint')
|
||||||
|
if (endpoint is None):
|
||||||
|
return jsonify(success=False, message="Missing endpoint information"), 400
|
||||||
|
|
||||||
# Searching for the subscription in the database using the endpoint
|
# Searching for the subscription in the database using the endpoint
|
||||||
subscription = Subscription.query.filter_by(endpoint=endpoint).first()
|
if 'notifyType' in content:
|
||||||
|
notify_type = content['notifyType']
|
||||||
# If the subscription is found, delete it from the database
|
print(f"Deleting subscription for {endpoint} with notifyType {notify_type}", flush=True)
|
||||||
if subscription:
|
db.session.query(Subscription).filter(and_(Subscription.endpoint == endpoint, Subscription.notify_type == notify_type)).delete(synchronize_session=False)
|
||||||
db.session.delete(subscription)
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return jsonify(success=True, message="Subscription deleted successfully"), 200
|
|
||||||
|
|
||||||
# If the subscription is not found, return an error message
|
|
||||||
else:
|
else:
|
||||||
return jsonify(success=False, message="Subscription not found"), 404
|
print(f"Deleting all subscriptions for {endpoint}", flush=True)
|
||||||
|
db.session.query(Subscription).filter(Subscription.endpoint == endpoint).delete(synchronize_session=False)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return jsonify(success=True, message="Subscription deleted successfully"), 200
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -543,9 +598,11 @@ class WebPushService():
|
|||||||
|
|
||||||
# Retrieving the subscription information from the incoming request
|
# Retrieving the subscription information from the incoming request
|
||||||
content = request.json
|
content = request.json
|
||||||
endpoint = content['endpoint']
|
endpoint = content.get('endpoint')
|
||||||
p256dh = content['keys']['p256dh']
|
p256dh = content.get('keys', {}).get('p256dh')
|
||||||
auth = content['keys']['auth']
|
auth = content.get('keys', {}).get('auth')
|
||||||
|
if (endpoint is None) or (p256dh is None) or (auth is None):
|
||||||
|
return jsonify({"success": False, "message": "Missing subscription information"}), 400
|
||||||
|
|
||||||
# Looking up the subscription in the database
|
# Looking up the subscription in the database
|
||||||
subscription = Subscription.query.filter_by(endpoint=endpoint, p256dh=p256dh, auth=auth).first()
|
subscription = Subscription.query.filter_by(endpoint=endpoint, p256dh=p256dh, auth=auth).first()
|
||||||
@@ -553,10 +610,10 @@ class WebPushService():
|
|||||||
# If the subscription is found, call the _send_push_notification method
|
# If the subscription is found, call the _send_push_notification method
|
||||||
if subscription:
|
if subscription:
|
||||||
subscription_info = {
|
subscription_info = {
|
||||||
"endpoint": subscription.endpoint,
|
"endpoint": endpoint,
|
||||||
"keys": {
|
"keys": {
|
||||||
"p256dh": subscription.p256dh,
|
"p256dh": p256dh,
|
||||||
"auth": subscription.auth
|
"auth": auth
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -571,13 +628,13 @@ class WebPushService():
|
|||||||
result = WebPushService._send_push_notification(
|
result = WebPushService._send_push_notification(
|
||||||
subscription_info,
|
subscription_info,
|
||||||
{"title": title, "message": message},
|
{"title": title, "message": message},
|
||||||
vapid_key
|
vapid_key.private_key
|
||||||
)
|
)
|
||||||
|
|
||||||
print(f"Test sent: {result['success']}")
|
print(f"Test sent: success={result['success']} message={result['message']}", flush=True)
|
||||||
return jsonify(success=result["success"], message=result["message"]), 200
|
return jsonify(success=result["success"], message=result["message"]), 200
|
||||||
else:
|
else:
|
||||||
print(f"Test failed due to missing subscription. Request: {json.dumps(content)}")
|
print(f"Test failed due to missing subscription. Request: {json.dumps(content)}", flush=True)
|
||||||
return jsonify({"success": False, "message": "Subscription not found"}), 404
|
return jsonify({"success": False, "message": "Subscription not found"}), 404
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -16,7 +16,10 @@ class Settings(db.Model):
|
|||||||
class Subscription(db.Model):
|
class Subscription(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
auth = db.Column(db.String(255), nullable=False)
|
auth = db.Column(db.String(255), nullable=False)
|
||||||
|
created_date = db.Column(db.String(29), nullable=True)
|
||||||
endpoint = db.Column(db.String(500), nullable=False)
|
endpoint = db.Column(db.String(500), nullable=False)
|
||||||
notify_time = db.Column(db.String(5), nullable=True) # HH:MM
|
message = db.Column(db.String(100), nullable=True)
|
||||||
|
notify_time = db.Column(db.String(5), nullable=False) # HH:MM
|
||||||
|
notify_type = db.Column(db.String(32), nullable=True) # 'DAILY_CHECK', 'DIRECT_NOTIFICATION'
|
||||||
p256dh = db.Column(db.String(255), nullable=False)
|
p256dh = db.Column(db.String(255), nullable=False)
|
||||||
vapid_key_id = db.Column(db.Integer, db.ForeignKey('vapid_key.id'), nullable=False)
|
vapid_key_id = db.Column(db.Integer, db.ForeignKey('vapid_key.id'), nullable=False)
|
||||||
|
|||||||
@@ -25,10 +25,10 @@ import sys
|
|||||||
# sVR_s8J4JHv3h4ZmvemL5w
|
# sVR_s8J4JHv3h4ZmvemL5w
|
||||||
|
|
||||||
subscription_info = {
|
subscription_info = {
|
||||||
"endpoint": "https://fcm.googleapis.com/fcm/send/eqNQV7MVPic:APA91bGrIMxqz3sQ4wboUkmZithJHMAdrNgjm6BYcIGmgJozgEGeg23JsXLlNpnKwzBCmUXh1ciHmE_3wZakHX-Rho5f9Xovc28nun4nH7w4BMoYzX27pOw_pC4FtfAkBQaQ-8jm36jf",
|
"endpoint": "https://fcm.googleapis.com/fcm/send/drt0Dj1eLFE:APA91bEwLMJ69OKH23-tX183oaH__BwEYPa7woAcI9hMZ0eOc4qmzYD4qsq_dr7L_H4xQhpa79ixFi6ZlfdQQXux_Mx2jKA9kgN9cdGXqcs8ynnzPcUYMRCkTIANw6JBH159DCqu66Lm",
|
||||||
"keys": {
|
"keys": {
|
||||||
"p256dh": "BDo2fIIN7qoA5bOVXdrHATZUSPHY7030V8PKW1mIHAZHDAxS-p6RggVeI7IZoi3bGxpR713RYY8H8vu-lX5LY1w",
|
"p256dh": "BKAc27ZWWY6-pwbz8BCSxH7DorffZ9lbPdTsbD8vOjNYn37OyBYtPmmNHSddChsVj0py6aPTON4J60e0jLgNIzI",
|
||||||
"auth": "sVR_s8J4JHv3h4ZmvemL5w"
|
"auth": "8o5vgTKJsPqb1Ve5GazVuQ"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user