Skip to content

Commit

Permalink
plugin-flow-builder: bot action limit characters (#2817)
Browse files Browse the repository at this point in the history
## Description

When using a Bot Action node with parameters that is connected to a
follow up. It is easy to exceed the character limit allowed by Telegram
(64bytes = 64 characters).

## Context

When connecting a BotActionNode to a button the payload will be
ba|BotActionNodeUUID|source_x

In the pre function of the plugin: first the |source_x is removed, then
if a payload starting with ba| is detected the BotActionNode is obtained
using the botActionNodeUUID and the payload is replaced by the payload
plus the parameters defined in the BotActionNode.

When we get to the bot routes we have the payload with the parameters.

In the action we can continue using the getPayloadParams function as we
did before.

## Testing

Add config for jest and github action
Add test for first interaction 
Add test for bot action node
  • Loading branch information
Iru89 authored May 8, 2024
1 parent e507007 commit 41d561a
Show file tree
Hide file tree
Showing 18 changed files with 620 additions and 31 deletions.
18 changes: 18 additions & 0 deletions .github/workflows/botonic-plugin-flow-builder-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: Botonic plugin flow builder tests

on:
push:
paths:
- 'packages/botonic-plugin-flow-builder/**'
- '.github/workflows/botonic-plugin-flow-builder-tests.yml'
workflow_dispatch:

jobs:
botonic-react-tests:
uses: ./.github/workflows/botonic-common-workflow.yml
secrets: inherit #pragma: allowlist secret
with:
PACKAGE_NAME: Botonic plugin flow builder tests
PACKAGE: botonic-plugin-flow-builder
BUILD_COMMAND: 'cd ../botonic-core && npm run build && cd ../botonic-react && npm run build && cd ../botonic-plugin-flow-builder && npm run build'
NEEDS_CODECOV_UPLOAD: 'yes'
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions packages/botonic-plugin-flow-builder/babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* This babel configuration is used along with Jest for execute tests,
* do not modify to avoid conflicts with webpack.config.js.
*/
// require("@babel/register");

/*
* This babel configuration is used along with Jest for execute tests,
* do not modify to avoid conflicts with webpack.config.js.
*/

module.exports = {
sourceType: 'unambiguous',
// .map files are not generated unless babel invoked with --source-maps
sourceMaps: true,
presets: ['@babel/preset-env', '@babel/preset-react'],
plugins: [
require('@babel/plugin-transform-modules-commonjs'),
require('@babel/plugin-transform-runtime'),
],
}
48 changes: 48 additions & 0 deletions packages/botonic-plugin-flow-builder/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires
const path = require('path')

/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
roots: ['src/', 'tests/'],
transform: {
'^.+\\.tsx?$': [
'ts-jest',
{
tsconfig: '<rootDir>/tsconfig.tests.json',
},
],
'^.+\\.jsx?$': [
'babel-jest',
{ configFile: path.resolve(__dirname, 'babel.config.js') },
],
},
preset: 'ts-jest',
testRegex: '(/tests/.*|(\\.|/)(test|spec))\\.([jt]sx?)$',
testPathIgnorePatterns: [
'dist',
'lib',
'.*.d.ts',
'tests/helpers',
'tests/mocks',
'.*json.*',
'__mocks__',
],
collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}', '!/node_modules/'],
transformIgnorePatterns: [
'node_modules/(?!@botonic|axios|escape-string-regexp).+\\.(js|jsx)$',
],
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
snapshotSerializers: [],
setupFilesAfterEnv: [
// 'jest-expect-message', // to display a message when an assert fails
// 'jest-extended', // more assertions
'<rootDir>/jest.setup.js',
],
modulePaths: ['node_modules', 'src'],
moduleNameMapper: {
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
'@botonic/dx/baseline/tests/__mocks__/file-mock.js',
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
},
testEnvironment: 'node',
}
Empty file.
6 changes: 3 additions & 3 deletions packages/botonic-plugin-flow-builder/package.json
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
{
"name": "@botonic/plugin-flow-builder",
"version": "0.26.1",
"version": "0.26.2-alpha.0",
"main": "./lib/cjs/index.js",
"module": "./lib/esm/index.js",
"description": "Use Flow Builder to show your contents",
"scripts": {
"build": "rm -rf lib && ../../node_modules/.bin/tsc -p tsconfig.json && ../../node_modules/.bin/tsc -p tsconfig.esm.json",
"build:watch": "npm run build -- --watch",
"test": "echo Skipping tests...",
"test": "../../node_modules/.bin/jest --coverage",
"cloc": "../../scripts/qa/cloc-package.sh .",
"prepublishOnly": "rm -rf lib && npm i && npm run build",
"lint": "npm run lint_core -- --fix",
"lint_core": "../../node_modules/.bin/eslint_d --cache --quiet 'src/**/*.ts*'"
},
"dependencies": {
"@botonic/react": "^0.26.0",
"@botonic/react": "^0.26.2-alpha.0",
"axios": "^1.6.8",
"uuid": "^9.0.1"
},
Expand Down
2 changes: 1 addition & 1 deletion packages/botonic-plugin-flow-builder/src/action/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { FlowBuilderApi } from '../api'
import { FlowContent, FlowHandoff } from '../content-fields'
import { HtNodeWithContent } from '../content-fields/hubtype-fields'
import { getFlowBuilderPlugin } from '../helpers'
import { createNodeFromKnowledgeBase } from './knowledge-bases'
import { EventName, trackEvent } from '../tracking'
import { createNodeFromKnowledgeBase } from './knowledge-bases'

export type FlowBuilderActionProps = {
contents: FlowContent[]
Expand Down
11 changes: 7 additions & 4 deletions packages/botonic-plugin-flow-builder/src/api.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { Input, PluginPreRequest } from '@botonic/core'
import axios from 'axios'

import { REG_EXP_PATTERN, SEPARATOR } from './constants'
import {
BOT_ACTION_PAYLOAD_PREFIX,
REG_EXP_PATTERN,
SEPARATOR,
} from './constants'
import {
HtBotActionNode,
HtFallbackNode,
Expand Down Expand Up @@ -209,14 +213,13 @@ export class FlowBuilderApi {
}

if (target.type === HtNodeWithoutContentType.BOT_ACTION) {
const botActionNode = this.getNodeById<HtBotActionNode>(target.id)
return this.createPayloadWithParams(botActionNode)
return `${BOT_ACTION_PAYLOAD_PREFIX}${target.id}`
}

return target.id
}

private createPayloadWithParams(botActionNode: HtBotActionNode): string {
createPayloadWithParams(botActionNode: HtBotActionNode): string {
const payloadId = botActionNode.content.payload_id
const payloadNode = this.getNodeById<HtPayloadNode>(payloadId)
const customParams = JSON.parse(
Expand Down
1 change: 1 addition & 0 deletions packages/botonic-plugin-flow-builder/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export const FLOW_BUILDER_API_URL_PROD =
'https://api.ent0.flowbuilder.prod.hubtype.com'
export const SEPARATOR = '|'
export const SOURCE_INFO_SEPARATOR = `${SEPARATOR}source_`
export const BOT_ACTION_PAYLOAD_PREFIX = `ba${SEPARATOR}`
export const VARIABLE_PATTERN = /{([^}]+)}/g
export const ACCESS_TOKEN_VARIABLE_KEY = '_access_token'
export const REG_EXP_PATTERN = /^\/(.*)\/([gimyus]*)$/
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ export class FlowButton extends ContentFieldsBase {
locale: string,
cmsApi: FlowBuilderApi
): FlowButton {
const payloadId = this.getPayloadId(cmsButton, locale)
const urlId = this.getUrlId(cmsButton, locale)

const newButton = new FlowButton(cmsButton.id)
Expand All @@ -30,11 +29,6 @@ export class FlowButton extends ContentFieldsBase {
newButton.payload = cmsApi.getPayload(cmsButton.target)
}

// OLD PAYLOAD
if (cmsButton.payload && payloadId) {
const payloadNode = cmsApi.getNodeById<HtPayloadNode>(payloadId)
newButton.payload = payloadNode.content.payload
}
if (cmsButton.url && urlId) {
const urlNode = cmsApi.getNodeById<HtUrlNode>(urlId)
newButton.url = urlNode.content.url
Expand All @@ -43,10 +37,6 @@ export class FlowButton extends ContentFieldsBase {
return newButton
}

static getPayloadId(cmsButton: HtButton, locale: string): string | undefined {
return cmsButton.payload.find(payload => payload.locale === locale)?.id
}

static getUrlId(cmsButton: HtButton, locale: string): string | undefined {
return cmsButton.url.find(url => url.locale === locale)?.id
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { HandOffBuilder } from '@botonic/core'
import { ActionRequest, WebchatSettings } from '@botonic/react'
import React from 'react'

import { EventName, trackEvent } from '../tracking'
import { FlowBuilderApi } from '../api'
import { getQueueAvailability } from '../functions/conditional-queue-status'
import { EventName, trackEvent } from '../tracking'
import { ContentFieldsBase } from './content-fields-base'
import { HtHandoffNode, HtPayloadNode, HtQueueLocale } from './hubtype-fields'

Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
import {
HtNodeLink,
HtPayloadLocale,
HtTextLocale,
HtUrlLocale,
} from './common'
import { HtNodeLink, HtTextLocale, HtUrlLocale } from './common'

export interface HtButton {
id: string
text: HtTextLocale[]
url: HtUrlLocale[]
payload: HtPayloadLocale[]
target?: HtNodeLink
hidden: string[]
}
28 changes: 25 additions & 3 deletions packages/botonic-plugin-flow-builder/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ActionRequest } from '@botonic/react'

import { FlowBuilderApi } from './api'
import {
BOT_ACTION_PAYLOAD_PREFIX,
FLOW_BUILDER_API_URL_PROD,
SEPARATOR,
SOURCE_INFO_SEPARATOR,
Expand All @@ -18,6 +19,7 @@ import {
} from './content-fields'
import { FlowWhatsappCtaUrlButtonNode } from './content-fields/flow-whatsapp-cta-url-button'
import {
HtBotActionNode,
HtFlowBuilderData,
HtFunctionArgument,
HtFunctionArguments,
Expand Down Expand Up @@ -91,13 +93,33 @@ export default class BotonicPluginFlowBuilder implements Plugin {
request.input.payload = this.cmsApi.getPayload(nodeByUserInput?.target)
}

this.updateRequestBeforeRoutes(request)
}

private updateRequestBeforeRoutes(request: PluginPreRequest) {
if (request.input.payload) {
request.input.payload = request.input.payload?.split(
SOURCE_INFO_SEPARATOR
)[0]
request.input.payload = this.removeSourceSeparatorFromPayload(
request.input.payload
)

if (request.input.payload.startsWith(BOT_ACTION_PAYLOAD_PREFIX)) {
request.input.payload = this.updateBotActionPayload(
request.input.payload
)
}
}
}

private removeSourceSeparatorFromPayload(payload: string): string {
return payload.split(SOURCE_INFO_SEPARATOR)[0]
}

private updateBotActionPayload(payload: string): string {
const botActionId = payload.split(SEPARATOR)[1]
const botActionNode = this.cmsApi.getNodeById<HtBotActionNode>(botActionId)
return this.cmsApi.createPayloadWithParams(botActionNode)
}

async getContentsByContentID(
contentID: string,
locale: string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ export async function getSmartIntentNodeByInput(
data: { text: request.input.data, intents: intentsInferenceParams },
timeout: 10000,
})
console.log({ response })
return smartIntentNodes.find(
smartIntentNode =>
smartIntentNode.content.title === response.data.intent_name
Expand Down
88 changes: 88 additions & 0 deletions packages/botonic-plugin-flow-builder/tests/bot-action.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { INPUT } from '@botonic/core'
import { describe, test } from '@jest/globals'

import { BOT_ACTION_PAYLOAD_PREFIX } from '../src/constants'
import { FlowText } from '../src/index'
import { ProcessEnvNodeEnvs } from '../src/types'
import { testFlow } from './helpers/flows'
import {
createFlowBuilderPlugin,
createRequest,
getContentsAfterPreAndBotonicInit,
} from './helpers/utils'

describe('Check the contents returned by the plugin', () => {
process.env.NODE_ENV = ProcessEnvNodeEnvs.PRODUCTION
const flowBuilderPlugin = createFlowBuilderPlugin(testFlow)

test('The starting content is displayed on the first interaction', async () => {
const request = createRequest({
input: { data: 'Hola', type: INPUT.TEXT },
isFirstInteraction: true,
plugins: {
// @ts-ignore
flowBuilderPlugin,
},
})

const { contents } = await getContentsAfterPreAndBotonicInit(
request,
flowBuilderPlugin
)

expect((contents[0] as FlowText).text).toBe('Welcome message')
})
})

describe('The user clicks on a button that is connected to a BotActionNode', () => {
process.env.NODE_ENV = ProcessEnvNodeEnvs.PRODUCTION
const flowBuilderPlugin = createFlowBuilderPlugin(testFlow)
const messageUUidWithButtonConectedToBotAction =
'386ba508-a3b3-49a2-94d0-5e239ba63106'
const botActionUuid = '8b0c87c0-77b2-4b05-bae0-3b353240caaa'
test('The button has the payload = ba|botActionUuid', async () => {
const request = createRequest({
input: {
type: INPUT.POSTBACK,
payload: messageUUidWithButtonConectedToBotAction,
},
plugins: {
// @ts-ignore
flowBuilderPlugin,
},
})

const { contents } = await getContentsAfterPreAndBotonicInit(
request,
flowBuilderPlugin
)

const nextPaylod = (contents[0] as FlowText).buttons[0].payload
expect(nextPaylod).toBe(`${BOT_ACTION_PAYLOAD_PREFIX}${botActionUuid}`)
})

test('The bot routes receive the correct payload, in the custom action the payloadParmas defined in the BotActionNode are obtained', async () => {
const request = createRequest({
input: {
type: INPUT.POSTBACK,
payload: `${BOT_ACTION_PAYLOAD_PREFIX}${botActionUuid}`,
},
plugins: {
// @ts-ignore
flowBuilderPlugin,
},
})

await flowBuilderPlugin.pre(request)
expect(request.input.payload).toBe(
'rating|{"value":1,"followUpContentID":"SORRY"}'
)

const payloadParams = flowBuilderPlugin.getPayloadParams(
request.input.payload as string
)
expect(payloadParams).toEqual(
JSON.parse('{"value":1,"followUpContentID":"SORRY"}')
)
})
})
Loading

0 comments on commit 41d561a

Please sign in to comment.