chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:34:48 +08:00
commit 77bb5bf71f
3762 changed files with 353249 additions and 0 deletions
+264
View File
@@ -0,0 +1,264 @@
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<typeof axiosRetry>[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<ZendeskUser[]> {
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<ZendeskTicket> = {}
): Promise<ZendeskTicket> {
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<string> {
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<string> {
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<void> {
await this._client.delete(`/api/v2/triggers/${triggerId}.json`).catch(this._summarizeAxiosError)
}
public async unsubscribeWebhook(subscriptionId: string): Promise<void> {
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<ReturnType<typeof this.updateTicket>>) {
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<ZendeskTicket>) {
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<ZendeskUser[]> {
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<ZendeskUser>): Promise<ZendeskUser> {
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<ZendeskUser>): Promise<ZendeskUser> {
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<ZendeskUser> {
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<void> {
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<void> {
await this._client.delete(`/api/v2/webhooks/${webhookId}`).catch(this._summarizeAxiosError)
}
public async findWebhooks(params?: Record<string, string>): Promise<ZendeskWebhook[]> {
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<string, string>,
status,
}
}
private _summarizeAxiosError = (thrown: unknown) => summarizeAxiosError(thrown, this._logger)
}
export type ZendeskClient = InstanceType<typeof ZendeskApi>
export const getZendeskClient = async (client: bp.Client, ctx: bp.Context, logger: bp.Logger): Promise<ZendeskApi> => {
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)
}