-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
12 changed files
with
309 additions
and
42 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
self.addEventListener('push', function (event) { | ||
let data = {}; | ||
if (event.data) { | ||
data = event.data.json(); | ||
} | ||
|
||
const title = data.title ?? 'SkyChat Notification'; | ||
const options = { | ||
body: data.body ?? 'New notification from SkyChat', | ||
icon: '/favicon.png', | ||
}; | ||
|
||
event.waitUntil(self.registration.showNotification(title, options)); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
export class WebPush { | ||
static SERVICE_WORKER_URL = 'service-worker.js'; | ||
|
||
static urlBase64ToUint8Array(base64String) { | ||
const padding = '='.repeat((4 - (base64String.length % 4)) % 4); | ||
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/'); | ||
const rawData = window.atob(base64); | ||
const outputArray = new Uint8Array(rawData.length); | ||
for (let i = 0; i < rawData.length; ++i) { | ||
outputArray[i] = rawData.charCodeAt(i); | ||
} | ||
return outputArray; | ||
} | ||
|
||
static async register(vapidPublicKey) { | ||
if (!('serviceWorker' in navigator) || !('PushManager' in window)) { | ||
throw new Error('WebPush is not supported'); | ||
} | ||
|
||
const registration = await navigator.serviceWorker.register(WebPush.SERVICE_WORKER_URL); | ||
const permission = await Notification.requestPermission(); | ||
|
||
if (permission !== 'granted') { | ||
throw new Error('Permission denied'); | ||
} | ||
|
||
const convertedVapidKey = WebPush.urlBase64ToUint8Array(vapidPublicKey); | ||
|
||
return registration.pushManager.subscribe({ | ||
userVisibleOnly: true, | ||
applicationServerKey: convertedVapidKey, | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
import { Connection } from '../../../skychat/Connection.js'; | ||
import { Logging } from '../../../skychat/Logging.js'; | ||
import { RoomManager } from '../../../skychat/RoomManager.js'; | ||
import { User } from '../../../skychat/User.js'; | ||
import { UserController } from '../../../skychat/UserController.js'; | ||
import { GlobalPlugin } from '../../GlobalPlugin.js'; | ||
|
||
import webpush from 'web-push'; | ||
|
||
export class WebPushPlugin extends GlobalPlugin { | ||
static readonly commandName = 'push'; | ||
|
||
static readonly commandAliases = []; | ||
|
||
static readonly ENDPOINTS_WHITELIST = ['fcm.googleapis.com', 'updates.push.services.mozilla.com', 'windows.com']; | ||
|
||
readonly minRight = 0; | ||
|
||
readonly rules = { | ||
push: { | ||
minCount: 1, | ||
coolDown: 2000, | ||
}, | ||
}; | ||
|
||
constructor(manager: RoomManager) { | ||
super(manager); | ||
|
||
console.log('VAPID_PUBLIC_KEY', process.env.VAPID_PUBLIC_KEY as string); | ||
console.log('VAPID_PRIVATE_KEY', process.env.VAPID_PRIVATE_KEY as string); | ||
webpush.setVapidDetails( | ||
`https://github.com/skychatorg/skychat`, | ||
process.env.VAPID_PUBLIC_KEY as string, | ||
process.env.VAPID_PRIVATE_KEY as string, | ||
); | ||
} | ||
|
||
async run(alias: string, param: string, connection: Connection): Promise<void> { | ||
const data = JSON.parse(param); | ||
if (!data || typeof data !== 'object') { | ||
throw new Error('Invalid data'); | ||
} | ||
|
||
if (typeof data.endpoint !== 'string') { | ||
throw new Error('Invalid endpoint'); | ||
} | ||
const url = new URL(data.endpoint); | ||
if (!url || !url.hostname || !WebPushPlugin.ENDPOINTS_WHITELIST.some((allowed) => url.hostname.endsWith(allowed))) { | ||
throw new Error(`Endpoint ${url.hostname} not allowed for push notifications`); | ||
} | ||
if (typeof data.expirationTime !== 'number' && data.expirationTime !== null) { | ||
throw new Error('Invalid expirationTime'); | ||
} | ||
if (typeof data.keys !== 'object') { | ||
throw new Error('Invalid keys'); | ||
} | ||
if (typeof data.keys.auth !== 'string') { | ||
throw new Error('Invalid keys.auth'); | ||
} | ||
if (typeof data.keys.p256dh !== 'string') { | ||
throw new Error('Invalid keys.p256dh'); | ||
} | ||
|
||
const user = connection.session.user; | ||
user.storage[WebPushPlugin.commandName] = { | ||
endpoint: data.endpoint, | ||
expirationTime: data.expirationTime, | ||
keys: { | ||
auth: data.keys.auth, | ||
p256dh: data.keys.p256dh, | ||
}, | ||
}; | ||
UserController.sync(user); | ||
} | ||
|
||
send(user: User, data: { title: string; body: string; tag: string }): void { | ||
const subscription = user.storage[WebPushPlugin.commandName]; | ||
if (!subscription) { | ||
return; | ||
} | ||
|
||
// We do not `await` not to block the main logic | ||
Logging.info(`Sending push notification to ${user.username}`); | ||
webpush | ||
.sendNotification(subscription, JSON.stringify(data)) | ||
.then(() => { | ||
Logging.info(`Push notification sent to ${user.username}`); | ||
}) | ||
.catch(async (error) => { | ||
// We delete the subscription if it is not valid anymore (we assume it is not if we can't send a notification once) | ||
Logging.error(`Error sending push notification to ${user.username}`, error); | ||
user.storage[WebPushPlugin.commandName] = null; | ||
await UserController.sync(user); | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.