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
This commit is contained in:
parent
4854d40508
commit
0057792802
@ -4,9 +4,9 @@ import pathlib
|
|||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from snek import snode
|
from snek import snode
|
||||||
from snek.view.threads import ThreadsView
|
from snek.view.threads import ThreadsView
|
||||||
import json
|
import json
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
@ -22,6 +22,8 @@ from app.app import Application as BaseApplication
|
|||||||
from jinja2 import FileSystemLoader
|
from jinja2 import FileSystemLoader
|
||||||
|
|
||||||
from snek.sssh import start_ssh_server
|
from snek.sssh import start_ssh_server
|
||||||
|
|
||||||
|
from snek.system.notification import get_notifications
|
||||||
from snek.docs.app import Application as DocsApplication
|
from snek.docs.app import Application as DocsApplication
|
||||||
from snek.mapper import get_mappers
|
from snek.mapper import get_mappers
|
||||||
from snek.service import get_services
|
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.drive import DriveApiView
|
||||||
from snek.view.index import IndexView
|
from snek.view.index import IndexView
|
||||||
from snek.view.login import LoginView
|
from snek.view.login import LoginView
|
||||||
|
from snek.view.push import PushView
|
||||||
from snek.view.logout import LogoutView
|
from snek.view.logout import LogoutView
|
||||||
from snek.view.register import RegisterView
|
from snek.view.register import RegisterView
|
||||||
from snek.view.rpc import RPCView
|
from snek.view.rpc import RPCView
|
||||||
@ -101,6 +104,8 @@ class Application(BaseApplication):
|
|||||||
self.time_start = datetime.now()
|
self.time_start = datetime.now()
|
||||||
self.ssh_host = "0.0.0.0"
|
self.ssh_host = "0.0.0.0"
|
||||||
self.ssh_port = 2242
|
self.ssh_port = 2242
|
||||||
|
|
||||||
|
get_notifications()
|
||||||
self.setup_router()
|
self.setup_router()
|
||||||
self.ssh_server = None
|
self.ssh_server = None
|
||||||
self.sync_service = None
|
self.sync_service = None
|
||||||
@ -110,20 +115,20 @@ class Application(BaseApplication):
|
|||||||
self.mappers = get_mappers(app=self)
|
self.mappers = get_mappers(app=self)
|
||||||
self.broadcast_service = None
|
self.broadcast_service = None
|
||||||
self.user_availability_service_task = None
|
self.user_availability_service_task = None
|
||||||
|
|
||||||
self.on_startup.append(self.prepare_asyncio)
|
self.on_startup.append(self.prepare_asyncio)
|
||||||
self.on_startup.append(self.start_user_availability_service)
|
self.on_startup.append(self.start_user_availability_service)
|
||||||
self.on_startup.append(self.start_ssh_server)
|
self.on_startup.append(self.start_ssh_server)
|
||||||
self.on_startup.append(self.prepare_database)
|
self.on_startup.append(self.prepare_database)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def uptime_seconds(self):
|
def uptime_seconds(self):
|
||||||
return (datetime.now() - self.time_start).total_seconds()
|
return (datetime.now() - self.time_start).total_seconds()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def uptime(self):
|
def uptime(self):
|
||||||
return self._format_uptime(self.uptime_seconds)
|
return self._format_uptime(self.uptime_seconds)
|
||||||
|
|
||||||
def _format_uptime(self,seconds):
|
def _format_uptime(self,seconds):
|
||||||
seconds = int(seconds)
|
seconds = int(seconds)
|
||||||
days, seconds = divmod(seconds, 86400)
|
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())
|
app.user_availability_service_task = asyncio.create_task(app.services.socket.user_availability_service())
|
||||||
async def snode_sync(self, app):
|
async def snode_sync(self, app):
|
||||||
self.sync_service = asyncio.create_task(snode.sync_service(app))
|
self.sync_service = asyncio.create_task(snode.sync_service(app))
|
||||||
|
|
||||||
async def start_ssh_server(self, app):
|
async def start_ssh_server(self, app):
|
||||||
app.ssh_server = await start_ssh_server(app,app.ssh_host,app.ssh_port)
|
app.ssh_server = await start_ssh_server(app,app.ssh_host,app.ssh_port)
|
||||||
if app.ssh_server:
|
if app.ssh_server:
|
||||||
@ -208,6 +213,7 @@ class Application(BaseApplication):
|
|||||||
self.router.add_view("/settings/index.html", SettingsIndexView)
|
self.router.add_view("/settings/index.html", SettingsIndexView)
|
||||||
self.router.add_view("/settings/profile.html", SettingsProfileView)
|
self.router.add_view("/settings/profile.html", SettingsProfileView)
|
||||||
self.router.add_view("/settings/profile.json", 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("/web.html", WebView)
|
||||||
self.router.add_view("/login.html", LoginView)
|
self.router.add_view("/login.html", LoginView)
|
||||||
self.router.add_view("/login.json", LoginView)
|
self.router.add_view("/login.json", LoginView)
|
||||||
@ -248,9 +254,9 @@ class Application(BaseApplication):
|
|||||||
self.git = GitApplication(self)
|
self.git = GitApplication(self)
|
||||||
self.add_subapp("/webdav", self.webdav)
|
self.add_subapp("/webdav", self.webdav)
|
||||||
self.add_subapp("/git",self.git)
|
self.add_subapp("/git",self.git)
|
||||||
|
|
||||||
#self.router.add_get("/{file_path:.*}", self.static_handler)
|
#self.router.add_get("/{file_path:.*}", self.static_handler)
|
||||||
|
|
||||||
async def handle_test(self, request):
|
async def handle_test(self, request):
|
||||||
|
|
||||||
return await self.render_template(
|
return await self.render_template(
|
||||||
@ -279,9 +285,9 @@ class Application(BaseApplication):
|
|||||||
async for subscribed_channel in self.services.channel_member.find(
|
async for subscribed_channel in self.services.channel_member.find(
|
||||||
user_uid=request.session.get("uid"), deleted_at=None, is_banned=False
|
user_uid=request.session.get("uid"), deleted_at=None, is_banned=False
|
||||||
):
|
):
|
||||||
|
|
||||||
parent_object = await subscribed_channel.get_channel()
|
parent_object = await subscribed_channel.get_channel()
|
||||||
|
|
||||||
item = {}
|
item = {}
|
||||||
other_user = await self.services.channel_member.get_other_dm_user(
|
other_user = await self.services.channel_member.get_other_dm_user(
|
||||||
subscribed_channel["channel_uid"], request.session.get("uid")
|
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)
|
user_static_path = await self.services.user.get_static_path(uid)
|
||||||
if user_static_path:
|
if user_static_path:
|
||||||
paths.append(user_static_path)
|
paths.append(user_static_path)
|
||||||
|
|
||||||
for admin_uid in self.services.user.get_admin_uids():
|
for admin_uid in self.services.user.get_admin_uids():
|
||||||
user_static_path = await self.services.user.get_static_path(admin_uid)
|
user_static_path = await self.services.user.get_static_path(admin_uid)
|
||||||
if user_static_path:
|
if user_static_path:
|
||||||
paths.append(user_static_path)
|
paths.append(user_static_path)
|
||||||
|
|
||||||
paths.append(self.static_path)
|
paths.append(self.static_path)
|
||||||
|
|
||||||
for path in paths:
|
for path in paths:
|
||||||
|
@ -1,34 +1,54 @@
|
|||||||
this.onpush = (event) => {
|
// this.onpush = (event) => {
|
||||||
console.log(event.data);
|
// console.log(event.data);
|
||||||
// From here we can write the data to IndexedDB, send it to any open
|
// // From here we can write the data to IndexedDB, send it to any open
|
||||||
// windows, display a notification, etc.
|
// // 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
|
navigator.serviceWorker
|
||||||
.register("/service-worker.js")
|
.register("/service-worker.js")
|
||||||
.then((serviceWorkerRegistration) => {
|
.then((serviceWorkerRegistration) => {
|
||||||
serviceWorkerRegistration.pushManager.subscribe().then(
|
console.log(serviceWorkerRegistration);
|
||||||
(pushSubscription) => {
|
serviceWorkerRegistration.pushManager.subscribe({
|
||||||
const subscriptionObject = {
|
userVisibleOnly: true,
|
||||||
endpoint: pushSubscription.endpoint,
|
applicationServerKey: publicKey,
|
||||||
keys: {
|
}).then(
|
||||||
p256dh: pushSubscription.getKey("p256dh"),
|
(pushSubscription) => {
|
||||||
auth: pushSubscription.getKey("auth"),
|
const subscriptionObject = {
|
||||||
},
|
...pushSubscription.toJSON(),
|
||||||
encoding: PushManager.supportedContentEncodings,
|
encoding: PushManager.supportedContentEncodings,
|
||||||
/* other app-specific data, such as user identity */
|
/* other app-specific data, such as user identity */
|
||||||
};
|
};
|
||||||
console.log(
|
console.log(pushSubscription.endpoint, pushSubscription, pushSubscription.toJSON(), subscriptionObject);
|
||||||
pushSubscription.endpoint,
|
// The push subscription details needed by the application
|
||||||
pushSubscription,
|
// server are now available, and can be sent to it using,
|
||||||
subscriptionObject,
|
// 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);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
@ -24,18 +24,14 @@ async function subscribeUser() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Service Worker (service-worker.js)
|
// Service Worker (service-worker.js)
|
||||||
self.addEventListener("push", (event) => {
|
// self.addEventListener("push", (event) => {
|
||||||
const data = event.data.json();
|
// const data = event.data.json();
|
||||||
self.registration.showNotification(data.title, {
|
// self.registration.showNotification(data.title, {
|
||||||
body: data.message,
|
// body: data.message,
|
||||||
icon: data.icon,
|
// icon: data.icon,
|
||||||
});
|
// });
|
||||||
});
|
// });
|
||||||
|
|
||||||
/*
|
|
||||||
self.addEventListener("install", (event) => {
|
|
||||||
console.log("Service worker installed");
|
|
||||||
});
|
|
||||||
|
|
||||||
self.addEventListener("push", (event) => {
|
self.addEventListener("push", (event) => {
|
||||||
if (!(self.Notification && self.Notification.permission === "granted")) {
|
if (!(self.Notification && self.Notification.permission === "granted")) {
|
||||||
@ -62,4 +58,4 @@ self.addEventListener("notificationclick", (event) => {
|
|||||||
event.notification.close();
|
event.notification.close();
|
||||||
event.waitUntil(clients.openWindow(
|
event.waitUntil(clients.openWindow(
|
||||||
"https://snek.molodetz.nl",));
|
"https://snek.molodetz.nl",));
|
||||||
});*/
|
});
|
||||||
|
125
src/snek/system/notification.py
Normal file
125
src/snek/system/notification.py
Normal file
@ -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()
|
@ -7,9 +7,7 @@
|
|||||||
<title>Snek</title>
|
<title>Snek</title>
|
||||||
<style>{{highlight_styles}}</style>
|
<style>{{highlight_styles}}</style>
|
||||||
<script src="/polyfills/Promise.withResolvers.js" type="module"></script>
|
<script src="/polyfills/Promise.withResolvers.js" type="module"></script>
|
||||||
<!--
|
<script src="/push.js" type="module"></script>
|
||||||
<script src="/push.js"></script>
|
|
||||||
-->
|
|
||||||
<script src="/fancy-button.js" type="module"></script>
|
<script src="/fancy-button.js" type="module"></script>
|
||||||
<script src="/upload-button.js" type="module"></script>
|
<script src="/upload-button.js" type="module"></script>
|
||||||
<script src="/generic-form.js" type="module"></script>
|
<script src="/generic-form.js" type="module"></script>
|
||||||
@ -34,9 +32,6 @@
|
|||||||
<script defer src="https://umami.molodetz.nl/script.js" data-website-id="d127c3e4-dc70-4041-a1c8-bcc32c2492ea"></script>
|
<script defer src="https://umami.molodetz.nl/script.js" data-website-id="d127c3e4-dc70-4041-a1c8-bcc32c2492ea"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<header>
|
<header>
|
||||||
<div class="logo no-select">{% block header_text %}{% endblock %}</div>
|
<div class="logo no-select">{% block header_text %}{% endblock %}</div>
|
||||||
<nav class="no-select" style="overflow:hidden;scroll-behavior:smooth">
|
<nav class="no-select" style="overflow:hidden;scroll-behavior:smooth">
|
||||||
@ -54,8 +49,8 @@
|
|||||||
{% block sidebar %}
|
{% block sidebar %}
|
||||||
{% include "sidebar_channels.html" %}
|
{% include "sidebar_channels.html" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
<main>
|
<main>
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
<chat-window class="chat-area"></chat-window>
|
<chat-window class="chat-area"></chat-window>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
124
src/snek/view/push.py
Normal file
124
src/snek/view/push.py
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
# 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 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(
|
||||||
|
encoding=serialization.Encoding.X962,
|
||||||
|
format=serialization.PublicFormat.UncompressedPoint
|
||||||
|
)
|
||||||
|
).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)
|
||||||
|
if not user:
|
||||||
|
return await self.json_response({"error": "User not found"}, status=404)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
print(body)
|
||||||
|
notifications =get_notifications()
|
||||||
|
|
||||||
|
cert = base64.b64encode(
|
||||||
|
notifications.public_key.public_bytes(
|
||||||
|
encoding=serialization.Encoding.X962,
|
||||||
|
format=serialization.PublicFormat.UncompressedPoint
|
||||||
|
)
|
||||||
|
).decode('utf-8').rstrip("=")
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"TTL": "60",
|
||||||
|
"Authorization": f"WebPush {notifications.create_notification_authorization(body['endpoint'])}",
|
||||||
|
"Crypto-Key": f"p256ecdsa={cert}",
|
||||||
|
}
|
||||||
|
|
||||||
|
print(headers)
|
||||||
|
|
||||||
|
post_notification = requests.post(
|
||||||
|
body["endpoint"],
|
||||||
|
headers=headers)
|
||||||
|
|
||||||
|
print(post_notification.status_code)
|
||||||
|
print(post_notification.text)
|
||||||
|
|
||||||
|
|
||||||
|
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"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
user = {
|
||||||
|
"username": user["username"],
|
||||||
|
"email": user["email"],
|
||||||
|
"nick": user["nick"],
|
||||||
|
"uid": user["uid"],
|
||||||
|
"color": user["color"],
|
||||||
|
"memberships": memberships,
|
||||||
|
}
|
||||||
|
|
||||||
|
return await self.json_response(
|
||||||
|
{
|
||||||
|
"user": user,
|
||||||
|
"cache": await self.app.cache.create_cache_key(
|
||||||
|
self.app.cache.cache, None
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
Loading…
Reference in New Issue
Block a user