Implement webpush from server

This commit is contained in:
Nicolas Mowen 2024-07-20 12:34:22 -06:00
parent 5398a661ff
commit f946690520
5 changed files with 54 additions and 13 deletions

View File

@ -38,4 +38,5 @@ google-generativeai == 0.6.*
ollama == 0.2.* ollama == 0.2.*
openai == 1.30.* openai == 1.30.*
# push notifications # push notifications
py-vapid == 1.9.* py-vapid == 1.9.*
pywebpush == 2.0.*

View File

@ -41,13 +41,13 @@ def get_vapid_pub_key():
def register_notifications(): def register_notifications():
username = request.headers.get("remote-user", type=str) or "admin" username = request.headers.get("remote-user", type=str) or "admin"
json: dict[str, any] = request.get_json(silent=True) or {} json: dict[str, any] = request.get_json(silent=True) or {}
token = json["token"] sub = json.get("sub")
if not token: if not sub:
return jsonify({"success": False, "message": "Token must be provided."}), 400 return jsonify({"success": False, "message": "Subscription must be provided."}), 400
try: try:
User.update(notification_tokens=User.notification_tokens.append(token)).where( User.update(notification_tokens=User.notification_tokens.append(sub)).where(
User.username == username User.username == username
).execute() ).execute()
return make_response( return make_response(

View File

@ -1,11 +1,13 @@
"""Handle sending notifications for Frigate via Firebase.""" """Handle sending notifications for Frigate via Firebase."""
import datetime
import json import json
import logging import logging
import os import os
from typing import Any, Callable from typing import Any, Callable
from py_vapid import Vapid01 from py_vapid import Vapid01
from pywebpush import WebPusher
from frigate.comms.dispatcher import Communicator from frigate.comms.dispatcher import Communicator
from frigate.config import FrigateConfig from frigate.config import FrigateConfig
@ -20,17 +22,17 @@ class WebPushClient(Communicator): # type: ignore[misc]
def __init__(self, config: FrigateConfig) -> None: def __init__(self, config: FrigateConfig) -> None:
self.config = config 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 # Pull keys from PEM or generate if they do not exist
self.key = Vapid01.from_file(os.path.join(CONFIG_DIR, "notifications.pem")) self.vapid = Vapid01.from_file(os.path.join(CONFIG_DIR, "notifications.pem"))
self.tokens = []
self.invalid_tokens = []
users: list[User] = User.select(User.notification_tokens).dicts().iterator() users: list[User] = User.select(User.notification_tokens).dicts().iterator()
for user in users: 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: def subscribe(self, receiver: Callable) -> None:
"""Wrapper for allowing dispatcher to subscribe.""" """Wrapper for allowing dispatcher to subscribe."""
@ -42,6 +44,20 @@ class WebPushClient(Communicator): # type: ignore[misc]
self.send_message(json.loads(payload)) self.send_message(json.loads(payload))
def send_message(self, payload: dict[str, any]) -> None: 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 # Only notify for alerts
if payload["after"]["severity"] != "alert": if payload["after"]["severity"] != "alert":
return return
@ -72,5 +88,17 @@ class WebPushClient(Communicator): # type: ignore[misc]
direct_url = f"{self.config.notifications.base_url}/review?id={reviewId}" 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", "")}' 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: def stop(self) -> None:
pass pass

6
package-lock.json generated Normal file
View File

@ -0,0 +1,6 @@
{
"name": "frigate",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

View File

@ -70,7 +70,10 @@ export default function NotificationView() {
// TODO make the notifications button show enable / disable depending on current state // TODO make the notifications button show enable / disable depending on current state
} }
<Button <Button
disabled={notificationsSubscribed == undefined} disabled={
notificationsSubscribed == undefined ||
publicKey == undefined
}
onClick={() => { onClick={() => {
Notification.requestPermission().then((permission) => { Notification.requestPermission().then((permission) => {
console.log("notification permissions are ", permission); console.log("notification permissions are ", permission);
@ -79,7 +82,10 @@ export default function NotificationView() {
.register(NOTIFICATION_SERVICE_WORKER) .register(NOTIFICATION_SERVICE_WORKER)
.then((registration) => { .then((registration) => {
registration.pushManager registration.pushManager
.subscribe() .subscribe({
userVisibleOnly: true,
applicationServerKey: publicKey,
})
.then((pushSubscription) => { .then((pushSubscription) => {
console.log(pushSubscription.endpoint); console.log(pushSubscription.endpoint);
axios.post("notifications/register", { axios.post("notifications/register", {