/** * A single deliverable part of a reminder. The bot's fire-reminder loop * walks these in order with ~1.5 s spacing between sends. The DB stores * one row per part in `reminder_messages` keyed by `position`. * * { kind: "text", textContent: "...", mediaId: null } -> plain text * { kind: "media", textContent: "...", mediaId: "uuid" } -> file w/ caption * { kind: "media", textContent: null, mediaId: "uuid" } -> file w/o caption * * `textContent` for a `media` part is the caption (sent by the bot via * the WhatsApp API's caption field, not as a separate text message). */ export interface MessagePart { kind: "text" | "media"; textContent: string | null; mediaId: string | null; } export function isValidMessagePart(p: unknown): p is MessagePart { if (!p || typeof p !== "object") return false; const m = p as Partial; if (m.kind !== "text" && m.kind !== "media") return false; if (m.textContent !== null && typeof m.textContent !== "string") return false; if (m.mediaId !== null && typeof m.mediaId !== "string") return false; if (m.kind === "text" && (!m.textContent || !m.textContent.trim())) return false; if (m.kind === "media" && !m.mediaId) return false; return true; } /** * Serialise a message stack into a single URL param. URI-encoded JSON * is shorter than base64-of-JSON for typical short-message stacks and * stays inside the ~2KB URL budget for 5–10 messages. */ export function encodeMessages(messages: MessagePart[]): string { return encodeURIComponent(JSON.stringify(messages)); } export function decodeMessages(s: string | null | undefined): MessagePart[] | null { if (!s) return null; try { const parsed = JSON.parse(decodeURIComponent(s)); if (!Array.isArray(parsed)) return null; const out = parsed.filter(isValidMessagePart); return out.length > 0 ? out : null; } catch { return null; } } /** * Backward-compat: if the wizard URL still carries the legacy single- * message fields (`text` / `mediaId` / `caption`) — bookmarked links, * old emails, etc. — synthesise a one-element MessagePart[] from them * so the new schema sees a uniform shape. */ export function legacyMessageToParts( text: string | null | undefined, mediaId: string | null | undefined, caption: string | null | undefined, ): MessagePart[] | null { if (mediaId) { return [ { kind: "media", mediaId, textContent: caption?.trim() || text?.trim() || null, }, ]; } if (text && text.trim()) { return [{ kind: "text", textContent: text, mediaId: null }]; } return null; }