import type { Notification, NotificationData, NotificationType, } from "../model/interfaces.ts"; import { APIErrorCode, APIException } from "../model/interfaces.ts"; import { db, isNotificationRow, notificationRowToApi } from "../model/db.ts"; import { sendToUser } from "./ws-service.ts"; // Regex: matches @username not already inside a markdown link ([...] or (...) const MENTION_RE = /(? 0) { sendToUser(userId, { type: "notification_created", notification: { id, userId, type, data, read: false, createdAt }, }); } } export function getNotificationsForUser( userId: string, page: number, limit: number, ): { items: Notification[]; total: number } { const offset = (page - 1) * limit; const rawRows = db.prepare( `SELECT * FROM notifications WHERE user_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?;`, ).all(userId, limit, offset) as Parameters[0][]; const totalRow = db.prepare( `SELECT COUNT(*) as count FROM notifications WHERE user_id = ?;`, ).get(userId) as { count: number } | undefined; if (!rawRows.every(isNotificationRow)) { throw new APIException( APIErrorCode.SERVER_ERROR, 500, "Malformed notification data", ); } return { items: rawRows.map(notificationRowToApi), total: totalRow?.count ?? 0, }; } export function getUnreadCount(userId: string): number { const row = db.prepare( `SELECT COUNT(*) as count FROM notifications WHERE user_id = ? AND read = 0;`, ).get(userId) as { count: number } | undefined; return row?.count ?? 0; } export function markAllRead(userId: string): void { db.prepare(`UPDATE notifications SET read = 1 WHERE user_id = ?;`).run( userId, ); } export function markOneRead(notificationId: string, userId: string): void { db.prepare( `UPDATE notifications SET read = 1 WHERE id = ? AND user_id = ?;`, ).run(notificationId, userId); } // ── Trigger helpers ─────────────────────────────────────────────────────────── export function notifyUserNewFollower( followerId: string, followedUserId: string, ): void { const followerRow = db.prepare( `SELECT username FROM users WHERE id = ?;`, ).get(followerId) as { username: string } | undefined; if (!followerRow) return; createNotification( followedUserId, "user_followed", { followerId, followerUsername: followerRow.username }, `user-followed:${followedUserId}:${followerId}`, ); } export function notifyPlaylistOwnerNewFollower( followerId: string, playlistId: string, ): void { const followerRow = db.prepare( `SELECT username FROM users WHERE id = ?;`, ).get(followerId) as { username: string } | undefined; const playlistRow = db.prepare( `SELECT title, user_id FROM playlists WHERE id = ?;`, ).get(playlistId) as { title: string; user_id: string } | undefined; if (!followerRow || !playlistRow) return; if (followerId === playlistRow.user_id) return; createNotification( playlistRow.user_id, "playlist_followed", { followerId, followerUsername: followerRow.username, playlistId, playlistTitle: playlistRow.title, }, `followed:${playlistId}:${followerId}`, ); } export function notifyUserFollowersNewDump( dumperId: string, dumpId: string, dumpTitle: string, ): void { const posterRow = db.prepare( `SELECT username FROM users WHERE id = ?;`, ).get(dumperId) as { username: string } | undefined; if (!posterRow) return; const followerRows = db.prepare( `SELECT follower_id FROM follows WHERE followed_user_id = ?;`, ).all(dumperId) as { follower_id: string }[]; for (const row of followerRows) { createNotification( row.follower_id, "user_dump_posted", { dumperId, dumperUsername: posterRow.username, dumpId, dumpTitle }, `dump:${dumpId}`, ); } } export function notifyDumpOwnerUpvote( voterId: string, dumpId: string, ): void { const voterRow = db.prepare( `SELECT username FROM users WHERE id = ?;`, ).get(voterId) as { username: string } | undefined; const dumpRow = db.prepare( `SELECT title, user_id FROM dumps WHERE id = ?;`, ).get(dumpId) as { title: string; user_id: string } | undefined; if (!voterRow || !dumpRow) return; if (voterId === dumpRow.user_id) return; // no self-notification createNotification( dumpRow.user_id, "dump_upvoted", { voterId, voterUsername: voterRow.username, dumpId, dumpTitle: dumpRow.title, }, `upvote:${dumpId}:${voterId}`, ); } export function notifyMentions( mentionerUserId: string, body: string, contextType: "comment" | "dump" | "playlist", contextId: string, contextTitle: string, dumpId?: string, ): void { const mentionerRow = db.prepare( `SELECT username FROM users WHERE id = ?;`, ).get(mentionerUserId) as { username: string } | undefined; if (!mentionerRow) return; const usernames = [ ...new Set( [...body.matchAll(MENTION_RE)].map((m) => m[1].toLowerCase()), ), ]; for (const username of usernames) { const mentionedRow = db.prepare( `SELECT id FROM users WHERE lower(username) = ?;`, ).get(username) as { id: string } | undefined; if (!mentionedRow || mentionedRow.id === mentionerUserId) continue; createNotification( mentionedRow.id, "user_mentioned", { mentionerId: mentionerUserId, mentionerUsername: mentionerRow.username, contextType, contextId, contextTitle, dumpId, }, `mention:${contextType}:${contextId}:${mentionedRow.id}`, ); } } export function notifyPlaylistFollowersNewDump( playlistId: string, playlistTitle: string, dumpId: string, dumpTitle: string, ): void { const followerRows = db.prepare( `SELECT follower_id FROM follows WHERE followed_playlist_id = ?;`, ).all(playlistId) as { follower_id: string }[]; for (const row of followerRows) { createNotification( row.follower_id, "playlist_dump_added", { dumpId, dumpTitle, playlistId, playlistTitle }, `pdump:${playlistId}:${dumpId}`, ); } }