import type { Object } from 'wildebeest/backend/src/activitypub/objects' import type { JWK } from 'wildebeest/backend/src/webpush/jwk' import * as actors from 'wildebeest/backend/src/activitypub/actors' import { urlToHandle } from 'wildebeest/backend/src/utils/handle' import { loadExternalMastodonAccount } from 'wildebeest/backend/src/mastodon/account' import { generateWebPushMessage } from 'wildebeest/backend/src/webpush' import { getPersonById } from 'wildebeest/backend/src/activitypub/actors' import type { WebPushInfos, WebPushMessage } from 'wildebeest/backend/src/webpush/webpushinfos' import { WebPushResult } from 'wildebeest/backend/src/webpush/webpushinfos' import type { Actor } from 'wildebeest/backend/src/activitypub/actors' import type { NotificationType, Notification } from 'wildebeest/backend/src/types/notification' import type { Subscription } from 'wildebeest/backend/src/mastodon/subscription' import { getSubscriptionForAllClients } from 'wildebeest/backend/src/mastodon/subscription' import { getVAPIDKeys } from 'wildebeest/backend/src/mastodon/subscription' import * as config from 'wildebeest/backend/src/config' export async function createNotification( db: D1Database, type: NotificationType, actor: Actor, fromActor: Actor, obj: Object ): Promise { const query = ` INSERT INTO actor_notifications (type, actor_id, from_actor_id, object_id) VALUES (?, ?, ?, ?) RETURNING id ` const row: any = await db .prepare(query) .bind(type, actor.id.toString(), fromActor.id.toString(), obj.id.toString()) .first() return row.id } export async function insertFollowNotification(db: D1Database, actor: Actor, fromActor: Actor): Promise { const type: NotificationType = 'follow' const query = ` INSERT INTO actor_notifications (type, actor_id, from_actor_id) VALUES (?, ?, ?) RETURNING id ` const row: any = await db.prepare(query).bind(type, actor.id.toString(), fromActor.id.toString()).first() return row.id } export async function sendFollowNotification(db: D1Database, follower: Actor, actor: Actor, notificationId: string) { const sub = await config.get(db, 'email') const data = { preferred_locale: 'en', notification_type: 'follow', notification_id: notificationId, icon: follower.icon!.url, title: 'New follower', body: `${follower.name} is now following you`, } const message: WebPushMessage = { data: JSON.stringify(data), urgency: 'normal', sub, ttl: 60 * 24 * 7, } return sendNotification(db, actor, message) } export async function sendLikeNotification(db: D1Database, fromActor: Actor, actor: Actor, notificationId: string) { const sub = await config.get(db, 'email') const data = { preferred_locale: 'en', notification_type: 'favourite', notification_id: notificationId, icon: fromActor.icon!.url, title: 'New favourite', body: `${fromActor.name} favourited your status`, } const message: WebPushMessage = { data: JSON.stringify(data), urgency: 'normal', sub, ttl: 60 * 24 * 7, } return sendNotification(db, actor, message) } export async function sendMentionNotification(db: D1Database, fromActor: Actor, actor: Actor, notificationId: string) { const sub = await config.get(db, 'email') const data = { preferred_locale: 'en', notification_type: 'favourite', notification_id: notificationId, icon: fromActor.icon!.url, title: 'New favourite', body: `${fromActor.name} favourited your status`, } const message: WebPushMessage = { data: JSON.stringify(data), urgency: 'normal', sub, ttl: 60 * 24 * 7, } return sendNotification(db, actor, message) } export async function sendReblogNotification(db: D1Database, fromActor: Actor, actor: Actor, notificationId: string) { const sub = await config.get(db, 'email') const data = { preferred_locale: 'en', notification_type: 'reblog', notification_id: notificationId, icon: fromActor.icon!.url, title: 'New boost', body: `${fromActor.name} boosted your status`, } const message: WebPushMessage = { data: JSON.stringify(data), urgency: 'normal', sub, ttl: 60 * 24 * 7, } return sendNotification(db, actor, message) } async function sendNotification(db: D1Database, actor: Actor, message: WebPushMessage) { const vapidKeys = await getVAPIDKeys(db) const subscriptions = await getSubscriptionForAllClients(db, actor) const promises = subscriptions.map(async (subscription) => { const device: WebPushInfos = { endpoint: subscription.gateway.endpoint, key: subscription.gateway.keys.p256dh, auth: subscription.gateway.keys.auth, } const result = await generateWebPushMessage(message, device, vapidKeys) if (result !== WebPushResult.Success) { throw new Error('failed to send push notification') } }) await Promise.allSettled(promises) } export async function getNotifications(db: D1Database, actor: Actor): Promise> { const query = ` SELECT objects.*, actor_notifications.type, actor_notifications.actor_id, actor_notifications.from_actor_id as notif_from_actor_id, actor_notifications.cdate as notif_cdate, actor_notifications.id as notif_id FROM actor_notifications LEFT JOIN objects ON objects.id=actor_notifications.object_id WHERE actor_id=? ORDER BY actor_notifications.cdate DESC LIMIT 20 ` const stmt = db.prepare(query).bind(actor.id.toString()) const { results, success, error } = await stmt.all() if (!success) { throw new Error('SQL error: ' + error) } const out: Array = [] if (!results || results.length === 0) { return [] } for (let i = 0, len = results.length; i < len; i++) { const result = results[i] as any const properties = JSON.parse(result.properties) const notifFromActorId = new URL(result.notif_from_actor_id) const notifFromActor = await getPersonById(db, notifFromActorId) if (!notifFromActor) { console.warn('unknown actor') continue } const acct = urlToHandle(notifFromActorId) const notifFromAccount = await loadExternalMastodonAccount(acct, notifFromActor) const notif: Notification = { id: result.notif_id.toString(), type: result.type, created_at: new Date(result.notif_cdate).toISOString(), account: notifFromAccount, } if (result.type === 'mention' || result.type === 'favourite') { const actorId = new URL(result.original_actor_id) const actor = await actors.getAndCache(actorId, db) const acct = urlToHandle(actorId) const account = await loadExternalMastodonAccount(acct, actor) notif.status = { id: result.mastodon_id, content: properties.content, uri: result.id, created_at: new Date(result.cdate).toISOString(), emojis: [], media_attachments: [], tags: [], mentions: [], account, // TODO: stub values visibility: 'public', spoiler_text: '', } } out.push(notif) } return out } export async function pregenerateNotifications(db: D1Database, cache: KVNamespace, actor: Actor) { const notifications = await getNotifications(db, actor) await cache.put(actor.id + '/notifications', JSON.stringify(notifications)) }