export type DiscordImage = { url: string filename: string contentType: string } const IMAGE_EXT = /\.(png|jpe?g|gif|webp|avif|bmp)$/i function isImageName(filename: string, contentType: string, url: string): boolean { if (contentType.toLowerCase().startsWith('image/')) return true return IMAGE_EXT.test(filename) || IMAGE_EXT.test(url.split('?')[0] || '') } function pushImage( images: DiscordImage[], seen: Set, row: Record, ): void { const url = (typeof row.url === 'string' && row.url.trim()) || (typeof row.proxy_url === 'string' && row.proxy_url.trim()) || '' if (!url || seen.has(url)) return const filename = (typeof row.filename === 'string' && row.filename.trim()) || url.split('/').pop()?.split('?')[0] || 'image' const contentType = (typeof row.content_type === 'string' && row.content_type.trim()) || (typeof row.contentType === 'string' && row.contentType.trim()) || '' if (!isImageName(filename, contentType, url)) return seen.add(url) images.push({ url, filename, contentType: contentType || 'image/png', }) } /** * Pull image attachments off a raw Discord MESSAGE_CREATE (or wrapped * subscription payload). `normalize-message-created` drops attachments, so * grok-bot has to read the raw event. */ export function collectDiscordImages(raw: unknown, depth = 0): DiscordImage[] { const images: DiscordImage[] = [] const seen = new Set() visit(raw, 0) return images function visit(value: unknown, level: number): void { if (level > 6 || value == null) return if (Array.isArray(value)) { for (const item of value) visit(item, level + 1) return } if (typeof value !== 'object') return const row = value as Record if (Array.isArray(row.attachments)) { for (const att of row.attachments) { if (att && typeof att === 'object') pushImage(images, seen, att as Record) } } if (row.image && typeof row.image === 'object') { pushImage(images, seen, row.image as Record) } if (row.thumbnail && typeof row.thumbnail === 'object') { pushImage(images, seen, row.thumbnail as Record) } if (Array.isArray(row.embeds)) visit(row.embeds, level + 1) if (row.payload) visit(row.payload, level + 1) if (row.event && row.event !== value) visit(row.event, level + 1) if (row.d) visit(row.d, level + 1) if (row.message && row.message !== value) visit(row.message, level + 1) } } export function buildImageWakePrompt(input: { botName: string content: string images: Array }): string { const lines: string[] = [] const isImogen = input.botName.trim().toLowerCase() === 'imogen' if (isImogen) { lines.push( 'Write one brief copyable alt block for the attached image. Most important part only. Nothing else. Reply in this Discord channel; stay quiet in Grok Bot.', ) if (input.content) { lines.push('') lines.push(`Kent said: ${input.content}`) } } else if (input.content) { lines.push(input.content) } else { lines.push('Kent posted an image in Discord.') } if (input.images.length) { lines.push('') lines.push('Attached image(s):') for (const image of input.images) { lines.push(`- ${image.filename}: ${image.url}`) } lines.push('Look at those image URLs. They are the message, not decoration.') } return lines.join('\n') }