import { packageStorage } from 'kody:runtime' import { storageGet } from './plain.ts' import { botKey, normalizeName, parseBotRecord, secretNameFor, writeBotRecord, type BotRecord, } from './registry.ts' export const GROK_BOT_CATEGORY_ID = '1542616354274676827' export const DISCORD_CATEGORY_KEY = 'discord.categoryId' export const CHANNEL_PREFIX = 'discord.channel.' export const BOT_CHANNEL_PREFIX = 'discord.botChannel.' export const KNOWN_GROK_BOT_CHANNELS = [ { name: 'kody', channelId: '1542616408020619334', botId: '71e7550e-746d-417f-b253-05165975ff69' }, { name: 'cole', channelId: '1542616409694015558', botId: '6519d644-b6ac-4e61-989c-3d96d0b63cca' }, { name: 'connie', channelId: '1542616411204096020', botId: '57072205-edee-46ed-b4fa-a24b657ef369' }, { name: 'howie', channelId: '1542616412428828822', botId: '021d6409-01e7-4af0-a59c-4cbf2785cb50' }, { name: 'pam', channelId: '1542616413708222547', botId: '58b84955-b713-4814-b0ca-b7982b4b5829' }, { name: 'felix', channelId: '1542616415280963725', botId: '60f91f03-6b17-4b1a-a590-529613c79977' }, { name: 'arthur', channelId: '1542616416547766332', botId: '775984a5-71ac-4e8c-bba3-b93de8ff6a06' }, { name: 'marley', channelId: '1542616417948536913', botId: 'a08ff635-d146-4205-bb62-d538e9723d77' }, { name: 'scott', channelId: '1542616422168002612', botId: 'b5cf16d8-dde6-4d3d-b5b8-4452bfade775' }, { name: 'imogen', channelId: '1542616423845728337', botId: 'f835c77a-c93f-424e-a0f1-f191c2235e19' }, ] as const export type ChannelMapping = { channelId: string botId: string name: string } function listEntries(listed: unknown): Array<{ key?: string; value?: unknown }> { if (Array.isArray(listed)) { return listed.filter((item) => item && typeof item === 'object') as Array<{ key?: string value?: unknown }> } if (listed && typeof listed === 'object') { const entries = (listed as { entries?: unknown }).entries if (Array.isArray(entries)) { return entries.filter((item) => item && typeof item === 'object') as Array<{ key?: string value?: unknown }> } } return [] } export function channelSlug(name: string): string { return name .trim() .toLowerCase() .replace(/[^a-z0-9-]+/g, '-') .replace(/-+/g, '-') .replace(/^-|-$/g, '') .slice(0, 100) } export function channelKey(channelId: string): string { return `${CHANNEL_PREFIX}${channelId}` } export function botChannelKey(botId: string): string { return `${BOT_CHANNEL_PREFIX}${botId}` } export function parseChannelMapping(value: unknown): ChannelMapping | null { let parsed = value if (typeof parsed === 'string') { try { parsed = JSON.parse(parsed) } catch { return null } } if (!parsed || typeof parsed !== 'object') return null const row = parsed as Record const channelId = typeof row.channelId === 'string' ? row.channelId.trim() : '' const botId = typeof row.botId === 'string' ? row.botId.trim() : '' const name = typeof row.name === 'string' ? row.name.trim() : '' if (!channelId || !botId || !name) return null return { channelId, botId, name } } export async function readCategoryId(): Promise { const raw = await packageStorage().get(DISCORD_CATEGORY_KEY) const stored = typeof raw === 'string' ? raw.trim() : '' return stored || GROK_BOT_CATEGORY_ID } export async function writeCategoryId(categoryId: string): Promise { const id = categoryId.trim() || GROK_BOT_CATEGORY_ID await packageStorage().set(DISCORD_CATEGORY_KEY, id) return id } export async function writeChannelMapping(mapping: ChannelMapping): Promise { const bucket = packageStorage() const previousRaw = await storageGet((key) => bucket.get(key), botChannelKey(mapping.botId)) const previous = typeof previousRaw === 'string' ? previousRaw.trim() : '' if (previous && previous !== mapping.channelId) { await bucket.delete(channelKey(previous)) } await bucket.set( channelKey(mapping.channelId), JSON.stringify({ channelId: mapping.channelId, botId: mapping.botId, name: mapping.name, }), ) await bucket.set(botChannelKey(mapping.botId), mapping.channelId) } export async function getMappingByChannelId( channelId: string, ): Promise { if (!channelId.trim()) return null const bucket = packageStorage() return parseChannelMapping( await storageGet((key) => bucket.get(key), channelKey(channelId.trim())), ) } export async function getMappingByBotId(botId: string): Promise { if (!botId.trim()) return null const bucket = packageStorage() const channelId = await storageGet((key) => bucket.get(key), botChannelKey(botId.trim())) const id = typeof channelId === 'string' ? channelId.trim() : '' if (!id) return null return getMappingByChannelId(id) } export async function listChannelMappings(): Promise { const bucket = packageStorage() let listed: unknown = null try { listed = await bucket.list({ prefix: CHANNEL_PREFIX }) } catch (error) { const message = error instanceof Error ? error.message : String(error) if (!/Proxy could not be serialized/i.test(message)) throw error } const mappings: ChannelMapping[] = [] const seen = new Set() const keys: string[] = [] for (const entry of listEntries(listed)) { const key = typeof entry.key === 'string' ? entry.key : '' if (key && key.startsWith(CHANNEL_PREFIX)) keys.push(key) } for (const key of keys) { const mapping = parseChannelMapping(await storageGet((k) => bucket.get(k), key)) if (!mapping || seen.has(mapping.channelId)) continue seen.add(mapping.channelId) mappings.push(mapping) } // Fallback when legacy object rows are unreadable Proxies. if (mappings.length === 0) { for (const known of KNOWN_GROK_BOT_CHANNELS) { mappings.push({ channelId: known.channelId, botId: known.botId, name: known.name, }) } } return mappings } export async function getMappingByName(name: string): Promise { const slug = channelSlug(name) if (!slug) return null const mappings = await listChannelMappings() return mappings.find((mapping) => channelSlug(mapping.name) === slug) ?? null } export async function resolveChannelMapping(event: { channelId?: string | null parentChannelId?: string | null threadId?: string | null }): Promise { const ids = [event.channelId, event.parentChannelId, event.threadId] for (const id of ids) { if (typeof id !== 'string' || !id.trim()) continue const mapping = await getMappingByChannelId(id) if (mapping) return mapping } return null } export async function upsertStubBotRecord(input: { id: string name: string channelId: string }): Promise { const now = new Date().toISOString() const existing = parseBotRecord( await storageGet((key) => packageStorage().get(key), botKey(input.id)), ) const normalized = normalizeName(input.name) const aliases = existing ? Array.from(new Set([...existing.aliases, normalized])) : [normalized] const record: BotRecord = { id: input.id, name: existing?.name || input.name, aliases, url: existing?.url ?? '', secretName: existing?.secretName || secretNameFor(input.id), channelId: input.channelId, createdAt: existing?.createdAt ?? now, updatedAt: now, } await writeBotRecord(record) return record }