From c0e5bfd42034f69b5c1d625bc8f8577813c4cb88 Mon Sep 17 00:00:00 2001 From: JustinBordage <38659460+JustinBordage@users.noreply.github.com> Date: Wed, 11 Feb 2026 10:23:57 -0500 Subject: [PATCH 1/3] fix(packages/cli): field title rules to work with non-legacy definitions (#14924) --- integrations/asana/integration.definition.ts | 2 +- integrations/asana/src/definitions/index.ts | 4 ++-- .../bigcommerce-sync/integration.definition.ts | 2 +- .../bigcommerce-sync/src/definitions/index.ts | 12 ++---------- integrations/dalle/integration.definition.ts | 4 ++-- integrations/email/integration.definition.ts | 15 +++++++++++---- integrations/make/integration.definition.ts | 4 ++-- integrations/webhook/integration.definition.ts | 4 +++- packages/cli/src/linter/rulesets/bot.ruleset.ts | 4 ++-- .../src/linter/rulesets/integration.ruleset.ts | 12 ++++++------ .../cli/src/linter/rulesets/interface.ruleset.ts | 8 ++++---- 11 files changed, 36 insertions(+), 35 deletions(-) diff --git a/integrations/asana/integration.definition.ts b/integrations/asana/integration.definition.ts index e62141b796e..ea0deb242b2 100644 --- a/integrations/asana/integration.definition.ts +++ b/integrations/asana/integration.definition.ts @@ -6,7 +6,7 @@ import { configuration, states, user, channels, actions } from './src/definition export default new IntegrationDefinition({ name: INTEGRATION_NAME, - version: '0.3.10', + version: '0.3.11', title: 'Asana', readme: 'hub.md', description: 'Connect your bot to your Asana inbox, create and update tasks, add comments, and locate users.', diff --git a/integrations/asana/src/definitions/index.ts b/integrations/asana/src/definitions/index.ts index 0041c74db18..a610406147c 100644 --- a/integrations/asana/src/definitions/index.ts +++ b/integrations/asana/src/definitions/index.ts @@ -5,8 +5,8 @@ export { channels } from './channels' export const configuration = { schema: z.object({ - apiToken: z.string().min(1).describe('API Token'), - workspaceGid: z.string().min(1).describe('Workspace Global ID'), + apiToken: z.string().min(1).title('API Token').describe('API Token'), + workspaceGid: z.string().min(1).title('Workspace Global ID').describe('Workspace Global ID'), }), } satisfies IntegrationDefinitionProps['configuration'] diff --git a/integrations/bigcommerce-sync/integration.definition.ts b/integrations/bigcommerce-sync/integration.definition.ts index 746279e2c4f..fe632c74683 100644 --- a/integrations/bigcommerce-sync/integration.definition.ts +++ b/integrations/bigcommerce-sync/integration.definition.ts @@ -4,7 +4,7 @@ import { configuration, states, actions } from './src/definitions/index' export default new IntegrationDefinition({ name: 'bigcommerce', title: 'BigCommerce', - version: '3.2.1', + version: '3.2.2', readme: 'hub.md', icon: 'icon.svg', description: 'Sync products from BigCommerce to Botpress', diff --git a/integrations/bigcommerce-sync/src/definitions/index.ts b/integrations/bigcommerce-sync/src/definitions/index.ts index 9735b119466..87ad59d8cda 100644 --- a/integrations/bigcommerce-sync/src/definitions/index.ts +++ b/integrations/bigcommerce-sync/src/definitions/index.ts @@ -2,16 +2,8 @@ import { z, IntegrationDefinitionProps } from '@botpress/sdk' export const configuration = { schema: z.object({ - storeHash: z - .string({ - description: 'Your BigCommerce store hash (e.g., abc123)', - }) - .min(1), - accessToken: z - .string({ - description: 'BigCommerce API Access Token', - }) - .min(1), + storeHash: z.string().min(1).title('Store Hash').describe('Your BigCommerce store hash (e.g., abc123)'), + accessToken: z.string().min(1).title('Access Token').describe('BigCommerce API Access Token'), }), } satisfies IntegrationDefinitionProps['configuration'] diff --git a/integrations/dalle/integration.definition.ts b/integrations/dalle/integration.definition.ts index 7b716bb6f46..b89c5a9e41e 100644 --- a/integrations/dalle/integration.definition.ts +++ b/integrations/dalle/integration.definition.ts @@ -7,14 +7,14 @@ const modelDescription = 'Model to use for image generation. Defaults to "dall-e export default new IntegrationDefinition({ name: 'dalle', - version: '0.3.5', + version: '0.3.6', icon: 'icon.svg', title: 'DALL-E (Deprecated)', description: 'Integrate DALL-E to generate images directly within your chatbot conversations.', readme: 'hub.md', configuration: { schema: z.object({ - apiKey: z.string().describe('Open AI Key'), + apiKey: z.string().title('OpenAI API Key').describe('An API Key for the OpenAI DALL-E service'), }), }, channels: {}, diff --git a/integrations/email/integration.definition.ts b/integrations/email/integration.definition.ts index 55594d32269..4fc593f2cfb 100644 --- a/integrations/email/integration.definition.ts +++ b/integrations/email/integration.definition.ts @@ -11,7 +11,7 @@ const emailSchema = z.object({ export default new IntegrationDefinition({ name: 'email', - version: '0.1.1', + version: '0.1.2', title: 'Email', description: 'Send and receive emails using IMAP and SMTP protocols', readme: 'hub.md', @@ -21,10 +21,17 @@ export default new IntegrationDefinition({ .object({ user: z .string() + .title('Email Address') .describe('The email account you want to use to receive and send messages. Example: example@gmail.com'), - password: z.string().describe('The password to the email account.'), - imapHost: z.string().describe('The imap server you want to connect to. Example: imap.gmail.com'), - smtpHost: z.string().describe('The smtp server you want to connect to. Example: smtp.gmail.com'), + password: z.string().title('Account Password').describe('The password to the email account.'), + imapHost: z + .string() + .title('IMAP Host') + .describe('The imap server you want to connect to. Example: imap.gmail.com'), + smtpHost: z + .string() + .title('SMTP Host') + .describe('The smtp server you want to connect to. Example: smtp.gmail.com'), }) .required(), }, diff --git a/integrations/make/integration.definition.ts b/integrations/make/integration.definition.ts index 67d2dc515c3..8435f76193a 100644 --- a/integrations/make/integration.definition.ts +++ b/integrations/make/integration.definition.ts @@ -4,7 +4,7 @@ const INTEGRATION_NAME = 'make' export default new IntegrationDefinition({ name: INTEGRATION_NAME, - version: '0.3.6', + version: '0.3.7', title: 'Make.com (Deprecated)', icon: 'icon.svg', description: @@ -13,7 +13,7 @@ export default new IntegrationDefinition({ configuration: { schema: z .object({ - webhookUrl: z.string().url().describe('Make.com webhook URL'), + webhookUrl: z.string().url().title('Webhook URL').describe('Make.com webhook URL'), }) .describe('Configuration schema for Make.com Integration'), }, diff --git a/integrations/webhook/integration.definition.ts b/integrations/webhook/integration.definition.ts index 00fe9a0b3f7..79f8fd8de9d 100644 --- a/integrations/webhook/integration.definition.ts +++ b/integrations/webhook/integration.definition.ts @@ -3,7 +3,7 @@ import { sentry as sentryHelpers } from '@botpress/sdk-addons' export default new IntegrationDefinition({ name: 'webhook', - version: '1.1.2', + version: '1.1.3', title: 'Webhook', description: 'Use webhooks to send and receive data from external systems and trigger workflows.', icon: 'icon.svg', @@ -13,12 +13,14 @@ export default new IntegrationDefinition({ secret: z .string() .optional() + .title('Webhook Header Secret') .describe( 'Secret that must be sent with the request as a header called "x-bp-secret." Leave empty to allow all requests without a secret.' ), allowedOrigins: z .array(z.string()) .optional() + .title('Allowed Origins') .describe( 'List of allowed origins for CORS. Leaving this field empty will block all requests originating from a browser and only allow requests from a server.' ), diff --git a/packages/cli/src/linter/rulesets/bot.ruleset.ts b/packages/cli/src/linter/rulesets/bot.ruleset.ts index 3f197a56fc8..0945e698c94 100644 --- a/packages/cli/src/linter/rulesets/bot.ruleset.ts +++ b/packages/cli/src/linter/rulesets/bot.ruleset.ts @@ -44,10 +44,10 @@ export const BOT_RULESET = preprocessRuleset({ message: '{{description}}: {{error}} {{callToAction}} provide a non-empty title by using .title() in its Zod schema', severity: 'error', - given: '$.configuration..schema.properties[*].x-zui', + given: '$.configuration..schema.properties[*]', then: [ { - field: 'title', + field: 'x-zui.title', function: truthyWithMessage({ failMsgMapper: ({ path, isFallback }) => `configuration parameter "${path.at(isFallback ? -5 : -3)}"`, fallbackExtractor: titleFallbackExtractor, diff --git a/packages/cli/src/linter/rulesets/integration.ruleset.ts b/packages/cli/src/linter/rulesets/integration.ruleset.ts index 90d72a7ab8d..f7fec38ee6b 100644 --- a/packages/cli/src/linter/rulesets/integration.ruleset.ts +++ b/packages/cli/src/linter/rulesets/integration.ruleset.ts @@ -82,10 +82,10 @@ export const INTEGRATION_RULESET = preprocessRuleset({ message: '{{description}}: {{error}} {{callToAction}} provide a non-empty title by using .title() in its Zod schema', severity: 'warn', - given: '$.actions[*].output..schema.properties[*].x-zui', + given: '$.actions[*].output..schema.properties[*]', then: [ { - field: 'title', + field: 'x-zui.title', function: truthyWithMessage({ failMsgMapper: ({ path, isFallback }) => `output parameter "${path.at(isFallback ? -5 : -3)}" of action "${path[1]}"`, @@ -174,10 +174,10 @@ export const INTEGRATION_RULESET = preprocessRuleset({ message: '{{description}}: {{error}} {{callToAction}} provide a non-empty title by using .title() in its Zod schema', severity: 'error', - given: '$.configuration..schema.properties[*].x-zui', + given: '$.configuration..schema.properties[*]', then: [ { - field: 'title', + field: 'x-zui.title', function: truthyWithMessage({ failMsgMapper: ({ path, isFallback }) => `configuration parameter "${path.at(isFallback ? -5 : -3)}"`, fallbackExtractor: titleFallbackExtractor, @@ -220,10 +220,10 @@ export const INTEGRATION_RULESET = preprocessRuleset({ message: '{{description}}: {{error}} {{callToAction}} provide a non-empty title by using .title() in its Zod schema', severity: 'error', - given: '$.configurations[*]..schema.properties[*].x-zui', + given: '$.configurations[*]..schema.properties[*]', then: [ { - field: 'title', + field: 'x-zui.title', function: truthyWithMessage({ failMsgMapper: ({ path, isFallback }) => `configuration field "${path.at(isFallback ? -5 : -3)}" of configuration "${path[1]}"`, diff --git a/packages/cli/src/linter/rulesets/interface.ruleset.ts b/packages/cli/src/linter/rulesets/interface.ruleset.ts index fa6c6143766..be5f0fe3e74 100644 --- a/packages/cli/src/linter/rulesets/interface.ruleset.ts +++ b/packages/cli/src/linter/rulesets/interface.ruleset.ts @@ -10,10 +10,10 @@ export const INTERFACE_RULESET = preprocessRuleset({ message: '{{description}}: {{error}} {{callToAction}} provide a non-empty title by using .title() in its Zod schema', severity: 'warn', - given: '$.actions[*].input..schema.properties[*].x-zui', + given: '$.actions[*].input..schema.properties[*]', then: [ { - field: 'title', + field: 'x-zui.title', function: truthyWithMessage({ failMsgMapper: ({ path, isFallback }) => `input parameter "${path.at(isFallback ? -5 : -3)}" of action "${path[1]}"`, @@ -44,10 +44,10 @@ export const INTERFACE_RULESET = preprocessRuleset({ message: '{{description}}: {{error}} {{callToAction}} provide a non-empty title by using .title() in its Zod schema', severity: 'warn', - given: '$.actions[*].output..schema.properties[*].x-zui', + given: '$.actions[*].output..schema.properties[*]', then: [ { - field: 'title', + field: 'x-zui.title', function: truthyWithMessage({ failMsgMapper: ({ path, isFallback }) => `output parameter "${path.at(isFallback ? -5 : -3)}" of action "${path[1]}"`, From d487db599352c840c335457a0d21abc69aecd60c Mon Sep 17 00:00:00 2001 From: Mathieu Faucher <99497774+Math-Fauch@users.noreply.github.com> Date: Wed, 11 Feb 2026 11:13:45 -0500 Subject: [PATCH 2/3] chore(.github): disable zendesk for seagull (#14934) --- .github/workflows/prod-master-version-verification.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/prod-master-version-verification.yml b/.github/workflows/prod-master-version-verification.yml index 128fed6a9b6..93d4d76ced5 100644 --- a/.github/workflows/prod-master-version-verification.yml +++ b/.github/workflows/prod-master-version-verification.yml @@ -17,7 +17,7 @@ jobs: run: pnpm bp login -y --token ${{ secrets.PRODUCTION_TOKEN_CLOUD_OPS_ACCOUNT }} --workspace-id ${{ secrets.PRODUCTION_CLOUD_OPS_WORKSPACE_ID }} - name: Check integration versions run: | - SKIP_INTEGRATIONS=("docusign" "zendesk-messaging-hitl") + SKIP_INTEGRATIONS=("docusign" "zendesk" "zendesk-messaging-hitl") integrations=$(ls -d integrations/*/ | xargs -n1 basename | sort -u) should_fail=0 From f2352160482b82bac33b977daf982eac661e7420 Mon Sep 17 00:00:00 2001 From: Mathieu Faucher <99497774+Math-Fauch@users.noreply.github.com> Date: Wed, 11 Feb 2026 11:37:44 -0500 Subject: [PATCH 3/3] feat(integrations/whatsapp): add read event (#14931) --- .../whatsapp/integration.definition.ts | 7 ++++- .../src/actions/start-conversation.ts | 15 ++++------ .../src/misc/phone-number-to-whatsapp.ts | 28 ++++++++++++++++++- .../whatsapp/src/webhook/handlers/messages.ts | 16 +++++------ .../whatsapp/src/webhook/handlers/status.ts | 18 +++++++++++- 5 files changed, 63 insertions(+), 21 deletions(-) diff --git a/integrations/whatsapp/integration.definition.ts b/integrations/whatsapp/integration.definition.ts index 541ecc6cb87..05396dc76cc 100644 --- a/integrations/whatsapp/integration.definition.ts +++ b/integrations/whatsapp/integration.definition.ts @@ -93,7 +93,7 @@ const defaultBotPhoneNumberId = { } export const INTEGRATION_NAME = 'whatsapp' -export const INTEGRATION_VERSION = '4.7.1' +export const INTEGRATION_VERSION = '4.8.0' export default new IntegrationDefinition({ name: INTEGRATION_NAME, version: INTEGRATION_VERSION, @@ -323,6 +323,11 @@ export default new IntegrationDefinition({ }, }, events: { + messageRead: { + title: 'Message Read', + description: 'Triggered when a user reads a message', + schema: z.object({}), + }, reactionAdded: { title: 'Reaction Added', description: 'Triggered when a user adds a reaction to a message', diff --git a/integrations/whatsapp/src/actions/start-conversation.ts b/integrations/whatsapp/src/actions/start-conversation.ts index 5dcaabeddfd..51867f96a3d 100644 --- a/integrations/whatsapp/src/actions/start-conversation.ts +++ b/integrations/whatsapp/src/actions/start-conversation.ts @@ -1,9 +1,8 @@ -import { isApiError } from '@botpress/client' import { posthogHelper } from '@botpress/common' import { INTEGRATION_NAME, INTEGRATION_VERSION } from 'integration.definition' import { BodyComponent, BodyParameter, Language, Template } from 'whatsapp-api-js/messages' import { getDefaultBotPhoneNumberId, getAuthenticatedWhatsappClient } from '../auth' -import { formatPhoneNumber } from '../misc/phone-number-to-whatsapp' +import { safeFormatPhoneNumber } from '../misc/phone-number-to-whatsapp' import { getTemplateText, parseTemplateVariablesJSON } from '../misc/template-utils' import { TemplateVariables } from '../misc/types' import { hasAtleastOne, logForBotAndThrow } from '../misc/util' @@ -40,11 +39,9 @@ export const startConversation: bp.IntegrationProps['actions']['startConversatio templateVariables = parseTemplateVariablesJSON(templateVariablesJson, logger) } - let formattedUserPhone = userPhone - try { - formattedUserPhone = formatPhoneNumber(userPhone) - } catch (thrown) { - const distinctId = isApiError(thrown) ? thrown.id : undefined + const formatPhoneNumberResponse = safeFormatPhoneNumber(userPhone) + if (formatPhoneNumberResponse.success === false) { + const distinctId = formatPhoneNumberResponse.error.id await posthogHelper.sendPosthogEvent( { distinctId: distinctId ?? 'no id', @@ -56,7 +53,7 @@ export const startConversation: bp.IntegrationProps['actions']['startConversatio }, { integrationName: INTEGRATION_NAME, integrationVersion: INTEGRATION_VERSION, key: bp.secrets.POSTHOG_KEY } ) - const errorMessage = (thrown instanceof Error ? thrown : new Error(String(thrown))).message + const errorMessage = formatPhoneNumberResponse.error.message logForBotAndThrow(`Failed to parse phone number "${userPhone}": ${errorMessage}`, logger) } @@ -64,7 +61,7 @@ export const startConversation: bp.IntegrationProps['actions']['startConversatio channel: 'channel', tags: { botPhoneNumberId, - userPhone: formattedUserPhone, + userPhone: formatPhoneNumberResponse.phoneNumber, }, }) diff --git a/integrations/whatsapp/src/misc/phone-number-to-whatsapp.ts b/integrations/whatsapp/src/misc/phone-number-to-whatsapp.ts index f665f8a2235..da8a04f4dab 100644 --- a/integrations/whatsapp/src/misc/phone-number-to-whatsapp.ts +++ b/integrations/whatsapp/src/misc/phone-number-to-whatsapp.ts @@ -1,4 +1,4 @@ -import { RuntimeError } from '@botpress/client' +import { ApiError, isApiError, RuntimeError } from '@botpress/client' import { parsePhoneNumber, ParsedPhoneNumber } from 'awesome-phonenumber' const ARGENTINA_COUNTRY_CODE = 54 @@ -6,6 +6,32 @@ const ARGENTINA_COUNTRY_CODE_AFTER_PREFIX = 9 const MEXICO_COUNTRY_CODE = 52 const MEXICO_COUNTRY_CODE_AFTER_PREFIX = 1 +type formatPhoneNumberResponse = + | { + success: true + phoneNumber: string + } + | { + success: false + error: ApiError + } + +export function safeFormatPhoneNumber(phoneNumber: string): formatPhoneNumberResponse { + try { + const formattedPhoneNumber = formatPhoneNumber(phoneNumber) + return { + success: true, + phoneNumber: formattedPhoneNumber, + } + } catch (thrown) { + const error = isApiError(thrown) ? thrown : new RuntimeError(String(thrown)) + return { + success: false, + error, + } + } +} + export function formatPhoneNumber(phoneNumber: string) { if (!phoneNumber.startsWith('+')) { // TODO log to use international phone number diff --git a/integrations/whatsapp/src/webhook/handlers/messages.ts b/integrations/whatsapp/src/webhook/handlers/messages.ts index 358a30901e4..f2b32f388d9 100644 --- a/integrations/whatsapp/src/webhook/handlers/messages.ts +++ b/integrations/whatsapp/src/webhook/handlers/messages.ts @@ -1,10 +1,10 @@ -import { RuntimeError, isApiError } from '@botpress/client' +import { RuntimeError } from '@botpress/client' import { posthogHelper } from '@botpress/common' import { ValueOf } from '@botpress/sdk/dist/utils/type-utils' import axios from 'axios' import { INTEGRATION_NAME, INTEGRATION_VERSION } from 'integration.definition' import { getAccessToken, getAuthenticatedWhatsappClient } from '../../auth' -import { formatPhoneNumber } from '../../misc/phone-number-to-whatsapp' +import { safeFormatPhoneNumber } from '../../misc/phone-number-to-whatsapp' import { WhatsAppMessage, WhatsAppMessageValue } from '../../misc/types' import { getMessageFromWhatsappMessageId } from '../../misc/util' import { getMediaInfos } from '../../misc/whatsapp-utils' @@ -39,11 +39,9 @@ async function _handleIncomingMessage( client: bp.Client, logger: bp.Logger ) { - let userPhone = message.from - try { - userPhone = formatPhoneNumber(message.from) - } catch (thrown) { - const distinctId = isApiError(thrown) ? thrown.id : undefined + const formatPhoneNumberResponse = safeFormatPhoneNumber(message.from) + if (formatPhoneNumberResponse.success === false) { + const distinctId = formatPhoneNumberResponse.error.id await posthogHelper.sendPosthogEvent( { distinctId: distinctId ?? 'no id', @@ -55,14 +53,14 @@ async function _handleIncomingMessage( }, { integrationName: INTEGRATION_NAME, integrationVersion: INTEGRATION_VERSION, key: bp.secrets.POSTHOG_KEY } ) - const errorMessage = thrown instanceof Error ? thrown.message : String(thrown) + const errorMessage = formatPhoneNumberResponse.error.message logger.error(`Failed to parse phone number "${message.from}": ${errorMessage}`) } const { conversation } = await client.getOrCreateConversation({ channel: 'channel', tags: { - userPhone, + userPhone: formatPhoneNumberResponse.success ? formatPhoneNumberResponse.phoneNumber : message.from, botPhoneNumberId: value.metadata.phone_number_id, }, }) diff --git a/integrations/whatsapp/src/webhook/handlers/status.ts b/integrations/whatsapp/src/webhook/handlers/status.ts index a91db6ec38e..cd2d486050f 100644 --- a/integrations/whatsapp/src/webhook/handlers/status.ts +++ b/integrations/whatsapp/src/webhook/handlers/status.ts @@ -1,8 +1,24 @@ +import { getMessageFromWhatsappMessageId } from 'src/misc/util' import { WhatsAppStatusValue } from '../../misc/types' import * as bp from '.botpress' export const statusHandler = async (value: WhatsAppStatusValue, props: bp.HandlerProps) => { - const { logger } = props + const { client, logger } = props + + if (value.status === 'read') { + const message = await getMessageFromWhatsappMessageId(value.id, client) + if (!message) { + logger.forBot().error(`No message found for WhatsApp message ID ${value.id}, cannot create messageRead event`) + return + } + + await client.createEvent({ + type: 'messageRead', + conversationId: message.conversationId, + messageId: message.id, + payload: {}, + }) + } if (value.status === 'failed') { const errorDetails =