import "server-only"; import { and, eq, inArray, ne, sql } from "drizzle-orm"; import webPush from "web-push"; import { getDb } from "@/db"; import { comments, commentPushSubscriptions, commentRevisions, users } from "@/db/schema"; import { pushPublicKey, validPushEndpoint } from "@/lib/commission/push"; import { createNotifications } from "@/lib/notifications/repository"; import { authorizeComment } from "./repository"; export async function sendCommentPush(id: string, version?: number) { const [reply] = await getDb().select().from(comments).where(eq(comments.id, id)).limit(1); if (!reply || reply.hidden || reply.deletedAt) return; const notificationVersion = version ?? reply.version; const edited = notificationVersion > 1; let recipients: { id: string; admin: boolean }[]; let url: string; let title: string; if (reply.replyToId) { const [parent] = await getDb().select().from(comments).where(eq(comments.id, reply.replyToId)).limit(1); if (!parent || parent.authorId === reply.authorId || parent.hidden || parent.deletedAt) return; const [recipient] = await getDb().select().from(users).where(eq(users.id, parent.authorId)).limit(1); if (!recipient || recipient.banned) return; let destination; try { ({ destination } = await authorizeComment(id, { id: recipient.id, admin: recipient.role === "admin" && recipient.emailVerified })); } catch { return; } recipients = [{ id: recipient.id, admin: false }]; url = `${destination.href}${destination.href.includes("?") ? "&" : "?"}reply=${id}#comment-${reply.rootId}`; title = edited ? "แก้ไขคำตอบกลับความคิดเห็นของคุณ" : "ตอบกลับความคิดเห็นของคุณ"; } else { recipients = (await getDb().select({ id: users.id }).from(users) .where(and(eq(users.role, "admin"), eq(users.emailVerified, true), ne(users.id, reply.authorId), sql`${users.banned} is not true`))) .map((user) => ({ ...user, admin: true })); if (!recipients.length) return; let destination; try { ({ destination } = await authorizeComment(id, { id: recipients[0].id, admin: true })); } catch { return; } if (!destination.writable) return; url = `/admin/comments?target=${encodeURIComponent(destination.target)}&comment=${id}`; title = edited ? `แก้ไขความคิดเห็นใน ${destination.name}` : `แสดงความคิดเห็นใหม่ใน ${destination.name}`; } const [author] = await getDb().select({ name: users.name }).from(users).where(eq(users.id, reply.authorId)).limit(1); const [revision] = await getDb().select({ text: commentRevisions.text }).from(commentRevisions) .where(and(eq(commentRevisions.commentId, id), eq(commentRevisions.version, notificationVersion))).limit(1); if (!revision) return; const notificationTitle = `${author?.name ?? "ผู้ใช้"} ${title}`; const body = revision?.text.slice(0, 300) || "ส่งรูปภาพ"; await createNotifications(recipients.map(recipient => ({ userId: recipient.id, adminOnly: recipient.admin, url })), { eventKey: edited ? `comment:${id}:edit:${notificationVersion}` : `comment:${id}`, kind: edited ? "comment_edit" : reply.replyToId ? "comment_reply" : "comment_new", title: notificationTitle, body, commentId: id, }); if (!pushPublicKey()) return; const subscriptions = await getDb().select().from(commentPushSubscriptions).where(inArray(commentPushSubscriptions.userId, recipients.map(recipient => recipient.id))); if (!subscriptions.length) return; webPush.setVapidDetails(process.env.WEB_PUSH_SUBJECT!, process.env.WEB_PUSH_PUBLIC_KEY!, process.env.WEB_PUSH_PRIVATE_KEY!); const payload = JSON.stringify({ id: `comment-${id}`, title: notificationTitle, body, icon: "/icon/nav/Comment.webp", url }); for (let offset = 0; offset < subscriptions.length; offset += 10) { await Promise.all(subscriptions.slice(offset, offset + 10).map(async (subscription) => { if (!validPushEndpoint(subscription.endpoint)) return; try { await webPush.sendNotification({ endpoint: subscription.endpoint, keys: { p256dh: subscription.p256dh, auth: subscription.auth } }, payload, { TTL: 60 * 60 * 24, timeout: 5000 }); } catch (cause) { if (cause && typeof cause === "object" && "statusCode" in cause && (cause.statusCode === 404 || cause.statusCode === 410)) await getDb().delete(commentPushSubscriptions).where(eq(commentPushSubscriptions.endpoint, subscription.endpoint)); } })); } }