import type { NotificationProvider } from "@rakazo/adapter-kit"; import { NUDGES_PER_DAY } from "@rakazo/contracts"; import type { PrismaClient } from "@rakazo/db"; import { claimDailyNudge, countPushSubscriptions, ensureMainConversation, isPersonalMainBot, loadNotificationPolicy, } from "@rakazo/db"; import { z } from "zod"; export const NOTIFY_USER_BODY_MAX_LENGTH = 200; export const NOTIFY_USER_TITLE_MAX_LENGTH = 60; const NotifyUserArgs = z.object({ body: z.string().trim().min(1).max(NOTIFY_USER_BODY_MAX_LENGTH), title: z.string().trim().max(NOTIFY_USER_TITLE_MAX_LENGTH).optional(), }); export interface NotifyUserToolContext { userId: string; spaceId: string; runId: string; bot: { id: string; name: string; kind: string }; signal: AbortSignal; } export type NotifyUserResult = { error: string } | { sent: true } | { sent: false; reason: string }; /** * A nudge from the main bot: a push that opens the main conversation. The user's nudge switch, * quiet hours and the daily cap all apply; a held-back nudge is reported, not an error. */ export async function notifyUserFromTool( deps: { prisma: PrismaClient; notifications?: NotificationProvider }, context: NotifyUserToolContext, rawArgs: Record, ): Promise { if ( !(await isPersonalMainBot(deps.prisma, { kind: context.bot.kind, spaceId: context.spaceId })) ) { return { error: "Only the user's main bot can notify the user." }; } const parsed = NotifyUserArgs.safeParse(rawArgs); if (!parsed.success) { return { error: `Invalid notification: body is required, up to ${NOTIFY_USER_BODY_MAX_LENGTH} characters; title up to ${NOTIFY_USER_TITLE_MAX_LENGTH}.`, }; } const { prisma, notifications } = deps; if (!notifications) { return { sent: false, reason: "Push notifications are not available on this server." }; } const policy = await loadNotificationPolicy(prisma, context.userId); if (!policy.nudges) return { sent: false, reason: "The user turned nudges off." }; if (policy.quiet) return { sent: false, reason: "It is the user's quiet hours." }; if ((await countPushSubscriptions(prisma, context.userId)) === 0) { return { sent: false, reason: "The user has no device with notifications turned on." }; } if (!(await claimDailyNudge(prisma, context.userId, policy.localDate))) { return { sent: false, reason: `The daily limit of ${NUDGES_PER_DAY} nudges is reached.` }; } const conversation = await ensureMainConversation(prisma, context.userId); try { await notifications.send( { kind: "nudge", title: parsed.data.title || context.bot.name, body: parsed.data.body, botId: context.bot.id, threadId: conversation.threadId, }, { operationId: `notify_user:${context.runId}`, traceId: context.runId, spaceId: context.spaceId, userId: context.userId, botId: context.bot.id, runId: context.runId, signal: context.signal, }, ); } catch { return { sent: false, reason: "No device accepted the notification." }; } return { sent: true }; }