├── .nvmrc ├── docs ├── static │ ├── .nojekyll │ └── img │ │ ├── flex.png │ │ ├── hero.png │ │ ├── channels │ │ ├── line-splash.png │ │ ├── line-webhook.png │ │ ├── viber-splash.png │ │ ├── viber-token.png │ │ ├── line-channel-id.png │ │ ├── googlechat-splash.png │ │ ├── instagram-splash.png │ │ ├── instagram-app-secret.png │ │ ├── line-channel-secret.png │ │ ├── googlechat-set-webhook.png │ │ ├── googlechat-visibility.png │ │ ├── instagram-set-webhook.png │ │ ├── line-channel-access-token.png │ │ ├── googlechat-service-account.png │ │ ├── instagram-page-access-token.png │ │ ├── googlechat-base64-credentials.png │ │ └── instagram-webhook-verify-token.png │ │ ├── conversations_adapters.png │ │ ├── guides │ │ ├── studio-workaround-prog-chat.png │ │ ├── studio-workaround-webhook.png │ │ └── studio-workaround-viber-flow.png │ │ └── getting-started │ │ ├── studio-flow-overall.png │ │ ├── task-channel-studio.png │ │ ├── github-actions-overall.png │ │ └── task-channel-create-task-channel.png ├── docs │ ├── guides │ │ ├── _category_.json │ │ └── 01_using-studio-widgets.md │ ├── channels │ │ ├── _category_.json │ │ ├── line.md │ │ ├── viber.md │ │ ├── overview.md │ │ ├── instagram.md │ │ └── googlechat.md │ ├── getting-started │ │ ├── _category_.json │ │ ├── 03_create-task-channel.md │ │ ├── 04_create-studio-flow.md │ │ ├── 02_deploy-via-cli.md │ │ └── 01_deploy-via-github-actions.md │ └── introduction │ │ └── introduction.md ├── babel.config.js ├── tsconfig.json ├── .gitignore ├── sidebars.js ├── README.md ├── src │ └── css │ │ └── custom.css ├── package.json └── docusaurus.config.js ├── plugin-conversations-icons ├── README.md ├── src │ ├── index.ts │ ├── ConversationsIconsPlugin.tsx │ └── components │ │ └── SocialIcons.tsx ├── public │ └── appConfig.example.js ├── jest.config.js ├── webpack.config.js ├── webpack.dev.js ├── package.json ├── tsconfig.json └── .gitignore ├── images └── conversations-adapters-hero.png ├── serverless-functions ├── tsconfig.json ├── src │ └── functions │ │ └── api │ │ ├── line │ │ ├── line_types.private.ts │ │ ├── incoming.ts │ │ ├── outgoing.ts │ │ └── line.helper.private.ts │ │ ├── googlechat │ │ ├── googlechat_types.private.ts │ │ ├── incoming.ts │ │ ├── outgoing.ts │ │ └── googlechat.helper.private.ts │ │ ├── viber │ │ ├── incoming.ts │ │ ├── viber_types.private.ts │ │ ├── outgoing.ts │ │ └── viber.helper.private.ts │ │ ├── instagram │ │ ├── instagram_types.private.ts │ │ ├── incoming.ts │ │ ├── outgoing.ts │ │ └── instagram.helper.private.ts │ │ ├── bot │ │ ├── incoming-conversation.ts │ │ └── bot.helper.private.ts │ │ └── common │ │ ├── studio-workaround.protected.ts │ │ └── common.helper.private.ts ├── .env.example ├── scripts │ ├── setup-environment.js │ └── common.js ├── package.json ├── .gitignore └── .twilioserverlessrc ├── .all-contributorsrc ├── LICENSE.md ├── .github └── workflows │ ├── docs_deploy.yaml │ └── flex_deploy.yaml ├── .gitignore └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 14 -------------------------------------------------------------------------------- /docs/static/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/docs/guides/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "position": 4, 3 | "label": "Guides" 4 | } -------------------------------------------------------------------------------- /docs/docs/channels/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "position": 3, 3 | "label": "Channels" 4 | } -------------------------------------------------------------------------------- /docs/docs/getting-started/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "position": 2, 3 | "label": "Getting Started" 4 | } -------------------------------------------------------------------------------- /docs/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /docs/static/img/flex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leroychan/twilio-flex-conversations-adapters/HEAD/docs/static/img/flex.png -------------------------------------------------------------------------------- /docs/static/img/hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leroychan/twilio-flex-conversations-adapters/HEAD/docs/static/img/hero.png -------------------------------------------------------------------------------- /plugin-conversations-icons/README.md: -------------------------------------------------------------------------------- 1 | # Plugin - Conversations Icons 2 | 3 | Display social messaging chat icons in Flex UI 2.x 4 | -------------------------------------------------------------------------------- /images/conversations-adapters-hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leroychan/twilio-flex-conversations-adapters/HEAD/images/conversations-adapters-hero.png -------------------------------------------------------------------------------- /docs/static/img/channels/line-splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leroychan/twilio-flex-conversations-adapters/HEAD/docs/static/img/channels/line-splash.png -------------------------------------------------------------------------------- /docs/static/img/channels/line-webhook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leroychan/twilio-flex-conversations-adapters/HEAD/docs/static/img/channels/line-webhook.png -------------------------------------------------------------------------------- /docs/static/img/channels/viber-splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leroychan/twilio-flex-conversations-adapters/HEAD/docs/static/img/channels/viber-splash.png -------------------------------------------------------------------------------- /docs/static/img/channels/viber-token.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leroychan/twilio-flex-conversations-adapters/HEAD/docs/static/img/channels/viber-token.png -------------------------------------------------------------------------------- /docs/static/img/channels/line-channel-id.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leroychan/twilio-flex-conversations-adapters/HEAD/docs/static/img/channels/line-channel-id.png -------------------------------------------------------------------------------- /docs/static/img/conversations_adapters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leroychan/twilio-flex-conversations-adapters/HEAD/docs/static/img/conversations_adapters.png -------------------------------------------------------------------------------- /docs/static/img/channels/googlechat-splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leroychan/twilio-flex-conversations-adapters/HEAD/docs/static/img/channels/googlechat-splash.png -------------------------------------------------------------------------------- /docs/static/img/channels/instagram-splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leroychan/twilio-flex-conversations-adapters/HEAD/docs/static/img/channels/instagram-splash.png -------------------------------------------------------------------------------- /docs/static/img/channels/instagram-app-secret.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leroychan/twilio-flex-conversations-adapters/HEAD/docs/static/img/channels/instagram-app-secret.png -------------------------------------------------------------------------------- /docs/static/img/channels/line-channel-secret.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leroychan/twilio-flex-conversations-adapters/HEAD/docs/static/img/channels/line-channel-secret.png -------------------------------------------------------------------------------- /docs/static/img/channels/googlechat-set-webhook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leroychan/twilio-flex-conversations-adapters/HEAD/docs/static/img/channels/googlechat-set-webhook.png -------------------------------------------------------------------------------- /docs/static/img/channels/googlechat-visibility.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leroychan/twilio-flex-conversations-adapters/HEAD/docs/static/img/channels/googlechat-visibility.png -------------------------------------------------------------------------------- /docs/static/img/channels/instagram-set-webhook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leroychan/twilio-flex-conversations-adapters/HEAD/docs/static/img/channels/instagram-set-webhook.png -------------------------------------------------------------------------------- /docs/static/img/channels/line-channel-access-token.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leroychan/twilio-flex-conversations-adapters/HEAD/docs/static/img/channels/line-channel-access-token.png -------------------------------------------------------------------------------- /docs/static/img/guides/studio-workaround-prog-chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leroychan/twilio-flex-conversations-adapters/HEAD/docs/static/img/guides/studio-workaround-prog-chat.png -------------------------------------------------------------------------------- /docs/static/img/guides/studio-workaround-webhook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leroychan/twilio-flex-conversations-adapters/HEAD/docs/static/img/guides/studio-workaround-webhook.png -------------------------------------------------------------------------------- /docs/static/img/channels/googlechat-service-account.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leroychan/twilio-flex-conversations-adapters/HEAD/docs/static/img/channels/googlechat-service-account.png -------------------------------------------------------------------------------- /docs/static/img/channels/instagram-page-access-token.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leroychan/twilio-flex-conversations-adapters/HEAD/docs/static/img/channels/instagram-page-access-token.png -------------------------------------------------------------------------------- /docs/static/img/getting-started/studio-flow-overall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leroychan/twilio-flex-conversations-adapters/HEAD/docs/static/img/getting-started/studio-flow-overall.png -------------------------------------------------------------------------------- /docs/static/img/getting-started/task-channel-studio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leroychan/twilio-flex-conversations-adapters/HEAD/docs/static/img/getting-started/task-channel-studio.png -------------------------------------------------------------------------------- /docs/static/img/guides/studio-workaround-viber-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leroychan/twilio-flex-conversations-adapters/HEAD/docs/static/img/guides/studio-workaround-viber-flow.png -------------------------------------------------------------------------------- /docs/static/img/channels/googlechat-base64-credentials.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leroychan/twilio-flex-conversations-adapters/HEAD/docs/static/img/channels/googlechat-base64-credentials.png -------------------------------------------------------------------------------- /docs/static/img/channels/instagram-webhook-verify-token.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leroychan/twilio-flex-conversations-adapters/HEAD/docs/static/img/channels/instagram-webhook-verify-token.png -------------------------------------------------------------------------------- /docs/static/img/getting-started/github-actions-overall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leroychan/twilio-flex-conversations-adapters/HEAD/docs/static/img/getting-started/github-actions-overall.png -------------------------------------------------------------------------------- /docs/static/img/getting-started/task-channel-create-task-channel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leroychan/twilio-flex-conversations-adapters/HEAD/docs/static/img/getting-started/task-channel-create-task-channel.png -------------------------------------------------------------------------------- /plugin-conversations-icons/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as FlexPlugin from '@twilio/flex-plugin'; 2 | 3 | import ConversationsIconsPlugin from './ConversationsIconsPlugin'; 4 | 5 | FlexPlugin.loadPlugin(ConversationsIconsPlugin); 6 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // This file is not used in compilation. It is here just for a nice editor experience. 3 | "extends": "@tsconfig/docusaurus/tsconfig.json", 4 | "compilerOptions": { 5 | "baseUrl": "." 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /plugin-conversations-icons/public/appConfig.example.js: -------------------------------------------------------------------------------- 1 | var appConfig = { 2 | pluginService: { 3 | enabled: true, 4 | url: '/plugins', 5 | }, 6 | ytica: false, 7 | logLevel: 'info', 8 | showSupervisorDesktopView: true, 9 | }; 10 | -------------------------------------------------------------------------------- /plugin-conversations-icons/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = (config, { isProd, isDev, isTest }) => { 2 | /** 3 | * Customize the Jest by modifying the config object. 4 | * Consult https://jestjs.io/docs/en/configuration for more information. 5 | */ 6 | 7 | return config; 8 | } 9 | -------------------------------------------------------------------------------- /plugin-conversations-icons/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = (config, { isProd, isDev, isTest }) => { 2 | /** 3 | * Customize the webpack by modifying the config object. 4 | * Consult https://webpack.js.org/configuration for more information 5 | */ 6 | 7 | return config; 8 | } 9 | -------------------------------------------------------------------------------- /plugin-conversations-icons/webpack.dev.js: -------------------------------------------------------------------------------- 1 | module.exports = (config, { isProd, isDev, isTest }) => { 2 | /** 3 | * Customize the webpack dev-server by modifying the config object. 4 | * Consult https://webpack.js.org/configuration/dev-server for more information. 5 | */ 6 | 7 | return config; 8 | } 9 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /serverless-functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "rootDir": "src", 8 | "outDir": "dist", 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "typeRoots": ["types", "./node_modules/@types/"], 12 | "types": [ 13 | "node" 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /serverless-functions/src/functions/api/line/line_types.private.ts: -------------------------------------------------------------------------------- 1 | export type LINEContext = { 2 | LINE_STUDIO_FLOW_SID: string; 3 | LINE_CHANNEL_ID: string; 4 | LINE_CHANNEL_SECRET: string; 5 | LINE_CHANNEL_ACCESS_TOKEN: string; 6 | ACCOUNT_SID: string; 7 | AUTH_TOKEN: string; 8 | DOMAIN_NAME_OVERRIDE: string; 9 | }; 10 | 11 | export enum LINEMessageType { 12 | TEXT = "text", 13 | FILE = "file", 14 | CONTACT = "contact", 15 | LOCATION = "location", 16 | STICKER = "sticker", 17 | IMAGE = "image", 18 | VIDEO = "video", 19 | URL = "url", 20 | } 21 | -------------------------------------------------------------------------------- /serverless-functions/src/functions/api/googlechat/googlechat_types.private.ts: -------------------------------------------------------------------------------- 1 | import { chat_v1 } from "googleapis"; 2 | 3 | export type GoogleChatContext = { 4 | GOOGLECHAT_STUDIO_FLOW_SID: string; 5 | GOOGLECHAT_SERVICE_ACCOUNT_KEY_BASE64: string; 6 | ACCOUNT_SID: string; 7 | AUTH_TOKEN: string; 8 | DOMAIN_NAME_OVERRIDE: string; 9 | }; 10 | 11 | export enum GoogleChatMessageType { 12 | TEXT = "text", 13 | AUDIO = "audio", 14 | IMAGE = "image", 15 | VIDEO = "video", 16 | } 17 | 18 | type TwilioInjectedRequest = { 19 | request: any; 20 | }; 21 | 22 | // Schema$DeprecatedEvent is the only suitable type from googleapis 23 | export type GoogleChatBaseMessage = chat_v1.Schema$DeprecatedEvent & 24 | TwilioInjectedRequest; 25 | -------------------------------------------------------------------------------- /plugin-conversations-icons/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plugin-conversations-icons", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "deploy": "twilio flex:plugins:deploy", 7 | "release": "twilio flex:plugins:release --plugin ${npm_package_name}@${npm_package_version}", 8 | "install-flex-plugin": "twilio plugins:install @twilio-labs/plugin-flex@6.1.2", 9 | "build": "twilio flex:plugins:build" 10 | }, 11 | "dependencies": { 12 | "@twilio-paste/core": "^10.14.0", 13 | "@twilio-paste/icons": "^5.7.0", 14 | "@twilio/flex-plugin-scripts": "6.1.2", 15 | "prop-types": "^15.7.2", 16 | "react": "17.0.2", 17 | "react-dom": "17.0.2" 18 | }, 19 | "devDependencies": { 20 | "@twilio/flex-ui": "2.3.2", 21 | "react-test-renderer": "17.0.2", 22 | "twilio-cli": "^5.5.0", 23 | "typescript": "^4" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /docs/sidebars.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creating a sidebar enables you to: 3 | - create an ordered group of docs 4 | - render a sidebar for each doc of that group 5 | - provide next/previous navigation 6 | 7 | The sidebars can be generated from the filesystem, or explicitly defined here. 8 | 9 | Create as many sidebars as you want. 10 | */ 11 | 12 | // @ts-check 13 | 14 | /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ 15 | const sidebars = { 16 | // By default, Docusaurus generates a sidebar from the docs folder structure 17 | tutorialSidebar: [{type: 'autogenerated', dirName: '.'}], 18 | 19 | // But you can create a sidebar manually 20 | /* 21 | tutorialSidebar: [ 22 | 'intro', 23 | 'hello', 24 | { 25 | type: 'category', 26 | label: 'Tutorial', 27 | items: ['tutorial-basics/create-a-document'], 28 | }, 29 | ], 30 | */ 31 | }; 32 | 33 | module.exports = sidebars; 34 | -------------------------------------------------------------------------------- /plugin-conversations-icons/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src", 4 | "rootDir": ".", 5 | "outDir": "build", 6 | "target": "es5", 7 | "lib": [ 8 | "dom", 9 | "dom.iterable", 10 | "esnext" 11 | ], 12 | "allowJs": true, 13 | "skipLibCheck": true, 14 | "esModuleInterop": true, 15 | "allowSyntheticDefaultImports": true, 16 | "strict": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "module": "esnext", 19 | "moduleResolution": "node", 20 | "resolveJsonModule": true, 21 | "isolatedModules": true, 22 | "noEmit": true, 23 | "jsx": "preserve", 24 | "noUnusedLocals": false, 25 | "noUnusedParameters": false 26 | }, 27 | "include": [ 28 | "./src/**/*" 29 | ], 30 | "exclude": [ 31 | "./**/*.test.ts", 32 | "./**/*.test.tsx", 33 | "./**/__mocks__/*.ts", 34 | "./**/__mocks__/*.tsx" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator. 4 | 5 | ### Installation 6 | 7 | ``` 8 | $ yarn 9 | ``` 10 | 11 | ### Local Development 12 | 13 | ``` 14 | $ yarn start 15 | ``` 16 | 17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ### Build 20 | 21 | ``` 22 | $ yarn build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ### Deployment 28 | 29 | Using SSH: 30 | 31 | ``` 32 | $ USE_SSH=true yarn deploy 33 | ``` 34 | 35 | Not using SSH: 36 | 37 | ``` 38 | $ GIT_USER= yarn deploy 39 | ``` 40 | 41 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 42 | -------------------------------------------------------------------------------- /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "commitType": "docs", 8 | "commitConvention": "angular", 9 | "contributors": [ 10 | { 11 | "login": "leroychan", 12 | "name": "Leroy Chan", 13 | "avatar_url": "https://avatars.githubusercontent.com/u/5236195?v=4", 14 | "profile": "https://github.com/leroychan", 15 | "contributions": [ 16 | "plugin" 17 | ] 18 | }, 19 | { 20 | "login": "chaosloth", 21 | "name": "Christopher Connolly", 22 | "avatar_url": "https://avatars.githubusercontent.com/u/425070?v=4", 23 | "profile": "https://github.com/chaosloth", 24 | "contributions": [ 25 | "plugin" 26 | ] 27 | } 28 | ], 29 | "contributorsPerLine": 7, 30 | "skipCi": true, 31 | "repoType": "github", 32 | "repoHost": "https://github.com", 33 | "projectName": "twilio-flex-conversations-adapters", 34 | "projectOwner": "leroychan" 35 | } 36 | -------------------------------------------------------------------------------- /serverless-functions/.env.example: -------------------------------------------------------------------------------- 1 | # 2 | # ACCOUNT INFO - Optional for deployment. Will be auto populated during deployment 3 | # 4 | ACCOUNT_SID= 5 | AUTH_TOKEN= 6 | 7 | # 8 | # OVERRIDES 9 | # 10 | DOMAIN_NAME_OVERRIDE= 11 | 12 | # 13 | # TWILIO STUDIO 14 | # 15 | INSTAGRAM_STUDIO_FLOW_SID= 16 | LINE_STUDIO_FLOW_SID= 17 | VIBER_STUDIO_FLOW_SID= 18 | GOOGLECHAT_STUDIO_FLOW_SID= 19 | 20 | # 21 | # GOOGLE CHAT 22 | # 23 | GOOGLECHAT_SERVICE_ACCOUNT_KEY_BASE64= 24 | 25 | # 26 | # INSTAGRAM 27 | # 28 | INSTAGRAM_APP_SECRET= 29 | INSTAGRAM_PAGE_ACCESS_TOKEN= 30 | INSTAGRAM_WEBHOOK_VERIFY_TOKEN= 31 | 32 | # 33 | # LINE 34 | # 35 | LINE_CHANNEL_ID= 36 | LINE_CHANNEL_SECRET= 37 | LINE_CHANNEL_ACCESS_TOKEN= 38 | 39 | # 40 | # VIBER 41 | # 42 | VIBER_AUTH_TOKEN= 43 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Leroy Chan 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 | -------------------------------------------------------------------------------- /docs/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Any CSS included here will be global. The classic template 3 | * bundles Infima by default. Infima is a CSS framework designed to 4 | * work well for content-centric websites. 5 | */ 6 | 7 | /* You can override the default Infima variables here. */ 8 | :root { 9 | --ifm-color-primary: #FF7A00; 10 | --ifm-color-primary-dark: #FF7A00; 11 | --ifm-color-primary-darker: #FF7A00; 12 | --ifm-color-primary-darkest: #FF7A00; 13 | --ifm-color-primary-light: #FF7A00; 14 | --ifm-color-primary-lighter: #FF7A00; 15 | --ifm-color-primary-lightest: #FF7A00; 16 | --ifm-code-font-size: 95%; 17 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); 18 | } 19 | 20 | /* For readability concerns, you should choose a lighter palette in dark mode. */ 21 | [data-theme='dark'] { 22 | --ifm-color-primary: #FF7A00; 23 | --ifm-color-primary-dark: #FF7A00; 24 | --ifm-color-primary-darker: #FF7A00; 25 | --ifm-color-primary-darkest: #FF7A00; 26 | --ifm-color-primary-light: #FF7A00; 27 | --ifm-color-primary-lighter: #FF7A00; 28 | --ifm-color-primary-lightest: #FF7A00; 29 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); 30 | } 31 | -------------------------------------------------------------------------------- /serverless-functions/scripts/setup-environment.js: -------------------------------------------------------------------------------- 1 | // Import Libraries 2 | const { 3 | copyFile, 4 | parseExampleEnvironmentVariables, 5 | replaceEnvironmentVariables, 6 | printContextVariables, 7 | } = require("./common"); 8 | 9 | // Set Variables 10 | const EXAMPLE_ENVIRONMENT_FILE_NAME = ".env.example"; 11 | 12 | // Step 1: Check Environment Variable Passed 13 | if (!process.argv[2]) { 14 | console.log(`Error in setup-environment: No Environment Detected`); 15 | return false; 16 | } 17 | 18 | // Step 2: Create ".env" Environment File 19 | const deployToEnvironment = process.argv[2]; 20 | const environmentFile = `.env.${deployToEnvironment}`; 21 | console.log(`Environment Selected: ${deployToEnvironment}`); 22 | console.log(`Environment File (To Be Created): ${environmentFile}`); 23 | 24 | // Step 3: Copy Environment Variable File from Example 25 | copyFile(EXAMPLE_ENVIRONMENT_FILE_NAME, environmentFile); 26 | 27 | // Step 4: Parse Example Environment Variable and Get Context 28 | let context = parseExampleEnvironmentVariables(EXAMPLE_ENVIRONMENT_FILE_NAME); 29 | 30 | // Step 5: Replace Environment Variable File with Actual Values 31 | context = replaceEnvironmentVariables(context, environmentFile); 32 | printContextVariables(context, "Summary of Environment"); 33 | -------------------------------------------------------------------------------- /plugin-conversations-icons/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # Flex related ignore 64 | appConfig.js 65 | pluginsService.js 66 | build/ 67 | -------------------------------------------------------------------------------- /docs/docs/getting-started/03_create-task-channel.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Create Task Channel 3 | position: 3 4 | --- 5 | 6 | # Create Task Channel 7 | 8 |

9 | Task Channel Overall 10 |

