Skip to content

Built for people who want to own their automations. Join the waitlist for an invite.

Package listing

@kentcdodds/grok-bot

src/discord-map.ts

190 lines · 6.3 KB · TypeScript
import { packageStorage } from 'kody:runtime'
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<string, unknown>
	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<string> {
	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<string> {
	const id = categoryId.trim() || GROK_BOT_CATEGORY_ID
	await packageStorage().set(DISCORD_CATEGORY_KEY, id)
	return id
}

export async function writeChannelMapping(mapping: ChannelMapping): Promise<void> {
	const bucket = packageStorage()
	const previousRaw = await bucket.get(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), {
		channelId: mapping.channelId,
		botId: mapping.botId,
		name: mapping.name,
	})
	await bucket.set(botChannelKey(mapping.botId), mapping.channelId)
}

export async function getMappingByChannelId(
	channelId: string,
): Promise<ChannelMapping | null> {
	if (!channelId.trim()) return null
	return parseChannelMapping(await packageStorage().get(channelKey(channelId.trim())))
}

export async function getMappingByBotId(botId: string): Promise<ChannelMapping | null> {
	if (!botId.trim()) return null
	const channelId = await packageStorage().get(botChannelKey(botId.trim()))
	const id = typeof channelId === 'string' ? channelId.trim() : ''
	if (!id) return null
	return getMappingByChannelId(id)
}

export async function listChannelMappings(): Promise<ChannelMapping[]> {
	const listed = await packageStorage().list({ prefix: CHANNEL_PREFIX })
	const mappings: ChannelMapping[] = []
	const seen = new Set<string>()
	for (const entry of listEntries(listed)) {
		const value = 'value' in entry ? entry.value : entry
		const mapping = parseChannelMapping(value)
		if (!mapping || seen.has(mapping.channelId)) continue
		seen.add(mapping.channelId)
		mappings.push(mapping)
	}
	return mappings
}

export async function getMappingByName(name: string): Promise<ChannelMapping | null> {
	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<ChannelMapping | null> {
	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<BotRecord> {
	const now = new Date().toISOString()
	const existing = parseBotRecord(await packageStorage().get(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
}