import type { MessageBlock, RoutineEventBlock } from "@rakazo/contracts"; import { formatCron, isOneShotRoutineCrons } from "./cron.js"; /** A message that is only timeline lines, shown without a speaker, bubble or reactions. */ export function isRoutineEventMessage(blocks: readonly MessageBlock[]): boolean { return ( blocks.length > 0 && blocks.every((block) => block.kind === "event" || block.kind === "autopilot") ); } /** The blocks with every line about `routineId` marked removed, or null when nothing changes. */ export function markRoutineEventsRemoved( blocks: readonly MessageBlock[], routineId: string, ): MessageBlock[] | null { let changed = false; const next = blocks.map((block) => { if (block.kind !== "event" || block.routineId !== routineId || block.removed) return block; changed = true; return { ...block, removed: true }; }); return changed ? next : null; } /** How a later turn reads the line, so the bot knows what it set up and what is gone. */ export function routineEventHistoryText(block: RoutineEventBlock): string { if (block.removed) return `[routine removed: ${block.name}]`; const verb = block.event === "routine.updated" ? "updated" : "set up"; const schedule = isOneShotRoutineCrons(block.crons) ? `once at ${block.nextRunAt ?? "an unknown time"}` : `${block.crons.map(formatCron).join(", ")}, ${block.timezone}`; return `[routine ${verb}: ${block.name} (${schedule})]`; } /** The parts of a routine schedule a one-line summary needs; clients word it in their locale. */ export type RoutineScheduleShape = | { kind: "once" } | { kind: "hourly" } | { kind: "minutes" | "hours"; every: number } | { kind: "daily" | "weekdays"; hour: number; minute: number } | { kind: "weekly"; weekday: number; hour: number; minute: number } | { kind: "monthly"; day: number; hour: number; minute: number } | { kind: "other" }; const CLOCK_FIELD = /^\d{1,2}$/; export function routineScheduleShape(crons: readonly string[]): RoutineScheduleShape { if (isOneShotRoutineCrons([...crons])) return { kind: "once" }; if (crons.length !== 1) return { kind: "other" }; const parts = crons[0]!.trim().split(/\s+/); if (parts.length !== 5) return { kind: "other" }; const [minute, hour, day, month, weekday] = parts as [string, string, string, string, string]; if (month !== "*") return { kind: "other" }; const step = (field: string) => /^\*\/(\d+)$/.exec(field)?.[1]; if (day === "*" && weekday === "*") { if (minute === "0" && hour === "*") return { kind: "hourly" }; const minuteStep = step(minute); if (minuteStep && hour === "*") return { kind: "minutes", every: Number(minuteStep) }; const hourStep = step(hour); if (minute === "0" && hourStep) return { kind: "hours", every: Number(hourStep) }; } if (!CLOCK_FIELD.test(minute) || !CLOCK_FIELD.test(hour)) return { kind: "other" }; const clock = { hour: Number(hour), minute: Number(minute) }; if (clock.hour > 23 || clock.minute > 59) return { kind: "other" }; if (day === "*" && weekday === "*") return { kind: "daily", ...clock }; if (day === "*" && weekday === "1-5") return { kind: "weekdays", ...clock }; if (day === "*" && /^[0-7]$/.test(weekday)) { return { kind: "weekly", weekday: Number(weekday) % 7, ...clock }; } if (weekday === "*" && /^([1-9]|[12]\d|3[01])$/.test(day)) { return { kind: "monthly", day: Number(day), ...clock }; } return { kind: "other" }; }