From 00577928022b7f232727a3e471c8b20aeaad00eb Mon Sep 17 00:00:00 2001 From: BordedDev <> Date: Mon, 17 Mar 2025 03:16:39 +0100 Subject: [PATCH 01/12] Initial setup for push notifications (still has issues with fcm aka chrome/opera) # Conflicts: # src/snek/templates/app.html # Conflicts: # src/snek/app.py # Conflicts: # src/snek/app.py # src/snek/templates/app.html # Conflicts: # src/snek/app.py # Conflicts: # src/snek/app.py # src/snek/static/push.js # src/snek/static/service-worker.js # src/snek/templates/app.html --- src/snek/app.py | 32 ++++---- src/snek/static/push.js | 82 ++++++++++++-------- src/snek/static/service-worker.js | 20 ++--- src/snek/system/notification.py | 125 ++++++++++++++++++++++++++++++ src/snek/templates/app.html | 11 +-- src/snek/view/push.py | 124 +++++++++++++++++++++++++++++ 6 files changed, 330 insertions(+), 64 deletions(-) create mode 100644 src/snek/system/notification.py create mode 100644 src/snek/view/push.py diff --git a/src/snek/app.py b/src/snek/app.py index c092b41..cb6ca45 100644 --- a/src/snek/app.py +++ b/src/snek/app.py @@ -4,9 +4,9 @@ import pathlib import time import uuid from datetime import datetime -from snek import snode +from snek import snode from snek.view.threads import ThreadsView -import json +import json logging.basicConfig(level=logging.DEBUG) from concurrent.futures import ThreadPoolExecutor @@ -22,6 +22,8 @@ from app.app import Application as BaseApplication from jinja2 import FileSystemLoader from snek.sssh import start_ssh_server + +from snek.system.notification import get_notifications from snek.docs.app import Application as DocsApplication from snek.mapper import get_mappers from snek.service import get_services @@ -38,6 +40,7 @@ from snek.view.drive import DriveView from snek.view.drive import DriveApiView from snek.view.index import IndexView from snek.view.login import LoginView +from snek.view.push import PushView from snek.view.logout import LogoutView from snek.view.register import RegisterView from snek.view.rpc import RPCView @@ -101,6 +104,8 @@ class Application(BaseApplication): self.time_start = datetime.now() self.ssh_host = "0.0.0.0" self.ssh_port = 2242 + + get_notifications() self.setup_router() self.ssh_server = None self.sync_service = None @@ -110,20 +115,20 @@ class Application(BaseApplication): self.mappers = get_mappers(app=self) self.broadcast_service = None self.user_availability_service_task = None - + self.on_startup.append(self.prepare_asyncio) self.on_startup.append(self.start_user_availability_service) self.on_startup.append(self.start_ssh_server) self.on_startup.append(self.prepare_database) - + @property def uptime_seconds(self): return (datetime.now() - self.time_start).total_seconds() - @property + @property def uptime(self): return self._format_uptime(self.uptime_seconds) - + def _format_uptime(self,seconds): seconds = int(seconds) days, seconds = divmod(seconds, 86400) @@ -147,7 +152,7 @@ class Application(BaseApplication): app.user_availability_service_task = asyncio.create_task(app.services.socket.user_availability_service()) async def snode_sync(self, app): self.sync_service = asyncio.create_task(snode.sync_service(app)) - + async def start_ssh_server(self, app): app.ssh_server = await start_ssh_server(app,app.ssh_host,app.ssh_port) if app.ssh_server: @@ -208,6 +213,7 @@ class Application(BaseApplication): self.router.add_view("/settings/index.html", SettingsIndexView) self.router.add_view("/settings/profile.html", SettingsProfileView) self.router.add_view("/settings/profile.json", SettingsProfileView) + self.router.add_view("/push.json", PushView) self.router.add_view("/web.html", WebView) self.router.add_view("/login.html", LoginView) self.router.add_view("/login.json", LoginView) @@ -248,9 +254,9 @@ class Application(BaseApplication): self.git = GitApplication(self) self.add_subapp("/webdav", self.webdav) self.add_subapp("/git",self.git) - + #self.router.add_get("/{file_path:.*}", self.static_handler) - + async def handle_test(self, request): return await self.render_template( @@ -279,9 +285,9 @@ class Application(BaseApplication): async for subscribed_channel in self.services.channel_member.find( user_uid=request.session.get("uid"), deleted_at=None, is_banned=False ): - + parent_object = await subscribed_channel.get_channel() - + item = {} other_user = await self.services.channel_member.get_other_dm_user( subscribed_channel["channel_uid"], request.session.get("uid") @@ -340,12 +346,12 @@ class Application(BaseApplication): user_static_path = await self.services.user.get_static_path(uid) if user_static_path: paths.append(user_static_path) - + for admin_uid in self.services.user.get_admin_uids(): user_static_path = await self.services.user.get_static_path(admin_uid) if user_static_path: paths.append(user_static_path) - + paths.append(self.static_path) for path in paths: diff --git a/src/snek/static/push.js b/src/snek/static/push.js index f9917f4..c644210 100644 --- a/src/snek/static/push.js +++ b/src/snek/static/push.js @@ -1,34 +1,54 @@ -this.onpush = (event) => { - console.log(event.data); - // From here we can write the data to IndexedDB, send it to any open - // windows, display a notification, etc. -}; +// this.onpush = (event) => { +// console.log(event.data); +// // From here we can write the data to IndexedDB, send it to any open +// // windows, display a notification, etc. +// }; + +function arrayBufferToBase64(buffer) { + return btoa(String.fromCharCode(...new Uint8Array(buffer))) +} + +const keyResponse = await fetch('/push.json') +const keyData = await keyResponse.json() + +const publicKey = Uint8Array.from(atob(keyData.publicKey), c => c.charCodeAt(0)) navigator.serviceWorker - .register("/service-worker.js") - .then((serviceWorkerRegistration) => { - serviceWorkerRegistration.pushManager.subscribe().then( - (pushSubscription) => { - const subscriptionObject = { - endpoint: pushSubscription.endpoint, - keys: { - p256dh: pushSubscription.getKey("p256dh"), - auth: pushSubscription.getKey("auth"), - }, - encoding: PushManager.supportedContentEncodings, - /* other app-specific data, such as user identity */ - }; - console.log( - pushSubscription.endpoint, - pushSubscription, - subscriptionObject, + .register("/service-worker.js") + .then((serviceWorkerRegistration) => { + console.log(serviceWorkerRegistration); + serviceWorkerRegistration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: publicKey, + }).then( + (pushSubscription) => { + const subscriptionObject = { + ...pushSubscription.toJSON(), + encoding: PushManager.supportedContentEncodings, + /* other app-specific data, such as user identity */ + }; + console.log(pushSubscription.endpoint, pushSubscription, pushSubscription.toJSON(), subscriptionObject); + // The push subscription details needed by the application + // server are now available, and can be sent to it using, + // for example, the fetch() API. + + fetch('/push.json', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(subscriptionObject), + }).then((response) => { + if (!response.ok) { + throw new Error('Bad status code from server.'); + } + return response.json(); + }).then((responseData) => { + console.log(responseData); + }); + }, + (error) => { + console.error(error); + }, ); - // The push subscription details needed by the application - // server are now available, and can be sent to it using, - // for example, the fetch() API. - }, - (error) => { - console.error(error); - }, - ); - }); + }); diff --git a/src/snek/static/service-worker.js b/src/snek/static/service-worker.js index c89b668..05dd7af 100644 --- a/src/snek/static/service-worker.js +++ b/src/snek/static/service-worker.js @@ -24,18 +24,14 @@ async function subscribeUser() { } // Service Worker (service-worker.js) -self.addEventListener("push", (event) => { - const data = event.data.json(); - self.registration.showNotification(data.title, { - body: data.message, - icon: data.icon, - }); -}); +// self.addEventListener("push", (event) => { +// const data = event.data.json(); +// self.registration.showNotification(data.title, { +// body: data.message, +// icon: data.icon, +// }); +// }); -/* -self.addEventListener("install", (event) => { - console.log("Service worker installed"); -}); self.addEventListener("push", (event) => { if (!(self.Notification && self.Notification.permission === "granted")) { @@ -62,4 +58,4 @@ self.addEventListener("notificationclick", (event) => { event.notification.close(); event.waitUntil(clients.openWindow( "https://snek.molodetz.nl",)); -});*/ +}); diff --git a/src/snek/system/notification.py b/src/snek/system/notification.py new file mode 100644 index 0000000..c35def3 --- /dev/null +++ b/src/snek/system/notification.py @@ -0,0 +1,125 @@ +import time +import base64 +import uuid +from functools import cache + +import jwt +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.backends import default_backend +from urllib.parse import urlparse +import os.path + + +PRIVATE_KEY_FILE = './notification-private.pem' +PRIVATE_KEY_PKCS8_FILE = './notification-private.pkcs8.pem' +PUBLIC_KEY_FILE = './notification-public.pem' + +def generate_private_key(): + if not os.path.isfile(PRIVATE_KEY_FILE): + private_key = ec.generate_private_key(ec.SECP256R1(), default_backend()) + + # Serialize the private key to PEM format + pem = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption() + ) + + # Write the private key to a file + with open(PRIVATE_KEY_FILE, 'wb') as pem_out: + pem_out.write(pem) + +def generate_pcks8_private_key(): + # openssl pkcs8 -topk8 -nocrypt -in private.pem -out private.pkcs8.pem + if not os.path.isfile(PRIVATE_KEY_PKCS8_FILE): + with open(PRIVATE_KEY_FILE, 'rb') as pem_in: + private_key = serialization.load_pem_private_key(pem_in.read(), password=None, backend=default_backend()) + + # Serialize the private key to PKCS8 format + pem = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption() + ) + + # Write the private key to a file + with open(PRIVATE_KEY_PKCS8_FILE, 'wb') as pem_out: + pem_out.write(pem) + +def generate_public_key(): + if not os.path.isfile(PUBLIC_KEY_FILE): + with open(PRIVATE_KEY_FILE, 'rb') as pem_in: + private_key = serialization.load_pem_private_key(pem_in.read(), password=None, backend=default_backend()) + + # Get the public key from the private key + public_key = private_key.public_key() + + # Serialize the public key to PEM format + pem = public_key.public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo + ) + + # Write the public key to a file + with open(PUBLIC_KEY_FILE, 'wb') as pem_out: + pem_out.write(pem) + +def ensure_certificates(): + generate_private_key() + generate_pcks8_private_key() + generate_public_key() + +class Notifications: + private_key_pem = None + public_key = None + public_key_jwk = None + + def __init__(self): + ensure_certificates() + with open(PRIVATE_KEY_FILE, 'rb') as pem_in: + private_key = serialization.load_pem_private_key(pem_in.read(), password=None, backend=default_backend()) + self.private_key_pem = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption() + ) + + with open(PUBLIC_KEY_FILE, 'rb') as pem_in: + self.public_key = serialization.load_pem_public_key(pem_in.read(), backend=default_backend()) + public_numbers = self.public_key.public_numbers() + + self.public_key_jwk = { + "kty": "EC", + "crv": "P-256", + "x": base64.urlsafe_b64encode(public_numbers.x.to_bytes(32, byteorder='big')).decode('utf-8'), + "y": base64.urlsafe_b64encode(public_numbers.y.to_bytes(32, byteorder='big')).decode('utf-8') + } + + print(f"Public key JWK: {self.public_key_jwk}") + + + + + def create_notification_authorization(self, push_url): + target = urlparse(push_url) + aud = f"{target.scheme}://{target.netloc}" + sub = "mailto:admin@molodetz.nl" + + identifier = str(uuid.uuid4()) + + print(f"Creating notification authorization for {aud} with identifier {identifier}") + + return jwt.encode({ + "sub": sub, + "aud": aud, + "exp": int(time.time()) + 60 * 60, + "nbf": int(time.time()), + "iat": int(time.time()), + "jti": identifier, + }, self.private_key_pem, algorithm='ES256') + + +@cache +def get_notifications(): + return Notifications() diff --git a/src/snek/templates/app.html b/src/snek/templates/app.html index c92ae76..ba9d0d1 100644 --- a/src/snek/templates/app.html +++ b/src/snek/templates/app.html @@ -7,9 +7,7 @@ Snek - + @@ -34,9 +32,6 @@ - - -
diff --git a/src/snek/view/push.py b/src/snek/view/push.py index db931e1..0dcae89 100644 --- a/src/snek/view/push.py +++ b/src/snek/view/push.py @@ -65,7 +65,7 @@ class PushView(BaseFormView): print(body) notifications =get_notifications() - cert = base64.b64encode( + cert = base64.urlsafe_b64encode( notifications.public_key.public_bytes( encoding=serialization.Encoding.X962, format=serialization.PublicFormat.UncompressedPoint From 43507145342760d59b097975e466cb48e3b76ff2 Mon Sep 17 00:00:00 2001 From: BordedDev <> Date: Sat, 12 Apr 2025 14:25:57 +0200 Subject: [PATCH 03/12] Added body to push notifications --- src/snek/static/push.js | 3 - src/snek/static/service-worker.js | 8 +- src/snek/system/notification.py | 165 ++++++++++++++++++++++++------ src/snek/view/push.py | 79 +++++++++----- 4 files changed, 191 insertions(+), 64 deletions(-) diff --git a/src/snek/static/push.js b/src/snek/static/push.js index f1089ae..2e40f2e 100644 --- a/src/snek/static/push.js +++ b/src/snek/static/push.js @@ -31,9 +31,6 @@ export const registerServiceWorker = async () => { /* other app-specific data, such as user identity */ }; console.log(pushSubscription.endpoint, pushSubscription, pushSubscription.toJSON(), subscriptionObject); - // The push subscription details needed by the application - // server are now available, and can be sent to it using, - // for example, the fetch() API. fetch('/push.json', { method: 'POST', diff --git a/src/snek/static/service-worker.js b/src/snek/static/service-worker.js index be89157..038bb5f 100644 --- a/src/snek/static/service-worker.js +++ b/src/snek/static/service-worker.js @@ -33,16 +33,18 @@ async function subscribeUser() { self.addEventListener("push", (event) => { - if (!(self.Notification && self.Notification.permission === "granted")) { + if (!self.Notification || self.Notification.permission !== "granted") { + console.log("Notification permission not granted"); return; } - console.log("Received a push message", event); const data = event.data?.json() ?? {}; + console.log("Received a push message", event, data); + const title = data.title || "Something Has Happened"; const message = data.message || "Here's something you might want to check out."; - const icon = "images/new-notification.png"; + const icon = data.icon || "images/new-notification.png"; console.log("showing message", title, message, icon); const reg = self.registration.showNotification(title, { diff --git a/src/snek/system/notification.py b/src/snek/system/notification.py index c35def3..3a60b7c 100644 --- a/src/snek/system/notification.py +++ b/src/snek/system/notification.py @@ -10,10 +10,14 @@ from cryptography.hazmat.backends import default_backend from urllib.parse import urlparse import os.path +from cryptography.hazmat.primitives.ciphers.aead import AESGCM +from cryptography.hazmat.primitives.hashes import SHA256 +from cryptography.hazmat.primitives.kdf.hkdf import HKDF + +PRIVATE_KEY_FILE = "./notification-private.pem" +PRIVATE_KEY_PKCS8_FILE = "./notification-private.pkcs8.pem" +PUBLIC_KEY_FILE = "./notification-public.pem" -PRIVATE_KEY_FILE = './notification-private.pem' -PRIVATE_KEY_PKCS8_FILE = './notification-private.pkcs8.pem' -PUBLIC_KEY_FILE = './notification-public.pem' def generate_private_key(): if not os.path.isfile(PRIVATE_KEY_FILE): @@ -23,34 +27,40 @@ def generate_private_key(): pem = private_key.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.NoEncryption() + encryption_algorithm=serialization.NoEncryption(), ) # Write the private key to a file - with open(PRIVATE_KEY_FILE, 'wb') as pem_out: + with open(PRIVATE_KEY_FILE, "wb") as pem_out: pem_out.write(pem) + def generate_pcks8_private_key(): # openssl pkcs8 -topk8 -nocrypt -in private.pem -out private.pkcs8.pem if not os.path.isfile(PRIVATE_KEY_PKCS8_FILE): - with open(PRIVATE_KEY_FILE, 'rb') as pem_in: - private_key = serialization.load_pem_private_key(pem_in.read(), password=None, backend=default_backend()) + with open(PRIVATE_KEY_FILE, "rb") as pem_in: + private_key = serialization.load_pem_private_key( + pem_in.read(), password=None, backend=default_backend() + ) # Serialize the private key to PKCS8 format pem = private_key.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.PKCS8, - encryption_algorithm=serialization.NoEncryption() + encryption_algorithm=serialization.NoEncryption(), ) # Write the private key to a file - with open(PRIVATE_KEY_PKCS8_FILE, 'wb') as pem_out: + with open(PRIVATE_KEY_PKCS8_FILE, "wb") as pem_out: pem_out.write(pem) + def generate_public_key(): if not os.path.isfile(PUBLIC_KEY_FILE): - with open(PRIVATE_KEY_FILE, 'rb') as pem_in: - private_key = serialization.load_pem_private_key(pem_in.read(), password=None, backend=default_backend()) + with open(PRIVATE_KEY_FILE, "rb") as pem_in: + private_key = serialization.load_pem_private_key( + pem_in.read(), password=None, backend=default_backend() + ) # Get the public key from the private key public_key = private_key.public_key() @@ -58,18 +68,30 @@ def generate_public_key(): # Serialize the public key to PEM format pem = public_key.public_bytes( encoding=serialization.Encoding.PEM, - format=serialization.PublicFormat.SubjectPublicKeyInfo + format=serialization.PublicFormat.SubjectPublicKeyInfo, ) # Write the public key to a file - with open(PUBLIC_KEY_FILE, 'wb') as pem_out: + with open(PUBLIC_KEY_FILE, "wb") as pem_out: pem_out.write(pem) + def ensure_certificates(): generate_private_key() generate_pcks8_private_key() generate_public_key() + +def hkdf(input_key, salt, info, length): + return HKDF( + algorithm=SHA256(), + length=length, + salt=salt, + info=info, + backend=default_backend(), + ).derive(input_key) + + class Notifications: private_key_pem = None public_key = None @@ -77,30 +99,35 @@ class Notifications: def __init__(self): ensure_certificates() - with open(PRIVATE_KEY_FILE, 'rb') as pem_in: - private_key = serialization.load_pem_private_key(pem_in.read(), password=None, backend=default_backend()) + with open(PRIVATE_KEY_FILE, "rb") as pem_in: + private_key = serialization.load_pem_private_key( + pem_in.read(), password=None, backend=default_backend() + ) self.private_key_pem = private_key.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.NoEncryption() + encryption_algorithm=serialization.NoEncryption(), ) - with open(PUBLIC_KEY_FILE, 'rb') as pem_in: - self.public_key = serialization.load_pem_public_key(pem_in.read(), backend=default_backend()) + with open(PUBLIC_KEY_FILE, "rb") as pem_in: + self.public_key = serialization.load_pem_public_key( + pem_in.read(), backend=default_backend() + ) public_numbers = self.public_key.public_numbers() self.public_key_jwk = { "kty": "EC", "crv": "P-256", - "x": base64.urlsafe_b64encode(public_numbers.x.to_bytes(32, byteorder='big')).decode('utf-8'), - "y": base64.urlsafe_b64encode(public_numbers.y.to_bytes(32, byteorder='big')).decode('utf-8') + "x": base64.urlsafe_b64encode( + public_numbers.x.to_bytes(32, byteorder="big") + ).decode("utf-8"), + "y": base64.urlsafe_b64encode( + public_numbers.y.to_bytes(32, byteorder="big") + ).decode("utf-8"), } print(f"Public key JWK: {self.public_key_jwk}") - - - def create_notification_authorization(self, push_url): target = urlparse(push_url) aud = f"{target.scheme}://{target.netloc}" @@ -108,17 +135,91 @@ class Notifications: identifier = str(uuid.uuid4()) - print(f"Creating notification authorization for {aud} with identifier {identifier}") + print( + f"Creating notification authorization for {aud} with identifier {identifier}" + ) - return jwt.encode({ - "sub": sub, - "aud": aud, - "exp": int(time.time()) + 60 * 60, - "nbf": int(time.time()), - "iat": int(time.time()), - "jti": identifier, - }, self.private_key_pem, algorithm='ES256') + return jwt.encode( + { + "sub": sub, + "aud": aud, + "exp": int(time.time()) + 60 * 60, + "nbf": int(time.time()), + "iat": int(time.time()), + "jti": identifier, + }, + self.private_key_pem, + algorithm="ES256", + ) + def create_encrypted_payload(self, auth: str, p256dh:str, payload:str): + # 1. Generate private key + private_key = ec.generate_private_key(ec.SECP256R1(), default_backend()) + + # 2. Get public key + public_key = private_key.public_key() + + salt = os.urandom(16) + + subscription_pub_key_bytes = base64.urlsafe_b64decode(p256dh+ '==') + subscription_public_key = ec.EllipticCurvePublicKey.from_encoded_point( + ec.SECP256R1(), subscription_pub_key_bytes + ) + shared_secret = private_key.exchange(ec.ECDH(), subscription_public_key) + + auth_dec = base64.b64decode(auth+ '==') + auth_enc = b"Content-Encoding: auth\x00" + prk = hkdf(shared_secret, auth_dec, auth_enc, 32) + + # 5. Build context + key_label = b"P-256\x00" + subscription_len = len(subscription_pub_key_bytes).to_bytes(2, "big") + local_pub_bytes = public_key.public_bytes( + encoding=serialization.Encoding.X962, format=serialization.PublicFormat.UncompressedPoint + ) + local_len = len(local_pub_bytes).to_bytes(2, "big") + context = ( + key_label + + subscription_len + + subscription_pub_key_bytes + + local_len + + local_pub_bytes + ) + + # 6. Generate nonce and CEK + nonce_enc = b"Content-Encoding: nonce\x00" + nonce_info = nonce_enc + context + cek_enc = b"Content-Encoding: aesgcm\x00" + cek_info = cek_enc + context + + nonce = hkdf(prk, salt, nonce_info, 12) + content_encryption_key = hkdf(prk, salt, cek_info, 16) + + # 7. Encrypt payload with AES-GCM + aesgcm = AESGCM(content_encryption_key) + padding_length = 0 # adjust if needed + padding = padding_length.to_bytes(2, "big") + b"\x00" * padding_length + combined = padding + payload.encode("utf-8") + encrypted = aesgcm.encrypt(nonce, combined, None) + + return { + 'payload': encrypted, + 'salt': salt, + 'public_key': local_pub_bytes + } + + @property + def public_key_base64(self): + return ( + base64.urlsafe_b64encode( + self.public_key.public_bytes( + encoding=serialization.Encoding.X962, + format=serialization.PublicFormat.UncompressedPoint, + ) + ) + .decode("utf-8") + .rstrip("=") + ) @cache def get_notifications(): diff --git a/src/snek/view/push.py b/src/snek/view/push.py index 0dcae89..00581ba 100644 --- a/src/snek/view/push.py +++ b/src/snek/view/push.py @@ -25,6 +25,7 @@ import base64 +import json import requests from cryptography.hazmat.primitives import serialization @@ -35,22 +36,25 @@ from snek.system.view import BaseFormView class PushView(BaseFormView): async def get(self): - notifications =get_notifications() + notifications = get_notifications() - return await self.json_response({ - "publicKey": base64.b64encode( - notifications.public_key.public_bytes( - encoding=serialization.Encoding.X962, - format=serialization.PublicFormat.UncompressedPoint + return await self.json_response( + { + "publicKey": base64.b64encode( + notifications.public_key.public_bytes( + encoding=serialization.Encoding.X962, + format=serialization.PublicFormat.UncompressedPoint, + ) ) - ).decode('utf-8').rstrip("="), - }) + .decode("utf-8") + .rstrip("="), + } + ) async def post(self): - memberships = [] user = {} - + user_id = self.session.get("uid") if user_id: user = await self.app.services.user.get(uid=user_id) @@ -59,34 +63,57 @@ class PushView(BaseFormView): body = await self.request.json() - if not ("encoding" in body and "endpoint" in body and "keys" in body and "p256dh" in body["keys"] and "auth" in body["keys"]): - return await self.json_response({"error": "Invalid request"}, status=400) + if not ( + "encoding" in body + and "endpoint" in body + and "keys" in body + and "p256dh" in body["keys"] + and "auth" in body["keys"] + ): + return await self.json_response( + {"error": "Invalid request"}, status=400 + ) print(body) - notifications =get_notifications() + notifications = get_notifications() - cert = base64.urlsafe_b64encode( - notifications.public_key.public_bytes( - encoding=serialization.Encoding.X962, - format=serialization.PublicFormat.UncompressedPoint - ) - ).decode('utf-8').rstrip("=") + cert = notifications.public_key_base64 + + test_payload = { + "title": "Hey retoor", + "message": "Guess what? ;P", + "icon": "https://molodetz.online/image/snek192.png", + "url": "https://localhost:8081", + } + + encryped = notifications.create_encrypted_payload( + body["keys"]["auth"], + body["keys"]["p256dh"], + json.dumps(test_payload), + ) + + payload = encryped["payload"] + salt = encryped["salt"] + public_key = encryped["public_key"] headers = { - "TTL": "60", - "Authorization": f"WebPush {notifications.create_notification_authorization(body['endpoint'])}", - "Crypto-Key": f"p256ecdsa={cert}", + "TTL": "60", + "Authorization": f"WebPush {notifications.create_notification_authorization(body['endpoint'])}", + "Crypto-Key": f"dh={base64.urlsafe_b64encode(public_key).decode('utf-8').rstrip('=')}; p256ecdsa={cert}", + "Encryption": f"salt={base64.urlsafe_b64encode(salt).decode('utf-8').rstrip('=')}", + + "Content-Encoding": "aesgcm", + "Content-Length": str(len(payload)), + "Content-Type": "application/octet-stream", } print(headers) - post_notification = requests.post( - body["endpoint"], - headers=headers) + post_notification = requests.post(body["endpoint"], headers=headers, data=payload) print(post_notification.status_code) print(post_notification.text) - + print(post_notification.headers) async for model in self.app.services.channel_member.find( user_uid=user_id, deleted_at=None, is_banned=False From d966c9529b26e366dfbdaa843160201cb59aced6 Mon Sep 17 00:00:00 2001 From: BordedDev <> Date: Sun, 13 Apr 2025 03:19:28 +0200 Subject: [PATCH 04/12] Cleaned up code a bit --- src/snek/static/push.js | 95 +++++++++++------------- src/snek/static/service-worker.js | 99 +++++++++++-------------- src/snek/system/notification.py | 117 ++++++++++++------------------ src/snek/templates/app.html | 2 +- src/snek/view/push.py | 4 +- 5 files changed, 131 insertions(+), 186 deletions(-) diff --git a/src/snek/static/push.js b/src/snek/static/push.js index 2e40f2e..89aacb6 100644 --- a/src/snek/static/push.js +++ b/src/snek/static/push.js @@ -1,62 +1,49 @@ -// this.onpush = (event) => { -// console.log(event.data); -// // From here we can write the data to IndexedDB, send it to any open -// // windows, display a notification, etc. -// }; +export const registerServiceWorker = async (silent = false) => { + try { + const serviceWorkerRegistration = await navigator.serviceWorker + .register("/service-worker.js") -function arrayBufferToBase64(buffer) { - return btoa(String.fromCharCode(...new Uint8Array(buffer))) + await serviceWorkerRegistration.update() + + const keyResponse = await fetch('/push.json') + const keyData = await keyResponse.json() + + const publicKey = Uint8Array.from(atob(keyData.publicKey), c => c.charCodeAt(0)) + + const pushSubscription = await serviceWorkerRegistration.pushManager.subscribe({ + userVisibleOnly: true, applicationServerKey: publicKey, + }) + + const subscriptionObject = { + ...pushSubscription.toJSON(), encoding: PushManager.supportedContentEncodings, + }; + console.log(pushSubscription.endpoint, pushSubscription, pushSubscription.toJSON(), subscriptionObject); + + const response = await fetch('/push.json', { + method: 'POST', headers: { + 'Content-Type': 'application/json', + }, body: JSON.stringify(subscriptionObject), + }) + + if (!response.ok) { + throw new Error('Bad status code from server.'); + } + + const responseData = await response.json(); + console.log('Registration response', responseData); + } catch (error) { + console.error("Error registering service worker:", error); + if (!silent) { + alert("Registering push notifications failed. Please check your browser settings and try again.\n\n" + error); + } + } } -const keyResponse = await fetch('/push.json') -const keyData = await keyResponse.json() -console.log("Key data", keyData); - -const publicKey = Uint8Array.from(atob(keyData.publicKey), c => c.charCodeAt(0)) - -export const registerServiceWorker = async () => { - navigator.serviceWorker - .register("/service-worker.js") - .then((serviceWorkerRegistration) => { - console.log(serviceWorkerRegistration); - serviceWorkerRegistration.pushManager.subscribe({ - userVisibleOnly: true, - applicationServerKey: publicKey, - }).then( - (pushSubscription) => { - const subscriptionObject = { - ...pushSubscription.toJSON(), - encoding: PushManager.supportedContentEncodings, - /* other app-specific data, such as user identity */ - }; - console.log(pushSubscription.endpoint, pushSubscription, pushSubscription.toJSON(), subscriptionObject); - - fetch('/push.json', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(subscriptionObject), - }).then((response) => { - if (!response.ok) { - throw new Error('Bad status code from server.'); - } - return response.json(); - }).then((responseData) => { - console.log(responseData); - }); - }, - (error) => { - console.error(error); - }, - ); - }); -} - -window.registerServiceWorker = () => { +window.registerNotificationsServiceWorker = () => { return Notification.requestPermission().then((permission) => { if (permission === "granted") { + console.log("Permission was granted"); return registerServiceWorker(); } else if (permission === "denied") { console.log("Permission was denied"); @@ -65,4 +52,4 @@ window.registerServiceWorker = () => { } }); }; -registerServiceWorker().catch(console.error); +registerServiceWorker(true).catch(console.error); diff --git a/src/snek/static/service-worker.js b/src/snek/static/service-worker.js index 038bb5f..310fa96 100644 --- a/src/snek/static/service-worker.js +++ b/src/snek/static/service-worker.js @@ -1,83 +1,66 @@ -async function requestNotificationPermission() { - const permission = await Notification.requestPermission(); - return permission === "granted"; -} +self.addEventListener("install", (event) => { + console.log("Service worker installing..."); + event.waitUntil( + caches.open("snek-cache").then((cache) => { + return cache.addAll([]); + }) + ); +}) -// Subscribe to Push Notifications -async function subscribeUser() { - const registration = - await navigator.serviceWorker.register("/service-worker.js"); - const subscription = await registration.pushManager.subscribe({ - userVisibleOnly: true, - applicationServerKey: urlBase64ToUint8Array(PUBLIC_VAPID_KEY), - }); +self.addEventListener("activate", (event) => { + event.waitUntil(self.registration?.navigationPreload.enable()); +}); - // Send subscription to your backend - await fetch("/subscribe", { - method: "POST", - body: JSON.stringify(subscription), - headers: { - "Content-Type": "application/json", - }, - }); -} +self.addEventListener("push", (event) => { + if (!self.Notification || self.Notification.permission !== "granted") { + console.log("Notification permission not granted"); + return; + } -// Service Worker (service-worker.js) -// self.addEventListener("push", (event) => { -// const data = event.data.json(); -// self.registration.showNotification(data.title, { -// body: data.message, -// icon: data.icon, -// }); -// }); + const data = event.data?.json() ?? {}; + console.log("Received a push message", event, data); + const title = data.title || "Something Has Happened"; + const message = + data.message || "Here's something you might want to check out."; + const icon = data.icon || "/image/snek512.png"; -self.addEventListener("push", (event) => { - if (!self.Notification || self.Notification.permission !== "granted") { - console.log("Notification permission not granted"); - return; - } + const notificationSettings = data.notificationSettings || {}; - const data = event.data?.json() ?? {}; - console.log("Received a push message", event, data); + console.log("Showing message", title, message, icon); - const title = data.title || "Something Has Happened"; - const message = - data.message || "Here's something you might want to check out."; - const icon = data.icon || "images/new-notification.png"; - console.log("showing message", title, message, icon); - - const reg = self.registration.showNotification(title, { - body: message, - tag: "simple-push-demo-notification", - icon, - }).then(e => console.log("success", e)).catch(console.error); - - event.waitUntil(reg); + const reg = self.registration.showNotification(title, { + body: message, + tag: "message-received", + icon, + badge: icon, + ...notificationSettings, + data, + }).then(e => console.log("Showing notification", e)).catch(console.error); + event.waitUntil(reg); }); self.addEventListener("notificationclick", (event) => { console.log("Notification click Received.", event); event.notification.close(); - event.waitUntil(clients.openWindow( - "https://snek.molodetz.nl",)); + event.waitUntil(clients.openWindow(`${event.notification.data.url || event.notification.data.link || `/web.html`}`)); }); self.addEventListener("notificationclose", (event) => { - + console.log("Notification closed", event); }) self.addEventListener("fetch", (event) => { // console.log("Fetch event for ", event.request.url); event.respondWith( caches.match(event.request).then((response) => { - if (response) { - // console.log("Found response in cache: ", response); - return response; - } - // console.log("No response found in cache. About to fetch from network..."); - return fetch(event.request); + if (response) { + // console.log("Found response in cache: ", response); + return response; + } + // console.log("No response found in cache. About to fetch from network..."); + return fetch(event.request); }) ); }) \ No newline at end of file diff --git a/src/snek/system/notification.py b/src/snek/system/notification.py index 3a60b7c..7f4274d 100644 --- a/src/snek/system/notification.py +++ b/src/snek/system/notification.py @@ -2,6 +2,7 @@ import time import base64 import uuid from functools import cache +from pathlib import Path import jwt from cryptography.hazmat.primitives.asymmetric import ec @@ -14,66 +15,55 @@ from cryptography.hazmat.primitives.ciphers.aead import AESGCM from cryptography.hazmat.primitives.hashes import SHA256 from cryptography.hazmat.primitives.kdf.hkdf import HKDF -PRIVATE_KEY_FILE = "./notification-private.pem" -PRIVATE_KEY_PKCS8_FILE = "./notification-private.pkcs8.pem" -PUBLIC_KEY_FILE = "./notification-public.pem" +# The only reason to persist the keys is to be able to use them in the web push + +PRIVATE_KEY_FILE = Path("./notification-private.pem") +PRIVATE_KEY_PKCS8_FILE = Path("./notification-private.pkcs8.pem") +PUBLIC_KEY_FILE = Path("./notification-public.pem") def generate_private_key(): - if not os.path.isfile(PRIVATE_KEY_FILE): + if not PRIVATE_KEY_FILE.exists(): private_key = ec.generate_private_key(ec.SECP256R1(), default_backend()) - # Serialize the private key to PEM format pem = private_key.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.TraditionalOpenSSL, encryption_algorithm=serialization.NoEncryption(), ) - # Write the private key to a file - with open(PRIVATE_KEY_FILE, "wb") as pem_out: - pem_out.write(pem) + PRIVATE_KEY_FILE.write_bytes(pem) def generate_pcks8_private_key(): - # openssl pkcs8 -topk8 -nocrypt -in private.pem -out private.pkcs8.pem - if not os.path.isfile(PRIVATE_KEY_PKCS8_FILE): - with open(PRIVATE_KEY_FILE, "rb") as pem_in: - private_key = serialization.load_pem_private_key( - pem_in.read(), password=None, backend=default_backend() - ) + if not PRIVATE_KEY_PKCS8_FILE.exists(): + private_key = serialization.load_pem_private_key( + PRIVATE_KEY_FILE.read_bytes(), password=None, backend=default_backend() + ) - # Serialize the private key to PKCS8 format pem = private_key.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.PKCS8, encryption_algorithm=serialization.NoEncryption(), ) - # Write the private key to a file - with open(PRIVATE_KEY_PKCS8_FILE, "wb") as pem_out: - pem_out.write(pem) + PRIVATE_KEY_PKCS8_FILE.write_bytes(pem) def generate_public_key(): - if not os.path.isfile(PUBLIC_KEY_FILE): - with open(PRIVATE_KEY_FILE, "rb") as pem_in: - private_key = serialization.load_pem_private_key( - pem_in.read(), password=None, backend=default_backend() - ) + if not PUBLIC_KEY_FILE.exists(): + private_key = serialization.load_pem_private_key( + PRIVATE_KEY_FILE.read_bytes(), password=None, backend=default_backend() + ) - # Get the public key from the private key public_key = private_key.public_key() - # Serialize the public key to PEM format pem = public_key.public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo, ) - # Write the public key to a file - with open(PUBLIC_KEY_FILE, "wb") as pem_out: - pem_out.write(pem) + PUBLIC_KEY_FILE.write_bytes(pem) def ensure_certificates(): @@ -92,41 +82,37 @@ def hkdf(input_key, salt, info, length): ).derive(input_key) +def _browser_base64(data): + return base64.urlsafe_b64encode(data).decode("utf-8").rstrip("=") + + class Notifications: private_key_pem = None public_key = None - public_key_jwk = None + public_key_base64 = None def __init__(self): ensure_certificates() - with open(PRIVATE_KEY_FILE, "rb") as pem_in: - private_key = serialization.load_pem_private_key( - pem_in.read(), password=None, backend=default_backend() - ) - self.private_key_pem = private_key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.NoEncryption(), - ) - with open(PUBLIC_KEY_FILE, "rb") as pem_in: - self.public_key = serialization.load_pem_public_key( - pem_in.read(), backend=default_backend() + private_key = serialization.load_pem_private_key( + PRIVATE_KEY_FILE.read_bytes(), password=None, backend=default_backend() + ) + self.private_key_pem = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption(), + ) + + self.public_key = serialization.load_pem_public_key( + PUBLIC_KEY_FILE.read_bytes(), backend=default_backend() + ) + + self.public_key_base64 = _browser_base64( + self.public_key.public_bytes( + encoding=serialization.Encoding.X962, + format=serialization.PublicFormat.UncompressedPoint, ) - public_numbers = self.public_key.public_numbers() - - self.public_key_jwk = { - "kty": "EC", - "crv": "P-256", - "x": base64.urlsafe_b64encode( - public_numbers.x.to_bytes(32, byteorder="big") - ).decode("utf-8"), - "y": base64.urlsafe_b64encode( - public_numbers.y.to_bytes(32, byteorder="big") - ).decode("utf-8"), - } - - print(f"Public key JWK: {self.public_key_jwk}") + ) def create_notification_authorization(self, push_url): target = urlparse(push_url) @@ -152,7 +138,7 @@ class Notifications: algorithm="ES256", ) - def create_encrypted_payload(self, auth: str, p256dh:str, payload:str): + def create_encrypted_payload(self, auth: str, p256dh: str, payload: str): # 1. Generate private key private_key = ec.generate_private_key(ec.SECP256R1(), default_backend()) @@ -161,13 +147,13 @@ class Notifications: salt = os.urandom(16) - subscription_pub_key_bytes = base64.urlsafe_b64decode(p256dh+ '==') + subscription_pub_key_bytes = base64.urlsafe_b64decode(p256dh + "====") subscription_public_key = ec.EllipticCurvePublicKey.from_encoded_point( ec.SECP256R1(), subscription_pub_key_bytes ) shared_secret = private_key.exchange(ec.ECDH(), subscription_public_key) - auth_dec = base64.b64decode(auth+ '==') + auth_dec = base64.urlsafe_b64decode(auth + "==") auth_enc = b"Content-Encoding: auth\x00" prk = hkdf(shared_secret, auth_dec, auth_enc, 32) @@ -175,7 +161,8 @@ class Notifications: key_label = b"P-256\x00" subscription_len = len(subscription_pub_key_bytes).to_bytes(2, "big") local_pub_bytes = public_key.public_bytes( - encoding=serialization.Encoding.X962, format=serialization.PublicFormat.UncompressedPoint + encoding=serialization.Encoding.X962, + format=serialization.PublicFormat.UncompressedPoint, ) local_len = len(local_pub_bytes).to_bytes(2, "big") context = ( @@ -208,18 +195,6 @@ class Notifications: 'public_key': local_pub_bytes } - @property - def public_key_base64(self): - return ( - base64.urlsafe_b64encode( - self.public_key.public_bytes( - encoding=serialization.Encoding.X962, - format=serialization.PublicFormat.UncompressedPoint, - ) - ) - .decode("utf-8") - .rstrip("=") - ) @cache def get_notifications(): diff --git a/src/snek/templates/app.html b/src/snek/templates/app.html index c27b803..0119606 100644 --- a/src/snek/templates/app.html +++ b/src/snek/templates/app.html @@ -41,7 +41,7 @@ 👥 ⚙️ - ✉️ + ✉️ 🔒 diff --git a/src/snek/view/push.py b/src/snek/view/push.py index 00581ba..bf1bca3 100644 --- a/src/snek/view/push.py +++ b/src/snek/view/push.py @@ -82,8 +82,8 @@ class PushView(BaseFormView): test_payload = { "title": "Hey retoor", "message": "Guess what? ;P", - "icon": "https://molodetz.online/image/snek192.png", - "url": "https://localhost:8081", + "icon": "/image/snek192.png", + "url": "/web.html", } encryped = notifications.create_encrypted_payload( From 326c549670d1155d4df5ac550066020453071ab1 Mon Sep 17 00:00:00 2001 From: BordedDev <> Date: Sun, 13 Apr 2025 23:14:01 +0200 Subject: [PATCH 05/12] Cleaned up code a bit --- src/snek/system/notification.py | 89 +++++++++++++++++---------------- src/snek/view/push.py | 19 ++----- 2 files changed, 49 insertions(+), 59 deletions(-) diff --git a/src/snek/system/notification.py b/src/snek/system/notification.py index 7f4274d..451e43a 100644 --- a/src/snek/system/notification.py +++ b/src/snek/system/notification.py @@ -1,3 +1,4 @@ +import random import time import base64 import uuid @@ -138,61 +139,61 @@ class Notifications: algorithm="ES256", ) - def create_encrypted_payload(self, auth: str, p256dh: str, payload: str): - # 1. Generate private key - private_key = ec.generate_private_key(ec.SECP256R1(), default_backend()) + def create_encrypted_payload(self, endpoint: str, auth: str, p256dh: str, payload: str): + message_private_key = ec.generate_private_key(ec.SECP256R1(), default_backend()) - # 2. Get public key - public_key = private_key.public_key() - - salt = os.urandom(16) - - subscription_pub_key_bytes = base64.urlsafe_b64decode(p256dh + "====") - subscription_public_key = ec.EllipticCurvePublicKey.from_encoded_point( - ec.SECP256R1(), subscription_pub_key_bytes - ) - shared_secret = private_key.exchange(ec.ECDH(), subscription_public_key) - - auth_dec = base64.urlsafe_b64decode(auth + "==") - auth_enc = b"Content-Encoding: auth\x00" - prk = hkdf(shared_secret, auth_dec, auth_enc, 32) - - # 5. Build context - key_label = b"P-256\x00" - subscription_len = len(subscription_pub_key_bytes).to_bytes(2, "big") - local_pub_bytes = public_key.public_bytes( + message_public_key_bytes = message_private_key.public_key().public_bytes( encoding=serialization.Encoding.X962, format=serialization.PublicFormat.UncompressedPoint, ) - local_len = len(local_pub_bytes).to_bytes(2, "big") - context = ( - key_label - + subscription_len - + subscription_pub_key_bytes - + local_len - + local_pub_bytes + + salt = os.urandom(16) + + user_key_bytes = base64.urlsafe_b64decode(p256dh + "==") + shared_secret = message_private_key.exchange( + ec.ECDH(), + ec.EllipticCurvePublicKey.from_encoded_point( + ec.SECP256R1(), user_key_bytes + ), ) - # 6. Generate nonce and CEK - nonce_enc = b"Content-Encoding: nonce\x00" - nonce_info = nonce_enc + context - cek_enc = b"Content-Encoding: aesgcm\x00" - cek_info = cek_enc + context + encryption_key = hkdf( + shared_secret, + base64.urlsafe_b64decode(auth + "=="), + b"Content-Encoding: auth\x00", + 32, + ) - nonce = hkdf(prk, salt, nonce_info, 12) - content_encryption_key = hkdf(prk, salt, cek_info, 16) + context = ( + b"P-256\x00" + + len(user_key_bytes).to_bytes(2, "big") + + user_key_bytes + + len(message_public_key_bytes).to_bytes(2, "big") + + message_public_key_bytes + ) - # 7. Encrypt payload with AES-GCM - aesgcm = AESGCM(content_encryption_key) - padding_length = 0 # adjust if needed + nonce = hkdf(encryption_key, salt, b"Content-Encoding: nonce\x00" + context, 12) + content_encryption_key = hkdf( + encryption_key, salt, b"Content-Encoding: aesgcm\x00" + context, 16 + ) + + padding_length = random.randint(0, 16) padding = padding_length.to_bytes(2, "big") + b"\x00" * padding_length - combined = padding + payload.encode("utf-8") - encrypted = aesgcm.encrypt(nonce, combined, None) + + data = AESGCM(content_encryption_key).encrypt( + nonce, padding + payload.encode("utf-8"), None + ) return { - 'payload': encrypted, - 'salt': salt, - 'public_key': local_pub_bytes + "headers": { + "Authorization": f"WebPush {self.create_notification_authorization(endpoint)}", + "Crypto-Key": f"dh={_browser_base64(message_public_key_bytes)}; p256ecdsa={self.public_key_base64}", + "Encryption": f"salt={_browser_base64(salt)}", + "Content-Encoding": "aesgcm", + "Content-Length": str(len(data)), + "Content-Type": "application/octet-stream", + }, + "data": data, } diff --git a/src/snek/view/push.py b/src/snek/view/push.py index bf1bca3..643e143 100644 --- a/src/snek/view/push.py +++ b/src/snek/view/push.py @@ -77,8 +77,6 @@ class PushView(BaseFormView): print(body) notifications = get_notifications() - cert = notifications.public_key_base64 - test_payload = { "title": "Hey retoor", "message": "Guess what? ;P", @@ -86,30 +84,21 @@ class PushView(BaseFormView): "url": "/web.html", } - encryped = notifications.create_encrypted_payload( + notification_info = notifications.create_encrypted_payload( + body['endpoint'], body["keys"]["auth"], body["keys"]["p256dh"], json.dumps(test_payload), ) - payload = encryped["payload"] - salt = encryped["salt"] - public_key = encryped["public_key"] - headers = { + **notification_info["headers"], "TTL": "60", - "Authorization": f"WebPush {notifications.create_notification_authorization(body['endpoint'])}", - "Crypto-Key": f"dh={base64.urlsafe_b64encode(public_key).decode('utf-8').rstrip('=')}; p256ecdsa={cert}", - "Encryption": f"salt={base64.urlsafe_b64encode(salt).decode('utf-8').rstrip('=')}", - - "Content-Encoding": "aesgcm", - "Content-Length": str(len(payload)), - "Content-Type": "application/octet-stream", } print(headers) - post_notification = requests.post(body["endpoint"], headers=headers, data=payload) + post_notification = requests.post(body["endpoint"], headers=headers, data=notification_info["data"]) print(post_notification.status_code) print(post_notification.text) From 744d0ace84776de5997d8d8ce12a16acee5505d0 Mon Sep 17 00:00:00 2001 From: BordedDev <> Date: Sun, 13 Apr 2025 23:20:30 +0200 Subject: [PATCH 06/12] Cleaned up code a bit --- src/snek/system/notification.py | 2 +- src/snek/view/push.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/snek/system/notification.py b/src/snek/system/notification.py index 451e43a..ad578d6 100644 --- a/src/snek/system/notification.py +++ b/src/snek/system/notification.py @@ -139,7 +139,7 @@ class Notifications: algorithm="ES256", ) - def create_encrypted_payload(self, endpoint: str, auth: str, p256dh: str, payload: str): + def create_notification_info_with_payload(self, endpoint: str, auth: str, p256dh: str, payload: str): message_private_key = ec.generate_private_key(ec.SECP256R1(), default_backend()) message_public_key_bytes = message_private_key.public_key().public_bytes( diff --git a/src/snek/view/push.py b/src/snek/view/push.py index 643e143..036cf85 100644 --- a/src/snek/view/push.py +++ b/src/snek/view/push.py @@ -84,7 +84,7 @@ class PushView(BaseFormView): "url": "/web.html", } - notification_info = notifications.create_encrypted_payload( + notification_info = notifications.create_notification_info_with_payload( body['endpoint'], body["keys"]["auth"], body["keys"]["p256dh"], From 272998f757c1e2b3f87ffa3ecd2a6cde5d1f7339 Mon Sep 17 00:00:00 2001 From: BordedDev <> Date: Sat, 19 Apr 2025 21:33:31 +0200 Subject: [PATCH 07/12] Updated conditional check --- src/snek/view/push.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/snek/view/push.py b/src/snek/view/push.py index 036cf85..15b082d 100644 --- a/src/snek/view/push.py +++ b/src/snek/view/push.py @@ -63,12 +63,14 @@ class PushView(BaseFormView): body = await self.request.json() - if not ( - "encoding" in body - and "endpoint" in body - and "keys" in body - and "p256dh" in body["keys"] - and "auth" in body["keys"] + if not all( + [ + "encoding" in body, + "endpoint" in body, + "keys" in body, + "p256dh" in body["keys"], + "auth" in body["keys"], + ] ): return await self.json_response( {"error": "Invalid request"}, status=400 @@ -85,7 +87,7 @@ class PushView(BaseFormView): } notification_info = notifications.create_notification_info_with_payload( - body['endpoint'], + body["endpoint"], body["keys"]["auth"], body["keys"]["p256dh"], json.dumps(test_payload), @@ -98,7 +100,9 @@ class PushView(BaseFormView): print(headers) - post_notification = requests.post(body["endpoint"], headers=headers, data=notification_info["data"]) + post_notification = requests.post( + body["endpoint"], headers=headers, data=notification_info["data"] + ) print(post_notification.status_code) print(post_notification.text) From aec2da11f27a0e045ac03a30fb70298d400d30ad Mon Sep 17 00:00:00 2001 From: BordedDev <> Date: Sat, 31 May 2025 19:08:55 +0200 Subject: [PATCH 08/12] Implement push notification service and registration --- src/snek/app.py | 8 +- src/snek/mapper/__init__.py | 2 + src/snek/mapper/push.py | 7 ++ src/snek/model/__init__.py | 2 + src/snek/model/push_registration.py | 8 ++ src/snek/service/__init__.py | 3 + src/snek/service/notification.py | 13 +++ .../notification.py => service/push.py} | 81 ++++++++++++++-- src/snek/static/push.js | 2 + src/snek/view/push.py | 97 +++++++------------ 10 files changed, 153 insertions(+), 70 deletions(-) create mode 100644 src/snek/mapper/push.py create mode 100644 src/snek/model/push_registration.py rename src/snek/{system/notification.py => service/push.py} (70%) diff --git a/src/snek/app.py b/src/snek/app.py index cb6ca45..ee82144 100644 --- a/src/snek/app.py +++ b/src/snek/app.py @@ -1,6 +1,7 @@ import asyncio import logging import pathlib +import ssl import time import uuid from datetime import datetime @@ -23,7 +24,6 @@ from jinja2 import FileSystemLoader from snek.sssh import start_ssh_server -from snek.system.notification import get_notifications from snek.docs.app import Application as DocsApplication from snek.mapper import get_mappers from snek.service import get_services @@ -105,7 +105,7 @@ class Application(BaseApplication): self.ssh_host = "0.0.0.0" self.ssh_port = 2242 - get_notifications() + self.setup_router() self.ssh_server = None self.sync_service = None @@ -380,7 +380,9 @@ app = Application(db_path="sqlite:///snek.db") async def main(): - await web._run_app(app, port=8081, host="0.0.0.0") + ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) + ssl_context.load_cert_chain('cert.pem', 'key.pem') + await web._run_app(app, port=8081, host="0.0.0.0", ssl_context=ssl_context) if __name__ == "__main__": diff --git a/src/snek/mapper/__init__.py b/src/snek/mapper/__init__.py index 69901ec..48a8a0e 100644 --- a/src/snek/mapper/__init__.py +++ b/src/snek/mapper/__init__.py @@ -9,6 +9,7 @@ from snek.mapper.notification import NotificationMapper from snek.mapper.user import UserMapper from snek.mapper.user_property import UserPropertyMapper from snek.mapper.repository import RepositoryMapper +from snek.mapper.push import PushMapper from snek.mapper.channel_attachment import ChannelAttachmentMapper from snek.mapper.container import ContainerMapper from snek.system.object import Object @@ -30,6 +31,7 @@ def get_mappers(app=None): "repository": RepositoryMapper(app=app), "channel_attachment": ChannelAttachmentMapper(app=app), "container": ContainerMapper(app=app), + "push": PushMapper(app=app), } ) diff --git a/src/snek/mapper/push.py b/src/snek/mapper/push.py new file mode 100644 index 0000000..2fe7c29 --- /dev/null +++ b/src/snek/mapper/push.py @@ -0,0 +1,7 @@ +from snek.model.push_registration import PushRegistrationModel +from snek.system.mapper import BaseMapper + + +class PushMapper(BaseMapper): + model_class = PushRegistrationModel + table_name = "push_registration" diff --git a/src/snek/model/__init__.py b/src/snek/model/__init__.py index dec05e8..e5c6054 100644 --- a/src/snek/model/__init__.py +++ b/src/snek/model/__init__.py @@ -12,6 +12,7 @@ from snek.model.user import UserModel from snek.model.user_property import UserPropertyModel from snek.model.repository import RepositoryModel from snek.model.channel_attachment import ChannelAttachmentModel +from snek.model.push_registration import PushRegistrationModel from snek.model.container import Container from snek.system.object import Object @@ -31,6 +32,7 @@ def get_models(): "repository": RepositoryModel, "channel_attachment": ChannelAttachmentModel, "container": Container, + "push_registration": PushRegistrationModel, } ) diff --git a/src/snek/model/push_registration.py b/src/snek/model/push_registration.py new file mode 100644 index 0000000..bb13be0 --- /dev/null +++ b/src/snek/model/push_registration.py @@ -0,0 +1,8 @@ +from snek.system.model import BaseModel, ModelField + + +class PushRegistrationModel(BaseModel): + user_uid = ModelField(name="user_uid", required=True) + endpoint = ModelField(name="endpoint", required=True) + key_auth = ModelField(name="key_auth", required=True) + key_p256dh = ModelField(name="key_p256dh", required=True) diff --git a/src/snek/service/__init__.py b/src/snek/service/__init__.py index 5669bdb..f1654fb 100644 --- a/src/snek/service/__init__.py +++ b/src/snek/service/__init__.py @@ -9,6 +9,7 @@ from snek.service.drive_item import DriveItemService from snek.service.notification import NotificationService from snek.service.socket import SocketService from snek.service.user import UserService +from snek.service.push import PushService from snek.service.user_property import UserPropertyService from snek.service.util import UtilService from snek.service.repository import RepositoryService @@ -17,6 +18,7 @@ from snek.service.container import ContainerService from snek.system.object import Object from snek.service.db import DBService + @functools.cache def get_services(app): return Object( @@ -36,6 +38,7 @@ def get_services(app): "db": DBService(app=app), "channel_attachment": ChannelAttachmentService(app=app), "container": ContainerService(app=app), + "push": PushService(app=app), } ) diff --git a/src/snek/service/notification.py b/src/snek/service/notification.py index a22e8ae..65d348f 100644 --- a/src/snek/service/notification.py +++ b/src/snek/service/notification.py @@ -62,4 +62,17 @@ class NotificationService(BaseService): except Exception: raise Exception(f"Failed to create notification: {model.errors}.") + try: + await self.app.services.push.notify_user( + user_uid=channel_member["user_uid"], + payload={ + "title": f"New message in {channel_member['label']}", + "message": f"{user['nick']}: {channel_message['message']}", + "icon": "/image/snek192.png", + "url": f"/channel/{channel_message['channel_uid']}.html", + }, + ) + except Exception as e: + print(f"Failed to send push notification:", e) + self.app.db.commit() diff --git a/src/snek/system/notification.py b/src/snek/service/push.py similarity index 70% rename from src/snek/system/notification.py rename to src/snek/service/push.py index ad578d6..ccba4af 100644 --- a/src/snek/system/notification.py +++ b/src/snek/service/push.py @@ -1,3 +1,7 @@ +import json + +import aiohttp +from snek.system.service import BaseService import random import time import base64 @@ -87,12 +91,15 @@ def _browser_base64(data): return base64.urlsafe_b64encode(data).decode("utf-8").rstrip("=") -class Notifications: +class PushService(BaseService): + mapper_name = "push" + private_key_pem = None public_key = None public_key_base64 = None - def __init__(self): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) ensure_certificates() private_key = serialization.load_pem_private_key( @@ -139,7 +146,9 @@ class Notifications: algorithm="ES256", ) - def create_notification_info_with_payload(self, endpoint: str, auth: str, p256dh: str, payload: str): + def create_notification_info_with_payload( + self, endpoint: str, auth: str, p256dh: str, payload: str + ): message_private_key = ec.generate_private_key(ec.SECP256R1(), default_backend()) message_public_key_bytes = message_private_key.public_key().public_bytes( @@ -196,7 +205,67 @@ class Notifications: "data": data, } + async def notify_user(self, user_uid: str, payload: dict): + async with aiohttp.ClientSession() as session: + async for subscription in self.find(user_uid=user_uid): + endpoint = subscription["endpoint"] + key_auth = subscription["key_auth"] + key_p256dh = subscription["key_p256dh"] -@cache -def get_notifications(): - return Notifications() + notification_info = self.create_notification_info_with_payload( + endpoint, key_auth, key_p256dh, json.dumps(payload) + ) + + headers = { + **notification_info["headers"], + "TTL": "60", + } + data = notification_info["data"] + + async with session.post( + endpoint, + headers=headers, + data=data, + ) as response: + if response.status == 201 or response.status == 200: + print( + f"Notification sent to user {user_uid} via endpoint {endpoint}" + ) + else: + print( + f"Failed to send notification to user {user_uid} via endpoint {endpoint}: {response.status}" + ) + else: + print(f"No push subscriptions found for user {user_uid}") + + async def register( + self, user_uid: str, endpoint: str, key_auth: str, key_p256dh: str + ): + if await self.exists( + user_uid=user_uid, + endpoint=endpoint, + key_auth=key_auth, + key_p256dh=key_p256dh, + ): + return + + model = await self.new() + model["user_uid"] = user_uid + model["endpoint"] = endpoint + model["key_auth"] = key_auth + model["key_p256dh"] = key_p256dh + + print( + f"Registering push subscription for user {user_uid} with endpoint {endpoint}" + ) + + if await self.save(model=model) and model: + print( + f"Push subscription registered for user {user_uid} with endpoint {endpoint}" + ) + + return model + + raise Exception( + f"Failed to register push subscription for user {user_uid} with endpoint {endpoint}" + ) \ No newline at end of file diff --git a/src/snek/static/push.js b/src/snek/static/push.js index 89aacb6..1652817 100644 --- a/src/snek/static/push.js +++ b/src/snek/static/push.js @@ -5,6 +5,8 @@ export const registerServiceWorker = async (silent = false) => { await serviceWorkerRegistration.update() + await navigator.serviceWorker.ready + const keyResponse = await fetch('/push.json') const keyData = await keyResponse.json() diff --git a/src/snek/view/push.py b/src/snek/view/push.py index 15b082d..9129ba6 100644 --- a/src/snek/view/push.py +++ b/src/snek/view/push.py @@ -30,18 +30,15 @@ import json import requests from cryptography.hazmat.primitives import serialization -from snek.system.notification import get_notifications from snek.system.view import BaseFormView class PushView(BaseFormView): async def get(self): - notifications = get_notifications() - return await self.json_response( { "publicKey": base64.b64encode( - notifications.public_key.public_bytes( + self.app.services.push.public_key.public_bytes( encoding=serialization.Encoding.X962, format=serialization.PublicFormat.UncompressedPoint, ) @@ -76,69 +73,47 @@ class PushView(BaseFormView): {"error": "Invalid request"}, status=400 ) - print(body) - notifications = get_notifications() - - test_payload = { - "title": "Hey retoor", - "message": "Guess what? ;P", - "icon": "/image/snek192.png", - "url": "/web.html", - } - - notification_info = notifications.create_notification_info_with_payload( - body["endpoint"], - body["keys"]["auth"], - body["keys"]["p256dh"], - json.dumps(test_payload), + regist = await self.app.services.push.register( + user_uid=user_id, + endpoint=body["endpoint"], + key_auth=body["keys"]["auth"], + key_p256dh=body["keys"]["p256dh"], ) - headers = { - **notification_info["headers"], - "TTL": "60", - } + if regist: + test_payload = { + "title": f"Welcome {user['nick']}!", + "message": "You'll now receive notifications from Snek :D", + "icon": "/image/snek192.png", + "url": "/web.html", + } - print(headers) - - post_notification = requests.post( - body["endpoint"], headers=headers, data=notification_info["data"] - ) - - print(post_notification.status_code) - print(post_notification.text) - print(post_notification.headers) - - async for model in self.app.services.channel_member.find( - user_uid=user_id, deleted_at=None, is_banned=False - ): - channel = await self.app.services.channel.get(uid=model["channel_uid"]) - memberships.append( - { - "name": channel["label"], - "description": model["description"], - "user_uid": model["user_uid"], - "is_moderator": model["is_moderator"], - "is_read_only": model["is_read_only"], - "is_muted": model["is_muted"], - "is_banned": model["is_banned"], - "channel_uid": model["channel_uid"], - "uid": model["uid"], - } + notification_info = ( + self.app.services.push.create_notification_info_with_payload( + body["endpoint"], + body["keys"]["auth"], + body["keys"]["p256dh"], + json.dumps(test_payload), + ) ) - user = { - "username": user["username"], - "email": user["email"], - "nick": user["nick"], - "uid": user["uid"], - "color": user["color"], - "memberships": memberships, - } + + headers = { + **notification_info["headers"], + "TTL": "60", + } + + print(headers) + + post_notification = requests.post( + body["endpoint"], headers=headers, data=notification_info["data"] + ) + + print(post_notification.status_code) + print(post_notification.text) + print(post_notification.headers) return await self.json_response( { - "user": user, - "cache": await self.app.cache.create_cache_key( - self.app.cache.cache, None - ), + "registered": True, } ) \ No newline at end of file From b01665f02cc2bca0e8337ddd65fa0d56864568e0 Mon Sep 17 00:00:00 2001 From: BordedDev <> Date: Sat, 31 May 2025 23:09:56 +0200 Subject: [PATCH 09/12] Added server (debug/testing) certs --- cert.pem | 21 +++++++++++++++++++++ key.pem | 28 ++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 cert.pem create mode 100644 key.pem diff --git a/cert.pem b/cert.pem new file mode 100644 index 0000000..677cf5f --- /dev/null +++ b/cert.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDazCCAlOgAwIBAgIUB7PQvHZD6v8hfxeaDbU3hC0nGQQwDQYJKoZIhvcNAQEL +BQAwRTELMAkGA1UEBhMCTkwxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNTA0MDYxOTUzMDhaFw0yNjA0 +MDYxOTUzMDhaMEUxCzAJBgNVBAYTAk5MMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw +HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQCtYf8PP7QjRJOfK6zmfAZhSKwMowCSYijKeChxsgyn +hDDE8A/OuOuluJh6M/X+ZH0Q4HWTAaTwrXesBBPhie+4KmtsykiI7QEHXVVrWHba +6t5ymKiFiu+rWMwJVznS7T8K+DPGLRO2bF71Fme4ofJ2Plb7PnF53R4Tc3aTMdIW +HrUsU1JMNmbCibSVlkfPXSg/HY3XLysCrtrldPHYbTGvBcDUil7qZ8hZ8ZxLMzu3 +GPo6awPc0RBqw3tZu6SCECwQJEM0gX2n5nSyVz+fVgvLozNL9kV89hbZo7H/M37O +zmxVNwsAwoHpAGmnYs3ZYt4Q8duYjF1AtgZyXgXgdMdLAgMBAAGjUzBRMB0GA1Ud +DgQWBBQtGeiVTYjzWb2hTqJwipRVXU1LnzAfBgNVHSMEGDAWgBQtGeiVTYjzWb2h +TqJwipRVXU1LnzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAc +1BacrGMlCd5nfYuvQfv0DdTVGc2FSqxPMRGrZKfjvjemgPMs0+DqUwCJiR6oEOGb +atOYoIBX9KGXSUKRYYc/N75bslwfV1CclNqd2mPxULfks/D8cAzf2mgw4kYSaDHs +tJkywBe9L6eIK4cQ5YJvutVNVKMYPi+9w+wKog/FafkamFfX/3SLCkGmV0Vv4g0q +Ro9KmTTQpJUvd63X8bONLs1t8p+HQfWmKlhuVn5+mncNdGREe8dbciXE5FKu8luN +dr/twoTZTPhmIHPmVEeNxS8hFSiu0iUPTO0HcCAODILGbtXbClA+1Z0ukiRfUya6 +tgVuEk0c64L86qGP7Ply +-----END CERTIFICATE----- diff --git a/key.pem b/key.pem new file mode 100644 index 0000000..01682ba --- /dev/null +++ b/key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCtYf8PP7QjRJOf +K6zmfAZhSKwMowCSYijKeChxsgynhDDE8A/OuOuluJh6M/X+ZH0Q4HWTAaTwrXes +BBPhie+4KmtsykiI7QEHXVVrWHba6t5ymKiFiu+rWMwJVznS7T8K+DPGLRO2bF71 +Fme4ofJ2Plb7PnF53R4Tc3aTMdIWHrUsU1JMNmbCibSVlkfPXSg/HY3XLysCrtrl +dPHYbTGvBcDUil7qZ8hZ8ZxLMzu3GPo6awPc0RBqw3tZu6SCECwQJEM0gX2n5nSy +Vz+fVgvLozNL9kV89hbZo7H/M37OzmxVNwsAwoHpAGmnYs3ZYt4Q8duYjF1AtgZy +XgXgdMdLAgMBAAECggEAFnbkqz8fweoNY8mEOiDGWth695rZuh20bKIA63+cRXV1 +NC8T0pRXGT5qUyW5sQpSwgWzINGiY09hJWJ/M5vBpDpVd4pbYj0DAxyZXV01mSER +TVvGNKH5x65WUWeB0Hh40J0JaEXy5edIrmIGx6oEAO9hfxAUzStUeES05QFxgk1Q +RI4rKgvVt4W4wEGqSqX7OMwSU1EHJkX+IKYUdXvFA4Gi192mHHhX9MMDK/RSaDOC +1ZzHzHeKoTlf4jaUcwATlibo8ExGu4wsY+y3+NKE15o6D36AZD7ObqDOF1RsyfGG +eyljXzcglZAJN9Ctrz0xj5Xt22HqwsPO0o0mJ7URYQKBgQDcUWiu2acJJyjEJ89F +aiw3z5RvyO9LksHXwkf6gAV+dro/JeUf7u9Qgz3bwnoqwL16u+vjZxrtcpzkjc2C ++DIr6spCf8XkneJ2FovrFDe6oJSFxbgeexkQEBgw0TskRKILN8PGS6FAOfe8Zkwz +OHAJOYjxoVVoSeDPnxdu6uwJSQKBgQDJdpwZrtjGKSxkcMJzUlmp3XAPdlI1hZkl +v56Sdj6+Wz9bNTFlgiPHS+4Z7M+LyotShOEqwMfe+MDqVxTIB9TWfnmvnFDxI1VB +orHogWVWMHOqPJAzGrrWgbG2CSIiwQ3WFxU1nXqAeNk9aIFidGco87l3lVb4XEZs +eoUOUic/8wKBgQCK6r3x+gULjWhz/pH/t8l3y2hR78WKxld5XuQZvB06t0wKQy+s +qfC1uHsJlR+I04zl1ZYQBdQBwlHQ/uSFX0/rRxkPQxeZZkADq4W/zTiycUwU6S2F +8qJD8ZH/Pf5niOsP3bKQ1uEu6R4e6fXEGiLyfheuG8cJggPBhhO1eWUpGQKBgQDC +L+OzFce46gLyJYYopl3qz5iuLrx6/nVp31O3lOZRkZ52CcW9ND3MYjH1Jz++XNMC +DTcEgKGnGFrLBnjvfiz3Ox2L2b5jUE1jYLDfjanh8/3pP0s3FzK0hHqJHjCbEz6E +9+bnsQ1dPB8Zg9wCzHSLErHYxEf6SOdQtJ//98wBZQKBgQDLON5QPUAJ21uZRvwv +9LsjKMpd5f/L6/q5j6YYXNpys5MREUgryDpR/uqcmyBuxCU3vBeK8tpYJzfXqO45 +5jFoiKhtEFXjb1+d18ACKg1gXQF0Ljry59HGiZOw7IubRPHh9CDdT5tzynylipr3 +xhhX7RsDOYMFKmn59DS1CQCZAA== +-----END PRIVATE KEY----- From 20dd16734f3794f40530d2d0fbc296051a80fa24 Mon Sep 17 00:00:00 2001 From: BordedDev <> Date: Sat, 31 May 2025 23:29:45 +0200 Subject: [PATCH 10/12] Cleaned up push register handler --- src/snek/view/push.py | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/src/snek/view/push.py b/src/snek/view/push.py index 9129ba6..fa19e54 100644 --- a/src/snek/view/push.py +++ b/src/snek/view/push.py @@ -27,7 +27,7 @@ import base64 import json -import requests +import aiohttp from cryptography.hazmat.primitives import serialization from snek.system.view import BaseFormView @@ -49,9 +49,6 @@ class PushView(BaseFormView): ) async def post(self): - memberships = [] - user = {} - user_id = self.session.get("uid") if user_id: user = await self.app.services.user.get(uid=user_id) @@ -102,18 +99,14 @@ class PushView(BaseFormView): "TTL": "60", } - print(headers) + async with aiohttp.ClientSession() as session: + async with session.post( + body["endpoint"], + headers=headers, + data=notification_info["data"], + ) as post_notification: + print(post_notification.status) + print(post_notification.text) + print(post_notification.headers) - post_notification = requests.post( - body["endpoint"], headers=headers, data=notification_info["data"] - ) - - print(post_notification.status_code) - print(post_notification.text) - print(post_notification.headers) - - return await self.json_response( - { - "registered": True, - } - ) \ No newline at end of file + return await self.json_response({ "registered": True }) \ No newline at end of file From 0738b1ff91bb7f3391e79372da2e615184fab526 Mon Sep 17 00:00:00 2001 From: BordedDev <> Date: Sun, 1 Jun 2025 00:56:39 +0200 Subject: [PATCH 11/12] Removed old comment --- src/snek/view/push.py | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/src/snek/view/push.py b/src/snek/view/push.py index fa19e54..fe9f0b8 100644 --- a/src/snek/view/push.py +++ b/src/snek/view/push.py @@ -1,29 +1,3 @@ -# Written by retoor@molodetz.nl - -# This code defines an async class-based view called StatusView for handling HTTP GET requests. It fetches user details and their associated channel memberships from a database and returns a JSON response with user information if the user is logged in. - -# The code uses an imported module `BaseView`. There are dependencies on the `snek.system.view` module which provides the BaseView class. - -# MIT License -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, -# INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -# PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF -# CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE -# OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - import base64 import json From 94b9d2c63bd17764f5c7003f83d25884800839b6 Mon Sep 17 00:00:00 2001 From: BordedDev <> Date: Sun, 1 Jun 2025 13:03:47 +0200 Subject: [PATCH 12/12] Added user check to not notify user sending message --- src/snek/service/notification.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/snek/service/notification.py b/src/snek/service/notification.py index 65d348f..1e34bae 100644 --- a/src/snek/service/notification.py +++ b/src/snek/service/notification.py @@ -62,17 +62,18 @@ class NotificationService(BaseService): except Exception: raise Exception(f"Failed to create notification: {model.errors}.") - try: - await self.app.services.push.notify_user( - user_uid=channel_member["user_uid"], - payload={ - "title": f"New message in {channel_member['label']}", - "message": f"{user['nick']}: {channel_message['message']}", - "icon": "/image/snek192.png", - "url": f"/channel/{channel_message['channel_uid']}.html", - }, - ) - except Exception as e: - print(f"Failed to send push notification:", e) + if channel_member["user_uid"] != user["uid"]: + try: + await self.app.services.push.notify_user( + user_uid=channel_member["user_uid"], + payload={ + "title": f"New message in {channel_member['label']}", + "message": f"{user['nick']}: {channel_message['message']}", + "icon": "/image/snek192.png", + "url": f"/channel/{channel_message['channel_uid']}.html", + }, + ) + except Exception as e: + print(f"Failed to send push notification:", e) self.app.db.commit()