Skip to content

src/thread.ts

116 lines · 3.1 KB · TypeScript
Raw
import { kody } from 'kody:runtime'

const LIST_LIMIT = 100
const DEFAULT_RECENT = 8
const MAX_RECENT = 25

export type RecentThreadInput = {
	/** Kody inbox thread id. */
	threadId?: string
	thread_id?: string
	/** How many newest messages to hydrate with bodies. 0 = count only. Defaults to 8. */
	limit?: number
	/** Optional inbox filter. */
	inbox_id?: string
}

export type RecentThreadMessage = {
	id: string
	direction?: string
	thread_id?: string | null
	from_address?: string | null
	to_addresses?: Array<string>
	cc_addresses?: Array<string>
	subject?: string | null
	received_at?: string | null
	sent_at?: string | null
	created_at?: string
	text_body?: string | null
	html_body?: string | null
	[key: string]: unknown
}

export type RecentThreadResult = {
	threadId: string
	count: number
	limit: number
	capped: boolean
	messages: RecentThreadMessage[]
}

function stamp(row: RecentThreadMessage): number {
	const raw = row.received_at || row.sent_at || row.created_at || ''
	const ms = Date.parse(String(raw))
	return Number.isFinite(ms) ? ms : 0
}

async function listDirection(
	direction: 'inbound' | 'outbound',
	inboxId?: string,
): Promise<{ rows: RecentThreadMessage[]; capped: boolean }> {
	const listed = await kody.emailMessageList({
		...(inboxId ? { inbox_id: inboxId } : {}),
		direction,
		limit: LIST_LIMIT,
	})
	const rows = Array.isArray(listed?.messages) ? listed.messages : []
	return {
		rows: rows as RecentThreadMessage[],
		capped: rows.length >= LIST_LIMIT,
	}
}

/**
 * Count messages on a Kody inbox thread and hydrate the newest `limit` with bodies.
 *
 * @example
 * import recentThread from 'kody:@kentcdodds/grok-bot/thread'
 * const { count, messages } = await recentThread({
 *   threadId: 'thread-id',
 *   limit: 8,
 * })
 */
export default async function recentThread(
	input: RecentThreadInput = {},
): Promise<RecentThreadResult> {
	const threadId = String(input.threadId || input.thread_id || '').trim()
	if (!threadId) {
		throw new Error('threadId is required')
	}
	const requested =
		typeof input.limit === 'number' && Number.isFinite(input.limit)
			? Math.trunc(input.limit)
			: DEFAULT_RECENT
	const limit = Math.max(0, Math.min(MAX_RECENT, requested))
	const inboxId =
		typeof input.inbox_id === 'string' && input.inbox_id.trim()
			? input.inbox_id.trim()
			: undefined

	const [inbound, outbound] = await Promise.all([
		listDirection('inbound', inboxId),
		listDirection('outbound', inboxId),
	])
	const byId = new Map<string, RecentThreadMessage>()
	for (const row of [...inbound.rows, ...outbound.rows]) {
		if (row?.id && row.thread_id === threadId) byId.set(row.id, row)
	}
	const inThread = [...byId.values()].sort((a, b) => stamp(b) - stamp(a))
	const recent = limit > 0 ? inThread.slice(0, limit) : []
	const messages: RecentThreadMessage[] = []
	for (const row of recent) {
		try {
			const full = await kody.emailMessageGet({ message_id: row.id })
			messages.push((full || row) as RecentThreadMessage)
		} catch {
			messages.push(row)
		}
	}
	return {
		threadId,
		count: inThread.length,
		limit,
		capped: inbound.capped || outbound.capped,
		messages,
	}
}