├── .env.example ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docs └── pt-br │ ├── README.md │ └── screenshots │ ├── functions-environment.png │ ├── functions-interface.png │ ├── modal.png │ ├── proxy-callback.png │ ├── sendFileButton.png │ └── thumbnail.png ├── jest.config.js ├── mms-handler ├── .env.example ├── functions │ ├── mms-handler.protected.js │ └── send-media-message.js ├── package-lock.json └── package.json ├── package-lock.json ├── package.json ├── public └── appConfig.example.js ├── screenshots ├── functions-environment.png ├── functions-interface.png ├── modal.png ├── proxy-callback.png ├── sendFileButton.png └── thumbnail.png ├── src ├── SmsMediaPlugin.js ├── components │ ├── BubbleMessageWrapper │ │ ├── BubbleMessageWrapper.Styles.js │ │ └── BubbleMessageWrapper.jsx │ ├── Button │ │ └── Button.jsx │ ├── DropMediaComponent │ │ ├── DropHereIcon.svg │ │ ├── DropMediaComponent.Style.js │ │ └── DropMediaComponent.jsx │ ├── ImageModal │ │ └── ImageModal.jsx │ ├── LoadingComponent │ │ ├── LoadingComponent.Styles.js │ │ └── LoadingComponent.jsx │ ├── MediaMessage │ │ ├── MediaMessage.Styles.js │ │ └── MediaMessage.jsx │ ├── PasteMediaComponent │ │ └── PasteMediaComponent.jsx │ ├── SendMediaComponent │ │ ├── SendMediaComponent.Styles.js │ │ └── SendMediaComponent.jsx │ └── __tests__ │ │ └── .gitkeep ├── index.js ├── services │ └── SendMediaService.js ├── setupTests.js └── states │ └── index.js ├── twilio-functions └── .gitignore ├── webpack.config.js └── webpack.dev.js /.env.example: -------------------------------------------------------------------------------- 1 | REACT_APP_SEND_MEDIA_ENDPOINT=https:/// 2 | -------------------------------------------------------------------------------- /.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 | .env.test 60 | .env.development 61 | .env.production 62 | 63 | # next.js build output 64 | .next 65 | 66 | # Flex related ignore 67 | appConfig.js 68 | pluginsService.js 69 | build/ -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at open-source@twilio.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Twilio 2 | 3 | All third party contributors acknowledge that any contributions they provide will be made under the same open source license that the open source project is provided under. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Twilio Inc. 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Twilio 3 | 4 | 5 | > **Note:** Detailed documentation is also available in Brazilian Portuguese. To read the instructions in that language, navigate to the `docs/pt-br` subfolder. 6 | 7 | # WhatsApp MMS for Flex WebChat 8 | 9 | The WhatsApp MMS for Flex Webchat plugin allows you to send a media message over WhatsApp and render it within a Flex chat window. Upon sending the message, any qualified agent will see an incoming chat request from a WhatsApp number following the `whatsapp:` format. 10 | 11 | ## Setup 12 | 13 | ### Requirements 14 | 15 | To deploy this plugin, you will need: 16 | - An active Twilio account with Flex provisioned. Refer to the [Flex Quickstart](https://www.twilio.com/docs/flex/quickstart/flex-basics#sign-up-for-or-sign-in-to-twilio-and-create-a-new-flex-project) to create one. 17 | - npm version 5.0.0 or 6 installed (type `npm -v` in your terminal to check) 18 | - Node version 10.12.0 or later installed (type `node -v` in your terminal to check) 19 | - A mobile device with WhatsApp installed 20 | - [Twilio CLI](https://www.twilio.com/docs/twilio-cli/quickstart#install-twilio-cli) along with the [Flex CLI Plugin](https://www.twilio.com/docs/twilio-cli/plugins#available-plugins) and the [Serverless Plugin](https://www.twilio.com/docs/twilio-cli/plugins#available-plugins). * Run the following commands to install them: 21 | ``` 22 | # Install the Twilio CLI 23 | npm install twilio-cli -g 24 | # Install the Serverless and Flex as Plugins 25 | twilio plugins:install @twilio-labs/plugin-serverless 26 | twilio plugins:install @twilio-labs/plugin-flex@beta 27 | ``` 28 | 29 | * The Twilio CLI with the Serverless and Flex Plugins are recommended for local development and debugging purposes, but you have the option to use the Functions UI in the Twilio Console. 30 | 31 | 32 | ### Twilio Account Settings 33 | 34 | Before we begin, we need to collect 35 | all the config values we need to run the plugin on your Flex application: 36 | 37 | | Config Value | Description | 38 | | :---------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | 39 | | Account Sid | Your primary Twilio account identifier - find this [on the Console Dashboard](https://www.twilio.com/console). | 40 | | Auth Token | Your Twilio Auth Token, which acts as your password - find this [on the Console Dashboard](https://www.twilio.com/console). | 41 | | Chat Service SID | Unique identifier for your Flex Chat Service. You can find this on the [Programmable Chat Dashboard](https://www.twilio.com/console/chat/dashboard). | 42 | | Twilio WhatsApp Number | The WhatsApp number to use when sending messages to a WhatsApp recipient. You can find this either on the [Twilio Sandbox for WhatsApp page](https://www.twilio.com/console/sms/whatsapp/learn) if you're using a test number, or the [WhatsApp Senders](https://www.twilio.com/console/sms/whatsapp/senders) if you've enabled personalized WhatsApp numbers. | 43 | | Twilio SMS Number | A Twilio phone number with MMS capability. You can check your numbers in the [Phone Numbers Dashboard](https://www.twilio.com/console/phone-numbers/incoming). | 44 | 45 | ## Plugin Details 46 | 47 | Twilio Proxy does not support media messages natively, so it is necessary to monitor Proxy messages to detect media messages and then update the Flex Chat Channel message attributes with the media URL and media type. To facilitate sending MMS via WhatsApp and rendering the media on the Flex Chat UI, we have added the following Twilio Functions: 48 | 49 | 1) **mms-handler.protected.js**: This Twilio Function will be called every time a Proxy interaction occurs. It uses the Proxy Callback URI to check for the existence of an MMS in an incoming message. 50 | 51 | 2) **send-media-message.js**: This Twilio Function is called by the Flex plugin to send the media to the recipient using the Programmable Messaging API. Since Proxy does not currently support media messages, we have to bypass it and call the Programmable Messaging API directly. 52 | 53 | ## Local Development 54 | 55 | After the above requirements have been met: 56 | 57 | 1. Clone this repository: `git clone https://github.com/twilio-labs/plugin-message-media.git` 58 | 1. Set your Functions [environment variables](#twilio-account-settings), install your package dependencies, and deploy your Twilio Functions. 59 | 1. Set your plugin environment variable with the value of your Twilio Functions domain. 60 | 1. Copy `public/appConfig.example.js` to `public/appConfig.js`. 61 | 1. Install plugin dependencies: `npm install` 62 | 63 | To test the plugin locally, run the following command: 64 | 65 | ```bash 66 | npm run start 67 | ``` 68 | Alternatively, you can use this command to start the server in development mode. It will reload whenever you change any files. 69 | 70 | ```bash 71 | npm run dev 72 | ``` 73 | 74 | > **Note:** We recommend you install the Twilio Flex Plugins CLI. To [start your plugin locally](https://www.twilio.com/docs/flex/quickstart/getting-started-plugin), you can run `twilio flex:plugins:start`. 75 | 76 | Navigate to [http://localhost:3000](http://localhost:3000). 77 | 78 | That's it! 79 | 80 | ## Functions Deployment 81 | 82 | You can upload these functions to your Twilio account in two ways: deploying via the Twilio CLI or copying and pasting the Functions code into the [Functions UI](https://www.twilio.com/console/functions/manage) within the Console. Make sure to set the environment variables and package dependencies regardless of which method you use. 83 | 84 | ### Serverless Toolkit 85 | 86 | #### Install the Twilio CLI and Serverless Plugins 87 | 88 | For installation instructions, see the [Requirements](#requirements) list. 89 | 90 | **Configure your Flex Workspace** 91 | 92 | In order to use this plugin, you need to prepare your **Flex Task Assignment** workspace. 93 | 94 | **Retrieve your Flex settings** 95 | 96 | Navigate to your Flex project in the [Twilio Console](https://www.twilio.com/console). Copy your **ACCOUNT SID** and **AUTH TOKEN**, and create a new Twilio CLI profile using those credentials and activate it. 97 | 98 | ``` 99 | twilio profiles:create 100 | You can find your Account SID and Auth Token at https://www.twilio.com/console 101 | » Your Auth Token will be used once to create an API Key for future CLI access to your Twilio Account or Subaccount, and then forgotten. 102 | ? The Account SID for your Twilio Account or Subaccount: ACxxx 103 | ? Your Twilio Auth Token for your Twilio Account or Subaccount: [hidden] 104 | 105 | ? Shorthand identifier for your profile: whatsappmmsforflex 106 | Created API Key SKxxx and stored the secret in your keychain. 107 | Saved whatsappmmsforflex. 108 | 109 | twilio profiles:use whatsappmmsforflex 110 | ``` 111 | 112 | Keep in mind that this account will be used for the rest of the deployment. In order to switch accounts, use the command `twilio profiles:use `. 113 | 114 | Retrieve your **Flex Task Assignment** workspace ID: 115 | ``` 116 | twilio api:taskrouter:v1:workspaces:list 117 | ``` 118 | 119 | **Example Workspace SID** 120 | 121 | ``` 122 | SID Friendly Name Prioritize Queue Order 123 | WSxxxx Flex Task Assignment FIFO 124 | ``` 125 | 126 | Inside the folder `mms-handler` of this repository, copy the `.env.example` and create a `.env` file. If you're running Windows, see the Windows Environment Variables section in [this blog post](https://www.twilio.com/blog/2017/01/how-to-set-environment-variables.html). For more details on the required values, see [Twilio Account Settings](twilio-account-settings). 127 | 128 | ``` 129 | ACCOUNT_SID=AC0000000000000000000000000000000000 130 | AUTH_TOKEN=0000000000000000000000000000000000000 131 | CHAT_SERVICE_SID=IS00000000000000000000000000000 132 | PROXY_SERVICE=KS00000000000000000000000000000000 133 | TWILIO_WHATSAPP_NUMBER=+14155238886 134 | TWILIO_SMS_NUMBER=+19999999999 135 | ``` 136 | 137 | After providing all these data, you can deploy these functions to your account: 138 | 139 | ```zsh 140 | $ twilio serverless:deploy 141 | 142 | # Output: 143 | 144 | Deployment Details 145 | Domain: your-domain-dev.twil.io 146 | Service: 147 | mms-media (ZS0000000000000000000000000000) 148 | Environment: 149 | dev (ZE0000000000000000000000000000) 150 | Build SID: 151 | ZB0000000000000000000000000000 152 | View Live Logs: 153 | https://www.twilio.com/console/assets/api/ZS0000000000000000000000000000/environment/ZE0000000000000000000000000000 154 | Functions: 155 | [protected] https://your-domain-dev.twil.io/mms-handler 156 | https://your-domain-dev.twil.io/send-media-message 157 | Assets: 158 | ``` 159 | 160 | When the deployment finishes, copy and save the Functions' and the Domain's URLs. You will need it in the next step. 161 | 162 | If you forget to copy the domain, you can also find it by navigating to [Functions > API](https://www.twilio.com/console/functions/api) in the Twilio Console. 163 | 164 | > Debugging Tip: Pass the -l or logging flag to review deployment logs. For example, you can pass `-l debug` to turn on debugging logs. 165 | 166 | #### Functions UI Deployment 167 | 168 | Copying the code of the Functions can be the fastest way to get started. In the [Functions Dashboard](https://www.twilio.com/console/functions/manage), you have to create two new functions: One named `MMS Handler` and another named `Send Media Message`. Define the path of these two functions as `/mms-handler` and `/send-media-message` respectively. Copy the code of each function that exists inside the directory `mms-handler/functions` and paste it into a Blank Function template in the UI. 169 | 170 | ![functions-interface](screenshots/functions-interface.png) 171 | 172 | Only the Function `mms-handler` should check for Twilio's Signature because it will be called directly by the Proxy service. The `send-media-message` Function needs to be called on the client-side by the Flex instance, so it will verify the Flex token that is passed on the request instead. 173 | 174 | Save your changes to both functions. Copy and paste the full paths of both functions into a text file since you will need them later. 175 | 176 | After you have added your Functions, go to the [Functions Configuration Page](https://www.twilio.com/console/functions/configure) and ensure "Enable ACCOUNT_SID and AUTH_TOKEN" is checked. In the section "Environment Variables", add the variables described in [Twilio Account Settings](twilio-account-settings). 177 | 178 | Also add the following npm Function dependencies: 179 | 180 | * twilio-flex-token-validator (1.5.3) -> Validates the Flex Token that will be sent in each request to the `Send Message Function` by the Flex UI. 181 | * verror (1.10.0) -> Used in the `MMS handler` function to add more details to the errors if something goes wrong. 182 | 183 | ![functions-environment](screenshots/functions-environment.png) 184 | 185 | > When adding the `twilio-flex-token-validator` dependency, make sure to copy and paste it from a clipboard or a text file. There is a bug in the interface that blocks the input when typing 'twilio' in the field and you will be unable to complete the dependency name. 186 | 187 | Save your changes. You are now ready to configure the Proxy Service and the Flex Plugin. 188 | 189 | ## Twilio Proxy Configuration 190 | 191 | We now need to configure the Proxy Callback URL to point to that Function. 192 | 193 | 1. Navigate to the [Proxy Dashboard](https://www.twilio.com/console/proxy) and click on the Proxy Service used by your MMS and WhatsApp numbers. 194 | 195 | 1. In the Callback URL field, enter the `mms-handler` Function URL. If the deploy was made using the Serverless Toolkit, paste the `mms-handler` URL provided in the deploy command output. Otherwise, you can get that URL by navigating to the [Twilio Functions page](https://www.twilio.com/console/functions/manage), selecting the `MMS Handler` Function, and clicking the Copy button next to the Path. 196 | ![image thumbnail](screenshots/proxy-callback.png) 197 | 198 | 1. Click **Save** at the bottom of the Proxy Configuration page after entering the Callback URL. 199 | 200 | ## Flex Plugin Deployment 201 | 202 | The WhatsApp MMS for Flex WebChat plugin allows customers to render MMS sent over WhatsApp in the Flex WebChat and allows the agent to send media files to customers from their computer. You can see a demo below of this plugin: 203 | 204 | ![image thumbnail](screenshots/thumbnail.png) 205 | 206 | ![image modal](screenshots/modal.png) 207 | 208 | ![send media buttons](screenshots/sendFileButton.png) 209 | 210 | From the plugin root directory, run the following commands: 211 | 212 | 1. Rename `public/appConfig.example.js` to `public/appConfig.js`. 213 | 1. Install the plugin dependencies. 214 | 215 | ```zsh 216 | $ npm i 217 | ``` 218 | 219 | After that, create `.env.production` based on `.env.example`, and modify the `REACT_APP_SEND_MEDIA_ENDPOINT` property to the URL of the send-media-message.js function. 220 | 221 | ```javascript 222 | REACT_APP_SEND_MEDIA_ENDPOINT=https://your-functions-domain/send-media-message.js 223 | ``` 224 | 225 | Check if you are logged in to the [Twilio CLI](https://github.com/twilio-labs/plugin-message-media#twilio-cli-setup) 226 | 227 | At last, run the `twilio flex:plugins:deploy` command. For more details about this command, read the [Twilio Flex plugin CLI documentation](https://www.twilio.com/docs/flex/developer/plugins/cli). 228 | 229 | ```zsh 230 | twilio flex:plugins:deploy --major --changelog "Notes for this version" --description "Functionality of the plugin" 231 | ``` 232 | 233 | Upon successfully deploying your plugin, you should see a suggested next step for enabling it on your Flex application. After running the suggested next step, navigate to the [Plugins Dashboard](https://flex.twilio.com/admin/?_ga=2.33188926.1245483994.1613494659-1672783450.1612487462) to review your recently deployed plugin and confirm that it’s enabled for your contact center. 234 | 235 | Now you can open your [Flex instance](https://flex.twilio.com/admin) and test if everything is working as expected! 236 | 237 | ## Contributing 238 | 239 | Check out [CONTRIBUTING](CONTRIBUTING.md) for more information on how to contribute to this project. 240 | 241 | ## License 242 | 243 | MIT © Twilio Inc. 244 | 245 | 246 | ## Credits 247 | 248 | This plugin was based on the [mms2FlexChat plugin](https://github.com/jprix/mms2FlexChat). Many thanks to the original contributors: 249 | * [jprix](https://github.com/jprix) 250 | * [Terence Rogers](https://github.com/trogers-twilio) 251 | * [Brad McAllister](https://github.com/bdm1981) 252 | 253 | ## License 254 | 255 | [MIT](http://www.opensource.org/licenses/mit-license.html) 256 | 257 | ## Disclaimer 258 | 259 | No warranty expressed or implied. Software is as is. 260 | 261 | [twilio]: https://www.twilio.com 262 | -------------------------------------------------------------------------------- /docs/pt-br/README.md: -------------------------------------------------------------------------------- 1 | # mms2FlexChat 2 | 3 | Permite que usuários enviem e recebia mídias MMS por SMS/Whatsapp e renderiza dentro da janela de chat do Flex. 4 | 5 | # Configuração 6 | 7 | ## Twilio Functions 8 | 9 | Para permitir o envio de mídia MMS para o chat do flex, duas Twilio Functions são necessárias. Elas podem ser encontradas dentro da pasta `twilio-functions/functions` desse repositório. Abaixo, você pode ver uma breve descrição sobre o que cada uma delas faz: 10 | 11 | 1) mms-handler.protected.js: Essa Function vai ser chamada toda vez que ocorrer uma interação com o Twilio Proxy, usando o URI do Proxy Callback. Ela vai checar se um MMS existe na mensagem de SMS/Whatsapp recebida. 12 | 13 | 2) send-media-message.js: Essa Function vai ser chamada pelo plugin do Flex para enviar a mídia para o cliente usando a API de mensagens da Twilio. O Proxy não suporta mensagens com mídia, então temos que chamar a API de mensagens diretamente por essa função para fazer um caminho alternativo. 14 | 15 | Você pode realizar o upload dessas funções em sua conta da Twilio por dos métodos: realizando o upload via o [Serverless Toolkit](https://www.twilio.com/docs/labs/serverless-toolkit) ou copiando e colando o código das funções na [interface de Functions](https://www.twilio.com/console/functions/manage) no Console da Twilio. 16 | 17 | Em qualquer uma das opções que você escolher, as seguintes variáveis de ambiente devem ser definidas: 18 | 19 | * **PROXY_SERVICE** 20 | * SID do Proxy Service em que as mensagens de MMS e de WhatsApp são direcionadas 21 | * Você pode encontrar essa informação no [Dashboard do Proxy](https://www.twilio.com/console/proxy). Por padrão, o Proxy usado pelo Flex tem o nome de "Flex Proxy Service". 22 | * **CHAT_SERVICE_SID** 23 | * SID do serviço de Programmable Chat usado em seus Flows do Flex 24 | * Você pode encontrar essa informação no [Dashboard do Programmable Chat](https://www.twilio.com/console/chat/dashboard). Por padrão, o Service usado pelo Flex tem o nome de "Flex Chat Service". 25 | * **TWILIO_WHATSAPP_NUMBER** 26 | * O número do WhatsApp que será usado para enviar mensagens à um destinatário do WhatsApp. 27 | * Você pode encontrar essa informação na página do [WhatsApp Learn](https://www.twilio.com/console/sms/whatsapp/learn) se você estiver usando um número de testes ou na página do [WhatsApp Senders](https://www.twilio.com/console/sms/whatsapp/senders) se você já habilitou o uso de números personalizados. O número deve seguir o formato `+1122333334444` (com o código do país e o DDD seguido pelo número) 28 | * **TWILIO_SMS_NUMBER** 29 | * O número que será usado para enviar mensagens à um destinatário por MMS. 30 | * Você pode usar o mesmo número do WhatsApp caso você já tenha habilitado um número personalizado com capacidade de enviar SMS. Caso o contrário, você pode usar algum outro número da Twilio para realizar o envio de mídias por SMS. Você pode gerenciar seus números no [Dashboard de números ativos da Twilio](https://www.twilio.com/console/phone-numbers/incoming). O número deve seguir o formato `+1122333334444` (com o código do país e o DDD seguido pelo número) 31 | 32 | ## Opção 1: Copiando o código para a interface do Twilio Functions 33 | 34 | Copiar o código das funções diretamente para a interface do console da Twilio é a forma mais rápida de começar. Porém, caso você queira debugar o código com mais detalhes e testar alterações em sua máquina local, dê uma olhada em [como usar o serverless toolkit](#Fazendo-o-deploy-das-functions-usando-o-serverless-toolkit). 35 | 36 | [No dashboard do Twilio Functions](https://www.twilio.com/console/functions/manage), crie duas novas funções: uma chamada `MMS Handler` e outra com o nome de `Send Media Message`. Defina o path delas como `/mms-handler` e `/send-media-message`, respectivamente. Copie o código e cada função dentro do diretório `twilio-functions/functions` e cole em suas respectivas Functions na interface. 37 | 38 | ![functions-interface](screenshots/functions-interface.png) 39 | 40 | Somente a função `mms-handler` deve checar a assinatura da Twilio. Isso porque ela vai ser chamada diretamente de dentro da Twilio pelo serviço do Proxy. Como a função `send-media-message` precisa ser chamada no lado do cliente pelo Flex, não podemos deixar essa opção habilitada no caso dela. 41 | 42 | Salve as alterações nas duas funções. Recomendo que você já salve o Path delas em algum arquivo de texto, já que você vai precisar dessa informação mais tarde. 43 | 44 | Agora, na [página de configuração das funções](https://www.twilio.com/console/functions/configure), certifique-se de que o checkbox "Enable ACCOUNT_SID and AUTH_TOKEN" está marcado. Na seção "Environment Variables", adicione as variáveis de ambientes descritas anteriormente. 45 | 46 | Você também vai precisar adicionar as seguintes dependências para as funções funcionarem corretamente: 47 | 48 | * twilio-flex-token-validator (1.5.3) -> Valida o token do Flex que será enviado no client-side do Flex UI 49 | * verror (1.10.0) -> usado na função de MMS handler para adicionar mais detalhes aos erros caso alguma coisa dê errado. 50 | 51 | ![functions-environment](screenshots/functions-environment.png) 52 | 53 | > Copie e cole o nome da dependência `twilio-flex-token-validator` quando você for defini-la. Existe um bug na interface que, se você digitar 'twilio' no nome da dependência, o campo vai travar e você não vai conseguir digitar mais nada nele. 54 | 55 | Salve as modificações. Depois disso, você está pronto para configurar o Proxy e o Plugin do Flex. 56 | 57 | ## Opção 2: Fazendo o Deploy das Functions usando o Serverless Toolkit 58 | 59 | ### Configurando o Twilio CLI 60 | 61 | Primeiramente, realize o clone desse repositório em sua máquina. Em seguida, certifique-se de que o [twilio-cli](https://www.twilio.com/docs/twilio-cli/quickstart) está instalado em sua máquina. Você pode fazer isso simplesmente digitando `twilio` em seu terminal: 62 | 63 | ```zsh 64 | $ twilio 65 | 66 | # output: 67 | 68 | unleash the power of Twilio from your command prompt 69 | 70 | VERSION 71 | twilio-cli/2.3.0 darwin-x64 node-v12.14.1 72 | 73 | USAGE 74 | $ twilio [COMMAND] 75 | [...] 76 | ``` 77 | Caso ele ainda não esteja instalado em sua máquina, siga o [quickstart](https://www.twilio.com/docs/twilio-cli/quickstart) para instalá-lo. 78 | 79 | Se o CLI já estava instalado em sua máquina, rode o comando `twilio profiles:list` e confira se a conta ativa no momento é a sua conta do Flex que você deseja usar para enviar/receber mídia: 80 | 81 | ```zsh 82 | $ twilio profiles:list 83 | 84 | ID Account SID Active 85 | flex-whatsapp AC00000000000000000000000000000000 true 86 | other-account AC00000000000000000000000000000000 87 | not-this-one AC00000000000000000000000000000000 88 | ``` 89 | 90 | Se sua conta do Flex já estiver na listagem mas não for a conta ativa no momento, você pode alternar para ela usando o comando `twilio profiles:use `: 91 | 92 | ```zsh 93 | $ twilio profiles:use flex-whatsapp 94 | set "flex-whatsapp" as active profile 95 | ``` 96 | 97 | Caso seja a primeira vez que você esteja usando o `twilio-cli` ou se sua conta do Flex não apareceu na listagem de profiles, então você deve executar o comando `twilio login` e informar seu **ACCOUNT_SID** e seu **AUTH_TOKEN** para se autenticar em sua conta do flex. 98 | 99 | ### Instalando o plugin do serverless e realizando o Deploy das functions 100 | 101 | Se o `twilio-cli` já estava instalado em sua máquina, confira se o plugin do serverless também já está instalado: 102 | 103 | ```zsh 104 | $ twilio plugins 105 | @twilio-labs/plugin-rtc 0.1.6 106 | @twilio-labs/plugin-serverless 1.1.1 # Serverless Plugin 107 | ``` 108 | 109 | Caso ele não tenha aparecido na listagem ou você tenha acabado de instalar o `twilio-cli`, rode o seguinte comando para instalar o plugin do serverless: 110 | 111 | ```zsh 112 | $ twilio plugins:install @twilio-labs/plugin-serverless 113 | ``` 114 | 115 | Dentro da pasta `twilio-functions`, copie o arquivo `.env.example` e crie um arquivo `.env` propriamente dito. Preencha ele com as informações definidas no arquivo de exemplo. Cheque a descrição das variáveis de ambientes mais acima caso você precise de mais detalhes sobre elas. 116 | 117 | ``` 118 | ACCOUNT_SID=AC0000000000000000000000000000000000 119 | AUTH_TOKEN=0000000000000000000000000000000000000 120 | CHAT_SERVICE_SID=IS00000000000000000000000000000 121 | PROXY_SERVICE=KS00000000000000000000000000000000 122 | TWILIO_WHATSAPP_NUMBER=+14155238886 123 | TWILIO_SMS_NUMBER=+19999999999 124 | ``` 125 | 126 | Depois de preencher todos esses dados, basta realizar o deploy das funções em sua conta: 127 | 128 | ```zsh 129 | $ twilio serverless:deploy 130 | 131 | # Output: 132 | 133 | Deployment Details 134 | Domain: your-domain-dev.twil.io 135 | Service: 136 | mms-media (ZS0000000000000000000000000000) 137 | Environment: 138 | dev (ZE0000000000000000000000000000) 139 | Build SID: 140 | ZB0000000000000000000000000000 141 | View Live Logs: 142 | https://www.twilio.com/console/assets/api/ZS0000000000000000000000000000/environment/ZE0000000000000000000000000000 143 | Functions: 144 | [protected] https://your-domain-dev.twil.io/mms-handler 145 | https://your-domain-dev.twil.io/send-media-message 146 | Assets: 147 | ``` 148 | 149 | Depois que o deploy for realizado com sucesso, copie e salve o link do seu domínio e a URL de suas Functions no resultado do comando acima. 150 | 151 | ## Configurando o Twilio Proxy 152 | 153 | Como o Twilio Proxy não suporta mensagens com mídia nativamente, é necessário monitorar essas mensagens para detectar essas mensagens com mídia e só então atualizar os atributos da mensagem com o URL da mídia e seu tipo. Isso é responsabilidade da função `mms-handler`, que o deploy foi realizado anteriormente. 154 | 155 | Agora, você precisa configurar o URL do Proxy Callback para o endpoint dessa função. 156 | 157 | 1) Navegue para o [Dashboard do Proxy](https://www.twilio.com/console/proxy) e clique no Proxy Service usado pelo seu número do WhatsApp e do MMS. 158 | 159 | 2) No campo `Callback URL`, coloque a URL da função `mms-handler`. Caso você tenha feito o deploy via `twilio-cli`, você terá acesso a essa URL depois que o deploy for feito com sucesso. Se você tiver copiado e colado o código da função usando a página de Functions no Twilio Console, você pode obter a URL acessando [o dashboard das Functions](https://www.twilio.com/console/functions/manage), selecionando a função do `mms-handler` e clicando no botão de cópia perto do campo `Path`. 160 | 161 | ![proxy-callback](screenshots/proxy-callback.png) 162 | 163 | 3) Clique no botão `Save` no final da página de configuração depois que você preencheu o `Callback URL`. 164 | 165 | ## Subindo o Plugin do Flex 166 | 167 | Na pasta `flex-plugin` na raiz desse repositório está um plugin do Flex que deve ser implementado para possibilitar que a mídia recebida seja exibida exibida corretamente. Abaixo, você pode ver algumas imagens demonstrando o funcionamento dele: 168 | 169 | ![image thumbnail](screenshots/thumbnail.png) 170 | 171 | ![image modal](screenshots/modal.png) 172 | 173 | ![send media buttons](screenshots/sendFileButton.png) 174 | 175 | Realizar o deploy desse plugin é bem simples: entre no diretório `flex-plugin` e instale as dependências: 176 | 177 | ```zsh 178 | $ npm i 179 | ``` 180 | 181 | Depois disso, crie um arquivo `.env.production` com base no `.env.example`, e preencha o único parâmetro com o domínio das funções: 182 | 183 | ```javascript 184 | REACT_APP_MMS_FUNCTIONS_DOMAIN=https://your_functions_domain 185 | ``` 186 | 187 | Após você ter feito isso, copie o arquivo `appConfig.example.js` dentro da pasta `flex-plugin/public` e crie um arquivo chamado `appConfig.js`. Defina o valor da variável `accountSid` como o **ACCOUNT_SID**: 188 | 189 | ```javascript 190 | // your account sid 191 | var accountSid = 'AC000000000000000000000000000000000'; 192 | 193 | // set to /plugins.json for local dev 194 | // set to /plugins.local.build.json for testing your build 195 | // set to "" for the default live plugin loader 196 | var pluginServiceUrl = '/plugins.json'; 197 | 198 | var appConfig = { 199 | pluginService: { 200 | enabled: true, 201 | url: pluginServiceUrl, 202 | }, 203 | sso: { 204 | accountSid: accountSid 205 | }, 206 | ytica: false, 207 | logLevel: 'debug', 208 | showSupervisorDesktopView: true, 209 | }; 210 | ``` 211 | 212 | Por último, rode o comando `npm run deploy`. Recomendo que ao rodar o comando pela primeira vez você defina o accountSid e o authToken da sua conta do flex da seguinte maneira: 213 | 214 | ``` 215 | TWILIO_ACCOUNT_SID=AC0000000 TWILIO_AUTH_TOKEN=00000000000 npm run deploy 216 | ``` 217 | 218 | Dessa forma, caso você já tenha realizado um deploy em alguma outra conta, você tem certeza de que o deploy desse plugin em específico vai subir na conta com o SID especificado. Você não precisa especificar esses parâmetros nos próximos deploys, já que essa conta será usada por padrão ou se você tiver mais de uma conta registrada você terá que selecionar em qual delas você quer realizar o deploy antes de fazer o processo. 219 | 220 | Agora é só você abrir o seu painel de [administrador do Flex](https://flex.twilio.com/admin) e testar se está tudo funcionando! 221 | 222 | ## Debuggando o código no ambiente local 223 | 224 | ### Rodando as Twilio Functions localmente usando o serverless toolkit 225 | 226 | Caso você tenha optado pela segunda opção para realizar o deploy das funções, você pode rodá-las em um ambiente local para testar as modificações ou debugar o que está acontecendo por trás dos panos. 227 | 228 | Dentro do diretório `twilio-functions`, instale as dependências do projeto: 229 | 230 | ```zsh 231 | $ npm i 232 | ``` 233 | 234 | Depois disso, use o Serverless Toolkit para inicializar suas funções: 235 | 236 | ```zsh 237 | $ twilio serverless:start 238 | 239 | ┌────────────────────────────────────────────────────────────────────────┐ 240 | │ │ 241 | │ Twilio functions available: │ 242 | │ ├── [protected] /mms-handler | http://localhost:3000/mms-handler │ 243 | │ └── /send-media-message | http://localhost:3000/send-media-message │ 244 | │ │ 245 | │ Twilio assets available: │ 246 | │ ⚠ No assets found │ 247 | │ │ 248 | └────────────────────────────────────────────────────────────────────────┘ 249 | 250 | ``` 251 | 252 | Para usar o `mms-handler` local no Callback URL do Proxy, você terá que usar o [ngrok](https://ngrok.com/) ou alguma ferramenta do tipo. O ngrok vai gerar uma URL que aponta para a sua máquina na porta definida ao inicializá-lo. Instale o ngrok em algum diretório da sua máquina e o execute passando os seguintes argumentos: 253 | 254 | ```zsh 255 | $ ./ngrok http 3000 256 | 257 | ngrok by @inconshreveable (Ctrl+C to quit) 258 | 259 | Session Status online 260 | Account Your account 261 | Version 2.3.35 262 | Region United States (us) 263 | Web Interface http://127.0.0.1:3041 264 | Forwarding http://your_url.ngrok.io -> http://localhost:3000 265 | Forwarding https://your_url.ngrok.io -> http://localhost:3000 266 | ``` 267 | 268 | Assim, uma URL será gerada para rotear conexões http para o seu localhost na porta 3000. Copie a última URL HTTPS e defina o endpoint do `mms-handler` no formato `https://your_url.ngrok.io/mms-handler` como o callback URL no [Proxy do Flex](https://www.twilio.com/console/proxy), assim como foi feito no passo de [configurar o Twilio Proxy](#Configurando-o-twilio-proxy). 269 | 270 | ![proxy-callback](screenshots/proxy-callback.png) 271 | 272 | No plugin do Flex, você pode definir para chamar a função `send-media-message` no localhost modificando o arquivo `flex-plugin/.env`. As instruções para rodar o plugin em sua máquina estão descritas abaixo. 273 | 274 | ### Rodando o Plugin do Flex em sua própria máquina 275 | 276 | Para testar rapidamente esse plugin rodando uma instância do Flex local, primeiro você deve realizar login no [Console da Twilio](https://www.twilio.com/login). Depois, dentro da pasta `flex-plugin`, instale as dependências do projeto: 277 | 278 | ```zsh 279 | $ npm i 280 | ``` 281 | 282 | Crie um arquivo `.env`, diferente do de produção, com base no `.env.example`, e preencha o único parâmetro com o domínio das funções. Caso você também esteja rodando as funções em seu ambiente local, especificar o domínio como localhost ou a URL do ngrok: 283 | 284 | ```javascript 285 | REACT_APP_MMS_FUNCTIONS_DOMAIN=https://your_functions_domain 286 | ``` 287 | 288 | > É recomendado que você tenha o arquivo `.env` para realizar testes locais e o `.env.production` para ser usado na hora do deploy. Quando você rodar o comando `npm run deploy`, o arquivo env de produção vai ser usado no lugar do `.env` padrão. 289 | 290 | Após você ter feito isso, copie o arquivo `appConfig.example.js` dentro da pasta `flex-plugin/public` e crie um arquivo chamado `appConfig.js`. Defina o valor da variável `accountSid` como o **ACCOUNT_SID**: 291 | 292 | ```javascript 293 | // your account sid 294 | var accountSid = 'AC000000000000000000000000000000000'; 295 | 296 | // set to /plugins.json for local dev 297 | // set to /plugins.local.build.json for testing your build 298 | // set to "" for the default live plugin loader 299 | var pluginServiceUrl = '/plugins.json'; 300 | 301 | var appConfig = { 302 | pluginService: { 303 | enabled: true, 304 | url: pluginServiceUrl, 305 | }, 306 | sso: { 307 | accountSid: accountSid 308 | }, 309 | ytica: false, 310 | logLevel: 'debug', 311 | showSupervisorDesktopView: true, 312 | }; 313 | ``` 314 | 315 | Finalmente, rode o comando `PORT=8080 npm start`. Isso deve iniciar um servidor na porta 8080 com sua instância local do plugin e abrir o seu browser padrão conectado ao flex. 316 | 317 | > A porta 8080 foi especificada para evitar conflito com as Functions se as mesmas estiverem rodando em um servidor local. 318 | 319 | ``` 320 | $ npm start 321 | 322 | Compiled successfully! 323 | 324 | You can now view plugin-sms-media in the browser. 325 | 326 | Local: http://localhost:8080/ 327 | On Your Network: http://192.168.0.2:8080/ 328 | 329 | Note that the development build is not optimized. 330 | To create a production build, use npm run build. 331 | ``` 332 | 333 | Pronto! Agora, você pode testar suas modificações localmente. 334 | -------------------------------------------------------------------------------- /docs/pt-br/screenshots/functions-environment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/plugin-message-media/b26f6ebe4436adab2564eaf8559c3cf088317852/docs/pt-br/screenshots/functions-environment.png -------------------------------------------------------------------------------- /docs/pt-br/screenshots/functions-interface.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/plugin-message-media/b26f6ebe4436adab2564eaf8559c3cf088317852/docs/pt-br/screenshots/functions-interface.png -------------------------------------------------------------------------------- /docs/pt-br/screenshots/modal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/plugin-message-media/b26f6ebe4436adab2564eaf8559c3cf088317852/docs/pt-br/screenshots/modal.png -------------------------------------------------------------------------------- /docs/pt-br/screenshots/proxy-callback.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/plugin-message-media/b26f6ebe4436adab2564eaf8559c3cf088317852/docs/pt-br/screenshots/proxy-callback.png -------------------------------------------------------------------------------- /docs/pt-br/screenshots/sendFileButton.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/plugin-message-media/b26f6ebe4436adab2564eaf8559c3cf088317852/docs/pt-br/screenshots/sendFileButton.png -------------------------------------------------------------------------------- /docs/pt-br/screenshots/thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/plugin-message-media/b26f6ebe4436adab2564eaf8559c3cf088317852/docs/pt-br/screenshots/thumbnail.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /mms-handler/.env.example: -------------------------------------------------------------------------------- 1 | ACCOUNT_SID= 2 | AUTH_TOKEN= 3 | CHAT_SERVICE_SID= 4 | PROXY_SERVICE= 5 | TWILIO_WHATSAPP_NUMBER= 6 | TWILIO_SMS_NUMBER= -------------------------------------------------------------------------------- /mms-handler/functions/mms-handler.protected.js: -------------------------------------------------------------------------------- 1 | const VError = require('verror'); 2 | const waitFor = (delay) => new Promise((resolve) => setTimeout(resolve, delay)); 3 | 4 | const getApiUtils = (context) => { 5 | const client = context.getTwilioClient(); 6 | 7 | return { 8 | fetchSubresourcesList: async (messageSid, retry = 3) => { 9 | for (let i = 0; i <= retry; i++) { 10 | console.log('MESSAGE SID =>', messageSid); 11 | const mediaMessage = await client.messages(messageSid).media.list({ limit: 1 }); 12 | if (mediaMessage.length > 0) { 13 | return mediaMessage; 14 | } else { 15 | console.log("Message is a MMS, but it doesn't have any media attached.", ` retry = ${i}`); 16 | } 17 | await waitFor(1000); 18 | } 19 | }, 20 | fetchSession: async (sessionSid) => { 21 | try { 22 | return await client.proxy 23 | .services(context.PROXY_SERVICE) 24 | .sessions(sessionSid) 25 | .fetch(); 26 | } catch (err) { 27 | throw new VError(err, "error while fecthing Proxy's session details"); 28 | } 29 | }, 30 | updateChatMessageMedia: async (channelSid, messageSid, data) => { 31 | try { 32 | return await client.chat 33 | .services(context.CHAT_SERVICE_SID) 34 | .channels(channelSid) 35 | .messages(messageSid) 36 | .update(data); 37 | } catch (err) { 38 | throw new VError(err, "error while updating Chat's message with incoming media"); 39 | } 40 | } 41 | } 42 | } 43 | 44 | const updateFlexChatMessageWithMMSMedia = async (context, event) => { 45 | const { fetchSession, fetchSubresourcesList, updateChatMessageMedia } = getApiUtils(context); 46 | 47 | const sessionSid = event.interactionSessionSid; 48 | const messageSid = event.outboundResourceSid; 49 | 50 | const subResourcesList = await fetchSubresourcesList(event.inboundResourceSid); 51 | 52 | if (subResourcesList.length) { 53 | const [subResource] = subResourcesList; 54 | const mediaType = subResource.contentType; 55 | const mediaURL = 56 | 'https://api.twilio.com' + subResource.uri.replace('.json', ''); 57 | 58 | const session = await fetchSession(sessionSid); 59 | const channelSid = session.uniqueName.split('.')[0]; 60 | 61 | const updatedChatMessage = await updateChatMessageMedia(channelSid, messageSid, { 62 | attributes: JSON.stringify({ 63 | mediaType, 64 | media: mediaURL, 65 | }) 66 | }); 67 | 68 | console.log(updatedChatMessage); 69 | } else { 70 | console.log('Message is a MMS, but it doesn\'t have any media attached.') 71 | } 72 | } 73 | 74 | exports.handler = async function(context, event, callback) { 75 | if (event.inboundResourceSid.includes('MM')) { 76 | try { 77 | await updateFlexChatMessageWithMMSMedia(context, event); 78 | callback(null); 79 | } catch (err) { 80 | console.error('An unexpected error occurred: ', err); 81 | callback(err); 82 | } 83 | } else { 84 | console.log('not MMS'); 85 | callback(null); 86 | } 87 | }; 88 | -------------------------------------------------------------------------------- /mms-handler/functions/send-media-message.js: -------------------------------------------------------------------------------- 1 | const TokenValidator = require('twilio-flex-token-validator').functionValidator; 2 | 3 | exports.handler = TokenValidator(async function (context, event, callback) { 4 | const response = new Twilio.Response(); 5 | response.appendHeader('Access-Control-Allow-Origin', '*'); 6 | response.appendHeader('Access-Control-Allow-Methods', 'OPTIONS, POST, GET'); 7 | response.appendHeader('Access-Control-Allow-Headers', 'Content-Type'); 8 | response.appendHeader('Content-Type', 'application/json'); 9 | 10 | const { to, mediaUrl } = event; 11 | const channel = event.channel && event.channel.toLowerCase(); 12 | const client = context.getTwilioClient(); 13 | 14 | const channelsFrom = { 15 | 'chat-whatsapp': `whatsapp:${context.TWILIO_WHATSAPP_NUMBER}`, 16 | 'chat-sms': context.TWILIO_SMS_NUMBER, 17 | }; 18 | 19 | const from = channelsFrom[channel]; 20 | 21 | if (!from) { 22 | console.warn('invalid channel: ', channel); 23 | response.setStatusCode(400); 24 | response.setBody(JSON.stringify({ success: false, msg: 'invalid channel' })); 25 | return callback(null, response); 26 | } 27 | 28 | try { 29 | const result = await client.messages.create({ 30 | from, 31 | to, 32 | mediaUrl, 33 | }); 34 | console.log('message create result:', result); 35 | response.setBody(JSON.stringify({ success: true })); 36 | callback(null, response); 37 | } catch (error) { 38 | console.error('error creating message:', error); 39 | response.setBody(JSON.stringify({ success: false, error })); 40 | callback(response, null); 41 | } 42 | }); 43 | -------------------------------------------------------------------------------- /mms-handler/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "message-media", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1", 7 | "start": "twilio-run --env" 8 | }, 9 | "dependencies": { 10 | "twilio": "3.29.2", 11 | "twilio-flex-token-validator": "^1.5.3", 12 | "verror": "^1.10.0" 13 | }, 14 | "devDependencies": { 15 | "twilio-run": "^2.0.0" 16 | }, 17 | "engines": { 18 | "node": "8.10.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plugin-message-media", 3 | "version": "1.2.1", 4 | "private": true, 5 | "scripts": { 6 | "postinstall": "flex-plugin pre-script-check", 7 | "start": "flex-plugin start" 8 | }, 9 | "dependencies": { 10 | "@material-ui/core": "3.9.4", 11 | "flex-plugin-scripts": "4.3.19-beta.0", 12 | "react": "16.5.2", 13 | "react-dom": "16.5.2" 14 | }, 15 | "devDependencies": { 16 | "@twilio/flex-ui": "^1", 17 | "svg-url-loader": "^7.1.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /public/appConfig.example.js: -------------------------------------------------------------------------------- 1 | var accountSid = '' 2 | 3 | var appConfig = { 4 | pluginService: { 5 | enabled: true, 6 | url: '/plugins', 7 | }, 8 | sso: { 9 | accountSid: accountSid 10 | }, 11 | ytica: false, 12 | logLevel: 'info', 13 | showSupervisorDesktopView: true, 14 | }; 15 | -------------------------------------------------------------------------------- /screenshots/functions-environment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/plugin-message-media/b26f6ebe4436adab2564eaf8559c3cf088317852/screenshots/functions-environment.png -------------------------------------------------------------------------------- /screenshots/functions-interface.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/plugin-message-media/b26f6ebe4436adab2564eaf8559c3cf088317852/screenshots/functions-interface.png -------------------------------------------------------------------------------- /screenshots/modal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/plugin-message-media/b26f6ebe4436adab2564eaf8559c3cf088317852/screenshots/modal.png -------------------------------------------------------------------------------- /screenshots/proxy-callback.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/plugin-message-media/b26f6ebe4436adab2564eaf8559c3cf088317852/screenshots/proxy-callback.png -------------------------------------------------------------------------------- /screenshots/sendFileButton.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/plugin-message-media/b26f6ebe4436adab2564eaf8559c3cf088317852/screenshots/sendFileButton.png -------------------------------------------------------------------------------- /screenshots/thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio-labs/plugin-message-media/b26f6ebe4436adab2564eaf8559c3cf088317852/screenshots/thumbnail.png -------------------------------------------------------------------------------- /src/SmsMediaPlugin.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { VERSION } from '@twilio/flex-ui'; 3 | import { FlexPlugin } from 'flex-plugin'; 4 | 5 | import reducers, { namespace } from './states'; 6 | import LoadingComponent from './components/LoadingComponent/LoadingComponent'; 7 | import BubbleMessageWrapper from "./components/BubbleMessageWrapper/BubbleMessageWrapper"; 8 | import DropMediaComponent from './components/DropMediaComponent/DropMediaComponent'; 9 | import ImageModal from "./components/ImageModal/ImageModal"; 10 | import PasteMediaComponent from './components/PasteMediaComponent/PasteMediaComponent'; 11 | import SendMediaComponent from './components/SendMediaComponent/SendMediaComponent'; 12 | 13 | import SendMediaService from './services/SendMediaService'; 14 | 15 | const PLUGIN_NAME = 'SmsMediaPlugin'; 16 | 17 | const ALLOWED_CHANNELS = [ 'chat-sms', 'chat-whatsapp' ]; 18 | 19 | export default class SmsMediaPlugin extends FlexPlugin { 20 | constructor() { 21 | super(PLUGIN_NAME); 22 | } 23 | 24 | /** 25 | * This code is run when your plugin is being started 26 | * Use this to modify any UI components or attach to the actions framework 27 | * 28 | * @param flex { typeof import('@twilio/flex-ui') } 29 | * @param manager { import('@twilio/flex-ui').Manager } 30 | */ 31 | init(flex, manager) { 32 | this.registerReducers(manager); 33 | 34 | flex.Actions.registerAction("smsModalControl", (payload) => { 35 | var event = new Event("smsModalControlOpen"); 36 | event.url = payload.url; 37 | document.dispatchEvent(event); 38 | return Promise.resolve(); 39 | }); 40 | 41 | // Unbind this action of the native attachments feature (this is under pilot yet). 42 | flex.Actions.replaceAction('AttachFile', (payload) => { return; }); 43 | 44 | flex.MessageBubble.Content.add(); 45 | 46 | flex.MainContainer.Content.add(, { 47 | sortOrder: 1 48 | }); 49 | 50 | const loadingRef = React.createRef(); 51 | const sendMediaService = new SendMediaService(manager); 52 | 53 | flex.MessagingCanvas.Content.add( 54 | 58 | ); 59 | 60 | flex.MessagingCanvas.Content.add( 61 | 67 | ); 68 | 69 | flex.MessagingCanvas.Content.add( 70 | 76 | ); 77 | flex.MessageInput.Content.add( 78 | 84 | ); 85 | 86 | // ignore "media not supported" errors 87 | manager.strings.MediaMessageError = ''; 88 | } 89 | 90 | /** 91 | * Registers the plugin reducers 92 | * 93 | * @param manager { Flex.Manager } 94 | */ 95 | registerReducers(manager) { 96 | if (!manager.store.addReducer) { 97 | // eslint: disable-next-line 98 | console.error(`You need FlexUI > 1.9.0 to use built-in redux; you are currently on ${VERSION}`); 99 | return; 100 | } 101 | 102 | manager.store.addReducer(namespace, reducers); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/components/BubbleMessageWrapper/BubbleMessageWrapper.Styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'react-emotion'; 2 | 3 | export const BubbleMessageWrapperDiv = styled('div')` 4 | padding: '5px'; 5 | margin: '0px'; 6 | `; 7 | -------------------------------------------------------------------------------- /src/components/BubbleMessageWrapper/BubbleMessageWrapper.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import { BubbleMessageWrapperDiv } from './BubbleMessageWrapper.Styles'; 4 | import MediaMessageComponent from '../MediaMessage/MediaMessage'; 5 | 6 | class MessageImageComponent extends Component { 7 | enabledChannels = ["sms", "whatsapp"]; 8 | 9 | constructor(props) { 10 | super(props); 11 | this.state = { 12 | mediaUrl: '' 13 | }; 14 | } 15 | 16 | async componentDidMount() { 17 | const message = this.props.message.source; 18 | const { channel } = message; 19 | 20 | if (this.enabledChannels.includes(channel.attributes.channel_type) && message.media) { 21 | const mediaUrl = await message.media.getContentUrl(); 22 | this.setState({ mediaUrl }); 23 | } 24 | } 25 | 26 | render() { 27 | const message = this.props.message.source; 28 | const { channel } = message; 29 | 30 | if (channel.attributes && !this.enabledChannels.includes(channel.attributes.channel_type)) { 31 | return
32 | } 33 | 34 | // Messages sent by the agent 35 | if (this.state.mediaUrl) { 36 | return ( 37 | 38 | 42 | 43 | ); 44 | } else if ( 45 | message.attributes && 46 | message.attributes.media && 47 | message.attributes.mediaType 48 | ) { 49 | // incoming messages with media 50 | return ( 51 | 52 | 56 | 57 | ); 58 | } 59 | 60 | return
; 61 | } 62 | } 63 | 64 | export default MessageImageComponent; 65 | -------------------------------------------------------------------------------- /src/components/Button/Button.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Button from '@material-ui/core/Button'; 3 | import { withStyles } from '@material-ui/core/styles'; 4 | 5 | const styles = theme => ({ 6 | button: { 7 | margin: 3, 8 | 'padding-left': 5, 9 | 'padding-right': 5, 10 | 'margin-bottom': 12, 11 | 'line-height': 1.25, 12 | 'text-transform': 'none', 13 | 'box-shadow': 'none' 14 | }, 15 | input: { 16 | display: 'none', 17 | }, 18 | }); 19 | 20 | class MyButton extends React.Component { 21 | render() { 22 | const { classes } = this.props; 23 | return ( 24 | 31 | ); 32 | } 33 | } 34 | 35 | export default withStyles(styles)(MyButton); 36 | -------------------------------------------------------------------------------- /src/components/DropMediaComponent/DropHereIcon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/DropMediaComponent/DropMediaComponent.Style.js: -------------------------------------------------------------------------------- 1 | import { css } from 'react-emotion'; 2 | 3 | export const DropAreaStyle = css` 4 | position: absolute; 5 | top: 0; 6 | bottom: 0; 7 | left: 0; 8 | right: 0; 9 | z-index: 1000; 10 | background: rgba(100,100,100,.5); 11 | text-align: center; 12 | & > img { 13 | width: 26%; 14 | height: auto; 15 | max-width: 300px; 16 | max-height: 300px; 17 | filter: invert(98%) sepia(100%) saturate(0%) hue-rotate( 18 | 130deg 19 | ) brightness(100%) contrast(100%); 20 | margin: auto; 21 | position: absolute; 22 | top: 0; 23 | left: 0; 24 | bottom: 0; 25 | right: 0; 26 | } 27 | `; 28 | -------------------------------------------------------------------------------- /src/components/DropMediaComponent/DropMediaComponent.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { css } from 'react-emotion'; 3 | import { withTaskContext } from '@twilio/flex-ui'; 4 | 5 | import DropHereIcon from './DropHereIcon.svg'; 6 | import { DropAreaStyle } from './DropMediaComponent.Style'; 7 | 8 | class DropMediaComponent extends React.Component { 9 | 10 | dropAreaRef = React.createRef(); 11 | 12 | // this counter is necessary to correctly control the drag enter and leave events 13 | // when the cursor hits a sub-component 14 | counter = 0; 15 | 16 | constructor(props) { 17 | super(props); 18 | 19 | this.state = { 20 | dragging: false 21 | }; 22 | } 23 | 24 | handleDragEnter = (e) => { 25 | if (this.state.dragging === false) { 26 | this.setState({ dragging: true }); 27 | } 28 | 29 | this.counter++; 30 | 31 | e.preventDefault(); 32 | e.stopPropagation(); 33 | } 34 | 35 | handleDragLeave = (e) => { 36 | this.counter--; 37 | 38 | if (this.counter === 0) { 39 | this.setState({ dragging: false }); 40 | } 41 | 42 | e.preventDefault(); 43 | e.stopPropagation(); 44 | } 45 | 46 | handleDropOnInvalidArea = (e) => { 47 | this.counter = 0; 48 | this.setState({ dragging: false }); 49 | 50 | e.preventDefault(); 51 | e.stopPropagation(); 52 | } 53 | 54 | handleDropOnDropArea = async (e) => { 55 | this.counter = 0 56 | this.setState({ dragging: false }); 57 | 58 | e.preventDefault(); 59 | e.stopPropagation(); 60 | 61 | if (!e.dataTransfer || !e.dataTransfer.files[0]) { 62 | console.log('No file dropped'); 63 | return; 64 | } 65 | 66 | const { sid: channelSid, channelDefinition, task } = this.props; 67 | 68 | if (!this.props.allowedChannels.includes(channelDefinition.name)) { 69 | console.log('Channel does not supports media'); 70 | return; 71 | } 72 | 73 | const file = e.dataTransfer.files[0]; 74 | if (file) { 75 | this.props.loading.current.show(); 76 | return this.props.sendMediaService.sendMedia(file, channelSid, channelDefinition, task) 77 | .then(() => this.props.loading.current.hide()); 78 | } 79 | 80 | return; 81 | } 82 | 83 | componentDidMount() { 84 | const div = this.dropAreaRef.current; 85 | 86 | window.addEventListener('dragenter', this.handleDragEnter); 87 | window.addEventListener('dragleave', this.handleDragLeave); 88 | window.addEventListener('drop', this.handleDropOnInvalidArea); 89 | div.addEventListener('drop', this.handleDropOnDropArea); 90 | } 91 | 92 | componentWillUnmount() { 93 | const div = this.dropAreaRef.current; 94 | 95 | window.removeEventListener('dragenter', this.handleDragEnter); 96 | window.removeEventListener('dragleave', this.handleDragLeave); 97 | window.removeEventListener('drop', this.handleDropOnInvalidArea); 98 | div.removeEventListener('drop', this.handleDropOnDropArea); 99 | } 100 | 101 | render() { 102 | if (this.state.dragging) { 103 | return ( 104 |
105 | Drop files here 106 |
107 | ); 108 | } 109 | 110 | return ( 111 |
112 | ); 113 | } 114 | } 115 | 116 | export default withTaskContext(DropMediaComponent); 117 | -------------------------------------------------------------------------------- /src/components/ImageModal/ImageModal.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Dialog from "@material-ui/core/Dialog"; 3 | import DialogContent from "@material-ui/core/DialogContent"; 4 | 5 | export default class ImageModal extends React.Component { 6 | constructor(props) { 7 | super(); 8 | this.props = props; 9 | this.showForm = this.showForm.bind(this); 10 | this.cancelForm = this.cancelForm.bind(this); 11 | this.state = { 12 | open: false, 13 | media: "", 14 | disposition: "option-1" 15 | }; 16 | } 17 | 18 | componentDidMount() { 19 | console.log("modal did mount"); 20 | document.addEventListener( 21 | "smsModalControlOpen", 22 | e => { 23 | this.showForm(e.url); 24 | }, 25 | false 26 | ); 27 | } 28 | 29 | showForm(media) { 30 | console.log("show form function"); 31 | this.setState({ open: true, media: media }); 32 | } 33 | 34 | cancelForm() { 35 | this.setState({ open: false }); 36 | } 37 | 38 | render() { 39 | return ( 40 | 47 | 48 | MMS Media 49 | 50 | 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/components/LoadingComponent/LoadingComponent.Styles.js: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'react-emotion'; 2 | 3 | export const LoadingWrapper = styled('div')` 4 | position: absolute; 5 | top: 0; 6 | bottom: 0; 7 | left: 0; 8 | right: 0; 9 | z-index: 1000; 10 | background: rgba(100,100,100,.5); 11 | text-align: center; 12 | `; 13 | 14 | export const Spinner = css` 15 | display: inline-block; 16 | width: 50px; 17 | height: 50px; 18 | border: 3px solid rgba(255,255,255,.3); 19 | border-radius: 50%; 20 | border-top-color: #fff; 21 | animation: spin 1s ease-in-out infinite; 22 | -webkit-animation: spin 1s ease-in-out infinite; 23 | margin: auto; 24 | position: absolute; 25 | top: 0; 26 | bottom: 0; 27 | left: 0; 28 | right: 0; 29 | @keyframes spin { 30 | to { -webkit-transform: rotate(360deg); } 31 | } 32 | @-webkit-keyframes spin { 33 | to { -webkit-transform: rotate(360deg); } 34 | } 35 | `; 36 | -------------------------------------------------------------------------------- /src/components/LoadingComponent/LoadingComponent.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { LoadingWrapper, Spinner } from './LoadingComponent.Styles'; 3 | 4 | class LoadingComponent extends React.Component { 5 | 6 | constructor(props) { 7 | super(props); 8 | 9 | this.state = { visible: false }; 10 | } 11 | 12 | show() { 13 | this.setState({ visible: true }); 14 | } 15 | 16 | hide() { 17 | this.setState({ visible: false }); 18 | } 19 | 20 | render() { 21 | if (this.state.visible) { 22 | return ( 23 | 24 |
25 |
26 | ); 27 | } 28 | 29 | return
; 30 | } 31 | } 32 | 33 | export default LoadingComponent; 34 | -------------------------------------------------------------------------------- /src/components/MediaMessage/MediaMessage.Styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'react-emotion'; 2 | 3 | export const ImageWrapper = styled('div')` 4 | padding-left: 10px; 5 | padding-bottom: 10px; 6 | `; 7 | 8 | export const AudioPlayerWrapper = styled('div')` 9 | padding: 5px; 10 | 11 | audio { 12 | width: 100%; 13 | } 14 | `; 15 | 16 | export const PdfViewerWrapper = styled('div')` 17 | padding: 5px; 18 | `; 19 | 20 | export const VideoPlayerWrapper = styled('div')` 21 | padding: 5px; 22 | `; 23 | -------------------------------------------------------------------------------- /src/components/MediaMessage/MediaMessage.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import * as Flex from '@twilio/flex-ui'; 5 | import { 6 | ImageWrapper, 7 | AudioPlayerWrapper, 8 | PdfViewerWrapper, 9 | VideoPlayerWrapper 10 | } from './MediaMessage.Styles'; 11 | 12 | class MediaMessageComponent extends Component { 13 | renderImage = () => { 14 | const { mediaUrl } = this.props; 15 | 16 | return ( 17 | 18 | MMS 23 | Flex.Actions.invokeAction('smsModalControl', { 24 | url: mediaUrl 25 | }) 26 | } 27 | /> 28 | 29 | ); 30 | }; 31 | 32 | renderAudioPlayer = () => { 33 | const { mediaUrl, mediaType } = this.props; 34 | 35 | return ( 36 | 37 | 40 | 41 | Full Size Player 42 | 43 | 44 | ); 45 | }; 46 | 47 | renderPdfViewer = () => { 48 | const { mediaUrl } = this.props; 49 | 50 | return ( 51 | 52 |