Something went wrong. Try again.
Something went wrong. Try again.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182// Loads application configuration from environment variables. Throws if any// required variable is missing or unparseable.
export type Config = Readonly<{ atprotoHandle: string; atprotoAppPassword: string; slackToken: string | undefined; slackChannel: string | undefined; weeklyNotifyDay: number; // ISO day-of-week: Monday=1 .. Sunday=7 weeklyNotifyHour: number; weeklyNotifyMinute: number; checkIntervalMs: number; enableStdout: boolean; openMeetBaseUrl: string; atprotoPdsUrl: string | undefined;}>;
// Narrow views over Config for functions that only need a subset of fields.// Composition roots keep using the full Config; downstream functions take// just what they read.export type OpenMeetConfig = Pick<Config, "openMeetBaseUrl">;
const WEEKDAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
export function loadConfig(env: Record<string, string | undefined> = process.env): Config { const required = (name: string): string => { const value = env[name]; if (!value) throw new Error(`missing required env var ${name}`); return value; };
const day = required("WEEKLY_NOTIFY_DAY"); const dayIndex = WEEKDAYS.indexOf(day); if (dayIndex === -1) throw new Error(`invalid WEEKLY_NOTIFY_DAY "${day}" (expected e.g. "Monday")`);
const time = required("WEEKLY_NOTIFY_TIME"); const match = /^(\d{2}):(\d{2})$/.exec(time); const hour = Number(match?.[1]); const minute = Number(match?.[2]); if (!match || hour > 23 || minute > 59) { throw new Error(`invalid WEEKLY_NOTIFY_TIME "${time}" (expected 24-hour HH:MM)`); }
return { atprotoHandle: required("ATPROTO_HANDLE"), atprotoAppPassword: required("ATPROTO_APP_PASSWORD"), slackToken: env.SLACK_TOKEN || undefined, slackChannel: env.SLACK_CHANNEL || undefined, weeklyNotifyDay: dayIndex + 1, weeklyNotifyHour: hour, weeklyNotifyMinute: minute, checkIntervalMs: parseDurationToMs(required("CHECK_INTERVAL")), enableStdout: env.ENABLE_STDOUT === "true", openMeetBaseUrl: env.OPENMEET_BASE_URL || "https://api.openmeet.net", atprotoPdsUrl: env.ATPROTO_PDS_URL || undefined, };}
const DURATION_UNITS: Record<string, number> = { ms: 1, s: 1_000, m: 60_000, h: 3_600_000,};
export function parseDurationToMs(input: string): number { let total = 0; let matched = ""; for (const m of input.matchAll(/(\d+)(ms|s|m|h)/g)) { total += Number(m[1]) * DURATION_UNITS[m[2]!]!; matched += m[0]; } if (matched !== input || input === "") { throw new Error(`invalid duration "${input}" (expected e.g. "30s", "5m", "1h")`); } if (total < 250) { throw new Error("check duration must be at least 250ms"); } return total;}