import { parseAuth } from './auth.ts' import { intercomRequest } from './client.ts' import { compactDefined, extractListItems, mapConversation } from './models.ts' import { getMe } from './me.ts' import type { DryRunResult, IntercomAuthInput, IntercomConversation, IntercomPageInfo, IntercomSearchQuery, JsonRecord, MutationInput, } from './types.ts' import { clampInt, optionalBoolean, optionalString, requireRecord, requireString, } from './types.ts' export type ListConversationsInput = IntercomAuthInput & { perPage?: number startingAfter?: string } export type GetConversationInput = IntercomAuthInput & { id: string } export type SearchConversationsInput = IntercomAuthInput & { query?: IntercomSearchQuery | JsonRecord state?: string perPage?: number startingAfter?: string } export type ReplyConversationInput = IntercomAuthInput & MutationInput & { id: string body: string adminId?: string messageType?: string type?: string } export type CloseConversationInput = IntercomAuthInput & MutationInput & { id: string adminId?: string } export type ConversationListResult = { items: Array pageInfo: IntercomPageInfo } function searchBody(input: SearchConversationsInput): JsonRecord { if (input.query) return { query: input.query } if (input.state) { return { query: { field: 'state', operator: '=', value: input.state, }, } } throw new Error('searchConversations requires query or state.') } async function resolveAdminId(input: IntercomAuthInput & { adminId?: string }): Promise { if (input.adminId) return requireString(input.adminId, 'adminId') const me = await getMe(input) return me.id } /** * List Intercom conversations. * @example * import { listConversations } from 'kody:@kody/intercom/conversations' * const { items } = await listConversations({ perPage: 10 }) */ export async function listConversations( input: ListConversationsInput = {}, ): Promise { const result = await intercomRequest({ ...input, operation: 'conversations.read', path: '/conversations', query: compactDefined({ per_page: clampInt(input.perPage, 1, 150, 20), starting_after: input.startingAfter, }), }) if ('dryRun' in result) { throw new Error('listConversations is read-only.') } return { items: extractListItems(result.data) .map(mapConversation) .filter((item): item is IntercomConversation => Boolean(item)), pageInfo: result.pageInfo, } } /** * Get one Intercom conversation by id. * @example * import { getConversation } from 'kody:@kody/intercom/conversations' * const conversation = await getConversation({ id }) */ export async function getConversation(input: GetConversationInput): Promise { const id = requireString(input.id, 'id') const result = await intercomRequest({ ...input, operation: 'conversations.read', path: `/conversations/${encodeURIComponent(id)}`, }) if ('dryRun' in result) { throw new Error('getConversation is read-only.') } const mapped = mapConversation(result.data) if (!mapped) throw new Error('Intercom did not return a conversation id.') return mapped } /** * Search Intercom conversations. POST `/conversations/search` is a read. * @example * import { searchConversations } from 'kody:@kody/intercom/conversations' * const { items } = await searchConversations({ state: 'open' }) */ export async function searchConversations( input: SearchConversationsInput, ): Promise { const result = await intercomRequest({ ...input, operation: 'conversations.read', method: 'POST', path: '/conversations/search', body: compactDefined({ ...searchBody(input), pagination: compactDefined({ per_page: input.perPage === undefined ? undefined : clampInt(input.perPage, 1, 150, 20), starting_after: input.startingAfter, }), }), }) if ('dryRun' in result) { throw new Error('searchConversations is read-only.') } return { items: extractListItems(result.data) .map(mapConversation) .filter((item): item is IntercomConversation => Boolean(item)), pageInfo: result.pageInfo, } } /** * Reply to an Intercom conversation. Requires `confirm: true`, or use `dryRun: true`. * When `adminId` is omitted, the connected `/me` admin is used. * @example * import { replyConversation } from 'kody:@kody/intercom/conversations' * const preview = await replyConversation({ id, body: 'Thanks — looking into this.', dryRun: true }) */ export async function replyConversation( input: ReplyConversationInput, ): Promise { const id = requireString(input.id, 'id') const body = requireString(input.body, 'body') const adminId = input.dryRun ? input.adminId ?? '' : await resolveAdminId(input) const result = await intercomRequest({ ...input, operation: 'conversations.write', method: 'POST', path: `/conversations/${encodeURIComponent(id)}/reply`, body: { message_type: input.messageType ?? 'comment', type: input.type ?? 'admin', admin_id: adminId, body, }, }) if ('dryRun' in result) return result const mapped = mapConversation(result.data) if (!mapped) throw new Error('Intercom did not return a conversation id after reply.') return mapped } /** * Close an Intercom conversation. Requires `confirm: true`, or use `dryRun: true`. * When `adminId` is omitted, the connected `/me` admin is used. * @example * import { closeConversation } from 'kody:@kody/intercom/conversations' * const preview = await closeConversation({ id, dryRun: true }) */ export async function closeConversation( input: CloseConversationInput, ): Promise { const id = requireString(input.id, 'id') const adminId = input.dryRun ? input.adminId ?? '' : await resolveAdminId(input) const result = await intercomRequest({ ...input, operation: 'conversations.write', method: 'POST', path: `/conversations/${encodeURIComponent(id)}/parts`, body: { message_type: 'close', type: 'admin', admin_id: adminId, }, }) if ('dryRun' in result) return result const mapped = mapConversation(result.data) if (!mapped) throw new Error('Intercom did not return a conversation id after close.') return mapped } export function parseListConversations(params: Record): ListConversationsInput { const input = requireRecord(params, 'list-conversations') return { ...parseAuth(input), perPage: input.perPage as number | undefined, startingAfter: optionalString(input.startingAfter, 'startingAfter'), } } export function parseGetConversation(params: Record): GetConversationInput { const input = requireRecord(params, 'get-conversation') return { ...parseAuth(input), id: requireString(input.id, 'id') } } export function parseSearchConversations( params: Record, ): SearchConversationsInput { const input = requireRecord(params, 'search-conversations') return { ...parseAuth(input), query: input.query as SearchConversationsInput['query'], state: optionalString(input.state, 'state'), perPage: input.perPage as number | undefined, startingAfter: optionalString(input.startingAfter, 'startingAfter'), } } export function parseReplyConversation(params: Record): ReplyConversationInput { const input = requireRecord(params, 'reply-conversation') return { ...parseAuth(input), id: requireString(input.id, 'id'), body: requireString(input.body, 'body'), adminId: optionalString(input.adminId, 'adminId'), messageType: optionalString(input.messageType, 'messageType'), type: optionalString(input.type, 'type'), confirm: optionalBoolean(input.confirm, 'confirm'), dryRun: optionalBoolean(input.dryRun, 'dryRun'), } } export function parseCloseConversation(params: Record): CloseConversationInput { const input = requireRecord(params, 'close-conversation') return { ...parseAuth(input), id: requireString(input.id, 'id'), adminId: optionalString(input.adminId, 'adminId'), confirm: optionalBoolean(input.confirm, 'confirm'), dryRun: optionalBoolean(input.dryRun, 'dryRun'), } } /** * Intercom conversation helpers. Writes require `confirm: true` or `dryRun: true`. * @example * import { listConversations } from 'kody:@kody/intercom/conversations' * const { items } = await listConversations({ perPage: 10 }) */ export default async function conversationsEntrypoint(params: Record = {}) { return listConversations(parseListConversations(params)) }