From f9466905203a53d113a5be7c382f1660f64a9575 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Sat, 20 Jul 2024 12:34:22 -0600 Subject: [PATCH] Implement webpush from server --- docker/main/requirements-wheels.txt | 3 +- frigate/api/notification.py | 8 ++-- frigate/comms/webpush.py | 40 ++++++++++++++++--- package-lock.json | 6 +++ .../settings/NotificationsSettingsView.tsx | 10 ++++- 5 files changed, 54 insertions(+), 13 deletions(-) create mode 100644 package-lock.json diff --git a/docker/main/requirements-wheels.txt b/docker/main/requirements-wheels.txt index 028a48925..f07dc149d 100644 --- a/docker/main/requirements-wheels.txt +++ b/docker/main/requirements-wheels.txt @@ -38,4 +38,5 @@ google-generativeai == 0.6.* ollama == 0.2.* openai == 1.30.* # push notifications -py-vapid == 1.9.* \ No newline at end of file +py-vapid == 1.9.* +pywebpush == 2.0.* \ No newline at end of file diff --git a/frigate/api/notification.py b/frigate/api/notification.py index de4e5770c..b49f2dd5a 100644 --- a/frigate/api/notification.py +++ b/frigate/api/notification.py @@ -41,13 +41,13 @@ def get_vapid_pub_key(): def register_notifications(): username = request.headers.get("remote-user", type=str) or "admin" json: dict[str, any] = request.get_json(silent=True) or {} - token = json["token"] + sub = json.get("sub") - if not token: - return jsonify({"success": False, "message": "Token must be provided."}), 400 + if not sub: + return jsonify({"success": False, "message": "Subscription must be provided."}), 400 try: - User.update(notification_tokens=User.notification_tokens.append(token)).where( + User.update(notification_tokens=User.notification_tokens.append(sub)).where( User.username == username ).execute() return make_response( diff --git a/frigate/comms/webpush.py b/frigate/comms/webpush.py index 994881a99..fdebe964c 100644 --- a/frigate/comms/webpush.py +++ b/frigate/comms/webpush.py @@ -1,11 +1,13 @@ """Handle sending notifications for Frigate via Firebase.""" +import datetime import json import logging import os from typing import Any, Callable from py_vapid import Vapid01 +from pywebpush import WebPusher from frigate.comms.dispatcher import Communicator from frigate.config import FrigateConfig @@ -20,17 +22,17 @@ class WebPushClient(Communicator): # type: ignore[misc] def __init__(self, config: FrigateConfig) -> None: self.config = config + self.claim = None + self.claim_headers = None + self.web_pushers: list[WebPusher] = [] # Pull keys from PEM or generate if they do not exist - self.key = Vapid01.from_file(os.path.join(CONFIG_DIR, "notifications.pem")) - - self.tokens = [] - self.invalid_tokens = [] + self.vapid = Vapid01.from_file(os.path.join(CONFIG_DIR, "notifications.pem")) users: list[User] = User.select(User.notification_tokens).dicts().iterator() - for user in users: - self.tokens.extend(user["notification_tokens"]) + for sub in user["notification_tokens"]: + self.web_pushers.append(WebPusher(sub)) def subscribe(self, receiver: Callable) -> None: """Wrapper for allowing dispatcher to subscribe.""" @@ -42,6 +44,20 @@ class WebPushClient(Communicator): # type: ignore[misc] self.send_message(json.loads(payload)) def send_message(self, payload: dict[str, any]) -> None: + # check for valid claim or create new one + now = datetime.datetime.now().timestamp() + if self.claim is None or self.claim["exp"] < now: + # create new claim + self.claim = { + "sub": "mailto:test@example.com", + "aud": "https://fcm.googleapis.com", + "exp": ( + datetime.datetime.now() + datetime.timedelta(hours=1) + ).timestamp(), + } + self.claim_headers = self.vapid.sign(self.claim) + logger.info(f"Updated claim with new headers {self.claim_headers}") + # Only notify for alerts if payload["after"]["severity"] != "alert": return @@ -72,5 +88,17 @@ class WebPushClient(Communicator): # type: ignore[misc] direct_url = f"{self.config.notifications.base_url}/review?id={reviewId}" image = f'{self.config.notifications.base_url}{payload["after"]["thumb_path"].replace("/media/frigate", "")}' + for pusher in self.web_pushers: + pusher.send( + headers=self.claim_headers, + ttl=0, + data=json.dumps({ + "title": title, + "message": message, + "direct_url": direct_url, + "image": image, + }), + ) + def stop(self) -> None: pass diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..13ac760ad --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "frigate", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/web/src/views/settings/NotificationsSettingsView.tsx b/web/src/views/settings/NotificationsSettingsView.tsx index 449a80e81..7247b26c2 100644 --- a/web/src/views/settings/NotificationsSettingsView.tsx +++ b/web/src/views/settings/NotificationsSettingsView.tsx @@ -70,7 +70,10 @@ export default function NotificationView() { // TODO make the notifications button show enable / disable depending on current state }