Web push notifications (#4942)
* WIP push notifications * testing push * cleanup for web-push bootstrapping
This commit is contained in:
parent
97b140b4b4
commit
2c513ae396
34
.github/workflows/dev-build.yaml
vendored
34
.github/workflows/dev-build.yaml
vendored
@ -6,19 +6,19 @@ concurrency:
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['onboarding-flag'] # put your current branch to create a build. Core team only.
|
||||
branches: ["web-push-notifications-bootstrap"] # put your current branch to create a build. Core team only.
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- 'cloud-deployments/*'
|
||||
- 'images/**/*'
|
||||
- '.vscode/**/*'
|
||||
- '**/.env.example'
|
||||
- '.github/ISSUE_TEMPLATE/**/*'
|
||||
- '.devcontainer/**/*'
|
||||
- 'embed/**/*' # Embed should be published to frontend (yarn build:publish) if any changes are introduced
|
||||
- 'browser-extension/**/*' # Chrome extension is submodule
|
||||
- 'server/utils/agents/aibitat/example/**/*' # Do not push new image for local dev testing of new aibitat images.
|
||||
- 'extras/**/*' # Extra is just for news and other local content.
|
||||
- "**.md"
|
||||
- "cloud-deployments/*"
|
||||
- "images/**/*"
|
||||
- ".vscode/**/*"
|
||||
- "**/.env.example"
|
||||
- ".github/ISSUE_TEMPLATE/**/*"
|
||||
- ".devcontainer/**/*"
|
||||
- "embed/**/*" # Embed should be published to frontend (yarn build:publish) if any changes are introduced
|
||||
- "browser-extension/**/*" # Chrome extension is submodule
|
||||
- "server/utils/agents/aibitat/example/**/*" # Do not push new image for local dev testing of new aibitat images.
|
||||
- "extras/**/*" # Extra is just for news and other local content.
|
||||
|
||||
jobs:
|
||||
push_dev_build_to_dockerhub:
|
||||
@ -48,15 +48,15 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
version: v0.22.0
|
||||
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
|
||||
# Only login to the Docker Hub if the repo is mintplex/anythingllm, to allow for forks to build on GHCR
|
||||
if: steps.dockerhub.outputs.enabled == 'true'
|
||||
if: steps.dockerhub.outputs.enabled == 'true'
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
|
||||
@ -82,7 +82,7 @@ jobs:
|
||||
|
||||
# For Docker scout there are some intermediary reported CVEs which exists outside
|
||||
# of execution content or are unreachable by an attacker but exist in image.
|
||||
# We create VEX files for these so they don't show in scout summary.
|
||||
# We create VEX files for these so they don't show in scout summary.
|
||||
- name: Collect known and verified CVE exceptions
|
||||
id: cve-list
|
||||
run: |
|
||||
@ -116,4 +116,4 @@ jobs:
|
||||
$tag
|
||||
done
|
||||
done
|
||||
shell: bash
|
||||
shell: bash
|
||||
|
||||
26
frontend/public/service-workers/push-notifications.js
Normal file
26
frontend/public/service-workers/push-notifications.js
Normal file
@ -0,0 +1,26 @@
|
||||
function parseEventData(event) {
|
||||
try {
|
||||
return event.data.json();
|
||||
} catch (e) {
|
||||
console.error('Failed to parse event data - is payload valid? .text():\n', event.data.text());
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
self.addEventListener('push', function (event) {
|
||||
const payload = parseEventData(event);
|
||||
if (!payload) return;
|
||||
|
||||
// options: https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification#options
|
||||
self.registration.showNotification(payload.title || 'AnythingLLM', {
|
||||
...payload,
|
||||
icon: '/favicon.png',
|
||||
});
|
||||
});
|
||||
|
||||
self.addEventListener('notificationclick', function (event) {
|
||||
event.notification.close();
|
||||
const { onClickUrl = null } = event.notification.data || {};
|
||||
if (!onClickUrl) return;
|
||||
event.waitUntil(clients.openWindow(onClickUrl));
|
||||
});
|
||||
123
frontend/src/hooks/useWebPushNotifications.js
Normal file
123
frontend/src/hooks/useWebPushNotifications.js
Normal file
@ -0,0 +1,123 @@
|
||||
import { useEffect } from "react";
|
||||
import { API_BASE } from "@/utils/constants";
|
||||
import { baseHeaders } from "@/utils/request";
|
||||
|
||||
const PUSH_PUBKEY_URL = `${API_BASE}/web-push/pubkey`;
|
||||
const PUSH_USER_SUBSCRIBE_URL = `${API_BASE}/web-push/subscribe`;
|
||||
|
||||
// If you update the service worker, increment this version or else
|
||||
// the service worker will not be updated with new changes -
|
||||
// Its version ID is independent of the app version to prevent reloading
|
||||
// or cache busting when not needed.
|
||||
const SW_VERSION = "1.0.0";
|
||||
|
||||
function log(message, ...args) {
|
||||
if (typeof message === "object") message = JSON.stringify(message, null, 2);
|
||||
console.log(`[useWebPushNotifications] ${message}`, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to push notifications for the current client - can be called multiple times without re-subscribing
|
||||
* or generating infinite tokens.
|
||||
* @returns {void}
|
||||
*/
|
||||
export async function subscribeToPushNotifications() {
|
||||
try {
|
||||
if (!("serviceWorker" in navigator) || !("PushManager" in window)) {
|
||||
log("Push notifications not supported");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check current permission status
|
||||
const permission = await Notification.requestPermission();
|
||||
if (permission !== "granted") {
|
||||
log("Notification permission not granted");
|
||||
return;
|
||||
}
|
||||
|
||||
const publicKey = await fetch(PUSH_PUBKEY_URL, { headers: baseHeaders() })
|
||||
.then((res) => res.json())
|
||||
.then(({ publicKey }) => {
|
||||
if (!publicKey) throw new Error("No public key found or generated");
|
||||
return publicKey;
|
||||
})
|
||||
.catch(() => null);
|
||||
|
||||
if (!publicKey) return log("No public key found or generated");
|
||||
|
||||
const swReg = await navigator.serviceWorker.register(
|
||||
`/service-workers/push-notifications.js?v=${SW_VERSION}`
|
||||
);
|
||||
|
||||
// Check for updates
|
||||
swReg.addEventListener("updatefound", () => {
|
||||
const newWorker = swReg.installing;
|
||||
log("Service worker update found");
|
||||
|
||||
newWorker.addEventListener("statechange", () => {
|
||||
if (
|
||||
newWorker.state === "installed" &&
|
||||
navigator.serviceWorker.controller
|
||||
) {
|
||||
// New service worker is installed and ready
|
||||
log("New service worker installed, ready to activate");
|
||||
|
||||
// Optionally show a notification to the user
|
||||
if (confirm("A new version is available. Reload to update?")) {
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Handle service worker updates
|
||||
navigator.serviceWorker.addEventListener("controllerchange", () => {
|
||||
log("Service worker controller changed");
|
||||
});
|
||||
|
||||
if (swReg.installing) {
|
||||
await new Promise((resolve) => {
|
||||
swReg.installing.addEventListener("statechange", () => {
|
||||
if (swReg.installing?.state === "activated") resolve();
|
||||
});
|
||||
});
|
||||
} else if (swReg.waiting) {
|
||||
await new Promise((resolve) => {
|
||||
swReg.waiting.addEventListener("statechange", () => {
|
||||
if (swReg.waiting?.state === "activated") resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const subscription = await swReg.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: urlBase64ToUint8Array(publicKey),
|
||||
});
|
||||
await fetch(PUSH_USER_SUBSCRIBE_URL, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(subscription),
|
||||
headers: baseHeaders(),
|
||||
});
|
||||
} catch (error) {
|
||||
log("Error subscribing to push notifications", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook that registers a service worker for push notifications.
|
||||
* @returns {void}
|
||||
*/
|
||||
export default function useWebPushNotifications() {
|
||||
useEffect(() => {
|
||||
subscribeToPushNotifications();
|
||||
}, []);
|
||||
}
|
||||
|
||||
function urlBase64ToUint8Array(base64String) {
|
||||
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
|
||||
const base64 = (base64String + padding)
|
||||
.replace(/\-/g, "+")
|
||||
.replace(/_/g, "/");
|
||||
const rawData = atob(base64);
|
||||
return new Uint8Array([...rawData].map((char) => char.charCodeAt(0)));
|
||||
}
|
||||
1
server/.gitignore
vendored
1
server/.gitignore
vendored
@ -15,6 +15,7 @@ storage/plugins/agent-flows/*
|
||||
storage/plugins/office-extensions/*
|
||||
storage/plugins/anythingllm_mcp_servers.json
|
||||
!storage/documents/DOCUMENTS.md
|
||||
storage/push-notifications/*
|
||||
storage/direct-uploads
|
||||
logs/server.log
|
||||
*.db
|
||||
|
||||
27
server/endpoints/webPush.js
Normal file
27
server/endpoints/webPush.js
Normal file
@ -0,0 +1,27 @@
|
||||
const { reqBody } = require("../utils/http");
|
||||
const { validatedRequest } = require("../utils/middleware/validatedRequest");
|
||||
const { pushNotificationService } = require("../utils/PushNotifications");
|
||||
|
||||
function webPushEndpoints(app) {
|
||||
if (!app) return;
|
||||
|
||||
app.post(
|
||||
"/web-push/subscribe",
|
||||
[validatedRequest],
|
||||
async (request, response) => {
|
||||
const subscription = reqBody(request);
|
||||
await pushNotificationService.registerSubscription(
|
||||
response.locals.user,
|
||||
subscription
|
||||
);
|
||||
response.status(201).json({});
|
||||
}
|
||||
);
|
||||
|
||||
app.get("/web-push/pubkey", [validatedRequest], (_request, response) => {
|
||||
const publicKey = pushNotificationService.publicVapidKey;
|
||||
response.status(200).json({ publicKey });
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { webPushEndpoints };
|
||||
@ -29,6 +29,7 @@ const { communityHubEndpoints } = require("./endpoints/communityHub");
|
||||
const { agentFlowEndpoints } = require("./endpoints/agentFlows");
|
||||
const { mcpServersEndpoints } = require("./endpoints/mcpServers");
|
||||
const { mobileEndpoints } = require("./endpoints/mobile");
|
||||
const { webPushEndpoints } = require("./endpoints/webPush");
|
||||
const { httpLogger } = require("./middleware/httpLogger");
|
||||
const app = express();
|
||||
const apiRouter = express.Router();
|
||||
@ -79,7 +80,7 @@ communityHubEndpoints(apiRouter);
|
||||
agentFlowEndpoints(apiRouter);
|
||||
mcpServersEndpoints(apiRouter);
|
||||
mobileEndpoints(apiRouter);
|
||||
|
||||
webPushEndpoints(apiRouter);
|
||||
// Externally facing embedder endpoints
|
||||
embeddedEndpoints(apiRouter);
|
||||
|
||||
|
||||
@ -87,7 +87,7 @@ const User = {
|
||||
},
|
||||
|
||||
filterFields: function (user = {}) {
|
||||
const { password, ...rest } = user;
|
||||
const { password, web_push_subscription_config, ...rest } = user;
|
||||
return { ...rest };
|
||||
},
|
||||
_identifyErrorAndFormatMessage: function (error) {
|
||||
@ -217,9 +217,14 @@ const User = {
|
||||
}
|
||||
},
|
||||
|
||||
// Explicit direct update of user object.
|
||||
// Only use this method when directly setting a key value
|
||||
// that takes no user input for the keys being modified.
|
||||
/**
|
||||
* Explicit direct update of user object.
|
||||
* Only use this method when directly setting a key value
|
||||
* that takes no user input for the keys being modified.
|
||||
* @param {number} id - The id of the user to update.
|
||||
* @param {Object} data - The data to update the user with.
|
||||
* @returns {Promise<Object>} The updated user object.
|
||||
*/
|
||||
_update: async function (id = null, data = {}) {
|
||||
if (!id) throw new Error("No user id provided for update");
|
||||
|
||||
@ -235,6 +240,26 @@ const User = {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all users that match the given clause without filtering the fields.
|
||||
* Internal use only - do not use this method for user-input flows
|
||||
* @param {Object} clause - The clause to filter the users by.
|
||||
* @param {number|null} limit - The maximum number of users to return.
|
||||
* @returns {Promise<Array<User>>} The users that match the given clause.
|
||||
*/
|
||||
_where: async function (clause = {}, limit = null) {
|
||||
try {
|
||||
const users = await prisma.users.findMany({
|
||||
where: clause,
|
||||
...(limit !== null ? { take: limit } : {}),
|
||||
});
|
||||
return users;
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a user object based on the clause provided.
|
||||
* @param {Object} clause - The clause to use to find the user.
|
||||
|
||||
@ -84,6 +84,7 @@
|
||||
"uuid": "^9.0.0",
|
||||
"uuid-apikey": "^1.5.3",
|
||||
"weaviate-ts-client": "^1.4.0",
|
||||
"web-push": "^3.6.7",
|
||||
"winston": "^3.13.0"
|
||||
},
|
||||
"resolutions": {
|
||||
|
||||
@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "users" ADD COLUMN "web_push_subscription_config" TEXT;
|
||||
@ -58,32 +58,33 @@ model system_settings {
|
||||
}
|
||||
|
||||
model users {
|
||||
id Int @id @default(autoincrement())
|
||||
username String? @unique
|
||||
password String
|
||||
pfpFilename String?
|
||||
role String @default("default")
|
||||
suspended Int @default(0)
|
||||
seen_recovery_codes Boolean? @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
lastUpdatedAt DateTime @default(now())
|
||||
dailyMessageLimit Int?
|
||||
bio String? @default("")
|
||||
workspace_chats workspace_chats[]
|
||||
workspace_users workspace_users[]
|
||||
embed_configs embed_configs[]
|
||||
embed_chats embed_chats[]
|
||||
threads workspace_threads[]
|
||||
recovery_codes recovery_codes[]
|
||||
password_reset_tokens password_reset_tokens[]
|
||||
workspace_agent_invocations workspace_agent_invocations[]
|
||||
slash_command_presets slash_command_presets[]
|
||||
browser_extension_api_keys browser_extension_api_keys[]
|
||||
temporary_auth_tokens temporary_auth_tokens[]
|
||||
system_prompt_variables system_prompt_variables[]
|
||||
prompt_history prompt_history[]
|
||||
desktop_mobile_devices desktop_mobile_devices[]
|
||||
workspace_parsed_files workspace_parsed_files[]
|
||||
id Int @id @default(autoincrement())
|
||||
username String? @unique
|
||||
password String
|
||||
pfpFilename String?
|
||||
role String @default("default")
|
||||
suspended Int @default(0)
|
||||
seen_recovery_codes Boolean? @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
lastUpdatedAt DateTime @default(now())
|
||||
dailyMessageLimit Int?
|
||||
bio String? @default("")
|
||||
web_push_subscription_config String?
|
||||
workspace_chats workspace_chats[]
|
||||
workspace_users workspace_users[]
|
||||
embed_configs embed_configs[]
|
||||
embed_chats embed_chats[]
|
||||
threads workspace_threads[]
|
||||
recovery_codes recovery_codes[]
|
||||
password_reset_tokens password_reset_tokens[]
|
||||
workspace_agent_invocations workspace_agent_invocations[]
|
||||
slash_command_presets slash_command_presets[]
|
||||
browser_extension_api_keys browser_extension_api_keys[]
|
||||
temporary_auth_tokens temporary_auth_tokens[]
|
||||
system_prompt_variables system_prompt_variables[]
|
||||
prompt_history prompt_history[]
|
||||
desktop_mobile_devices desktop_mobile_devices[]
|
||||
workspace_parsed_files workspace_parsed_files[]
|
||||
}
|
||||
|
||||
model recovery_codes {
|
||||
|
||||
222
server/utils/PushNotifications/index.js
Normal file
222
server/utils/PushNotifications/index.js
Normal file
@ -0,0 +1,222 @@
|
||||
const webpush = require("web-push");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { User } = require("../../models/user");
|
||||
const { SystemSettings } = require("../../models/systemSettings");
|
||||
const { safeJsonParse } = require("../http");
|
||||
|
||||
/**
|
||||
* For more options, see:
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification#options
|
||||
* @typedef {Object} PushNotificationPayload
|
||||
* @property {string} title - The title of the notification.
|
||||
* @property {string} body - The message of the notification.
|
||||
* @property {Object} data - Unstructured data for the notification. Use this for anything non-standard.
|
||||
* @property {string} [data.onClickUrl] - The URL to open when the notification is clicked. Note: Can be relative or absolute.
|
||||
* @property {Object[]} actions - The actions for the notification.
|
||||
* @property {string} [actions[].action] - The action to perform when the notification is clicked. Handled in the service worker.
|
||||
* @property {string} [actions[].title] - The title of the action to show in the Options dropdown
|
||||
* @property {string} image - A string containing the URL of an image to be displayed in the notification.
|
||||
*/
|
||||
|
||||
class PushNotifications {
|
||||
static mailTo = "anythingllm@localhost";
|
||||
/**
|
||||
* @type {PushNotifications}
|
||||
*/
|
||||
static instance = null;
|
||||
|
||||
/**
|
||||
* The VAPID keys for the push notification service.
|
||||
* @type {{publicKey: string | null, privateKey: string | null}}
|
||||
*/
|
||||
#vapidKeys = {
|
||||
publicKey: null,
|
||||
privateKey: null,
|
||||
};
|
||||
|
||||
/**
|
||||
* The subscriptions for the push notification service.
|
||||
* @type {Map<string, Object>}
|
||||
*/
|
||||
#subscriptions = new Map();
|
||||
|
||||
constructor() {
|
||||
if (PushNotifications.instance) return PushNotifications.instance;
|
||||
PushNotifications.instance = this;
|
||||
}
|
||||
|
||||
#log(text, ...args) {
|
||||
console.log(`\x1b[36m[PushNotifications]\x1b[0m ${text}`, ...args);
|
||||
}
|
||||
|
||||
get pushService() {
|
||||
try {
|
||||
const vapidKeys = this.existingVapidKeys;
|
||||
if (!vapidKeys.publicKey || !vapidKeys.privateKey)
|
||||
throw new Error(
|
||||
"VAPID keys not found. Make sure they are generated in the main process first."
|
||||
);
|
||||
webpush.setVapidDetails(
|
||||
`mailto:${this.mailTo}`,
|
||||
vapidKeys.publicKey,
|
||||
vapidKeys.privateKey
|
||||
);
|
||||
return webpush;
|
||||
} catch (e) {
|
||||
console.error("Failed to set VAPID details", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
get storagePath() {
|
||||
return process.env.NODE_ENV === "development"
|
||||
? path.resolve(__dirname, `../../storage`, "push-notifications")
|
||||
: path.resolve(process.env.STORAGE_DIR, "push-notifications");
|
||||
}
|
||||
|
||||
get primarySubscriptionPath() {
|
||||
return path.resolve(this.storagePath, `primary-subscription.json`);
|
||||
}
|
||||
|
||||
get existingVapidKeys() {
|
||||
// Already loaded and binded to the instance
|
||||
if (this.#vapidKeys.publicKey && this.#vapidKeys.privateKey)
|
||||
return this.#vapidKeys;
|
||||
|
||||
const vapidKeysPath = path.resolve(this.storagePath, `vapid-keys.json`);
|
||||
if (!fs.existsSync(vapidKeysPath))
|
||||
return { publicKey: null, privateKey: null };
|
||||
|
||||
const existingVapidKeys = JSON.parse(
|
||||
fs.readFileSync(vapidKeysPath, "utf8")
|
||||
);
|
||||
this.#log(`Loaded existing VAPID keys!`);
|
||||
this.#vapidKeys.publicKey = existingVapidKeys.publicKey;
|
||||
this.#vapidKeys.privateKey = existingVapidKeys.privateKey;
|
||||
return this.#vapidKeys;
|
||||
}
|
||||
|
||||
get publicVapidKey() {
|
||||
return this.existingVapidKeys.publicKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the subscriptions for the push notification service.
|
||||
* In single user mode, the subscription is stored in the primary-subscription.json file.
|
||||
* In multi user mode, the subscriptions are stored in the database so we grab them from there
|
||||
* and store them in the #subscriptions map for reference later.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async loadSubscriptions() {
|
||||
const isMultiUserMode = await SystemSettings.isMultiUserMode();
|
||||
if (isMultiUserMode) {
|
||||
const users = await User._where({
|
||||
web_push_subscription_config: { not: null },
|
||||
});
|
||||
for (const user of users) {
|
||||
const subscription = safeJsonParse(
|
||||
user.web_push_subscription_config,
|
||||
null
|
||||
);
|
||||
if (subscription) this.#subscriptions.set(user.id, subscription);
|
||||
}
|
||||
this.#log(`Loaded ${this.#subscriptions.size} existing subscriptions.`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.#log("Loading single user mode subscriptions...");
|
||||
if (!fs.existsSync(this.primarySubscriptionPath)) return;
|
||||
const subscription = JSON.parse(
|
||||
fs.readFileSync(this.primarySubscriptionPath, "utf8")
|
||||
);
|
||||
if (subscription) this.#subscriptions.set("primary", subscription);
|
||||
this.#log(`Loaded primary user's existing subscription.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new subscription for a user.
|
||||
* In single user mode, the userId is mapped to "primary"
|
||||
* In multi user mode, the userId is the user's id in the database
|
||||
*
|
||||
* @param {Object|null} user - The user to register the subscription for.
|
||||
* @param {Object} subscription - The subscription to register.
|
||||
* @returns {Promise<PushNotifications>}
|
||||
*/
|
||||
async registerSubscription(user = null, subscription) {
|
||||
let userId = user?.id || "primary";
|
||||
this.#subscriptions.set(userId, subscription);
|
||||
|
||||
// If this was a real user, write the subscription to the database
|
||||
if (!!user) {
|
||||
await User._update(user.id, {
|
||||
web_push_subscription_config: JSON.stringify(subscription),
|
||||
});
|
||||
this.#log(`Registered or updated subscription for user - ${user.id}`);
|
||||
} else {
|
||||
if (!fs.existsSync(this.storagePath))
|
||||
fs.mkdirSync(this.storagePath, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
this.primarySubscriptionPath,
|
||||
JSON.stringify(subscription, null, 2)
|
||||
);
|
||||
this.#log(`Registered or updated primary user's subscription.`);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a push notification to all subscribed clients.
|
||||
* @param {Object} options - The options for the notification.
|
||||
* @param {"primary"|number} [options.to] - The subscription to send the notification to. "all" sends to all subscriptions, "primary" sends to the primary user (single user mode only), a number sends subscription to specific user
|
||||
* @param {PushNotificationPayload} [options.payload] - The payload to send to the clients.
|
||||
* @returns {void}
|
||||
*/
|
||||
sendNotification({ to = "primary", payload = {} } = {}) {
|
||||
if (this.#subscriptions.size === 0)
|
||||
return this.#log(".sendNotification() - No subscriptions found");
|
||||
if (!this.#subscriptions.has(to))
|
||||
return this.#log(
|
||||
`.sendNotification() - Subscription for user ${to} not found`
|
||||
);
|
||||
this.#log(`.sendNotification() - Sending notification to user ${to}`);
|
||||
this.pushService.sendNotification(
|
||||
this.#subscriptions.get(to),
|
||||
JSON.stringify(payload)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup the push notification service.
|
||||
* This will generate new VAPID keys if they don't exist and save them to the storage path.
|
||||
* It will also load the subscriptions from the database or the primary-subscription.json file.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async setupPushNotificationService() {
|
||||
const instance = PushNotifications.instance;
|
||||
const existingVapidKeys = instance.existingVapidKeys;
|
||||
|
||||
if (!existingVapidKeys.publicKey || !existingVapidKeys.privateKey) {
|
||||
instance.#log("Generating new VAPID keys...");
|
||||
const vapidKeys = webpush.generateVAPIDKeys();
|
||||
instance.#vapidKeys.publicKey = vapidKeys.publicKey;
|
||||
instance.#vapidKeys.privateKey = vapidKeys.privateKey;
|
||||
instance.#log(`New VAPID keys generated!`);
|
||||
if (!fs.existsSync(instance.storagePath))
|
||||
fs.mkdirSync(instance.storagePath, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.resolve(instance.storagePath, `vapid-keys.json`),
|
||||
JSON.stringify(vapidKeys, null, 2)
|
||||
);
|
||||
}
|
||||
|
||||
await instance.loadSubscriptions();
|
||||
instance.pushService;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
pushNotificationService: new PushNotifications(),
|
||||
PushNotifications,
|
||||
};
|
||||
@ -5,6 +5,7 @@ const { CommunicationKey } = require("../comKey");
|
||||
const setupTelemetry = require("../telemetry");
|
||||
const eagerLoadContextWindows = require("./eagerLoadContextWindows");
|
||||
const markOnboarded = require("./markOnboarded");
|
||||
const { PushNotifications } = require("../PushNotifications");
|
||||
|
||||
// Testing SSL? You can make a self signed certificate and point the ENVs to that location
|
||||
// make a directory in server called 'sslcert' - cd into it
|
||||
@ -35,6 +36,7 @@ function bootSSL(app, port = 3001) {
|
||||
new EncryptionManager();
|
||||
new BackgroundService().boot();
|
||||
await eagerLoadContextWindows();
|
||||
await PushNotifications.setupPushNotificationService();
|
||||
console.log(`Primary server in HTTPS mode listening on port ${port}`);
|
||||
})
|
||||
.on("error", catchSigTerms);
|
||||
@ -66,6 +68,7 @@ function bootHTTP(app, port = 3001) {
|
||||
new EncryptionManager();
|
||||
new BackgroundService().boot();
|
||||
await eagerLoadContextWindows();
|
||||
await PushNotifications.setupPushNotificationService();
|
||||
console.log(`Primary server in HTTP mode listening on port ${port}`);
|
||||
})
|
||||
.on("error", catchSigTerms);
|
||||
|
||||
@ -3874,6 +3874,16 @@ arraybuffer.prototype.slice@^1.0.3:
|
||||
is-array-buffer "^3.0.4"
|
||||
is-shared-array-buffer "^1.0.2"
|
||||
|
||||
asn1.js@^5.3.0:
|
||||
version "5.4.1"
|
||||
resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07"
|
||||
integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==
|
||||
dependencies:
|
||||
bn.js "^4.0.0"
|
||||
inherits "^2.0.1"
|
||||
minimalistic-assert "^1.0.0"
|
||||
safer-buffer "^2.1.0"
|
||||
|
||||
async@^3.2.3:
|
||||
version "3.2.5"
|
||||
resolved "https://registry.npmjs.org/async/-/async-3.2.5.tgz"
|
||||
@ -3992,6 +4002,11 @@ bl@^6.0.3:
|
||||
inherits "^2.0.4"
|
||||
readable-stream "^4.2.0"
|
||||
|
||||
bn.js@^4.0.0:
|
||||
version "4.12.2"
|
||||
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.2.tgz#3d8fed6796c24e177737f7cc5172ee04ef39ec99"
|
||||
integrity sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==
|
||||
|
||||
body-parser@^1.20.3, body-parser@~1.20.3:
|
||||
version "1.20.4"
|
||||
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.4.tgz#f8e20f4d06ca8a50a71ed329c15dccad1cdc547f"
|
||||
@ -5906,6 +5921,11 @@ http-proxy-agent@^7.0.0:
|
||||
agent-base "^7.1.0"
|
||||
debug "^4.3.4"
|
||||
|
||||
http_ece@1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/http_ece/-/http_ece-1.2.0.tgz#84d5885f052eae8c9b075eee4d2eb5105f114479"
|
||||
integrity sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA==
|
||||
|
||||
https-proxy-agent@^7.0.0:
|
||||
version "7.0.4"
|
||||
resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz"
|
||||
@ -5990,7 +6010,7 @@ inflight@^1.0.4:
|
||||
once "^1.3.0"
|
||||
wrappy "1"
|
||||
|
||||
inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3, inherits@~2.0.4:
|
||||
inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3, inherits@~2.0.4:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz"
|
||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||
@ -6826,6 +6846,11 @@ mimic-response@^3.1.0:
|
||||
resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz"
|
||||
integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==
|
||||
|
||||
minimalistic-assert@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
|
||||
integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
|
||||
|
||||
minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz"
|
||||
@ -6833,7 +6858,7 @@ minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.6:
|
||||
minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6:
|
||||
version "1.2.8"
|
||||
resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz"
|
||||
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
|
||||
@ -7927,7 +7952,7 @@ safe-timers@^1.1.0:
|
||||
resolved "https://registry.npmjs.org/safe-timers/-/safe-timers-1.1.0.tgz"
|
||||
integrity sha512-9aqY+v5eMvmRaluUEtdRThV1EjlSElzO7HuCj0sTW9xvp++8iJ9t/RWGNWV6/WHcUJLHpyT2SNf/apoKTU2EpA==
|
||||
|
||||
"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0":
|
||||
"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.1.0:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||
@ -8799,6 +8824,17 @@ weaviate-ts-client@^1.4.0:
|
||||
isomorphic-fetch "^3.0.0"
|
||||
uuid "^9.0.1"
|
||||
|
||||
web-push@^3.6.7:
|
||||
version "3.6.7"
|
||||
resolved "https://registry.yarnpkg.com/web-push/-/web-push-3.6.7.tgz#5f5e645951153e37ef90a6ddea5c150ea0f709e1"
|
||||
integrity sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A==
|
||||
dependencies:
|
||||
asn1.js "^5.3.0"
|
||||
http_ece "1.2.0"
|
||||
https-proxy-agent "^7.0.0"
|
||||
jws "^4.0.0"
|
||||
minimist "^1.2.5"
|
||||
|
||||
web-streams-polyfill@4.0.0-beta.3:
|
||||
version "4.0.0-beta.3"
|
||||
resolved "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user