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<string, unknown> {
return value && typeof value === 'object' && !Array.isArray(value)
? (value as Record<string, unknown>)
: {}
}
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<NotifyChannelConfig> {
try {
const store = packageStorage()
return parseStored(await store.get(CONFIG_KEY))
} catch {
return {}
}
}
export async function writeChannelConfig(
config: NotifyChannelConfig,
): Promise<NotifyChannelConfig> {
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
}