11 | 12 | ## Overview 13 | 14 | - The main purpose of the TaskRouter - Task Channel is to uniquely identify the different inbound channels and report within Flex Insights accordingly (using `Communication Channels` attribute) 15 | - It is recommended to create 1 Task Channel per custom channel. 16 | 17 | ## Instructions 18 | 19 | 1. Login to [Twilio Console](https://console.twilio.com/) and under `TaskRouter`, select `Workspaces` and then `Flex Task Assignment` 20 | 1. On the left-hand side menu bar, select `Task Channels` 21 | 1. Click on the `Create new Task Channel` button 22 | 1. Input the custom channel's name and click `Create` 23 | - It is recommended for the Task Channel's name to be in all lowercase with no spaces 24 | 25 | :::info 26 | 27 | You do NOT need to take note of the Task Channel's SID. When creating a new Studio Flow with the `Send to Flex` widget, you are able to select via a dropdown list your desired Task Channel 28 | 29 | ![Task Channel - Studio](/img/getting-started/task-channel-studio.png) 30 | 31 | ::: 32 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids", 15 | "typecheck": "tsc" 16 | }, 17 | "dependencies": { 18 | "@docusaurus/core": "2.4.1", 19 | "@docusaurus/preset-classic": "2.4.1", 20 | "@mdx-js/react": "^1.6.22", 21 | "clsx": "^1.2.1", 22 | "prism-react-renderer": "^1.3.5", 23 | "react": "^17.0.2", 24 | "react-dom": "^17.0.2" 25 | }, 26 | "devDependencies": { 27 | "@docusaurus/module-type-aliases": "2.4.1", 28 | "@tsconfig/docusaurus": "^1.0.5", 29 | "typescript": "^4.7.4" 30 | }, 31 | "browserslist": { 32 | "production": [ 33 | ">0.5%", 34 | "not dead", 35 | "not op_mini all" 36 | ], 37 | "development": [ 38 | "last 1 chrome version", 39 | "last 1 firefox version", 40 | "last 1 safari version" 41 | ] 42 | }, 43 | "engines": { 44 | "node": ">=16.14" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /.github/workflows/docs_deploy.yaml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy Documentation to Github Pages 3 | 4 | on: 5 | push: 6 | branches: ["master"] 7 | 8 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 9 | permissions: 10 | contents: read 11 | pages: write 12 | id-token: write 13 | 14 | # Allow one concurrent deployment 15 | concurrency: 16 | group: "pages" 17 | cancel-in-progress: true 18 | 19 | env: 20 | # Hosted GitHub runners have 7 GB of memory available, let's use 6 GB 21 | NODE_OPTIONS: --max-old-space-size=6144 22 | 23 | jobs: 24 | # Single deploy job since we're just deploying 25 | deploy: 26 | environment: 27 | name: github-pages 28 | url: ${{ steps.deployment.outputs.page_url }} 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v3 33 | - name: Set up Node.js 34 | uses: actions/setup-node@v3 35 | with: 36 | node-version: 16.x 37 | - name: Build 38 | working-directory: docs 39 | run: | 40 | npm install 41 | npm run build 42 | - name: Setup Pages 43 | uses: actions/configure-pages@v1 44 | - name: Upload artifact 45 | uses: actions/upload-pages-artifact@v1 46 | with: 47 | # Upload entire repository 48 | path: docs/build 49 | - name: Deploy to GitHub Pages 50 | id: deployment 51 | uses: actions/deploy-pages@v1 52 | -------------------------------------------------------------------------------- /docs/docs/introduction/introduction.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction 3 | sidebar_position: 1 4 | slug: / 5 | --- 6 | 7 | # Introduction 8 | 9 |

10 | 11 | Conversations Adapters Logo 12 | 13 |

14 | 15 | ## Overview 16 | 17 | _**Conversations Adapters**_ is a comprehensive framework designed to facilitate custom channel development for Twilio Flex using Twilio Conversations. This project empowers developers with a collection of pre-built connectors while also offering extensive extendibility options, enabling seamless integration with existing channels and facilitating the creation of new connectors. 18 | 19 |

20 | 21 | Conversations Adapters Preview 22 | 23 |

24 | 25 | ## Disclaimer: Open Source Project 26 | 27 | Conversations Adapters is an open source project and is not affiliated, endorsed, or associated with Twilio in any manner. This project is maintained and developed by independent contributors who aim to provide a comprehensive framework and collection of connectors for chat-based channels. While Conversations Adapters may support integration with various chat platforms, it is not officially supported or endorsed by Twilio. Any references made to Twilio or its products within this project are purely for informative purposes. For official support or inquiries related to Twilio, please refer to their official documentation and support channels. 28 | -------------------------------------------------------------------------------- /serverless-functions/src/functions/api/viber/incoming.ts: -------------------------------------------------------------------------------- 1 | // Imports global types 2 | import "@twilio-labs/serverless-runtime-types"; 3 | // Fetches specific types 4 | import { 5 | Context, 6 | ServerlessCallback, 7 | ServerlessFunctionSignature, 8 | } from "@twilio-labs/serverless-runtime-types/types"; 9 | import * as ViberTypes from "./viber_types.private"; 10 | import * as Helper from "./viber.helper.private"; 11 | 12 | // Load Libraries 13 | const { ViberMessageType } = ( 14 | require(Runtime.getFunctions()["api/viber/viber_types"].path) 15 | ); 16 | 17 | // Load Libraries 18 | const { wrappedSendToFlex } = ( 19 | require(Runtime.getFunctions()["api/viber/viber.helper"].path) 20 | ); 21 | 22 | export const handler: ServerlessFunctionSignature< 23 | ViberTypes.ViberContext, 24 | ViberTypes.ViberMessage 25 | > = async (context, event, callback: ServerlessCallback) => { 26 | console.log("event received - /api/viber/incoming: ", event); 27 | 28 | try { 29 | // Debug: Console Log Incoming Events 30 | console.log("---Start of Raw Event---"); 31 | console.log(event); 32 | console.log("---End of Raw Event---"); 33 | 34 | // Step 1: Verify viber signature 35 | // TODO: Add verification 36 | 37 | // Step 2: Process Twilio Conversations 38 | if (event.sender && event.sender.name) { 39 | const userId = event.sender.id; 40 | console.log(`event.sender.id: ${event.sender.id}`); 41 | await wrappedSendToFlex(context, userId, event); 42 | } 43 | 44 | return callback(null, { 45 | success: true, 46 | }); 47 | } catch (err) { 48 | console.log(err); 49 | return callback("outer catch error"); 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /serverless-functions/src/functions/api/instagram/instagram_types.private.ts: -------------------------------------------------------------------------------- 1 | export type InstagramContext = { 2 | INSTAGRAM_STUDIO_FLOW_SID: string; 3 | INSTAGRAM_APP_SECRET: string; 4 | INSTAGRAM_PAGE_ACCESS_TOKEN: string; 5 | INSTAGRAM_WEBHOOK_VERIFY_TOKEN: string; 6 | ACCOUNT_SID: string; 7 | AUTH_TOKEN: string; 8 | DOMAIN_NAME_OVERRIDE: string; 9 | }; 10 | 11 | export enum InstagramMessageType { 12 | TEXT = "text", 13 | AUDIO = "audio", 14 | IMAGE = "image", 15 | VIDEO = "video", 16 | } 17 | 18 | export type InstagramBaseMessage = { 19 | "hub.mode"?: string; 20 | "hub.challenge"?: string; 21 | "hub.verify_token"?: string; 22 | request: any; 23 | object: string; 24 | entry: Array; 25 | }; 26 | 27 | export type InstagramEntry = { 28 | time: number; 29 | id: string; 30 | messaging: Array; 31 | }; 32 | 33 | export type InstagramMessaging = { 34 | sender: { 35 | id: string; 36 | }; 37 | recipient: { 38 | id: string; 39 | }; 40 | message: InstagramMessageText | InstagramMessageMedia; 41 | }; 42 | 43 | export type InstagramMessageText = { 44 | mid: string; 45 | text: string; 46 | is_echo?: boolean; 47 | }; 48 | 49 | export type InstagramMessageMedia = { 50 | mid: string; 51 | attachments: Array<{ 52 | type: string; 53 | payload: { 54 | url: string; 55 | }; 56 | }>; 57 | is_echo?: boolean; 58 | }; 59 | 60 | export type InstagramSendMessagePayload = { 61 | recipient: { 62 | id: string; 63 | }; 64 | message: { 65 | text: string; 66 | }; 67 | }; 68 | 69 | export type InstagramSendMediaPayload = { 70 | recipient: { 71 | id: string; 72 | }; 73 | message: { 74 | attachment: { 75 | type: string; 76 | payload: { 77 | url: string; 78 | }; 79 | }; 80 | }; 81 | }; 82 | -------------------------------------------------------------------------------- /serverless-functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twilio-flex-conversations-adapters", 3 | "version": "2.0.0", 4 | "private": true, 5 | "scripts": { 6 | "test": "tsc --noEmit", 7 | "start": "twilio serverless:start --functions-folder dist/functions --assets-folder dist/assets", 8 | "deploy": "twilio serverless:deploy --functions-folder dist/functions --assets-folder dist/assets --override-existing-project --runtime node18", 9 | "deploy-env": "npm run deploy -- --env \".env.$ENVIRONMENT\"", 10 | "build": "tsc && npm run build:copy-assets && npm run build:copy-js-functions", 11 | "build:copy-assets": "copyfiles src/assets/* src/assets/**/* --up 2 --exclude **/*.ts dist/assets/", 12 | "build:copy-js-functions": "copyfiles src/functions/*.js src/functions/**/*.js --up 2 --exclude **/*.ts dist/functions/", 13 | "install-serverless-plugin": "twilio plugins:install @twilio-labs/plugin-serverless@v2", 14 | "prestart": "npm run build", 15 | "predeploy": "npm run build", 16 | "setup-environment": "node scripts/setup-environment.js" 17 | }, 18 | "dependencies": { 19 | "@line/bot-sdk": "^7.5.2", 20 | "@twilio-labs/serverless-runtime-types": "^2.2.3", 21 | "@twilio/mcs-client": "^0.6.1", 22 | "@twilio/runtime-handler": "1.2.5", 23 | "googleapis": "^122.0.0", 24 | "jsonwebtoken": "^9.0.1", 25 | "node-fetch": "2.6.7", 26 | "twilio": "^3.56", 27 | "uuid": "^9.0.0" 28 | }, 29 | "devDependencies": { 30 | "@types/jsonwebtoken": "^9.0.2", 31 | "@types/node": "^20.3.3", 32 | "@types/node-fetch": "^2.6.4", 33 | "@types/uuid": "^9.0.2", 34 | "copyfiles": "^2.2.0", 35 | "dotenv": "^16.3.1", 36 | "shelljs": "^0.8.5", 37 | "twilio-cli": "^5.5.0", 38 | "twilio-run": "^3.5.2", 39 | "typescript": "^5.1.6" 40 | }, 41 | "engines": { 42 | "node": "18" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /docs/docs/getting-started/04_create-studio-flow.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Create Studio Flow 3 | position: 4 4 | --- 5 | 6 | # Create Studio Flow 7 | 8 |

9 | Studio Flow Overall 10 |

11 | 12 | ## Overview 13 | 14 | - The main purpose of the Studio Flow is to route the incoming conversations from custom channels into Twilio Flex 15 | - It is recommended to create 1 Studio Flow per custom channel. 16 | 17 | ## Instructions 18 | 19 | 1. Login to [Twilio Console](https://console.twilio.com/) and under `Studio`, click the button `Create new Flow` on the top right corner. 20 | 1. For `Flow Name`, input a friendly and yet descriptive name (i.e. `Flex - LINE Flow`, `Flex - Viber Flow`) 21 | 1. For `Template`, select `Start from Scratch` 22 | 1. Within the Studio Flow, drag-and-drop the `Send to Flex` widget into the canvas. 23 | 1. Configure the `Workflow` of `Send to Flex` widget to any chosen TaskRouter workflow. You can choose the default `Assign to Anyone` workflow. 24 | 1. Configure `Task Channel` of `Send to Flex` widget. Select the Task Channel that you have just created created (i.e. `googlechat`, `instagram`, `line`, `viber`). 25 | 1. Connect the `Incoming Conversation` trigger to `Send to Flex` widget and click `Publish`. Do remember to click `Publish` otherwise the Studio Flow will not be activated. 26 | 1. The resultant Studio Flow should look similar to the image above. 27 | 1. Take note of the Flow SID which starts with `FWxxxx` 28 | - To obtain the Flow SID, click the back arrow button on the left-hand side menubar. The Flow SID will be displayed under your Flow name 29 | 30 | :::info 31 | 32 | It is recommended to create **1 Studio Flow per custom channel**. Take note of each of the Flow SID(s) that you have created as it will be required to inserted as environment variable(s). 33 | 34 | ::: 35 | 36 | ## Known Limitations 37 | 38 | - `Send Message` Widget and `Send and wait for reply` Widget does not work with custom channels 39 | -------------------------------------------------------------------------------- /docs/docs/channels/line.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: LINE 3 | --- 4 | 5 | # LINE 6 | 7 |

8 | Studio Flow Overall 9 |

10 | 11 | # Required Variables 12 | 13 | 1. `LINE_STUDIO_FLOW_SID` ([Guide](../getting-started/create-studio-flow)) 14 | 1. `LINE_CHANNEL_ID` 15 | 1. `LINE_CHANNEL_SECRET` 16 | 1. `LINE_CHANNEL_ACCESS_TOKEN` 17 | 18 | ## Setup 19 | 20 | 1. Login to [LINE Developer Console](https://developers.line.biz/console/) 21 | 1. Create a `Provider` if you do not have any existing `Provider` 22 | 1. Within the created `Provider`, Create a `LINE - Messaging API Channel`. 23 | 1. Under `Channels > YOUR_CREATED_CHANNEL > Messaging API > LINE Official Account features`, **disable** `Auto-reply messages` and `Greeting messages` 24 | 1. Obtain the value for `LINE_CHANNEL_ID` which is under `Basic Settings` 25 | - ![LINE Channel ID](/img/channels/line-channel-id.png) 26 | 1. Obtain the value for `LINE_CHANNEL_SECRET` which is under `Basic Settings` 27 | - ![LINE Channel Secret](/img/channels/line-channel-secret.png) 28 | 1. Obtain the value for `LINE_CHANNEL_ACCESS_TOKEN` which is under `Messaging API` 29 | - ![LINE Channel Access Token](/img/channels/line-channel-access-token.png) 30 | 31 | :::info 32 | 33 | Ensure that you have obtained **all** the necessary values for the variables stated in `Required Variables` 34 | 35 | ::: 36 | 37 | ## Configure Incoming Webhook 38 | 39 | 1. Ensure you have deployed Conversations Adapters into your Twilio Flex account 40 | 1. Ensure you are logged into [LINE Developer Console](https://developers.line.biz/console/) 41 | 1. Within your created `LINE - Messaging API Channel`, click on `Messaging API` 42 | 1. Under `Webhook settings`, click `Edit` and insert your deployed incoming webhook 43 | - The Conversations Adapters incoming webhook URL should be in the format of `https://twilio-flex-conversations-adapters--dev.twil.io/api/line/incoming` 44 | - ![LINE Webhook](/img/channels/line-webhook.png) 45 | -------------------------------------------------------------------------------- /docs/docs/guides/01_using-studio-widgets.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Using with Twilio Studio's Widgets 3 | position: 1 4 | --- 5 | 6 |

7 | Studio Viber Flow 8 |

9 | 10 | ## Overview 11 | 12 | By default, the functionalities of the `Send Message` and `Send And Wait For Reply` widgets in Twilio Studio are not supported with custom channels. This limitation arises from the absence of invocation for conversation-scoped webhooks when a message is sent from Twilio Studio, a measure taken to prevent excessive invocation of webhooks. 13 | 14 | To continue utilizing the `Send Message` and `Send And Wait For Reply` widgets while working with custom channels, we need to leverage the underlying concept that each message sent via these widgets will invoke a Programmable Chat webhook. This remains true even when we are implementing Twilio Conversations / Flex Conversations. 15 | 16 | ## Solution Instructions 17 | 18 | 1. Ensure you have Conversations Adapters deployed successfully in your Twilio Flex instance 19 | 1. Login to [Twilio Console](https://console.twilio.com/) and select `Explore Products` 20 | 1. Select `Chat` 21 | 1. Under `Chat Services`, click on `Flex Chat Service` 22 | - ![Flex Chat Service](/img/guides/studio-workaround-prog-chat.png) 23 | 1. Select `Webhooks` from the left-hand side menu bar and scroll down till you see the `Post-Event Webhooks` section 24 | 1. Under `Post-Event Webhooks` section 25 | - CALLBACK URL: `https://twilio-flex-conversations-adapters-<>-dev.twil.io/api/common/studio-workaround` 26 | - CALLBACK EVENTS: `onMessageSent: Sent a Message` (You will only need this particular event) 27 | - ![Webhook](/img/guides/studio-workaround-webhook.png) 28 | 1. Click on `Save` 29 | 1. You are now ready to leverage `Send Message` and `Send And Wait For Reply` widgets in Twilio Studio without any additional modifications! 30 | 31 | ## Known Limitations 32 | 33 | 1. There will be a notable latency (~10s) between messages sent by Twilio Studio and receiving at the custom channel's end 34 | -------------------------------------------------------------------------------- /docs/docs/channels/viber.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Viber 3 | --- 4 | 5 | # Viber 6 | 7 |

8 | Studio Flow Overall 9 |

