import webpush from "web-push"; import { CouchDBChangesStream } from "@hereconnect/couchdb-changes-stream"; import { qrDb, messagesDbUrl, webPushSubscriptionsDb } from "./dbs"; import { setupVapidDetails } from "./setupVapidDetails"; import type { MessageDocument } from "@hereconnect/types"; import { ServiceSeqTracker } from "./serviceSeqTracker"; import { startHealthServer } from "./health-server"; const abortController = new AbortController(); startHealthServer(abortController); function limitStringLengthWithEllipsis(str: string, maxLength: number): string { return str.length > maxLength ? str.substring(0, maxLength) + "…" : str; } const start = async () => { await setupVapidDetails(); const serviceSeqTracker = await ServiceSeqTracker.Init( "_local/service:message-delivery-method-web-push", ); const changesStream = new CouchDBChangesStream( messagesDbUrl, { abortController, since: serviceSeqTracker.lastSeq, feed: "continuous", include_docs: true, heartbeat: 1000, filter: "_selector", selector: { type: "message", }, }, ); // Gracefully handle SIGINT process.on("SIGINT", async () => { changesStream?.stop(); process.stdout.write("\n"); process.exit(2); }); console.info("Service started from sequence", serviceSeqTracker.lastSeq); for await (const change of changesStream) { const { seq, doc: msgDoc } = change; if (!msgDoc) { console.error("Change missing document:", change); continue; } serviceSeqTracker.checkNextSeq(seq); try { if (msgDoc.from === "guest") { // send message to owner const qrDoc = await qrDb.get(`qr_code:${msgDoc.qr_code_uri}`); if (qrDoc.messageDeliveryMethod !== "webPush") { console.log(`Skip !webPush`); continue; } const { rows } = await webPushSubscriptionsDb.allDocs({ startkey: `web_push_subscription:${qrDoc.user_uuid}:`, endkey: `web_push_subscription:${qrDoc.user_uuid}:\uffff`, include_docs: true, }); if (rows.length === 0) { console.log("NO SUBSCRIPTIONS"); } else { console.log(`sending to ${rows.length} devices`); } for (const row of rows) { try { const subscription = row.doc!.subscription; const topic = `chat-${msgDoc.qr_code_uri}-${msgDoc.chat}`; const timestamp = new Date(msgDoc.created_at).getTime(); const tag = `${topic}-${(timestamp - new Date(qrDoc.created_at).getTime()).toString(36)}`; const title = limitStringLengthWithEllipsis( `${qrDoc.name}: новое сообщение`, 53, ); const body = limitStringLengthWithEllipsis(msgDoc.body, 470); const options: NotificationOptions & { timestamp: number } = { icon: "/images/qr-code-logo-200x200.jpg", body, tag, requireInteraction: true, timestamp, data: { url: `/manage/${msgDoc.qr_code_uri}/chat/${msgDoc.chat}`, payload: { tag }, }, } as const; await webpush.sendNotification( subscription, JSON.stringify({ type: "notification", notification: { title, options, }, }), { urgency: "high", topic, }, ); } catch (error) { console.error( `Error sending notification to ${row.id} - ${JSON.stringify(error)}`, ); } } } else if (msgDoc.from === "owner") { console.log("FROM OWNER"); } else { continue; } console.log( `Processed change: ${JSON.stringify({ _id: change.id, ref: msgDoc._rev, seq: change.seq })}`, ); } catch (error) { console.error( `Failed processing: ${JSON.stringify(error)} change: ${JSON.stringify({ id: msgDoc._id, rev: msgDoc._rev, seq: seq })}`, ); } finally { await serviceSeqTracker.saveSeq(seq); } } }; start().catch((error) => { console.error("error", error); process.exit(1); });