mirror of
https://github.com/binwiederhier/ntfy.git
synced 2026-05-15 07:35:49 -06:00
Merge 6a7c1c47aa into c6e252b3d6
This commit is contained in:
commit
8dfb7d6a83
5 changed files with 139 additions and 9 deletions
|
|
@ -255,6 +255,7 @@ Reference: <https://stackoverflow.com/questions/34160509/options-for-testing-ser
|
|||
go run main.go \
|
||||
--log-level debug \
|
||||
serve \
|
||||
--base-url http://localhost \
|
||||
--web-push-public-key KEY \
|
||||
--web-push-private-key KEY \
|
||||
--web-push-email-address <email> \
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { NavigationRoute, registerRoute } from "workbox-routing";
|
|||
import { NetworkFirst } from "workbox-strategies";
|
||||
import { clientsClaim } from "workbox-core";
|
||||
import { dbAsync } from "../src/app/db";
|
||||
import session from "../src/app/Session";
|
||||
import { ACTION_HTTP, ACTION_VIEW } from "../src/app/actions";
|
||||
import { badge, icon, messageWithSequenceId, notificationTag, toNotificationParams } from "../src/app/notificationUtils";
|
||||
import initI18n from "../src/app/i18n";
|
||||
|
|
@ -35,6 +36,7 @@ const broadcastChannel = new BroadcastChannel("web-push-broadcast");
|
|||
*/
|
||||
const handlePushMessage = async (data) => {
|
||||
const { subscription_id: subscriptionId, message } = data;
|
||||
|
||||
const db = await dbAsync();
|
||||
|
||||
console.log("[ServiceWorker] Message received", data);
|
||||
|
|
@ -43,9 +45,24 @@ const handlePushMessage = async (data) => {
|
|||
const subscription = await db.subscriptions.get(subscriptionId);
|
||||
if (!subscription) {
|
||||
console.log("[ServiceWorker] Subscription not found", subscriptionId);
|
||||
handlePushUnknown(data);
|
||||
return;
|
||||
}
|
||||
|
||||
// NOTE: As soon as possible, to avoid this Safari error:
|
||||
// > Push event handling completed without showing any notification via
|
||||
// > ServiceWorkerRegistration.showNotification(). This may trigger removal of
|
||||
// > the push subscription.
|
||||
await self.registration.showNotification(
|
||||
...toNotificationParams({
|
||||
message,
|
||||
defaultTitle: message.topic,
|
||||
topicRoute: new URL(message.topic, self.location.origin).toString(),
|
||||
baseUrl: subscription.baseUrl,
|
||||
topic: subscription.topic,
|
||||
})
|
||||
);
|
||||
|
||||
// Delete existing notification with same sequence ID (if any)
|
||||
const sequenceId = message.sequence_id || message.id;
|
||||
if (sequenceId) {
|
||||
|
|
@ -71,17 +88,71 @@ const handlePushMessage = async (data) => {
|
|||
// Broadcast the message to potentially play a sound
|
||||
broadcastChannel.postMessage(message);
|
||||
|
||||
await self.registration.showNotification(
|
||||
...toNotificationParams({
|
||||
message,
|
||||
defaultTitle: message.topic,
|
||||
topicRoute: new URL(message.topic, self.location.origin).toString(),
|
||||
baseUrl: subscription.baseUrl,
|
||||
topic: subscription.topic,
|
||||
})
|
||||
);
|
||||
await extendToken();
|
||||
};
|
||||
|
||||
const refreshThreshold = 1000 * 60 * 60; // 1 hour
|
||||
const extendToken = async () => {
|
||||
if (import.meta.env.DEV) {
|
||||
console.warn("[ServiceWorker] Skipping token extension in development since no config.base_url exists");
|
||||
return;
|
||||
}
|
||||
|
||||
const token = await session.tokenAsync();
|
||||
if (!token) {
|
||||
console.debug("[ServiceWorker] No session token, skipping token extension");
|
||||
return;
|
||||
}
|
||||
|
||||
const lastExtendedAt = await session.lastExtendedAtAsync();
|
||||
const now = Date.now();
|
||||
|
||||
if (lastExtendedAt && now - lastExtendedAt < refreshThreshold) {
|
||||
console.debug(`[ServiceWorker] Token extended ${Math.floor((now - lastExtendedAt) / 1000 / 60)} minutes ago, skipping`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("[ServiceWorker] Extending user access token");
|
||||
|
||||
// duplicated from utils.js#accountTokenUrl since we can't import that here
|
||||
// as long as there's mp3 and other incompatible imports there
|
||||
const tokenUrl = `${config.base_url}/v1/account/token`;
|
||||
|
||||
try {
|
||||
const response = await fetch(tokenUrl, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
await session.setLastExtendedAtAsync();
|
||||
console.log(`[ServiceWorker] Token extended successfully`);
|
||||
} else {
|
||||
console.error(`[ServiceWorker] Failed to extend token: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[ServiceWorker] Failed to extend token", e);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Registers a periodic-sync listener for `extend-token` (see hooks.js).
|
||||
* This extends the token regardless of whether the browser is open.
|
||||
*
|
||||
* CAVEATS:
|
||||
* - Chromium-only
|
||||
* - Only when the PWA is _installed_ (not just running in a browser tab)
|
||||
* - Only when notifications are granted
|
||||
*/
|
||||
self.addEventListener("periodicsync", (event) => {
|
||||
if (event.tag === "extend-token") {
|
||||
console.log('[ServiceWorker] Received periodicsync event "extend-token"');
|
||||
event.waitUntil(extendToken());
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Handle a message_delete event: delete the notification from the database.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -155,6 +155,7 @@ class AccountApi {
|
|||
method: "PATCH",
|
||||
headers: withBearerAuth({}, session.token()),
|
||||
});
|
||||
await session.setLastExtendedAtAsync();
|
||||
}
|
||||
|
||||
async deleteToken(token) {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ class Session {
|
|||
await this.db.kv.bulkPut([
|
||||
{ key: "user", value: username },
|
||||
{ key: "token", value: token },
|
||||
{ key: "lastExtendedAt", value: Date.now() },
|
||||
]);
|
||||
localStorage.setItem("user", username);
|
||||
localStorage.setItem("token", token);
|
||||
|
|
@ -52,6 +53,18 @@ class Session {
|
|||
return (await this.db.kv.get({ key: "user" }))?.value;
|
||||
}
|
||||
|
||||
async tokenAsync() {
|
||||
return (await this.db.kv.get({ key: "token" }))?.value;
|
||||
}
|
||||
|
||||
async lastExtendedAtAsync() {
|
||||
return (await this.db.kv.get({ key: "lastExtendedAt" }))?.value;
|
||||
}
|
||||
|
||||
async setLastExtendedAtAsync() {
|
||||
await this.db.kv.put({ key: "lastExtendedAt", value: Date.now() });
|
||||
}
|
||||
|
||||
exists() {
|
||||
return this.username() && this.token();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -283,6 +283,49 @@ export const useStandaloneWebPushAutoSubscribe = () => {
|
|||
}, [isLaunchedPWA]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Registers a periodicsync listener for `extend-token` (see sw.js).
|
||||
* This extends the token regardless of whether the browser is open.
|
||||
*
|
||||
* CAVEATS:
|
||||
* - Chromium-only
|
||||
* - Only when the PWA is _installed_ (not just running in a browser tab)
|
||||
* - Only when notifications are granted
|
||||
*/
|
||||
const usePeriodicTokenExtend = () => {
|
||||
const isLaunchedPWA = useIsLaunchedPWA();
|
||||
const pushPossible = useNotificationPermissionListener(() => notifier.pushPossible());
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (!isLaunchedPWA) {
|
||||
console.debug("[usePeriodicTokenExtend] Skipping: Not running as PWA");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!pushPossible) {
|
||||
console.debug("[usePeriodicTokenExtend] Skipping: Web push not possible or granted");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const registration = await navigator.serviceWorker.ready;
|
||||
if (!registration.periodicSync) {
|
||||
console.debug("[usePeriodicTokenExtend] Skipping: Periodic Sync not supported");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[usePeriodicTokenExtend] Turning on periodicsync "extend-token"`);
|
||||
await registration.periodicSync.register("extend-token", {
|
||||
minInterval: 24 * 60 * 60 * 1000, // 24 hours
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("[usePeriodicTokenExtend] Periodic Sync could not be registered", error);
|
||||
}
|
||||
})();
|
||||
}, [isLaunchedPWA, pushPossible]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Start the poller and the pruner. This is done in a side effect as opposed to just in Pruner.js
|
||||
* and Poller.js, because side effect imports are not a thing in JS, and "Optimize imports" cleans
|
||||
|
|
@ -305,6 +348,7 @@ const stopWorkers = () => {
|
|||
|
||||
export const useBackgroundProcesses = () => {
|
||||
useStandaloneWebPushAutoSubscribe();
|
||||
usePeriodicTokenExtend();
|
||||
|
||||
useEffect(() => {
|
||||
console.log("[useBackgroundProcesses] mounting");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue