| import { TRPCError } from "@trpc/server"; |
| import { ENV } from "./env"; |
|
|
| export type NotificationPayload = { |
| title: string; |
| content: string; |
| }; |
|
|
| const TITLE_MAX_LENGTH = 1200; |
| const CONTENT_MAX_LENGTH = 20000; |
|
|
| const trimValue = (value: string): string => value.trim(); |
| const isNonEmptyString = (value: unknown): value is string => |
| typeof value === "string" && value.trim().length > 0; |
|
|
| const buildEndpointUrl = (baseUrl: string): string => { |
| const normalizedBase = baseUrl.endsWith("/") |
| ? baseUrl |
| : `${baseUrl}/`; |
| return new URL( |
| "webdevtoken.v1.WebDevService/SendNotification", |
| normalizedBase |
| ).toString(); |
| }; |
|
|
| const validatePayload = (input: NotificationPayload): NotificationPayload => { |
| if (!isNonEmptyString(input.title)) { |
| throw new TRPCError({ |
| code: "BAD_REQUEST", |
| message: "Notification title is required.", |
| }); |
| } |
| if (!isNonEmptyString(input.content)) { |
| throw new TRPCError({ |
| code: "BAD_REQUEST", |
| message: "Notification content is required.", |
| }); |
| } |
|
|
| const title = trimValue(input.title); |
| const content = trimValue(input.content); |
|
|
| if (title.length > TITLE_MAX_LENGTH) { |
| throw new TRPCError({ |
| code: "BAD_REQUEST", |
| message: `Notification title must be at most ${TITLE_MAX_LENGTH} characters.`, |
| }); |
| } |
|
|
| if (content.length > CONTENT_MAX_LENGTH) { |
| throw new TRPCError({ |
| code: "BAD_REQUEST", |
| message: `Notification content must be at most ${CONTENT_MAX_LENGTH} characters.`, |
| }); |
| } |
|
|
| return { title, content }; |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| export async function notifyOwner( |
| payload: NotificationPayload |
| ): Promise<boolean> { |
| const { title, content } = validatePayload(payload); |
|
|
| if (!ENV.forgeApiUrl) { |
| throw new TRPCError({ |
| code: "INTERNAL_SERVER_ERROR", |
| message: "Notification service URL is not configured.", |
| }); |
| } |
|
|
| if (!ENV.forgeApiKey) { |
| throw new TRPCError({ |
| code: "INTERNAL_SERVER_ERROR", |
| message: "Notification service API key is not configured.", |
| }); |
| } |
|
|
| const endpoint = buildEndpointUrl(ENV.forgeApiUrl); |
|
|
| try { |
| const response = await fetch(endpoint, { |
| method: "POST", |
| headers: { |
| accept: "application/json", |
| authorization: `Bearer ${ENV.forgeApiKey}`, |
| "content-type": "application/json", |
| "connect-protocol-version": "1", |
| }, |
| body: JSON.stringify({ title, content }), |
| }); |
|
|
| if (!response.ok) { |
| const detail = await response.text().catch(() => ""); |
| console.warn( |
| `[Notification] Failed to notify owner (${response.status} ${response.statusText})${ |
| detail ? `: ${detail}` : "" |
| }` |
| ); |
| return false; |
| } |
|
|
| return true; |
| } catch (error) { |
| console.warn("[Notification] Error calling notification service:", error); |
| return false; |
| } |
| } |
|
|