├── src ├── common-regex │ ├── README.md │ └── commonRegex.js ├── count-exception │ ├── README.md │ └── countException.js ├── input-processing │ ├── README.md │ └── inputProcessing.js ├── last-state-with-input │ ├── README.md │ └── lastStateWithInput.js ├── last-bot-with-input │ └── lastBotWithInput.js ├── get-user-channel │ └── getUserChannel.js ├── channel-bold-tags │ └── getChannelBoldTags.js ├── channel-tags │ └── getChannelTags.js └── create-menu │ ├── README.md │ └── createMenu.js ├── .prettierignore ├── .husky ├── pre-commit └── commit-msg ├── commitlint.config.js ├── imgs ├── text_menu.png ├── wpp_list_menu.png ├── wpp_quick_reply.png ├── how_to_use_script.png ├── wpp_list_menu_open.png ├── text_menu_with_sections.png ├── how_to_use_the_variable_menu.png ├── wpp_quick_reply_with_video.png └── wpp_list_menu_open_with_sections.png ├── README.md ├── .prettierrc ├── .editorconfig ├── CHANGELOG.md ├── AUTHORS.md ├── LICENSE.md ├── .eslintrc.json ├── package.json └── .gitignore /src/common-regex/README.md: -------------------------------------------------------------------------------- 1 | # commonRegex 2 | -------------------------------------------------------------------------------- /src/count-exception/README.md: -------------------------------------------------------------------------------- 1 | # countException() 2 | -------------------------------------------------------------------------------- /src/input-processing/README.md: -------------------------------------------------------------------------------- 1 | # createMenu() 2 | -------------------------------------------------------------------------------- /src/last-state-with-input/README.md: -------------------------------------------------------------------------------- 1 | # lastStateWithInput() 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | coverage 4 | .vscode 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /imgs/text_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joaosoaresmatos/blip-scripts/HEAD/imgs/text_menu.png -------------------------------------------------------------------------------- /imgs/wpp_list_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joaosoaresmatos/blip-scripts/HEAD/imgs/wpp_list_menu.png -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit "" 5 | -------------------------------------------------------------------------------- /imgs/wpp_quick_reply.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joaosoaresmatos/blip-scripts/HEAD/imgs/wpp_quick_reply.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # blip-scripts 2 | 3 | This repository is a tool to help the developers of platform blip in some tasks 4 | -------------------------------------------------------------------------------- /imgs/how_to_use_script.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joaosoaresmatos/blip-scripts/HEAD/imgs/how_to_use_script.png -------------------------------------------------------------------------------- /imgs/wpp_list_menu_open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joaosoaresmatos/blip-scripts/HEAD/imgs/wpp_list_menu_open.png -------------------------------------------------------------------------------- /imgs/text_menu_with_sections.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joaosoaresmatos/blip-scripts/HEAD/imgs/text_menu_with_sections.png -------------------------------------------------------------------------------- /imgs/how_to_use_the_variable_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joaosoaresmatos/blip-scripts/HEAD/imgs/how_to_use_the_variable_menu.png -------------------------------------------------------------------------------- /imgs/wpp_quick_reply_with_video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joaosoaresmatos/blip-scripts/HEAD/imgs/wpp_quick_reply_with_video.png -------------------------------------------------------------------------------- /imgs/wpp_list_menu_open_with_sections.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joaosoaresmatos/blip-scripts/HEAD/imgs/wpp_list_menu_open_with_sections.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "singleQuote": true, 4 | "tabWidth": 4, 5 | "semi": true, 6 | "trailingComma": "none", 7 | "endOfLine": "auto" 8 | } 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | indent_style = space 6 | indent_size = 4 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | # Authors 2 | 3 | **Ordered by first contribution.** 4 | 5 | - Joao Soares de Matos Neto ([joaosoaresmatos28@gmail.com](joaosoaresmatos28@gmail.com)) 6 | - Pietro Bondioli ([pietrobondiolideveloper@gmail.com](pietrobondiolideveloper@gmail.com)) 7 | - Raphael de Assis Silva ([contato.raphael.assis@gmail.com](contato.raphael.assis@gmail.com)) 8 | -------------------------------------------------------------------------------- /src/last-bot-with-input/lastBotWithInput.js: -------------------------------------------------------------------------------- 1 | function getLastBotWithInput(lastBotWithInputNew, lastBotWithInputOld) { 2 | let lastBotWithInput = { 3 | name: lastBotWithInputNew, 4 | previousName: null 5 | }; 6 | 7 | try { 8 | let lastBotWithInputOldParsed = JSON.parse(lastBotWithInputOld); 9 | lastBotWithInput.previousName = lastBotWithInputOldParsed.name; 10 | } finally { 11 | return lastBotWithInput; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/count-exception/countException.js: -------------------------------------------------------------------------------- 1 | function getCountException(countExceptionCurrent, lastStateWithInput) { 2 | let countException = 0; 3 | try { 4 | lastStateWithInputParsed = JSON.parse(lastStateWithInput); 5 | if ( 6 | lastStateWithInputParsed.id === 7 | lastStateWithInputParsed.previousId && 8 | lastStateWithInputParsed.name === 9 | lastStateWithInputParsed.previousName 10 | ) { 11 | countException = countExceptionCurrent; 12 | countException++; 13 | } 14 | } finally { 15 | return countException; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/last-state-with-input/lastStateWithInput.js: -------------------------------------------------------------------------------- 1 | function getLastStateWithInput(stateName, stateId, lastStateWithInputCurrent) { 2 | let nowDate = new Date().toLocaleDateString('pt-BR'); 3 | let lastStateWithInput = { 4 | name: stateName, 5 | id: stateId, 6 | date: nowDate, 7 | previousName: null, 8 | previousId: null, 9 | previousDate: null 10 | }; 11 | 12 | try { 13 | let lastStateWithInputCurrentParsed = JSON.parse( 14 | lastStateWithInputCurrent 15 | ); 16 | lastStateWithInput.previousName = lastStateWithInputCurrentParsed.name; 17 | lastStateWithInput.previousId = lastStateWithInputCurrentParsed.id; 18 | lastStateWithInput.previousDate = lastStateWithInputCurrentParsed.date; 19 | } finally { 20 | return lastStateWithInput; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/get-user-channel/getUserChannel.js: -------------------------------------------------------------------------------- 1 | function run(contactIdentity) { 2 | return getUserChannel(contactIdentity); 3 | } 4 | 5 | function getUserChannel(contactIdentity) { 6 | const DEFAULT_CHANNEL = 'default'; 7 | const CHANNEL_INDEX = 1; 8 | 9 | const CHANNEL_IDENTIFIERS = { 10 | 'wa.gw.msging.net': 'whatsapp', 11 | '0mn.io': 'blipchat', 12 | 'take.io': 'takeSMS', 13 | 'messenger.gw.msging.net': 'facebook', 14 | 'instagram.gw.msging.net': 'instagram', 15 | 'abs.gw.msging.net': 'teams', 16 | 'businessmessages.gw.msging.net': 'gbm', 17 | 'skype.gw.msging.net': 'skype', 18 | 'telegram.gw.msging.net': 'telegram', 19 | 'workplace.gw.msging.net': 'workplace', 20 | 'mailgun.gw.msging.net': 'email', 21 | 'pagseguro.gw.msging.net': 'pagseguro' 22 | }; 23 | 24 | let contactChannelId = contactIdentity.split('@')[CHANNEL_INDEX]; 25 | 26 | return CHANNEL_IDENTIFIERS[contactChannelId] || DEFAULT_CHANNEL; 27 | } 28 | 29 | // Just for testing purposes 30 | console.log(run('551140028922@wa.gw.msging.net')); 31 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Joao Soares de Matos Neto 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "jest": true 6 | }, 7 | "extends": ["airbnb-base", "prettier"], 8 | "parserOptions": { 9 | "ecmaVersion": 12, 10 | "sourceType": "module" 11 | }, 12 | "plugins": ["prettier"], 13 | "rules": { 14 | "prettier/prettier": "error", 15 | "indent": ["error", 4, { "SwitchCase": 2 }], 16 | "no-console": "off", 17 | "camelcase": "off", 18 | "class-methods-use-this": "off", 19 | "radix": "off", 20 | "consistent-return": "off", 21 | "no-underscore-dangle": "off", 22 | "no-var": ["error"], 23 | "semi": ["error", "always"], 24 | "comma-dangle": ["error", "never"], 25 | "curly": ["error", "all"], 26 | "no-param-reassign": "off", 27 | "no-use-before-define": "off", 28 | "no-unused-vars": "off", 29 | "no-restricted-syntax": "off", 30 | "guard-for-in": "off", 31 | "prefer-const": "off", 32 | "no-useless-catch": "off", 33 | "no-unsafe-finally": "off", 34 | "no-shadow": "off", 35 | "no-plusplus": "off", 36 | "no-undef": "off" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/channel-bold-tags/getChannelBoldTags.js: -------------------------------------------------------------------------------- 1 | function run(userChannel) { 2 | return getBoldTagsByChannel(userChannel); 3 | } 4 | 5 | function getBoldTagsByChannel(userChannel) { 6 | const BOLD_TAGS = { 7 | empty: { 8 | open: '', 9 | close: '' 10 | }, 11 | default: { 12 | open: '*', 13 | close: '*' 14 | }, 15 | html: { 16 | open: '', 17 | close: '' 18 | }, 19 | markdown: { 20 | open: '**', 21 | close: '**' 22 | } 23 | }; 24 | 25 | const CHANNELS = { 26 | default: BOLD_TAGS.empty, 27 | whatsapp: BOLD_TAGS.default, 28 | blipchat: BOLD_TAGS.html, 29 | facebook: BOLD_TAGS.default, 30 | teams: BOLD_TAGS.markdown, 31 | gbm: BOLD_TAGS.markdown, 32 | telegram: BOLD_TAGS.markdown, 33 | workplace: BOLD_TAGS.default 34 | // takeSMS: BOLD_TAGS.empty, 35 | // instagram: BOLD_TAGS.empty, 36 | // skype: BOLD_TAGS.empty, 37 | // email: BOLD_TAGS.empty, 38 | // pagseguro: BOLD_TAGS.empty, 39 | }; 40 | 41 | return CHANNELS[userChannel] || CHANNELS.default; 42 | } 43 | 44 | // Just for testing purposes 45 | console.log(run('whatsapp')); 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blip-scripts", 3 | "version": "1.0.0", 4 | "description": "This repository is a tool to help the developers of platform blip in some tasks", 5 | "author": "Joao Soares de Matos Neto ", 6 | "contributors": [ 7 | { 8 | "name": "Pietro Bondioli", 9 | "email": "pietrobondiolideveloper@gmail.com", 10 | "url": "http://pietrobondioli.com.br/" 11 | } 12 | ], 13 | "license": "MIT", 14 | "main": "index.js", 15 | "scripts": { 16 | "test": "jest", 17 | "prepare": "husky install" 18 | }, 19 | "lint-staged": { 20 | "**/*.js": "eslint --fix", 21 | "**/*": "prettier --write --ignore-unknown" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/joaosoaresmatos/blip-scripts.git" 26 | }, 27 | "bugs": { 28 | "url": "https://github.com/joaosoaresmatos/blip-scripts/issues" 29 | }, 30 | "homepage": "https://github.com/joaosoaresmatos/blip-scripts#readme", 31 | "devDependencies": { 32 | "@commitlint/cli": "^13.1.0", 33 | "@commitlint/config-conventional": "^13.1.0", 34 | "eslint": "^7.32.0", 35 | "eslint-config-airbnb-base": "^14.2.1", 36 | "eslint-config-prettier": "^8.3.0", 37 | "eslint-plugin-import": "^2.24.2", 38 | "eslint-plugin-prettier": "^3.4.1", 39 | "husky": "^7.0.2", 40 | "jest": "^27.0.6", 41 | "lint-staged": "^11.1.2", 42 | "prettier": "^2.3.2" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Optional REPL history 57 | .node_repl_history 58 | 59 | # Output of 'npm pack' 60 | *.tgz 61 | 62 | # Yarn Integrity file 63 | .yarn-integrity 64 | 65 | # dotenv environment variables file 66 | .env 67 | .env.test 68 | 69 | # parcel-bundler cache (https://parceljs.org/) 70 | .cache 71 | 72 | # next.js build output 73 | .next 74 | 75 | # nuxt.js build output 76 | .nuxt 77 | 78 | # vuepress build output 79 | .vuepress/dist 80 | 81 | # Serverless directories 82 | .serverless/ 83 | 84 | # FuseBox cache 85 | .fusebox/ 86 | 87 | # DynamoDB Local files 88 | .dynamodb/ 89 | -------------------------------------------------------------------------------- /src/common-regex/commonRegex.js: -------------------------------------------------------------------------------- 1 | const commonRegex = [ 2 | { 3 | regex: /\b(n+((o)+)|^n+$)\b/, 4 | value: 'No' 5 | }, 6 | { 7 | regex: /\b((y+(e+(s)+))|^s+$)\b/, 8 | value: 'Yes' 9 | }, 10 | // Phone Validation | Valid Examples: +5531999999999, (31)999999999 11 | { 12 | regex: /\b(?:(?:\+|00)?(55)\s?)?(?:\(?([1-9][0-9])\)?\s?)(?:((?:9\d|[2-9])\d{3})-?(\d{4}))\b/, 13 | value: 'Phone number' 14 | }, 15 | // Email Validation | Valid Examples: name.optional@domain.com 16 | { 17 | regex: /^((?!\.)[\w-_.]*[^.])(@\w+)(\.\w+(\.\w+)?[^.\W])$/, 18 | value: 'Email' 19 | }, 20 | // Name Validation | Valid Examples: joao soares, joão soares, joAO sOares.!! 21 | { 22 | regex: /^(([A-Za-z]+)?[A-Za-z]{2}(\s+([A-Za-z]+)?[A-Za-z]{2})+)$/, 23 | value: 'Name' 24 | }, 25 | // Car plate Validation | Valid Examples: CMG-3164, CMG 3164, qrm7e33, RIO2A18 26 | { 27 | regex: /\b^([a-z]{3}(-?|\s?)[0-9]{4}|[a-z]{3}[0-9][a-z][0-9]{2})$\b/, 28 | value: 'Car plate' 29 | } 30 | ]; 31 | 32 | // //begin name logic 33 | if (selectedMenuOption.value === 'Nome valido') { 34 | selectedMenuOption.inputMatch = capitalizeAll(inputContentOriginal); 35 | selectedMenuOption.inputMatchClean = capitalizeAll( 36 | selectedMenuOption.inputMatchClean 37 | ); 38 | } 39 | // end name logic 40 | // //begin phone logic 41 | if (selectedMenuOption.value === 'Telefone') { 42 | selectedMenuOption.inputMatch = removeWhiteSpace( 43 | selectedMenuOption.inputMatch 44 | ); 45 | selectedMenuOption.inputMatchClean = removeWhiteSpace( 46 | selectedMenuOption.inputMatchClean 47 | ); 48 | } 49 | // end phone logic 50 | -------------------------------------------------------------------------------- /src/channel-tags/getChannelTags.js: -------------------------------------------------------------------------------- 1 | function run(userChannel) { 2 | return getChannelStylingTags(userChannel); 3 | } 4 | 5 | function getChannelStylingTags(userChannel) { 6 | const TAGS = { 7 | bold: { 8 | empty: { 9 | open: '', 10 | close: '' 11 | }, 12 | default: { 13 | open: '*', 14 | close: '*' 15 | }, 16 | html: { 17 | open: '', 18 | close: '' 19 | }, 20 | markdown: { 21 | open: '**', 22 | close: '**' 23 | } 24 | }, 25 | italic: { 26 | empty: { 27 | open: '', 28 | close: '' 29 | }, 30 | default: { 31 | open: '_', 32 | close: '_' 33 | }, 34 | html: { 35 | open: '', 36 | close: '' 37 | }, 38 | markdown: { 39 | open: '_', 40 | close: '_' 41 | } 42 | }, 43 | strikethrough: { 44 | empty: { 45 | open: '', 46 | close: '' 47 | }, 48 | default: { 49 | open: '~', 50 | close: '~' 51 | }, 52 | html: { 53 | open: '', 54 | close: '' 55 | }, 56 | markdown: { 57 | open: '~~', 58 | close: '~~' 59 | } 60 | } 61 | }; 62 | 63 | const CHANNELS = { 64 | default: { 65 | bold: TAGS.bold.empty, 66 | italic: TAGS.italic.empty, 67 | strikethrough: TAGS.strikethrough.empty 68 | }, 69 | whatsapp: { 70 | bold: TAGS.bold.default, 71 | italic: TAGS.italic.default, 72 | strikethrough: TAGS.strikethrough.default 73 | }, 74 | blipchat: { 75 | bold: TAGS.bold.html, 76 | italic: TAGS.italic.html, 77 | strikethrough: TAGS.strikethrough.html 78 | }, 79 | facebook: { 80 | bold: TAGS.bold.default, 81 | italic: TAGS.italic.default, 82 | strikethrough: TAGS.strikethrough.default 83 | }, 84 | teams: { 85 | bold: TAGS.bold.markdown, 86 | italic: TAGS.italic.markdown, 87 | strikethrough: TAGS.strikethrough.markdown 88 | }, 89 | gbm: { 90 | bold: TAGS.bold.markdown, 91 | italic: TAGS.italic.markdown, 92 | strikethrough: TAGS.strikethrough.markdown 93 | }, 94 | telegram: { 95 | bold: TAGS.bold.markdown, 96 | italic: TAGS.italic.markdown, 97 | strikethrough: TAGS.strikethrough.markdown 98 | }, 99 | workplace: { 100 | bold: TAGS.bold.default, 101 | italic: TAGS.italic.default, 102 | strikethrough: TAGS.strikethrough.default 103 | } 104 | // takeSMS: , 105 | // instagram: , 106 | // skype: , 107 | // email: , 108 | // pagseguro: , 109 | }; 110 | 111 | return CHANNELS[userChannel] || CHANNELS.default; 112 | } 113 | 114 | // Just for testing purposes 115 | console.log(run('whatsapp')); 116 | -------------------------------------------------------------------------------- /src/create-menu/README.md: -------------------------------------------------------------------------------- 1 | # createMenu 2 | 3 | This script has some functions to create custom menus for each channel in a practical way and with support for several customizations. 4 | 5 | This script has a main function called `createMenu` which receives the settings passed to the menu generation and returns a `menu` object with the `content` and `type` fields. These two fields must be used in the dynamic content block in the Builder, as illustrated in the image below. 6 | 7 | ![how to use the variable menu example](../../imgs/how_to_use_the_variable_menu.png) 8 | 9 | To use it on the Blip platform, just copy and paste the script into a resource variable on the router bot as a text variable. For this example I stored the script in a variable called `scriptMenu`. Next, add a script as an input action in the block where you want to display the menu and call the `createMenu` function passing the `props` and `config` objects as parameters. 10 | 11 | ![how to use the variable menu example](../../imgs/how_to_use_script.png) 12 | 13 | The props object must contain the properties of the menu to be generated. An object is expected with the fields `userChannel` (string with the name of the user's channel. It must be the same as some key of the menuFields.text field), `channelBoldTags` (Object with the opening and closing tags in bold in the platform informed ), `userLanguage` (language of the menu texts. It must be the same as some language informed in the fields of the menuFields object) and `menuFields` (Object with the fields of the menu to be generated). 14 | 15 | Below is an example of the channelBoldTags object configured for the whatsapp channel. 16 | 17 | ```Javascript 18 | const channelBoldTags = { 19 | open: '*', 20 | close: '*' 21 | } 22 | ``` 23 | 24 | The `config` object is optional and defines the type of menu to be generated. By default, Quick Reply type menus are always generated when possible and text type menus when Quick Reply is not possible. When the generated menu is of the text type, the order of the numbers of the options is ascending. 25 | 26 | Config object structure: 27 | 28 | ```Javascript 29 | const config = { 30 | hasDefaultQuickReply = true, 31 | hasWppQuickReply = true, 32 | hasWppListMenu = false, 33 | isBlipImmediateMenu = true, 34 | orderOptions = 'asc' 35 | } 36 | ``` 37 | 38 | The `orderOptions` field informs the order in which the options indexes will be generated in the text menu. The order can be ascending (asc) or descending (desc). The field `hasWppListMenu` informs that the check list type menu will be generated when the channel is Whatsapp. The `hasWppQuickReply` field informs that the quick reply menu will be generated when the channel is Whatsapp. The `hasDefaultQuickReply` field informs that whenever possible a quick reply menu will be generated. Finally, the `isBlipImmediateMenu` field sets the quick reply menu to disappear after the user selects an option. 39 | 40 | ## Text menu 41 | 42 | Is a menu that is a text message formatted like a menu. This type of menu works in whathever channel and the user answers this message with a direct input by keyboard. 43 | 44 | ![text menu example](../../imgs/text_menu.png) 45 | 46 | For this menu, there are some settings to customize it. It has four fields: 47 | 48 | - Header (optional): a text that appears at the top of the menu. 49 | - Text: a string describing the question / menu options. 50 | - Options: A list of options to be selected by the user. 51 | - Footer (Optional): A text that appears at the bottom of the menu, after the menu options. 52 | 53 | ![text menu with sections example](../../imgs/text_menu_with_sections.png) 54 | 55 | To use this menu, create a structure like the one described in the example below and add this variable to the `props` object. 56 | 57 | ```Javascript 58 | const menuFields = { 59 | text: { 60 | default: { 61 | 'en-US': `This is a text to describe the menu that will be generated to Default channel, the user will ${channelBoldTags.open}choose one of the options bellow${channelBoldTags.close}`, 62 | 'pt-BR': `Este é um texto para descrever o menu que será gerado para o canal Padrão, o usuário ${channelBoldTags.open} escolherá uma das opções abaixo de ${channelBoldTags.close}` 63 | }, 64 | whatsapp: { 65 | 'en-US': `This is a text to describe the menu that will be generated to WhatsApp channel, the user will ${channelBoldTags.open}choose one of the options bellow${channelBoldTags.close}`, 66 | 'pt-BR': `Este é um texto para descrever o menu que será gerado para o canal Whatsapp, o usuário ${channelBoldTags.open} escolherá uma das opções abaixo de ${channelBoldTags.close}` 67 | }, 68 | facebook: { 69 | 'en-US': `This is a text to describe the menu that will be generated to Facebook channel, the user will ${channelBoldTags.open}choose one of the options bellow${channelBoldTags.close}`, 70 | 'pt-BR': `Este é um texto para descrever o menu que será gerado para o canal Facebook, o usuário ${channelBoldTags.open} escolherá uma das opções abaixo de ${channelBoldTags.close}` 71 | }, 72 | telegram: { 73 | 'en-US': `This is a text to describe the menu that will be generated to Telegram channel, the user will ${channelBoldTags.open}choose one of the options bellow${channelBoldTags.close}`, 74 | 'pt-BR': `Este é um texto para descrever o menu que será gerado para o canal Telegram, o usuário ${channelBoldTags.open} escolherá uma das opções abaixo de ${channelBoldTags.close}` 75 | } 76 | }, 77 | options: { 78 | 'en-US': ['Option 1', 'Option 2', 'Option 3'], 79 | 'pt-BR': ['Opção 1', 'Opção 2', 'Opção 3'] 80 | }, 81 | header: { 82 | 'en-US': 83 | 'This is a text to describe the menu (in the top)', 84 | 'pt-BR': 85 | 'Este é um texto para descrever o menu (no topo)' 86 | }, 87 | footer: { 88 | 'en-US': 89 | 'This is a text to describe the menu (in the bottom)', 90 | 'pt-BR': 91 | 'Este é um texto para descrever o menu (na parte inferior)' 92 | } 93 | }; 94 | ``` 95 | 96 | The options field can be passed in two ways, as an array of strings or as a dictionary whose key is a string and the value is an array of strings. In the first case, the generated menu will only have a continuous list with the array options. In the second case the options will be separated by sessions whose session title is the key and the session options are the strings of that key's array of options. In both cases, you can place the special character `\n` at the beginning of the text of one of the options to skip a line, as noted in the first screenshot of this section. 97 | 98 | Example for creating an option menu without sessions: 99 | 100 | ```Javascript 101 | const menuFields = { 102 | ... 103 | options: { 104 | 'pt-BR': ['Opção 1', 'Opção 2', '\nOpção 3'] 105 | }, 106 | ... 107 | } 108 | ``` 109 | 110 | Example for creating an option menu with sessions: 111 | 112 | ```Javascript 113 | const menuFields = { 114 | ... 115 | options: { 116 | 'en-US': { 117 | "Section 1": ['Option 1', 'Option 2', 'Option 3', 'Option 4', 'Option 5'], 118 | "Section 2": ['Option 6', 'Option 7', 'Option 8'] 119 | }, 120 | 'pt-BR': { 121 | "Sessão 1": ['Option 1', 'Option 2', 'Option 3', 'Option 4', 'Option 5'], 122 | "Sessão 2": ['Option 6', 'Option 7', 'Option 8'] 123 | } 124 | }, 125 | ... 126 | } 127 | ``` 128 | 129 | ## Quick Reply Menu 130 | 131 | Is a menu that have iterative buttons that user can click and send a message without interact with keyboard. This type of menu works on Whatsapp, Blipchat, Facebook, Workchat and Telegram. 132 | 133 | ![Whatsapp quick reply example](../../imgs/wpp_quick_reply.png) 134 | 135 | For the Whatsapp channel this menu supported media types like video, image and documents. To use media files in this menu, the header fields in props object shoud be a object media for Whatsapp. You can see more datails in this [link](https://developers.facebook.com/docs/whatsapp/guides/interactive-messages). 136 | 137 | The examples below show how to configure the header field for send media files on whatsapp quick reply menu. 138 | 139 | ```Javascript 140 | # For image 141 | "header": { 142 | "type": "image", 143 | "image": { 144 | "link": "http(s)://the-url" 145 | } 146 | } 147 | 148 | # For video 149 | "header": { 150 | "type": "video", 151 | "video": { 152 | "link": "the-provider-name/protocol://the-url", 153 | } 154 | } 155 | 156 | # For document 157 | "header": { 158 | "type": "document", 159 | "document": { 160 | "link": "the-provider-name/protocol://the-url", 161 | "filename": "some-file-name" 162 | } 163 | } 164 | ``` 165 | 166 | The Quick Reply Menu in Whatsapp channel whith a video is showed in the image bellow. 167 | 168 | ![Whatsapp button menu with video example](../../imgs/wpp_quick_reply_with_video.png) 169 | 170 | The menu fields are detailed in the image below. 171 | 172 | ![Whatsapp menu fields ilustrate](https://scontent.fplu23-1.fna.fbcdn.net/v/t39.8562-6/185178652_167746018610362_8499207846916681529_n.png?_nc_cat=102&ccb=1-5&_nc_sid=6825c5&_nc_ohc=baP0fBE23EMAX_GRyIX&_nc_ht=scontent.fplu23-1.fna&oh=1cfe26553c377919b8aff7f004af59cb&oe=615A4E98) 173 | 174 | ## Whatsapp List menu 175 | 176 | Is a menu that have a check box list. This menu have two states, opened and closed. When is closed the menu have just a text of body and the button to open it. When is open the menu show the options list in a check box and after the user select one option, a button to send the message apear in the end of menu. 177 | 178 | ![Whatsapp list menu fields example](../../imgs/wpp_list_menu.png) 179 | ![Whatsapp list menu fields example](../../imgs/wpp_list_menu_open.png) 180 | 181 | The menu fields are detailed in the image below. 182 | 183 | ![Whatsapp menu fields ilustrate](https://scontent.fplu23-1.fna.fbcdn.net/v/t39.8562-6/183554814_504218921028568_8013384280208209094_n.png?_nc_cat=101&ccb=1-5&_nc_sid=6825c5&_nc_ohc=Hx-a5IH7w44AX_43kmD&_nc_ht=scontent.fplu23-1.fna&oh=ebe5909a912cffd5f1e591b1f8e6af16&oe=6159CEF3) 184 | 185 | You can separate menu options into sessions by sending an object in the options field. As default, the menu have none sections. 186 | 187 | Example: 188 | 189 | ```Javascript 190 | options: { 191 | 'pt-BR': { 192 | "Sessão 1": ['Option 1', 'Option 2'], 193 | "Sessão 2": ['Option 3', 'Option 4'], 194 | "Sessão 3": ['Option 5', 'Option 6'], 195 | "Sessão 4": ['Option 7', 'Option 8'] 196 | } 197 | }, 198 | ``` 199 | 200 | ![Whatsapp list menu fields example](../../imgs/wpp_list_menu_open_with_sections.png) 201 | -------------------------------------------------------------------------------- /src/input-processing/inputProcessing.js: -------------------------------------------------------------------------------- 1 | // Assert [REQUIREMENTS] variables. 2 | 3 | let testInput = 'apple'; 4 | console.log(run(testInput, 'text/plain', 'en-US')); 5 | 6 | // Code of builder here 7 | 8 | function run(inputContent, inputType, userLanguage) { 9 | return getSelectedMenuOption(inputContent, inputType, userLanguage); 10 | } 11 | 12 | function getSelectedMenuOption(inputContent, inputType, userLanguage) { 13 | let options = [ 14 | { 15 | regex: { 16 | 'en-US': /^(apple)$/, 17 | 'pt-BR': /^(maça)$/ 18 | }, 19 | value: 'apple' 20 | }, 21 | { 22 | regex: { 23 | 'en-US': /^(pineapple)$/, 24 | 'pt-BR': /^(abacaxi)$/ 25 | }, 26 | value: 'pineapple' 27 | }, 28 | { 29 | regex: { 30 | 'en-US': /^(strawberry)$/, 31 | 'pt-BR': /^(morango)$/ 32 | }, 33 | value: 'strawberry' 34 | } 35 | ]; 36 | let props = { 37 | input: inputContent, 38 | inputType, 39 | options, 40 | userLanguage 41 | }; 42 | let config = { 43 | isNumberMenu: true, 44 | isReversed: false, 45 | shouldRemoveSpecialCharacters: true, 46 | shouldRemoveWhiteSpaces: false 47 | }; 48 | let selectedMenuOption = validateInputOptions(props, config); 49 | 50 | return selectedMenuOption; 51 | } 52 | 53 | // Below are all scripts used to process inputs 54 | // It should be put in the router resources in order to be used by the above script 55 | 56 | function validateInputOptions( 57 | { 58 | input = null, 59 | inputType = null, 60 | options = null, 61 | userLanguage = 'pt-BR' 62 | } = {}, 63 | { 64 | isNumberMenu = false, 65 | isReversed = false, 66 | shouldRemoveSpecialCharacters = true, 67 | shouldRemoveWhiteSpaces = true 68 | } = {} 69 | ) { 70 | let props = { 71 | input, 72 | inputType, 73 | options, 74 | userLanguage 75 | }; 76 | let config = { 77 | isNumberMenu, 78 | isReversed, 79 | shouldRemoveSpecialCharacters, 80 | shouldRemoveWhiteSpaces 81 | }; 82 | 83 | const UNEXPECTED_INPUT = 'Input inesperado'; 84 | let inputInfo = { 85 | input, 86 | value: UNEXPECTED_INPUT, 87 | tracking: UNEXPECTED_INPUT, 88 | inputMatch: UNEXPECTED_INPUT, 89 | inputMatchClean: UNEXPECTED_INPUT, 90 | chosenOptionNumber: UNEXPECTED_INPUT 91 | }; 92 | if (isInvalidType(inputType)) { 93 | return inputInfo; 94 | } 95 | 96 | try { 97 | props = normalizeProps(props); 98 | let { options, userLanguage } = props; 99 | 100 | input = config.shouldRemoveWhiteSpaces 101 | ? removeExcessWhiteSpace(input) 102 | : input; 103 | let inputCleaned = config.shouldRemoveSpecialCharacters 104 | ? removeSpecialCharacters(input, config) 105 | : input; 106 | 107 | for (let option in options) { 108 | let matching = new RegExp(options[option].regex, 'gi'); 109 | let numberOption = parseInt(option) + 1; 110 | let matchArray = null; 111 | if (config.isReversed) { 112 | numberOption = options.length - parseInt(option); 113 | } 114 | if (config.isNumberMenu) { 115 | let matchingNumber = new RegExp( 116 | getNumberWrittenRegex(numberOption, userLanguage), 117 | 'gi' 118 | ); 119 | matchArray = matchingNumber.exec(input); 120 | 121 | if (!matchArray) { 122 | matchArray = matchingNumber.exec(inputCleaned); 123 | } 124 | } 125 | if (!matchArray) { 126 | matchArray = matching.exec(input); 127 | } 128 | if (!matchArray) { 129 | matchArray = matching.exec(inputCleaned); 130 | } 131 | if (matchArray) { 132 | inputInfo.value = options[option].value; 133 | inputInfo.chosenOptionNumber = parseInt(option) + 1; 134 | if (options[option].tracking) { 135 | inputInfo.tracking = options[option].tracking; 136 | } else { 137 | inputInfo.tracking = getCleanedInputToTracking( 138 | options[option].value, 139 | config 140 | ); 141 | } 142 | inputInfo.inputMatch = matchArray.shift(); 143 | inputInfo.inputMatchClean = removeSpecialCharacters( 144 | inputInfo.inputMatch, 145 | config 146 | ); 147 | break; 148 | } 149 | } 150 | } catch (exception) { 151 | throw exception; 152 | } finally { 153 | return inputInfo; 154 | } 155 | } 156 | 157 | function isInvalidType(inputType) { 158 | const validType = 'text/plain'; 159 | return inputType !== validType; 160 | } 161 | 162 | function normalizeProps(props) { 163 | let options = normalizeOptions(props); 164 | props.options = options; 165 | return props; 166 | } 167 | 168 | function normalizeOptions(props) { 169 | let { options, userLanguage } = props; 170 | 171 | return options.map((option) => { 172 | let obj = {}; 173 | if (option.regex[userLanguage]) { 174 | obj.regex = option.regex[userLanguage]; 175 | } else { 176 | obj.regex = option.regex; 177 | } 178 | obj.value = option.value; 179 | return obj; 180 | }); 181 | } 182 | 183 | function capitalizeAll(text) { 184 | text = removeExcessWhiteSpace(text); 185 | const SPACE_STR = ' '; 186 | let loweredText = text.toLowerCase(); 187 | let words = loweredText.split(SPACE_STR); 188 | for (let word in words) { 189 | let capitalizedWord = words[word]; 190 | let firstLetter = capitalizedWord[0]; 191 | capitalizedWord = firstLetter.toUpperCase() + capitalizedWord.slice(1); 192 | words[word] = capitalizedWord; 193 | } 194 | return words.join(SPACE_STR); 195 | } 196 | 197 | function capitalizeFirst(text) { 198 | text = removeExcessWhiteSpace(text); 199 | let loweredText = text.toLowerCase(); 200 | let firstLetter = loweredText[0]; 201 | let capitalizedText = firstLetter.toUpperCase() + loweredText.slice(1); 202 | return capitalizedText; 203 | } 204 | 205 | function removeWhiteSpace(input) { 206 | input = input.trim(); 207 | const EMPTY_STR = ''; 208 | const WHITE_SPACES = RegExp('(\\s+)', 'gi'); 209 | return input.replace(WHITE_SPACES, EMPTY_STR); 210 | } 211 | 212 | function removeExcessWhiteSpace(input) { 213 | input = input.trim(); 214 | const SPACE_STR = ' '; 215 | const WHITE_SPACES = RegExp('(\\s+)', 'gi'); 216 | return input.replace(WHITE_SPACES, SPACE_STR); 217 | } 218 | 219 | function removeSpecialCharacters(input, config) { 220 | const EMPTY_STR = ''; 221 | const SPECIAL_CHAR = RegExp('[^\\w\\s]*', 'gi'); 222 | input = replaceSpecialLetters(input); 223 | input = input.replace(SPECIAL_CHAR, EMPTY_STR); 224 | input = config.shouldRemoveWhiteSpaces 225 | ? removeExcessWhiteSpace(input) 226 | : input; 227 | return input; 228 | } 229 | 230 | function replaceSpecialLetters(input) { 231 | const specialCharToCommonChar = { 232 | á: 'a', 233 | à: 'a', 234 | â: 'a', 235 | ä: 'a', 236 | ã: 'a', 237 | é: 'e', 238 | è: 'e', 239 | ê: 'e', 240 | ë: 'e', 241 | í: 'i', 242 | ì: 'i', 243 | î: 'i', 244 | ï: 'i', 245 | ó: 'o', 246 | ò: 'o', 247 | ô: 'o', 248 | õ: 'o', 249 | ö: 'o', 250 | ù: 'u', 251 | ú: 'u', 252 | û: 'u', 253 | ü: 'u', 254 | ñ: 'n', 255 | ç: 'c' 256 | }; 257 | for (const key in specialCharToCommonChar) { 258 | let keyRegex = new RegExp(`${key}`, 'gi'); 259 | input = input.replace(keyRegex, specialCharToCommonChar[key]); 260 | } 261 | return input; 262 | } 263 | 264 | function getNumberWrittenRegex(number, userLanguage) { 265 | const numbersWrittenRegex = { 266 | 1: { 267 | 'en-US': 268 | /^(((((option)|(number))\s)?(one|1(\.0)?))|(first(\soption)?))$/, 269 | 'pt-BR': 270 | /^(((((opcao)|(numero))\s)?(um|1(\.0)?))|(primeir(a|o)(\sopcao)?))$/ 271 | }, 272 | 2: { 273 | 'en-US': 274 | /^(((((option)|(number))\s)?(two|2(\.0)?))|(second(\soption)?))$/, 275 | 'pt-BR': 276 | /^(((((opcao)|(numero))\s)?(dois|2(\.0)?))|(segund(a|o)(\sopcao)?))$/ 277 | }, 278 | 3: { 279 | 'en-US': 280 | /^(((((option)|(number))\s)?(three|3(\.0)?))|(third(\soption)?))$/, 281 | 'pt-BR': 282 | /^(((((opcao)|(numero))\s)?(tres|3(\.0)?))|(terceir(a|o)(\sopcao)?))$/ 283 | }, 284 | 4: { 285 | 'en-US': 286 | /^(((((option)|(number))\s)?(four|4(\.0)?))|(fourth(\soption)?))$/, 287 | 'pt-BR': 288 | /^(((((opcao)|(numero))\s)?(quatro|4(\.0)?))|(quart(a|o)(\sopcao)?))$/ 289 | }, 290 | 5: { 291 | 'en-US': 292 | /^(((((option)|(number))\s)?(five|5(\.0)?))|(fifth(\soption)?))$/, 293 | 'pt-BR': 294 | /^(((((opcao)|(numero))\s)?(cinco|5(\.0)?))|(quint(a|o)(\sopcao)?))$/ 295 | }, 296 | 6: { 297 | 'en-US': 298 | /^(((((option)|(number))\s)?(six|6(\.0)?))|(sixth(\soption)?))$/, 299 | 'pt-BR': 300 | /^(((((opcao)|(numero))\s)?(seis|6(\.0)?))|(sext(a|o)(\sopcao)?))$/ 301 | }, 302 | 7: { 303 | 'en-US': 304 | /^(((((option)|(number))\s)?(seven|7(\.0)?))|(seventh(\soption)?))$/, 305 | 'pt-BR': 306 | /^(((((opcao)|(numero))\s)?(sete|7(\.0)?))|(setim(a|o)(\sopcao)?))$/ 307 | }, 308 | 8: { 309 | 'en-US': 310 | /^(((((option)|(number))\s)?(eight|8(\.0)?))|(eighth(\soption)?))$/, 311 | 'pt-BR': 312 | /^(((((opcao)|(numero))\s)?(oito|8(\.0)?))|(oitav(a|o)(\sopcao)?))$/ 313 | }, 314 | 9: { 315 | 'en-US': 316 | /^(((((option)|(number))\s)?(nine|9(\.0)?))|(nineth(\soption)?))$/, 317 | 'pt-BR': 318 | /^(((((opcao)|(numero))\s)?(nove|9(\.0)?))|(non(a|o)(\sopcao)?))$/ 319 | }, 320 | 10: { 321 | 'en-US': 322 | /^(((((option)|(number))\s)?(ten|10(\.0)?))|(tenth(\soption)?))$/, 323 | 'pt-BR': 324 | /^(((((opcao)|(numero))\s)?(dez|10(\.0)?))|(decim(a|o)(\sopcao)?))$/ 325 | } 326 | }; 327 | 328 | return numbersWrittenRegex[`${number}`][userLanguage]; 329 | } 330 | 331 | function getCleanedInputToNlp(input) { 332 | let cleanedInputToNlp = replaceSpecialLetters(input); 333 | cleanedInputToNlp = removeLinks(cleanedInputToNlp); 334 | cleanedInputToNlp = removeExcessWhiteSpace(cleanedInputToNlp); 335 | return cleanedInputToNlp; 336 | } 337 | 338 | // The convention of Trackings is only first letter uppercase without special characters 339 | 340 | function getCleanedInputToTracking(input, config) { 341 | let cleanedInputToTracking = removeSpecialCharacters(input, config); 342 | cleanedInputToTracking = capitalizeFirst(cleanedInputToTracking); 343 | return cleanedInputToTracking; 344 | } 345 | 346 | function removeLinks(input) { 347 | const EMPTY_STR = ''; 348 | const LINK_STR = RegExp( 349 | /\b(((https?:\/\/)[^\s.]+|(www))\.[\w][^\s]+)\b/, 350 | 'gi' 351 | ); 352 | input = replaceSpecialLetters(input); 353 | input = input.replace(LINK_STR, EMPTY_STR); 354 | return input; 355 | } 356 | -------------------------------------------------------------------------------- /src/create-menu/createMenu.js: -------------------------------------------------------------------------------- 1 | // Assert [REQUIREMENTS] variables. 2 | 3 | // let channelBoldTags = { open: '', close: '' }; 4 | let channelBoldTags = { open: '*', close: '*' }; 5 | channelBoldTags = JSON.stringify(channelBoldTags); 6 | let userChannel = 'whatsapp'; 7 | 8 | console.log('-------------'); 9 | console.log(JSON.stringify(run(userChannel, channelBoldTags))); 10 | console.log('-------------'); 11 | const result = run(userChannel, channelBoldTags); 12 | console.log(result); 13 | console.log('-------------'); 14 | 15 | // Code of builder here 16 | 17 | function run(userChannel, channelBoldTags) { 18 | return getMenu(userChannel, channelBoldTags); 19 | } 20 | 21 | function getMenu(userChannel, channelBoldTags) { 22 | channelBoldTags = JSON.parse(channelBoldTags); 23 | const menuFields = { 24 | text: { 25 | default: { 26 | 'en-US': `This is a text to describe the menu that will be generated to Default channel, the user will ${channelBoldTags.open}choose one of the options bellow${channelBoldTags.close}`, 27 | 'pt-BR': `This is a text to describe the menu that will be generated to Default channel, the user will ${channelBoldTags.open}choose one of the options bellow${channelBoldTags.close}` 28 | }, 29 | whatsapp: { 30 | 'en-US': `This is a text to describe the menu that will be generated to WhatsApp channel, the user will ${channelBoldTags.open}choose one of the options bellow${channelBoldTags.close}`, 31 | 'pt-BR': `This is a text to describe the menu that will be generated to WhatsApp channel, the user will ${channelBoldTags.open}choose one of the options bellow${channelBoldTags.close}` 32 | }, 33 | facebook: { 34 | 'en-US': `This is a text to describe the menu that will be generated to Facebook channel, the user will ${channelBoldTags.open}choose one of the options bellow${channelBoldTags.close}`, 35 | 'pt-BR': `This is a text to describe the menu that will be generated to Facebook channel, the user will ${channelBoldTags.open}choose one of the options bellow${channelBoldTags.close}` 36 | }, 37 | telegram: { 38 | 'en-US': `This is a text to describe the menu that will be generated to Telegram channel, the user will ${channelBoldTags.open}choose one of the options bellow${channelBoldTags.close}`, 39 | 'pt-BR': `This is a text to describe the menu that will be generated to Telegram channel, the user will ${channelBoldTags.open}choose one of the options bellow${channelBoldTags.close}` 40 | } 41 | }, 42 | options: { 43 | // For break a line between options, add a '\n' in beginning of option text. Its works only in text menu. 44 | 'en-US': ['Option 1', 'Option 2', 'Option 3'], 45 | 'pt-BR': ['Opção 1', 'Opção 2', 'Opção 3'] 46 | }, 47 | /* options: { //This option structure allows you to create a menu separated by sessions (For Whatsapp-list and text menus, only). For Whatsapp list menu, it's has a maximum of 10 options (regardless of the number of sessions) 48 | 'en-US': { 49 | "Sessão 1": ['Option 1', 'Option 2'], 50 | "Sessão 2": ['Option 3'] 51 | }, 52 | 'pt-BR': { 53 | "Sessão 1": ['Option 1', 'Option 2', 'Option 3', 'Option 4', 'Option 5'], 54 | "Sessão 2": ['Option 6', 'Option 7', 'Option 8'] 55 | } 56 | }, */ 57 | header: { 58 | // Optional. It works in Whatsapp-list and textMenu only 59 | 'en-US': 60 | 'This is a text to describe the menu (in the top) that will be generated to Whatsapp channel. Its should have in max 60 characters', 61 | 'pt-BR': 62 | 'This is a text to describe the menu (in the top) that will be generated to Whatsapp channel. Its should have in max 60 characters' 63 | }, 64 | footer: { 65 | // Optional. It works in Whatsapp-list and textMenu only 66 | 'en-US': 67 | 'This is a text to describe the menu (in the bottom) that will be generated to Whatsapp channel. Its should have in max 60 characters', 68 | 'pt-BR': 69 | 'This is a text to describe the menu (in the bottom) that will be generated to Whatsapp channel. Its should have in max 60 characters' 70 | }, 71 | button: { 72 | // Required if list type Whatsapp menu 73 | 'en-US': 74 | 'This is a text of menu button in Whatsapp channel. Its should have in max 20 characters', 75 | 'pt-BR': 76 | 'This is a text of menu button in Whatsapp channel. Its should have in max 20 characters' 77 | } 78 | }; 79 | let props = { 80 | menuFields, 81 | userChannel, 82 | channelBoldTags, 83 | userLanguage: 'pt-BR' 84 | }; 85 | let config = { 86 | hasDefaultQuickReply: false, 87 | hasWppQuickReply: true, 88 | hasWppListMenu: true, 89 | isBlipImmediateMenu: false, 90 | orderOptions: 'desc' 91 | }; 92 | let menu = createMenu(props, config); 93 | return menu; 94 | } 95 | 96 | // Below are all scripts used in menu creation process 97 | // It should be put in the router resources in order to be used by the above script 98 | 99 | function createMenu( 100 | { 101 | userChannel = null, 102 | channelBoldTags = null, 103 | menuFields = null, 104 | userLanguage = null 105 | } = {}, 106 | { 107 | hasDefaultQuickReply = true, 108 | hasWppQuickReply = true, 109 | hasWppListMenu = false, 110 | isBlipImmediateMenu = true, 111 | orderOptions = 'asc' 112 | } = {} 113 | ) { 114 | let props = { 115 | userChannel, 116 | channelBoldTags, 117 | menuFields, 118 | userLanguage 119 | }; 120 | let config = { 121 | hasDefaultQuickReply, 122 | hasWppQuickReply, 123 | hasWppListMenu, 124 | isBlipImmediateMenu, 125 | orderOptions 126 | }; 127 | let menu = {}; 128 | try { 129 | props = normalizeProps(props); 130 | let { userChannel } = props; 131 | if ( 132 | config.hasDefaultQuickReply && 133 | (userChannel === 'blipchat' || userChannel === 'facebook') 134 | ) { 135 | menu.content = getQuickReply(props, config); 136 | menu.type = 'application/vnd.lime.select+json'; 137 | } else if ( 138 | userChannel === 'whatsapp' && 139 | config.hasWppQuickReply && 140 | validateOptionsToWhatsappMenu(props, 4) 141 | ) { 142 | menu.content = getWppQuickReply(props, config); 143 | menu.type = 'application/json'; 144 | } else if ( 145 | userChannel === 'whatsapp' && 146 | config.hasWppListMenu && 147 | validateOptionsToWhatsappMenu(props, 11) 148 | ) { 149 | menu.content = getWppListMenu(props, config); 150 | menu.type = 'application/json'; 151 | } else { 152 | menu.content = getTextMenu(props, config); 153 | if (userChannel === 'facebook' || userChannel === 'gbm') { 154 | menu.type = 'application/json'; 155 | } else { 156 | menu.type = 'application/vnd.lime.select+json'; 157 | } 158 | } 159 | } catch (exception) { 160 | menu.type = 'application/vnd.lime.select+json'; 161 | menu.content = { 162 | text: `Something went wrong while generating menu. Please, visit https://github.com/joaosoaresmatos/blip-scripts/blob/main/README.md to read more about it.\n\nDescription:\n\n${exception}` 163 | }; 164 | throw exception; 165 | } finally { 166 | return menu; 167 | } 168 | } 169 | 170 | function getQuickReply(props, config) { 171 | let { menuFields } = props; 172 | let menuText = menuFields.text; 173 | let menuOptions = menuFields.options; 174 | let quickReplyOptions = []; 175 | if (menuOptions) { 176 | for (let i = 0; i < menuOptions.length; i++) { 177 | quickReplyOptions.push({ 178 | text: menuOptions[i], 179 | type: 'text/plain', 180 | value: menuOptions[i] 181 | }); 182 | } 183 | } 184 | let quickReplyContent = { 185 | text: menuText, 186 | options: quickReplyOptions 187 | }; 188 | if (config.isBlipImmediateMenu) { 189 | quickReplyContent.scope = 'immediate'; 190 | } 191 | return quickReplyContent; 192 | } 193 | 194 | function getWppQuickReply(props, config) { 195 | const action = { 196 | buttons: buildQuickReplyOptions(props.menuFields.options) 197 | }; 198 | return getInteractiveMenu(props.menuFields, 'button', action); 199 | } 200 | 201 | function getWppListMenu(props) { 202 | const action = { 203 | button: props.menuFields.textButton, 204 | sections: buildSections(props.menuFields.options) 205 | }; 206 | return getInteractiveMenu(props.menuFields, 'list', action); 207 | } 208 | 209 | function getTextMenu(props, config) { 210 | let { menuFields } = props; 211 | let menuHeader = menuFields.header ? `${menuFields.header}\n\n` : ''; 212 | let menuFooter = menuFields.footer ? `\n\n${menuFields.footer}` : ''; 213 | let menuText = `${menuHeader}${menuFields.text}\n`; 214 | menuText += buildMenuTextOptions(props, config); 215 | menuText += menuFooter; 216 | let textMenu = { text: menuText }; 217 | return textMenu; 218 | } 219 | 220 | function buildMenuTextOptions(props, config) { 221 | let { menuFields } = props; 222 | let menuOptions = menuFields.options; 223 | if (menuOptions && Array.isArray(menuOptions)) { 224 | return buildMenuTextOptionsWhenIsArray(props, config); 225 | } 226 | if (menuOptions && typeof menuOptions === 'object') { 227 | return buildMenuTextOptionsWhenIsObject(props, config); 228 | } 229 | } 230 | 231 | function buildMenuTextOptionsWhenIsArray(props, config) { 232 | let { channelBoldTags, menuFields } = props; 233 | let menuOptions = menuFields.options; 234 | let menuText = ''; 235 | if (props.enableOptions !== false) { 236 | let totalItens = parseInt(menuOptions.length); 237 | if (config.orderOptions === 'desc') { 238 | start = totalItens - 1; 239 | for (let i = start, j = 0; i >= 0; i--, j++) { 240 | if (menuOptions[j][0] === '\n') { 241 | menuOptions[j] = menuOptions[j].replace('\n', ''); 242 | menuText += `\n\n${channelBoldTags.open}${i + 1}${ 243 | channelBoldTags.close 244 | }. ${menuOptions[j]}`; 245 | } else { 246 | menuText += `\n${channelBoldTags.open}${i + 1}${ 247 | channelBoldTags.close 248 | }. ${menuOptions[j]}`; 249 | } 250 | } 251 | } else { 252 | for (let i = 0; i < totalItens; i++) { 253 | let option = i + 1; 254 | if (props.isSurvey) { 255 | option = totalItens - i; 256 | } 257 | if (menuOptions[i][0] === '\n') { 258 | menuOptions[i] = menuOptions[i].replace('\n', ''); 259 | menuText += `\n\n${channelBoldTags.open}${option}${channelBoldTags.close}. ${menuOptions[i]}`; 260 | } else { 261 | menuText += `\n${channelBoldTags.open}${option}${channelBoldTags.close}. ${menuOptions[i]}`; 262 | } 263 | } 264 | } 265 | } 266 | return menuText; 267 | } 268 | 269 | function buildMenuTextOptionsWhenIsObject(props, config) { 270 | let { channelBoldTags, menuFields } = props; 271 | let menuOptions = menuFields.options; 272 | let menuText = ''; 273 | try { 274 | const sections = Object.keys(menuOptions); 275 | let totalItens = getNumberOfOptions(menuOptions); 276 | if (config.orderOptions === 'desc') { 277 | start = totalItens - 1; 278 | let i = start; 279 | for (let k = 0; k < sections.length; k++) { 280 | menuText += sections[k] ? `\n${sections[k]}\n` : '\n'; 281 | for (let j = 0; j < menuOptions[sections[k]].length; i--, j++) { 282 | if (menuOptions[sections[k]][j][0] === '\n') { 283 | menuOptions[sections[k]][j] = menuOptions[sections[k]][ 284 | j 285 | ].replace('\n', ''); 286 | menuText += `\n\n${channelBoldTags.open}${i + 1}${ 287 | channelBoldTags.close 288 | }. ${menuOptions[sections[k]][j]}`; 289 | } else { 290 | menuText += `\n${channelBoldTags.open}${i + 1}${ 291 | channelBoldTags.close 292 | }. ${menuOptions[sections[k]][j]}`; 293 | } 294 | } 295 | menuText += k < sections.length - 1 ? '\n' : ''; 296 | } 297 | } else { 298 | let i = 1; 299 | for (let k = 0; k < sections.length; k++) { 300 | menuText += sections[k] ? `\n${sections[k]}\n` : '\n'; 301 | for (let j = 0; j < menuOptions[sections[k]].length; j++, i++) { 302 | let option = i; 303 | if (menuFields.isSurvey) { 304 | option = totalItens - i; 305 | } 306 | if (menuOptions[sections[k]][j][0] === '\n') { 307 | menuOptions[sections[k]][j] = menuOptions[sections[k]][ 308 | j 309 | ].replace('\n', ''); 310 | menuText += `\n\n${channelBoldTags.open}${option}${ 311 | channelBoldTags.close 312 | }. ${menuOptions[sections[k]][j]}`; 313 | } else { 314 | menuText += `\n${channelBoldTags.open}${option}${ 315 | channelBoldTags.close 316 | }. ${menuOptions[sections[k]][j]}`; 317 | } 318 | } 319 | menuText += k < sections.length - 1 ? '\n' : ''; 320 | } 321 | } 322 | } catch (error) { 323 | return ''; 324 | } finally { 325 | return menuText; 326 | } 327 | } 328 | 329 | function getInteractiveMenu(menuFields, type, action) { 330 | return { 331 | recipient_type: 'individual', 332 | type: 'interactive', 333 | interactive: { 334 | type, 335 | ...buildHeader(menuFields), 336 | body: { 337 | text: menuFields.text 338 | }, 339 | ...buildFooter(menuFields), 340 | action 341 | } 342 | }; 343 | } 344 | 345 | function buildQuickReplyOptions(menuOptions) { 346 | let optionsArray = []; 347 | 348 | if (menuOptions && typeof menuOptions === 'object') { 349 | try { 350 | const options = Object.keys(menuOptions); 351 | for (let i = 0; i < options.length; i++) { 352 | optionsArray = optionsArray.concat(menuOptions[options[i]]); 353 | } 354 | } catch (error) { 355 | return []; 356 | } 357 | } 358 | 359 | menuOptions = optionsArray; 360 | 361 | let quickReplyOptions = []; 362 | 363 | if (menuOptions && Array.isArray(menuOptions)) { 364 | for (let i = 0; i < menuOptions.length; i++) { 365 | quickReplyOptions.push({ 366 | type: 'reply', 367 | reply: { 368 | id: menuOptions[i], 369 | title: menuOptions[i] 370 | } 371 | }); 372 | } 373 | } 374 | 375 | return quickReplyOptions; 376 | } 377 | 378 | function buildHeader(menuFields) { 379 | if (menuFields.header && typeof menuFields.header === 'object') { 380 | return { header: { ...menuFields.header } }; 381 | } 382 | return !menuFields.header 383 | ? {} 384 | : { header: { type: 'text', text: menuFields.header } }; 385 | } 386 | 387 | function buildFooter(menuFields) { 388 | return !menuFields.footer ? {} : { footer: { text: menuFields.footer } }; 389 | } 390 | 391 | function validateOptionsToWhatsappMenu(props, maxNumberOfOptions) { 392 | if (Array.isArray(props.menuFields.options)) { 393 | return props.menuFields.options.length < maxNumberOfOptions; 394 | } 395 | if (typeof props === 'object') { 396 | try { 397 | const options = Object.keys(props.menuFields.options); 398 | let optionsCount = 0; 399 | for (let i = 0; i < options.length; i++) { 400 | optionsCount += props.menuFields.options[options[i]].length; 401 | } 402 | return optionsCount < maxNumberOfOptions; 403 | } catch (error) { 404 | return false; 405 | } 406 | } else { 407 | return false; 408 | } 409 | } 410 | 411 | function getNumberOfOptions(menuOptions) { 412 | let optionsCount = 0; 413 | try { 414 | if (typeof menuOptions === 'object') { 415 | const options = Object.keys(menuOptions); 416 | for (let i = 0; i < options.length; i++) { 417 | optionsCount += menuOptions[options[i]].length; 418 | } 419 | } 420 | } finally { 421 | return optionsCount; 422 | } 423 | } 424 | 425 | function buildSections(menuOptions) { 426 | if (Array.isArray(menuOptions)) { 427 | return [ 428 | { 429 | rows: buildListOptions(menuOptions) 430 | } 431 | ]; 432 | } 433 | 434 | return Object.keys(menuOptions).map((key, id) => ({ 435 | title: key, 436 | rows: buildListOptions(menuOptions[key], id) 437 | })); 438 | } 439 | 440 | function buildListOptions(options, section_id = 1) { 441 | return options.map((option, idx) => ({ 442 | id: `id:${section_id}.${idx}`, 443 | ...buildListRowTitle(option) 444 | })); 445 | } 446 | 447 | function buildListRowTitle(options) { 448 | const split_option = options.split('\n'); 449 | const description = 450 | split_option.length > 1 ? { description: split_option[1] } : ''; 451 | return { 452 | title: split_option[0], 453 | ...description 454 | }; 455 | } 456 | 457 | function normalizeProps(props) { 458 | let menuText = normalizeMenuText(props); 459 | let menuOptions = normalizeMenuOptions(props); 460 | let menuHeader = normalizeMenuHeader(props); 461 | let menuFooter = normalizeMenuFooter(props); 462 | let menuButton = normalizeMenuButton(props); 463 | 464 | props.menuFields.text = menuText; 465 | props.menuFields.options = menuOptions; 466 | props.menuFields.header = menuHeader; 467 | props.menuFields.footer = menuFooter; 468 | props.menuFields.textButton = menuButton; 469 | 470 | return props; 471 | } 472 | 473 | function normalizeMenuText(props) { 474 | let { userChannel, menuFields, userLanguage } = props; 475 | let menuText; 476 | if ( 477 | menuFields.text[userChannel] && 478 | menuFields.text[userChannel][userLanguage] 479 | ) { 480 | menuText = menuFields.text[userChannel][userLanguage]; 481 | } else if ( 482 | menuFields.text.default && 483 | menuFields.text.default[userLanguage] 484 | ) { 485 | menuText = menuFields.text.default[userLanguage]; 486 | } else if (menuFields.text[userChannel]) { 487 | menuText = menuFields.text[userChannel]; 488 | } else if (menuFields.text.default) { 489 | menuText = menuFields.text.default; 490 | } else { 491 | menuText = menuFields.text; 492 | } 493 | return menuText; 494 | } 495 | 496 | function normalizeMenuHeader(props) { 497 | let { menuFields, userLanguage, userChannel } = props; 498 | let headerText; 499 | if (menuFields.header && typeof menuFields.header === 'string') { 500 | headerText = menuFields.header; 501 | } else if ( 502 | menuFields.header && 503 | menuFields.header[userLanguage] && 504 | typeof menuFields.header[userLanguage] === 'string' 505 | ) { 506 | headerText = menuFields.header[userLanguage]; 507 | } else if ( 508 | menuFields.header && 509 | menuFields.header[userLanguage] && 510 | userChannel === 'whatsapp' 511 | ) { 512 | headerText = menuFields.header[userLanguage]; 513 | } else if (menuFields.header && userChannel === 'whatsapp') { 514 | headerText = menuFields.header; 515 | } else { 516 | return; 517 | } 518 | return headerText; 519 | } 520 | 521 | function normalizeMenuFooter(props) { 522 | let { menuFields, userLanguage } = props; 523 | let footerText; 524 | if (menuFields.footer && menuFields.footer[userLanguage]) { 525 | footerText = menuFields.footer[userLanguage]; 526 | } else if (typeof menuFields.footer === 'string') { 527 | footerText = menuFields.footer; 528 | } else { 529 | footerText = null; 530 | } 531 | return footerText; 532 | } 533 | 534 | function normalizeMenuButton(props) { 535 | const DEFAULT_BUTTON = 'Options'; 536 | let { menuFields, userLanguage } = props; 537 | let buttonText; 538 | if (menuFields.button && menuFields.button[userLanguage]) { 539 | buttonText = menuFields.button[userLanguage]; 540 | } else if (typeof menuFields.button === 'string') { 541 | buttonText = menuFields.button; 542 | } else { 543 | buttonText = DEFAULT_BUTTON; 544 | } 545 | return buttonText; 546 | } 547 | 548 | function normalizeMenuOptions(props) { 549 | let { menuFields, userLanguage } = props; 550 | let menuOptions; 551 | if (menuFields.options[userLanguage]) { 552 | menuOptions = menuFields.options[userLanguage]; 553 | } else { 554 | menuOptions = menuFields.options; 555 | } 556 | return menuOptions; 557 | } 558 | 559 | module.exports = { 560 | getMenu, 561 | createMenu, 562 | getQuickReply, 563 | getWppQuickReply, 564 | getTextMenu, 565 | normalizeProps 566 | }; 567 | --------------------------------------------------------------------------------