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()
}
}
}