10 | 11 | # Required Variables 12 | 13 | 1. `VIBER_STUDIO_FLOW_SID` ([Guide](../getting-started/create-studio-flow)) 14 | 1. `VIBER_AUTH_TOKEN` 15 | 16 | ## Setup 17 | 18 | 1. Ensure you have installed and created a Viber account on your mobile phone 19 | 1. Login to [Viber Admin Panel](https://partners.viber.com/account/create-bot-account) 20 | 1. Follow on-screen instructions to create a bot account 21 | 1. Obtain the value for `VIBER_AUTH_TOKEN` which is under `Token` 22 | - ![Viber Token](/img/channels/viber-token.png) 23 | 24 | :::info 25 | 26 | Ensure that you have obtained **all** the necessary values for the variables stated in `Required Variables` 27 | 28 | ::: 29 | 30 | ## Configure Incoming Webhook 31 | 32 | 1. Ensure you have deployed Conversations Adapters into your Twilio Flex account 33 | 1. The Conversations Adapters incoming webhook URL should be in the format of `https://twilio-flex-conversations-adapters--dev.twil.io/api/viber/incoming` 34 | 1. Using [Postman App](https://www.postman.com/downloads/) or cURL, send a request with the following to configure the webhook: 35 | - URL: `https://chatapi.viber.com/pa/set_webhook` 36 | - Method: `POST` 37 | - Headers: `X-Viber-Auth-Token` with the value of `VIBER_AUTH_TOKEN` 38 | - Body (JSON): 39 | ```json 40 | { 41 | "url": "https://twilio-flex-conversations-adapters--dev.twil.io/api/viber/incoming", 42 | "event_types": [ 43 | "message", 44 | "delivered", 45 | "seen", 46 | "failed", 47 | "subscribed", 48 | "unsubscribed", 49 | "conversation_started" 50 | ], 51 | "send_name": true, 52 | "send_photo": true 53 | } 54 | ``` 55 | 56 | ## Known Limitations 57 | 58 | - Unable to verify Viber's webhook signature as Twilio Serverless Functions do not support BigInt (`message_token` attribute of the webhook payload) 59 | -------------------------------------------------------------------------------- /serverless-functions/src/functions/api/viber/viber_types.private.ts: -------------------------------------------------------------------------------- 1 | export type ViberContext = { 2 | VIBER_STUDIO_FLOW_SID: string; 3 | VIBER_AUTH_TOKEN: string; 4 | ACCOUNT_SID: string; 5 | AUTH_TOKEN: string; 6 | DOMAIN_NAME_OVERRIDE: string; 7 | }; 8 | 9 | export type ViberMessage = ViberBaseMessage; 10 | 11 | export enum ViberMessageType { 12 | TEXT = "text", 13 | FILE = "file", 14 | CONTACT = "contact", 15 | LOCATION = "location", 16 | STICKER = "sticker", 17 | PICTURE = "picture", 18 | VIDEO = "video", 19 | URL = "url", 20 | } 21 | export type ViberBaseMessage = { 22 | request: any; 23 | receiver: string; 24 | min_api_version: number; 25 | sender: { 26 | id: string; 27 | name: string; 28 | avatar: string; 29 | }; 30 | tracking_data: string; 31 | message: 32 | | ViberMessageText 33 | | ViberMessagePicture 34 | | ViberMessageVideo 35 | | ViberMessageFile 36 | | ViberMessageContact 37 | | ViberMessageLocation 38 | | ViberMessageUrl 39 | | ViberMessageSticker; 40 | }; 41 | 42 | export type ViberMessageText = { 43 | type: ViberMessageType.TEXT; 44 | text: string; 45 | }; 46 | 47 | export type ViberMessagePicture = { 48 | type: ViberMessageType.PICTURE; 49 | media: string; 50 | thumbnail: string; 51 | file_name: string; 52 | }; 53 | 54 | export type ViberMessageVideo = { 55 | type: ViberMessageType.VIDEO; 56 | media: string; 57 | thumbnail: string; 58 | size: number; 59 | duration: number; 60 | file_name: string; 61 | }; 62 | 63 | export type ViberMessageFile = { 64 | type: ViberMessageType.FILE; 65 | media: string; 66 | thumbnail: string; 67 | size: number; 68 | file_name: string; 69 | }; 70 | 71 | export type ViberMessageContact = { 72 | type: ViberMessageType.CONTACT; 73 | contact: { 74 | name: string; 75 | phone_number: string; 76 | }; 77 | }; 78 | 79 | export type ViberMessageLocation = { 80 | type: ViberMessageType.LOCATION; 81 | location: { 82 | lat: string; 83 | lon: string; 84 | }; 85 | }; 86 | 87 | export type ViberMessageUrl = { 88 | type: ViberMessageType.URL; 89 | media: string; 90 | }; 91 | 92 | export type ViberMessageSticker = { 93 | type: ViberMessageType.STICKER; 94 | sticker_id: string; 95 | }; 96 | -------------------------------------------------------------------------------- /serverless-functions/src/functions/api/googlechat/incoming.ts: -------------------------------------------------------------------------------- 1 | // Imports global types 2 | import "@twilio-labs/serverless-runtime-types"; 3 | 4 | // Fetches specific types 5 | import { 6 | ServerlessCallback, 7 | ServerlessFunctionSignature, 8 | } from "@twilio-labs/serverless-runtime-types/types"; 9 | import * as GoogleChatTypes from "./googlechat_types.private"; 10 | import * as Helper from "./googlechat.helper.private"; 11 | 12 | const { GoogleChatMessageType } = ( 13 | require(Runtime.getFunctions()["api/googlechat/googlechat_types"].path) 14 | ); 15 | const { googleChatVerifyJwt, wrappedSendToFlex } = ( 16 | require(Runtime.getFunctions()["api/googlechat/googlechat.helper"].path) 17 | ); 18 | 19 | export const handler: ServerlessFunctionSignature< 20 | GoogleChatTypes.GoogleChatContext, 21 | GoogleChatTypes.GoogleChatBaseMessage 22 | > = async (context, event, callback: ServerlessCallback) => { 23 | try { 24 | console.log("event received - /api/googlechat/incoming: ", event); 25 | 26 | // Step 1: Verify Google JWT 27 | if ( 28 | !event.request.headers.authorization || 29 | !event.request.headers.authorization.startsWith("Bearer ") 30 | ) { 31 | return callback("Invalid JWT"); 32 | } 33 | const authorizationHeader = event.request.headers.authorization; 34 | const googleJwt = authorizationHeader.substring( 35 | 7, 36 | authorizationHeader.length 37 | ); 38 | const validateResult = await googleChatVerifyJwt(googleJwt); 39 | if (!validateResult) { 40 | console.log("Invalid JWT"); 41 | return callback("Invalid JWT"); 42 | } 43 | 44 | // Step 2: Process Message 45 | if ( 46 | event.type !== "MESSAGE" || 47 | !event.space || 48 | event.space.type !== "DM" || 49 | !event.user || 50 | !event.user.name 51 | ) { 52 | // Supports 'MESSAGE' with Direct Messaging (DM) - Graceful Response 53 | return callback(null, {}); 54 | } 55 | const parsedUserId = event.user.name.replace("users/", ""); 56 | await wrappedSendToFlex(context, parsedUserId, event); 57 | return callback(null, {}); 58 | } catch (err) { 59 | console.log(err); 60 | return callback("outer catch error"); 61 | } 62 | }; 63 | -------------------------------------------------------------------------------- /docs/docs/channels/overview.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Overview 3 | sidebar_position: 1 4 | --- 5 | 6 | # Overview 7 | 8 | :::info 9 | 10 | To enable any of the pre-built channels, you will need to obtain all the respective values under the `Required Variables` column and insert them into `GitHub Environments - Secrets` or your `.env` file. 11 | 12 | ::: 13 | 14 | | Channel | Integration Type | Required Variables | Supports Text | Supports Images | Supports Video | 15 | | :-------------------------: | :------------------------------------------------------------------------------------------: | -------------------------------------------------------------------------------------------------------------------------------------- | :----------------: | :----------------: | :----------------: | 16 | | [Google Chat](./googlechat) | [Google Chat REST API](https://developers.google.com/chat/api/reference/rest) | `GOOGLECHAT_STUDIO_FLOW_SID`
`GOOGLECHAT_SERVICE_ACCOUNT_KEY_BASE64` | :white_check_mark: | :white_check_mark: | :x: | 17 | | [Instagram](./instagram) | [Instagram Messenger API](https://developers.facebook.com/docs/messenger-platform/instagram) | `INSTAGRAM_STUDIO_FLOW_SID`
`INSTAGRAM_APP_SECRET`
`INSTAGRAM_PAGE_ACCESS_TOKEN`
`INSTAGRAM_WEBHOOK_VERIFY_TOKEN` | :white_check_mark: | :white_check_mark: | :white_check_mark: | 18 | | [LINE](./line) | [LINE Messaging API](https://developers.line.biz/en/docs/messaging-api/overview/) | `LINE_STUDIO_FLOW_SID`
`LINE_CHANNEL_ID`
`LINE_CHANNEL_SECRET`
`LINE_CHANNEL_ACCESS_TOKEN` | :white_check_mark: | :white_check_mark: | :white_check_mark: | 19 | | [Viber](./viber) | [Viber REST Bot API](https://developers.viber.com/docs/api/rest-bot-api/) | `VIBER_STUDIO_FLOW_SID`
`VIBER_AUTH_TOKEN` | :white_check_mark: | :white_check_mark: | :white_check_mark: | 20 | -------------------------------------------------------------------------------- /serverless-functions/src/functions/api/line/incoming.ts: -------------------------------------------------------------------------------- 1 | // Imports global types 2 | import "@twilio-labs/serverless-runtime-types"; 3 | 4 | // Fetches specific types 5 | import { 6 | ServerlessCallback, 7 | ServerlessFunctionSignature, 8 | } from "@twilio-labs/serverless-runtime-types/types"; 9 | import * as LINETypes from "./line_types.private"; 10 | import * as Helper from "./line.helper.private"; 11 | 12 | // Load Libraries 13 | const { LINEMessageType } = ( 14 | require(Runtime.getFunctions()["api/line/line_types"].path) 15 | ); 16 | const { wrappedSendToFlex, lineValidateSignature } = ( 17 | require(Runtime.getFunctions()["api/line/line.helper"].path) 18 | ); 19 | export const handler: ServerlessFunctionSignature< 20 | LINETypes.LINEContext, 21 | any 22 | > = async (context, event, callback: ServerlessCallback) => { 23 | console.log("event received - /api/line/incoming: ", event); 24 | try { 25 | // Debug: Console Log Incoming Events 26 | console.log("---Start of Raw Event---"); 27 | console.log(event); 28 | console.log(event.request); 29 | console.log(event.destination); 30 | console.log(event.events); 31 | console.log("---End of Raw Event---"); 32 | 33 | // Step 1: Verify LINE signature 34 | const lineSignature = event.request.headers["x-line-signature"]; 35 | const lineSignatureBody = JSON.stringify({ 36 | destination: event.destination, 37 | events: event.events, 38 | }); 39 | const validSignature = lineValidateSignature( 40 | lineSignature, 41 | lineSignatureBody, 42 | context.LINE_CHANNEL_SECRET 43 | ); 44 | if (!validSignature) { 45 | console.log("Invalid Signature"); 46 | return callback("Invalid Signature"); 47 | } 48 | 49 | // Step 2: Process Twilio Conversations 50 | // -- Handle Multiple Events Recieved in Webhook 51 | for (const msg of event.events) { 52 | // -- Process Each Event 53 | if (msg.source.userId && msg.message) { 54 | const userId = msg.source.userId; 55 | const message = msg.message; 56 | await wrappedSendToFlex(context, userId, message); 57 | } 58 | } 59 | return callback(null, { 60 | success: true, 61 | }); 62 | } catch (err) { 63 | console.log(err); 64 | return callback("outer catch error"); 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /serverless-functions/src/functions/api/instagram/incoming.ts: -------------------------------------------------------------------------------- 1 | // Imports global types 2 | import "@twilio-labs/serverless-runtime-types"; 3 | 4 | // Fetches specific types 5 | import { 6 | ServerlessCallback, 7 | ServerlessFunctionSignature, 8 | } from "@twilio-labs/serverless-runtime-types/types"; 9 | import * as InstagramTypes from "./instagram_types.private"; 10 | import * as Helper from "./instagram.helper.private"; 11 | 12 | const { InstagramMessageType } = ( 13 | require(Runtime.getFunctions()["api/instagram/instagram_types"].path) 14 | ); 15 | const { wrappedSendToFlex, instagramValidateSignature } = ( 16 | require(Runtime.getFunctions()["api/instagram/instagram.helper"].path) 17 | ); 18 | 19 | export const handler: ServerlessFunctionSignature< 20 | InstagramTypes.InstagramContext, 21 | InstagramTypes.InstagramBaseMessage 22 | > = async (context, event, callback: ServerlessCallback) => { 23 | try { 24 | console.log("event received - /api/instagram/incoming: ", event); 25 | 26 | // Meta Verification Request 27 | if ( 28 | event["hub.mode"] && 29 | event["hub.challenge"] && 30 | event["hub.verify_token"] === context.INSTAGRAM_WEBHOOK_VERIFY_TOKEN 31 | ) { 32 | return callback(null, event["hub.challenge"]); 33 | } 34 | 35 | // Step 1: Verify Meta signature 36 | const metaSignature = event.request?.headers[ 37 | "x-hub-signature-256" 38 | ]?.replace("sha256=", ""); 39 | const metaSignatureBody = event; 40 | metaSignatureBody.request ? delete metaSignatureBody["request"] : null; 41 | const replaceForwardSlashPayload = JSON.stringify( 42 | metaSignatureBody 43 | ).replace(/\//g, "\\/"); 44 | const validSignature = instagramValidateSignature( 45 | metaSignature, 46 | replaceForwardSlashPayload, 47 | context.INSTAGRAM_APP_SECRET 48 | ); 49 | if (!validSignature) { 50 | console.log("Invalid Signature"); 51 | return callback("Invalid Signature"); 52 | } 53 | // Step 2: Process Instagram Message 54 | if (event.object !== "instagram") { 55 | console.log("Not an Instagram payload"); 56 | return callback("Invalid Payload"); 57 | } 58 | 59 | // -- Handle multiple entries 60 | for (const entry of event.entry) { 61 | if (entry.time && entry.id && entry.messaging) { 62 | for (const msg of entry.messaging) { 63 | // Send to Flex 64 | const userId = msg.sender.id; 65 | const message = msg.message; 66 | await wrappedSendToFlex(context, userId, msg); 67 | } 68 | } 69 | } 70 | return callback(null, { 71 | success: true, 72 | }); 73 | } catch (err) { 74 | console.log(err); 75 | return callback("outer catch error"); 76 | } 77 | }; 78 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # NPM Package Lock 2 | package-lock.json 3 | 4 | # Twilio Serverless 5 | .twiliodeployinfo 6 | .DS_Store 7 | 8 | # Logs 9 | logs 10 | *.log 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | lerna-debug.log* 15 | .pnpm-debug.log* 16 | 17 | # Diagnostic reports (https://nodejs.org/api/report.html) 18 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 19 | 20 | # Runtime data 21 | pids 22 | *.pid 23 | *.seed 24 | *.pid.lock 25 | 26 | # Directory for instrumented libs generated by jscoverage/JSCover 27 | lib-cov 28 | 29 | # Coverage directory used by tools like istanbul 30 | coverage 31 | *.lcov 32 | 33 | # nyc test coverage 34 | .nyc_output 35 | 36 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 37 | .grunt 38 | 39 | # Bower dependency directory (https://bower.io/) 40 | bower_components 41 | 42 | # node-waf configuration 43 | .lock-wscript 44 | 45 | # Compiled binary addons (https://nodejs.org/api/addons.html) 46 | build/Release 47 | 48 | # Dependency directories 49 | node_modules/ 50 | jspm_packages/ 51 | 52 | # Snowpack dependency directory (https://snowpack.dev/) 53 | web_modules/ 54 | 55 | # TypeScript cache 56 | *.tsbuildinfo 57 | 58 | # Optional npm cache directory 59 | .npm 60 | 61 | # Optional eslint cache 62 | .eslintcache 63 | 64 | # Optional stylelint cache 65 | .stylelintcache 66 | 67 | # Microbundle cache 68 | .rpt2_cache/ 69 | .rts2_cache_cjs/ 70 | .rts2_cache_es/ 71 | .rts2_cache_umd/ 72 | 73 | # Optional REPL history 74 | .node_repl_history 75 | 76 | # Output of 'npm pack' 77 | *.tgz 78 | 79 | # Yarn Integrity file 80 | .yarn-integrity 81 | 82 | # dotenv environment variable files 83 | .env 84 | .env.development.local 85 | .env.test.local 86 | .env.production.local 87 | .env.local 88 | 89 | # parcel-bundler cache (https://parceljs.org/) 90 | .cache 91 | .parcel-cache 92 | 93 | # Next.js build output 94 | .next 95 | out 96 | 97 | # Nuxt.js build / generate output 98 | .nuxt 99 | dist 100 | 101 | # Gatsby files 102 | .cache/ 103 | # Comment in the public line in if your project uses Gatsby and not Next.js 104 | # https://nextjs.org/blog/next-9-1#public-directory-support 105 | # public 106 | 107 | # vuepress build output 108 | .vuepress/dist 109 | 110 | # vuepress v2.x temp and cache directory 111 | .temp 112 | .cache 113 | 114 | # Docusaurus cache and generated files 115 | .docusaurus 116 | 117 | # Serverless directories 118 | .serverless/ 119 | 120 | # FuseBox cache 121 | .fusebox/ 122 | 123 | # DynamoDB Local files 124 | .dynamodb/ 125 | 126 | # TernJS port file 127 | .tern-port 128 | 129 | # Stores VSCode versions used for testing VSCode extensions 130 | .vscode-test 131 | 132 | # yarn v2 133 | .yarn/cache 134 | .yarn/unplugged 135 | .yarn/build-state.yml 136 | .yarn/install-state.gz 137 | .pnp.* 138 | -------------------------------------------------------------------------------- /serverless-functions/.gitignore: -------------------------------------------------------------------------------- 1 | # NPM Package Lock 2 | package-lock.json 3 | 4 | # Twilio Serverless 5 | .twiliodeployinfo 6 | .DS_Store 7 | 8 | # Logs 9 | logs 10 | *.log 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | lerna-debug.log* 15 | .pnpm-debug.log* 16 | 17 | # Diagnostic reports (https://nodejs.org/api/report.html) 18 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 19 | 20 | # Runtime data 21 | pids 22 | *.pid 23 | *.seed 24 | *.pid.lock 25 | 26 | # Directory for instrumented libs generated by jscoverage/JSCover 27 | lib-cov 28 | 29 | # Coverage directory used by tools like istanbul 30 | coverage 31 | *.lcov 32 | 33 | # nyc test coverage 34 | .nyc_output 35 | 36 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 37 | .grunt 38 | 39 | # Bower dependency directory (https://bower.io/) 40 | bower_components 41 | 42 | # node-waf configuration 43 | .lock-wscript 44 | 45 | # Compiled binary addons (https://nodejs.org/api/addons.html) 46 | build/Release 47 | 48 | # Dependency directories 49 | node_modules/ 50 | jspm_packages/ 51 | 52 | # Snowpack dependency directory (https://snowpack.dev/) 53 | web_modules/ 54 | 55 | # TypeScript cache 56 | *.tsbuildinfo 57 | 58 | # Optional npm cache directory 59 | .npm 60 | 61 | # Optional eslint cache 62 | .eslintcache 63 | 64 | # Optional stylelint cache 65 | .stylelintcache 66 | 67 | # Microbundle cache 68 | .rpt2_cache/ 69 | .rts2_cache_cjs/ 70 | .rts2_cache_es/ 71 | .rts2_cache_umd/ 72 | 73 | # Optional REPL history 74 | .node_repl_history 75 | 76 | # Output of 'npm pack' 77 | *.tgz 78 | 79 | # Yarn Integrity file 80 | .yarn-integrity 81 | 82 | # dotenv environment variable files 83 | .env 84 | .env.development.local 85 | .env.test.local 86 | .env.production.local 87 | .env.local 88 | 89 | # parcel-bundler cache (https://parceljs.org/) 90 | .cache 91 | .parcel-cache 92 | 93 | # Next.js build output 94 | .next 95 | out 96 | 97 | # Nuxt.js build / generate output 98 | .nuxt 99 | dist 100 | 101 | # Gatsby files 102 | .cache/ 103 | # Comment in the public line in if your project uses Gatsby and not Next.js 104 | # https://nextjs.org/blog/next-9-1#public-directory-support 105 | # public 106 | 107 | # vuepress build output 108 | .vuepress/dist 109 | 110 | # vuepress v2.x temp and cache directory 111 | .temp 112 | .cache 113 | 114 | # Docusaurus cache and generated files 115 | .docusaurus 116 | 117 | # Serverless directories 118 | .serverless/ 119 | 120 | # FuseBox cache 121 | .fusebox/ 122 | 123 | # DynamoDB Local files 124 | .dynamodb/ 125 | 126 | # TernJS port file 127 | .tern-port 128 | 129 | # Stores VSCode versions used for testing VSCode extensions 130 | .vscode-test 131 | 132 | # yarn v2 133 | .yarn/cache 134 | .yarn/unplugged 135 | .yarn/build-state.yml 136 | .yarn/install-state.gz 137 | .pnp.* 138 | -------------------------------------------------------------------------------- /docs/docs/channels/instagram.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Instagram 3 | --- 4 | 5 | # Instagram 6 | 7 |

8 | Instagram Splash 9 |

10 | 11 | :::info 12 | 13 | If your Meta developer app is in `Development` mode and NOT `Live` mode, you will need to add your testing instagram account (i.e. the account that you are using to send messages into Instagram Business Account) into [Meta Apps](https://developers.facebook.com/docs/development/build-and-test/app-roles/) 14 | 15 | ::: 16 | 17 | # Required Variables 18 | 19 | 1. `INSTAGRAM_STUDIO_FLOW_SID` ([Guide](../getting-started/create-studio-flow)) 20 | 1. `INSTAGRAM_APP_SECRET` 21 | 1. `INSTAGRAM_PAGE_ACCESS_TOKEN` 22 | 1. `INSTAGRAM_WEBHOOK_VERIFY_TOKEN` 23 | 24 | ## Setup 25 | 26 | 1. Ensure you have the following before proceeding 27 | - An Instagram Professional account ([Guide](https://help.instagram.com/502981923235522)) 28 | - A Facebook Page connected to that account ([Guide](https://www.facebook.com/business/help/connect-instagram-to-page)) 29 | - A Facebook Developer account that can perform Tasks with atleast "Moderate" level access on that Page ([Guide](https://developers.facebook.com/docs/development/register/)) 30 | - A registered Facebook App with Basic settings configured ([Guide](https://developers.facebook.com/docs/development/create-an-app/)) 31 | 1. Login to [Meta Developer Console](https://developers.facebook.com/apps/) 32 | 1. Select your Meta developer app that is managing your Facebook Page 33 | 1. Under `Add products to your app`, click `Set up` for `Messenger` 34 | 1. Obtain the value for `INSTAGRAM_APP_SECRET` which is under `Settings > Basic` 35 | - ![Instagram App Secret](/img/channels/instagram-app-secret.png) 36 | 1. Obtain the value for `INSTAGRAM_PAGE_ACCESS_TOKEN` which is under `Messenger > Instagram Settings > Access Token`. Click the `Generate token` button. 37 | - ![Instagram Page Access Token](/img/channels/instagram-page-access-token.png) 38 | 1. Obtain the value for `INSTAGRAM_WEBHOOK_VERIFY_TOKEN` which is under `Messenger > Instagram Settings > Webhooks`. The verify token is a self-inserted value and can be any freeform text. 39 | - ![Instagram Webhook Verify Token](/img/channels/instagram-webhook-verify-token.png) 40 | 41 | :::info 42 | 43 | Ensure that you have obtained **all** the necessary values for the variables stated in `Required Variables` 44 | 45 | ::: 46 | 47 | ## Configure Incoming Webhook 48 | 49 | 1. Ensure you have deployed Conversations Adapters into your Twilio Flex account 50 | 1. Ensure you are logged into [Meta Developer Console](https://developers.facebook.com/apps/) 51 | 1. Select your Meta developer app 52 | 1. Under `Messenger > Instagram Settings`, insert your deployed incoming webhook. For the `Verify token`, insert any random string that you wish. You will need the value of `Verify token` as you will need to insert them into `GitHub Environments - Secrets` or your `.env` file. 53 | - For Subscriptions, you only need `messages`. 54 | - The Conversations Adapters incoming webhook URL should be in the format of `https://twilio-flex-conversations-adapters--dev.twil.io/api/instagram/incoming` 55 | - ![Instagram Webhook](/img/channels/instagram-set-webhook.png) 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Twilio Flex - Conversations Adapters 2 | 3 | 4 | 5 | [![All Contributors](https://img.shields.io/badge/all_contributors-2-orange.svg?style=flat-square)](#contributors-) 6 | 7 | 8 | 9 | [![MIT License](https://img.shields.io/badge/license-MIT-blue?style=flat-square&labelColor=black)](https://github.com/leroychan/twilio-flex-conversations-adapters/blob/master/LICENSE.md)[![Prettier Code Formatting](https://img.shields.io/badge/code_style-prettier-brightgreen.svg?style=flat-square&labelColor=black)](https://prettier.io) 10 | 11 | _**Conversations Adapters**_ is a comprehensive framework designed to facilitate custom channel development for Twilio Flex using Twilio Conversations. This project empowers developers with a collection of pre-built connectors while also offering extensive extendibility options, enabling seamless integration with existing channels and facilitating the creation of new connectors. 12 | 13 |

14 | Conversations Adapters 15 |

16 | 17 | ## Pre-Built Connectors 18 | 19 | - Google Chat 20 | - Instagram 21 | - LINE 22 | - Viber 23 | 24 | ## Documentation Page 25 | 26 | Conversations Adapters: [Conversations Adapters Documentation Page](https://leroychan.github.io/twilio-flex-conversations-adapters) 27 | 28 | ## Contributors 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |
Leroy Chan
Leroy Chan

🔌
Christopher Connolly
Christopher Connolly

🔌
41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | ## License 55 | 56 | MIT 57 | 58 | ## Disclaimer: Open Source Project 59 | 60 | Conversations Adapters is an open source project and is not affiliated, endorsed, or associated with Twilio in any manner. This project is maintained and developed by independent contributors who aim to provide a comprehensive framework and collection of connectors for chat-based channels. While Conversations Adapters may support integration with various chat platforms, it is not officially supported or endorsed by Twilio. Any references made to Twilio or its products within this project are purely for informative purposes. For official support or inquiries related to Twilio, please refer to their official documentation and support channels. 61 | -------------------------------------------------------------------------------- /docs/docs/channels/googlechat.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Google Chat 3 | --- 4 | 5 | # Google Chat 6 | 7 |

8 | Google Chat Splash 9 |

10 | 11 | # Required Variables 12 | 13 | 1. `GOOGLECHAT_STUDIO_FLOW_SID` ([Guide](../getting-started/create-studio-flow)) 14 | 1. `GOOGLECHAT_SERVICE_ACCOUNT_KEY_BASE64` 15 | 16 | ## Setup 17 | 18 | 1. Ensure you have the following before proceeding 19 | - A Google Workspace account with access to Google Chat. 20 | - A Google Cloud Project ([Guide](https://developers.google.com/workspace/guides/create-project)) 21 | 1. Login to your [Google Cloud Console](https://console.cloud.google.com/apis/api/chat.googleapis.com/) and Enable Google Chat API 22 | 1. Within your `Google Cloud Console > Google Chat API`, click on `Credentials` followed by `+ Create Credentials` 23 | 1. Select `Service Account` 24 | 1. Follow the on-screen instructions to create the `Service Account` and click `CREATE AND CONTINUE` 25 | - Service Account Name: `flex-conversations-adapaters` 26 | - Service Account ID: `<>` 27 | - Service Account Description: `Conversations Adapters - Google Chat` 28 | 1. Click `DONE`. You can skip the optional steps. 29 | - ![Service Account](/img/channels/googlechat-service-account.png) 30 | 1. After your `Service Account` has been successfully created, click into it and go to `KEYS > ADD KEYS > Create new key`. Select `JSON` for the Key Type and click `CREATE`. 31 | - The `.json` credentials file will be automatically downloaded to your local computer 32 | 1. Open up your terminal / bash client and `cd` to the folder where your `.json` credential files reside and issue the following command 33 | - ```bash 34 | cat <> | base64 35 | ``` 36 | 1. Copy the entire output from your terminal / bash client as it will be the value for `GOOGLECHAT_SERVICE_ACCOUNT_KEY_BASE64` 37 | - ![Base64 Credentials](/img/channels/googlechat-base64-credentials.png) 38 | 39 | ## Configure Incoming Webhook 40 | 41 | 1. Ensure you have deployed Conversations Adapters into your Twilio Flex account 42 | 1. Ensure you are logged into [Google Cloud Console - Chat API](https://console.cloud.google.com/apis/api/chat.googleapis.com/) 43 | 1. Select `Google Chat API > Configuration` 44 | 1. Under `App Url`, insert your deployed incoming webhook 45 | - The Conversations Adapters incoming webhook URL should be in the format of `https://twilio-flex-conversations-adapters--dev.twil.io/api/googlechat/incoming` 46 | - ![Google Chat Webhook](/img/channels/googlechat-set-webhook.png) 47 | 1. To make the Google Chat App visible, add the email addresses of people/group under the `Visibility` section 48 | - ![Google Chat Visibility](/img/channels/googlechat-visibility.png) 49 | 50 | ## Known Limitations 51 | 52 | - Images sent from Flex Agent to End User will be invalidated after 300 seconds (Read more: [here](https://www.twilio.com/docs/conversations/media-support-conversations#platform-differences-for-media-messaging)). If you require longer validity, do consider extending the codebase to upload media to Google Drive. 53 | - Google's Service Account Authentication for Google Chat API does not support Media Upload (i.e. Video). Read [here](https://developers.google.com/chat/api/guides/auth#asynchronous-chat-calls) for more information 54 | -------------------------------------------------------------------------------- /docs/docusaurus.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Note: type annotations allow type checking and IDEs autocompletion 3 | 4 | const lightCodeTheme = require("prism-react-renderer/themes/github"); 5 | const darkCodeTheme = require("prism-react-renderer/themes/dracula"); 6 | 7 | /** @type {import('@docusaurus/types').Config} */ 8 | const config = { 9 | title: "Conversations Adapaters", 10 | tagline: "Conversations Adapters", 11 | favicon: "img/flex.png", 12 | 13 | // Set the production url of your site here 14 | url: "https://leroychan.github.io", 15 | // Set the // pathname under which your site is served 16 | // For GitHub pages deployment, it is often '//' 17 | baseUrl: "/twilio-flex-conversations-adapters", 18 | 19 | // GitHub pages deployment config. 20 | // If you aren't using GitHub pages, you don't need these. 21 | organizationName: "leroychan", // Usually your GitHub org/user name. 22 | projectName: "twilio-flex-conversations-adapters", // Usually your repo name. 23 | 24 | onBrokenLinks: "throw", 25 | onBrokenMarkdownLinks: "warn", 26 | 27 | // Even if you don't use internalization, you can use this field to set useful 28 | // metadata like html lang. For example, if your site is Chinese, you may want 29 | // to replace "en" with "zh-Hans". 30 | i18n: { 31 | defaultLocale: "en", 32 | locales: ["en"], 33 | }, 34 | 35 | presets: [ 36 | [ 37 | "classic", 38 | /** @type {import('@docusaurus/preset-classic').Options} */ 39 | ({ 40 | docs: { 41 | sidebarPath: require.resolve("./sidebars.js"), 42 | routeBasePath: "/", 43 | }, 44 | theme: { 45 | customCss: require.resolve("./src/css/custom.css"), 46 | }, 47 | }), 48 | ], 49 | ], 50 | 51 | themeConfig: 52 | /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ 53 | ({ 54 | // Replace with your project's social card 55 | image: "img/docusaurus-social-card.jpg", 56 | navbar: { 57 | logo: { 58 | alt: "Conversations Adapters Logo", 59 | src: "img/conversations_adapters.png", 60 | }, 61 | items: [ 62 | { 63 | href: "https://github.com/leroychan/twilio-flex-conversations-adapters", 64 | label: "GitHub", 65 | position: "right", 66 | }, 67 | ], 68 | }, 69 | footer: { 70 | style: "dark", 71 | links: [ 72 | { 73 | title: "Twilio Flex Docs", 74 | items: [ 75 | { 76 | label: "Flex UI Documentation", 77 | href: "https://assets.flex.twilio.com/docs/releases/flex-ui/latest", 78 | }, 79 | ], 80 | }, 81 | { 82 | title: "More", 83 | items: [ 84 | { 85 | label: "GitHub", 86 | href: "https://github.com/leroychan/twilio-flex-conversations-adapters", 87 | }, 88 | ], 89 | }, 90 | ], 91 | copyright: `Copyright © ${new Date().getFullYear()} Twilio Flex Conversations Adapters`, 92 | }, 93 | prism: { 94 | theme: lightCodeTheme, 95 | darkTheme: darkCodeTheme, 96 | }, 97 | }), 98 | }; 99 | 100 | module.exports = config; 101 | -------------------------------------------------------------------------------- /serverless-functions/.twilioserverlessrc: -------------------------------------------------------------------------------- 1 | { 2 | "commands": {}, 3 | "environments": {}, 4 | "projects": {}, 5 | // "assets": true /* Upload assets. Can be turned off with --no-assets */, 6 | // "assetsFolder": null /* Specific folder name to be used for static assets */, 7 | // "buildSid": null /* An existing Build SID to deploy to the new environment */, 8 | // "createEnvironment": false /* Creates environment if it couldn't find it. */, 9 | // "cwd": null /* Sets the directory of your existing Serverless project. Defaults to current directory */, 10 | // "detailedLogs": false /* Toggles detailed request logging by showing request body and query params */, 11 | // "edge": null /* Twilio API Region */, 12 | // "env": null /* Path to .env file for environment variables that should be installed */, 13 | // "environment": "dev" /* The environment name (domain suffix) you want to use for your deployment. Alternatively you can specify an environment SID starting with ZE. */, 14 | // "extendedOutput": false /* Show an extended set of properties on the output */, 15 | // "force": false /* Will run deployment in force mode. Can be dangerous. */, 16 | // "forkProcess": true /* Disable forking function processes to emulate production environment */, 17 | // "functionSid": null /* Specific Function SID to retrieve logs for */, 18 | // "functions": true /* Upload functions. Can be turned off with --no-functions */, 19 | // "functionsFolder": null /* Specific folder name to be used for static functions */, 20 | // "inspect": null /* Enables Node.js debugging protocol */, 21 | // "inspectBrk": null /* Enables Node.js debugging protocol, stops execution until debugger is attached */, 22 | // "legacyMode": false /* Enables legacy mode, it will prefix your asset paths with /assets */, 23 | // "live": true /* Always serve from the current functions (no caching) */, 24 | // "loadLocalEnv": false /* Includes the local environment variables */, 25 | // "loadSystemEnv": false /* Uses system environment variables as fallback for variables specified in your .env file. Needs to be used with --env explicitly specified. */, 26 | // "logCacheSize": null /* Tailing the log endpoint will cache previously seen entries to avoid duplicates. The cache is topped at a maximum of 1000 by default. This option can change that. */, 27 | // "logLevel": "info" /* Level of logging messages. */, 28 | // "logs": true /* Toggles request logging */, 29 | // "ngrok": null /* Uses ngrok to create a public url. Pass a string to set the subdomain (requires a paid-for ngrok account). */, 30 | // "outputFormat": "" /* Output the results in a different format */, 31 | // "overrideExistingProject": false /* Deploys Serverless project to existing service if a naming conflict has been found. */, 32 | // "port": "3000" /* Override default port of 3000 */, 33 | // "production": false /* Promote build to the production environment (no domain suffix). Overrides environment flag */, 34 | // "properties": null /* Specify the output properties you want to see. Works best on single types */, 35 | // "region": null /* Twilio API Region */, 36 | "runtime": "node18" /* The version of Node.js to deploy the build to. (node14) */, 37 | // "serviceName": null /* Overrides the name of the Serverless project. Default: the name field in your package.json */, 38 | // "serviceSid": null /* SID of the Twilio Serverless Service to deploy to */, 39 | // "sourceEnvironment": null /* SID or suffix of an existing environment you want to deploy from. */, 40 | // "tail": false /* Continuously stream the logs */, 41 | // "template": null /* undefined */, 42 | } -------------------------------------------------------------------------------- /.github/workflows/flex_deploy.yaml: -------------------------------------------------------------------------------- 1 | name: Deploy to Flex 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | environment: 7 | required: true 8 | type: environment 9 | description: "Environment to use for deployment" 10 | deploy_icons_plugin: 11 | required: true 12 | type: boolean 13 | default: false 14 | description: Deploy UI Icons Plugin? 15 | 16 | jobs: 17 | deploy-serverless: 18 | runs-on: ubuntu-latest 19 | environment: ${{ github.event.inputs.environment }} 20 | env: 21 | ENVIRONMENT: ${{ github.event.inputs.environment }} 22 | TWILIO_ACCOUNT_SID: ${{ secrets.TWILIO_ACCOUNT_SID }} 23 | TWILIO_API_KEY: ${{ secrets.TWILIO_API_KEY }} 24 | TWILIO_API_SECRET: ${{ secrets.TWILIO_API_SECRET }} 25 | GOOGLECHAT_STUDIO_FLOW_SID: ${{ secrets.GOOGLECHAT_STUDIO_FLOW_SID }} 26 | GOOGLECHAT_SERVICE_ACCOUNT_KEY_BASE64: ${{ secrets.GOOGLECHAT_SERVICE_ACCOUNT_KEY_BASE64 }} 27 | INSTAGRAM_STUDIO_FLOW_SID: ${{ secrets.INSTAGRAM_STUDIO_FLOW_SID }} 28 | INSTAGRAM_APP_SECRET: ${{ secrets.INSTAGRAM_APP_SECRET }} 29 | INSTAGRAM_PAGE_ACCESS_TOKEN: ${{ secrets.INSTAGRAM_PAGE_ACCESS_TOKEN }} 30 | INSTAGRAM_WEBHOOK_VERIFY_TOKEN: ${{ secrets.INSTAGRAM_WEBHOOK_VERIFY_TOKEN }} 31 | LINE_STUDIO_FLOW_SID: ${{ secrets.LINE_STUDIO_FLOW_SID }} 32 | LINE_CHANNEL_ID: ${{ secrets.LINE_CHANNEL_ID }} 33 | LINE_CHANNEL_SECRET: ${{ secrets.LINE_CHANNEL_SECRET }} 34 | LINE_CHANNEL_ACCESS_TOKEN: ${{ secrets.LINE_CHANNEL_ACCESS_TOKEN }} 35 | VIBER_STUDIO_FLOW_SID: ${{ secrets.VIBER_STUDIO_FLOW_SID }} 36 | VIBER_AUTH_TOKEN: ${{ secrets.VIBER_AUTH_TOKEN }} 37 | steps: 38 | - uses: actions/checkout@v3 39 | - name: Setup Node.js v18 40 | uses: actions/setup-node@v3 41 | with: 42 | node-version: "18" 43 | - name: Install NPM packages 44 | id: install-npm 45 | working-directory: serverless-functions 46 | run: | 47 | npm install 48 | npm run install-serverless-plugin 49 | - name: Setup Environment 50 | id: setup-environment 51 | working-directory: serverless-functions 52 | run: | 53 | npm run setup-environment $ENVIRONMENT 54 | - name: Deploy to Environment 55 | id: deploy-to-environment 56 | working-directory: serverless-functions 57 | run: | 58 | npm run deploy-env 59 | 60 | deploy-icons-plugin: 61 | if: | 62 | github.event.inputs.deploy_icons_plugin == 'true' 63 | runs-on: ubuntu-latest 64 | environment: ${{ github.event.inputs.environment }} 65 | env: 66 | ENVIRONMENT: ${{ github.event.inputs.environment }} 67 | TWILIO_ACCOUNT_SID: ${{ secrets.TWILIO_ACCOUNT_SID }} 68 | TWILIO_API_KEY: ${{ secrets.TWILIO_API_KEY }} 69 | TWILIO_API_SECRET: ${{ secrets.TWILIO_API_SECRET }} 70 | steps: 71 | - uses: actions/checkout@v3 72 | - name: Setup Node.js v18 73 | uses: actions/setup-node@v3 74 | with: 75 | node-version: "18" 76 | - name: Install NPM packages 77 | id: install-npm 78 | working-directory: plugin-conversations-icons 79 | run: | 80 | npm install 81 | npm run install-flex-plugin 82 | - name: Deploy to Environment 83 | id: deploy-to-environment 84 | working-directory: plugin-conversations-icons 85 | run: | 86 | npm run deploy -- --changelog="Deploy from CI/CD for commit ${{ github.sha }}" 87 | npm run release -- --name="Release from CI/CD for commit ${{ github.sha }}" --description="Release from CI/CD for commit ${{ github.sha }}" 88 | -------------------------------------------------------------------------------- /serverless-functions/src/functions/api/bot/incoming-conversation.ts: -------------------------------------------------------------------------------- 1 | import { ServerlessFunctionSignature } from "@twilio-labs/serverless-runtime-types/types"; 2 | import { 3 | signRequest, 4 | getBotId, 5 | sendMessageToBot, 6 | readConversationAttributes 7 | } from "./bot.helper.private"; 8 | 9 | // Import Twilio for Response object - using require for Twilio.Response compatibility 10 | const Twilio = require('twilio'); 11 | 12 | // Define the context interface 13 | interface BotContext { 14 | getTwilioClient: () => any; 15 | DOMAIN_NAME: string; 16 | ACCOUNT_SID: string; 17 | AUTH_TOKEN: string; 18 | [key: string]: any; // Index signature to satisfy EnvironmentVariables constraint 19 | } 20 | 21 | // Define the event interface 22 | interface BotEvent { 23 | request: { 24 | cookies: Record; 25 | headers: Record; 26 | }; 27 | Body?: string; 28 | ConversationSid: string; 29 | ChatServiceSid: string; 30 | Author: string; 31 | [key: string]: any; // Index signature for additional properties 32 | } 33 | 34 | /** 35 | * Handler for Bot onMessageAdded events 36 | */ 37 | export const handler: ServerlessFunctionSignature = 38 | async function (context, event, callback) { 39 | const assistantSid = await getBotId(context, event); 40 | 41 | const { ConversationSid, ChatServiceSid, Author } = event; 42 | const BotIdentity = 43 | typeof event.AssistantIdentity === "string" 44 | ? event.AssistantIdentity 45 | : undefined; 46 | 47 | let identity = Author.includes(":") ? Author : `user_id:${Author}`; 48 | 49 | const client = context.getTwilioClient(); 50 | 51 | const webhooks = ( 52 | await client.conversations.v1 53 | .services(ChatServiceSid) 54 | .conversations(ConversationSid) 55 | .webhooks.list() 56 | ).filter((entry: { target: string }) => entry.target === "studio"); 57 | 58 | if (webhooks.length > 0) { 59 | // ignoring if the conversation has a studio webhook set (assuming it was handed over) 60 | return callback(null, ""); 61 | } 62 | 63 | const participants = await client.conversations.v1 64 | .services(ChatServiceSid) 65 | .conversations(ConversationSid) 66 | .participants.list(); 67 | 68 | if (participants.length > 1) { 69 | // Ignoring the conversation because there is more than one human 70 | return callback(null, ""); 71 | } 72 | 73 | const token = await signRequest(context, event); 74 | const params = new URLSearchParams(); 75 | params.append("_token", token); 76 | if (typeof BotIdentity === "string") { 77 | params.append("_assistantIdentity", BotIdentity); 78 | } 79 | const body = { 80 | body: event.Body, 81 | identity: identity, 82 | session_id: `conversations__${ChatServiceSid}/${ConversationSid}`, 83 | // using a callback to handle AI Assistant responding 84 | webhook: `https://${ 85 | context.DOMAIN_NAME 86 | }/channels/conversations/response?${params.toString()}`, 87 | }; 88 | 89 | const response = new Twilio.Response(); 90 | response.appendHeader("content-type", "text/plain"); 91 | response.setBody(""); 92 | 93 | const attributes = await readConversationAttributes( 94 | context, 95 | ChatServiceSid, 96 | ConversationSid 97 | ); 98 | await client.conversations.v1 99 | .services(ChatServiceSid) 100 | .conversations(ConversationSid) 101 | .update({ 102 | attributes: JSON.stringify({ ...attributes, assistantIsTyping: true }), 103 | }); 104 | 105 | try { 106 | await sendMessageToBot(context, assistantSid, body); 107 | } catch (err) { 108 | console.error(err); 109 | } 110 | 111 | callback(null, response); 112 | }; 113 | -------------------------------------------------------------------------------- /serverless-functions/src/functions/api/line/outgoing.ts: -------------------------------------------------------------------------------- 1 | // Imports global types 2 | import "@twilio-labs/serverless-runtime-types"; 3 | // Fetches specific types 4 | import { 5 | ServerlessCallback, 6 | ServerlessFunctionSignature, 7 | } from "@twilio-labs/serverless-runtime-types/types"; 8 | 9 | import * as Helper from "./line.helper.private"; 10 | import * as Util from "../common/common.helper.private"; 11 | import * as LINETypes from "./line_types.private"; 12 | 13 | // Load Libraries 14 | const { LINEMessageType } = ( 15 | require(Runtime.getFunctions()["api/line/line_types"].path) 16 | ); 17 | 18 | // Load Libraries 19 | const { lineSendTextMessage, lineSendMediaMessage } = ( 20 | require(Runtime.getFunctions()["api/line/line.helper"].path) 21 | ); 22 | 23 | // Load Libraries 24 | const { twilioGetMediaResource } = ( 25 | require(Runtime.getFunctions()["api/common/common.helper"].path) 26 | ); 27 | 28 | type IncomingMessageType = { 29 | request: { 30 | cookies: object; 31 | headers: object; 32 | }; 33 | Source: string; 34 | user_id: string; 35 | Media: string; 36 | Body: string; 37 | ChatServiceSid: string; 38 | }; 39 | 40 | export const handler: ServerlessFunctionSignature< 41 | LINETypes.LINEContext, 42 | IncomingMessageType 43 | > = async (context, event, callback: ServerlessCallback) => { 44 | console.log("event received - /api/line/outgoing: ", event); 45 | 46 | // Process Only Agent Messages 47 | if (event.Source === "SDK") { 48 | // Parse Type of Messages 49 | console.log("---Start of Raw Event---"); 50 | console.log(event); 51 | console.log(`RAW event.user_id: ${event.user_id}`); 52 | console.log("---End of Raw Event---"); 53 | if (!event.Media) { 54 | // Agent Message Type: Text 55 | await lineSendTextMessage(context, event.user_id, event.Body); 56 | } else { 57 | // Agent Message Type: Media 58 | // -- Handle Multiple Media object(s) 59 | for (let media of JSON.parse(event.Media)) { 60 | console.log("---Media Payload---"); 61 | console.log(media); 62 | console.log(`Media SID: ${media.Sid}`); 63 | console.log(`Chat Service SID: ${event.ChatServiceSid}`); 64 | // -- Obtain Media Type 65 | let mediaType: LINETypes.LINEMessageType; 66 | 67 | switch (media.ContentType) { 68 | case "image/png": 69 | mediaType = LINEMessageType.IMAGE; 70 | break; 71 | case "image/jpeg": 72 | mediaType = LINEMessageType.IMAGE; 73 | break; 74 | case "image/jpg": 75 | mediaType = LINEMessageType.IMAGE; 76 | break; 77 | case "video/mp4": 78 | mediaType = LINEMessageType.VIDEO; 79 | break; 80 | case "video/mpeg": 81 | mediaType = LINEMessageType.VIDEO; 82 | break; 83 | default: 84 | return callback("File type is not supported"); 85 | } 86 | 87 | // -- Retrieve Temporary URL (Public) of Twilio Media Resource 88 | const mediaResource = await twilioGetMediaResource( 89 | { accountSid: context.ACCOUNT_SID, authToken: context.AUTH_TOKEN }, 90 | event.ChatServiceSid, 91 | media.Sid 92 | ); 93 | if ( 94 | !mediaResource || 95 | !mediaResource.links || 96 | !mediaResource.links.content_direct_temporary 97 | ) { 98 | return callback("Unable to get temporary URL for image"); 99 | } 100 | // -- Send to LINE 101 | await lineSendMediaMessage( 102 | context, 103 | event.user_id, 104 | mediaType, 105 | mediaResource.links.content_direct_temporary 106 | ); 107 | } 108 | } 109 | 110 | return callback(null, { 111 | success: true, 112 | }); 113 | } else { 114 | // Ignoring all end user added messages 115 | console.log("Outgoing Hook: No Action Needed"); 116 | } 117 | 118 | return callback(null, { 119 | success: true, 120 | }); 121 | }; 122 | -------------------------------------------------------------------------------- /docs/docs/getting-started/02_deploy-via-cli.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Deploy via CLI 3 | position: 2 4 | --- 5 | 6 | ## Pre-requisites 7 | 8 | 1. Twilio Flex Account ([Guide](https://support.twilio.com/hc/en-us/articles/360020442333-Setup-a-Twilio-Flex-Account)) 9 | 2. Node.js v16.x.x only ([Guide](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)) 10 | 3. Typescript v.5.1.6 or above ([Guide](https://www.typescriptlang.org/download)) 11 | 4. Twilio CLI v5.8.1 or above ([Guide](https://www.twilio.com/docs/twilio-cli/quickstart)) 12 | 5. Twilio CLI Serverless Plugin v3.1.3 or above ([Guide](https://www.twilio.com/docs/labs/serverless-toolkit/getting-started)) 13 | 14 | ## Instructions 15 | 16 | 1. On your computer, open up your preferred terminal and clone this repository 17 | 18 | - ```bash 19 | // Clone Project 20 | git clone https://github.com/leroychan/twilio-flex-conversations-adapters.git 21 | 22 | // Change to working directory 23 | cd twilio-flex-conversations-adapters/serverless-functions 24 | 25 | // Install NPM Packages 26 | npm install 27 | 28 | // Copy sample enviroment file 29 | cp .env.example .env 30 | ``` 31 | 32 | 1. For each custom channel that you would like to enable, it is recommended that you create 1 Studio per custom channel ([Guide](./create-studio-flow)). 33 | - You will need to have the Studio Flow SID (starting with `FWxxxxxxxxx`) of each custom channel that you would like to enable. 34 | - For channels that you **do not** want to enable, you **do not** need to create the Studio Flow. 35 | 1. For each custom channel that you would like to enable, perform the necessary setup and obtain the required values. The required variables per custom channel can be found in the [Channels Overview page](../channels/overview). 36 | 1. Configure the `.env` file using your preferred code editor with all the required values obtained previously. You can leave `ACCOUNT_SID=xxx` and `AUTH_TOKEN=xxx` empty as it will be populated by default during run time. Before you deploy, ensure that `twilio profiles:list` has an active account set. 37 | - **Required variables per custom channel that you want to enable** 38 | - **Example**: To enable only LINE, you would need the following variables: 39 | - `LINE_STUDIO_FLOW_SID=FWxxxx` 40 | - `LINE_CHANNEL_ID=xxxx` 41 | - `LINE_CHANNEL_SECRET=xxxx` 42 | - `LINE_CHANNEL_ACCESS_TOKEN=xxxx` 43 | - **Example**: To enable both LINE and Viber, you would need the following variables: 44 | - `LINE_STUDIO_FLOW_SID=FWxxxx` 45 | - `LINE_CHANNEL_ID=xxxx` 46 | - `LINE_CHANNEL_SECRET=xxxx` 47 | - `LINE_CHANNEL_ACCESS_TOKEN=xxxx` 48 | - `VIBER_STUDIO_FLOW_SID=FWxxxxxx` 49 | - `VIBER_AUTH_TOKEN=xxxx` 50 | 1. Once configured and you are ready to deploy it, go back to your terminal and issue the following 51 | command: 52 | - ```bash 53 | npm run deploy 54 | ``` 55 | 1. Within [Twilio Console](https://console.twilio.com/), on the left hand side menu bar, navigate to `Functions and Assets > Services`. Look for `twilio-flex-conversations-adapters` and click on `Service Details`. Under `Environments > Domain`, take note of the domain URL. 56 | - The domain URL should be in the format of `twilio-flex-conversations-adapters--dev.twil.io`. 57 | 1. We will need the respective custom channels `incoming` webhook URL that is in the format of `https://twilio-flex-conversations-adapters--dev.twil.io/api//incoming` 58 | - **Example**: 59 | - `LINE`: `https://twilio-flex-conversations-adapters--dev.twil.io/api/line/incoming` 60 | - `Viber`: `https://twilio-flex-conversations-adapters--dev.twil.io/api/viber/incoming` 61 | 1. Configure each custom channel's webhook setting in their respective consoles with the `incoming` webhook URL in the previous step. Refer to the respective documentation page under `Channels` for exact instructions. 62 | 1. You have now completed the setup. Proceed to test the integration by logging into your Flex Agent console and put yourself as `Available`. Send a test message to your custom channnel and it should appear in your Flex Agent console. 63 | 1. [_Optional_] In order to have the icons of their respective custom channel displayed in Flex agent interface, you will need to deploy the Converasations Icons Plugin, perform the following on your CLI: 64 | - ```bash 65 | cd .. 66 | cd plugin-conversations-icons 67 | npm run deploy 68 | npm run release 69 | ``` 70 | -------------------------------------------------------------------------------- /serverless-functions/src/functions/api/viber/outgoing.ts: -------------------------------------------------------------------------------- 1 | // Imports global types 2 | import "@twilio-labs/serverless-runtime-types"; 3 | // Fetches specific types 4 | import { 5 | Context, 6 | ServerlessCallback, 7 | ServerlessFunctionSignature, 8 | } from "@twilio-labs/serverless-runtime-types/types"; 9 | 10 | import * as Helper from "./viber.helper.private"; 11 | import * as Util from "../common/common.helper.private"; 12 | import * as ViberTypes from "./viber_types.private"; 13 | 14 | // Load Libraries 15 | const { ViberMessageType } = ( 16 | require(Runtime.getFunctions()["api/viber/viber_types"].path) 17 | ); 18 | 19 | // Load Libraries 20 | const { viberSendTextMessage, viberSendMedia } = ( 21 | require(Runtime.getFunctions()["api/viber/viber.helper"].path) 22 | ); 23 | 24 | // Load Libraries 25 | const { twilioGetMediaResource } = ( 26 | require(Runtime.getFunctions()["api/common/common.helper"].path) 27 | ); 28 | 29 | type IncomingMessageType = { 30 | request: { 31 | cookies: object; 32 | headers: object; 33 | }; 34 | Source: string; 35 | user_id: string; 36 | Media: string; 37 | Body: string; 38 | ChatServiceSid: string; 39 | }; 40 | export const handler: ServerlessFunctionSignature< 41 | ViberTypes.ViberContext, 42 | IncomingMessageType 43 | > = async (context, event, callback: ServerlessCallback) => { 44 | console.log("event received - /api/viber/outgoing: ", event); 45 | 46 | // Process Only Agent Messages 47 | if (event.Source === "SDK") { 48 | // Parse Type of Messages 49 | console.log("---Start of Raw Event---"); 50 | console.log(event); 51 | console.log(`RAW event.user_id: ${event.user_id}`); 52 | console.log("---End of Raw Event---"); 53 | if (!event.Media) { 54 | // Agent Message Type: Text 55 | await viberSendTextMessage( 56 | context, 57 | decodeURIComponent(event.user_id), 58 | event.Body 59 | ); 60 | } else { 61 | // Agent Message Type: Media 62 | // -- Handle Multiple Media object(s) 63 | for (let media of JSON.parse(event.Media)) { 64 | console.log("---Media Payload---"); 65 | console.log(media); 66 | console.log(`Media SID: ${media.Sid}`); 67 | console.log(`Chat Service SID: ${event.ChatServiceSid}`); 68 | // -- Obtain Media Type 69 | let mediaType: ViberTypes.ViberMessageType; 70 | 71 | switch (media.ContentType) { 72 | case "image/png": 73 | mediaType = ViberMessageType.PICTURE; 74 | break; 75 | case "image/jpeg": 76 | mediaType = ViberMessageType.PICTURE; 77 | break; 78 | case "image/jpg": 79 | mediaType = ViberMessageType.PICTURE; 80 | break; 81 | case "video/mp4": 82 | mediaType = ViberMessageType.VIDEO; 83 | break; 84 | case "video/mpeg": 85 | mediaType = ViberMessageType.VIDEO; 86 | break; 87 | default: 88 | return callback("File type is not supported"); 89 | } 90 | 91 | // -- Retrieve Temporary URL (Public) of Twilio Media Resource 92 | const mediaResource = await twilioGetMediaResource( 93 | { accountSid: context.ACCOUNT_SID, authToken: context.AUTH_TOKEN }, 94 | event.ChatServiceSid, 95 | media.Sid 96 | ); 97 | if ( 98 | !mediaResource || 99 | !mediaResource.links || 100 | !mediaResource.links.content_direct_temporary 101 | ) { 102 | return callback("Unable to get temporary URL for image"); 103 | } 104 | // -- Send to Viber 105 | if (mediaType == ViberMessageType.VIDEO) { 106 | await viberSendMedia( 107 | context, 108 | decodeURIComponent(event.user_id), 109 | mediaType, 110 | mediaResource.links.content_direct_temporary, 111 | mediaResource.size 112 | ); 113 | } else { 114 | await viberSendMedia( 115 | context, 116 | decodeURIComponent(event.user_id), 117 | mediaType, 118 | mediaResource.links.content_direct_temporary 119 | ); 120 | } 121 | } 122 | } 123 | 124 | return callback(null, { 125 | success: true, 126 | }); 127 | } else { 128 | // Ignoring all end user added messages 129 | console.log("Outgoing Hook: No Action Needed"); 130 | } 131 | 132 | return callback(null, { 133 | success: true, 134 | }); 135 | }; 136 | -------------------------------------------------------------------------------- /serverless-functions/src/functions/api/bot/bot.helper.private.ts: -------------------------------------------------------------------------------- 1 | import { sign, decode } from "jsonwebtoken"; 2 | import { Context } from "@twilio-labs/serverless-runtime-types/types"; 3 | 4 | /** 5 | * Sends a message to the bot 6 | * @param context - The Twilio runtime context 7 | * @param botInstanceId - The ID of the bot instance 8 | * @param body - The message body to send 9 | */ 10 | export async function sendMessageToBot( 11 | context: Context & { TWILIO_REGION?: string; ACCOUNT_SID: string; AUTH_TOKEN: string }, 12 | botInstanceId: string, 13 | body: Record 14 | ) { 15 | const url = `https://ROVO_URL_HERE`; 16 | 17 | const response = await fetch(url, { 18 | method: "POST", 19 | body: JSON.stringify(body), 20 | headers: { 21 | Authorization: `Basic ${Buffer.from( 22 | `${context.ACCOUNT_SID}:${context.AUTH_TOKEN}`, 23 | "utf-8" 24 | ).toString("base64")}`, 25 | "Content-Type": "application/json", 26 | Accept: "application/json", 27 | }, 28 | }); 29 | if (response.ok) { 30 | console.log("Sent message to Bot"); 31 | return; 32 | } else { 33 | throw new Error( 34 | "Failed to send request to Bot. " + (await response.text()) 35 | ); 36 | } 37 | } 38 | 39 | /** 40 | * Reads attributes from a conversation 41 | * @param context - The Twilio runtime context 42 | * @param chatServiceSid - The Chat Service SID 43 | * @param conversationSid - The Conversation SID 44 | * @returns The parsed conversation attributes 45 | */ 46 | export async function readConversationAttributes( 47 | context: Context & { getTwilioClient: () => any }, 48 | chatServiceSid: string, 49 | conversationSid: string 50 | ) { 51 | try { 52 | const client = context.getTwilioClient(); 53 | const data = await client.conversations.v1 54 | .services(chatServiceSid) 55 | .conversations(conversationSid) 56 | .fetch(); 57 | return JSON.parse(data.attributes); 58 | } catch (err) { 59 | console.error(err); 60 | return {}; 61 | } 62 | } 63 | 64 | /** 65 | * Gets the bot ID from context or event 66 | * @param context - The Twilio runtime context 67 | * @param event - The event object 68 | * @returns The bot ID 69 | */ 70 | export async function getBotId( 71 | context: Context & { AUTH_TOKEN: string; BOT_ID?: string }, 72 | event: { 73 | EventType?: string; 74 | botId?: string; 75 | ConversationSid?: string; 76 | ChatServiceSid?: string; 77 | } 78 | ) { 79 | if (event.EventType === "onMessageAdded") { 80 | try { 81 | const { ConversationSid, ChatServiceSid } = event; 82 | const parsed = await readConversationAttributes( 83 | context, 84 | ChatServiceSid, 85 | ConversationSid 86 | ); 87 | if (typeof parsed.botId === "string" && parsed.botId) { 88 | return parsed.botId; 89 | } 90 | } catch (err) { 91 | console.log("Invalid attribute structure", err); 92 | } 93 | } 94 | const botId = event.botId || context.BOT_ID || event.botId || context.BOT_ID; 95 | 96 | if (!botId) { 97 | throw new Error("Missing Bot ID configuration"); 98 | } 99 | 100 | return botId; 101 | } 102 | 103 | /** 104 | * Signs a request with JWT 105 | * @param context - The Twilio runtime context 106 | * @param event - The event object 107 | * @returns The signed JWT token 108 | */ 109 | export async function signRequest( 110 | context: Context & { AUTH_TOKEN: string }, 111 | event: Record 112 | ) { 113 | const assistantSid = await getBotId(context, event); 114 | const authToken = context.AUTH_TOKEN; 115 | if (!authToken) { 116 | throw new Error("No auth token found"); 117 | } 118 | return sign({ assistantSid }, authToken, { expiresIn: "5m" }); 119 | } 120 | 121 | /** 122 | * Verifies a request token 123 | * @param context - The Twilio runtime context 124 | * @param event - The event object containing the token 125 | * @returns Whether the token is valid 126 | */ 127 | export function verifyRequest( 128 | context: Context & { AUTH_TOKEN: string }, 129 | event: { _token: string } 130 | ) { 131 | const token = event._token; 132 | if (!token) { 133 | throw new Error("Missing token"); 134 | } 135 | 136 | const authToken = context.AUTH_TOKEN; 137 | if (!authToken) { 138 | throw new Error("No auth token found"); 139 | } 140 | 141 | try { 142 | // The decode function from jsonwebtoken only takes a token and options 143 | const decoded = decode(token, { json: true }); 144 | if (decoded && typeof decoded === 'object' && 'assistantSid' in decoded) { 145 | return true; 146 | } 147 | } catch (err) { 148 | console.error("Failed to verify token", err); 149 | return false; 150 | } 151 | return false; 152 | } 153 | 154 | // All functions are already exported using named exports 155 | -------------------------------------------------------------------------------- /serverless-functions/src/functions/api/instagram/outgoing.ts: -------------------------------------------------------------------------------- 1 | // Imports global types 2 | import "@twilio-labs/serverless-runtime-types"; 3 | // Fetches specific types 4 | import { 5 | Context, 6 | ServerlessCallback, 7 | ServerlessFunctionSignature, 8 | } from "@twilio-labs/serverless-runtime-types/types"; 9 | 10 | import * as Helper from "./instagram.helper.private"; 11 | import * as Util from "../common/common.helper.private"; 12 | import * as InstagramTypes from "./instagram_types.private"; 13 | 14 | // Load Libraries 15 | const { InstagramMessageType } = ( 16 | require(Runtime.getFunctions()["api/instagram/instagram_types"].path) 17 | ); 18 | const { instagramSendTextMessage, instagramSendMediaMessage } = ( 19 | require(Runtime.getFunctions()["api/instagram/instagram.helper"].path) 20 | ); 21 | 22 | const { twilioGetMediaResource } = ( 23 | require(Runtime.getFunctions()["api/common/common.helper"].path) 24 | ); 25 | 26 | // Type - Flex Request 27 | type IncomingMessageType = { 28 | request: { 29 | cookies: object; 30 | headers: object; 31 | }; 32 | Source: string; 33 | user_id: string; 34 | Media: string; 35 | Body: string; 36 | ChatServiceSid: string; 37 | }; 38 | 39 | export const handler: ServerlessFunctionSignature< 40 | InstagramTypes.InstagramContext, 41 | IncomingMessageType 42 | > = async (context, event, callback: ServerlessCallback) => { 43 | console.log("event received - /api/instagram/outgoing: ", event); 44 | 45 | // Process Only Agent Messages 46 | if (event.Source === "SDK") { 47 | // Parse Type of Messages 48 | console.log("---Start of Raw Event---"); 49 | console.log(event); 50 | console.log(`RAW event.user_id: ${event.user_id}`); 51 | console.log("---End of Raw Event---"); 52 | if (!event.Media) { 53 | // Agent Message Type: Text 54 | await instagramSendTextMessage(context, event.user_id, event.Body); 55 | } else { 56 | // Agent Message Type: Media 57 | // -- Handle Multiple Media object(s) 58 | for (let media of JSON.parse(event.Media)) { 59 | console.log("---Media Payload---"); 60 | console.log(media); 61 | console.log(`Media SID: ${media.Sid}`); 62 | console.log(`Chat Service SID: ${event.ChatServiceSid}`); 63 | // -- Obtain Media Type 64 | let mediaType: InstagramTypes.InstagramMessageType; 65 | 66 | switch (media.ContentType) { 67 | case "image/png": 68 | mediaType = InstagramMessageType.IMAGE; 69 | break; 70 | case "image/jpeg": 71 | mediaType = InstagramMessageType.IMAGE; 72 | break; 73 | case "image/jpg": 74 | mediaType = InstagramMessageType.IMAGE; 75 | break; 76 | case "image/gif": 77 | mediaType = InstagramMessageType.IMAGE; 78 | break; 79 | case "video/mp4": 80 | mediaType = InstagramMessageType.VIDEO; 81 | break; 82 | case "video/ogg": 83 | mediaType = InstagramMessageType.VIDEO; 84 | break; 85 | case "video/x-msvideo": 86 | mediaType = InstagramMessageType.VIDEO; 87 | break; 88 | case "video/quicktime": 89 | mediaType = InstagramMessageType.VIDEO; 90 | break; 91 | case "video/webm": 92 | mediaType = InstagramMessageType.VIDEO; 93 | break; 94 | case "application/vnd.americandynamics.acc": 95 | mediaType = InstagramMessageType.AUDIO; 96 | break; 97 | case "audio/mp4": 98 | mediaType = InstagramMessageType.AUDIO; 99 | break; 100 | case "audio/wav": 101 | mediaType = InstagramMessageType.AUDIO; 102 | break; 103 | default: 104 | return callback("File type is not supported"); 105 | } 106 | 107 | // -- Retrieve Temporary URL (Public) of Twilio Media Resource 108 | const mediaResource = await twilioGetMediaResource( 109 | { accountSid: context.ACCOUNT_SID, authToken: context.AUTH_TOKEN }, 110 | event.ChatServiceSid, 111 | media.Sid 112 | ); 113 | if ( 114 | !mediaResource || 115 | !mediaResource.links || 116 | !mediaResource.links.content_direct_temporary 117 | ) { 118 | return callback("Unable to get temporary URL for image"); 119 | } 120 | // -- Send to Instagram 121 | await instagramSendMediaMessage( 122 | context, 123 | event.user_id, 124 | mediaType, 125 | mediaResource.links.content_direct_temporary 126 | ); 127 | } 128 | } 129 | 130 | return callback(null, { 131 | success: true, 132 | }); 133 | } else { 134 | // Ignoring all end user added messages 135 | console.log("Outgoing Hook: No Action Needed"); 136 | } 137 | 138 | return callback(null, { 139 | success: true, 140 | }); 141 | }; 142 | -------------------------------------------------------------------------------- /serverless-functions/src/functions/api/googlechat/outgoing.ts: -------------------------------------------------------------------------------- 1 | // Imports global types 2 | import "@twilio-labs/serverless-runtime-types"; 3 | // Fetches specific types 4 | import { 5 | Context, 6 | ServerlessCallback, 7 | ServerlessFunctionSignature, 8 | } from "@twilio-labs/serverless-runtime-types/types"; 9 | 10 | import * as Helper from "./googlechat.helper.private"; 11 | import * as Util from "../common/common.helper.private"; 12 | import * as GoogleChatTypes from "./googlechat_types.private"; 13 | 14 | // Load Libraries 15 | const { GoogleChatMessageType } = ( 16 | require(Runtime.getFunctions()["api/googlechat/googlechat_types"].path) 17 | ); 18 | const { 19 | getGoogleChatClient, 20 | googleChatSendTextMessage, 21 | googleChatSendMediaMessage, 22 | } = ( 23 | require(Runtime.getFunctions()["api/googlechat/googlechat.helper"].path) 24 | ); 25 | 26 | const { twilioGetMediaResource, twilioGetConversation } = ( 27 | require(Runtime.getFunctions()["api/common/common.helper"].path) 28 | ); 29 | 30 | // Type - Flex Request 31 | type IncomingMessageType = { 32 | request: { 33 | cookies: object; 34 | headers: object; 35 | }; 36 | Source: string; 37 | user_id: string; 38 | Media: string; 39 | Body: string; 40 | ChatServiceSid: string; 41 | ConversationSid: string; 42 | }; 43 | 44 | export const handler: ServerlessFunctionSignature< 45 | GoogleChatTypes.GoogleChatContext, 46 | IncomingMessageType 47 | > = async (context, event, callback: ServerlessCallback) => { 48 | console.log("event received - /api/googlechat/outgoing: ", event); 49 | 50 | // Process Only Agent Messages 51 | if (event.Source === "SDK") { 52 | // -- Debug 53 | console.log("---Start of Raw Event---"); 54 | console.log(event); 55 | console.log(`RAW event.user_id: ${event.user_id}`); 56 | console.log("---End of Raw Event---"); 57 | // -- Initialise Google Chat Resoruces 58 | const chatClient = await getGoogleChatClient(context); 59 | const conversationResource = await twilioGetConversation( 60 | context.getTwilioClient(), 61 | event.ConversationSid 62 | ); 63 | if (!conversationResource) { 64 | return callback(null, { 65 | success: false, 66 | }); 67 | } 68 | let preEngagementData: any = {}; 69 | try { 70 | preEngagementData = JSON.parse( 71 | conversationResource.attributes 72 | ).pre_engagement_data; 73 | } catch (err) { 74 | return callback(null, { 75 | success: false, 76 | }); 77 | } 78 | const googleChatSpaceName = preEngagementData.spaceName; 79 | if (!googleChatSpaceName) { 80 | return callback(null, { 81 | success: false, 82 | }); 83 | } 84 | if (!event.Media) { 85 | // Agent Message Type: Text 86 | await googleChatSendTextMessage( 87 | chatClient, 88 | googleChatSpaceName, 89 | event.Body 90 | ); 91 | } else { 92 | // Agent Message Type: Media 93 | // -- Handle Multiple Media object(s) 94 | for (let media of JSON.parse(event.Media)) { 95 | console.log("---Media Payload---"); 96 | console.log(media); 97 | console.log(`Media SID: ${media.Sid}`); 98 | console.log(`Chat Service SID: ${event.ChatServiceSid}`); 99 | // -- Obtain Media Type 100 | let mediaType: GoogleChatTypes.GoogleChatMessageType; 101 | 102 | switch (media.ContentType) { 103 | case "image/png": 104 | mediaType = GoogleChatMessageType.IMAGE; 105 | break; 106 | case "image/jpeg": 107 | mediaType = GoogleChatMessageType.IMAGE; 108 | break; 109 | case "image/jpg": 110 | mediaType = GoogleChatMessageType.IMAGE; 111 | break; 112 | case "image/gif": 113 | mediaType = GoogleChatMessageType.IMAGE; 114 | break; 115 | default: 116 | return callback("File type is not supported"); 117 | } 118 | 119 | // // -- Retrieve Temporary URL (Public) of Twilio Media Resource 120 | const mediaResource = await twilioGetMediaResource( 121 | { accountSid: context.ACCOUNT_SID, authToken: context.AUTH_TOKEN }, 122 | event.ChatServiceSid, 123 | media.Sid 124 | ); 125 | if (!mediaResource?.links?.content_direct_temporary) { 126 | return callback("Unable to get temporary URL for image"); 127 | } 128 | // // -- Send to Google Chat 129 | await googleChatSendMediaMessage( 130 | chatClient, 131 | googleChatSpaceName, 132 | media.Filename, 133 | mediaResource.links.content_direct_temporary 134 | ); 135 | } 136 | } 137 | 138 | return callback(null, { 139 | success: true, 140 | }); 141 | } else { 142 | // Ignoring all end user added messages 143 | console.log("Outgoing Hook: No Action Needed"); 144 | } 145 | 146 | return callback(null, { 147 | success: true, 148 | }); 149 | }; 150 | -------------------------------------------------------------------------------- /serverless-functions/src/functions/api/common/studio-workaround.protected.ts: -------------------------------------------------------------------------------- 1 | // Imports global types 2 | import "@twilio-labs/serverless-runtime-types"; 3 | 4 | // Fetches specific types 5 | import { 6 | ServerlessCallback, 7 | ServerlessFunctionSignature, 8 | } from "@twilio-labs/serverless-runtime-types/types"; 9 | 10 | // Load Libraries 11 | // -- Helper Lib 12 | import * as Util from "./common.helper.private"; 13 | const { twilioGetConversation } = ( 14 | require(Runtime.getFunctions()["api/common/common.helper"].path) 15 | ); 16 | // -- Google Chat 17 | import * as GoogleChatHelper from "../googlechat/googlechat.helper.private"; 18 | const { googleChatSendTextMessage, getGoogleChatClient } = < 19 | typeof GoogleChatHelper 20 | >require(Runtime.getFunctions()["api/googlechat/googlechat.helper"].path); 21 | // -- Instagram 22 | import * as InstagramHelper from "../instagram/instagram.helper.private"; 23 | const { instagramSendTextMessage } = ( 24 | require(Runtime.getFunctions()["api/instagram/instagram.helper"].path) 25 | ); 26 | // -- LINE 27 | import * as LINEHelper from "../line/line.helper.private"; 28 | const { lineSendTextMessage } = ( 29 | require(Runtime.getFunctions()["api/line/line.helper"].path) 30 | ); 31 | // -- Viber 32 | import * as ViberHelper from "../viber/viber.helper.private"; 33 | const { viberSendTextMessage } = ( 34 | require(Runtime.getFunctions()["api/viber/viber.helper"].path) 35 | ); 36 | 37 | // Types 38 | export type TwilioChatEnvironmentVariables = { 39 | ACCOUNT_SID: string; 40 | AUTH_TOKEN: string; 41 | DOMAIN_NAME_OVERRIDE: string; 42 | GOOGLECHAT_STUDIO_FLOW_SID: string; 43 | GOOGLECHAT_SERVICE_ACCOUNT_KEY_BASE64: string; 44 | INSTAGRAM_STUDIO_FLOW_SID: string; 45 | INSTAGRAM_APP_SECRET: string; 46 | INSTAGRAM_PAGE_ACCESS_TOKEN: string; 47 | INSTAGRAM_WEBHOOK_VERIFY_TOKEN: string; 48 | LINE_STUDIO_FLOW_SID: string; 49 | LINE_CHANNEL_ID: string; 50 | LINE_CHANNEL_SECRET: string; 51 | LINE_CHANNEL_ACCESS_TOKEN: string; 52 | VIBER_STUDIO_FLOW_SID: string; 53 | VIBER_AUTH_TOKEN: string; 54 | }; 55 | 56 | export type TwilioChatWebhookRequest = { 57 | request: { 58 | headers: any; 59 | cookies: any; 60 | }; 61 | ChannelSid: string; 62 | RetryCount: string; 63 | EventType: string; 64 | InstanceSid: string; 65 | Attributes: string; 66 | DateCreated: string; 67 | Index: string; 68 | From: string; 69 | MessageSid: string; 70 | Body: string; 71 | AccountSid: string; 72 | Source: string; 73 | }; 74 | 75 | export const handler: ServerlessFunctionSignature< 76 | TwilioChatEnvironmentVariables, 77 | TwilioChatWebhookRequest 78 | > = async (context, event, callback: ServerlessCallback) => { 79 | console.log("event received - /api/common/studio-workaround: ", event); 80 | // Step 0: Get Client 81 | const client = context.getTwilioClient(); 82 | // Step 1: Check Request 83 | if (!event.From || !event.MessageSid || !event.Body) { 84 | callback("Invalid Payload"); 85 | } 86 | // -- Check if event is a Conversation from Studio 87 | const conversationsSidRegex = /^CH[A-Za-z0-9]{32}$/; 88 | const isConversations = conversationsSidRegex.test(event.From); 89 | if (!isConversations) { 90 | console.log("Event is not a conversation from Studio. Ignoring.."); 91 | callback(null, { 92 | success: true, 93 | message: "event is not a conversation from Studio", 94 | }); 95 | } 96 | try { 97 | const conversationResult = await twilioGetConversation(client, event.From); 98 | if (!conversationResult) { 99 | return callback(null, { 100 | success: true, 101 | message: "Unable to get Conversation", 102 | }); 103 | } 104 | // Obtain Custom Channel Name 105 | // NOTE: Needs to follow the naming convent: 106 | const splitFriendlyName = conversationResult.friendlyName.split(" "); 107 | const adapterName = splitFriendlyName[0].toLowerCase(); 108 | const userId = splitFriendlyName[2]; 109 | switch (adapterName) { 110 | case "googlechat": 111 | const googlechatClient = await getGoogleChatClient(context); 112 | const conversationAttributes = JSON.parse( 113 | conversationResult.attributes 114 | ); 115 | const spaceId = 116 | conversationAttributes["pre_engagement_data"]["spaceName"]; 117 | await googleChatSendTextMessage(googlechatClient, spaceId, event.Body); 118 | break; 119 | case "instagram": 120 | await instagramSendTextMessage(context, userId, event.Body); 121 | break; 122 | case "line": 123 | await lineSendTextMessage(context, userId, event.Body); 124 | break; 125 | case "viber": 126 | const decodedUserId = decodeURIComponent(userId); 127 | await viberSendTextMessage(context, decodedUserId, event.Body); 128 | break; 129 | default: 130 | return callback(null, { 131 | success: true, 132 | message: "Custom Channel Not Supported", 133 | }); 134 | } 135 | console.log("Studio Message successfully sent to Custom Channel"); 136 | return callback(null, { 137 | success: true, 138 | }); 139 | } catch (err) { 140 | console.log(err); 141 | callback("Unable to process request"); 142 | } 143 | }; 144 | -------------------------------------------------------------------------------- /docs/docs/getting-started/01_deploy-via-github-actions.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Deploy via GitHub Actions (Recommended) 3 | --- 4 | 5 | # Deploy via GitHub Actions (Recommended) 6 | 7 |

8 | Github Actions Overall 9 |

10 | 11 | ## Overview 12 | 13 | GitHub Actions streamlines the deployment process to multiple Twilio Flex instances through a comprehensive release pipeline. It facilitates enhanced management of environment variables using the Environment feature of GitHub, allowing the use of multiple environments to ensure consistency of deployment across different Twilio Flex instances through utilizing the same code base. 14 | 15 | ## Pre-Requisites 16 | 17 | - Twilio Flex Account ([Guide](https://support.twilio.com/hc/en-us/articles/360020442333-Setup-a-Twilio-Flex-Account)) 18 | - GitHub Account ([Guide](https://docs.github.com/en/get-started/signing-up-for-github/signing-up-for-a-new-github-account)) 19 | - Required values for each respective custom channels ([Guide](../channels/overview)) 20 | 21 | ## Instructions 22 | 23 | 1. [Fork this GitHub repository](https://github.com/leroychan/twilio-flex-conversations-adapters/fork) into your own GitHub account 24 | 1. Set the forked GitHub repository to be a **public repository** as GitHub Actions is only [free for public repositories](https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions). 25 | 1. Navigate to your [Twilio Console](https://console.twilio.com/) to create your Twilio API Key and API Secret ([Guide](https://www.twilio.com/docs/glossary/what-is-an-api-key#how-can-i-create-api-keys)). 26 | - You will need to obtain the following before proceeding: 27 | - `TWILIO_ACCOUNT_SID`: Twilio Account SID that you are deploying to (starting with `ACxxxxxxxxx`) 28 | - `TWILIO_API_KEY`: Twilio API Key (starting with `SKxxxxxxxxx`) 29 | - `TWILIO_API_SECRET`: Twilio API Secret 30 | 1. For each custom channel that you would like to enable, create 1 TaskRouter - Task Channel per custom channel ([Guide](./create-task-channel)). 31 | 1. For each custom channel that you would like to enable, it is recommended that you create 1 Studio per custom channel ([Guide](./create-studio-flow)). 32 | - You will need to have the Studio Flow SID (starting with `FWxxxxxxxxx`) of each custom channel that you would like to enable. 33 | - For channels that you **do not** want to enable, you **do not** need to create the Studio Flow. 34 | 1. For each custom channel that you would like to enable, perform the necessary setup and obtain the required values. The required variables per custom channel can be found in the [Channels Overview page](../channels/overview). 35 | 1. Within GitHub Console, navigate to the repository that you have forked, click on `Settings > Environment > New Environment` and add the following **secrets** with the value you have obtained from the previous steps: 36 | - `TWILIO_ACCOUNT_SID` 37 | - `TWILIO_API_KEY` 38 | - `TWILIO_API_SECRET` 39 | - **Required variables per custom channel that you want to enable** 40 | - **Example**: To enable only LINE, you would need the following variables with their respective values: 41 | - `TWILIO_ACCOUNT_SID` 42 | - `TWILIO_API_KEY` 43 | - `TWILIO_API_SECRET` 44 | - `LINE_STUDIO_FLOW_SID` 45 | - `LINE_CHANNEL_ID` 46 | - `LINE_CHANNEL_SECRET` 47 | - `LINE_CHANNEL_ACCESS_TOKEN` 48 | - **Example**: To enable both LINE and Viber, you would need the following variables with their respective values: 49 | - `TWILIO_ACCOUNT_SID` 50 | - `TWILIO_API_KEY` 51 | - `TWILIO_API_SECRET` 52 | - `LINE_STUDIO_FLOW_SID` 53 | - `LINE_CHANNEL_ID` 54 | - `LINE_CHANNEL_SECRET` 55 | - `LINE_CHANNEL_ACCESS_TOKEN` 56 | - `VIBER_STUDIO_FLOW_SID` 57 | - `VIBER_AUTH_TOKEN` 58 | 1. Within GitHub Console, navigate to the repository that you have forked, click on `Actions > Deploy to Flex > Run workflow`. Select the environment that you have created previously and click `Run workflow` 59 | - If you are deploying for the first time, tick the box that says `Deploy UI Icons Plugin?`. You will only need to do this once per Flex Account SID. 60 | 1. The workflow will run to build, compile and process all the required components and deploy it to your chosen environment (i.e. Flex Account). You can also click on each of the workflow runs to view the logs. 61 | 1. Within [Twilio Console](https://console.twilio.com/), on the left hand side menu bar, navigate to `Functions and Assets > Services`. Look for `twilio-flex-conversations-adapters` and click on `Service Details`. Under `Environments > Domain`, take note of the domain URL. 62 | - The domain URL should be in the format of `twilio-flex-conversations-adapters--dev.twil.io`. 63 | 1. We will need the respective custom channels `incoming` webhook URL that is in the format of `https://twilio-flex-conversations-adapters--dev.twil.io/api//incoming` 64 | - **Example**: 65 | - `LINE`: `https://twilio-flex-conversations-adapters--dev.twil.io/api/line/incoming` 66 | - `Viber`: `https://twilio-flex-conversations-adapters--dev.twil.io/api/viber/incoming` 67 | 1. Configure each custom channel's webhook setting in their respective consoles with the `incoming` webhook URL in the previous step. Refer to the respective documentation page under `Channels` for exact instructions. 68 | 1. You have now completed the setup. Proceed to test the integration by logging into your Flex Agent console and put yourself as `Available`. Send a test message to your custom channnel and it should appear in your Flex Agent console. 69 | -------------------------------------------------------------------------------- /serverless-functions/src/functions/api/viber/viber.helper.private.ts: -------------------------------------------------------------------------------- 1 | import crypto from "crypto"; 2 | import { Context } from "@twilio-labs/serverless-runtime-types/types"; 3 | import fetch from "node-fetch"; 4 | import * as Util from "../common/common.helper.private"; 5 | import * as ViberTypes from "./viber_types.private"; 6 | 7 | // Load Libraries 8 | const { ViberMessageType } = ( 9 | require(Runtime.getFunctions()["api/viber/viber_types"].path) 10 | ); 11 | 12 | // Load Libraries 13 | const { 14 | twilioUploadMediaResource, 15 | twilioFindExistingConversation, 16 | twilioCreateConversation, 17 | twilioCreateParticipant, 18 | twilioCreateScopedWebhookStudio, 19 | twilioCreateScopedWebhook, 20 | twilioCreateMessage, 21 | } = ( 22 | require(Runtime.getFunctions()["api/common/common.helper"].path) 23 | ); 24 | 25 | export const validateSignature = ( 26 | signature: string, 27 | body: string, 28 | secret: string 29 | ) => { 30 | // Generate HMAC-256 Digest 31 | const digest = crypto 32 | .createHmac("sha256", secret) 33 | .update(body) 34 | .digest("base64"); 35 | if (digest === signature) { 36 | return true; 37 | } else { 38 | return false; 39 | } 40 | }; 41 | 42 | /* 43 | * Raw Function - Viber Get Content 44 | */ 45 | const viberGetMessageContent = async (uri: string) => { 46 | // Send message 47 | const response = await fetch(uri, { 48 | method: "get", 49 | }); 50 | return response; 51 | }; 52 | 53 | export const wrappedSendToFlex = async ( 54 | context: Context, 55 | userId: string, 56 | event: ViberTypes.ViberMessage 57 | ) => { 58 | const client = context.getTwilioClient(); 59 | 60 | // Step 1: Check for any existing conversation. If doesn't exist, create a new conversation -> add participant -> add webhooks 61 | const identity = `viber:${userId}`; 62 | console.log(identity); 63 | 64 | let { conversationSid, chatServiceSid } = 65 | await twilioFindExistingConversation(client, identity); 66 | 67 | console.log(`Old Convo ID: ${conversationSid}`); 68 | console.log(`[Via Existing] Chat Service ID: ${chatServiceSid}`); 69 | 70 | if (!conversationSid) { 71 | // -- Create Conversation 72 | const createConversationResult = await twilioCreateConversation( 73 | "VIBER", 74 | client, 75 | userId, 76 | event.sender 77 | ); 78 | conversationSid = createConversationResult.conversationSid; 79 | chatServiceSid = createConversationResult.chatServiceSid; 80 | // -- Add Participant into Conversation 81 | const addParticipantResult = await twilioCreateParticipant( 82 | client, 83 | conversationSid, 84 | identity 85 | ); 86 | // -- Create Webhook (Conversation Scoped) for Studio 87 | const addWebhookStudioResult = await twilioCreateScopedWebhookStudio( 88 | client, 89 | conversationSid, 90 | context.VIBER_STUDIO_FLOW_SID 91 | ); 92 | // -- Create Webhook (Conversation Scoped) for Outgoing Conversation (Flex to Viber) 93 | let domainName = context.DOMAIN_NAME; 94 | if ( 95 | context.DOMAIN_NAME_OVERRIDE && 96 | context.DOMAIN_NAME_OVERRIDE !== "" 97 | ) { 98 | domainName = context.DOMAIN_NAME_OVERRIDE; 99 | } 100 | const addWebhookResult = await twilioCreateScopedWebhook( 101 | client, 102 | conversationSid, 103 | userId, 104 | domainName, 105 | "api/viber/outgoing" 106 | ); 107 | } 108 | 109 | console.log("Message type is: ", event.message.type); 110 | 111 | // Step 2: Add Message to Conversation 112 | // -- Process Message Type 113 | let addMessageResult; 114 | if (event.message.type === ViberMessageType.TEXT) { 115 | // -- Message Type: text 116 | addMessageResult = await twilioCreateMessage( 117 | client, 118 | conversationSid, 119 | event.sender.name, 120 | (event.message as ViberTypes.ViberMessageText).text 121 | ); 122 | } else if ( 123 | event.message.type === ViberMessageType.PICTURE || 124 | event.message.type === ViberMessageType.VIDEO || 125 | event.message.type === ViberMessageType.FILE 126 | ) { 127 | // -- Message Type: image, video 128 | console.log("--- Message Type: Media (Verbose) ---"); 129 | console.log(`Content Provider Type: ${event.message.type}`); 130 | 131 | if (chatServiceSid == undefined) { 132 | console.log("Chat Service SID is undefined"); 133 | return; 134 | } 135 | 136 | const downloadFile = await viberGetMessageContent(event.message.media); 137 | const data = downloadFile.body; 138 | const fileType = downloadFile.headers.get("content-type"); 139 | 140 | if (fileType == undefined) { 141 | console.log("File Type is undefined"); 142 | return; 143 | } 144 | 145 | console.log(`Incoming File Type (from HTTP Header): ${fileType}`); 146 | console.log("Uploading to Twilio MCS..."); 147 | let uploadMCSResult = await twilioUploadMediaResource( 148 | { accountSid: context.ACCOUNT_SID, authToken: context.AUTH_TOKEN }, 149 | chatServiceSid, 150 | fileType, 151 | data, 152 | event.message.file_name 153 | ); 154 | 155 | if (!uploadMCSResult.sid) { 156 | return false; 157 | } 158 | console.log(`Uploaded Twilio Media SID: ${uploadMCSResult.sid}`); 159 | addMessageResult = await twilioCreateMessage( 160 | client, 161 | conversationSid, 162 | event.sender.name, 163 | event.message.file_name, 164 | uploadMCSResult.sid 165 | ); 166 | } 167 | }; 168 | 169 | /* 170 | * Viber - Send Push Message 171 | */ 172 | export const viberSendTextMessage = async ( 173 | context: Context, 174 | userId: string, 175 | message: string 176 | ) => { 177 | try { 178 | // Get viber client 179 | const payload = { 180 | receiver: userId, 181 | type: "text", 182 | text: message, 183 | sender: { name: "Twilio" }, 184 | }; 185 | 186 | // Send message 187 | const response = await fetch("https://chatapi.viber.com/pa/send_message", { 188 | method: "post", 189 | body: JSON.stringify(payload), 190 | headers: { 191 | "Content-Type": "application/json", 192 | "X-Viber-Auth-Token": context.VIBER_AUTH_TOKEN, 193 | }, 194 | }); 195 | const data = await response.json(); 196 | 197 | console.log(data); 198 | 199 | return true; 200 | } catch (err) { 201 | console.log(err); 202 | throw err; 203 | } 204 | }; 205 | 206 | /* 207 | * Viber - Send Push Message 208 | */ 209 | export const viberSendMedia = async ( 210 | context: Context, 211 | userId: string, 212 | type: ViberTypes.ViberMessageType, 213 | contentUrl: string, 214 | size?: number 215 | ) => { 216 | try { 217 | // Get viber client 218 | const payload = { 219 | receiver: userId, 220 | type: type, 221 | text: "Attachment", 222 | media: contentUrl, 223 | sender: { name: "Twilio" }, 224 | ...(size && { size }), 225 | }; 226 | 227 | // Send message 228 | const response = await fetch("https://chatapi.viber.com/pa/send_message", { 229 | method: "post", 230 | body: JSON.stringify(payload), 231 | headers: { 232 | "Content-Type": "application/json", 233 | "X-Viber-Auth-Token": context.VIBER_AUTH_TOKEN, 234 | }, 235 | }); 236 | const data = await response.json(); 237 | 238 | console.log(data); 239 | 240 | return true; 241 | } catch (err) { 242 | console.log(err); 243 | throw err; 244 | } 245 | }; 246 | -------------------------------------------------------------------------------- /serverless-functions/src/functions/api/line/line.helper.private.ts: -------------------------------------------------------------------------------- 1 | // Import Libraries 2 | import crypto from "crypto"; 3 | import fetch, { Response } from "node-fetch"; 4 | import { Context } from "@twilio-labs/serverless-runtime-types/types"; 5 | import { 6 | ClientConfig, 7 | Client, 8 | TextMessage, 9 | ImageMessage, 10 | VideoMessage, 11 | EventMessage, 12 | } from "@line/bot-sdk"; 13 | import * as Util from "../common/common.helper.private"; 14 | import * as LINETypes from "./line_types.private"; 15 | 16 | // Load TypeScript - Types 17 | const { LINEMessageType } = ( 18 | require(Runtime.getFunctions()["api/line/line_types"].path) 19 | ); 20 | 21 | // Load Twilio Helper 22 | const { 23 | twilioUploadMediaResource, 24 | twilioFindExistingConversation, 25 | twilioCreateConversation, 26 | twilioCreateParticipant, 27 | twilioCreateScopedWebhookStudio, 28 | twilioCreateScopedWebhook, 29 | twilioCreateMessage, 30 | } = ( 31 | require(Runtime.getFunctions()["api/common/common.helper"].path) 32 | ); 33 | 34 | export const wrappedSendToFlex = async ( 35 | context: Context, 36 | userId: string, 37 | message: EventMessage 38 | ) => { 39 | const client = context.getTwilioClient(); 40 | 41 | // Step 1: Check for any existing conversation. If doesn't exist, create a new conversation -> add participant -> add webhooks 42 | const identity = `line:${userId}`; 43 | console.log(identity); 44 | 45 | let { conversationSid, chatServiceSid } = 46 | await twilioFindExistingConversation(client, identity); 47 | 48 | console.log(`Old Convo ID: ${conversationSid}`); 49 | console.log(`[Via Existing] Chat Service ID: ${chatServiceSid}`); 50 | 51 | if (!conversationSid) { 52 | // -- Create Conversation 53 | const createConversationResult = await twilioCreateConversation( 54 | "LINE", 55 | client, 56 | userId, 57 | {} 58 | ); 59 | conversationSid = createConversationResult.conversationSid; 60 | chatServiceSid = createConversationResult.chatServiceSid; 61 | // -- Add Participant into Conversation 62 | await twilioCreateParticipant(client, conversationSid, identity); 63 | // -- Create Webhook (Conversation Scoped) for Studio 64 | await twilioCreateScopedWebhookStudio( 65 | client, 66 | conversationSid, 67 | context.LINE_STUDIO_FLOW_SID 68 | ); 69 | // -- Create Webhook (Conversation Scoped) for Outgoing Conversation (Flex to LINE) 70 | let domainName = context.DOMAIN_NAME; 71 | if ( 72 | context.DOMAIN_NAME_OVERRIDE && 73 | context.DOMAIN_NAME_OVERRIDE !== "" 74 | ) { 75 | domainName = context.DOMAIN_NAME_OVERRIDE; 76 | } 77 | await twilioCreateScopedWebhook( 78 | client, 79 | conversationSid, 80 | userId, 81 | domainName, 82 | "api/line/outgoing" 83 | ); 84 | } 85 | 86 | console.log("Message type is: ", message.type); 87 | 88 | // Step 2: Add Message to Conversation 89 | // -- Process Message Type 90 | if (message.type === LINEMessageType.TEXT) { 91 | // -- Message Type: text 92 | await twilioCreateMessage( 93 | client, 94 | conversationSid, 95 | identity, 96 | (message as TextMessage).text 97 | ); 98 | } else if ( 99 | message.type === LINEMessageType.IMAGE || 100 | message.type === LINEMessageType.VIDEO 101 | ) { 102 | // -- Message Type: image, video 103 | console.log("--- Message Type: Media (Verbose) ---"); 104 | console.log(`Content Provider Type: ${message.contentProvider.type}`); 105 | 106 | if (chatServiceSid == undefined) { 107 | console.log("Chat Service SID is undefined"); 108 | return; 109 | } 110 | 111 | const downloadFile = await lineGetMessageContent(context, message.id); 112 | const data = downloadFile.body; 113 | const fileType = downloadFile.headers.get("content-type"); 114 | 115 | if (fileType == undefined) { 116 | console.log("File Type is undefined"); 117 | return; 118 | } 119 | 120 | console.log(`Incoming File Type (from HTTP Header): ${fileType}`); 121 | console.log("Uploading to Twilio MCS..."); 122 | let uploadMCSResult = await twilioUploadMediaResource( 123 | { accountSid: context.ACCOUNT_SID, authToken: context.AUTH_TOKEN }, 124 | chatServiceSid, 125 | fileType, 126 | data, 127 | "file" 128 | ); 129 | 130 | if (!uploadMCSResult.sid) { 131 | return false; 132 | } 133 | console.log(`Uploaded Twilio Media SID: ${uploadMCSResult.sid}`); 134 | await twilioCreateMessage( 135 | client, 136 | conversationSid, 137 | identity, 138 | "file", 139 | uploadMCSResult.sid 140 | ); 141 | } 142 | }; 143 | 144 | /** 145 | * Validate LINE Webhook Signature 146 | * @param {string} signature - Webhook 147 | * @param {string} body - API Response Payload 148 | * @param {string} secret - LINE Secret Key 149 | * @return {boolean} - Signature's validity 150 | */ 151 | export const lineValidateSignature = ( 152 | signature: string, 153 | body: string, 154 | secret: string 155 | ) => { 156 | // Generate HMAC-256 Digest 157 | const digest = crypto 158 | .createHmac("sha256", secret) 159 | .update(body) 160 | .digest("base64"); 161 | if (digest === signature) { 162 | return true; 163 | } else { 164 | return false; 165 | } 166 | }; 167 | 168 | /** 169 | * Send LINE Text Message 170 | * @param {LINETypes.LINEContext} context - LINE Context 171 | * @param {string} userId - LINE User ID 172 | * @param {string} message - Message 173 | * @returns {boolean} - Message sending status 174 | */ 175 | export const lineSendTextMessage = async ( 176 | context: Context, 177 | userId: string, 178 | message: string 179 | ) => { 180 | try { 181 | // Initialise LINE Client 182 | const clientConfig: ClientConfig = { 183 | channelAccessToken: context.LINE_CHANNEL_ACCESS_TOKEN, 184 | channelSecret: context.LINE_CHANNEL_SECRET, 185 | }; 186 | const client = new Client(clientConfig); 187 | 188 | // Send Text Message 189 | const sendMessagePayload: TextMessage = { 190 | type: "text", 191 | text: message, 192 | }; 193 | const result = await client.pushMessage(userId, sendMessagePayload); 194 | console.log("lineSendTextMessage: ", result); 195 | return true; 196 | } catch (err) { 197 | console.log(err); 198 | return false; 199 | } 200 | }; 201 | 202 | /** 203 | * Send LINE Media Message 204 | * @param {LINETypes.LINEContext} context - LINE Context 205 | * @param {string} userId - LINE User ID 206 | * @param {string} type - Media Type - Image or Video 207 | * @param {string} contentUrl - URL of Image or Video 208 | * @returns {boolean} - Message sending status 209 | */ 210 | export const lineSendMediaMessage = async ( 211 | context: Context, 212 | userId: string, 213 | type: "image" | "video", 214 | contentUrl: string 215 | ) => { 216 | try { 217 | // Initialise LINE Client 218 | const clientConfig: ClientConfig = { 219 | channelAccessToken: context.LINE_CHANNEL_ACCESS_TOKEN, 220 | channelSecret: context.LINE_CHANNEL_SECRET, 221 | }; 222 | const client = new Client(clientConfig); 223 | 224 | // Send Text Message 225 | const sendMessagePayload: ImageMessage | VideoMessage = { 226 | type: type, 227 | originalContentUrl: contentUrl, 228 | previewImageUrl: contentUrl, 229 | }; 230 | const result = await client.pushMessage(userId, sendMessagePayload); 231 | console.log("lineSendMediaMessage: ", result); 232 | return true; 233 | } catch (err) { 234 | console.log(err); 235 | return false; 236 | } 237 | }; 238 | 239 | /** 240 | * Get LINE Message Content 241 | * @param {LINETypes.LINEContext} context - LINE Context 242 | * @param {string} messageId - LINE Message ID 243 | * @returns {Response} - HTTP call response object 244 | */ 245 | export const lineGetMessageContent = async ( 246 | context: Context, 247 | messageID: string 248 | ) => { 249 | try { 250 | // Initialise LINE Client 251 | const response = await fetch( 252 | `https://api-data.line.me/v2/bot/message/${messageID}/content`, 253 | { 254 | method: "get", 255 | headers: { 256 | Authorization: `Bearer ${context.LINE_CHANNEL_ACCESS_TOKEN}`, 257 | }, 258 | } 259 | ); 260 | return response; 261 | } catch (err) { 262 | console.log(err); 263 | throw err; 264 | } 265 | }; 266 | -------------------------------------------------------------------------------- /serverless-functions/src/functions/api/instagram/instagram.helper.private.ts: -------------------------------------------------------------------------------- 1 | import crypto from "crypto"; 2 | import { Context } from "@twilio-labs/serverless-runtime-types/types"; 3 | import fetch from "node-fetch"; 4 | import * as Util from "../common/common.helper.private"; 5 | import * as InstagramTypes from "./instagram_types.private"; 6 | 7 | // Load Libraries 8 | const { InstagramMessageType } = ( 9 | require(Runtime.getFunctions()["api/instagram/instagram_types"].path) 10 | ); 11 | 12 | // Load Libraries 13 | const { 14 | twilioUploadMediaResource, 15 | twilioFindExistingConversation, 16 | twilioCreateConversation, 17 | twilioCreateParticipant, 18 | twilioCreateScopedWebhookStudio, 19 | twilioCreateScopedWebhook, 20 | twilioCreateMessage, 21 | } = ( 22 | require(Runtime.getFunctions()["api/common/common.helper"].path) 23 | ); 24 | 25 | /** 26 | * Wrapped Method to Push Incoming Webhook to Flex 27 | * @param {InstagramTypes.InstagramContext} context - Context 28 | * @param {string} userId - User ID of incoming message 29 | * @param {InstagramTypes.InstagramMessaging} messaging - Incoming Message Webhook 30 | * @return {boolean} - Status of Sending to Flex 31 | */ 32 | export const wrappedSendToFlex = async ( 33 | context: Context, 34 | userId: string, 35 | messaging: InstagramTypes.InstagramMessaging 36 | ) => { 37 | // Ignore Instagram Echo Events 38 | if (messaging.message.is_echo) { 39 | return true; 40 | } 41 | const client = context.getTwilioClient(); 42 | 43 | // Step 1: Check for any existing conversation. If doesn't exist, create a new conversation -> add participant -> add webhooks 44 | const identity = `instagram:${userId}`; 45 | console.log(identity); 46 | 47 | let { conversationSid, chatServiceSid } = 48 | await twilioFindExistingConversation(client, identity); 49 | 50 | console.log(`Old Convo ID: ${conversationSid}`); 51 | console.log(`[Via Existing] Chat Service ID: ${chatServiceSid}`); 52 | 53 | if (!conversationSid) { 54 | // -- Create Conversation 55 | const createConversationResult = await twilioCreateConversation( 56 | "Instagram", 57 | client, 58 | userId, 59 | {} 60 | ); 61 | conversationSid = createConversationResult.conversationSid; 62 | chatServiceSid = createConversationResult.chatServiceSid; 63 | // -- Add Participant into Conversation 64 | await twilioCreateParticipant(client, conversationSid, identity); 65 | // -- Create Webhook (Conversation Scoped) for Studio 66 | await twilioCreateScopedWebhookStudio( 67 | client, 68 | conversationSid, 69 | context.INSTAGRAM_STUDIO_FLOW_SID 70 | ); 71 | // -- Create Webhook (Conversation Scoped) for Outgoing Conversation (Flex to LINE) 72 | let domainName = context.DOMAIN_NAME; 73 | if ( 74 | context.DOMAIN_NAME_OVERRIDE && 75 | context.DOMAIN_NAME_OVERRIDE !== "" 76 | ) { 77 | domainName = context.DOMAIN_NAME_OVERRIDE; 78 | } 79 | await twilioCreateScopedWebhook( 80 | client, 81 | conversationSid, 82 | userId, 83 | domainName, 84 | "api/instagram/outgoing" 85 | ); 86 | } 87 | 88 | // Step 2: Add Message to Conversation 89 | if ("text" in messaging.message) { 90 | // -- Message Type: text 91 | await twilioCreateMessage( 92 | client, 93 | conversationSid, 94 | identity, 95 | (messaging.message as InstagramTypes.InstagramMessageText).text 96 | ); 97 | return true; 98 | } else if ("attachments" in messaging.message) { 99 | // -- Message Type: image, video, audio 100 | console.log("--- Message Type: Media (Verbose) ---"); 101 | if (chatServiceSid == undefined) { 102 | console.log("Chat Service SID is undefined"); 103 | return; 104 | } 105 | for (const attachment of messaging.message.attachments) { 106 | console.log("Content Type", attachment.type); 107 | const downloadFile = await instagramGetMediaContent( 108 | attachment.payload.url 109 | ); 110 | const data = downloadFile.body; 111 | const fileType = downloadFile.headers.get("content-type"); 112 | if (fileType == undefined) { 113 | console.log("File Type is undefined"); 114 | return; 115 | } 116 | console.log(`Incoming File Type (from HTTP Header): ${fileType}`); 117 | console.log("Uploading to Twilio MCS..."); 118 | let uploadMCSResult = await twilioUploadMediaResource( 119 | { accountSid: context.ACCOUNT_SID, authToken: context.AUTH_TOKEN }, 120 | chatServiceSid, 121 | fileType, 122 | data, 123 | "file" 124 | ); 125 | 126 | if (!uploadMCSResult.sid) { 127 | return false; 128 | } 129 | console.log(`Uploaded Twilio Media SID: ${uploadMCSResult.sid}`); 130 | await twilioCreateMessage( 131 | client, 132 | conversationSid, 133 | identity, 134 | "file", 135 | uploadMCSResult.sid 136 | ); 137 | } 138 | return true; 139 | } 140 | }; 141 | 142 | /** 143 | * Validate Instagram Webhook Signature 144 | * @param {string} signature - Webhook 145 | * @param {string} body - API Response Payload 146 | * @param {string} secret - Meta App Secret 147 | * @return {boolean} - Signature's validity 148 | */ 149 | export const instagramValidateSignature = ( 150 | signature: string, 151 | body: string, 152 | secret: string 153 | ) => { 154 | // Generate HMAC-256 Digest 155 | const digest = crypto.createHmac("sha256", secret).update(body).digest("hex"); 156 | if (digest === signature) { 157 | return true; 158 | } else { 159 | return false; 160 | } 161 | }; 162 | 163 | /** 164 | * Send Instagram Text Message 165 | * @param {InstagramTypes.InstagramContext} context - Instagram Context 166 | * @param {string} userId - Instagram Recipient ID 167 | * @param {string} message - Message 168 | * @returns {boolean} - Message sending status 169 | */ 170 | export const instagramSendTextMessage = async ( 171 | context: Context, 172 | userId: string, 173 | message: string 174 | ) => { 175 | try { 176 | // Formulate Payload 177 | const payload: InstagramTypes.InstagramSendMessagePayload = { 178 | recipient: { 179 | id: userId, 180 | }, 181 | message: { 182 | text: message, 183 | }, 184 | }; 185 | 186 | // Send Text Message 187 | const response = await fetch( 188 | `https://graph.facebook.com/v17.0/me/messages?access_token=${context.INSTAGRAM_PAGE_ACCESS_TOKEN}`, 189 | { 190 | method: "post", 191 | headers: { "Content-Type": "application/json" }, 192 | body: JSON.stringify(payload), 193 | } 194 | ); 195 | const data = await response.json(); 196 | console.log("instagramSendTextMessage: Message ID", data.message_id); 197 | return true; 198 | } catch (err) { 199 | console.log(err); 200 | throw err; 201 | } 202 | }; 203 | 204 | /** 205 | * Send Instagram Send Media 206 | * @param {InstagramTypes.InstagramContext} context - Instagram Context 207 | * @param {string} userId - Instagram Recipient ID 208 | * @param {contentType} contentType - Content Type of media: image, video or audio 209 | * @param {contentType} contentUrl - Public URL of content 210 | * @returns {boolean} - Message sending status 211 | */ 212 | export const instagramSendMediaMessage = async ( 213 | context: Context, 214 | userId: string, 215 | contentType: string, 216 | contentUrl: string 217 | ) => { 218 | try { 219 | // Formulate Payload 220 | const payload: InstagramTypes.InstagramSendMediaPayload = { 221 | recipient: { 222 | id: userId, 223 | }, 224 | message: { 225 | attachment: { 226 | type: contentType, 227 | payload: { 228 | url: contentUrl, 229 | }, 230 | }, 231 | }, 232 | }; 233 | 234 | // Send Text Message 235 | const response = await fetch( 236 | `https://graph.facebook.com/v17.0/me/messages?access_token=${context.INSTAGRAM_PAGE_ACCESS_TOKEN}`, 237 | { 238 | method: "post", 239 | headers: { "Content-Type": "application/json" }, 240 | body: JSON.stringify(payload), 241 | } 242 | ); 243 | const data = await response.json(); 244 | console.log("instagramSendTextMessage: Message ID", data.message_id); 245 | return true; 246 | } catch (err) { 247 | console.log(err); 248 | throw err; 249 | } 250 | }; 251 | 252 | /** 253 | * Get Media content 254 | * @param {string} context - Instagram Context 255 | * @returns {any} - Raw Content 256 | */ 257 | const instagramGetMediaContent = async (url: string) => { 258 | // Send message 259 | const response = await fetch(url, { 260 | method: "get", 261 | }); 262 | return response; 263 | }; 264 | -------------------------------------------------------------------------------- /serverless-functions/src/functions/api/common/common.helper.private.ts: -------------------------------------------------------------------------------- 1 | import * as twilio from "twilio"; 2 | import fetch from "node-fetch"; 3 | 4 | export type TwilioCredentials = { 5 | accountSid: string; 6 | authToken: string; 7 | }; 8 | 9 | export type TwilioMediaResponse = { 10 | sid: string; 11 | links: { 12 | content_direct_temporary: string; 13 | }; 14 | size?: number; 15 | }; 16 | 17 | /* 18 | * Raw Function - Twilio - Get Media 19 | */ 20 | export const twilioGetMediaResource = async ( 21 | credentials: TwilioCredentials, 22 | chatServiceSid: string, 23 | mediaSid: string 24 | ) => { 25 | // Authenticate with Twilio 26 | let auth = 27 | "Basic " + 28 | Buffer.from(credentials.accountSid + ":" + credentials.authToken).toString( 29 | "base64" 30 | ); 31 | 32 | // Send message 33 | const response = await fetch( 34 | `https://mcs.us1.twilio.com/v1/Services/${chatServiceSid}/Media/${mediaSid}`, 35 | { 36 | method: "get", 37 | headers: { "Content-Type": "application/json", Authorization: auth }, 38 | } 39 | ); 40 | return (await response.json()) as TwilioMediaResponse; 41 | }; 42 | 43 | /* 44 | * Raw Function - Twilio - Upload Media Resource 45 | */ 46 | export const twilioUploadMediaResource = async ( 47 | credentials: TwilioCredentials, 48 | chatServiceSid: string, 49 | contentType: string, 50 | data: any, 51 | fileName: string 52 | ) => { 53 | // Authenticate with Twilio 54 | let auth = 55 | "Basic " + 56 | Buffer.from(credentials.accountSid + ":" + credentials.authToken).toString( 57 | "base64" 58 | ); 59 | 60 | // Send message 61 | const response = await fetch( 62 | `https://mcs.us1.twilio.com/v1/Services/${chatServiceSid}/Media`, 63 | { 64 | method: "post", 65 | headers: { "Content-Type": contentType, Authorization: auth }, 66 | body: data, 67 | } 68 | ); 69 | return (await response.json()) as TwilioMediaResponse; 70 | }; 71 | 72 | /* 73 | * Twilio - Create Message in Conversation 74 | */ 75 | export const twilioCreateMessage = async ( 76 | client: twilio.Twilio, 77 | conversationSid: string, 78 | author: string, 79 | body: string, 80 | mediaSid: string | null = null 81 | ) => { 82 | try { 83 | let result; 84 | if (!mediaSid) { 85 | result = await client.conversations 86 | .conversations(conversationSid) 87 | .messages.create({ 88 | author: author, 89 | body: body, 90 | xTwilioWebhookEnabled: "true", 91 | }); 92 | } else { 93 | result = await client.conversations 94 | .conversations(conversationSid) 95 | .messages.create({ 96 | author: author, 97 | body: body, 98 | mediaSid: mediaSid, 99 | xTwilioWebhookEnabled: "true", 100 | }); 101 | } 102 | if (result.sid) { 103 | return result.sid; 104 | } else { 105 | return false; 106 | } 107 | } catch (err) { 108 | console.log(err); 109 | return false; 110 | } 111 | }; 112 | 113 | /* 114 | * Twilio - Create Message in Conversation 115 | */ 116 | export const twilioDeleteMessage = async ( 117 | client: twilio.Twilio, 118 | conversationSid: string, 119 | messageSid: string 120 | ) => { 121 | try { 122 | const result = await client.conversations 123 | .conversations(conversationSid) 124 | .messages(messageSid) 125 | .remove(); 126 | 127 | if (result) { 128 | return result; 129 | } else { 130 | return false; 131 | } 132 | } catch (err) { 133 | console.log(err); 134 | return false; 135 | } 136 | }; 137 | 138 | /* 139 | * Twilio - Create Participant in Conversations 140 | */ 141 | export const twilioCreateParticipant = async ( 142 | client: twilio.Twilio, 143 | conversationSid: string, 144 | identity: string 145 | ) => { 146 | try { 147 | const result = await client.conversations 148 | .conversations(conversationSid) 149 | .participants.create({ identity: identity }); 150 | if (result.sid) { 151 | return result.sid; 152 | } else { 153 | return false; 154 | } 155 | } catch (err) { 156 | console.log(err); 157 | return false; 158 | } 159 | }; 160 | 161 | /* 162 | * Twilio - Create Conversation 163 | */ 164 | export const twilioCreateConversation = async ( 165 | adapter: string, 166 | client: twilio.Twilio, 167 | userId: string, 168 | pre_engagement_attributes: any = {} 169 | ) => { 170 | const result = await client.conversations.v1.conversations.create({ 171 | friendlyName: `${adapter} Conversation ${userId}`, 172 | attributes: JSON.stringify({ 173 | pre_engagement_data: pre_engagement_attributes, 174 | }), 175 | }); 176 | if (result.sid) { 177 | return { 178 | conversationSid: result.sid, 179 | chatServiceSid: result.chatServiceSid, 180 | }; 181 | } else { 182 | throw new Error("Could not create new conversation"); 183 | } 184 | }; 185 | /* 186 | * Twilio - Find Existing Conversation 187 | */ 188 | export const twilioFindExistingConversation = async ( 189 | client: twilio.Twilio, 190 | identity: string 191 | ) => { 192 | const result = await client.conversations.participantConversations.list({ 193 | identity: identity, 194 | }); 195 | 196 | let existingConversation = result.find( 197 | (conversation) => conversation.conversationState !== "closed" 198 | ); 199 | 200 | if (existingConversation) { 201 | console.log(existingConversation); 202 | console.log(existingConversation.chatServiceSid); 203 | return { 204 | conversationSid: existingConversation.conversationSid, 205 | chatServiceSid: existingConversation.chatServiceSid, 206 | }; 207 | } else { 208 | return { 209 | conversationSid: undefined, 210 | chatServiceSid: undefined, 211 | }; 212 | } 213 | }; 214 | 215 | /* 216 | * Twilio - Get Conversation 217 | */ 218 | export const twilioGetConversation = async ( 219 | client: twilio.Twilio, 220 | conversationSid: string 221 | ) => { 222 | try { 223 | const result = await client.conversations 224 | .conversations(conversationSid) 225 | .fetch(); 226 | if (result.sid) { 227 | return result; 228 | } else { 229 | return false; 230 | } 231 | } catch (err) { 232 | console.log(err); 233 | return false; 234 | } 235 | }; 236 | 237 | /* 238 | * Twilio - Create Scoped Webhook for onMessageAdded to send to Studio Flow 239 | */ 240 | export const twilioCreateScopedWebhookStudio = async ( 241 | client: twilio.Twilio, 242 | conversationSid: string, 243 | studioFlowSid: string 244 | ) => { 245 | try { 246 | const result = await client.conversations 247 | .conversations(conversationSid) 248 | .webhooks.create({ 249 | configuration: { filters: "onMessageAdded", flowSid: studioFlowSid }, 250 | target: "studio", 251 | }); 252 | if (result.sid) { 253 | return result.sid; 254 | } else { 255 | return false; 256 | } 257 | } catch (err) { 258 | console.log(err); 259 | return false; 260 | } 261 | }; 262 | 263 | /* 264 | * Twilio - Get Scoped Webhook 265 | */ 266 | export const twilioGetScopedWebhookStudio = async ( 267 | client: twilio.Twilio, 268 | conversationSid: string 269 | ) => { 270 | try { 271 | const result = await client.conversations 272 | .conversations(conversationSid) 273 | .webhooks.list({ limit: 20 }); 274 | if (result.length <= 0) { 275 | return false; 276 | } 277 | for (const wh of result) { 278 | if (wh.target === "studio") { 279 | return wh; 280 | } 281 | } 282 | return false; 283 | } catch (err) { 284 | console.log(err); 285 | return false; 286 | } 287 | }; 288 | 289 | /* 290 | * Twilio - Delete Scoped Webhook 291 | */ 292 | export const twilioDeleteScopedWebhookStudio = async ( 293 | client: twilio.Twilio, 294 | conversationSid: string, 295 | webhookSid: string 296 | ) => { 297 | try { 298 | const result = await client.conversations 299 | .conversations(conversationSid) 300 | .webhooks(webhookSid) 301 | .remove(); 302 | return true; 303 | } catch (err) { 304 | console.log(err); 305 | return false; 306 | } 307 | }; 308 | 309 | /* 310 | * Twilio - Create Scoped Webhook for onMessageAdded to receive New Message 311 | */ 312 | export const twilioCreateScopedWebhook = async ( 313 | client: twilio.Twilio, 314 | conversationSid: string, 315 | userId: string, 316 | domainName: string, 317 | outgoingPath: string 318 | ) => { 319 | try { 320 | console.log(`https://${domainName}/${outgoingPath}`); 321 | const result = await client.conversations 322 | .conversations(conversationSid) 323 | .webhooks.create({ 324 | target: "webhook", 325 | configuration: { 326 | filters: "onMessageAdded", 327 | method: "POST", 328 | url: 329 | `https://${domainName}/${outgoingPath}?user_id=` + 330 | encodeURIComponent(userId), 331 | }, 332 | }); 333 | if (result.sid) { 334 | return result.sid; 335 | } else { 336 | return false; 337 | } 338 | } catch (err) { 339 | console.log(err); 340 | return false; 341 | } 342 | }; 343 | -------------------------------------------------------------------------------- /serverless-functions/scripts/common.js: -------------------------------------------------------------------------------- 1 | // Import Libraries 2 | const shell = require("shelljs"); 3 | const fs = require("fs"); 4 | const path = require("path"); 5 | const dotenv = require("dotenv"); 6 | 7 | exports.copyFile = ( 8 | exampleFileName = ".env.example", 9 | destinationFileName = ".env" 10 | ) => { 11 | try { 12 | // Check if exampleFileName exist 13 | if (!shell.test("-e", exampleFileName)) { 14 | console.log("copyFile", `exampleFileName does not exist`); 15 | return false; 16 | } 17 | // Check if destinationFileName exist 18 | if (shell.test("-e", destinationFileName)) { 19 | console.log( 20 | "copyFile", 21 | `destinationFileName already exist. No copy is needed` 22 | ); 23 | return false; 24 | } 25 | shell.cp(exampleFileName, destinationFileName); 26 | console.log( 27 | "copyFile", 28 | `Successfully copied ${exampleFileName} into ${destinationFileName}` 29 | ); 30 | return true; 31 | } catch (e) { 32 | console.log("Error in copyFile:"); 33 | console.log(e); 34 | return false; 35 | } 36 | }; 37 | 38 | exports.parseExampleEnvironmentVariables = ( 39 | exampleEnvironmentFileName = ".env.example" 40 | ) => { 41 | try { 42 | // Step 1: Set Variables 43 | const defaultTwilioVariables = ["ACCOUNT_SID", "AUTH_TOKEN"]; 44 | let context = { 45 | twilio: {}, 46 | conversations_adapters: {}, 47 | env_requires_replacement: {}, 48 | env_error: {}, 49 | env_raw: {}, 50 | }; 51 | 52 | // Step 2: Parse Example Environment Variables into JSON Object 53 | const exampleEnvironmentVariables = dotenv.parse( 54 | fs.readFileSync(exampleEnvironmentFileName) 55 | ); 56 | context.env_raw = { 57 | ...exampleEnvironmentVariables, 58 | }; 59 | 60 | // Step 3: Process Environment Variables and Set into Context 61 | for (const jsonKey of Object.keys(exampleEnvironmentVariables)) { 62 | // Set Default Twilio Variables into Context 63 | if (defaultTwilioVariables.includes(jsonKey)) { 64 | context.twilio[jsonKey] = exampleEnvironmentVariables[jsonKey]; 65 | } 66 | // Set Variables That Requires Replacement into Context - Value to conform with naming convention of "" 67 | else if ( 68 | exampleEnvironmentVariables[jsonKey].startsWith(``) 69 | ) { 70 | context.env_requires_replacement[jsonKey] = 71 | exampleEnvironmentVariables[jsonKey]; 72 | } 73 | // Set Ready-to-Use Variables into Context 74 | else if (exampleEnvironmentVariables[jsonKey]) { 75 | context.conversations_adapters[jsonKey] = 76 | exampleEnvironmentVariables[jsonKey]; 77 | } 78 | // Set Error/Empty Variables into Context 79 | else { 80 | context.env_error[jsonKey] = exampleEnvironmentVariables[jsonKey]; 81 | } 82 | } 83 | // Step 4: Return Context 84 | return context; 85 | } catch (err) { 86 | console.log("Error in parseExampleEnvironments:"); 87 | console.log(err); 88 | return false; 89 | } 90 | }; 91 | 92 | exports.replaceEnvironmentVariables = (context, destinationFileName) => { 93 | try { 94 | if ( 95 | context && 96 | context.env_requires_replacement && 97 | Object.keys(context.env_requires_replacement).length > 0 98 | ) { 99 | // Process Environment Variables That Requires Replacement 100 | for (const key of Object.keys(context.env_requires_replacement)) { 101 | const regexExpression = new RegExp(``, "g"); 102 | if (process.env[key] && process.env[key].length <= 255) { 103 | // Replace Variable 104 | shell.sed( 105 | "-i", 106 | regexExpression, 107 | `${process.env[key]}`, 108 | destinationFileName 109 | ); 110 | context.conversations_adapters[key] = process.env[key]; 111 | // Remove From Context 112 | delete context.env_requires_replacement[key]; 113 | } else if (process.env[key] && process.env[key].length >= 255) { 114 | // Twilio Functions only allow a maximum of 255 characters for it's environment variable 115 | // Supports only base64 encoded JSON - will write it to a private Asset file in Twilio Functions 116 | console.log( 117 | `${key} has a value of more than 255 characters - processing now` 118 | ); 119 | // -- Set Variables 120 | const splitVariableName = key.split("_"); 121 | const adapterName = splitVariableName[0].toLowerCase(); 122 | const suffix = "-credentials.private.json"; 123 | // -- Check if variable's value is base64 124 | const base64RegExp = 125 | /^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=|[A-Za-z0-9+\/]{4})$/; 126 | const isBase64 = base64RegExp.test(process.env[key]); 127 | if (isBase64) { 128 | console.log(`${key}'s content is formatted in base64`); 129 | const content = Buffer.from(process.env[key], "base64").toString( 130 | "utf-8" 131 | ); 132 | try { 133 | const parsedContentJSON = JSON.parse(content); 134 | const contentJSON = JSON.stringify(parsedContentJSON); 135 | const directoryPath = path.join(__dirname, "..", "src", "assets"); 136 | const filePath = path.join( 137 | __dirname, 138 | "..", 139 | "src", 140 | "assets", 141 | `${adapterName}${suffix}` 142 | ); 143 | if (!fs.existsSync(directoryPath)) { 144 | fs.mkdirSync(directoryPath, { recursive: true }); 145 | } 146 | fs.writeFileSync(filePath, contentJSON); 147 | console.log( 148 | `${key}'s content is successfully parsed and written into a JSON file under Twilio Assets` 149 | ); 150 | // Replace Variable 151 | shell.sed( 152 | "-i", 153 | regexExpression, 154 | `SUCCESSFULLY_CREATED_FILE_${adapterName}${suffix}`, 155 | destinationFileName 156 | ); 157 | context.conversations_adapters[key] = process.env[key]; 158 | // Remove From Context 159 | delete context.env_requires_replacement[key]; 160 | } catch (err) { 161 | console.log("Error in Parsing JSON", err); 162 | // Replace Variable 163 | shell.sed( 164 | "-i", 165 | regexExpression, 166 | `VALUE_IS_MORE_THAN_255_CHARACTERS_AND_UNABLE_TO_PARSE_JSON`, 167 | destinationFileName 168 | ); 169 | context.conversations_adapters[key] = process.env[key]; 170 | // Remove From Context 171 | delete context.env_requires_replacement[key]; 172 | } 173 | } else { 174 | // Replace Variable 175 | shell.sed( 176 | "-i", 177 | regexExpression, 178 | `VALUE_IS_MORE_THAN_255_CHARACTERS`, 179 | destinationFileName 180 | ); 181 | context.conversations_adapters[key] = process.env[key]; 182 | // Remove From Context 183 | delete context.env_requires_replacement[key]; 184 | } 185 | } 186 | } 187 | } 188 | return context; 189 | } catch (err) { 190 | console.log("Error in updateEnvironmentVariables:"); 191 | console.log(err); 192 | return false; 193 | } 194 | }; 195 | 196 | exports.printContextVariables = (context, headerMessage = "Context") => { 197 | try { 198 | console.log(`======== Start: ${headerMessage} =======`); 199 | console.log(""); 200 | // Print Twilio Default Variables 201 | if (context.twilio && Object.keys(context.twilio).length > 0) { 202 | console.log("=== Twilio Default Variables ==="); 203 | for (const key of Object.keys(context.twilio)) { 204 | console.log(`${key}: ${context.twilio[key]}`); 205 | } 206 | console.log(""); 207 | console.log( 208 | `Note: ACCOUNT_SID and AUTH_TOKEN can be empty as it will be auto-populated during deployment` 209 | ); 210 | console.log(""); 211 | } 212 | // Print Flex Default Variables 213 | if (context.flex && Object.keys(context.flex).length > 0) { 214 | console.log("=== Flex Default Variables ==="); 215 | for (const key of Object.keys(context.flex)) { 216 | console.log(`${key}: ${context.flex[key]}`); 217 | } 218 | console.log(""); 219 | } 220 | // Print Conversation Adapters Ready-to-Use Variables 221 | if ( 222 | context.conversations_adapters && 223 | Object.keys(context.conversations_adapters).length > 0 224 | ) { 225 | console.log("=== Conversations Adapters: Ready-to-Use Variables ==="); 226 | for (const key of Object.keys(context.conversations_adapters)) { 227 | console.log(`${key}: ${context.conversations_adapters[key]}`); 228 | } 229 | console.log(""); 230 | } 231 | // Print Conversation Adapters Need-to-Replace Variables 232 | if ( 233 | context.env_requires_replacement && 234 | Object.keys(context.env_requires_replacement).length > 0 235 | ) { 236 | console.log("=== Conversations Adapters: Need-To-Replace Variables ==="); 237 | for (const key of Object.keys(context.env_requires_replacement)) { 238 | console.log(`${key}: ${context.env_requires_replacement[key]}`); 239 | } 240 | console.log(""); 241 | console.log( 242 | `Note: These variables are not being replaced as no corresponding values (either in .env or Github Actions) have been found` 243 | ); 244 | console.log(""); 245 | } 246 | // Print Error Variables 247 | if (context.env_error && Object.keys(context.env_error).length > 0) { 248 | console.log("=== Conversations Adapters: Error Variables ==="); 249 | for (const key of Object.keys(context.env_error)) { 250 | console.log(`${key}: ${context.env_error[key]}`); 251 | } 252 | console.log(""); 253 | } 254 | console.log(`======== End: ${headerMessage} ========`); 255 | } catch (err) { 256 | console.log("Error in printContextVariables:"); 257 | console.log(err); 258 | return false; 259 | } 260 | }; 261 | -------------------------------------------------------------------------------- /plugin-conversations-icons/src/ConversationsIconsPlugin.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as Flex from "@twilio/flex-ui"; 3 | import { FlexPlugin } from "@twilio/flex-plugin"; 4 | import { 5 | DiscordIcon, 6 | FacebookMessengerIcon, 7 | GoogleChatIcon, 8 | InstagramIcon, 9 | KakaoTalkIcon, 10 | LINEIcon, 11 | MicrosoftTeamsIcon, 12 | SlackIcon, 13 | TelegramIcon, 14 | TikTokIcon, 15 | TwitterIcon, 16 | ViberIcon, 17 | WeChatIcon, 18 | ZaloIcon, 19 | } from "./components/SocialIcons"; 20 | 21 | const PLUGIN_NAME = "ConversationsIconsPlugin"; 22 | 23 | export default class ConversationsIconsPlugin extends FlexPlugin { 24 | constructor() { 25 | super(PLUGIN_NAME); 26 | } 27 | 28 | /** 29 | * Flex Conversations Adapters - Show Icons 30 | * 31 | * @param flex { typeof Flex } 32 | */ 33 | async init(flex: typeof Flex, manager: Flex.Manager): Promise { 34 | /** 35 | * Check Channel Source 36 | * @param {Flex.ITask} task - Flex's Task 37 | * @param {string} identityPrefix - Identity's prefix 38 | * @return {boolean} - Whether channel matches 39 | */ 40 | const checkChannel = (task: Flex.ITask, identityPrefix: string) => { 41 | if (task?.attributes?.customerAddress?.startsWith(identityPrefix)) { 42 | return true; 43 | } else { 44 | return false; 45 | } 46 | }; 47 | 48 | // Channel: Discord 49 | const chatChannelDiscord = Flex.DefaultTaskChannels.createChatTaskChannel( 50 | "discord", 51 | (task) => checkChannel(task, "discord") 52 | ); 53 | chatChannelDiscord.icons = { 54 | active: , 55 | list: { 56 | Assigned: , 57 | Canceled: , 58 | Completed: , 59 | Pending: , 60 | Reserved: , 61 | Wrapping: , 62 | }, 63 | main: , 64 | }; 65 | Flex.TaskChannels.register(chatChannelDiscord); 66 | 67 | // Channel: Facebook Messenger 68 | const chatChannelFBM = Flex.DefaultTaskChannels.createChatTaskChannel( 69 | "messenger", 70 | (task) => checkChannel(task, "messenger") 71 | ); 72 | chatChannelFBM.icons = { 73 | active: , 74 | list: { 75 | Assigned: , 76 | Canceled: , 77 | Completed: , 78 | Pending: , 79 | Reserved: , 80 | Wrapping: , 81 | }, 82 | main: , 83 | }; 84 | Flex.TaskChannels.register(chatChannelFBM); 85 | 86 | // Channel: Google Chat 87 | const chatChannelGoogleChat = 88 | Flex.DefaultTaskChannels.createChatTaskChannel( 89 | "gchat", 90 | (task) => 91 | checkChannel(task, "gchat") || checkChannel(task, "googlechat") 92 | ); 93 | chatChannelGoogleChat.icons = { 94 | active: , 95 | list: { 96 | Assigned: , 97 | Canceled: , 98 | Completed: , 99 | Pending: , 100 | Reserved: , 101 | Wrapping: , 102 | }, 103 | main: , 104 | }; 105 | Flex.TaskChannels.register(chatChannelGoogleChat); 106 | 107 | // Channel: Instagram 108 | const chatChannelInstagram = Flex.DefaultTaskChannels.createChatTaskChannel( 109 | "instagram", 110 | (task) => checkChannel(task, "ig") || checkChannel(task, "instagram") 111 | ); 112 | chatChannelInstagram.icons = { 113 | active: , 114 | list: { 115 | Assigned: , 116 | Canceled: , 117 | Completed: , 118 | Pending: , 119 | Reserved: , 120 | Wrapping: , 121 | }, 122 | main: , 123 | }; 124 | Flex.TaskChannels.register(chatChannelInstagram); 125 | 126 | // Channel: KakaoTalk 127 | const chatChannelKakaoTalk = Flex.DefaultTaskChannels.createChatTaskChannel( 128 | "kakaotalk", 129 | (task) => checkChannel(task, "kakaotalk") 130 | ); 131 | chatChannelKakaoTalk.icons = { 132 | active: , 133 | list: { 134 | Assigned: , 135 | Canceled: , 136 | Completed: , 137 | Pending: , 138 | Reserved: , 139 | Wrapping: , 140 | }, 141 | main: , 142 | }; 143 | Flex.TaskChannels.register(chatChannelKakaoTalk); 144 | 145 | // Channel: LINE 146 | const chatChannelLINE = Flex.DefaultTaskChannels.createChatTaskChannel( 147 | "line", 148 | (task) => checkChannel(task, "line") 149 | ); 150 | chatChannelLINE.icons = { 151 | active: , 152 | list: { 153 | Assigned: , 154 | Canceled: , 155 | Completed: , 156 | Pending: , 157 | Reserved: , 158 | Wrapping: , 159 | }, 160 | main: , 161 | }; 162 | Flex.TaskChannels.register(chatChannelLINE); 163 | 164 | // Channel: Microsoft Teams 165 | const chatChannelTeams = Flex.DefaultTaskChannels.createChatTaskChannel( 166 | "teams", 167 | (task) => checkChannel(task, "teams") 168 | ); 169 | chatChannelTeams.icons = { 170 | active: , 171 | list: { 172 | Assigned: , 173 | Canceled: , 174 | Completed: , 175 | Pending: , 176 | Reserved: , 177 | Wrapping: , 178 | }, 179 | main: , 180 | }; 181 | Flex.TaskChannels.register(chatChannelTeams); 182 | 183 | // Channel: Slack 184 | const chatChannelSlack = Flex.DefaultTaskChannels.createChatTaskChannel( 185 | "slack", 186 | (task) => checkChannel(task, "slack") 187 | ); 188 | chatChannelSlack.icons = { 189 | active: , 190 | list: { 191 | Assigned: , 192 | Canceled: , 193 | Completed: , 194 | Pending: , 195 | Reserved: , 196 | Wrapping: , 197 | }, 198 | main: , 199 | }; 200 | Flex.TaskChannels.register(chatChannelSlack); 201 | 202 | // Channel: Telegram 203 | const chatChannelTelegram = Flex.DefaultTaskChannels.createChatTaskChannel( 204 | "telegram", 205 | (task) => checkChannel(task, "telegram") 206 | ); 207 | chatChannelTelegram.icons = { 208 | active: , 209 | list: { 210 | Assigned: , 211 | Canceled: , 212 | Completed: , 213 | Pending: , 214 | Reserved: , 215 | Wrapping: , 216 | }, 217 | main: , 218 | }; 219 | Flex.TaskChannels.register(chatChannelTelegram); 220 | 221 | // Channel: TikTok 222 | const chatChannelTikTok = Flex.DefaultTaskChannels.createChatTaskChannel( 223 | "tiktok", 224 | (task) => checkChannel(task, "tiktok") 225 | ); 226 | chatChannelTikTok.icons = { 227 | active: , 228 | list: { 229 | Assigned: , 230 | Canceled: , 231 | Completed: , 232 | Pending: , 233 | Reserved: , 234 | Wrapping: , 235 | }, 236 | main: , 237 | }; 238 | Flex.TaskChannels.register(chatChannelTikTok); 239 | 240 | // Channel: Twitter 241 | const chatChannelTwitter = Flex.DefaultTaskChannels.createChatTaskChannel( 242 | "twitter", 243 | (task) => checkChannel(task, "twitter") 244 | ); 245 | chatChannelTwitter.icons = { 246 | active: , 247 | list: { 248 | Assigned: , 249 | Canceled: , 250 | Completed: , 251 | Pending: , 252 | Reserved: , 253 | Wrapping: , 254 | }, 255 | main: , 256 | }; 257 | Flex.TaskChannels.register(chatChannelTwitter); 258 | 259 | // Channel: Viber 260 | const chatChannelViber = Flex.DefaultTaskChannels.createChatTaskChannel( 261 | "viber", 262 | (task) => checkChannel(task, "viber") 263 | ); 264 | chatChannelViber.icons = { 265 | active: , 266 | list: { 267 | Assigned: , 268 | Canceled: , 269 | Completed: , 270 | Pending: , 271 | Reserved: , 272 | Wrapping: , 273 | }, 274 | main: , 275 | }; 276 | Flex.TaskChannels.register(chatChannelViber); 277 | 278 | // Channel: WeChat 279 | const chatChannelWeChat = Flex.DefaultTaskChannels.createChatTaskChannel( 280 | "wechat", 281 | (task) => checkChannel(task, "wechat") 282 | ); 283 | chatChannelWeChat.icons = { 284 | active: , 285 | list: { 286 | Assigned: , 287 | Canceled: , 288 | Completed: , 289 | Pending: , 290 | Reserved: , 291 | Wrapping: , 292 | }, 293 | main: , 294 | }; 295 | Flex.TaskChannels.register(chatChannelWeChat); 296 | 297 | // Channel: Zalo 298 | const chatChannelZalo = Flex.DefaultTaskChannels.createChatTaskChannel( 299 | "zalo", 300 | (task) => checkChannel(task, "zalo") 301 | ); 302 | chatChannelZalo.icons = { 303 | active: , 304 | list: { 305 | Assigned: , 306 | Canceled: , 307 | Completed: , 308 | Pending: , 309 | Reserved: , 310 | Wrapping: , 311 | }, 312 | main: , 313 | }; 314 | Flex.TaskChannels.register(chatChannelZalo); 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /serverless-functions/src/functions/api/googlechat/googlechat.helper.private.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "@twilio-labs/serverless-runtime-types/types"; 2 | import fetch from "node-fetch"; 3 | import jwt from "jsonwebtoken"; 4 | import { v4 as uuidv4 } from "uuid"; 5 | 6 | import { google, chat_v1 } from "googleapis"; 7 | 8 | import * as Util from "../common/common.helper.private"; 9 | import * as GoogleChatTypes from "./googlechat_types.private"; 10 | 11 | // Load Libraries 12 | const { GoogleChatMessageType } = ( 13 | require(Runtime.getFunctions()["api/googlechat/googlechat_types"].path) 14 | ); 15 | 16 | // Load Libraries 17 | const { 18 | twilioUploadMediaResource, 19 | twilioFindExistingConversation, 20 | twilioCreateConversation, 21 | twilioCreateParticipant, 22 | twilioCreateScopedWebhookStudio, 23 | twilioCreateScopedWebhook, 24 | twilioCreateMessage, 25 | } = ( 26 | require(Runtime.getFunctions()["api/common/common.helper"].path) 27 | ); 28 | 29 | /** 30 | * Wrapped Method to Push Incoming Webhook to Flex 31 | * @param {GoogleChatTypes.GoogleChatContext} context - Context 32 | * @param {string} userId - User ID of incoming message 33 | * @param {GoogleChatTypes.GoogleChatBaseMessage} event - Incoming Message Webhook 34 | * @return {boolean} - Status of Sending to Flex 35 | */ 36 | export const wrappedSendToFlex = async ( 37 | context: Context, 38 | userId: string, 39 | event: GoogleChatTypes.GoogleChatBaseMessage 40 | ) => { 41 | const client = context.getTwilioClient(); 42 | 43 | // Step 0: Validate Required Incoming Data 44 | if (!event.message || !event.space) { 45 | return false; 46 | } 47 | 48 | // Step 1: Check for any existing conversation. If doesn't exist, create a new conversation -> add participant -> add webhooks 49 | const identity = `googlechat:${userId}`; 50 | const spaceName = event.space.name; 51 | console.log("identity:", identity); 52 | console.log("spaceName:", spaceName); 53 | 54 | let { conversationSid, chatServiceSid } = 55 | await twilioFindExistingConversation(client, identity); 56 | 57 | console.log(`Old Convo ID: ${conversationSid}`); 58 | console.log(`[Via Existing] Chat Service ID: ${chatServiceSid}`); 59 | 60 | if (!conversationSid) { 61 | // -- Create Conversation 62 | const createConversationResult = await twilioCreateConversation( 63 | "GoogleChat", 64 | client, 65 | userId, 66 | { spaceName: event.space?.name } 67 | ); 68 | conversationSid = createConversationResult.conversationSid; 69 | chatServiceSid = createConversationResult.chatServiceSid; 70 | // -- Add Participant into Conversation 71 | await twilioCreateParticipant(client, conversationSid, identity); 72 | // -- Create Webhook (Conversation Scoped) for Studio 73 | await twilioCreateScopedWebhookStudio( 74 | client, 75 | conversationSid, 76 | context.GOOGLECHAT_STUDIO_FLOW_SID 77 | ); 78 | // -- Create Webhook (Conversation Scoped) for Outgoing Conversation (Flex to LINE) 79 | let domainName = context.DOMAIN_NAME; 80 | if ( 81 | context.DOMAIN_NAME_OVERRIDE && 82 | context.DOMAIN_NAME_OVERRIDE !== "" 83 | ) { 84 | domainName = context.DOMAIN_NAME_OVERRIDE; 85 | } 86 | await twilioCreateScopedWebhook( 87 | client, 88 | conversationSid, 89 | userId, 90 | domainName, 91 | "api/googlechat/outgoing" 92 | ); 93 | } 94 | 95 | // Step 2: Add Message to Conversation 96 | if ("text" in event.message && !("attachment" in event.message)) { 97 | // -- Message Type: text 98 | if (!event.message.text || !event.user || !event.user.displayName) { 99 | return false; 100 | } 101 | await twilioCreateMessage( 102 | client, 103 | conversationSid, 104 | event.user.displayName, 105 | event.message.text 106 | ); 107 | return true; 108 | } else if ("attachment" in event.message) { 109 | // -- Message Type: image, video, audio 110 | console.log("--- Message Type: Media (Verbose) ---"); 111 | if (chatServiceSid == undefined || !event.message.attachment) { 112 | console.log("Chat Service SID and/or Attachment is undefined"); 113 | return false; 114 | } 115 | const chatClient = await getGoogleChatClient(context); 116 | for (const attachment of event.message.attachment) { 117 | if ( 118 | !attachment.attachmentDataRef || 119 | !attachment.attachmentDataRef.resourceName || 120 | !event.user || 121 | !event.user.displayName 122 | ) { 123 | return false; 124 | } 125 | console.log("Content Type", attachment.contentType); 126 | const downloadFile = await googleChatGetAttachmentContent( 127 | chatClient, 128 | attachment.attachmentDataRef.resourceName 129 | ); 130 | const data = downloadFile; 131 | const fileName = attachment.contentName || "file"; 132 | const fileType = attachment.contentType; 133 | if (fileType == undefined) { 134 | console.log("File Type is undefined"); 135 | return; 136 | } 137 | console.log(`Incoming File Type (from HTTP Header): ${fileType}`); 138 | console.log("Uploading to Twilio MCS..."); 139 | let uploadMCSResult = await twilioUploadMediaResource( 140 | { accountSid: context.ACCOUNT_SID, authToken: context.AUTH_TOKEN }, 141 | chatServiceSid, 142 | fileType, 143 | data, 144 | fileName 145 | ); 146 | 147 | if (!uploadMCSResult.sid) { 148 | return false; 149 | } 150 | console.log(`Uploaded Twilio Media SID: ${uploadMCSResult.sid}`); 151 | // -- Check if Text sent together with Attachment 152 | if (event.message.text) { 153 | await twilioCreateMessage( 154 | client, 155 | conversationSid, 156 | event.user.displayName, 157 | event.message.text 158 | ); 159 | } 160 | await twilioCreateMessage( 161 | client, 162 | conversationSid, 163 | event.user.displayName, 164 | fileName, 165 | uploadMCSResult.sid 166 | ); 167 | } 168 | return true; 169 | } 170 | }; 171 | 172 | /** 173 | * Validate Google Chat Webhook JWT 174 | * @param {string} googleJwt - Google Chat's JWT 175 | * @return {boolean} - Google JWT's validity 176 | */ 177 | export const googleChatVerifyJwt = async (googleJwt: string) => { 178 | try { 179 | // Step 1: Get Public Certs from Google 180 | // Note: Google Certs expires frequently. Do not hard code. 181 | const certUrl = 182 | "https://www.googleapis.com/service_accounts/v1/metadata/x509/chat@system.gserviceaccount.com"; 183 | const getCertsResponse = await fetch(certUrl); 184 | const getCerts = await getCertsResponse.json(); 185 | 186 | // Step 2: Get Incoming Webhook's JWT Header 187 | const jwtDecoded = jwt.decode(googleJwt, { complete: true }); 188 | if ( 189 | jwtDecoded === null || 190 | jwtDecoded.header === null || 191 | jwtDecoded.header.kid === null 192 | ) { 193 | return false; 194 | } 195 | const jwtKid = jwtDecoded.header.kid || 0; 196 | 197 | // Step 3: Verify JWT 198 | if (!getCerts.hasOwnProperty(jwtKid)) { 199 | return false; 200 | } 201 | await jwt.verify(googleJwt, getCerts[jwtKid]); 202 | return true; 203 | } catch (err) { 204 | console.log(err); 205 | return false; 206 | } 207 | }; 208 | 209 | /** 210 | * Send Google Chat Text Message 211 | * @param {string} spaceId - Google Chat Space ID 212 | * @param {string} message - Message 213 | * @returns {boolean} - Message sending status 214 | */ 215 | export const googleChatSendTextMessage = async ( 216 | chatClient: chat_v1.Chat, 217 | spaceId: string, 218 | message: string 219 | ) => { 220 | try { 221 | // Formulate Payload 222 | const payload: chat_v1.Params$Resource$Spaces$Messages$Create = { 223 | parent: spaceId, 224 | requestBody: { 225 | text: message, 226 | }, 227 | }; 228 | // Send Message 229 | const response = await chatClient.spaces.messages.create({ 230 | ...payload, 231 | }); 232 | console.log(googleChatSendTextMessage, response); 233 | return true; 234 | } catch (err) { 235 | console.log(err); 236 | throw err; 237 | } 238 | }; 239 | 240 | /** 241 | * Send Google Chat Media Message 242 | * @param {chat_v1.Chat} chatClient - Google Chat Authenticated Client 243 | * @param {string} spaceId - Google Chat Space ID 244 | * @param {string} fileName - File name 245 | * @param {string} contentUrl - Public URL of content 246 | * @returns {boolean} - Message sending status 247 | */ 248 | export const googleChatSendMediaMessage = async ( 249 | chatClient: chat_v1.Chat, 250 | spaceId: string, 251 | fileName: string, 252 | contentUrl: string 253 | ) => { 254 | try { 255 | // Formulate Payload 256 | const payload: chat_v1.Params$Resource$Spaces$Messages$Create = { 257 | parent: spaceId, 258 | requestBody: { 259 | cardsV2: [ 260 | { 261 | cardId: uuidv4(), 262 | card: { 263 | sections: [ 264 | { 265 | header: "Attachment", 266 | collapsible: true, 267 | uncollapsibleWidgetsCount: 1, 268 | widgets: [ 269 | { 270 | image: { 271 | imageUrl: contentUrl, 272 | altText: fileName, 273 | }, 274 | }, 275 | ], 276 | }, 277 | ], 278 | }, 279 | }, 280 | ], 281 | }, 282 | }; 283 | // Send Message 284 | const response = await chatClient.spaces.messages.create({ 285 | ...payload, 286 | }); 287 | console.log(googleChatSendTextMessage, response); 288 | return true; 289 | } catch (err) { 290 | console.log(err); 291 | throw err; 292 | } 293 | }; 294 | 295 | /** 296 | * Get Authenticated Google Chat Client 297 | * @param {GoogleChatTypes.GoogleChatContext} context - Google Context 298 | * @returns {chat_v1.Chat} - Message sending status 299 | */ 300 | export const getGoogleChatClient = async ( 301 | context: Context 302 | ) => { 303 | try { 304 | // Step 1: Get Google Service Account Credentials 305 | const googleChatCredentialsFileName = "/googlechat-credentials.json"; 306 | let credentials; 307 | if ( 308 | Object.keys(Runtime.getAssets()).length !== 0 && 309 | Runtime.getAssets()[googleChatCredentialsFileName] 310 | ) { 311 | // -- Priority 1: Use Private Asset File 312 | const rawCredentials = 313 | Runtime.getAssets()[googleChatCredentialsFileName].open; 314 | const rawCredentialsContent = rawCredentials(); 315 | credentials = JSON.parse(rawCredentialsContent); 316 | } else { 317 | // -- Priority 2: Use Environment Variable 318 | // -- Mainly for local development as environment variable in Twilio Functions cannot exceed 255 characters 319 | const rawCredentials = context.GOOGLECHAT_SERVICE_ACCOUNT_KEY_BASE64; 320 | credentials = JSON.parse( 321 | Buffer.from(rawCredentials, "base64").toString("utf-8") 322 | ); 323 | } 324 | 325 | // Step 2: Get Chat Client 326 | const auth = new google.auth.GoogleAuth({ 327 | credentials, 328 | scopes: ["https://www.googleapis.com/auth/chat.bot"], 329 | }); 330 | const chatClient = new chat_v1.Chat({ 331 | auth, 332 | }); 333 | return chatClient; 334 | } catch (err) { 335 | console.log(err); 336 | throw err; 337 | } 338 | }; 339 | 340 | /** 341 | * Get Media Attachment Content from Google Chat 342 | * @param {chat_v1.Chat} chatClient - Google Chat Authenticated Client 343 | * @param {string} resourceName - Resource Name from the attachmentDataRef attribute 344 | * @returns {any} - Raw Content 345 | */ 346 | const googleChatGetAttachmentContent = async ( 347 | chatClient: chat_v1.Chat, 348 | resourceName: string 349 | ) => { 350 | try { 351 | // Get Attachment Content 352 | const attachmentContent = await chatClient.media.download( 353 | { 354 | alt: "media", 355 | resourceName, 356 | }, 357 | { 358 | responseType: "arraybuffer", 359 | } 360 | ); 361 | return attachmentContent.data; 362 | } catch (err) { 363 | console.log(JSON.stringify(err)); 364 | throw err; 365 | } 366 | }; 367 | -------------------------------------------------------------------------------- /plugin-conversations-icons/src/components/SocialIcons.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const DiscordIcon = () => { 4 | return ( 5 | 6 | 7 | 8 | 12 | 16 | 17 | 18 | 19 | 20 | ); 21 | }; 22 | 23 | export const FacebookMessengerIcon = () => { 24 | return ( 25 | 26 | 34 | 35 | 36 | 37 | 38 | 39 | 43 | 47 | 48 | ); 49 | }; 50 | 51 | export const GoogleChatIcon = () => { 52 | return ( 53 | 58 | 62 | 66 | 70 | 71 | ); 72 | }; 73 | 74 | export const InstagramIcon = () => { 75 | return ( 76 | 84 | 90 | 91 | 92 | 93 | 101 | 102 | 103 | 104 | 105 | 106 | 110 | 118 | 119 | 120 | 121 | 122 | 123 | 127 | 128 | 129 | 130 | 131 | ); 132 | }; 133 | 134 | export const KakaoTalkIcon = () => { 135 | return ( 136 | 142 | 146 | 147 | 151 | 152 | ); 153 | }; 154 | 155 | export const LINEIcon = () => { 156 | return ( 157 | 163 | 164 | 191 | 192 | 193 | ); 194 | }; 195 | 196 | export const MicrosoftTeamsIcon = () => { 197 | return ( 198 | 199 | 203 | 204 | 205 | 209 | 213 | 217 | 221 | 225 | 229 | 233 | 237 | 241 | 250 | 251 | 252 | 253 | 254 | 258 | 262 | 263 | ); 264 | }; 265 | 266 | export const SlackIcon = () => { 267 | return ( 268 | 274 | 278 | 282 | 286 | 290 | 291 | ); 292 | }; 293 | 294 | export const TelegramIcon = () => { 295 | return ( 296 | 302 | 303 | 310 | 311 | 312 | 313 | 314 | Telegram_logo 315 | 316 | 320 | 324 | 328 | 329 | ); 330 | }; 331 | 332 | export const TikTokIcon = () => { 333 | return ( 334 | 340 | 341 | 342 | 343 | 344 | 345 | 349 | 353 | 357 | 358 | ); 359 | }; 360 | 361 | export const TwitterIcon = () => { 362 | return ( 363 | 368 | 372 | 373 | ); 374 | }; 375 | 376 | export const ViberIcon = () => { 377 | return ( 378 | 385 | 386 | 387 | 392 | 401 | 410 | 419 | 424 | 425 | 426 | 427 | ); 428 | }; 429 | 430 | export const WeChatIcon = () => { 431 | return ( 432 | 442 | 447 | 448 | 458 | 467 | 468 | 469 | ); 470 | }; 471 | 472 | export const ZaloIcon = () => { 473 | return ( 474 | 481 | 487 | 494 | 500 | 504 | 508 | 512 | 516 | 520 | 521 | ); 522 | }; 523 | --------------------------------------------------------------------------------