From 8d2d2ad442c457b712db317459a040348d33d253 Mon Sep 17 00:00:00 2001 From: Mathieu Faucher <99497774+Math-Fauch@users.noreply.github.com> Date: Wed, 10 Sep 2025 14:25:57 -0400 Subject: [PATCH 1/5] fix(zendesk): throw runtime error instead of logger errror (#14220) Co-authored-by: Mathieu Faucher Co-authored-by: Mathieu Faucher --- .../zendesk/integration.definition.ts | 2 +- integrations/zendesk/src/setup.ts | 136 +++++++++++------- 2 files changed, 85 insertions(+), 53 deletions(-) diff --git a/integrations/zendesk/integration.definition.ts b/integrations/zendesk/integration.definition.ts index 7e9f3503afe..09af738aac3 100644 --- a/integrations/zendesk/integration.definition.ts +++ b/integrations/zendesk/integration.definition.ts @@ -7,7 +7,7 @@ import { actions, events, configuration, channels, states, user } from './src/de export default new sdk.IntegrationDefinition({ name: 'zendesk', title: 'Zendesk', - version: '2.8.3', + version: '2.8.4', icon: 'icon.svg', description: 'Optimize your support workflow. Trigger workflows from ticket updates as well as manage tickets, access conversations, and engage with customers.', diff --git a/integrations/zendesk/src/setup.ts b/integrations/zendesk/src/setup.ts index cba0a617052..08441e915a2 100644 --- a/integrations/zendesk/src/setup.ts +++ b/integrations/zendesk/src/setup.ts @@ -1,4 +1,5 @@ import { isApiError } from '@botpress/client' +import * as sdk from '@botpress/sdk' import { getZendeskClient } from './client' import { uploadArticlesToKb } from './misc/upload-articles-to-kb' import { deleteKbArticles } from './misc/utils' @@ -13,32 +14,39 @@ export const register: bp.IntegrationProps['register'] = async ({ client, ctx, w } const zendeskClient = getZendeskClient(ctx.configuration) - const subscriptionId = await zendeskClient.subscribeWebhook(webhookUrl) + const subscriptionId = await zendeskClient + .subscribeWebhook(webhookUrl) + .catch(_throwRuntimeError('Failed to create webhook subscription')) if (!subscriptionId) { - logger.forBot().error('Could not create webhook subscription') - return + throw new sdk.RuntimeError('Failed to create webhook subscription') } - await zendeskClient.createArticleWebhook(webhookUrl, ctx.webhookId) - - const user = await zendeskClient.createOrUpdateUser({ - role: 'end-user', - external_id: ctx.botUserId, - name: 'Botpress', - // FIXME: use a PNG image hosted on the Botpress CDN - remote_photo_url: 'https://app.botpress.dev/favicon/bp.svg', - }) - - await client.updateUser({ - id: ctx.botUserId, - pictureUrl: 'https://app.botpress.dev/favicon/bp.svg', - name: 'Botpress', - tags: { - id: `${user.id}`, - role: 'bot-user', - }, - }) + await zendeskClient + .createArticleWebhook(webhookUrl, ctx.webhookId) + .catch(_throwRuntimeError('Failed to create article webhook')) + + const user = await zendeskClient + .createOrUpdateUser({ + role: 'end-user', + external_id: ctx.botUserId, + name: 'Botpress', + // FIXME: use a PNG image hosted on the Botpress CDN + remote_photo_url: 'https://app.botpress.dev/favicon/bp.svg', + }) + .catch(_throwRuntimeError('Failed to create or update user')) + + await client + .updateUser({ + id: ctx.botUserId, + pictureUrl: 'https://app.botpress.dev/favicon/bp.svg', + name: 'Botpress', + tags: { + id: `${user.id}`, + role: 'bot-user', + }, + }) + .catch(_throwRuntimeError('Failed updating user')) const triggersCreated: string[] = [] @@ -48,23 +56,26 @@ export const register: bp.IntegrationProps['register'] = async ({ client, ctx, w triggersCreated.push(triggerId) } } finally { - await client.setState({ - type: 'integration', - id: ctx.integrationId, - name: 'subscriptionInfo', - payload: { - subscriptionId, - triggerIds: triggersCreated, - }, - }) + await client + .setState({ + type: 'integration', + id: ctx.integrationId, + name: 'subscriptionInfo', + payload: { + subscriptionId, + triggerIds: triggersCreated, + }, + }) + .catch(_throwRuntimeError('Failed setting state')) } if (ctx.configuration.syncKnowledgeBaseWithBot) { if (!ctx.configuration.knowledgeBaseId) { - logger.forBot().error('No KB id provided') - return + throw new sdk.RuntimeError('Knowledge base id was not provided') } - await uploadArticlesToKb({ ctx, client, logger, kbId: ctx.configuration.knowledgeBaseId }) + await uploadArticlesToKb({ ctx, client, logger, kbId: ctx.configuration.knowledgeBaseId }).catch( + _throwRuntimeError('Failed uploading articles to knowledge base') + ) } } @@ -81,8 +92,8 @@ export const unregister: bp.IntegrationProps['unregister'] = async ({ ctx, clien if (isApiError(thrown) && thrown.type === 'ResourceNotFound') { return { state: null } } - logger.forBot().error('Could not get subscription info state', thrown) - throw thrown + const err = thrown instanceof Error ? thrown : new Error(String(thrown)) + throw new sdk.RuntimeError(`Failed to get state : ${err.message}`) }) if (state === null) { @@ -91,27 +102,33 @@ export const unregister: bp.IntegrationProps['unregister'] = async ({ ctx, clien } if (state.payload.subscriptionId?.length) { - await zendeskClient.unsubscribeWebhook(state.payload.subscriptionId).catch((err) => { - logger.forBot().error('Could not unsubscribe webhook', err) - }) + await zendeskClient + .unsubscribeWebhook(state.payload.subscriptionId) + .catch(_logError(logger, 'Failed to unsubscribe webhook')) } if (state.payload.triggerIds?.length) { - for (const trigger of state.payload.triggerIds) { - await zendeskClient.deleteTrigger(trigger).catch((err) => { - logger.forBot().error('Could not delete trigger', err) - }) - } + await Promise.all( + state.payload.triggerIds.map((trigger) => + zendeskClient.deleteTrigger(trigger).catch(_logError(logger, `Failed to delete trigger : ${trigger}`)) + ) + ) } - const articleWebhooks = await zendeskClient.findWebhooks({ - 'filter[name_contains]': `bpc_article_event_${ctx.webhookId}`, - }) - - for (const articleWebhook of articleWebhooks) { - await zendeskClient.deleteWebhook(articleWebhook.id).catch((err) => { - logger.forBot().error('Could not delete article webhook', err) + const articleWebhooks = await zendeskClient + .findWebhooks({ + 'filter[name_contains]': `bpc_article_event_${ctx.webhookId}`, }) + .catch(_logError(logger, 'Failed to find webhooks')) + + if (articleWebhooks) { + await Promise.all( + articleWebhooks.map((articleWebhook) => + zendeskClient + .deleteWebhook(articleWebhook.id) + .catch(_logError(logger, `Failed to delete webhook : ${articleWebhook.name}`)) + ) + ) } if (ctx.configuration.syncKnowledgeBaseWithBot) { @@ -119,6 +136,21 @@ export const unregister: bp.IntegrationProps['unregister'] = async ({ ctx, clien logger.forBot().error('Knowledge base id was not provided') return } - await deleteKbArticles(ctx.configuration.knowledgeBaseId, client) + await deleteKbArticles(ctx.configuration.knowledgeBaseId, client).catch( + _logError(logger, 'Failed to delete knowledge base articles') + ) } } + +const _buildMessage = (outer: string, thrown: unknown) => { + const inner = (thrown instanceof Error ? thrown : new Error(String(thrown))).message + return inner ? `${outer} : ${inner}` : outer +} + +const _throwRuntimeError = (outer: string) => (thrown: unknown) => { + throw new sdk.RuntimeError(_buildMessage(outer, thrown)) +} + +const _logError = (logger: sdk.IntegrationLogger, outer: string) => (thrown: unknown) => { + logger.forBot().error(_buildMessage(outer, thrown)) +} From 7f5c64244da51885d4b636b4b728f628d5f03dac Mon Sep 17 00:00:00 2001 From: Mathieu Faucher <99497774+Math-Fauch@users.noreply.github.com> Date: Wed, 10 Sep 2025 14:36:39 -0400 Subject: [PATCH 2/5] fix(cli): Throw when bot registration failed using dev (#14211) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mathieu Faucher Co-authored-by: François Levasseur --- packages/cli/src/command-implementations/dev-command.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/cli/src/command-implementations/dev-command.ts b/packages/cli/src/command-implementations/dev-command.ts index 14fc0a211f9..575683d86c9 100644 --- a/packages/cli/src/command-implementations/dev-command.ts +++ b/packages/cli/src/command-implementations/dev-command.ts @@ -325,6 +325,15 @@ export class DevCommand extends ProjectCommand { const { bot: updatedBot } = await api.client.updateBot(updateBotBody).catch((thrown) => { throw errors.BotpressCLIError.wrap(thrown, 'Could not deploy dev bot') }) + + const failedIntegrationNames = Object.values(updatedBot.integrations) + .filter((integration) => integration.enabled && integration.status === 'registration_failed') + .map(({ name }) => name) + + if (failedIntegrationNames.length > 0) { + throw new errors.BotpressCLIError(`${failedIntegrationNames.join(', ')} integrations failed to register`) + } + updateLine.success(`Dev Bot deployed with id "${updatedBot.id}" at "${externalUrl}"`) updateLine.commit() From 92e797defc0af35cd5391b8f02eff4de393c9aea Mon Sep 17 00:00:00 2001 From: yxL05 Date: Wed, 10 Sep 2025 15:29:42 -0400 Subject: [PATCH 3/5] fix(integrations/loops): updated integration version so changes from previous fix actually apply (#14226) --- integrations/loops/integration.definition.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integrations/loops/integration.definition.ts b/integrations/loops/integration.definition.ts index 080d8b022e9..20daa8352db 100644 --- a/integrations/loops/integration.definition.ts +++ b/integrations/loops/integration.definition.ts @@ -5,7 +5,7 @@ export default new IntegrationDefinition({ name: 'loops', title: 'Loops', description: 'Handle transactional emails from your chatbot.', - version: '0.1.0', + version: '0.1.1', readme: 'hub.md', icon: 'icon.svg', configuration, From af00ae3e887991c8516c71cf747c7794fdc044f5 Mon Sep 17 00:00:00 2001 From: Nathaniel Girard <72364963+Nathaniel-Girard@users.noreply.github.com> Date: Wed, 10 Sep 2025 16:04:37 -0400 Subject: [PATCH 4/5] fix(plugins/conversation-insights): changed after to before message handlers (#14227) --- plugins/conversation-insights/plugin.definition.ts | 2 +- plugins/conversation-insights/src/index.ts | 6 +++--- plugins/conversation-insights/src/types.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/conversation-insights/plugin.definition.ts b/plugins/conversation-insights/plugin.definition.ts index 31c316da43f..98692979057 100644 --- a/plugins/conversation-insights/plugin.definition.ts +++ b/plugins/conversation-insights/plugin.definition.ts @@ -3,7 +3,7 @@ import llm from './bp_modules/llm' export default new PluginDefinition({ name: 'conversation-insights', - version: '0.3.0', + version: '0.3.1', configuration: { schema: z.object({ modelId: z.string().describe('The AI model id (ex: gpt-4.1-nano-2025-04-14)'), diff --git a/plugins/conversation-insights/src/index.ts b/plugins/conversation-insights/src/index.ts index b4f51d3ee40..05cd3cf43ad 100644 --- a/plugins/conversation-insights/src/index.ts +++ b/plugins/conversation-insights/src/index.ts @@ -10,7 +10,7 @@ const plugin = new bp.Plugin({ actions: {}, }) -plugin.on.afterIncomingMessage('*', async (props) => { +plugin.on.beforeIncomingMessage('*', async (props) => { if (isBrowser) { return } @@ -30,11 +30,11 @@ plugin.on.afterIncomingMessage('*', async (props) => { return undefined }) -plugin.on.afterOutgoingMessage('*', async (props) => { +plugin.on.beforeOutgoingMessage('*', async (props) => { if (isBrowser) { return } - const { conversation } = await props.client.getConversation({ id: props.data.message.conversationId }) + const { conversation } = await props.client.getConversation({ id: props.data.conversationId }) await onNewMessageHandler.onNewMessage({ ...props, conversation }) return undefined }) diff --git a/plugins/conversation-insights/src/types.ts b/plugins/conversation-insights/src/types.ts index 3141bc3f4b4..8f09eabe821 100644 --- a/plugins/conversation-insights/src/types.ts +++ b/plugins/conversation-insights/src/types.ts @@ -2,6 +2,6 @@ import * as bp from '.botpress' // TODO: generate a type for CommonProps in the CLI / SDK export type CommonProps = - | bp.HookHandlerProps['after_incoming_message'] - | bp.HookHandlerProps['after_outgoing_message'] + | bp.HookHandlerProps['before_incoming_message'] + | bp.HookHandlerProps['before_outgoing_message'] | bp.EventHandlerProps From 1eb7048cbb93b30c17cda8d477633033b8bd66ff Mon Sep 17 00:00:00 2001 From: Xavier Hamel <76625630+xavierhamel@users.noreply.github.com> Date: Wed, 10 Sep 2025 16:27:44 -0400 Subject: [PATCH 5/5] feat(integrations/feature-base): implement board/post CRUD (#14222) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Xavier Hamel Co-authored-by: François Levasseur --- .../feature-base/definitions/boards.ts | 62 +++++++ .../feature-base/definitions/posts.ts | 158 ++++++++++++++++++ integrations/feature-base/hub.md | 11 ++ integrations/feature-base/icon.svg | 13 ++ .../feature-base/integration.definition.ts | 25 +++ integrations/feature-base/package.json | 18 ++ integrations/feature-base/src/client.ts | 128 ++++++++++++++ integrations/feature-base/src/index.ts | 44 +++++ integrations/feature-base/tsconfig.json | 8 + pnpm-lock.yaml | 19 +++ 10 files changed, 486 insertions(+) create mode 100644 integrations/feature-base/definitions/boards.ts create mode 100644 integrations/feature-base/definitions/posts.ts create mode 100644 integrations/feature-base/hub.md create mode 100644 integrations/feature-base/icon.svg create mode 100644 integrations/feature-base/integration.definition.ts create mode 100644 integrations/feature-base/package.json create mode 100644 integrations/feature-base/src/client.ts create mode 100644 integrations/feature-base/src/index.ts create mode 100644 integrations/feature-base/tsconfig.json diff --git a/integrations/feature-base/definitions/boards.ts b/integrations/feature-base/definitions/boards.ts new file mode 100644 index 00000000000..55ea61b66be --- /dev/null +++ b/integrations/feature-base/definitions/boards.ts @@ -0,0 +1,62 @@ +import { z } from '@botpress/sdk' +const boardModel = { + id: z.string().title('id').describe('The unique identifier of the board.'), + category: z.string().title('Category').describe('The name of the board/category. Example: "Feature Requests"'), + private: z.boolean().title('Private').optional().describe('Flag indicating whether this board is private.'), + segmentIds: z.array(z.string()).title('Segment Ids').describe('An array of segment IDs associated with this board.'), + roles: z.array(z.string()).title('Roles').describe('An array of roles that have access to this board.'), + hiddenFromRoles: z + .array(z.string()) + .title('Hidden From Roles') + .describe('An array of roles that cannot see this board.'), + disablePostCreation: z + .boolean() + .title('Disable Post Creation') + .optional() + .describe('Flag indicating whether post creation is disabled for this board.'), + disableFollowUpQuestions: z + .boolean() + .title('Disable Follow Up Questions') + .optional() + .describe('Flag indicating whether follow-up questions are disabled for this board.'), + customInputFields: z + .array(z.string()) + .title('Custom Input Fields') + .describe('An array of custom input fields ids that apply to this board.'), + defaultAuthorOnly: z + .boolean() + .title('Default Author Only') + .optional() + .describe('Flag indicating whether posts in this board are visible to the author only by default.'), + defaultCompanyOnly: z + .boolean() + .title('Default Company Only') + .optional() + .describe('Flag indicating whether posts in this board are visible to the company only by default.'), +} + +export const listBoards = { + title: 'List boards', + description: 'List all boards', + input: { + schema: z.object({}), + }, + output: { + schema: z.object({ + results: z.array(z.object(boardModel)).title('Results').describe('An array of boards.'), + }), + }, +} + +export const getBoard = { + title: 'Get a board', + description: 'Get a board by ID', + input: { + schema: z.object({ + id: z.string().optional().title('ID').describe('The unique identifier of the board to retrieve.'), + }), + }, + output: { + schema: z.object(boardModel).title('Board').describe('A single board'), + }, +} diff --git a/integrations/feature-base/definitions/posts.ts b/integrations/feature-base/definitions/posts.ts new file mode 100644 index 00000000000..3d73e77d0c3 --- /dev/null +++ b/integrations/feature-base/definitions/posts.ts @@ -0,0 +1,158 @@ +import { z } from '@botpress/sdk' + +export const createPost = { + title: 'Create a post', + description: 'Create a post on feature base', + input: { + schema: z.object({ + title: z.string().title('Title').describe('The title of the submission. It must be at least 2 characters long.'), + category: z.string().title('Category').describe('The board (a.k.a category) of the submission.'), + content: z + .string() + .title('Content') + .optional() + .describe('The content of the submission. Can be an empty string.'), + email: z + .string() + .title('Email') + .optional() + .describe( + 'The email of the user submitting the post. Will create a new user if the email is not associated with an existing user.' + ), + authorName: z + .string() + .title('Author Name') + .optional() + .describe( + 'Used when you provide an email. If the email is not associated with an existing user, a new user will be created with this name.' + ), + tags: z + .array(z.string()) + .title('Tags') + .optional() + .describe('The tags associated with the submission. Needs to be an array of tag names.'), + commentsAllowed: z + .boolean() + .title('Comments Allowed') + .optional() + .describe('Flag indicating whether comments are allowed on the submission.'), + status: z.string().title('Status').optional().describe('The status of the submission.'), + date: z.date().title('Date').optional().describe('Set the post creation date.'), + }), + }, + output: { + schema: z.object({ + submission: z + .object({ + id: z.string(), + }) + .title('Submission') + .describe('Represent the created post.'), + }), + }, +} + +export const listPosts = { + title: 'List posts', + description: 'List all posts', + input: { + schema: z.object({ + id: z.string().title('ID').optional().describe("Find submission by it's id."), + q: z.string().title('Query').optional().describe('Search for posts by title or content.'), + category: z + .array(z.string()) + .title('Category') + .optional() + .describe('Filter posts by providing an array of category(board) names.'), + status: z.array(z.string()).title('Status').optional().describe('Filter posts by status ids.'), + sortBy: z.string().title('Sort By').optional().describe('Sort posts by a specific attribute.'), + startDate: z.date().title('Start Date').optional().describe('Get posts created after a specific date.'), + endDate: z.date().title('End Date').optional().describe('Get posts created before a specific date.'), + limit: z.number().title('Limit').optional().describe('Number of results per page'), + page: z.number().title('Page').optional().describe('Page number. Starts at 1'), + nextToken: z.string().title('Next Token').optional().describe('Page number. Starts at 1'), + }), + }, + output: { + schema: z.object({ + nextToken: z.string().title('Next Token').optional().describe('Use the token to fetch the next page of posts.'), + results: z + .array( + z.object({ + title: z.string(), + content: z.string(), + author: z.string(), + authorId: z.string(), + organization: z.string(), + postCategory: z.object({ + category: z.string(), + }), + id: z.string(), + }) + ) + .title('Results') + .describe('An array of posts.'), + }), + }, +} + +export const updatePost = { + title: 'Update post', + description: 'Update a post', + input: { + schema: z.object({ + id: z.string().title('ID').describe('The id of the submission.'), + title: z.string().title('Title').optional().describe('The title of the post. Example: "Add dark mode support"'), + content: z + .string() + .title('Content') + .optional() + .describe( + 'The HTML content of the post. Example: "

It would be great to have dark mode support for better viewing at night.

"' + ), + status: z.string().title('Status').optional().describe('The status of the submission. Example: "In Progress"'), + commentsAllowed: z + .boolean() + .title('Comments Allowed') + .optional() + .describe('Flag indicating whether comments are allowed on the submission. Example: true'), + category: z + .string() + .title('Category') + .optional() + .describe('The category of the submission. Example: "💡 Feature Request"'), + sendStatusUpdateEmail: z + .boolean() + .title('Send Status Update Email') + .optional() + .describe('Flag indicating whether to send a status update email to the upvoters. Default: false'), + tags: z + .array(z.string()) + .title('Tags') + .optional() + .describe('The tags of the submission. Example: ["tag1", "tag2"]'), + inReview: z + .boolean() + .title('In Review') + .optional() + .describe('Flag indicating whether the submission is in review. In review posts are not visible to users.'), + date: z.date().title('Date').optional().describe('The post creation date.'), + }), + }, + output: { + schema: z.object({}), + }, +} + +export const deletePost = { + title: 'Delete post', + description: 'Delete a post', + input: { + schema: z.object({ + id: z.string().title('ID').describe('The id of the submission.'), + }), + }, + output: { + schema: z.object({}), + }, +} diff --git a/integrations/feature-base/hub.md b/integrations/feature-base/hub.md new file mode 100644 index 00000000000..1264be947bc --- /dev/null +++ b/integrations/feature-base/hub.md @@ -0,0 +1,11 @@ +# Feature Base integration + +## Description + +This integration allows Botpress bots to interact with Feature Base’s API. By connecting your bot to Feature Base, you can dynamically manage and display content such as posts or boards without leaving the conversation. Whether it’s retrieving live data, adding new entries, or updating existing ones, this integration turns your chatbot into a real-time content manager for Feature Base. + +## Getting started + +### Configuration + +Your API key is needed to configure this integration. How to find your API key is describe in [this documentation](https://docs.featurebase.app/quickstart). diff --git a/integrations/feature-base/icon.svg b/integrations/feature-base/icon.svg new file mode 100644 index 00000000000..3b3cc730465 --- /dev/null +++ b/integrations/feature-base/icon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/integrations/feature-base/integration.definition.ts b/integrations/feature-base/integration.definition.ts new file mode 100644 index 00000000000..50fc28a4513 --- /dev/null +++ b/integrations/feature-base/integration.definition.ts @@ -0,0 +1,25 @@ +import { z, IntegrationDefinition } from '@botpress/sdk' +import { listBoards, getBoard } from 'definitions/boards' +import { listPosts, createPost, deletePost, updatePost } from 'definitions/posts' + +export default new IntegrationDefinition({ + name: 'feature-base', + version: '0.1.0', + title: 'Feature Base', + description: 'CRUD operations for Feature Base', + readme: 'hub.md', + icon: 'icon.svg', + configuration: { + schema: z.object({ + apiKey: z.string().min(1, 'API Key is required').describe('Your Feature Base API Key').title('API Key'), + }), + }, + actions: { + listBoards, + getBoard, + createPost, + listPosts, + deletePost, + updatePost, + }, +}) diff --git a/integrations/feature-base/package.json b/integrations/feature-base/package.json new file mode 100644 index 00000000000..118bb90e8fc --- /dev/null +++ b/integrations/feature-base/package.json @@ -0,0 +1,18 @@ +{ + "name": "@botpresshub/feature-base", + "scripts": { + "build": "bp add -y && bp build", + "check:type": "tsc --noEmit", + "check:bplint": "bp lint" + }, + "private": true, + "dependencies": { + "@botpress/client": "workspace:*", + "@botpress/sdk": "workspace:*", + "axios": "^1.11.0" + }, + "devDependencies": { + "@types/node": "^22.16.4", + "typescript": "^5.6.3" + } +} diff --git a/integrations/feature-base/src/client.ts b/integrations/feature-base/src/client.ts new file mode 100644 index 00000000000..8df2f64a0ce --- /dev/null +++ b/integrations/feature-base/src/client.ts @@ -0,0 +1,128 @@ +import { RuntimeError } from '@botpress/client' +import axios, { Axios, AxiosResponse } from 'axios' +import * as bp from '.botpress' + +type Actions = bp.actions.Actions +type Input = Actions[K]['input'] + +export type ErrorResponse = { + code: number + message: string +} + +type Output = Actions[K]['output'] +type ApiOutput = Output | ErrorResponse + +type PagedApiOutput = + | ErrorResponse + | (Omit, 'nextToken'> & { + page: number + limit: number + totalResults: number + }) + +export class FeatureBaseClient { + private _client: Axios + + public constructor(apiKey: string) { + this._client = axios.create({ + baseURL: 'https://do.featurebase.app', + headers: { + 'X-API-Key': apiKey, + 'Content-Type': 'application/json', + }, + }) + } + + private _unwrapPagedResponse(response: PagedApiOutput): Output { + if ('message' in response) { + throw new RuntimeError(response.message) + } + const { limit, page, totalResults, ...result } = response + let nextToken: string | undefined = undefined + if (limit * page < totalResults) { + nextToken = String(page + 1) + } + return { + ...result, + nextToken, + } + } + + private _parsePagedParams( + params: Input + ): Omit, 'nextToken'> & { page?: number } { + if (!('nextToken' in params)) { + return params + } + let page: number | undefined = undefined + if (params.nextToken && !isNaN(Number(params.nextToken))) { + page = Number(params.nextToken) + } + return { + ...params, + page, + } + } + + private _unwrapResponse(response: ApiOutput): Output { + if ('message' in response) { + throw new RuntimeError(response.message) + } + + return response + } + + private _handleAxiosError(thrown: unknown): never { + if (axios.isAxiosError(thrown)) { + throw new RuntimeError(thrown.response?.data?.message || thrown.message) + } else { + const error = thrown instanceof Error ? thrown : new Error(String(thrown)) + throw new RuntimeError(error.message) + } + } + + public async listBoards(): Promise> { + const response: AxiosResponse> = await this._client + .get('/v2/boards') + .catch(this._handleAxiosError) + return this._unwrapResponse(response.data) + } + + public async getBoard(params: Input<'getBoard'>): Promise> { + const response: AxiosResponse> = await this._client + .get(`/v2/boards/${params.id}`) + .catch(this._handleAxiosError) + return this._unwrapResponse(response.data) + } + + public async listPosts(params: Input<'listPosts'>): Promise> { + const response: AxiosResponse> = await this._client + .get('/v2/posts', { + params: this._parsePagedParams(params), + }) + .catch(this._handleAxiosError) + return this._unwrapPagedResponse(response.data) + } + + public async createPost(params: Input<'createPost'>): Promise> { + const response: AxiosResponse> = await this._client + .post('/v2/posts', params) + .catch(this._handleAxiosError) + return this._unwrapResponse(response.data) + } + + public async deletePost(params: Input<'deletePost'>): Promise> { + const response: AxiosResponse> = await this._client + .delete('/v2/posts', { data: params }) + .catch(this._handleAxiosError) + return this._unwrapResponse(response.data) + } + + public async updatePost(params: Input<'updatePost'>): Promise> { + const response: AxiosResponse> = await this._client + .patch('/v2/posts', params) + .catch(this._handleAxiosError) + return this._unwrapResponse(response.data) + } +} diff --git a/integrations/feature-base/src/index.ts b/integrations/feature-base/src/index.ts new file mode 100644 index 00000000000..c1a9fefd2fc --- /dev/null +++ b/integrations/feature-base/src/index.ts @@ -0,0 +1,44 @@ +import { RuntimeError } from '@botpress/client' +import { FeatureBaseClient } from './client' +import * as bp from '.botpress' + +export default new bp.Integration({ + register: async (props) => { + const client = new FeatureBaseClient(props.ctx.configuration.apiKey) + try { + await client.listBoards() + } catch { + throw new RuntimeError('Failed to register the integration.') + } + }, + unregister: async () => {}, + actions: { + listPosts: async (props) => { + const client = new FeatureBaseClient(props.ctx.configuration.apiKey) + const posts = await client.listPosts(props.input) + return posts + }, + createPost: async (props) => { + const client = new FeatureBaseClient(props.ctx.configuration.apiKey) + return await client.createPost(props.input) + }, + updatePost: async (props) => { + const client = new FeatureBaseClient(props.ctx.configuration.apiKey) + return await client.updatePost(props.input) + }, + deletePost: async (props) => { + const client = new FeatureBaseClient(props.ctx.configuration.apiKey) + return await client.deletePost(props.input) + }, + listBoards: async (props) => { + const client = new FeatureBaseClient(props.ctx.configuration.apiKey) + return await client.listBoards() + }, + getBoard: async (props) => { + const client = new FeatureBaseClient(props.ctx.configuration.apiKey) + return await client.getBoard(props.input) + }, + }, + channels: {}, + handler: async () => {}, +}) diff --git a/integrations/feature-base/tsconfig.json b/integrations/feature-base/tsconfig.json new file mode 100644 index 00000000000..0c1062fd8ce --- /dev/null +++ b/integrations/feature-base/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "outDir": "dist" + }, + "include": [".botpress/**/*", "definitions/**/*", "src/**/*", "*.ts"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9df1146c94e..6c3c63b259e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -708,6 +708,25 @@ importers: specifier: ^6.4.4 version: 6.4.8 + integrations/feature-base: + dependencies: + '@botpress/client': + specifier: workspace:* + version: link:../../packages/client + '@botpress/sdk': + specifier: workspace:* + version: link:../../packages/sdk + axios: + specifier: ^1.11.0 + version: 1.11.0 + devDependencies: + '@types/node': + specifier: ^22.16.4 + version: 22.16.4 + typescript: + specifier: ^5.6.3 + version: 5.8.3 + integrations/fireworks-ai: dependencies: '@botpress/client':