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<IntercomConversation>
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<string> {
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<ConversationListResult> {
const result = await intercomRequest<unknown>({
...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<IntercomConversation> {
const id = requireString(input.id, 'id')
const result = await intercomRequest<unknown>({
...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<ConversationListResult> {
const result = await intercomRequest<unknown>({
...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<IntercomConversation | DryRunResult> {
const id = requireString(input.id, 'id')
const body = requireString(input.body, 'body')
const adminId = input.dryRun
? input.adminId ?? '<connected-admin>'
: await resolveAdminId(input)
const result = await intercomRequest<unknown>({
...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<IntercomConversation | DryRunResult> {
const id = requireString(input.id, 'id')
const adminId = input.dryRun
? input.adminId ?? '<connected-admin>'
: await resolveAdminId(input)
const result = await intercomRequest<unknown>({
...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<string, unknown>): 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<string, unknown>): GetConversationInput {
const input = requireRecord(params, 'get-conversation')
return { ...parseAuth(input), id: requireString(input.id, 'id') }
}
export function parseSearchConversations(
params: Record<string, unknown>,
): 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<string, unknown>): 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<string, unknown>): 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<string, unknown> = {}) {
return listConversations(parseListConversations(params))
}