wildebeest/backend/src/mastodon/notification.ts

258 wiersze
7.1 KiB
TypeScript

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 { getSubscriptionForAllClients } from 'wildebeest/backend/src/mastodon/subscription'
import type { Cache } from 'wildebeest/backend/src/cache'
export async function createNotification(
db: D1Database,
type: NotificationType,
actor: Actor,
fromActor: Actor,
obj: Object
): Promise<string> {
const query = `
INSERT INTO actor_notifications (type, actor_id, from_actor_id, object_id)
VALUES (?, ?, ?, ?)
RETURNING id
`
const row: { id: string } = 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<string> {
const type: NotificationType = 'follow'
const query = `
INSERT INTO actor_notifications (type, actor_id, from_actor_id)
VALUES (?, ?, ?)
RETURNING id
`
const row: { id: string } = 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,
adminEmail: string,
vapidKeys: JWK
) {
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: adminEmail,
ttl: 60 * 24 * 7,
}
return sendNotification(db, actor, message, vapidKeys)
}
export async function sendLikeNotification(
db: D1Database,
fromActor: Actor,
actor: Actor,
notificationId: string,
adminEmail: string,
vapidKeys: JWK
) {
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: adminEmail,
ttl: 60 * 24 * 7,
}
return sendNotification(db, actor, message, vapidKeys)
}
export async function sendMentionNotification(
db: D1Database,
fromActor: Actor,
actor: Actor,
notificationId: string,
adminEmail: string,
vapidKeys: JWK
) {
const data = {
preferred_locale: 'en',
notification_type: 'mention',
notification_id: notificationId,
icon: fromActor.icon!.url,
title: 'New mention',
body: `You were mentioned by ${fromActor.name}`,
}
const message: WebPushMessage = {
data: JSON.stringify(data),
urgency: 'normal',
sub: adminEmail,
ttl: 60 * 24 * 7,
}
return sendNotification(db, actor, message, vapidKeys)
}
export async function sendReblogNotification(
db: D1Database,
fromActor: Actor,
actor: Actor,
notificationId: string,
adminEmail: string,
vapidKeys: JWK
) {
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: adminEmail,
ttl: 60 * 24 * 7,
}
return sendNotification(db, actor, message, vapidKeys)
}
async function sendNotification(db: D1Database, actor: Actor, message: WebPushMessage, vapidKeys: JWK) {
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, domain: string): Promise<Array<Notification>> {
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<Notification> = []
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,
url: new URL('/statuses/' + result.mastodon_id, 'https://' + domain),
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: Cache, actor: Actor, domain: string) {
const notifications = await getNotifications(db, actor, domain)
await cache.put(actor.id + '/notifications', notifications)
}