import { packageStorage } from 'kody:runtime' import { assertNever, DESTINATIONS, type Destination, isDestination, } from './connect.ts' export const CONFIG_KEY = 'channels' export type EmailChannelConfig = { enabled?: boolean } export type SlackChannelConfig = { enabled?: boolean /** Slack conversation id for the *fork owner's* destination. Never copy another account's channel. */ channel?: string /** Kody OAuth connection name. Default `slack`. Use `slack-work` for a second workspace. */ integration?: string } export type DiscordChannelConfig = { enabled?: boolean /** Discord channel snowflake for the *fork owner's* destination. Never copy another account's channel. */ channelId?: string /** Bot-token secret name. Default `discordBotToken`. */ secretName?: string } export type TelegramChannelConfig = { enabled?: boolean /** Telegram chat id for the *fork owner's* destination. Never copy another account's chat. */ chatId?: string /** Bot-token secret name. Default `telegramBotToken`. */ secretName?: string } export type NotifyChannelConfig = { email?: EmailChannelConfig slack?: SlackChannelConfig discord?: DiscordChannelConfig telegram?: TelegramChannelConfig } function asRecord(value: unknown): Record { return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record) : {} } function optionalBoolean(value: unknown): boolean | undefined { if (value === undefined) return undefined if (typeof value !== 'boolean') { throw new Error('enabled must be a boolean when provided.') } return value } function optionalTrimmed(value: unknown, field: string): string | undefined { if (value === undefined) return undefined if (typeof value !== 'string' || value.trim() === '') { throw new Error(`${field} must be a non-empty string when provided.`) } return value.trim() } function parseEmail(raw: unknown): EmailChannelConfig | undefined { if (raw === undefined) return undefined const input = asRecord(raw) return { enabled: optionalBoolean(input.enabled) } } function parseSlack(raw: unknown): SlackChannelConfig | undefined { if (raw === undefined) return undefined const input = asRecord(raw) return { enabled: optionalBoolean(input.enabled), channel: optionalTrimmed(input.channel, 'slack.channel'), integration: optionalTrimmed(input.integration, 'slack.integration'), } } function parseDiscord(raw: unknown): DiscordChannelConfig | undefined { if (raw === undefined) return undefined const input = asRecord(raw) return { enabled: optionalBoolean(input.enabled), channelId: optionalTrimmed(input.channelId, 'discord.channelId'), secretName: optionalTrimmed(input.secretName, 'discord.secretName'), } } function parseTelegram(raw: unknown): TelegramChannelConfig | undefined { if (raw === undefined) return undefined const input = asRecord(raw) return { enabled: optionalBoolean(input.enabled), chatId: optionalTrimmed(input.chatId, 'telegram.chatId'), secretName: optionalTrimmed(input.secretName, 'telegram.secretName'), } } export function parseChannelConfig(raw: unknown): NotifyChannelConfig { const input = asRecord(raw) return { email: parseEmail(input.email), slack: parseSlack(input.slack), discord: parseDiscord(input.discord), telegram: parseTelegram(input.telegram), } } export function mergeChannelConfig( current: NotifyChannelConfig, patch: NotifyChannelConfig, ): NotifyChannelConfig { return { email: patch.email ? { ...current.email, ...patch.email } : current.email, slack: patch.slack ? { ...current.slack, ...patch.slack } : current.slack, discord: patch.discord ? { ...current.discord, ...patch.discord } : current.discord, telegram: patch.telegram ? { ...current.telegram, ...patch.telegram } : current.telegram, } } function parseStored(raw: unknown): NotifyChannelConfig { if (!raw) return {} try { const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw return parseChannelConfig(parsed) } catch { return {} } } export async function readChannelConfig(): Promise { try { const store = packageStorage() return parseStored(await store.get(CONFIG_KEY)) } catch { return {} } } export async function writeChannelConfig( config: NotifyChannelConfig, ): Promise { try { const store = packageStorage() await store.set(CONFIG_KEY, JSON.stringify(config)) return config } catch { throw new Error( 'Could not write notify channel config. Live @kody/notify storage is the platform package bucket. Fork this package (or packages.invoke your copy) so packageStorage belongs to the user, then save their destinations — never Kent or official Kody channel ids.', ) } } export function emailEnabled(config: NotifyChannelConfig): boolean { return config.email?.enabled !== false } export function slackIntegrationName(config: NotifyChannelConfig): string { return config.slack?.integration || 'slack' } export function discordSecretName(config: NotifyChannelConfig): string { return config.discord?.secretName || 'discordBotToken' } export function telegramSecretName(config: NotifyChannelConfig): string { return config.telegram?.secretName || 'telegramBotToken' } export function destinationReady( destination: Destination, config: NotifyChannelConfig, ): { enabled: boolean; ready: boolean; missing: string | null } { switch (destination) { case 'email': { const enabled = emailEnabled(config) return { enabled, ready: enabled, missing: enabled ? null : 'email disabled in packageStorage' } } case 'slack': { const enabled = config.slack?.enabled === true const channel = config.slack?.channel if (!enabled) { return { enabled: false, ready: false, missing: 'slack.enabled is not true' } } if (!channel) { return { enabled: true, ready: false, missing: 'slack.channel is not set in packageStorage', } } return { enabled: true, ready: true, missing: null } } case 'discord': { const enabled = config.discord?.enabled === true const channelId = config.discord?.channelId if (!enabled) { return { enabled: false, ready: false, missing: 'discord.enabled is not true' } } if (!channelId) { return { enabled: true, ready: false, missing: 'discord.channelId is not set in packageStorage', } } return { enabled: true, ready: true, missing: null } } case 'telegram': { const enabled = config.telegram?.enabled === true const chatId = config.telegram?.chatId if (!enabled) { return { enabled: false, ready: false, missing: 'telegram.enabled is not true' } } if (!chatId) { return { enabled: true, ready: false, missing: 'telegram.chatId is not set in packageStorage', } } return { enabled: true, ready: true, missing: null } } default: return assertNever(destination) } } export function resolveDestinations( requested: unknown, config: NotifyChannelConfig, ): Destination[] { if (requested === undefined) { return DESTINATIONS.filter((destination) => { return destinationReady(destination, config).enabled }) } if (!Array.isArray(requested) || requested.length === 0) { throw new Error( 'channels must be a non-empty array of email, slack, discord, and/or telegram.', ) } const selected: Destination[] = [] for (const value of requested) { if (!isDestination(value)) { throw new Error( `Unknown channel ${JSON.stringify(value)}. Use email, slack, discord, or telegram.`, ) } if (!selected.includes(value)) selected.push(value) } return selected }