diff --git a/bots/doppel-doer/bot.definition.ts b/bots/doppel-doer/bot.definition.ts index 28b35b0afbf..1ad7f6a3fdc 100644 --- a/bots/doppel-doer/bot.definition.ts +++ b/bots/doppel-doer/bot.definition.ts @@ -12,9 +12,6 @@ export default new sdk.BotDefinition({ recurringEvents: {}, user: {}, conversation: {}, - __advanced: { - useLegacyZuiTransformer: true, - }, }) .addIntegration(todoist, { alias: 'todoist-src', diff --git a/bots/echo/bot.definition.ts b/bots/echo/bot.definition.ts index e0e643c9742..0516fced102 100644 --- a/bots/echo/bot.definition.ts +++ b/bots/echo/bot.definition.ts @@ -6,7 +6,4 @@ export default new sdk.BotDefinition({ states: {}, events: {}, recurringEvents: {}, - __advanced: { - useLegacyZuiTransformer: true, - }, }).addIntegration(chat, { enabled: true, configuration: {} }) diff --git a/bots/hello-world/bot.definition.ts b/bots/hello-world/bot.definition.ts index 7db956ba52c..0ec60960fd4 100644 --- a/bots/hello-world/bot.definition.ts +++ b/bots/hello-world/bot.definition.ts @@ -17,9 +17,6 @@ export default new sdk.BotDefinition({ }, }, }, - __advanced: { - useLegacyZuiTransformer: true, - }, }) .addIntegration(telegram, { enabled: true, diff --git a/bots/hit-looper/bot.definition.ts b/bots/hit-looper/bot.definition.ts index 5ba26f353c4..8df6d94a9be 100644 --- a/bots/hit-looper/bot.definition.ts +++ b/bots/hit-looper/bot.definition.ts @@ -21,9 +21,6 @@ export default new sdk.BotDefinition({ }, }, conversation: {}, - __advanced: { - useLegacyZuiTransformer: true, - }, }) .addIntegration(chat, { enabled: true, diff --git a/bots/knowledgiani/bot.definition.ts b/bots/knowledgiani/bot.definition.ts index 6e12e4d1449..2ad7a5a89a9 100644 --- a/bots/knowledgiani/bot.definition.ts +++ b/bots/knowledgiani/bot.definition.ts @@ -7,11 +7,7 @@ import telegram from './bp_modules/telegram' type OpenAiModel = sdk.z.infer -export default new sdk.BotDefinition({ - __advanced: { - useLegacyZuiTransformer: true, - }, -}) +export default new sdk.BotDefinition({}) .addIntegration(telegram, { enabled: true, configuration: { diff --git a/bots/notionaut/bot.definition.ts b/bots/notionaut/bot.definition.ts index 35c24f80594..b3438ab4631 100644 --- a/bots/notionaut/bot.definition.ts +++ b/bots/notionaut/bot.definition.ts @@ -14,9 +14,6 @@ export default new sdk.BotDefinition({ recurringEvents: {}, user: {}, conversation: {}, - __advanced: { - useLegacyZuiTransformer: true, - }, }) .addIntegration(chat, { enabled: true, diff --git a/bots/sheetzy/bot.definition.ts b/bots/sheetzy/bot.definition.ts index 5bf1de5f454..f855bc0faf8 100644 --- a/bots/sheetzy/bot.definition.ts +++ b/bots/sheetzy/bot.definition.ts @@ -18,9 +18,6 @@ export default new sdk.BotDefinition({ }, }, }, - __advanced: { - useLegacyZuiTransformer: true, - }, }) .addIntegration(gsheets, { enabled: true, diff --git a/bots/sinlin/bot.definition.ts b/bots/sinlin/bot.definition.ts index f79d5fce337..b58fdcb13d0 100644 --- a/bots/sinlin/bot.definition.ts +++ b/bots/sinlin/bot.definition.ts @@ -5,11 +5,7 @@ import chat from './bp_modules/chat' import linear from './bp_modules/linear' import synchronizer from './bp_modules/synchronizer' -export default new sdk.BotDefinition({ - __advanced: { - useLegacyZuiTransformer: true, - }, -}) +export default new sdk.BotDefinition({}) .addIntegration(linear, { enabled: true, configurationType: 'apiKey', diff --git a/bots/synchrotron/bot.definition.ts b/bots/synchrotron/bot.definition.ts index 8c17a7bebb5..f168015c857 100644 --- a/bots/synchrotron/bot.definition.ts +++ b/bots/synchrotron/bot.definition.ts @@ -14,9 +14,6 @@ export default new sdk.BotDefinition({ recurringEvents: {}, user: {}, conversation: {}, - __advanced: { - useLegacyZuiTransformer: true, - }, }) .addIntegration(chat, { enabled: true, diff --git a/integrations/sunco/integration.definition.ts b/integrations/sunco/integration.definition.ts index 02f7b5bf21a..05c5f1d8cc1 100644 --- a/integrations/sunco/integration.definition.ts +++ b/integrations/sunco/integration.definition.ts @@ -3,10 +3,11 @@ import { sentry as sentryHelpers } from '@botpress/sdk-addons' import proactiveConversation from 'bp_modules/proactive-conversation' import proactiveUser from 'bp_modules/proactive-user' import typingIndicator from 'bp_modules/typing-indicator' +import { events } from './src/definitions' export default new IntegrationDefinition({ name: 'sunco', - version: '1.0.4', + version: '1.1.1', title: 'Sunshine Conversations', description: 'Give your bot access to a powerful omnichannel messaging platform.', icon: 'icon.svg', @@ -43,7 +44,7 @@ export default new IntegrationDefinition({ }, }, actions: {}, - events: {}, + events, secrets: sentryHelpers.COMMON_SECRET_NAMES, user: { tags: { diff --git a/integrations/sunco/src/definitions/index.ts b/integrations/sunco/src/definitions/index.ts new file mode 100644 index 00000000000..3b9541e8afa --- /dev/null +++ b/integrations/sunco/src/definitions/index.ts @@ -0,0 +1,13 @@ +import { z, IntegrationDefinitionProps } from '@botpress/sdk' + +export const events = { + conversationCreated: { + title: 'Conversation Created', + description: 'This event occurs when a conversation is created', + schema: z.object({ + userId: z.string().title('User ID').describe('The Botpress user ID'), + conversationId: z.string().title('Conversation ID').describe('The Botpress conversation ID'), + }), + ui: {}, + }, +} satisfies IntegrationDefinitionProps['events'] diff --git a/integrations/sunco/src/events/conversation-created.ts b/integrations/sunco/src/events/conversation-created.ts new file mode 100644 index 00000000000..f9fcc5e86a8 --- /dev/null +++ b/integrations/sunco/src/events/conversation-created.ts @@ -0,0 +1,42 @@ +import type { ConversationCreateEvent } from '../messaging-events' +import { Logger, Client } from '.botpress' + +export const executeConversationCreated = async (props: { + event: ConversationCreateEvent + client: Client + logger: Logger +}) => { + const { event, client, logger } = props + const payload = event.payload + + const conversationId = payload.conversation?.id + const userId = payload.user?.id + + if (!conversationId?.length || !userId?.length) { + logger.forBot().warn('conversation:create event missing conversation ID or user ID') + return + } + + const { conversation } = await client.getOrCreateConversation({ + channel: 'channel', + tags: { + id: conversationId, + }, + }) + + const { user } = await client.getOrCreateUser({ + tags: { + id: userId, + }, + }) + + await client.createEvent({ + type: 'conversationCreated', + conversationId: conversation.id, + userId: user.id, + payload: { + userId: user.id, + conversationId: conversation.id, + }, + }) +} diff --git a/integrations/sunco/src/index.ts b/integrations/sunco/src/index.ts index 00dcfae4761..43c28d9f0db 100644 --- a/integrations/sunco/src/index.ts +++ b/integrations/sunco/src/index.ts @@ -1,5 +1,8 @@ import { RuntimeError } from '@botpress/client' import { sentry as sentryHelpers } from '@botpress/sdk-addons' +import { executeConversationCreated } from './events/conversation-created' +import { register, unregister } from './setup' +import { createClient } from './sunshine-api' import * as bp from '.botpress' const SunshineConversationsClient = require('sunshine-conversations-client') @@ -36,8 +39,8 @@ const POSTBACK_PREFIX = 'postback::' const SAY_PREFIX = 'say::' const integration = new bp.Integration({ - register: async () => {}, - unregister: async () => {}, + register, + unregister, actions: { startTypingIndicator: async ({ client, ctx, input }) => { const { conversationId } = input @@ -151,7 +154,7 @@ const integration = new bp.Integration({ }, }, }, - handler: async ({ req, client }) => { + handler: async ({ req, client, logger }) => { if (!req.body) { console.warn('Handler received an empty body') return @@ -160,7 +163,9 @@ const integration = new bp.Integration({ const data = JSON.parse(req.body) for (const event of data.events) { - if (event.type !== 'conversation:message') { + if (event.type === 'conversation:create') { + await executeConversationCreated({ event, client, logger }) + } else if (event.type !== 'conversation:message') { console.warn('Received an event that is not a message') continue } @@ -275,21 +280,6 @@ function getConversationId(conversation: SendMessageProps['conversation']) { return conversationId } -function createClient(keyId: string, keySecret: string) { - const client = new SunshineConversationsClient.ApiClient() - const auth = client.authentications['basicAuth'] - auth.username = keyId - auth.password = keySecret - - return { - messages: new SunshineConversationsClient.MessagesApi(client), - activity: new SunshineConversationsClient.ActivitiesApi(client), - apps: new SunshineConversationsClient.AppsApi(client), - conversations: new SunshineConversationsClient.ConversationsApi(client), - users: new SunshineConversationsClient.UsersApi(client), - } -} - type SendMessageProps = Pick async function sendMessage({ conversation, ctx, ack }: SendMessageProps, payload: any) { @@ -303,7 +293,7 @@ async function sendMessage({ conversation, ctx, ack }: SendMessageProps, payload const { messages } = await client.messages.postMessage(ctx.configuration.appId, getConversationId(conversation), data) - const message = messages[0] + const message = messages?.[0] if (!message) { throw new Error('Message not sent') @@ -327,13 +317,13 @@ async function sendActivity({ client, ctx, conversationId, typingStatus, markAsR const { appId, keyId, keySecret } = ctx.configuration const suncoClient = createClient(keyId, keySecret) if (markAsRead) { - await suncoClient.activity.postActivity(appId, suncoConversationId, { + await suncoClient.activities.postActivity(appId, suncoConversationId, { type: 'conversation:read', author: { type: 'business' }, }) } if (typingStatus) { - await suncoClient.activity.postActivity(appId, suncoConversationId, { + await suncoClient.activities.postActivity(appId, suncoConversationId, { type: `typing:${typingStatus}`, author: { type: 'business' }, }) diff --git a/integrations/sunco/src/messaging-events.ts b/integrations/sunco/src/messaging-events.ts new file mode 100644 index 00000000000..c9680a5be22 --- /dev/null +++ b/integrations/sunco/src/messaging-events.ts @@ -0,0 +1,29 @@ +/** + * Types for Sunshine Conversations webhook events + * Based on the Sunshine Conversations API webhook structure + */ +export type ConversationCreateEvent = { + type: 'conversation:create' + payload: { + conversation?: { + id: string + type?: string + brandId?: string + activeSwitchboardIntegration?: { + id: string + name: string + integrationId: string + integrationType: string + } + } + user?: { + id: string + authenticated?: boolean + } + creationReason?: string + source?: { + type: string + integrationId: string + } + } +} diff --git a/integrations/sunco/src/setup/index.ts b/integrations/sunco/src/setup/index.ts new file mode 100644 index 00000000000..bb8d7eb37e6 --- /dev/null +++ b/integrations/sunco/src/setup/index.ts @@ -0,0 +1,2 @@ +export { register } from './register' +export { unregister } from './unregister' diff --git a/integrations/sunco/src/setup/register.ts b/integrations/sunco/src/setup/register.ts new file mode 100644 index 00000000000..4ad17df76d0 --- /dev/null +++ b/integrations/sunco/src/setup/register.ts @@ -0,0 +1,23 @@ +import { RuntimeError } from '@botpress/client' +import { getNetworkErrorDetails } from 'src/util' +import * as bp from '../../.botpress' +import { createClient } from '../sunshine-api' + +export const register: bp.IntegrationProps['register'] = async ({ ctx, logger }) => { + logger.forBot().info('Starting Sunshine Conversations integration registration...') + + const suncoClient = createClient(ctx.configuration.keyId, ctx.configuration.keySecret) + + logger.forBot().info('Verifying credentials...') + try { + const app = await suncoClient.apps.getApp(ctx.configuration.appId) + logger.forBot().info('✅ Credentials verified successfully. App details:', JSON.stringify(app, null, 2)) + } catch (thrown: unknown) { + const details = getNetworkErrorDetails(thrown) + if (details) { + throw new RuntimeError(`Invalid credentials: ${details?.message}`) + } + const errMsg = thrown instanceof Error ? thrown.message : String(thrown) + throw new RuntimeError(errMsg) + } +} diff --git a/integrations/sunco/src/setup/unregister.ts b/integrations/sunco/src/setup/unregister.ts new file mode 100644 index 00000000000..96104963a7f --- /dev/null +++ b/integrations/sunco/src/setup/unregister.ts @@ -0,0 +1,3 @@ +import * as bp from '../../.botpress' + +export const unregister: bp.IntegrationProps['unregister'] = async ({}) => {} diff --git a/integrations/sunco/src/sunshine-api.ts b/integrations/sunco/src/sunshine-api.ts new file mode 100644 index 00000000000..f05003ca01f --- /dev/null +++ b/integrations/sunco/src/sunshine-api.ts @@ -0,0 +1,663 @@ +// @ts-expect-error No types for sunshine-conversations-client +import * as SunshineConversationsClientModule from 'sunshine-conversations-client' + +export function createClient(keyId: string, keySecret: string) { + const apiClient = new SunshineConversationsApi.ApiClient() + const auth = apiClient.authentications['basicAuth'] + auth.username = keyId + auth.password = keySecret + + return { + activities: new SunshineConversationsApi.ActivitiesApi(apiClient), + apps: new SunshineConversationsApi.AppsApi(apiClient), + users: new SunshineConversationsApi.UsersApi(apiClient), + conversations: new SunshineConversationsApi.ConversationsApi(apiClient), + messages: new SunshineConversationsApi.MessagesApi(apiClient), + webhooks: new SunshineConversationsApi.WebhooksApi(apiClient), + integrations: new SunshineConversationsApi.IntegrationsApi(apiClient), + switchboard: new SunshineConversationsApi.SwitchboardsApi(apiClient), + switchboardActions: new SunshineConversationsApi.SwitchboardActionsApi(apiClient), + switchboardIntegrations: new SunshineConversationsApi.SwitchboardIntegrationsApi(apiClient), + } +} + +// The typings below were generated using AI based on dist from sunshine-conversations-client + +// ============================================================================ +// ApiClient Types +// ============================================================================ + +export type BasicAuth = { + type: 'basic' + username?: string + password?: string +} + +export type BearerAuth = { + type: 'bearer' + accessToken?: string +} + +export type ApiClientAuthentications = { + basicAuth: BasicAuth + bearerAuth: BearerAuth +} + +// This is a very simplified type for the ApiClient +export type ApiClient = { + authentications: ApiClientAuthentications +} + +// ============================================================================ +// AppsApi Types +// ============================================================================ + +export type App = { + id: string + displayName?: string + subdomain?: string + settings?: unknown + metadata?: unknown +} + +export type CreateAppRequest = { + displayName?: string + settings?: unknown + metadata?: unknown +} + +export type UpdateAppRequest = { + displayName?: string + settings?: unknown + metadata?: unknown +} + +export type AppListFilter = { + serviceAccountId?: string +} + +export type AppsApi = { + getApp(appId: string): Promise<{ app: App }> + createApp(appPost: CreateAppRequest): Promise<{ app: App }> + updateApp(appId: string, appUpdate: UpdateAppRequest): Promise<{ app: App }> + deleteApp(appId: string): Promise + listApps(opts?: { page?: Page; filter?: AppListFilter }): Promise<{ apps?: App[] }> +} + +// ============================================================================ +// UsersApi Types +// ============================================================================ + +export type UserProfile = { + givenName?: string + surname?: string + email?: string + avatarUrl?: string +} + +export type User = { + id: string + externalId?: string + profile?: UserProfile +} + +export type CreateUserRequest = { + externalId: string + profile?: UserProfile +} + +export type UpdateUserRequest = { + profile?: UserProfile +} + +export type UsersApi = { + getUser(appId: string, userIdOrExternalId: string): Promise<{ user: User }> + createUser(appId: string, userPost: CreateUserRequest): Promise<{ user: User }> + updateUser(appId: string, userIdOrExternalId: string, userUpdate: UpdateUserRequest): Promise<{ user: User }> + deleteUser(appId: string, userIdOrExternalId: string): Promise + deleteUserPersonalInformation(appId: string, userIdOrExternalId: string): Promise<{ user: User }> +} + +// ============================================================================ +// ConversationsApi Types +// ============================================================================ + +export type Conversation = { + id: string + type?: 'personal' | 'sdkGroup' + displayName?: string + activeSwitchboardIntegration?: { + id: string + name?: string + integrationId?: string + integrationType?: string + } + pendingSwitchboardIntegration?: { + id: string + name?: string + integrationId?: string + integrationType?: string + } +} + +export type ConversationParticipant = { + userId: string + subscribeSDKClient?: boolean +} + +export type CreateConversationRequest = { + type: 'personal' | 'sdkGroup' + displayName?: string + participants: ConversationParticipant[] + metadata?: Record +} + +export type UpdateConversationRequest = { + displayName?: string + metadata?: Record +} + +export type ConversationListFilter = { + userId?: string + userExternalId?: string +} + +export type Page = { + after?: string + before?: string + size?: number +} + +export type ConversationsApi = { + getConversation(appId: string, conversationId: string): Promise<{ conversation: Conversation }> + createConversation( + appId: string, + conversationPost: CreateConversationRequest + ): Promise<{ conversation: Conversation }> + updateConversation( + appId: string, + conversationId: string, + conversationUpdate: UpdateConversationRequest + ): Promise<{ conversation: Conversation }> + deleteConversation(appId: string, conversationId: string): Promise + listConversations( + appId: string, + filter: ConversationListFilter, + opts?: { page?: Page } + ): Promise<{ conversations?: Conversation[] }> +} + +// ============================================================================ +// MessagesApi Types +// ============================================================================ + +export type MessageAuthor = { + type?: 'user' | 'business' + userId?: string + displayName?: string + avatarUrl?: string + subtypes?: string[] +} + +export type TextMessageContent = { + type: 'text' + text: string +} + +export type ImageMessageContent = { + type: 'image' + mediaUrl: string + mediaType?: string + altText?: string +} + +export type FileMessageContent = { + type: 'file' + mediaUrl: string + mediaType?: string + altText?: string +} + +export type LocationMessageContent = { + type: 'location' + coordinates: { + lat: number + long: number + } +} + +export type CarouselMessageContent = { + type: 'carousel' + items: unknown[] +} + +export type ListMessageContent = { + type: 'list' + items: unknown[] +} + +export type MessageContent = + | TextMessageContent + | ImageMessageContent + | FileMessageContent + | LocationMessageContent + | CarouselMessageContent + | ListMessageContent + +export type Message = { + id: string + type?: string + author?: MessageAuthor + content?: MessageContent + received?: string + source?: { + type?: string + originalMessageTimestamp?: string + } + metadata?: Record +} + +export type PostMessageRequest = { + author: MessageAuthor + content: MessageContent + metadata?: Record +} + +export type MessagesApi = { + postMessage(appId: string, conversationId: string, messagePost: PostMessageRequest): Promise<{ messages?: Message[] }> + listMessages(appId: string, conversationId: string, opts?: { page?: Page }): Promise<{ messages?: Message[] }> + deleteMessage(appId: string, conversationId: string, messageId: string): Promise + deleteAllMessages(appId: string, conversationId: string): Promise +} + +// ============================================================================ +// IntegrationsApi Types +// ============================================================================ + +export type Integration = { + id: string + type?: string + status?: 'active' | 'inactive' + displayName?: string + webhooks?: Webhook[] +} + +export type CreateIntegrationRequest = { + type: 'custom' | string + status?: 'active' | 'inactive' + displayName: string + webhooks?: CreateWebhookRequest[] +} + +export type UpdateIntegrationRequest = { + status?: 'active' | 'inactive' + displayName?: string +} + +export type IntegrationListFilter = { + types?: string +} + +export type IntegrationsApi = { + getIntegration(appId: string, integrationId: string): Promise<{ integration: Integration }> + createIntegration(appId: string, integrationPost: CreateIntegrationRequest): Promise<{ integration: Integration }> + updateIntegration( + appId: string, + integrationId: string, + integrationUpdate: UpdateIntegrationRequest + ): Promise<{ integration: Integration }> + deleteIntegration(appId: string, integrationId: string): Promise + listIntegrations( + appId: string, + opts?: { page?: Page; filter?: IntegrationListFilter } + ): Promise<{ integrations?: Integration[] }> +} + +// ============================================================================ +// WebhooksApi Types +// ============================================================================ + +export type Webhook = { + id: string + target: string + triggers?: string[] + includeFullUser?: boolean + includeFullSource?: boolean +} + +export type CreateWebhookRequest = { + target: string + triggers: string[] + includeFullUser?: boolean + includeFullSource?: boolean +} + +export type UpdateWebhookRequest = { + target?: string + triggers?: string[] + includeFullUser?: boolean + includeFullSource?: boolean +} + +export type WebhooksApi = { + getWebhook(appId: string, integrationId: string, webhookId: string): Promise<{ webhook: Webhook }> + createWebhook(appId: string, integrationId: string, webhookPost: CreateWebhookRequest): Promise<{ webhook: Webhook }> + updateWebhook( + appId: string, + integrationId: string, + webhookId: string, + webhookUpdate: UpdateWebhookRequest + ): Promise<{ webhook: Webhook }> + deleteWebhook(appId: string, integrationId: string, webhookId: string): Promise + listWebhooks(appId: string, integrationId: string): Promise<{ webhooks?: Webhook[] }> +} + +// ============================================================================ +// SwitchboardsApi Types +// ============================================================================ + +export type Switchboard = { + id: string + enabled?: boolean + defaultSwitchboardIntegrationId?: string +} + +export type SwitchboardUpdateBody = { + enabled?: boolean + defaultSwitchboardIntegrationId?: string +} + +export type SwitchboardsApi = { + createSwitchboard(appId: string): Promise<{ switchboard: Switchboard }> + updateSwitchboard( + appId: string, + switchboardId: string, + switchboardUpdate: SwitchboardUpdateBody + ): Promise<{ switchboard: Switchboard }> + deleteSwitchboard(appId: string, switchboardId: string): Promise + listSwitchboards(appId: string): Promise<{ switchboards?: Switchboard[] }> +} + +// ============================================================================ +// SwitchboardIntegrationsApi Types +// ============================================================================ + +export type SwitchboardIntegration = { + id: string + name?: string + integrationId?: string + integrationType?: string + deliverStandbyEvents?: boolean +} + +export type CreateSwitchboardIntegrationRequest = { + name: string + integrationId: string + deliverStandbyEvents?: boolean +} + +export type UpdateSwitchboardIntegrationRequest = { + name?: string + deliverStandbyEvents?: boolean +} + +export type SwitchboardIntegrationsApi = { + createSwitchboardIntegration( + appId: string, + switchboardId: string, + switchboardIntegrationPost: CreateSwitchboardIntegrationRequest + ): Promise<{ switchboardIntegration: SwitchboardIntegration }> + updateSwitchboardIntegration( + appId: string, + switchboardId: string, + switchboardIntegrationId: string, + switchboardIntegrationUpdate: UpdateSwitchboardIntegrationRequest + ): Promise<{ switchboardIntegration: SwitchboardIntegration }> + deleteSwitchboardIntegration(appId: string, switchboardId: string, switchboardIntegrationId: string): Promise + listSwitchboardIntegrations( + appId: string, + switchboardId: string + ): Promise<{ switchboardIntegrations?: SwitchboardIntegration[] }> +} + +// ============================================================================ +// SwitchboardActionsApi Types +// ============================================================================ + +export type PassControlBody = { + switchboardIntegration: string + metadata?: Record +} + +export type OfferControlBody = { + switchboardIntegration: string + metadata?: Record +} + +export type AcceptControlBody = { + metadata?: Record +} + +export type SwitchboardActionsApi = { + passControl(appId: string, conversationId: string, passControlBody: PassControlBody): Promise + offerControl(appId: string, conversationId: string, offerControlBody: OfferControlBody): Promise + acceptControl(appId: string, conversationId: string, acceptControlBody: AcceptControlBody): Promise + releaseControl(appId: string, conversationId: string): Promise +} + +// ============================================================================ +// ClientsApi Types +// ============================================================================ + +export type Client = { + id: string + type?: string + status?: 'active' | 'pending' | 'inactive' | 'blocked' + integrationId?: string + displayName?: string + avatarUrl?: string + info?: Record + raw?: Record + lastSeen?: string + linkedAt?: string + externalId?: string +} + +export type ClientMatchCriteria = { + type: string + integrationId?: string + externalId?: string + [key: string]: unknown +} + +export type ClientCreateBody = { + matchCriteria: ClientMatchCriteria + confirmation?: { + type: 'immediate' | 'userActivity' | 'prompt' + message?: { author: MessageAuthor; content: MessageContent } + } + target?: { + conversationId?: string + } +} + +export type ClientsApi = { + createClient(appId: string, userIdOrExternalId: string, clientCreate: ClientCreateBody): Promise<{ client: Client }> + removeClient(appId: string, userIdOrExternalId: string, clientId: string): Promise + listClients(appId: string, userIdOrExternalId: string, opts?: { page?: Page }): Promise<{ clients?: Client[] }> +} + +// ============================================================================ +// ParticipantsApi Types +// ============================================================================ + +export type Participant = { + id: string + oderId?: string + userExternalId?: string + unreadCount?: number + lastRead?: string +} + +export type ParticipantJoinBody = { + userId?: string + userExternalId?: string + subscribeSDKClient?: boolean +} + +export type ParticipantLeaveBody = { + participantId?: string + userId?: string + userExternalId?: string +} + +export type ParticipantsApi = { + joinConversation( + appId: string, + conversationId: string, + participantJoinBody: ParticipantJoinBody + ): Promise<{ participant: Participant }> + leaveConversation(appId: string, conversationId: string, participantLeaveBody: ParticipantLeaveBody): Promise + listParticipants( + appId: string, + conversationId: string, + opts?: { page?: Page } + ): Promise<{ participants?: Participant[] }> +} + +// ============================================================================ +// AttachmentsApi Types +// ============================================================================ + +export type AttachmentSchema = { + mediaUrl?: string + mediaType?: string + mediaSize?: number +} + +export type AttachmentDeleteBody = { + mediaUrl: string +} + +export type AttachmentMediaTokenBody = { + attachmentPaths: string[] +} + +export type AttachmentMediaTokenResponse = { + mediaToken?: string +} + +export type AttachmentsApi = { + uploadAttachment( + appId: string, + access: 'public' | 'private', + source: File | Blob, + opts?: { _for?: string; conversationId?: string } + ): Promise<{ attachment: AttachmentSchema }> + deleteAttachment(appId: string, attachmentDeleteBody: AttachmentDeleteBody): Promise + generateMediaJsonWebToken( + appId: string, + attachmentMediaTokenBody: AttachmentMediaTokenBody + ): Promise + setCookie(appId: string): Promise +} + +// ============================================================================ +// ActivitiesApi Types +// ============================================================================ + +export type ActivityPostType = 'conversation:read' | 'typing:start' | 'typing:stop' + +export type ActivityPost = { + author: { + type: 'user' | 'business' + userId?: string + } + type: ActivityPostType +} + +export type ActivitiesApi = { + postActivity(appId: string, conversationId: string, activityPost: ActivityPost): Promise +} + +// ============================================================================ +// AppKeysApi Types +// ============================================================================ + +export type AppKey = { + id: string + key?: string + secret?: string +} + +export type AppKeysApi = { + createAppKey(appId: string, appKeyPost: { displayName?: string }): Promise<{ key: AppKey }> + deleteAppKey(appId: string, keyId: string): Promise + listAppKeys(appId: string): Promise<{ keys?: AppKey[] }> +} + +// ============================================================================ +// CustomIntegrationApiKeysApi Types +// ============================================================================ + +export type CustomIntegrationApiKey = { + id: string + key?: string + secret?: string +} + +export type CustomIntegrationApiKeysApi = { + createCustomIntegrationApiKey( + appId: string, + integrationId: string, + customIntegrationApiKeyPost: { displayName?: string } + ): Promise<{ key: CustomIntegrationApiKey }> + deleteCustomIntegrationApiKey(appId: string, integrationId: string, keyId: string): Promise + listCustomIntegrationApiKeys(appId: string, integrationId: string): Promise<{ keys?: CustomIntegrationApiKey[] }> +} + +// ============================================================================ +// OAuthEndpointsApi Types +// ============================================================================ + +export type OAuthEndpoint = { + id: string + uri?: string +} + +export type OAuthEndpointsApi = { + createOAuthEndpoint( + appId: string, + integrationId: string, + oAuthEndpointPost: { uri: string } + ): Promise<{ oauthEndpoint: OAuthEndpoint }> + deleteOAuthEndpoint(appId: string, integrationId: string, oauthEndpointId: string): Promise + listOAuthEndpoints(appId: string, integrationId: string): Promise<{ oauthEndpoints?: OAuthEndpoint[] }> +} + +// ============================================================================ +// SunshineConversationsApi Export +// ============================================================================ + +// Helper type to create a constructor that returns an instance implementing the interface +type TypedApiConstructor = new (apiClient: ApiClient) => T + +export const SunshineConversationsApi = SunshineConversationsClientModule as { + ApiClient: new () => ApiClient + ActivitiesApi: TypedApiConstructor + AppKeysApi: TypedApiConstructor + AppsApi: TypedApiConstructor + AttachmentsApi: TypedApiConstructor + ClientsApi: TypedApiConstructor + ConversationsApi: TypedApiConstructor + CustomIntegrationApiKeysApi: TypedApiConstructor + IntegrationsApi: TypedApiConstructor + MessagesApi: TypedApiConstructor + OAuthEndpointsApi: TypedApiConstructor + ParticipantsApi: TypedApiConstructor + SwitchboardActionsApi: TypedApiConstructor + SwitchboardIntegrationsApi: TypedApiConstructor + SwitchboardsApi: TypedApiConstructor + UsersApi: TypedApiConstructor + WebhooksApi: TypedApiConstructor +} diff --git a/integrations/sunco/src/util.ts b/integrations/sunco/src/util.ts new file mode 100644 index 00000000000..2d3ffb75cb8 --- /dev/null +++ b/integrations/sunco/src/util.ts @@ -0,0 +1,72 @@ +export function isNetworkError(error: unknown): error is { + status?: number + body?: any + request?: { + data?: unknown + body?: unknown + } + response?: { + status?: number + text?: string + req: { + method: string + url: string + headers: Record + data?: unknown + body?: unknown + } + header: Record + } +} { + return typeof error === 'object' && error !== null && 'status' in error +} + +export function getNetworkErrorDetails(error: unknown): + | { + message: string + status?: number + data?: unknown + requestBody?: unknown + } + | undefined { + if (typeof error !== 'object' || error === null) { + return undefined + } + + if (!isNetworkError(error)) { + return undefined + } + + // Parse error message from various formats + let message: string | undefined + + // Check for Sunshine Conversations API error format (errors array in body) + if (Array.isArray(error.body?.errors)) { + const errorMessages = (error.body.errors as Array<{ title?: string; code?: string }>) + .map((err) => { + if (err.title) { + return err.code ? `${err.title}: ${err.code}` : err.title + } + return JSON.stringify(err) + }) + .filter((msg): msg is string => msg !== undefined) + + if (errorMessages.length > 0) { + message = errorMessages.join('; ') + } + } else if (error.body?.message?.length) { + message = error.body?.message + } else if (error.body) { + message = JSON.stringify(error.body) + } + + const requestBody = + error.request?.data ?? error.request?.body ?? error.response?.req?.data ?? error.response?.req?.body + + return { + message: message ?? 'Unknown error', + status: error.status ?? error.response?.status, + data: error.body, + requestBody, + } +} diff --git a/integrations/zendesk-messaging-hitl/integration.definition.ts b/integrations/zendesk-messaging-hitl/integration.definition.ts index 244c7ef1b7b..b4bd12b3ee7 100644 --- a/integrations/zendesk-messaging-hitl/integration.definition.ts +++ b/integrations/zendesk-messaging-hitl/integration.definition.ts @@ -4,7 +4,7 @@ import hitl from './bp_modules/hitl' export default new sdk.IntegrationDefinition({ name: 'zendesk-messaging-hitl', - version: '0.1.0', + version: '0.1.1', title: 'Zendesk Messaging HITL', description: 'This integration allows your bot to use Sunshine Conversations (Sunco) as a HITL Provider for Zendesk', icon: 'icon.svg', @@ -53,7 +53,7 @@ export default new sdk.IntegrationDefinition({ description: 'A support request', schema: sdk.z.object({ priority: sdk.z - .enum(['Low', 'Medium', 'High', 'Urgent']) + .enum(['Low', 'Normal', 'High', 'Urgent']) .title('Priority') .describe('Priority of the conversation. Leave empty for default priority.') .optional(), diff --git a/integrations/zendesk-messaging-hitl/src/actions/hitl.ts b/integrations/zendesk-messaging-hitl/src/actions/hitl.ts index 0410e3f48e4..216842e4727 100644 --- a/integrations/zendesk-messaging-hitl/src/actions/hitl.ts +++ b/integrations/zendesk-messaging-hitl/src/actions/hitl.ts @@ -97,7 +97,7 @@ export const stopHitl: bp.IntegrationProps['actions']['stopHitl'] = async ({ inp const suncoClient = getSuncoClient(ctx.configuration) logger.forBot().info(`Releasing control from switchboard for conversation ${suncoConversationId}`) - await suncoClient.switchboardActionsReleaseControl(suncoConversationId, 'ticketClosed') + await suncoClient.switchboardActionsReleaseControl(suncoConversationId) logger.forBot().info(`HITL conversation ${suncoConversationId} stopped and control released`) } catch (thrown: unknown) { const errMsg = thrown instanceof Error ? thrown.message : String(thrown) diff --git a/integrations/zendesk-messaging-hitl/src/client.ts b/integrations/zendesk-messaging-hitl/src/client.ts index 75095b4218e..e91f52e9f02 100644 --- a/integrations/zendesk-messaging-hitl/src/client.ts +++ b/integrations/zendesk-messaging-hitl/src/client.ts @@ -13,6 +13,7 @@ import { type PostMessageRequest, type MessageAuthor, type MessageContent, + Message, } from './sunshine-api' export class SuncoClientError extends RuntimeError { @@ -282,7 +283,7 @@ class SuncoClient { messageParts: Array ) { try { - let message + let message: Message | undefined let author: MessageAuthor if (typeof userIdOrAuthor === 'string') { @@ -363,27 +364,6 @@ class SuncoClient { } } - public async createWebhook(integrationId: string, webhookUrl: string) { - try { - return this._client.webhooks.createIntegrationWebhook(this._appId, integrationId, { - target: webhookUrl, - triggers: ['conversation:message', 'conversation:read'], - includeFullUser: false, - includeFullSource: false, - }) - } catch (thrown: unknown) { - this._handleError(thrown, 'create webhook', { integrationId }) - } - } - - public async deleteWebhook(integrationId: string, webhookId: string) { - try { - await this._client.webhooks.deleteIntegrationWebhook(this._appId, integrationId, webhookId) - } catch (thrown: unknown) { - this._handleError(thrown, 'delete webhook', { integrationId, webhookId }) - } - } - public async deleteIntegration(integrationId: string) { try { await this._client.integrations.deleteIntegration(this._appId, integrationId) @@ -478,15 +458,12 @@ class SuncoClient { } } - public async switchboardActionsReleaseControl(conversationId: string, reason?: string) { + public async switchboardActionsReleaseControl(conversationId: string) { try { - await this._client.switchboardActions.releaseControl(this._appId, conversationId, { - ...(reason && { reason }), - }) + await this._client.switchboardActions.releaseControl(this._appId, conversationId) } catch (thrown: unknown) { this._handleError(thrown, 'release control from switchboard', { conversationId, - reason, }) } } diff --git a/integrations/zendesk-messaging-hitl/src/sunshine-api.ts b/integrations/zendesk-messaging-hitl/src/sunshine-api.ts index 8d345f3ce80..847de0dd044 100644 --- a/integrations/zendesk-messaging-hitl/src/sunshine-api.ts +++ b/integrations/zendesk-messaging-hitl/src/sunshine-api.ts @@ -52,12 +52,16 @@ export type UpdateAppRequest = { metadata?: unknown } +export type AppListFilter = { + serviceAccountId?: string +} + export type AppsApi = { getApp(appId: string): Promise<{ app: App }> createApp(appPost: CreateAppRequest): Promise<{ app: App }> updateApp(appId: string, appUpdate: UpdateAppRequest): Promise<{ app: App }> - deleteApp(appId: string): Promise - listApps(): Promise<{ apps?: App[] }> + deleteApp(appId: string): Promise + listApps(opts?: { page?: Page; filter?: AppListFilter }): Promise<{ apps?: App[] }> } // ============================================================================ @@ -89,9 +93,9 @@ export type UpdateUserRequest = { export type UsersApi = { getUser(appId: string, userIdOrExternalId: string): Promise<{ user: User }> createUser(appId: string, userPost: CreateUserRequest): Promise<{ user: User }> - updateUser(appId: string, userId: string, userUpdate: UpdateUserRequest): Promise<{ user: User }> - deleteUser(appId: string, userId: string): Promise - listUsers(appId: string, page?: number, limit?: number): Promise<{ users?: User[] }> + updateUser(appId: string, userIdOrExternalId: string, userUpdate: UpdateUserRequest): Promise<{ user: User }> + deleteUser(appId: string, userIdOrExternalId: string): Promise + deleteUserPersonalInformation(appId: string, userIdOrExternalId: string): Promise<{ user: User }> } // ============================================================================ @@ -133,6 +137,17 @@ export type UpdateConversationRequest = { metadata?: Record } +export type ConversationListFilter = { + userId?: string + userExternalId?: string +} + +export type Page = { + after?: string + before?: string + size?: number +} + export type ConversationsApi = { getConversation(appId: string, conversationId: string): Promise<{ conversation: Conversation }> createConversation( @@ -144,8 +159,12 @@ export type ConversationsApi = { conversationId: string, conversationUpdate: UpdateConversationRequest ): Promise<{ conversation: Conversation }> - deleteConversation(appId: string, conversationId: string): Promise - listConversations(appId: string, page?: number, limit?: number): Promise<{ conversations?: Conversation[] }> + deleteConversation(appId: string, conversationId: string): Promise + listConversations( + appId: string, + filter: ConversationListFilter, + opts?: { page?: Page } + ): Promise<{ conversations?: Conversation[] }> } // ============================================================================ @@ -226,9 +245,9 @@ export type PostMessageRequest = { export type MessagesApi = { postMessage(appId: string, conversationId: string, messagePost: PostMessageRequest): Promise<{ messages?: Message[] }> - listMessages(appId: string, conversationId: string, page?: number, limit?: number): Promise<{ messages?: Message[] }> - deleteMessage(appId: string, conversationId: string, messageId: string): Promise - deleteAllMessages(appId: string, conversationId: string): Promise + listMessages(appId: string, conversationId: string, opts?: { page?: Page }): Promise<{ messages?: Message[] }> + deleteMessage(appId: string, conversationId: string, messageId: string): Promise + deleteAllMessages(appId: string, conversationId: string): Promise } // ============================================================================ @@ -255,6 +274,10 @@ export type UpdateIntegrationRequest = { displayName?: string } +export type IntegrationListFilter = { + types?: string +} + export type IntegrationsApi = { getIntegration(appId: string, integrationId: string): Promise<{ integration: Integration }> createIntegration(appId: string, integrationPost: CreateIntegrationRequest): Promise<{ integration: Integration }> @@ -263,8 +286,11 @@ export type IntegrationsApi = { integrationId: string, integrationUpdate: UpdateIntegrationRequest ): Promise<{ integration: Integration }> - deleteIntegration(appId: string, integrationId: string): Promise - listIntegrations(appId: string): Promise<{ integrations?: Integration[] }> + deleteIntegration(appId: string, integrationId: string): Promise + listIntegrations( + appId: string, + opts?: { page?: Page; filter?: IntegrationListFilter } + ): Promise<{ integrations?: Integration[] }> } // ============================================================================ @@ -294,20 +320,16 @@ export type UpdateWebhookRequest = { } export type WebhooksApi = { - getIntegrationWebhook(appId: string, integrationId: string, webhookId: string): Promise<{ webhook: Webhook }> - createIntegrationWebhook( - appId: string, - integrationId: string, - webhookPost: CreateWebhookRequest - ): Promise<{ webhook: Webhook }> - updateIntegrationWebhook( + getWebhook(appId: string, integrationId: string, webhookId: string): Promise<{ webhook: Webhook }> + createWebhook(appId: string, integrationId: string, webhookPost: CreateWebhookRequest): Promise<{ webhook: Webhook }> + updateWebhook( appId: string, integrationId: string, webhookId: string, webhookUpdate: UpdateWebhookRequest ): Promise<{ webhook: Webhook }> - deleteIntegrationWebhook(appId: string, integrationId: string, webhookId: string): Promise - listIntegrationWebhooks(appId: string, integrationId: string): Promise<{ webhooks?: Webhook[] }> + deleteWebhook(appId: string, integrationId: string, webhookId: string): Promise + listWebhooks(appId: string, integrationId: string): Promise<{ webhooks?: Webhook[] }> } // ============================================================================ @@ -316,19 +338,23 @@ export type WebhooksApi = { export type Switchboard = { id: string - name?: string - integrationId?: string + enabled?: boolean + defaultSwitchboardIntegrationId?: string +} + +export type SwitchboardUpdateBody = { + enabled?: boolean + defaultSwitchboardIntegrationId?: string } export type SwitchboardsApi = { - getSwitchboard(appId: string, switchboardId: string): Promise<{ switchboard: Switchboard }> - createSwitchboard(appId: string, switchboardPost: { name?: string }): Promise<{ switchboard: Switchboard }> + createSwitchboard(appId: string): Promise<{ switchboard: Switchboard }> updateSwitchboard( appId: string, switchboardId: string, - switchboardUpdate: { name?: string } + switchboardUpdate: SwitchboardUpdateBody ): Promise<{ switchboard: Switchboard }> - deleteSwitchboard(appId: string, switchboardId: string): Promise + deleteSwitchboard(appId: string, switchboardId: string): Promise listSwitchboards(appId: string): Promise<{ switchboards?: Switchboard[] }> } @@ -356,11 +382,6 @@ export type UpdateSwitchboardIntegrationRequest = { } export type SwitchboardIntegrationsApi = { - getSwitchboardIntegration( - appId: string, - switchboardId: string, - switchboardIntegrationId: string - ): Promise<{ switchboardIntegration: SwitchboardIntegration }> createSwitchboardIntegration( appId: string, switchboardId: string, @@ -372,7 +393,7 @@ export type SwitchboardIntegrationsApi = { switchboardIntegrationId: string, switchboardIntegrationUpdate: UpdateSwitchboardIntegrationRequest ): Promise<{ switchboardIntegration: SwitchboardIntegration }> - deleteSwitchboardIntegration(appId: string, switchboardId: string, switchboardIntegrationId: string): Promise + deleteSwitchboardIntegration(appId: string, switchboardId: string, switchboardIntegrationId: string): Promise listSwitchboardIntegrations( appId: string, switchboardId: string @@ -383,24 +404,25 @@ export type SwitchboardIntegrationsApi = { // SwitchboardActionsApi Types // ============================================================================ -export type PassControlRequest = { +export type PassControlBody = { switchboardIntegration: string metadata?: Record } -export type OfferControlRequest = { +export type OfferControlBody = { switchboardIntegration: string metadata?: Record } -export type ReleaseControlRequest = { - reason?: string +export type AcceptControlBody = { + metadata?: Record } export type SwitchboardActionsApi = { - passControl(appId: string, conversationId: string, passControlBody: PassControlRequest): Promise - offerControl(appId: string, conversationId: string, offerControlBody: OfferControlRequest): Promise - releaseControl(appId: string, conversationId: string, releaseControlBody: ReleaseControlRequest): Promise + passControl(appId: string, conversationId: string, passControlBody: PassControlBody): Promise + offerControl(appId: string, conversationId: string, offerControlBody: OfferControlBody): Promise + acceptControl(appId: string, conversationId: string, acceptControlBody: AcceptControlBody): Promise + releaseControl(appId: string, conversationId: string): Promise } // ============================================================================ @@ -410,14 +432,39 @@ export type SwitchboardActionsApi = { export type Client = { id: string type?: string - userId?: string + status?: 'active' | 'pending' | 'inactive' | 'blocked' + integrationId?: string + displayName?: string + avatarUrl?: string + info?: Record + raw?: Record + lastSeen?: string + linkedAt?: string + externalId?: string +} + +export type ClientMatchCriteria = { + type: string + integrationId?: string + externalId?: string + [key: string]: unknown +} + +export type ClientCreateBody = { + matchCriteria: ClientMatchCriteria + confirmation?: { + type: 'immediate' | 'userActivity' | 'prompt' + message?: { author: MessageAuthor; content: MessageContent } + } + target?: { + conversationId?: string + } } export type ClientsApi = { - getClient(appId: string, userId: string, clientId: string): Promise<{ client: Client }> - createClient(appId: string, userId: string, clientPost: { type?: string }): Promise<{ client: Client }> - deleteClient(appId: string, userId: string, clientId: string): Promise - listClients(appId: string, userId: string): Promise<{ clients?: Client[] }> + createClient(appId: string, userIdOrExternalId: string, clientCreate: ClientCreateBody): Promise<{ client: Client }> + removeClient(appId: string, userIdOrExternalId: string, clientId: string): Promise + listClients(appId: string, userIdOrExternalId: string, opts?: { page?: Page }): Promise<{ clients?: Client[] }> } // ============================================================================ @@ -426,60 +473,91 @@ export type ClientsApi = { export type Participant = { id: string + oderId?: string + userExternalId?: string + unreadCount?: number + lastRead?: string +} + +export type ParticipantJoinBody = { userId?: string - joinDate?: string + userExternalId?: string + subscribeSDKClient?: boolean +} + +export type ParticipantLeaveBody = { + participantId?: string + userId?: string + userExternalId?: string } export type ParticipantsApi = { - addParticipant( + joinConversation( appId: string, conversationId: string, - participantPost: { userId: string; subscribeSDKClient?: boolean } + participantJoinBody: ParticipantJoinBody ): Promise<{ participant: Participant }> - removeParticipant(appId: string, conversationId: string, participantId: string): Promise - listParticipants(appId: string, conversationId: string): Promise<{ participants?: Participant[] }> + leaveConversation(appId: string, conversationId: string, participantLeaveBody: ParticipantLeaveBody): Promise + listParticipants( + appId: string, + conversationId: string, + opts?: { page?: Page } + ): Promise<{ participants?: Participant[] }> } // ============================================================================ // AttachmentsApi Types // ============================================================================ -export type Attachment = { - id: string +export type AttachmentSchema = { mediaUrl?: string mediaType?: string mediaSize?: number } +export type AttachmentDeleteBody = { + mediaUrl: string +} + +export type AttachmentMediaTokenBody = { + attachmentPaths: string[] +} + +export type AttachmentMediaTokenResponse = { + mediaToken?: string +} + export type AttachmentsApi = { - getAttachment(appId: string, attachmentId: string): Promise<{ attachment: Attachment }> uploadAttachment( appId: string, - attachmentPost: { source?: string; access?: string } - ): Promise<{ attachment: Attachment }> - deleteAttachment(appId: string, attachmentId: string): Promise + access: 'public' | 'private', + source: File | Blob, + opts?: { _for?: string; conversationId?: string } + ): Promise<{ attachment: AttachmentSchema }> + deleteAttachment(appId: string, attachmentDeleteBody: AttachmentDeleteBody): Promise + generateMediaJsonWebToken( + appId: string, + attachmentMediaTokenBody: AttachmentMediaTokenBody + ): Promise + setCookie(appId: string): Promise } // ============================================================================ // ActivitiesApi Types // ============================================================================ -export type Activity = { - id: string - type?: string - author?: { - type?: string +export type ActivityPostType = 'conversation:read' | 'typing:start' | 'typing:stop' + +export type ActivityPost = { + author: { + type: 'user' | 'business' userId?: string } + type: ActivityPostType } export type ActivitiesApi = { - listActivities( - appId: string, - conversationId: string, - page?: number, - limit?: number - ): Promise<{ activities?: Activity[] }> + postActivity(appId: string, conversationId: string, activityPost: ActivityPost): Promise } // ============================================================================