Your Device Info
15 | 16 |22 | 23 |
Make a Call
27 |Incoming Call Controls
38 |39 | Incoming Call from 40 |
41 | 42 | 43 | 44 |49 | 50 | 51 |
├── .dockerignore
├── .env.example
├── .eslintignore
├── .eslintrc.yml
├── .github
├── dependabot.yml
└── workflows
│ └── nodejs.yml
├── .gitignore
├── .mergify.yml
├── .prettierignore
├── .prettierrc.json
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── ConsoleHowTos
├── BuyVoicePhoneNumber
│ ├── BuyVoicePhoneNumber.md
│ └── screenshots
│ │ ├── Click_Buy_Voice_Number.png
│ │ ├── Phone_numbers_page.png
│ │ ├── Review_Phone_Number.png
│ │ ├── Search_for_voice_numbers.png
│ │ └── Select_new_number_from_active_numbers_list.png
├── CreateAPIKey
│ ├── CreateAPIKey.md
│ └── screenshots
│ │ ├── API_Key_and_Secret.png
│ │ ├── API_Keys_Page.png
│ │ └── Create_New_API_Key.png
└── CreateNewTwiMLApp
│ ├── CreateNewTwiMLApp.md
│ └── screenshots
│ ├── Create_new_TwiML_App.png
│ ├── Find_your_TwiML_App_SID.png
│ ├── Select_TwiML_App_from_list.png
│ └── TwiML_Apps_Console.png
├── Dockerfile
├── LICENSE
├── README.md
├── config.js
├── docker-compose.yml
├── index.js
├── name_generator.js
├── package-lock.json
├── package.json
├── public
├── index.html
├── quickstart.js
├── site.css
└── twilio.min.js
├── screenshots
├── BrowserToBrowserCall.png
├── ConfigurePhoneNumberWithTwiMLApp.png
├── InitializeDevice.png
└── UpdateRequestURL.png
├── src
├── handler.js
└── router.js
└── tests
└── handler.test.js
/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log
3 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | TWILIO_ACCOUNT_SID=ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
2 |
3 | # The Twilio number for making outbound and receiving inbound calls
4 | # Purchase a Twilio phone number in the console
5 | # https://www.twilio.com/console/phone-numbers/search
6 | TWILIO_CALLER_ID=+1XXXYYYZZZZ
7 |
8 | # SID of your TwiML Application
9 | # Create a new TwiML app in the console
10 | # https://www.twilio.com/console/voice/twiml/apps
11 | TWILIO_TWIML_APP_SID=APXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
12 |
13 | # Your REST API Key information
14 | # Create a new key in the console (the TWILIO_API_KEY will be the SID shown starting with 'SK')
15 | # https://www.twilio.com/console/project/api-keys
16 | TWILIO_API_KEY=SKXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
17 | TWILIO_API_SECRET=XXXXXXXXXXXXXXXXX
18 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | public/**/*.js
2 |
--------------------------------------------------------------------------------
/.eslintrc.yml:
--------------------------------------------------------------------------------
1 | extends: google
2 | parserOptions:
3 | ecmaVersion: 6
4 | rules:
5 | linebreak-style: 0
6 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: npm
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | open-pull-requests-limit: 10
8 |
--------------------------------------------------------------------------------
/.github/workflows/nodejs.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: Node.js
5 |
6 | on:
7 | push:
8 | branches: [master, next]
9 | pull_request:
10 | branches: [master, next]
11 |
12 | jobs:
13 | build:
14 | runs-on: ${{ matrix.platform }}
15 |
16 | strategy:
17 | matrix:
18 | node-version: [10.x, 12.x]
19 | platform: [windows-latest, macos-latest, ubuntu-latest]
20 |
21 | steps:
22 | - uses: actions/checkout@v2
23 | - name: Use Node.js ${{ matrix.node-version }}
24 | uses: actions/setup-node@v1
25 | with:
26 | node-version: ${{ matrix.node-version }}
27 | - run: npm ci
28 | - run: npm test
29 | env:
30 | CI: true
31 | TWILIO_ACCOUNT_SID: ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
32 | TWILIO_TWIML_APP_SID: APXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
33 | TWILIO_CALLER_ID: +1XXXYYYZZZZ
34 | TWILIO_API_KEY: SKXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
35 | TWILIO_API_SECRET: XXXXXXXXXXXXXXXXX
36 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | npm-debug.log
4 | .env
5 | .tool-versions
6 |
--------------------------------------------------------------------------------
/.mergify.yml:
--------------------------------------------------------------------------------
1 | pull_request_rules:
2 | - name: automatic merge for Dependabot pull requests
3 | conditions:
4 | - author=dependabot-preview[bot]
5 | - status-success=build (10.x, macos-latest)
6 | - status-success=build (12.x, macos-latest)
7 | - status-success=build (10.x, windows-latest)
8 | - status-success=build (12.x, windows-latest)
9 | - status-success=build (10.x, ubuntu-latest)
10 | - status-success=build (12.x, ubuntu-latest)
11 | actions:
12 | merge:
13 | method: squash
14 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # Ignore artifacts:
2 | build
3 | coverage
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/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
74 |
--------------------------------------------------------------------------------
/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.
4 |
--------------------------------------------------------------------------------
/ConsoleHowTos/BuyVoicePhoneNumber/BuyVoicePhoneNumber.md:
--------------------------------------------------------------------------------
1 | # Buy a Voice Phone Number
2 |
3 | 1. Log in to your [Twilio Console](https://www.twilio.com/console)
4 |
5 | 2. Navigate to [Phone Numbers](https://www.twilio.com/console/phone-numbers/incoming)
6 |
7 | 3. Click on the 'Buy a number' button in the top right corner.
8 |
9 | 
10 |
11 | 4. On the 'Buy a Number' screen, select 'Voice' under capabilities, then click 'Search'.
12 |
13 | 
14 |
15 | 5. Choose a phone number and click 'Buy'.
16 |
17 | 
18 |
19 | 6. On the 'Review Phone Number' modal, click 'Buy (XXX) XXX-XXXX' button in bottom right.
20 |
21 | 
22 |
--------------------------------------------------------------------------------
/ConsoleHowTos/BuyVoicePhoneNumber/screenshots/Click_Buy_Voice_Number.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/voice-javascript-sdk-quickstart-node/b1cb14a30da7bc243431e6c1167797bf1fc2cb37/ConsoleHowTos/BuyVoicePhoneNumber/screenshots/Click_Buy_Voice_Number.png
--------------------------------------------------------------------------------
/ConsoleHowTos/BuyVoicePhoneNumber/screenshots/Phone_numbers_page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/voice-javascript-sdk-quickstart-node/b1cb14a30da7bc243431e6c1167797bf1fc2cb37/ConsoleHowTos/BuyVoicePhoneNumber/screenshots/Phone_numbers_page.png
--------------------------------------------------------------------------------
/ConsoleHowTos/BuyVoicePhoneNumber/screenshots/Review_Phone_Number.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/voice-javascript-sdk-quickstart-node/b1cb14a30da7bc243431e6c1167797bf1fc2cb37/ConsoleHowTos/BuyVoicePhoneNumber/screenshots/Review_Phone_Number.png
--------------------------------------------------------------------------------
/ConsoleHowTos/BuyVoicePhoneNumber/screenshots/Search_for_voice_numbers.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/voice-javascript-sdk-quickstart-node/b1cb14a30da7bc243431e6c1167797bf1fc2cb37/ConsoleHowTos/BuyVoicePhoneNumber/screenshots/Search_for_voice_numbers.png
--------------------------------------------------------------------------------
/ConsoleHowTos/BuyVoicePhoneNumber/screenshots/Select_new_number_from_active_numbers_list.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/voice-javascript-sdk-quickstart-node/b1cb14a30da7bc243431e6c1167797bf1fc2cb37/ConsoleHowTos/BuyVoicePhoneNumber/screenshots/Select_new_number_from_active_numbers_list.png
--------------------------------------------------------------------------------
/ConsoleHowTos/CreateAPIKey/CreateAPIKey.md:
--------------------------------------------------------------------------------
1 | # Create an API Key
2 |
3 | 1. Log in to your [Twilio Console](https://www.twilio.com/console)
4 |
5 | 2. Navigate to [Settings > API Keys](https://www.twilio.com/console/project/api-keys).
6 |
7 | 3. On the API Keys screen, click on the 'Create API Key' button in the top right corner.
8 |
9 | 
10 |
11 | 4. Give your API Key a Friendly Name and click the 'Create API Key' Button.
12 |
13 | 
14 |
15 | 5. On the 'Copy secret key' page, copy the SID and the Secret. The SID will be the `TWILIO_API_KEY` in your `.env` file. The Secret will be the `TWILIO_API_SECRET` in your `.env` file. You will only see the Secret on this screen, so you must copy it and keep it in a safe location.
16 |
17 | 
18 |
--------------------------------------------------------------------------------
/ConsoleHowTos/CreateAPIKey/screenshots/API_Key_and_Secret.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/voice-javascript-sdk-quickstart-node/b1cb14a30da7bc243431e6c1167797bf1fc2cb37/ConsoleHowTos/CreateAPIKey/screenshots/API_Key_and_Secret.png
--------------------------------------------------------------------------------
/ConsoleHowTos/CreateAPIKey/screenshots/API_Keys_Page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/voice-javascript-sdk-quickstart-node/b1cb14a30da7bc243431e6c1167797bf1fc2cb37/ConsoleHowTos/CreateAPIKey/screenshots/API_Keys_Page.png
--------------------------------------------------------------------------------
/ConsoleHowTos/CreateAPIKey/screenshots/Create_New_API_Key.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/voice-javascript-sdk-quickstart-node/b1cb14a30da7bc243431e6c1167797bf1fc2cb37/ConsoleHowTos/CreateAPIKey/screenshots/Create_New_API_Key.png
--------------------------------------------------------------------------------
/ConsoleHowTos/CreateNewTwiMLApp/CreateNewTwiMLApp.md:
--------------------------------------------------------------------------------
1 | # Create a new TwiML App
2 |
3 | 1. Log in to your [Twilio Console](https://www.twilio.com/console)
4 |
5 | 2. Navigate to [Programmable Voice > TwiML > TwiML Apps](https://www.twilio.com/console/voice/twiml/apps)
6 |
7 | 3. Click on the "Create new TwiML App" button in the top right corner.
8 |
9 | 
10 |
11 | 4. Give your TwiML App a friendly name and click 'Create'.
12 |
13 | 
14 |
15 | 5. Click on your new TwiML App in the list of TwiML Apps.
16 |
17 | 
18 |
19 | 6. Find your TwiML App's SID. You will need this to configure your project.
20 |
21 | 
22 |
--------------------------------------------------------------------------------
/ConsoleHowTos/CreateNewTwiMLApp/screenshots/Create_new_TwiML_App.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/voice-javascript-sdk-quickstart-node/b1cb14a30da7bc243431e6c1167797bf1fc2cb37/ConsoleHowTos/CreateNewTwiMLApp/screenshots/Create_new_TwiML_App.png
--------------------------------------------------------------------------------
/ConsoleHowTos/CreateNewTwiMLApp/screenshots/Find_your_TwiML_App_SID.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/voice-javascript-sdk-quickstart-node/b1cb14a30da7bc243431e6c1167797bf1fc2cb37/ConsoleHowTos/CreateNewTwiMLApp/screenshots/Find_your_TwiML_App_SID.png
--------------------------------------------------------------------------------
/ConsoleHowTos/CreateNewTwiMLApp/screenshots/Select_TwiML_App_from_list.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/voice-javascript-sdk-quickstart-node/b1cb14a30da7bc243431e6c1167797bf1fc2cb37/ConsoleHowTos/CreateNewTwiMLApp/screenshots/Select_TwiML_App_from_list.png
--------------------------------------------------------------------------------
/ConsoleHowTos/CreateNewTwiMLApp/screenshots/TwiML_Apps_Console.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/voice-javascript-sdk-quickstart-node/b1cb14a30da7bc243431e6c1167797bf1fc2cb37/ConsoleHowTos/CreateNewTwiMLApp/screenshots/TwiML_Apps_Console.png
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:10
2 |
3 | WORKDIR /usr/src/app
4 |
5 | COPY package*.json ./
6 |
7 | RUN npm install
8 |
9 | COPY . .
10 |
11 | EXPOSE 3000
12 |
13 | CMD [ "npm", "start" ]
14 |
--------------------------------------------------------------------------------
/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.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # Twilio Voice JavaScript SDK Quickstart for Node.js
6 |
7 | 
8 |
9 | > This template is part of Twilio CodeExchange. If you encounter any issues with this code, please open an issue at [github.com/twilio-labs/code-exchange/issues](https://github.com/twilio-labs/code-exchange/issues).
10 |
11 | ## About
12 |
13 | This application should give you a ready-made starting point for writing your own voice apps with Twilio Voice JavaScript SDK 2.0 (Formerly known as Twilio Client).
14 |
15 | This application is built in Node.
16 |
17 | Implementations in other languages:
18 |
19 | | .NET | Java | Python | PHP | Ruby |
20 | | :---------- | :---------- | :---------------------------------------------------------------------------- | :---------- | :---------- |
21 | | [Done](https://github.com/TwilioDevEd/voice-javascript-sdk-quickstart-csharp) | [Done](https://github.com/TwilioDevEd/voice-javascript-sdk-quickstart-java)| [Done](https://github.com/TwilioDevEd/voice-javascript-sdk-quickstart-python) | [Done](https://github.com/TwilioDevEd/voice-javascript-sdk-quickstart-php) | [Done](https://github.com/TwilioDevEd/voice-javascript-sdk-quickstart-ruby) |
22 |
23 | ## Set Up
24 |
25 | ### Requirements
26 |
27 | - [Nodejs](https://nodejs.org/) version **14.0** or above.
28 | - [ngrok](https://ngrok.com/download) - this is used to expose your local development server to the internet. For more information, read [this Twilio blog post](https://www.twilio.com/blog/2015/09/6-awesome-reasons-to-use-ngrok-when-testing-webhooks.html).
29 | - A WebRTC enabled browser (Google Chrome or Mozilla Firefox are recommended). Edge and Internet Explorer will not work for testing.
30 |
31 | ### Create a TwiML Application, Purchase a Phone Number, Create an API Key
32 |
33 | 1. [Create a TwiML Application in the Twilio Console](https://www.twilio.com/console/voice/twiml/apps). Once you create the TwiML Application, click on it in your list of TwiML Apps to find the TwiML App SID. You will need this SID for your `.env` file. **Note:** You will need to configure the Voice "REQUEST URL" in your TwiML App later.
34 |
35 | - For detailed instructions with screenshots, see the [Create a TwiML App.md file](ConsoleHowTos/CreateNewTwiMLApp/CreateNewTwiMLApp.md)
36 |
37 | 2. [Purchase a Voice phone number](https://www.twilio.com/console/phone-numbers/incoming). You will need this phone number in [E.164 format](https://en.wikipedia.org/wiki/E.164) for your `.env` file.
38 |
39 | - For detailed instructions with screenshots, see the [Buy a Phone Number.md file](ConsoleHowTos/BuyVoicePhoneNumber/BuyVoicePhoneNumber.md)
40 |
41 | 3. [Create an API Key in the Twilio Console](https://www.twilio.com/console/project/api-keys). Keep the API Key SID and the API Secret in a safe place, since you will need them for your `.env` file. Your API KEY is needed to create an [Access Token](https://www.twilio.com/docs/iam/access-tokens).
42 |
43 | - For detailed instructions with screenshots, see the [Create an API Key.md file](ConsoleHowTos/CreateAPIKey/CreateAPIKey.md)
44 |
45 | ### Gather Config Values
46 |
47 | Before we begin local development, we need to collect all the config values we need to run the application.
48 |
49 | | Config Value | Description |
50 | | :------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
51 | | `TWILIO_ACCOUNT_SID` | Your primary Twilio account identifier - find this [in the console here](https://www.twilio.com/console). |
52 | | `TWILIO_TWIML_APP_SID` | The SID of the TwiML App you created in step 1 above. Find the SID [in the console here](https://www.twilio.com/console/voice/twiml/apps). |
53 | | `TWILIO_CALLER_ID` | Your Twilio phone number in [E.164 format](https://en.wikipedia.org/wiki/E.164) - you can [find your number here](https://www.twilio.com/console/phone-numbers/incoming) |
54 | | `TWILIO_API_KEY` / `TWILIO_API_SECRET` | The `TWILIO_API_KEY` is the API Key SID you created in step 3 above, and the `TWILIO_API_SECRET` is the secret associated with that key. |
55 |
56 | ### Local development
57 |
58 | 1. First clone this repository and cd into it:
59 |
60 | ```bash
61 | git clone https://github.com/TwilioDevEd/voice-javascript-sdk-quickstart-node.git
62 | cd voice-javascript-sdk-quickstart-node
63 | ```
64 |
65 | 2. Create a configuration file for your application by copying the `.env.example` and edit the `.env` file with the configuration values from above.
66 |
67 | ```bash
68 | cp .env.example .env
69 | ```
70 |
71 | 3. Install the dependencies.
72 |
73 | ```bash
74 | npm install
75 | ```
76 |
77 | 4. Copy the `twilio.min.js` file from your `node_modules` to your `public` directory. Run the following from the root directory of your project:
78 |
79 | ```bash
80 | cp node_modules/@twilio/voice-sdk/dist/twilio.min.js public
81 | ```
82 | **Note:** In order to keep this quickstart as simple as possible, this step is used to avoid any need for build tools like Webpack.
83 |
84 | 5. Launch local development web server.
85 |
86 | ```bash
87 | npm start
88 | ```
89 |
90 | 6. Navigate to [http://localhost:3000](http://localhost:3000) in your browser.
91 |
92 | 7. Expose your application to the wider internet using `ngrok`. This step is **crucial** for the app to work as expected.
93 |
94 | ```bash
95 | ngrok http 3000
96 | ```
97 |
98 | 8. `ngrok` will assign a unique URL to your tunnel.
99 | It might be something like `https://asdf456.ngrok.io`. You will need this to configure your TwiML app in the next step.
100 |
101 | 9. Configure your TwiML app
102 |
103 | - In the Twilio Console, navigate to [Programmable Voice > TwiML > TwiML Apps](https://www.twilio.com/console/voice/twiml/apps)
104 | - Select the TwiML App you created earlier
105 | - On your TwiML App's information page, find the 'Voice Configuration' section.
106 | - Change the Request URL to your ngrok url with `/voice` appended to the end. (E.g: `https://asdf456.ngrok.io/voice`) **Note:** You **must** use the https URL, otherwise some browsers will block
107 | microphone access.
108 | - Click the 'Save' button.
109 |
110 | 
111 |
112 | You should now be ready to make and receive calls from your browser.
113 |
114 | ## Your Web Application
115 |
116 | When you navigate to `localhost:3000`, you should see the web application containing a 'Start up the Device' button. Click this button to initialize a `Twilio.Device`.
117 |
118 | 
119 |
120 | When the `Twilio.Device` is initialized, you will be assigned a random "client name", which will appear in the 'Device Info' column on the left side of the page. This client name is used as the `identity` field when generating an Access Token for the `Twilio.Device`, and is also used to route SDK-to-SDK calls to the correct `Twilio.Device`.
121 |
122 | ### To make an outbound call to a phone number:
123 |
124 | - Under 'Make a Call', enter a phone number in [E.164 format](https://en.wikipedia.org/wiki/E.164) and press the 'Call' button
125 |
126 | ### To make a browser-to browser call:
127 |
128 | Open two browser windows to `localhost:3000` and click 'Start up the Device' button in both windows. You should see a different client name in each window.
129 |
130 | Enter one client's name in the other client's 'Make a Call' input and press the 'Call' button.
131 |
132 | 
133 |
134 | ### Receiving Incoming Calls from a Non-Browser Device
135 |
136 | You will first need to configure your Twilio Voice Phone Number to use the TwiML App we created earlier. This tells Twilio how to handle an incoming call directed to your Twilio Voice Number.
137 |
138 | 1. Log in to your [Twilio Console](https://www.twilio.com/console)
139 | 2. Navigate to your [Active Numbers list](https://www.twilio.com/console/phone-numbers/incoming)
140 | 3. Click on the number you purchased earlier
141 | 4. Scroll down to find the 'Voice & Fax' section and look for 'CONFIGURE WITH'
142 | 5. Select 'TwiML' App
143 | 6. Under 'TWIML APP', choose the TwiML App you created earlier.
144 | 7. Click the 'Save' button at the bottom of the browser window.
145 |
146 | 
147 |
148 | You can now call your Twilio Voice Phone Number from your cell or landline phone.
149 |
150 | **Note:** Since this is a quickstart with limited functionality, incoming calls will only be routed to your most recently-created `Twilio.Device`.
151 |
152 | ### Unknown Audio Devices
153 |
154 | If you see "Unknown Audio Output Device 1" in the "Ringtone" or "Speaker" devices lists, click the button below the boxes (Seeing "Unknown" Devices?) to have your browser identify your input and output devices.
155 |
156 | ### Docker
157 |
158 | If you have [Docker](https://www.docker.com/) already installed on your machine, you can use our `docker-compose.yml` to setup your project.
159 |
160 | 1. Make sure you have the project cloned.
161 | 2. Setup the `.env` file as outlined in the [Local Development](#local-development) steps.
162 | 3. Run `docker-compose up`.
163 | 4. Follow the steps in [Local Development](#local-development) on how to expose your port to Twilio using a tool like [ngrok](https://ngrok.com/) and configure the remaining parts of your application.
164 |
165 | ### Cloud deployment
166 |
167 | In addition to trying out this application locally, you can deploy it to a variety of host services. Heroku is one option, linked below.
168 |
169 | Please be aware that some of these services may charge you for the usage and/or might make the source code for this application visible to the public. When in doubt, research the respective hosting service first.
170 |
171 | | Service | |
172 | | :-------------------------------- | :---------------------------------------------------------------------------------- |
173 | | [Heroku](https://www.heroku.com/) | [](https://heroku.com/deploy) |
174 |
175 | ## Resources
176 |
177 | - The CodeExchange repository can be found [here](https://github.com/twilio-labs/code-exchange/).
178 |
179 | ## Contributing
180 |
181 | This template is open source and welcomes contributions. All contributions are subject to our [Code of Conduct](https://github.com/twilio-labs/.github/blob/master/CODE_OF_CONDUCT.md).
182 |
183 | ## License
184 |
185 | [MIT](http://www.opensource.org/licenses/mit-license.html)
186 |
187 | ## Disclaimer
188 |
189 | No warranty expressed or implied. Software is as is.
190 |
191 | [twilio]: https://www.twilio.com
192 |
--------------------------------------------------------------------------------
/config.js:
--------------------------------------------------------------------------------
1 | const dotenv = require("dotenv");
2 | const cfg = {};
3 |
4 | if (process.env.NODE_ENV !== "test") {
5 | dotenv.config({ path: ".env" });
6 | } else {
7 | dotenv.config({ path: ".env.example", silent: true });
8 | }
9 |
10 | // HTTP Port to run our web application
11 | cfg.port = process.env.PORT || 3000;
12 |
13 | // Your Twilio account SID and auth token, both found at:
14 | // https://www.twilio.com/user/account
15 | //
16 | // A good practice is to store these string values as system environment
17 | // variables, and load them from there as we are doing below. Alternately,
18 | // you could hard code these values here as strings.
19 | cfg.accountSid = process.env.TWILIO_ACCOUNT_SID;
20 |
21 | cfg.twimlAppSid = process.env.TWILIO_TWIML_APP_SID;
22 | cfg.callerId = process.env.TWILIO_CALLER_ID;
23 |
24 | cfg.apiKey = process.env.TWILIO_API_KEY;
25 | cfg.apiSecret = process.env.TWILIO_API_SECRET;
26 |
27 | // Export configuration object
28 | module.exports = cfg;
29 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.8"
2 | services:
3 | app:
4 | restart: always
5 | build: .
6 | ports:
7 | - "3000:3000"
8 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | const http = require("http");
2 | const path = require("path");
3 | const express = require("express");
4 | const bodyParser = require("body-parser");
5 |
6 | const router = require("./src/router");
7 |
8 | // Create Express webapp
9 | const app = express();
10 | app.use(express.static(path.join(__dirname, "public")));
11 | app.use(bodyParser.urlencoded({ extended: false }));
12 | app.use(bodyParser.json());
13 |
14 | app.use(router);
15 |
16 | // Create http server and run it
17 | const server = http.createServer(app);
18 | const port = process.env.PORT || 3000;
19 |
20 | server.listen(port, function () {
21 | console.log("Express server running on *:" + port);
22 | });
23 |
--------------------------------------------------------------------------------
/name_generator.js:
--------------------------------------------------------------------------------
1 | const ADJECTIVES = [
2 | "Awesome",
3 | "Bold",
4 | "Creative",
5 | "Dapper",
6 | "Eccentric",
7 | "Fiesty",
8 | "Golden",
9 | "Holy",
10 | "Ignominious",
11 | "Jolly",
12 | "Kindly",
13 | "Lucky",
14 | "Mushy",
15 | "Natural",
16 | "Oaken",
17 | "Precise",
18 | "Quiet",
19 | "Rowdy",
20 | "Sunny",
21 | "Tall",
22 | "Unique",
23 | "Vivid",
24 | "Wonderful",
25 | "Xtra",
26 | "Yawning",
27 | "Zesty",
28 | ];
29 |
30 | const FIRST_NAMES = [
31 | "Anna",
32 | "Bobby",
33 | "Cameron",
34 | "Danny",
35 | "Emmett",
36 | "Frida",
37 | "Gracie",
38 | "Hannah",
39 | "Isaac",
40 | "Jenova",
41 | "Kendra",
42 | "Lando",
43 | "Mufasa",
44 | "Nate",
45 | "Owen",
46 | "Penny",
47 | "Quincy",
48 | "Roddy",
49 | "Samantha",
50 | "Tammy",
51 | "Ulysses",
52 | "Victoria",
53 | "Wendy",
54 | "Xander",
55 | "Yolanda",
56 | "Zelda",
57 | ];
58 |
59 | const LAST_NAMES = [
60 | "Anchorage",
61 | "Berlin",
62 | "Cucamonga",
63 | "Davenport",
64 | "Essex",
65 | "Fresno",
66 | "Gunsight",
67 | "Hanover",
68 | "Indianapolis",
69 | "Jamestown",
70 | "Kane",
71 | "Liberty",
72 | "Minneapolis",
73 | "Nevis",
74 | "Oakland",
75 | "Portland",
76 | "Quantico",
77 | "Raleigh",
78 | "SaintPaul",
79 | "Tulsa",
80 | "Utica",
81 | "Vail",
82 | "Warsaw",
83 | "XiaoJin",
84 | "Yale",
85 | "Zimmerman",
86 | ];
87 |
88 | const rand = (arr) => arr[Math.floor(Math.random() * arr.length)];
89 |
90 | module.exports = () => rand(ADJECTIVES) + rand(FIRST_NAMES) + rand(LAST_NAMES);
91 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "twilio-voice-javascript-sdk-quickstart-node",
3 | "private": true,
4 | "version": "1.0.0",
5 | "description": "Quick Start application template for Twilio Voice JavaScript SDK on Node.js",
6 | "main": "index.js",
7 | "scripts": {
8 | "start": "node index.js",
9 | "eslint": "./node_modules/.bin/eslint .",
10 | "test": "./node_modules/.bin/jest"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "github.com/TwilioDevEd/client-quickstart-node"
15 | },
16 | "keywords": [
17 | "twilio",
18 | "voip",
19 | "ip",
20 | "chat",
21 | "real",
22 | "time",
23 | "diggity"
24 | ],
25 | "author": "Twilio Developer Education",
26 | "license": "MIT",
27 | "engines": {
28 | "node": ">=10.x"
29 | },
30 | "dependencies": {
31 | "@twilio/voice-sdk": "^2.2.0",
32 | "body-parser": "^1.19.0",
33 | "dotenv": "^10.0.0",
34 | "express": "^4.17.1",
35 | "twilio": "~3.64.0"
36 | },
37 | "devDependencies": {
38 | "eslint": "^7.29.0",
39 | "eslint-config-google": "^0.14.0",
40 | "jest": "^27.0.5",
41 | "prettier": "2.3.1"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
39 | Incoming Call from 40 |
41 | 42 | 43 | 44 |> ${message}
`; 215 | logDiv.scrollTop = logDiv.scrollHeight; 216 | } 217 | 218 | function setClientNameUI(clientName) { 219 | var div = document.getElementById("client-name"); 220 | div.innerHTML = `Your client name: ${clientName}`; 221 | } 222 | 223 | function resetIncomingCallUI() { 224 | incomingPhoneNumberEl.innerHTML = ""; 225 | incomingCallAcceptButton.classList.remove("hide"); 226 | incomingCallRejectButton.classList.remove("hide"); 227 | incomingCallHangupButton.classList.add("hide"); 228 | incomingCallDiv.classList.add("hide"); 229 | } 230 | 231 | // AUDIO CONTROLS 232 | 233 | async function getAudioDevices() { 234 | await navigator.mediaDevices.getUserMedia({ audio: true }); 235 | updateAllAudioDevices.bind(device); 236 | } 237 | 238 | function updateAllAudioDevices() { 239 | if (device) { 240 | updateDevices(speakerDevices, device.audio.speakerDevices.get()); 241 | updateDevices(ringtoneDevices, device.audio.ringtoneDevices.get()); 242 | } 243 | } 244 | 245 | function updateOutputDevice() { 246 | const selectedDevices = Array.from(speakerDevices.children) 247 | .filter((node) => node.selected) 248 | .map((node) => node.getAttribute("data-id")); 249 | 250 | device.audio.speakerDevices.set(selectedDevices); 251 | } 252 | 253 | function updateRingtoneDevice() { 254 | const selectedDevices = Array.from(ringtoneDevices.children) 255 | .filter((node) => node.selected) 256 | .map((node) => node.getAttribute("data-id")); 257 | 258 | device.audio.ringtoneDevices.set(selectedDevices); 259 | } 260 | 261 | function bindVolumeIndicators(call) { 262 | call.on("volume", function (inputVolume, outputVolume) { 263 | var inputColor = "red"; 264 | if (inputVolume < 0.5) { 265 | inputColor = "green"; 266 | } else if (inputVolume < 0.75) { 267 | inputColor = "yellow"; 268 | } 269 | 270 | inputVolumeBar.style.width = Math.floor(inputVolume * 300) + "px"; 271 | inputVolumeBar.style.background = inputColor; 272 | 273 | var outputColor = "red"; 274 | if (outputVolume < 0.5) { 275 | outputColor = "green"; 276 | } else if (outputVolume < 0.75) { 277 | outputColor = "yellow"; 278 | } 279 | 280 | outputVolumeBar.style.width = Math.floor(outputVolume * 300) + "px"; 281 | outputVolumeBar.style.background = outputColor; 282 | }); 283 | } 284 | 285 | // Update the available ringtone and speaker devices 286 | function updateDevices(selectEl, selectedDevices) { 287 | selectEl.innerHTML = ""; 288 | 289 | device.audio.availableOutputDevices.forEach(function (device, id) { 290 | var isActive = selectedDevices.size === 0 && id === "default"; 291 | selectedDevices.forEach(function (device) { 292 | if (device.deviceId === id) { 293 | isActive = true; 294 | } 295 | }); 296 | 297 | var option = document.createElement("option"); 298 | option.label = device.label; 299 | option.setAttribute("data-id", id); 300 | if (isActive) { 301 | option.setAttribute("selected", "selected"); 302 | } 303 | selectEl.appendChild(option); 304 | }); 305 | } 306 | }); 307 | -------------------------------------------------------------------------------- /public/site.css: -------------------------------------------------------------------------------- 1 | @import url(https://fonts.googleapis.com/css?family=Share+Tech+Mono); 2 | 3 | body, 4 | p { 5 | padding: 0; 6 | margin: auto; 7 | font-family: Arial, Helvetica, sans-serif; 8 | } 9 | 10 | h1 { 11 | text-align: center; 12 | } 13 | 14 | h2 { 15 | margin-top: 0; 16 | border-bottom: 1px solid black; 17 | } 18 | 19 | button { 20 | margin-bottom: 10px; 21 | } 22 | 23 | label { 24 | text-align: left; 25 | font-size: 1.25em; 26 | color: #777776; 27 | display: block; 28 | } 29 | 30 | header { 31 | text-align: center; 32 | } 33 | 34 | main { 35 | padding: 3em; 36 | max-width: 1200px; 37 | margin: 0 auto; 38 | display: flex; 39 | justify-content: space-between; 40 | align-items: flex-start; 41 | } 42 | 43 | .left-column, 44 | .center-column, 45 | .right-column { 46 | width: 30%; 47 | min-width: 16em; 48 | margin: 0 1.5em; 49 | text-align: center; 50 | } 51 | 52 | /* Left Column */ 53 | #client-name { 54 | text-align: left; 55 | margin-bottom: 1em; 56 | font-family: "Helvetica Light", Helvetica, sans-serif; 57 | font-size: 1.25em; 58 | color: #777776; 59 | } 60 | 61 | select { 62 | width: 300px; 63 | height: 60px; 64 | margin-bottom: 10px; 65 | } 66 | 67 | /* Center Column */ 68 | input { 69 | font-family: Helvetica-LightOblique, Helvetica, sans-serif; 70 | font-style: oblique; 71 | font-size: 1em; 72 | width: 100%; 73 | height: 2.5em; 74 | padding: 0; 75 | display: block; 76 | margin: 10px 0; 77 | } 78 | 79 | div#volume-indicators { 80 | padding: 10px; 81 | margin-top: 20px; 82 | width: 500px; 83 | text-align: left; 84 | } 85 | 86 | div#volume-indicators > div { 87 | display: block; 88 | height: 20px; 89 | width: 0; 90 | } 91 | 92 | /* Right Column */ 93 | .right-column { 94 | padding: 0 1.5em; 95 | } 96 | 97 | #log { 98 | text-align: left; 99 | border: 1px solid #686865; 100 | padding: 10px; 101 | height: 9.5em; 102 | overflow-y: scroll; 103 | } 104 | 105 | .log-entry { 106 | color: #686865; 107 | font-family: "Share Tech Mono", "Courier New", Courier, fixed-width; 108 | font-size: 1.25em; 109 | line-height: 1.25em; 110 | margin-left: 1em; 111 | text-indent: -1.25em; 112 | width: 90%; 113 | } 114 | 115 | /* Other Styles */ 116 | .hide { 117 | position: absolute !important; 118 | top: -9999px !important; 119 | left: -9999px !important; 120 | } 121 | 122 | button:disabled { 123 | cursor: not-allowed; 124 | } 125 | -------------------------------------------------------------------------------- /screenshots/BrowserToBrowserCall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/voice-javascript-sdk-quickstart-node/b1cb14a30da7bc243431e6c1167797bf1fc2cb37/screenshots/BrowserToBrowserCall.png -------------------------------------------------------------------------------- /screenshots/ConfigurePhoneNumberWithTwiMLApp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/voice-javascript-sdk-quickstart-node/b1cb14a30da7bc243431e6c1167797bf1fc2cb37/screenshots/ConfigurePhoneNumberWithTwiMLApp.png -------------------------------------------------------------------------------- /screenshots/InitializeDevice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/voice-javascript-sdk-quickstart-node/b1cb14a30da7bc243431e6c1167797bf1fc2cb37/screenshots/InitializeDevice.png -------------------------------------------------------------------------------- /screenshots/UpdateRequestURL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/voice-javascript-sdk-quickstart-node/b1cb14a30da7bc243431e6c1167797bf1fc2cb37/screenshots/UpdateRequestURL.png -------------------------------------------------------------------------------- /src/handler.js: -------------------------------------------------------------------------------- 1 | const VoiceResponse = require("twilio").twiml.VoiceResponse; 2 | const AccessToken = require("twilio").jwt.AccessToken; 3 | const VoiceGrant = AccessToken.VoiceGrant; 4 | 5 | const nameGenerator = require("../name_generator"); 6 | const config = require("../config"); 7 | 8 | var identity; 9 | 10 | exports.tokenGenerator = function tokenGenerator() { 11 | identity = nameGenerator(); 12 | 13 | const accessToken = new AccessToken( 14 | config.accountSid, 15 | config.apiKey, 16 | config.apiSecret 17 | ); 18 | accessToken.identity = identity; 19 | const grant = new VoiceGrant({ 20 | outgoingApplicationSid: config.twimlAppSid, 21 | incomingAllow: true, 22 | }); 23 | accessToken.addGrant(grant); 24 | 25 | // Include identity and token in a JSON response 26 | return { 27 | identity: identity, 28 | token: accessToken.toJwt(), 29 | }; 30 | }; 31 | 32 | exports.voiceResponse = function voiceResponse(requestBody) { 33 | const toNumberOrClientName = requestBody.To; 34 | const callerId = config.callerId; 35 | let twiml = new VoiceResponse(); 36 | 37 | // If the request to the /voice endpoint is TO your Twilio Number, 38 | // then it is an incoming call towards your Twilio.Device. 39 | if (toNumberOrClientName == callerId) { 40 | let dial = twiml.dial(); 41 | 42 | // This will connect the caller with your Twilio.Device/client 43 | dial.client(identity); 44 | 45 | } else if (requestBody.To) { 46 | // This is an outgoing call 47 | 48 | // set the callerId 49 | let dial = twiml.dial({ callerId }); 50 | 51 | // Check if the 'To' parameter is a Phone Number or Client Name 52 | // in order to use the appropriate TwiML noun 53 | const attr = isAValidPhoneNumber(toNumberOrClientName) 54 | ? "number" 55 | : "client"; 56 | dial[attr]({}, toNumberOrClientName); 57 | } else { 58 | twiml.say("Thanks for calling!"); 59 | } 60 | 61 | return twiml.toString(); 62 | }; 63 | 64 | /** 65 | * Checks if the given value is valid as phone number 66 | * @param {Number|String} number 67 | * @return {Boolean} 68 | */ 69 | function isAValidPhoneNumber(number) { 70 | return /^[\d\+\-\(\) ]+$/.test(number); 71 | } 72 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | const Router = require("express").Router; 2 | const { tokenGenerator, voiceResponse } = require("./handler"); 3 | 4 | const router = new Router(); 5 | 6 | router.get("/token", (req, res) => { 7 | res.send(tokenGenerator()); 8 | }); 9 | 10 | router.post("/voice", (req, res) => { 11 | res.set("Content-Type", "text/xml"); 12 | res.send(voiceResponse(req.body)); 13 | }); 14 | 15 | module.exports = router; 16 | -------------------------------------------------------------------------------- /tests/handler.test.js: -------------------------------------------------------------------------------- 1 | const jwt = require("jsonwebtoken"); 2 | const { tokenGenerator, voiceResponse } = require("../src/handler"); 3 | 4 | const when = describe; 5 | 6 | describe("#tokenGenerator", () => { 7 | it("generates a new token", () => { 8 | const token = tokenGenerator(); 9 | const decoded = jwt.decode(token.token, { complete: true }); 10 | 11 | expect(decoded.payload).toHaveProperty("jti"); 12 | expect(decoded.payload).toHaveProperty("grants"); 13 | expect(decoded.payload.grants.identity).toBe(token.identity); 14 | }); 15 | }); 16 | 17 | describe("#voiceResponse", () => { 18 | when("receives an empty or no value value", () => { 19 | it("returns a goodbye message", () => { 20 | const twiml = voiceResponse(); 21 | const count = countWord(twiml); 22 | 23 | // TwiML Verbs 24 | expect(count("Say")).toBe(2); 25 | 26 | // TwiML content 27 | expect(twiml).toContain("Thanks for calling!"); 28 | }); 29 | }); 30 | 31 | when("receives a value as string", () => { 32 | it("returns a dial verb with the client attribute", () => { 33 | const toNumber = "BigBoss"; 34 | const twiml = voiceResponse(toNumber); 35 | const count = countWord(twiml); 36 | 37 | // TwiML Verbs 38 | expect(count("Dial")).toBe(2); 39 | expect(count("Client")).toBe(2); 40 | 41 | // TwiML options 42 | expect(twiml).toContain(toNumber); 43 | }); 44 | }); 45 | 46 | when("receives a valid phone number", () => { 47 | it("returns a dial verb with the number attribute", () => { 48 | const toNumber = "+1235555555"; 49 | const twiml = voiceResponse(toNumber); 50 | const count = countWord(twiml); 51 | 52 | // TwiML Verbs 53 | expect(count("Dial")).toBe(2); 54 | expect(count("Number")).toBe(2); 55 | 56 | // TwiML options 57 | expect(twiml).toContain(toNumber); 58 | }); 59 | }); 60 | }); 61 | 62 | /** 63 | * Counts how many times a word is repeated 64 | * @param {String} paragraph 65 | * @return {String[]} 66 | */ 67 | function countWord(paragraph) { 68 | return (word) => { 69 | const regex = new RegExp(`\<${word}[ | \/?\>]|\<\/${word}?\>`); 70 | return paragraph.split(regex).length - 1; 71 | }; 72 | } 73 | --------------------------------------------------------------------------------