├── 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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
169 |
170 | The menu fields are detailed in the image below.
171 |
172 | 
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 | 
179 | 
180 |
181 | The menu fields are detailed in the image below.
182 |
183 | 
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 | 
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 |
--------------------------------------------------------------------------------