Skip to content

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

Package listing

@kentcdodds/grok-bot

src/handle-discord-message-created.ts

148 lines · 4.1 KB · TypeScript
import postMessage from 'kody:@kentcdodds/discord/post-message'
import startTyping from 'kody:@kentcdodds/discord/start-typing-indicator'
import normalizeMessageCreated from 'kody:@kentcdodds/discord/normalize-message-created'
import { resolveChannelMapping } from './discord-map.ts'
import {
	botKey,
	isSecretPending,
	parseBotRecord,
} from './registry.ts'
import wake from './wake.ts'
import { packageStorage } from 'kody:runtime'

export type HandleDiscordMessageCreatedInput = {
	event?: Record<string, unknown>
	dryRun?: boolean
	[key: string]: unknown
}

function unwrapPackageEvent(input: Record<string, unknown>) {
	if (
		input?.event === '@kentcdodds/discord.message.created' &&
		input?.payload &&
		typeof input.payload === 'object'
	) {
		return input.payload as Record<string, unknown>
	}
	return input
}

function pickDryRun(...values: unknown[]): boolean {
	return values.some((value) => value === true)
}

/**
 * Production `@kentcdodds/discord.message.created` subscriber for Grok Bot channels.
 *
 * A Kent-authored message in a mapped per-bot channel wakes that bot. Unmapped
 * channels return `{ handled: false }` so other subscribers can run.
 *
 * @param params.event - Normalized or raw message.created event from Discord dispatch
 * @param params.dryRun - Inspect routing without posting or waking
 * @returns Handler result with handled flag and bot/channel metadata
 * @example
 * import handleDiscordMessageCreated from 'kody:@kentcdodds/grok-bot/handle-discord-message-created'
 * const result = await handleDiscordMessageCreated({
 *   dryRun: true,
 *   event: { channelId: '1542616408020619334', messageId: '1', content: 'hi', author: { id: '105755735731781632' } },
 * })
 * // => { handled: true, reason: 'bot-not-registered-for-wake', bot: { id, name } }
 */
export default async function handleDiscordMessageCreated(
	input: HandleDiscordMessageCreatedInput = {},
): Promise<Record<string, unknown>> {
	const params = unwrapPackageEvent(input as Record<string, unknown>)
	const dryRun = pickDryRun(input.dryRun, params.dryRun)
	const event = await normalizeMessageCreated(params.event || params)
	const mapping = await resolveChannelMapping(event)

	if (event.author?.bot === true) {
		return {
			subscriber: 'grok-bot',
			handled: false,
			reason: 'ignored-bot-author',
		}
	}

	const content = typeof event.content === 'string' ? event.content.trim() : ''
	if (!content) {
		return {
			subscriber: 'grok-bot',
			handled: Boolean(mapping),
			reason: 'ignored-empty-content',
			channelId: event.channelId,
		}
	}

	if (!mapping) {
		return {
			subscriber: 'grok-bot',
			handled: false,
			reason: 'unmapped-channel',
			channelId: event.channelId,
		}
	}

	const record = parseBotRecord(await packageStorage().get(botKey(mapping.botId)))
	const pendingSecret = record ? await isSecretPending(record.secretName) : true
	const hasUrl = Boolean(record?.url)

	if (!hasUrl || pendingSecret) {
		const notice = `${mapping.name} is not registered for wake yet. Call register with this bot's webhook URL, then save the sender key on kody.codes.`
		const posted =
			dryRun === true
				? null
				: await postMessage({
						channelId: event.channelId,
						content: notice,
						replyToMessageId: event.messageId,
					})
		return {
			subscriber: 'grok-bot',
			handled: true,
			reason: 'bot-not-registered-for-wake',
			bot: { id: mapping.botId, name: mapping.name },
			channelId: mapping.channelId,
			messageId: event.messageId,
			pendingSecret,
			hasUrl,
			dryRun,
			posted,
		}
	}

	let typing: { info?: unknown; stop?: () => Promise<void> } | null = null
	try {
		typing = await startTyping({
			channelId: event.channelId,
			dryRun,
		})
	} catch {
		typing = null
	}

	try {
		const woke = await wake({
			bot: mapping.botId,
			prompt: content,
			discordChannelId: event.channelId,
			discordMessageId: event.messageId,
			dryRun,
		})
		return {
			subscriber: 'grok-bot',
			handled: true,
			reason: 'woke',
			bot: { id: mapping.botId, name: mapping.name },
			channelId: mapping.channelId,
			messageId: event.messageId,
			dryRun,
			wake: woke,
			typingIndicator: typing?.info ?? null,
		}
	} finally {
		if (typeof typing?.stop === 'function') {
			await typing.stop()
		}
	}
}