import * as sdk from '@botpress/sdk' import axios, { AxiosInstance, AxiosRequestConfig } from 'axios' import axiosRetry from 'axios-retry' import type { ZendeskUser, ZendeskTicket, ZendeskWebhook } from './definitions/schemas' import { summarizeAxiosError } from './misc/axios-utils' import { ConditionsData, getTriggerTemplate, type TriggerNames } from './triggers' import type { ZendeskEventType } from './webhookEvents' import * as bp from '.botpress' export type TicketRequester = | { name?: string email?: string } | { id: string } export type Trigger = { url: string id: string } const _makeBaseUrl = (organizationDomain: string) => { return organizationDomain.startsWith('https') ? organizationDomain : `https://${organizationDomain}.zendesk.com` } type AxiosRetryClient = Parameters[0] type ZendeskConfig = { type: 'OAuth' accessToken: string subdomain: string } class ZendeskApi { private _client: AxiosInstance private _logger: bp.Logger public constructor(config: ZendeskConfig, logger: bp.Logger) { this._logger = logger this._client = axios.create({ baseURL: _makeBaseUrl(config.subdomain), headers: { Authorization: `Bearer ${config.accessToken}`, }, }) axiosRetry(this._client as AxiosRetryClient, { retries: 3, retryDelay: axiosRetry.exponentialDelay, retryCondition: (error) => { const rateLimitReached = error.response?.status === 429 return axiosRetry.isNetworkOrIdempotentRequestError(error) || rateLimitReached }, }) } public async findCustomers(query: string): Promise { const { data } = await this._client .get<{ users: ZendeskUser[] }>(`/api/v2/users/search.json?query=${query}`) .catch(this._summarizeAxiosError) return data.users } public async getTicket(ticketId: string) { const { data } = await this._client .get<{ ticket: ZendeskTicket }>(`/api/v2/tickets/${ticketId}.json`) .catch(this._summarizeAxiosError) return data.ticket } public async createTicket( subject: string, comment: string, requester: TicketRequester, extraFields: Partial = {} ): Promise { const requesterPayload = 'id' in requester ? { requester_id: requester.id } : { requester } const { data } = await this._client .post<{ ticket: ZendeskTicket }>('/api/v2/tickets.json', { ticket: { subject, comment: { body: comment }, ...requesterPayload, ...extraFields, }, }) .catch(this._summarizeAxiosError) return data.ticket } public async subscribeWebhook(webhookUrl: string): Promise { const { data } = await this._client .post<{ webhook: { id: string } }>('/api/v2/webhooks', { webhook: { name: 'bpc_integration_webhook', status: 'active', endpoint: webhookUrl, http_method: 'POST', request_format: 'json', subscriptions: ['conditional_ticket_events'], }, }) .catch(this._summarizeAxiosError) return data.webhook?.id } public async createTrigger(name: TriggerNames, subscriptionId: string, conditions: ConditionsData): Promise { const { data } = await this._client .post<{ trigger: Trigger }>('/api/v2/triggers.json', { trigger: { actions: [ { field: 'notification_webhook', value: [subscriptionId, JSON.stringify(getTriggerTemplate(name), null, 2)], }, ], conditions, title: `bpc_${name}`, }, }) .catch(this._summarizeAxiosError) return `${data.trigger.id}` } public async deleteTrigger(triggerId: string): Promise { await this._client.delete(`/api/v2/triggers/${triggerId}.json`).catch(this._summarizeAxiosError) } public async unsubscribeWebhook(subscriptionId: string): Promise { await this._client.delete(`/api/v2/webhooks/${subscriptionId}`).catch(this._summarizeAxiosError) } public async createPlaintextComment(ticketId: string, authorId: string, content: string) { const response = await this.updateTicket(ticketId, { comment: { body: content, author_id: authorId, }, }).catch(this._summarizeAxiosError) const zendeskCommentId = this._extractCommentId(response) return { zendeskCommentId } } private _extractCommentId(response: Awaited>) { const commentId = response.audit.events.find((event) => event.type === 'Comment' && 'id' in event)?.id if (!commentId) { throw new sdk.RuntimeError('Failed to retrieve comment ID') } return commentId } public async updateTicket(ticketId: string | number, updateFields: Partial) { const response = await this._client .put<{ ticket: ZendeskTicket audit: { events: ( | { id: number type: 'Comment' } | { type: string } )[] } }>(`/api/v2/tickets/${ticketId}.json`, { ticket: updateFields, }) .catch(this._summarizeAxiosError) return response.data } public async getAgents(online?: boolean): Promise { const { data } = await this._client .get<{ users: ZendeskUser[] }>('/api/v2/users.json?role[]=agent&role[]=admin') .catch(this._summarizeAxiosError) return online ? data.users.filter((user) => user.user_fields?.availability === 'online') : data.users } public async createOrUpdateUser(fields: Partial): Promise { const { data } = await this._client .post<{ user: ZendeskUser }>('/api/v2/users/create_or_update', { user: fields, }) .catch(this._summarizeAxiosError) return data.user } public async updateUser(userId: number | string, fields: Partial): Promise { const { data } = await this._client .put<{ user: ZendeskUser }>(`/api/v2/users/${userId}.json`, { user: fields, }) .catch(this._summarizeAxiosError) return data.user } public async getUser(userId: number | string): Promise { const { data } = await this._client .get<{ user: ZendeskUser }>(`/api/v2/users/${userId}.json`) .catch(this._summarizeAxiosError) return data.user } public async createArticleWebhook(webhookUrl: string, webhookId: string): Promise { const subscriptions: ZendeskEventType[] = ['zen:event-type:article.published', 'zen:event-type:article.unpublished'] await this._client .post('/api/v2/webhooks', { webhook: { endpoint: `${webhookUrl}/article-event`, http_method: 'POST', name: `bpc_article_event_${webhookId}`, request_format: 'json', status: 'active', subscriptions, }, }) .catch(this._summarizeAxiosError) } public async deleteWebhook(webhookId: string): Promise { await this._client.delete(`/api/v2/webhooks/${webhookId}`).catch(this._summarizeAxiosError) } public async findWebhooks(params?: Record): Promise { const { data } = await this._client.get('/api/v2/webhooks', { params }).catch(this._summarizeAxiosError) return data.webhooks } public async makeRequest(requestConfig: AxiosRequestConfig) { const { data, headers, status } = await this._client.request(requestConfig).catch(this._summarizeAxiosError) return { data, headers: headers as Record, status, } } private _summarizeAxiosError = (thrown: unknown) => summarizeAxiosError(thrown, this._logger) } export type ZendeskClient = InstanceType export const getZendeskClient = async (client: bp.Client, ctx: bp.Context, logger: bp.Logger): Promise => { const { accessToken, subdomain } = await client .getState({ type: 'integration', name: 'credentials', id: ctx.integrationId }) .then((result) => result.state.payload) if (accessToken === undefined) { throw new sdk.RuntimeError('Failed to get the OAuth accessToken') } if (subdomain === undefined) { throw new sdk.RuntimeError('Failed to get the subdomain') } return new ZendeskApi({ type: 'OAuth', accessToken, subdomain }, logger) }