├── .dockerignore ├── .env.example ├── .env.example.ps1 ├── .github └── workflows │ └── flask.yml ├── .gitignore ├── .mergify.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── app.py ├── docker-compose.yml ├── requirements.txt ├── screenshots ├── BrowserToBrowserCall.png ├── ConfigurePhoneNumber.png ├── ConfigurePhoneNumberWithTwiMLApps.png ├── Homepage.png ├── InitializeDevice.png ├── UnknownDevices.png └── UpdateRequestURL.png ├── static ├── index.html ├── package-lock.json ├── package.json ├── quickstart.js ├── site.css └── twilio.min.js └── tests ├── conftest.py └── test_routes.py /.dockerignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | -------------------------------------------------------------------------------- /.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 | # OR with the Twilio CLI 12 | # twilio api:core:applications:create --friendly-name=voice-client-javascript 13 | TWILIO_TWIML_APP_SID=APXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 14 | 15 | # Your REST API Key information 16 | # Create a new key in the console and copy the SID and secret. 17 | # https://www.twilio.com/console/project/api-keys 18 | # NOTE: Make sure to copy the secret, it will only be displayed once 19 | API_KEY=SKXXXXXXXXXXXX # the SID of the key 20 | API_SECRET=XXXXXXXXXXXXXX 21 | -------------------------------------------------------------------------------- /.env.example.ps1: -------------------------------------------------------------------------------- 1 | $Env:TWILIO_ACCOUNT_SID = "ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" 2 | $Env:TWILIO_TWIML_APP_SID = "APXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" 3 | $Env:TWILIO_CALLER_ID = "+1XXXYYYZZZZ" 4 | $Env:API_KEY = "SKXXXXXXXXXXXX" 5 | $Env:API_SECRET = "XXXXXXXXXXXXXX" 6 | 7 | # Uncomment the following if you'd like the environment variables 8 | # to be permanently set on your user account for this machine. 9 | 10 | <# 11 | 12 | [Environment]::SetEnvironmentVariable("TWILIO_ACCOUNT_SID", $Env:TWILIO_ACCOUNT_SID, "User") 13 | [Environment]::SetEnvironmentVariable("TWILIO_TWIML_APP_SID", $Env:TWILIO_TWIML_APP_SID, "User") 14 | [Environment]::SetEnvironmentVariable("TWILIO_CALLER_ID", $Env:TWILIO_CALLER_ID, "User") 15 | [Environment]::SetEnvironmentVariable("API_KEY", $Env:API_KEY, "User") 16 | [Environment]::SetEnvironmentVariable("API_SECRET", $Env:API_SECRET, "User") 17 | 18 | #> -------------------------------------------------------------------------------- /.github/workflows/flask.yml: -------------------------------------------------------------------------------- 1 | name: Flask 2 | 3 | on: 4 | push: 5 | branches: [ master, next ] 6 | pull_request: 7 | branches: [ master, next ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ${{ matrix.platform }} 13 | strategy: 14 | max-parallel: 4 15 | matrix: 16 | python-version: [3.6, 3.7, 3.8] 17 | platform: [windows-latest, macos-latest, ubuntu-latest] 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Set up Python ${{ matrix.python-version }} 22 | uses: actions/setup-python@v1 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | - name: Install Dependencies 26 | run: | 27 | python -m pip install --upgrade pip 28 | pip install -r requirements.txt 29 | - name: Run Tests 30 | run: | 31 | python -m pytest 32 | env: 33 | TWILIO_ACCOUNT_SID: ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 34 | TWILIO_CALLER_ID: +1XXXYYYZZZZ 35 | TWILIO_TWIML_APP_SID: APXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 36 | API_KEY: SKXXXXXXXXXXXX 37 | API_SECRET: XXXXXXXXXXXXXX 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | *.pyo 4 | env 5 | env* 6 | dist 7 | build 8 | *.egg 9 | *.egg-info 10 | _mailinglist 11 | .tox 12 | .env 13 | .env.ps1 14 | venv 15 | .vscode 16 | __pycache__ 17 | .pytest_cache 18 | .tool-versions 19 | node_modules/ 20 | -------------------------------------------------------------------------------- /.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 (3.6, macos-latest) 6 | - status-success=build (3.7, macos-latest) 7 | - status-success=build (3.8, macos-latest) 8 | - status-success=build (3.6, windows-latest) 9 | - status-success=build (3.7, windows-latest) 10 | - status-success=build (3.8, windows-latest) 11 | - status-success=build (3.6, ubuntu-latest) 12 | - status-success=build (3.7, ubuntu-latest) 13 | - status-success=build (3.8, ubuntu-latest) 14 | actions: 15 | merge: 16 | method: squash 17 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY requirements.txt ./ 6 | 7 | COPY Makefile ./ 8 | 9 | RUN make install 10 | 11 | COPY . . 12 | 13 | EXPOSE 5000 14 | 15 | CMD ["sh", "-c", ". /usr/src/app/venv/bin/activate && make serve"] 16 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: venv install serve 2 | UNAME := $(shell uname) 3 | 4 | venv: 5 | ifeq ($(UNAME), Windows) 6 | py -3 -m venv venv; 7 | else 8 | python3 -m venv venv 9 | endif 10 | 11 | install: venv 12 | ifeq ($(UNAME), Windows) 13 | venv\Scripts\activate.bat; \ 14 | pip3 install -r requirements.txt; 15 | else 16 | . venv/bin/activate; \ 17 | pip3 install -r requirements.txt; 18 | endif 19 | 20 | serve: 21 | ifeq ($(UNAME), Windows) 22 | venv\Scripts\activate.bat; \ 23 | python3 app.py 24 | else 25 | . venv/bin/activate; \ 26 | python3 app.py 27 | endif 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Twilio 3 | 4 | 5 | # Twilio Voice JavaScript SDK Quickstart for Python 6 | 7 | ![](https://github.com/TwilioDevEd/client-quickstart-python/workflows/Flask/badge.svg) 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 the Twilio Voice JavaScript SDK (formerly known as Twilio Client). 14 | 15 | This application uses the lightweight [Flask Framework](http://flask.pocoo.org/). Once you set up the application, you will be able to make and receive calls from your browser. You will also be able to switch between audio input/output devices, and see dynamic volume levels on the call. 16 | 17 | ![screenshot of application homepage](./screenshots/Homepage.png) 18 | 19 | Implementations in other languages: 20 | 21 | | .NET | Java | Node | PHP | Ruby | 22 | | :--- | :--- | :----- | :-- | :--- | 23 | | [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-node) | [Done](https://github.com/TwilioDevEd/voice-javascript-sdk-quickstart-php) | [Done](https://github.com/TwilioDevEd/voice-javascript-sdk-quickstart-ruby) | 24 | 25 | ## Set Up 26 | 27 | ### Requirements 28 | 29 | - [Python](https://www.python.org/) **3.6**, **3.7**, **3.8**, or **3.9** version. 30 | - [ngrok](https://ngrok.com/download) 31 | 32 | ### Twilio Account Settings 33 | 34 | Before we begin, we need to collect all the config values we need to run the application. 35 | 36 | | Config Value | Description | 37 | | :------------- |:------------- | 38 | `TWILIO_ACCOUNT_SID` | Your primary Twilio account identifier - find this [in the console here](https://www.twilio.com/console). 39 | `TWILIO_TWIML_APP_SID` | The TwiML application with a voice URL configured to access your server running this app - create one [in the console here](https://www.twilio.com/console/voice/twiml/apps). Also, you will need to configure the Voice "REQUEST URL" on the TwiML app once you've got your server up and running. 40 | `TWILIO_CALLER_ID` | A Twilio phone number in [E.164 format](https://www.twilio.com/docs/glossary/what-e164) - you can [get one here](https://www.twilio.com/console/phone-numbers/incoming) 41 | `API_KEY` / `API_SECRET` | Your REST API Key information needed to create an [Access Token](https://www.twilio.com/docs/iam/access-tokens) - create [an API key here](https://www.twilio.com/console/project/api-keys). The `API_KEY` value should be the key's `SID`. 42 | 43 | ### Local development 44 | 45 | 1. First, clone this repository and `cd` into it. 46 | 47 | ```bash 48 | git clone https://github.com/TwilioDevEd/voice-javascript-sdk-quickstart-python.git 49 | cd voice-javascript-sdk-quickstart-python 50 | ``` 51 | 52 | 2. Run `make install`. This command will create a Python virtual environment, load it, and install the Python dependencies. 53 | 54 | ```bash 55 | make install 56 | ``` 57 | 58 | 3. Download the Twilio Voice JavaScript SDK code from GitHub. 59 | 60 | In a production environment, we recommend using `npm` to install the SDK. However, for the purposes of this quickstart, 61 | we are not introducing Node or build tools, and are instead getting the SDK code directly from GitHub. 62 | 63 | See the instructions [here](https://github.com/twilio/twilio-voice.js#github) for downloading the SDK code from GitHub. 64 | You will download a zip or tarball for a specific release version of the Voice JavaScript SDK (ex: `2.0.0`), extract the 65 | files, and retrieve the `twilio.min.js` file from the `dist/` folder. Move that `twilio.min.js` file into this project's `static/` directory. 66 | 67 | 4. Create a configuration file for your application by copying the `.env.example` file to a new file called `.env`. Then, edit the `.env` file to include your account and application details. 68 | 69 | ```bash 70 | cp .env.example .env 71 | ``` 72 | 73 | See [Twilio Account Settings](#twilio-account-settings) to locate the necessary environment variables. 74 | 75 | #### Windows (PowerShell) 76 | 77 | Begin by creating a configuration file for your application: 78 | ```powershell 79 | cp .env.example.ps1 .env.ps1 80 | ``` 81 | 82 | Edit `.env.ps1` with the four configuration parameters we gathered from above. 83 | "Dot-source" the file in PowerShell like so: 84 | ```powershell 85 | . .\.env.ps1 86 | ``` 87 | This assumes you will run the application in the same PowerShell session. If not, 88 | edit the `.env.ps1` and uncomment the `[Environment]::SetEnvironmentVariable` calls. 89 | After re-running the script, the environment variables will be peramently set for 90 | your user account. 91 | 92 | 5. Run the application. It will run locally on port 5000. 93 | 94 | ```bash 95 | make serve 96 | ``` 97 | 98 | 6. Navigate to [http://localhost:5000](http://localhost:5000) 99 | 100 | 7. Expose your application to the wider internet using [ngrok](https://ngrok.com/download). You can click [here](https://www.twilio.com/blog/2015/09/6-awesome-reasons-to-use-ngrok-when-testing-webhooks.html) for more details. This step **is important** and your application won't work if you only run the server on localhost. 101 | 102 | ```bash 103 | ngrok http 5000 104 | ``` 105 | 106 | 8. When ngrok starts up, it will assign a unique URL to your tunnel. 107 | It might be something like `https://asdf456.ngrok.io`. Take note of this. 108 | 109 | 9. [Configure your TwiML app](https://www.twilio.com/console/voice/twiml/apps)'s 110 | Voice "REQUEST URL" to be your ngrok URL plus `/voice`. For example: 111 | 112 | ![screenshot of twiml app](./screenshots/UpdateRequestURL.png) 113 | 114 | > **Note:** You must set your webhook urls to the `https` ngrok tunnel created. 115 | 116 | You should now be ready to rock! Make some phone calls or receiving incoming calls in the application. 117 | Note that Twilio Client requires WebRTC enabled browsers, so Edge and Internet Explorer will not work for testing. 118 | We'd recommend Google Chrome or Mozilla Firefox instead. 119 | 120 | ## Your Web Application 121 | 122 | When you navigate to `localhost:5000`, you should see the web application containing a "Start up the Device" button. Click this button to initialize a `Twilio.Device`. 123 | 124 | ![screenshot of application homepage](./screenshots/InitializeDevice.png) 125 | 126 | When the `Twilio.Device` is initialized, you will be assigned a random client name, which will appear in the top left corner of the homepage. 127 | This client name is used as the identity field when generating an access token for the client, and is also used to route incoming calls to the correct client device. 128 | 129 | ### To make an outbound call to a phone number: 130 | 131 | Under "Make a Call", enter a phone number in [E.164 format](https://www.twilio.com/docs/glossary/what-e164) and press the "Call" button. 132 | 133 | ### To make a browser-to-browser call: 134 | 135 | Open two browser windows to `localhost:5000` and click "Start up the Device" button in both windows. You should see a different client name in each window. 136 | 137 | Enter one client's name in the other client's "Make a Call" input field, and press the "Call" button. 138 | 139 | ![screenshot of browser-to-browser calling](./screenshots/BrowserToBrowserCall.png) 140 | 141 | ### Receiving incoming calls from a non-browser device: 142 | 143 | You will first need to configure your Twilio Voice phone number (the phone number you used as the `TWILIO_CALLER_ID` configuration value) to route incoming calls to your TwiML app. This tells Twilio how to handle an incoming call directed to your Twilio Voice number. 144 | 145 | 1. Log in to the [Twilio Console](https://www.twilio.com/console) 146 | 2. Navigate to your [Active Number list](https://www.twilio.com/console/phone-numbers/incoming) 147 | 3. Click on the number you are using as your `TWILIO_CALLER_ID`. 148 | 4. Scroll down to find the "Voice & Fax" section and look for "CONFIGURE WITH". 149 | 5. Select "TwiML App". 150 | 6. Under "TwiML App", choose the TwiML App you created earlier for this quickstart. 151 | 7. Click the "Save" button at the bottom of the browser window. 152 | 153 | ![screenshot of configuring phone number for incoming calls](./screenshots/ConfigurePhoneNumberWithTwiMLApps.png) 154 | 155 | You can now call your Twilio Voice phone number from your phone. 156 | 157 | **Note:** Since this is a quickstart with limited functionality, incoming calls will only be routed to your most recently created `Twilio.Device`. 158 | 159 | ### Unknown Devices 160 | 161 | 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. 162 | ![screenshot of unknown devices](./screenshots/UnknownDevices.png) 163 | 164 | ### Docker 165 | 166 | If you have [Docker](https://www.docker.com/) already installed on your machine, you can use our `docker-compose.yml` to setup your project. 167 | 168 | 1. Make sure you have the project cloned and that Docker is running on your machine. 169 | 2. Retrieve the `twilio.min.js` file and move it to the `static` directory as outlined in Step 3 of the [Local Development](#local-development) steps. 170 | 3. Setup the `.env` file as outlined in Step 4 of the [Local Development](#local-development) steps. 171 | 4. Run `docker-compose up`. 172 | 5. Follow the steps in [Local Development](#local-development) on how to expose your port to Twilio using [ngrok](https://ngrok.com/) and configure the remaining parts of your application. 173 | 174 | ### Tests 175 | 176 | You can run the tests locally with the following command. Before running, make sure the virtual environment is activated. 177 | 178 | ```bash 179 | source venv/bin/activate 180 | python3 -m pytest 181 | ``` 182 | 183 | ### Cloud deployment 184 | 185 | Additionally to trying out this application locally, you can deploy it to a variety of host services. Here is a small selection of them. 186 | 187 | Please be aware that some of these might charge you for the usage or might make the source code for this application visible to the public. When in doubt research the respective hosting service first. 188 | 189 | | Service | | 190 | | :-------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | 191 | | [Heroku](https://www.heroku.com/) | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) | 192 | 193 | ## Resources 194 | 195 | - The CodeExchange repository can be found [here](https://github.com/twilio-labs/code-exchange/). 196 | 197 | ## Contributing 198 | 199 | 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). 200 | 201 | ## License 202 | 203 | [MIT](http://www.opensource.org/licenses/mit-license.html) 204 | 205 | ## Disclaimer 206 | 207 | No warranty expressed or implied. Software is as is. 208 | 209 | [twilio]: https://www.twilio.com 210 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import re 5 | 6 | from dotenv import load_dotenv 7 | from faker import Faker 8 | from flask import Flask, Response, jsonify, redirect, request 9 | from twilio.jwt.access_token import AccessToken 10 | from twilio.jwt.access_token.grants import VoiceGrant 11 | from twilio.twiml.voice_response import Dial, VoiceResponse 12 | 13 | load_dotenv() 14 | 15 | app = Flask(__name__) 16 | fake = Faker() 17 | alphanumeric_only = re.compile("[\W_]+") 18 | phone_pattern = re.compile(r"^[\d\+\-\(\) ]+$") 19 | 20 | twilio_number = os.environ.get("TWILIO_CALLER_ID") 21 | 22 | # Store the most recently created identity in memory for routing calls 23 | IDENTITY = {"identity": ""} 24 | 25 | 26 | @app.route("/") 27 | def index(): 28 | return app.send_static_file("index.html") 29 | 30 | 31 | @app.route("/token", methods=["GET"]) 32 | def token(): 33 | # get credentials for environment variables 34 | account_sid = os.environ["TWILIO_ACCOUNT_SID"] 35 | application_sid = os.environ["TWILIO_TWIML_APP_SID"] 36 | api_key = os.environ["API_KEY"] 37 | api_secret = os.environ["API_SECRET"] 38 | 39 | # Generate a random user name and store it 40 | identity = alphanumeric_only.sub("", fake.user_name()) 41 | IDENTITY["identity"] = identity 42 | 43 | # Create access token with credentials 44 | token = AccessToken(account_sid, api_key, api_secret, identity=identity) 45 | 46 | # Create a Voice grant and add to token 47 | voice_grant = VoiceGrant( 48 | outgoing_application_sid=application_sid, 49 | incoming_allow=True, 50 | ) 51 | token.add_grant(voice_grant) 52 | 53 | # Return token info as JSON 54 | token = token.to_jwt() 55 | 56 | # Return token info as JSON 57 | return jsonify(identity=identity, token=token) 58 | 59 | 60 | @app.route("/voice", methods=["POST"]) 61 | def voice(): 62 | resp = VoiceResponse() 63 | if request.form.get("To") == twilio_number: 64 | # Receiving an incoming call to our Twilio number 65 | dial = Dial() 66 | # Route to the most recently created client based on the identity stored in the session 67 | dial.client(IDENTITY["identity"]) 68 | resp.append(dial) 69 | elif request.form.get("To"): 70 | # Placing an outbound call from the Twilio client 71 | dial = Dial(caller_id=twilio_number) 72 | # wrap the phone number or client name in the appropriate TwiML verb 73 | # by checking if the number given has only digits and format symbols 74 | if phone_pattern.match(request.form["To"]): 75 | dial.number(request.form["To"]) 76 | else: 77 | dial.client(request.form["To"]) 78 | resp.append(dial) 79 | else: 80 | resp.say("Thanks for calling!") 81 | 82 | return Response(str(resp), mimetype="text/xml") 83 | 84 | 85 | if __name__ == "__main__": 86 | app.run(debug=True, host="0.0.0.0") 87 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | services: 3 | app: 4 | container_name: app 5 | restart: always 6 | build: . 7 | ports: 8 | - "5000:5000" 9 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flask~=2.1.3 2 | twilio~=6.38.1 3 | faker~=4.0.3 4 | python-dotenv==0.13.0 5 | pytest==5.4.1 6 | -------------------------------------------------------------------------------- /screenshots/BrowserToBrowserCall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/voice-javascript-sdk-quickstart-python/37d091663c0758d58f09c88040d5ba3ce3878407/screenshots/BrowserToBrowserCall.png -------------------------------------------------------------------------------- /screenshots/ConfigurePhoneNumber.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/voice-javascript-sdk-quickstart-python/37d091663c0758d58f09c88040d5ba3ce3878407/screenshots/ConfigurePhoneNumber.png -------------------------------------------------------------------------------- /screenshots/ConfigurePhoneNumberWithTwiMLApps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/voice-javascript-sdk-quickstart-python/37d091663c0758d58f09c88040d5ba3ce3878407/screenshots/ConfigurePhoneNumberWithTwiMLApps.png -------------------------------------------------------------------------------- /screenshots/Homepage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/voice-javascript-sdk-quickstart-python/37d091663c0758d58f09c88040d5ba3ce3878407/screenshots/Homepage.png -------------------------------------------------------------------------------- /screenshots/InitializeDevice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/voice-javascript-sdk-quickstart-python/37d091663c0758d58f09c88040d5ba3ce3878407/screenshots/InitializeDevice.png -------------------------------------------------------------------------------- /screenshots/UnknownDevices.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/voice-javascript-sdk-quickstart-python/37d091663c0758d58f09c88040d5ba3ce3878407/screenshots/UnknownDevices.png -------------------------------------------------------------------------------- /screenshots/UpdateRequestURL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/voice-javascript-sdk-quickstart-python/37d091663c0758d58f09c88040d5ba3ce3878407/screenshots/UpdateRequestURL.png -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Twilio Voice JavaScript SDK Quickstart 5 | 6 | 7 | 8 |
9 |

Twilio Voice JavaScript SDK Quickstart

10 | 11 |
12 |
13 |
14 |

Your Device Info

15 |
16 |
17 | 18 | 19 | 20 |
22 | 23 |
24 |
25 |
26 |

Make a Call

27 |
28 |
29 | 32 | 33 | 34 |
35 | 36 |
37 |

Incoming Call Controls

38 |

39 | Incoming Call from 40 |

41 | 42 | 43 | 44 |
45 |
46 | 47 |
48 |

49 | 50 |
51 |
52 |
53 |
54 |
55 |

Event Log

56 |
57 |
58 |
59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /static/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twilio-voice-sdk-flask", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "twilio-voice-sdk-flask", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@twilio/voice-sdk": "git+https://github.com/twilio/twilio-voice.js.git#2.0.0" 13 | } 14 | }, 15 | "node_modules/@twilio/audioplayer": { 16 | "version": "1.0.6", 17 | "resolved": "https://registry.npmjs.org/@twilio/audioplayer/-/audioplayer-1.0.6.tgz", 18 | "integrity": "sha512-c9cjX/ifICgXqShtyAQdVMqfe7odnxougiuRMXBJtn3dZ320mFdt7kmuKedpNnc3ZJ6irOZ9M9MZi9/vuEqHiw==", 19 | "dependencies": { 20 | "babel-runtime": "^6.26.0" 21 | } 22 | }, 23 | "node_modules/@twilio/voice-errors": { 24 | "version": "1.1.1", 25 | "resolved": "https://registry.npmjs.org/@twilio/voice-errors/-/voice-errors-1.1.1.tgz", 26 | "integrity": "sha512-3IJzRhgAqsS3uW2PO7crUXEFxuFhggHeLvt/Q4hz7lrTLFChl37hWiImCMIaM5VHiybQi6ECVQsId2X8UdTr2A==", 27 | "dependencies": { 28 | "npm-run-all": "^4.1.5" 29 | } 30 | }, 31 | "node_modules/@twilio/voice-sdk": { 32 | "version": "2.0.0", 33 | "resolved": "git+ssh://git@github.com/twilio/twilio-voice.js.git#4e7135bb029ac00b90b9fd136f3c0914159402eb", 34 | "license": "Apache-2.0", 35 | "dependencies": { 36 | "@twilio/audioplayer": "1.0.6", 37 | "@twilio/voice-errors": "1.1.1", 38 | "backoff": "2.5.0", 39 | "loglevel": "1.6.7", 40 | "rtcpeerconnection-shim": "1.2.8", 41 | "ws": "6.1.3", 42 | "xmlhttprequest": "1.8.0" 43 | } 44 | }, 45 | "node_modules/ansi-styles": { 46 | "version": "3.2.1", 47 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 48 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 49 | "dependencies": { 50 | "color-convert": "^1.9.0" 51 | }, 52 | "engines": { 53 | "node": ">=4" 54 | } 55 | }, 56 | "node_modules/async-limiter": { 57 | "version": "1.0.1", 58 | "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", 59 | "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" 60 | }, 61 | "node_modules/babel-runtime": { 62 | "version": "6.26.0", 63 | "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", 64 | "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", 65 | "dependencies": { 66 | "core-js": "^2.4.0", 67 | "regenerator-runtime": "^0.11.0" 68 | } 69 | }, 70 | "node_modules/backoff": { 71 | "version": "2.5.0", 72 | "resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz", 73 | "integrity": "sha1-9hbtqdPktmuMp/ynn2lXIsX44m8=", 74 | "dependencies": { 75 | "precond": "0.2" 76 | }, 77 | "engines": { 78 | "node": ">= 0.6" 79 | } 80 | }, 81 | "node_modules/balanced-match": { 82 | "version": "1.0.2", 83 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 84 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" 85 | }, 86 | "node_modules/brace-expansion": { 87 | "version": "1.1.11", 88 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 89 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 90 | "dependencies": { 91 | "balanced-match": "^1.0.0", 92 | "concat-map": "0.0.1" 93 | } 94 | }, 95 | "node_modules/call-bind": { 96 | "version": "1.0.2", 97 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", 98 | "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", 99 | "dependencies": { 100 | "function-bind": "^1.1.1", 101 | "get-intrinsic": "^1.0.2" 102 | }, 103 | "funding": { 104 | "url": "https://github.com/sponsors/ljharb" 105 | } 106 | }, 107 | "node_modules/chalk": { 108 | "version": "2.4.2", 109 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", 110 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 111 | "dependencies": { 112 | "ansi-styles": "^3.2.1", 113 | "escape-string-regexp": "^1.0.5", 114 | "supports-color": "^5.3.0" 115 | }, 116 | "engines": { 117 | "node": ">=4" 118 | } 119 | }, 120 | "node_modules/color-convert": { 121 | "version": "1.9.3", 122 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 123 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 124 | "dependencies": { 125 | "color-name": "1.1.3" 126 | } 127 | }, 128 | "node_modules/color-name": { 129 | "version": "1.1.3", 130 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 131 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" 132 | }, 133 | "node_modules/concat-map": { 134 | "version": "0.0.1", 135 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 136 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" 137 | }, 138 | "node_modules/core-js": { 139 | "version": "2.6.12", 140 | "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", 141 | "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", 142 | "deprecated": "core-js@<3.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Please, upgrade your dependencies to the actual version of core-js.", 143 | "hasInstallScript": true 144 | }, 145 | "node_modules/cross-spawn": { 146 | "version": "6.0.5", 147 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", 148 | "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", 149 | "dependencies": { 150 | "nice-try": "^1.0.4", 151 | "path-key": "^2.0.1", 152 | "semver": "^5.5.0", 153 | "shebang-command": "^1.2.0", 154 | "which": "^1.2.9" 155 | }, 156 | "engines": { 157 | "node": ">=4.8" 158 | } 159 | }, 160 | "node_modules/define-properties": { 161 | "version": "1.1.3", 162 | "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", 163 | "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", 164 | "dependencies": { 165 | "object-keys": "^1.0.12" 166 | }, 167 | "engines": { 168 | "node": ">= 0.4" 169 | } 170 | }, 171 | "node_modules/error-ex": { 172 | "version": "1.3.2", 173 | "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", 174 | "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", 175 | "dependencies": { 176 | "is-arrayish": "^0.2.1" 177 | } 178 | }, 179 | "node_modules/es-abstract": { 180 | "version": "1.18.3", 181 | "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.3.tgz", 182 | "integrity": "sha512-nQIr12dxV7SSxE6r6f1l3DtAeEYdsGpps13dR0TwJg1S8gyp4ZPgy3FZcHBgbiQqnoqSTb+oC+kO4UQ0C/J8vw==", 183 | "dependencies": { 184 | "call-bind": "^1.0.2", 185 | "es-to-primitive": "^1.2.1", 186 | "function-bind": "^1.1.1", 187 | "get-intrinsic": "^1.1.1", 188 | "has": "^1.0.3", 189 | "has-symbols": "^1.0.2", 190 | "is-callable": "^1.2.3", 191 | "is-negative-zero": "^2.0.1", 192 | "is-regex": "^1.1.3", 193 | "is-string": "^1.0.6", 194 | "object-inspect": "^1.10.3", 195 | "object-keys": "^1.1.1", 196 | "object.assign": "^4.1.2", 197 | "string.prototype.trimend": "^1.0.4", 198 | "string.prototype.trimstart": "^1.0.4", 199 | "unbox-primitive": "^1.0.1" 200 | }, 201 | "engines": { 202 | "node": ">= 0.4" 203 | }, 204 | "funding": { 205 | "url": "https://github.com/sponsors/ljharb" 206 | } 207 | }, 208 | "node_modules/es-to-primitive": { 209 | "version": "1.2.1", 210 | "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", 211 | "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", 212 | "dependencies": { 213 | "is-callable": "^1.1.4", 214 | "is-date-object": "^1.0.1", 215 | "is-symbol": "^1.0.2" 216 | }, 217 | "engines": { 218 | "node": ">= 0.4" 219 | }, 220 | "funding": { 221 | "url": "https://github.com/sponsors/ljharb" 222 | } 223 | }, 224 | "node_modules/escape-string-regexp": { 225 | "version": "1.0.5", 226 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 227 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 228 | "engines": { 229 | "node": ">=0.8.0" 230 | } 231 | }, 232 | "node_modules/function-bind": { 233 | "version": "1.1.1", 234 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 235 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" 236 | }, 237 | "node_modules/get-intrinsic": { 238 | "version": "1.1.1", 239 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", 240 | "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", 241 | "dependencies": { 242 | "function-bind": "^1.1.1", 243 | "has": "^1.0.3", 244 | "has-symbols": "^1.0.1" 245 | }, 246 | "funding": { 247 | "url": "https://github.com/sponsors/ljharb" 248 | } 249 | }, 250 | "node_modules/graceful-fs": { 251 | "version": "4.2.6", 252 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", 253 | "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==" 254 | }, 255 | "node_modules/has": { 256 | "version": "1.0.3", 257 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 258 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 259 | "dependencies": { 260 | "function-bind": "^1.1.1" 261 | }, 262 | "engines": { 263 | "node": ">= 0.4.0" 264 | } 265 | }, 266 | "node_modules/has-bigints": { 267 | "version": "1.0.1", 268 | "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", 269 | "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", 270 | "funding": { 271 | "url": "https://github.com/sponsors/ljharb" 272 | } 273 | }, 274 | "node_modules/has-flag": { 275 | "version": "3.0.0", 276 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 277 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", 278 | "engines": { 279 | "node": ">=4" 280 | } 281 | }, 282 | "node_modules/has-symbols": { 283 | "version": "1.0.2", 284 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", 285 | "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", 286 | "engines": { 287 | "node": ">= 0.4" 288 | }, 289 | "funding": { 290 | "url": "https://github.com/sponsors/ljharb" 291 | } 292 | }, 293 | "node_modules/hosted-git-info": { 294 | "version": "2.8.9", 295 | "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", 296 | "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==" 297 | }, 298 | "node_modules/is-arrayish": { 299 | "version": "0.2.1", 300 | "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", 301 | "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" 302 | }, 303 | "node_modules/is-bigint": { 304 | "version": "1.0.2", 305 | "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.2.tgz", 306 | "integrity": "sha512-0JV5+SOCQkIdzjBK9buARcV804Ddu7A0Qet6sHi3FimE9ne6m4BGQZfRn+NZiXbBk4F4XmHfDZIipLj9pX8dSA==", 307 | "funding": { 308 | "url": "https://github.com/sponsors/ljharb" 309 | } 310 | }, 311 | "node_modules/is-boolean-object": { 312 | "version": "1.1.1", 313 | "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.1.tgz", 314 | "integrity": "sha512-bXdQWkECBUIAcCkeH1unwJLIpZYaa5VvuygSyS/c2lf719mTKZDU5UdDRlpd01UjADgmW8RfqaP+mRaVPdr/Ng==", 315 | "dependencies": { 316 | "call-bind": "^1.0.2" 317 | }, 318 | "engines": { 319 | "node": ">= 0.4" 320 | }, 321 | "funding": { 322 | "url": "https://github.com/sponsors/ljharb" 323 | } 324 | }, 325 | "node_modules/is-callable": { 326 | "version": "1.2.3", 327 | "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz", 328 | "integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==", 329 | "engines": { 330 | "node": ">= 0.4" 331 | }, 332 | "funding": { 333 | "url": "https://github.com/sponsors/ljharb" 334 | } 335 | }, 336 | "node_modules/is-core-module": { 337 | "version": "2.4.0", 338 | "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz", 339 | "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==", 340 | "dependencies": { 341 | "has": "^1.0.3" 342 | }, 343 | "funding": { 344 | "url": "https://github.com/sponsors/ljharb" 345 | } 346 | }, 347 | "node_modules/is-date-object": { 348 | "version": "1.0.4", 349 | "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.4.tgz", 350 | "integrity": "sha512-/b4ZVsG7Z5XVtIxs/h9W8nvfLgSAyKYdtGWQLbqy6jA1icmgjf8WCoTKgeS4wy5tYaPePouzFMANbnj94c2Z+A==", 351 | "engines": { 352 | "node": ">= 0.4" 353 | }, 354 | "funding": { 355 | "url": "https://github.com/sponsors/ljharb" 356 | } 357 | }, 358 | "node_modules/is-negative-zero": { 359 | "version": "2.0.1", 360 | "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz", 361 | "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==", 362 | "engines": { 363 | "node": ">= 0.4" 364 | }, 365 | "funding": { 366 | "url": "https://github.com/sponsors/ljharb" 367 | } 368 | }, 369 | "node_modules/is-number-object": { 370 | "version": "1.0.5", 371 | "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.5.tgz", 372 | "integrity": "sha512-RU0lI/n95pMoUKu9v1BZP5MBcZuNSVJkMkAG2dJqC4z2GlkGUNeH68SuHuBKBD/XFe+LHZ+f9BKkLET60Niedw==", 373 | "engines": { 374 | "node": ">= 0.4" 375 | }, 376 | "funding": { 377 | "url": "https://github.com/sponsors/ljharb" 378 | } 379 | }, 380 | "node_modules/is-regex": { 381 | "version": "1.1.3", 382 | "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.3.tgz", 383 | "integrity": "sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==", 384 | "dependencies": { 385 | "call-bind": "^1.0.2", 386 | "has-symbols": "^1.0.2" 387 | }, 388 | "engines": { 389 | "node": ">= 0.4" 390 | }, 391 | "funding": { 392 | "url": "https://github.com/sponsors/ljharb" 393 | } 394 | }, 395 | "node_modules/is-string": { 396 | "version": "1.0.6", 397 | "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.6.tgz", 398 | "integrity": "sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w==", 399 | "engines": { 400 | "node": ">= 0.4" 401 | }, 402 | "funding": { 403 | "url": "https://github.com/sponsors/ljharb" 404 | } 405 | }, 406 | "node_modules/is-symbol": { 407 | "version": "1.0.4", 408 | "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", 409 | "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", 410 | "dependencies": { 411 | "has-symbols": "^1.0.2" 412 | }, 413 | "engines": { 414 | "node": ">= 0.4" 415 | }, 416 | "funding": { 417 | "url": "https://github.com/sponsors/ljharb" 418 | } 419 | }, 420 | "node_modules/isexe": { 421 | "version": "2.0.0", 422 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 423 | "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" 424 | }, 425 | "node_modules/json-parse-better-errors": { 426 | "version": "1.0.2", 427 | "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", 428 | "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" 429 | }, 430 | "node_modules/load-json-file": { 431 | "version": "4.0.0", 432 | "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", 433 | "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", 434 | "dependencies": { 435 | "graceful-fs": "^4.1.2", 436 | "parse-json": "^4.0.0", 437 | "pify": "^3.0.0", 438 | "strip-bom": "^3.0.0" 439 | }, 440 | "engines": { 441 | "node": ">=4" 442 | } 443 | }, 444 | "node_modules/loglevel": { 445 | "version": "1.6.7", 446 | "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.7.tgz", 447 | "integrity": "sha512-cY2eLFrQSAfVPhCgH1s7JI73tMbg9YC3v3+ZHVW67sBS7UxWzNEk/ZBbSfLykBWHp33dqqtOv82gjhKEi81T/A==", 448 | "engines": { 449 | "node": ">= 0.6.0" 450 | }, 451 | "funding": { 452 | "type": "tidelift", 453 | "url": "https://tidelift.com/subscription/pkg/npm-loglevel?utm_medium=referral&utm_source=npm_fund" 454 | } 455 | }, 456 | "node_modules/memorystream": { 457 | "version": "0.3.1", 458 | "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", 459 | "integrity": "sha1-htcJCzDORV1j+64S3aUaR93K+bI=", 460 | "engines": { 461 | "node": ">= 0.10.0" 462 | } 463 | }, 464 | "node_modules/minimatch": { 465 | "version": "3.0.4", 466 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 467 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 468 | "dependencies": { 469 | "brace-expansion": "^1.1.7" 470 | }, 471 | "engines": { 472 | "node": "*" 473 | } 474 | }, 475 | "node_modules/nice-try": { 476 | "version": "1.0.5", 477 | "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", 478 | "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" 479 | }, 480 | "node_modules/normalize-package-data": { 481 | "version": "2.5.0", 482 | "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", 483 | "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", 484 | "dependencies": { 485 | "hosted-git-info": "^2.1.4", 486 | "resolve": "^1.10.0", 487 | "semver": "2 || 3 || 4 || 5", 488 | "validate-npm-package-license": "^3.0.1" 489 | } 490 | }, 491 | "node_modules/npm-run-all": { 492 | "version": "4.1.5", 493 | "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", 494 | "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", 495 | "dependencies": { 496 | "ansi-styles": "^3.2.1", 497 | "chalk": "^2.4.1", 498 | "cross-spawn": "^6.0.5", 499 | "memorystream": "^0.3.1", 500 | "minimatch": "^3.0.4", 501 | "pidtree": "^0.3.0", 502 | "read-pkg": "^3.0.0", 503 | "shell-quote": "^1.6.1", 504 | "string.prototype.padend": "^3.0.0" 505 | }, 506 | "bin": { 507 | "npm-run-all": "bin/npm-run-all/index.js", 508 | "run-p": "bin/run-p/index.js", 509 | "run-s": "bin/run-s/index.js" 510 | }, 511 | "engines": { 512 | "node": ">= 4" 513 | } 514 | }, 515 | "node_modules/object-inspect": { 516 | "version": "1.10.3", 517 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.3.tgz", 518 | "integrity": "sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==", 519 | "funding": { 520 | "url": "https://github.com/sponsors/ljharb" 521 | } 522 | }, 523 | "node_modules/object-keys": { 524 | "version": "1.1.1", 525 | "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", 526 | "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", 527 | "engines": { 528 | "node": ">= 0.4" 529 | } 530 | }, 531 | "node_modules/object.assign": { 532 | "version": "4.1.2", 533 | "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", 534 | "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", 535 | "dependencies": { 536 | "call-bind": "^1.0.0", 537 | "define-properties": "^1.1.3", 538 | "has-symbols": "^1.0.1", 539 | "object-keys": "^1.1.1" 540 | }, 541 | "engines": { 542 | "node": ">= 0.4" 543 | }, 544 | "funding": { 545 | "url": "https://github.com/sponsors/ljharb" 546 | } 547 | }, 548 | "node_modules/parse-json": { 549 | "version": "4.0.0", 550 | "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", 551 | "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", 552 | "dependencies": { 553 | "error-ex": "^1.3.1", 554 | "json-parse-better-errors": "^1.0.1" 555 | }, 556 | "engines": { 557 | "node": ">=4" 558 | } 559 | }, 560 | "node_modules/path-key": { 561 | "version": "2.0.1", 562 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", 563 | "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", 564 | "engines": { 565 | "node": ">=4" 566 | } 567 | }, 568 | "node_modules/path-parse": { 569 | "version": "1.0.7", 570 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", 571 | "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" 572 | }, 573 | "node_modules/path-type": { 574 | "version": "3.0.0", 575 | "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", 576 | "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", 577 | "dependencies": { 578 | "pify": "^3.0.0" 579 | }, 580 | "engines": { 581 | "node": ">=4" 582 | } 583 | }, 584 | "node_modules/pidtree": { 585 | "version": "0.3.1", 586 | "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz", 587 | "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==", 588 | "bin": { 589 | "pidtree": "bin/pidtree.js" 590 | }, 591 | "engines": { 592 | "node": ">=0.10" 593 | } 594 | }, 595 | "node_modules/pify": { 596 | "version": "3.0.0", 597 | "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", 598 | "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", 599 | "engines": { 600 | "node": ">=4" 601 | } 602 | }, 603 | "node_modules/precond": { 604 | "version": "0.2.3", 605 | "resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz", 606 | "integrity": "sha1-qpWRvKokkj8eD0hJ0kD0fvwQdaw=", 607 | "engines": { 608 | "node": ">= 0.6" 609 | } 610 | }, 611 | "node_modules/read-pkg": { 612 | "version": "3.0.0", 613 | "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", 614 | "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", 615 | "dependencies": { 616 | "load-json-file": "^4.0.0", 617 | "normalize-package-data": "^2.3.2", 618 | "path-type": "^3.0.0" 619 | }, 620 | "engines": { 621 | "node": ">=4" 622 | } 623 | }, 624 | "node_modules/regenerator-runtime": { 625 | "version": "0.11.1", 626 | "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", 627 | "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" 628 | }, 629 | "node_modules/resolve": { 630 | "version": "1.20.0", 631 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", 632 | "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", 633 | "dependencies": { 634 | "is-core-module": "^2.2.0", 635 | "path-parse": "^1.0.6" 636 | }, 637 | "funding": { 638 | "url": "https://github.com/sponsors/ljharb" 639 | } 640 | }, 641 | "node_modules/rtcpeerconnection-shim": { 642 | "version": "1.2.8", 643 | "resolved": "https://registry.npmjs.org/rtcpeerconnection-shim/-/rtcpeerconnection-shim-1.2.8.tgz", 644 | "integrity": "sha512-5Sx90FGru1sQw9aGOM+kHU4i6mbP8eJPgxliu2X3Syhg8qgDybx8dpDTxUwfJvPnubXFnZeRNl59DWr4AttJKQ==", 645 | "dependencies": { 646 | "sdp": "^2.6.0" 647 | }, 648 | "engines": { 649 | "node": ">=6.0.0", 650 | "npm": ">=3.10.0" 651 | } 652 | }, 653 | "node_modules/sdp": { 654 | "version": "2.12.0", 655 | "resolved": "https://registry.npmjs.org/sdp/-/sdp-2.12.0.tgz", 656 | "integrity": "sha512-jhXqQAQVM+8Xj5EjJGVweuEzgtGWb3tmEEpl3CLP3cStInSbVHSg0QWOGQzNq8pSID4JkpeV2mPqlMDLrm0/Vw==" 657 | }, 658 | "node_modules/semver": { 659 | "version": "5.7.1", 660 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", 661 | "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", 662 | "bin": { 663 | "semver": "bin/semver" 664 | } 665 | }, 666 | "node_modules/shebang-command": { 667 | "version": "1.2.0", 668 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", 669 | "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", 670 | "dependencies": { 671 | "shebang-regex": "^1.0.0" 672 | }, 673 | "engines": { 674 | "node": ">=0.10.0" 675 | } 676 | }, 677 | "node_modules/shebang-regex": { 678 | "version": "1.0.0", 679 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", 680 | "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", 681 | "engines": { 682 | "node": ">=0.10.0" 683 | } 684 | }, 685 | "node_modules/shell-quote": { 686 | "version": "1.7.2", 687 | "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz", 688 | "integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==" 689 | }, 690 | "node_modules/spdx-correct": { 691 | "version": "3.1.1", 692 | "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", 693 | "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", 694 | "dependencies": { 695 | "spdx-expression-parse": "^3.0.0", 696 | "spdx-license-ids": "^3.0.0" 697 | } 698 | }, 699 | "node_modules/spdx-exceptions": { 700 | "version": "2.3.0", 701 | "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", 702 | "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==" 703 | }, 704 | "node_modules/spdx-expression-parse": { 705 | "version": "3.0.1", 706 | "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", 707 | "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", 708 | "dependencies": { 709 | "spdx-exceptions": "^2.1.0", 710 | "spdx-license-ids": "^3.0.0" 711 | } 712 | }, 713 | "node_modules/spdx-license-ids": { 714 | "version": "3.0.9", 715 | "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.9.tgz", 716 | "integrity": "sha512-Ki212dKK4ogX+xDo4CtOZBVIwhsKBEfsEEcwmJfLQzirgc2jIWdzg40Unxz/HzEUqM1WFzVlQSMF9kZZ2HboLQ==" 717 | }, 718 | "node_modules/string.prototype.padend": { 719 | "version": "3.1.2", 720 | "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.2.tgz", 721 | "integrity": "sha512-/AQFLdYvePENU3W5rgurfWSMU6n+Ww8n/3cUt7E+vPBB/D7YDG8x+qjoFs4M/alR2bW7Qg6xMjVwWUOvuQ0XpQ==", 722 | "dependencies": { 723 | "call-bind": "^1.0.2", 724 | "define-properties": "^1.1.3", 725 | "es-abstract": "^1.18.0-next.2" 726 | }, 727 | "engines": { 728 | "node": ">= 0.4" 729 | }, 730 | "funding": { 731 | "url": "https://github.com/sponsors/ljharb" 732 | } 733 | }, 734 | "node_modules/string.prototype.trimend": { 735 | "version": "1.0.4", 736 | "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", 737 | "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", 738 | "dependencies": { 739 | "call-bind": "^1.0.2", 740 | "define-properties": "^1.1.3" 741 | }, 742 | "funding": { 743 | "url": "https://github.com/sponsors/ljharb" 744 | } 745 | }, 746 | "node_modules/string.prototype.trimstart": { 747 | "version": "1.0.4", 748 | "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", 749 | "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", 750 | "dependencies": { 751 | "call-bind": "^1.0.2", 752 | "define-properties": "^1.1.3" 753 | }, 754 | "funding": { 755 | "url": "https://github.com/sponsors/ljharb" 756 | } 757 | }, 758 | "node_modules/strip-bom": { 759 | "version": "3.0.0", 760 | "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", 761 | "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", 762 | "engines": { 763 | "node": ">=4" 764 | } 765 | }, 766 | "node_modules/supports-color": { 767 | "version": "5.5.0", 768 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 769 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 770 | "dependencies": { 771 | "has-flag": "^3.0.0" 772 | }, 773 | "engines": { 774 | "node": ">=4" 775 | } 776 | }, 777 | "node_modules/unbox-primitive": { 778 | "version": "1.0.1", 779 | "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", 780 | "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==", 781 | "dependencies": { 782 | "function-bind": "^1.1.1", 783 | "has-bigints": "^1.0.1", 784 | "has-symbols": "^1.0.2", 785 | "which-boxed-primitive": "^1.0.2" 786 | }, 787 | "funding": { 788 | "url": "https://github.com/sponsors/ljharb" 789 | } 790 | }, 791 | "node_modules/validate-npm-package-license": { 792 | "version": "3.0.4", 793 | "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", 794 | "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", 795 | "dependencies": { 796 | "spdx-correct": "^3.0.0", 797 | "spdx-expression-parse": "^3.0.0" 798 | } 799 | }, 800 | "node_modules/which": { 801 | "version": "1.3.1", 802 | "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", 803 | "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", 804 | "dependencies": { 805 | "isexe": "^2.0.0" 806 | }, 807 | "bin": { 808 | "which": "bin/which" 809 | } 810 | }, 811 | "node_modules/which-boxed-primitive": { 812 | "version": "1.0.2", 813 | "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", 814 | "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", 815 | "dependencies": { 816 | "is-bigint": "^1.0.1", 817 | "is-boolean-object": "^1.1.0", 818 | "is-number-object": "^1.0.4", 819 | "is-string": "^1.0.5", 820 | "is-symbol": "^1.0.3" 821 | }, 822 | "funding": { 823 | "url": "https://github.com/sponsors/ljharb" 824 | } 825 | }, 826 | "node_modules/ws": { 827 | "version": "6.1.3", 828 | "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.3.tgz", 829 | "integrity": "sha512-tbSxiT+qJI223AP4iLfQbkbxkwdFcneYinM2+x46Gx2wgvbaOMO36czfdfVUBRTHvzAMRhDd98sA5d/BuWbQdg==", 830 | "dependencies": { 831 | "async-limiter": "~1.0.0" 832 | } 833 | }, 834 | "node_modules/xmlhttprequest": { 835 | "version": "1.8.0", 836 | "resolved": "https://registry.npmjs.org/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz", 837 | "integrity": "sha1-Z/4HXFwk/vOfnWX197f+dRcZaPw=", 838 | "engines": { 839 | "node": ">=0.4.0" 840 | } 841 | } 842 | }, 843 | "dependencies": { 844 | "@twilio/audioplayer": { 845 | "version": "1.0.6", 846 | "resolved": "https://registry.npmjs.org/@twilio/audioplayer/-/audioplayer-1.0.6.tgz", 847 | "integrity": "sha512-c9cjX/ifICgXqShtyAQdVMqfe7odnxougiuRMXBJtn3dZ320mFdt7kmuKedpNnc3ZJ6irOZ9M9MZi9/vuEqHiw==", 848 | "requires": { 849 | "babel-runtime": "^6.26.0" 850 | } 851 | }, 852 | "@twilio/voice-errors": { 853 | "version": "1.1.1", 854 | "resolved": "https://registry.npmjs.org/@twilio/voice-errors/-/voice-errors-1.1.1.tgz", 855 | "integrity": "sha512-3IJzRhgAqsS3uW2PO7crUXEFxuFhggHeLvt/Q4hz7lrTLFChl37hWiImCMIaM5VHiybQi6ECVQsId2X8UdTr2A==", 856 | "requires": { 857 | "npm-run-all": "^4.1.5" 858 | } 859 | }, 860 | "@twilio/voice-sdk": { 861 | "version": "git+ssh://git@github.com/twilio/twilio-voice.js.git#4e7135bb029ac00b90b9fd136f3c0914159402eb", 862 | "from": "@twilio/voice-sdk@git+https://github.com/twilio/twilio-voice.js.git#2.0.0", 863 | "requires": { 864 | "@twilio/audioplayer": "1.0.6", 865 | "@twilio/voice-errors": "1.1.1", 866 | "backoff": "2.5.0", 867 | "loglevel": "1.6.7", 868 | "rtcpeerconnection-shim": "1.2.8", 869 | "ws": "6.1.3", 870 | "xmlhttprequest": "1.8.0" 871 | } 872 | }, 873 | "ansi-styles": { 874 | "version": "3.2.1", 875 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 876 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 877 | "requires": { 878 | "color-convert": "^1.9.0" 879 | } 880 | }, 881 | "async-limiter": { 882 | "version": "1.0.1", 883 | "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", 884 | "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" 885 | }, 886 | "babel-runtime": { 887 | "version": "6.26.0", 888 | "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", 889 | "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", 890 | "requires": { 891 | "core-js": "^2.4.0", 892 | "regenerator-runtime": "^0.11.0" 893 | } 894 | }, 895 | "backoff": { 896 | "version": "2.5.0", 897 | "resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz", 898 | "integrity": "sha1-9hbtqdPktmuMp/ynn2lXIsX44m8=", 899 | "requires": { 900 | "precond": "0.2" 901 | } 902 | }, 903 | "balanced-match": { 904 | "version": "1.0.2", 905 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 906 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" 907 | }, 908 | "brace-expansion": { 909 | "version": "1.1.11", 910 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 911 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 912 | "requires": { 913 | "balanced-match": "^1.0.0", 914 | "concat-map": "0.0.1" 915 | } 916 | }, 917 | "call-bind": { 918 | "version": "1.0.2", 919 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", 920 | "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", 921 | "requires": { 922 | "function-bind": "^1.1.1", 923 | "get-intrinsic": "^1.0.2" 924 | } 925 | }, 926 | "chalk": { 927 | "version": "2.4.2", 928 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", 929 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 930 | "requires": { 931 | "ansi-styles": "^3.2.1", 932 | "escape-string-regexp": "^1.0.5", 933 | "supports-color": "^5.3.0" 934 | } 935 | }, 936 | "color-convert": { 937 | "version": "1.9.3", 938 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 939 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 940 | "requires": { 941 | "color-name": "1.1.3" 942 | } 943 | }, 944 | "color-name": { 945 | "version": "1.1.3", 946 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 947 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" 948 | }, 949 | "concat-map": { 950 | "version": "0.0.1", 951 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 952 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" 953 | }, 954 | "core-js": { 955 | "version": "2.6.12", 956 | "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", 957 | "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==" 958 | }, 959 | "cross-spawn": { 960 | "version": "6.0.5", 961 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", 962 | "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", 963 | "requires": { 964 | "nice-try": "^1.0.4", 965 | "path-key": "^2.0.1", 966 | "semver": "^5.5.0", 967 | "shebang-command": "^1.2.0", 968 | "which": "^1.2.9" 969 | } 970 | }, 971 | "define-properties": { 972 | "version": "1.1.3", 973 | "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", 974 | "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", 975 | "requires": { 976 | "object-keys": "^1.0.12" 977 | } 978 | }, 979 | "error-ex": { 980 | "version": "1.3.2", 981 | "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", 982 | "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", 983 | "requires": { 984 | "is-arrayish": "^0.2.1" 985 | } 986 | }, 987 | "es-abstract": { 988 | "version": "1.18.3", 989 | "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.3.tgz", 990 | "integrity": "sha512-nQIr12dxV7SSxE6r6f1l3DtAeEYdsGpps13dR0TwJg1S8gyp4ZPgy3FZcHBgbiQqnoqSTb+oC+kO4UQ0C/J8vw==", 991 | "requires": { 992 | "call-bind": "^1.0.2", 993 | "es-to-primitive": "^1.2.1", 994 | "function-bind": "^1.1.1", 995 | "get-intrinsic": "^1.1.1", 996 | "has": "^1.0.3", 997 | "has-symbols": "^1.0.2", 998 | "is-callable": "^1.2.3", 999 | "is-negative-zero": "^2.0.1", 1000 | "is-regex": "^1.1.3", 1001 | "is-string": "^1.0.6", 1002 | "object-inspect": "^1.10.3", 1003 | "object-keys": "^1.1.1", 1004 | "object.assign": "^4.1.2", 1005 | "string.prototype.trimend": "^1.0.4", 1006 | "string.prototype.trimstart": "^1.0.4", 1007 | "unbox-primitive": "^1.0.1" 1008 | } 1009 | }, 1010 | "es-to-primitive": { 1011 | "version": "1.2.1", 1012 | "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", 1013 | "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", 1014 | "requires": { 1015 | "is-callable": "^1.1.4", 1016 | "is-date-object": "^1.0.1", 1017 | "is-symbol": "^1.0.2" 1018 | } 1019 | }, 1020 | "escape-string-regexp": { 1021 | "version": "1.0.5", 1022 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 1023 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" 1024 | }, 1025 | "function-bind": { 1026 | "version": "1.1.1", 1027 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 1028 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" 1029 | }, 1030 | "get-intrinsic": { 1031 | "version": "1.1.1", 1032 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", 1033 | "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", 1034 | "requires": { 1035 | "function-bind": "^1.1.1", 1036 | "has": "^1.0.3", 1037 | "has-symbols": "^1.0.1" 1038 | } 1039 | }, 1040 | "graceful-fs": { 1041 | "version": "4.2.6", 1042 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", 1043 | "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==" 1044 | }, 1045 | "has": { 1046 | "version": "1.0.3", 1047 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 1048 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 1049 | "requires": { 1050 | "function-bind": "^1.1.1" 1051 | } 1052 | }, 1053 | "has-bigints": { 1054 | "version": "1.0.1", 1055 | "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", 1056 | "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==" 1057 | }, 1058 | "has-flag": { 1059 | "version": "3.0.0", 1060 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 1061 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" 1062 | }, 1063 | "has-symbols": { 1064 | "version": "1.0.2", 1065 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", 1066 | "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==" 1067 | }, 1068 | "hosted-git-info": { 1069 | "version": "2.8.9", 1070 | "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", 1071 | "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==" 1072 | }, 1073 | "is-arrayish": { 1074 | "version": "0.2.1", 1075 | "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", 1076 | "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" 1077 | }, 1078 | "is-bigint": { 1079 | "version": "1.0.2", 1080 | "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.2.tgz", 1081 | "integrity": "sha512-0JV5+SOCQkIdzjBK9buARcV804Ddu7A0Qet6sHi3FimE9ne6m4BGQZfRn+NZiXbBk4F4XmHfDZIipLj9pX8dSA==" 1082 | }, 1083 | "is-boolean-object": { 1084 | "version": "1.1.1", 1085 | "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.1.tgz", 1086 | "integrity": "sha512-bXdQWkECBUIAcCkeH1unwJLIpZYaa5VvuygSyS/c2lf719mTKZDU5UdDRlpd01UjADgmW8RfqaP+mRaVPdr/Ng==", 1087 | "requires": { 1088 | "call-bind": "^1.0.2" 1089 | } 1090 | }, 1091 | "is-callable": { 1092 | "version": "1.2.3", 1093 | "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz", 1094 | "integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==" 1095 | }, 1096 | "is-core-module": { 1097 | "version": "2.4.0", 1098 | "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz", 1099 | "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==", 1100 | "requires": { 1101 | "has": "^1.0.3" 1102 | } 1103 | }, 1104 | "is-date-object": { 1105 | "version": "1.0.4", 1106 | "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.4.tgz", 1107 | "integrity": "sha512-/b4ZVsG7Z5XVtIxs/h9W8nvfLgSAyKYdtGWQLbqy6jA1icmgjf8WCoTKgeS4wy5tYaPePouzFMANbnj94c2Z+A==" 1108 | }, 1109 | "is-negative-zero": { 1110 | "version": "2.0.1", 1111 | "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz", 1112 | "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==" 1113 | }, 1114 | "is-number-object": { 1115 | "version": "1.0.5", 1116 | "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.5.tgz", 1117 | "integrity": "sha512-RU0lI/n95pMoUKu9v1BZP5MBcZuNSVJkMkAG2dJqC4z2GlkGUNeH68SuHuBKBD/XFe+LHZ+f9BKkLET60Niedw==" 1118 | }, 1119 | "is-regex": { 1120 | "version": "1.1.3", 1121 | "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.3.tgz", 1122 | "integrity": "sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==", 1123 | "requires": { 1124 | "call-bind": "^1.0.2", 1125 | "has-symbols": "^1.0.2" 1126 | } 1127 | }, 1128 | "is-string": { 1129 | "version": "1.0.6", 1130 | "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.6.tgz", 1131 | "integrity": "sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w==" 1132 | }, 1133 | "is-symbol": { 1134 | "version": "1.0.4", 1135 | "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", 1136 | "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", 1137 | "requires": { 1138 | "has-symbols": "^1.0.2" 1139 | } 1140 | }, 1141 | "isexe": { 1142 | "version": "2.0.0", 1143 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 1144 | "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" 1145 | }, 1146 | "json-parse-better-errors": { 1147 | "version": "1.0.2", 1148 | "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", 1149 | "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" 1150 | }, 1151 | "load-json-file": { 1152 | "version": "4.0.0", 1153 | "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", 1154 | "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", 1155 | "requires": { 1156 | "graceful-fs": "^4.1.2", 1157 | "parse-json": "^4.0.0", 1158 | "pify": "^3.0.0", 1159 | "strip-bom": "^3.0.0" 1160 | } 1161 | }, 1162 | "loglevel": { 1163 | "version": "1.6.7", 1164 | "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.7.tgz", 1165 | "integrity": "sha512-cY2eLFrQSAfVPhCgH1s7JI73tMbg9YC3v3+ZHVW67sBS7UxWzNEk/ZBbSfLykBWHp33dqqtOv82gjhKEi81T/A==" 1166 | }, 1167 | "memorystream": { 1168 | "version": "0.3.1", 1169 | "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", 1170 | "integrity": "sha1-htcJCzDORV1j+64S3aUaR93K+bI=" 1171 | }, 1172 | "minimatch": { 1173 | "version": "3.0.4", 1174 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 1175 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 1176 | "requires": { 1177 | "brace-expansion": "^1.1.7" 1178 | } 1179 | }, 1180 | "nice-try": { 1181 | "version": "1.0.5", 1182 | "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", 1183 | "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" 1184 | }, 1185 | "normalize-package-data": { 1186 | "version": "2.5.0", 1187 | "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", 1188 | "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", 1189 | "requires": { 1190 | "hosted-git-info": "^2.1.4", 1191 | "resolve": "^1.10.0", 1192 | "semver": "2 || 3 || 4 || 5", 1193 | "validate-npm-package-license": "^3.0.1" 1194 | } 1195 | }, 1196 | "npm-run-all": { 1197 | "version": "4.1.5", 1198 | "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", 1199 | "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", 1200 | "requires": { 1201 | "ansi-styles": "^3.2.1", 1202 | "chalk": "^2.4.1", 1203 | "cross-spawn": "^6.0.5", 1204 | "memorystream": "^0.3.1", 1205 | "minimatch": "^3.0.4", 1206 | "pidtree": "^0.3.0", 1207 | "read-pkg": "^3.0.0", 1208 | "shell-quote": "^1.6.1", 1209 | "string.prototype.padend": "^3.0.0" 1210 | } 1211 | }, 1212 | "object-inspect": { 1213 | "version": "1.10.3", 1214 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.3.tgz", 1215 | "integrity": "sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==" 1216 | }, 1217 | "object-keys": { 1218 | "version": "1.1.1", 1219 | "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", 1220 | "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" 1221 | }, 1222 | "object.assign": { 1223 | "version": "4.1.2", 1224 | "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", 1225 | "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", 1226 | "requires": { 1227 | "call-bind": "^1.0.0", 1228 | "define-properties": "^1.1.3", 1229 | "has-symbols": "^1.0.1", 1230 | "object-keys": "^1.1.1" 1231 | } 1232 | }, 1233 | "parse-json": { 1234 | "version": "4.0.0", 1235 | "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", 1236 | "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", 1237 | "requires": { 1238 | "error-ex": "^1.3.1", 1239 | "json-parse-better-errors": "^1.0.1" 1240 | } 1241 | }, 1242 | "path-key": { 1243 | "version": "2.0.1", 1244 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", 1245 | "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" 1246 | }, 1247 | "path-parse": { 1248 | "version": "1.0.7", 1249 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", 1250 | "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" 1251 | }, 1252 | "path-type": { 1253 | "version": "3.0.0", 1254 | "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", 1255 | "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", 1256 | "requires": { 1257 | "pify": "^3.0.0" 1258 | } 1259 | }, 1260 | "pidtree": { 1261 | "version": "0.3.1", 1262 | "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz", 1263 | "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==" 1264 | }, 1265 | "pify": { 1266 | "version": "3.0.0", 1267 | "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", 1268 | "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" 1269 | }, 1270 | "precond": { 1271 | "version": "0.2.3", 1272 | "resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz", 1273 | "integrity": "sha1-qpWRvKokkj8eD0hJ0kD0fvwQdaw=" 1274 | }, 1275 | "read-pkg": { 1276 | "version": "3.0.0", 1277 | "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", 1278 | "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", 1279 | "requires": { 1280 | "load-json-file": "^4.0.0", 1281 | "normalize-package-data": "^2.3.2", 1282 | "path-type": "^3.0.0" 1283 | } 1284 | }, 1285 | "regenerator-runtime": { 1286 | "version": "0.11.1", 1287 | "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", 1288 | "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" 1289 | }, 1290 | "resolve": { 1291 | "version": "1.20.0", 1292 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", 1293 | "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", 1294 | "requires": { 1295 | "is-core-module": "^2.2.0", 1296 | "path-parse": "^1.0.6" 1297 | } 1298 | }, 1299 | "rtcpeerconnection-shim": { 1300 | "version": "1.2.8", 1301 | "resolved": "https://registry.npmjs.org/rtcpeerconnection-shim/-/rtcpeerconnection-shim-1.2.8.tgz", 1302 | "integrity": "sha512-5Sx90FGru1sQw9aGOM+kHU4i6mbP8eJPgxliu2X3Syhg8qgDybx8dpDTxUwfJvPnubXFnZeRNl59DWr4AttJKQ==", 1303 | "requires": { 1304 | "sdp": "^2.6.0" 1305 | } 1306 | }, 1307 | "sdp": { 1308 | "version": "2.12.0", 1309 | "resolved": "https://registry.npmjs.org/sdp/-/sdp-2.12.0.tgz", 1310 | "integrity": "sha512-jhXqQAQVM+8Xj5EjJGVweuEzgtGWb3tmEEpl3CLP3cStInSbVHSg0QWOGQzNq8pSID4JkpeV2mPqlMDLrm0/Vw==" 1311 | }, 1312 | "semver": { 1313 | "version": "5.7.1", 1314 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", 1315 | "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" 1316 | }, 1317 | "shebang-command": { 1318 | "version": "1.2.0", 1319 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", 1320 | "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", 1321 | "requires": { 1322 | "shebang-regex": "^1.0.0" 1323 | } 1324 | }, 1325 | "shebang-regex": { 1326 | "version": "1.0.0", 1327 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", 1328 | "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" 1329 | }, 1330 | "shell-quote": { 1331 | "version": "1.7.2", 1332 | "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz", 1333 | "integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==" 1334 | }, 1335 | "spdx-correct": { 1336 | "version": "3.1.1", 1337 | "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", 1338 | "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", 1339 | "requires": { 1340 | "spdx-expression-parse": "^3.0.0", 1341 | "spdx-license-ids": "^3.0.0" 1342 | } 1343 | }, 1344 | "spdx-exceptions": { 1345 | "version": "2.3.0", 1346 | "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", 1347 | "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==" 1348 | }, 1349 | "spdx-expression-parse": { 1350 | "version": "3.0.1", 1351 | "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", 1352 | "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", 1353 | "requires": { 1354 | "spdx-exceptions": "^2.1.0", 1355 | "spdx-license-ids": "^3.0.0" 1356 | } 1357 | }, 1358 | "spdx-license-ids": { 1359 | "version": "3.0.9", 1360 | "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.9.tgz", 1361 | "integrity": "sha512-Ki212dKK4ogX+xDo4CtOZBVIwhsKBEfsEEcwmJfLQzirgc2jIWdzg40Unxz/HzEUqM1WFzVlQSMF9kZZ2HboLQ==" 1362 | }, 1363 | "string.prototype.padend": { 1364 | "version": "3.1.2", 1365 | "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.2.tgz", 1366 | "integrity": "sha512-/AQFLdYvePENU3W5rgurfWSMU6n+Ww8n/3cUt7E+vPBB/D7YDG8x+qjoFs4M/alR2bW7Qg6xMjVwWUOvuQ0XpQ==", 1367 | "requires": { 1368 | "call-bind": "^1.0.2", 1369 | "define-properties": "^1.1.3", 1370 | "es-abstract": "^1.18.0-next.2" 1371 | } 1372 | }, 1373 | "string.prototype.trimend": { 1374 | "version": "1.0.4", 1375 | "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", 1376 | "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", 1377 | "requires": { 1378 | "call-bind": "^1.0.2", 1379 | "define-properties": "^1.1.3" 1380 | } 1381 | }, 1382 | "string.prototype.trimstart": { 1383 | "version": "1.0.4", 1384 | "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", 1385 | "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", 1386 | "requires": { 1387 | "call-bind": "^1.0.2", 1388 | "define-properties": "^1.1.3" 1389 | } 1390 | }, 1391 | "strip-bom": { 1392 | "version": "3.0.0", 1393 | "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", 1394 | "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=" 1395 | }, 1396 | "supports-color": { 1397 | "version": "5.5.0", 1398 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 1399 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 1400 | "requires": { 1401 | "has-flag": "^3.0.0" 1402 | } 1403 | }, 1404 | "unbox-primitive": { 1405 | "version": "1.0.1", 1406 | "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", 1407 | "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==", 1408 | "requires": { 1409 | "function-bind": "^1.1.1", 1410 | "has-bigints": "^1.0.1", 1411 | "has-symbols": "^1.0.2", 1412 | "which-boxed-primitive": "^1.0.2" 1413 | } 1414 | }, 1415 | "validate-npm-package-license": { 1416 | "version": "3.0.4", 1417 | "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", 1418 | "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", 1419 | "requires": { 1420 | "spdx-correct": "^3.0.0", 1421 | "spdx-expression-parse": "^3.0.0" 1422 | } 1423 | }, 1424 | "which": { 1425 | "version": "1.3.1", 1426 | "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", 1427 | "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", 1428 | "requires": { 1429 | "isexe": "^2.0.0" 1430 | } 1431 | }, 1432 | "which-boxed-primitive": { 1433 | "version": "1.0.2", 1434 | "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", 1435 | "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", 1436 | "requires": { 1437 | "is-bigint": "^1.0.1", 1438 | "is-boolean-object": "^1.1.0", 1439 | "is-number-object": "^1.0.4", 1440 | "is-string": "^1.0.5", 1441 | "is-symbol": "^1.0.3" 1442 | } 1443 | }, 1444 | "ws": { 1445 | "version": "6.1.3", 1446 | "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.3.tgz", 1447 | "integrity": "sha512-tbSxiT+qJI223AP4iLfQbkbxkwdFcneYinM2+x46Gx2wgvbaOMO36czfdfVUBRTHvzAMRhDd98sA5d/BuWbQdg==", 1448 | "requires": { 1449 | "async-limiter": "~1.0.0" 1450 | } 1451 | }, 1452 | "xmlhttprequest": { 1453 | "version": "1.8.0", 1454 | "resolved": "https://registry.npmjs.org/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz", 1455 | "integrity": "sha1-Z/4HXFwk/vOfnWX197f+dRcZaPw=" 1456 | } 1457 | } 1458 | } 1459 | -------------------------------------------------------------------------------- /static/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twilio-voice-sdk-flask", 3 | "version": "1.0.0", 4 | "main": "quickstart.js", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "dependencies": { 9 | "@twilio/voice-sdk": "git+https://github.com/twilio/twilio-voice.js.git#2.0.0" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "description": "" 14 | } 15 | -------------------------------------------------------------------------------- /static/quickstart.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | const speakerDevices = document.getElementById("speaker-devices"); 3 | const ringtoneDevices = document.getElementById("ringtone-devices"); 4 | const outputVolumeBar = document.getElementById("output-volume"); 5 | const inputVolumeBar = document.getElementById("input-volume"); 6 | const volumeIndicators = document.getElementById("volume-indicators"); 7 | const callButton = document.getElementById("button-call"); 8 | const outgoingCallHangupButton = document.getElementById("button-hangup-outgoing"); 9 | const callControlsDiv = document.getElementById("call-controls"); 10 | const audioSelectionDiv = document.getElementById("output-selection"); 11 | const getAudioDevicesButton = document.getElementById("get-devices"); 12 | const logDiv = document.getElementById("log"); 13 | const incomingCallDiv = document.getElementById("incoming-call"); 14 | const incomingCallHangupButton = document.getElementById( 15 | "button-hangup-incoming" 16 | ); 17 | const incomingCallAcceptButton = document.getElementById( 18 | "button-accept-incoming" 19 | ); 20 | const incomingCallRejectButton = document.getElementById( 21 | "button-reject-incoming" 22 | ); 23 | const phoneNumberInput = document.getElementById("phone-number"); 24 | const incomingPhoneNumberEl = document.getElementById("incoming-number"); 25 | const startupButton = document.getElementById("startup-button"); 26 | 27 | let device; 28 | let token; 29 | 30 | // Event Listeners 31 | 32 | callButton.onclick = (e) => { 33 | e.preventDefault(); 34 | makeOutgoingCall(); 35 | }; 36 | getAudioDevicesButton.onclick = getAudioDevices; 37 | speakerDevices.addEventListener("change", updateOutputDevice); 38 | ringtoneDevices.addEventListener("change", updateRingtoneDevice); 39 | 40 | // SETUP STEP 1: 41 | // Browser client should be started after a user gesture 42 | // to avoid errors in the browser console re: AudioContext 43 | startupButton.addEventListener("click", startupClient); 44 | 45 | // SETUP STEP 2: Request an Access Token 46 | async function startupClient() { 47 | log("Requesting Access Token..."); 48 | 49 | try { 50 | const data = await $.getJSON("/token"); 51 | log("Got a token."); 52 | token = data.token; 53 | setClientNameUI(data.identity); 54 | intitializeDevice(); 55 | } catch (err) { 56 | console.log(err); 57 | log("An error occurred. See your browser console for more information."); 58 | } 59 | } 60 | 61 | // SETUP STEP 3: 62 | // Instantiate a new Twilio.Device 63 | function intitializeDevice() { 64 | logDiv.classList.remove("hide"); 65 | log("Initializing device"); 66 | device = new Twilio.Device(token, { 67 | logLevel: 1, 68 | // Set Opus as our preferred codec. Opus generally performs better, requiring less bandwidth and 69 | // providing better audio quality in restrained network conditions. 70 | codecPreferences: ["opus", "pcmu"] 71 | }); 72 | 73 | addDeviceListeners(device); 74 | 75 | // Device must be registered in order to receive incoming calls 76 | device.register(); 77 | } 78 | 79 | // SETUP STEP 4: 80 | // Listen for Twilio.Device states 81 | function addDeviceListeners(device) { 82 | device.on("registered", function () { 83 | log("Twilio.Device Ready to make and receive calls!"); 84 | callControlsDiv.classList.remove("hide"); 85 | }); 86 | 87 | device.on("error", function (error) { 88 | log("Twilio.Device Error: " + error.message); 89 | }); 90 | 91 | device.on("incoming", handleIncomingCall); 92 | 93 | device.audio.on("deviceChange", updateAllAudioDevices.bind(device)); 94 | 95 | // Show audio selection UI if it is supported by the browser. 96 | if (device.audio.isOutputSelectionSupported) { 97 | audioSelectionDiv.classList.remove("hide"); 98 | } 99 | } 100 | 101 | // MAKE AN OUTGOING CALL 102 | 103 | async function makeOutgoingCall() { 104 | var params = { 105 | // get the phone number to call from the DOM 106 | To: phoneNumberInput.value, 107 | }; 108 | 109 | if (device) { 110 | log(`Attempting to call ${params.To} ...`); 111 | 112 | // Twilio.Device.connect() returns a Call object 113 | const call = await device.connect({ params }); 114 | 115 | // add listeners to the Call 116 | // "accepted" means the call has finished connecting and the state is now "open" 117 | call.on("accept", updateUIAcceptedOutgoingCall); 118 | call.on("disconnect", updateUIDisconnectedOutgoingCall); 119 | call.on("cancel", updateUIDisconnectedOutgoingCall); 120 | 121 | outgoingCallHangupButton.onclick = () => { 122 | log("Hanging up ..."); 123 | call.disconnect(); 124 | }; 125 | 126 | } else { 127 | log("Unable to make call."); 128 | } 129 | } 130 | 131 | function updateUIAcceptedOutgoingCall(call) { 132 | log("Call in progress ..."); 133 | callButton.disabled = true; 134 | outgoingCallHangupButton.classList.remove("hide"); 135 | volumeIndicators.classList.remove("hide"); 136 | bindVolumeIndicators(call); 137 | } 138 | 139 | function updateUIDisconnectedOutgoingCall() { 140 | log("Call disconnected."); 141 | callButton.disabled = false; 142 | outgoingCallHangupButton.classList.add("hide"); 143 | volumeIndicators.classList.add("hide"); 144 | } 145 | 146 | // HANDLE INCOMING CALL 147 | 148 | function handleIncomingCall(call) { 149 | log(`Incoming call from ${call.parameters.From}`); 150 | 151 | //show incoming call div and incoming phone number 152 | incomingCallDiv.classList.remove("hide"); 153 | incomingPhoneNumberEl.innerHTML = call.parameters.From; 154 | 155 | //add event listeners for Accept, Reject, and Hangup buttons 156 | incomingCallAcceptButton.onclick = () => { 157 | acceptIncomingCall(call); 158 | }; 159 | 160 | incomingCallRejectButton.onclick = () => { 161 | rejectIncomingCall(call); 162 | }; 163 | 164 | incomingCallHangupButton.onclick = () => { 165 | hangupIncomingCall(call); 166 | }; 167 | 168 | // add event listener to call object 169 | call.on("cancel", handleDisconnectedIncomingCall); 170 | call.on("disconnect", handleDisconnectedIncomingCall); 171 | call.on("reject", handleDisconnectedIncomingCall); 172 | } 173 | 174 | // ACCEPT INCOMING CALL 175 | 176 | function acceptIncomingCall(call) { 177 | call.accept(); 178 | 179 | //update UI 180 | log("Accepted incoming call."); 181 | incomingCallAcceptButton.classList.add("hide"); 182 | incomingCallRejectButton.classList.add("hide"); 183 | incomingCallHangupButton.classList.remove("hide"); 184 | } 185 | 186 | // REJECT INCOMING CALL 187 | 188 | function rejectIncomingCall(call) { 189 | call.reject(); 190 | log("Rejected incoming call"); 191 | resetIncomingCallUI(); 192 | } 193 | 194 | // HANG UP INCOMING CALL 195 | 196 | function hangupIncomingCall(call) { 197 | call.disconnect(); 198 | log("Hanging up incoming call"); 199 | resetIncomingCallUI(); 200 | } 201 | 202 | // HANDLE CANCELLED INCOMING CALL 203 | 204 | function handleDisconnectedIncomingCall() { 205 | log("Incoming call ended."); 206 | resetIncomingCallUI(); 207 | } 208 | 209 | // MISC USER INTERFACE 210 | 211 | // Activity log 212 | function log(message) { 213 | logDiv.innerHTML += `

>  ${message}

`; 214 | logDiv.scrollTop = logDiv.scrollHeight; 215 | } 216 | 217 | function setClientNameUI(clientName) { 218 | var div = document.getElementById("client-name"); 219 | div.innerHTML = `Your client name: ${clientName}`; 220 | } 221 | 222 | function resetIncomingCallUI() { 223 | incomingPhoneNumberEl.innerHTML = ""; 224 | incomingCallAcceptButton.classList.remove("hide"); 225 | incomingCallRejectButton.classList.remove("hide"); 226 | incomingCallHangupButton.classList.add("hide"); 227 | incomingCallDiv.classList.add("hide"); 228 | } 229 | 230 | // AUDIO CONTROLS 231 | 232 | async function getAudioDevices() { 233 | await navigator.mediaDevices.getUserMedia({ audio: true }); 234 | updateAllAudioDevices.bind(device); 235 | } 236 | 237 | function updateAllAudioDevices() { 238 | if (device) { 239 | updateDevices(speakerDevices, device.audio.speakerDevices.get()); 240 | updateDevices(ringtoneDevices, device.audio.ringtoneDevices.get()); 241 | } 242 | } 243 | 244 | function updateOutputDevice() { 245 | const selectedDevices = Array.from(speakerDevices.children) 246 | .filter((node) => node.selected) 247 | .map((node) => node.getAttribute("data-id")); 248 | 249 | device.audio.speakerDevices.set(selectedDevices); 250 | } 251 | 252 | function updateRingtoneDevice() { 253 | const selectedDevices = Array.from(ringtoneDevices.children) 254 | .filter((node) => node.selected) 255 | .map((node) => node.getAttribute("data-id")); 256 | 257 | device.audio.ringtoneDevices.set(selectedDevices); 258 | } 259 | 260 | function bindVolumeIndicators(call) { 261 | call.on("volume", function (inputVolume, outputVolume) { 262 | var inputColor = "red"; 263 | if (inputVolume < 0.5) { 264 | inputColor = "green"; 265 | } else if (inputVolume < 0.75) { 266 | inputColor = "yellow"; 267 | } 268 | 269 | inputVolumeBar.style.width = Math.floor(inputVolume * 300) + "px"; 270 | inputVolumeBar.style.background = inputColor; 271 | 272 | var outputColor = "red"; 273 | if (outputVolume < 0.5) { 274 | outputColor = "green"; 275 | } else if (outputVolume < 0.75) { 276 | outputColor = "yellow"; 277 | } 278 | 279 | outputVolumeBar.style.width = Math.floor(outputVolume * 300) + "px"; 280 | outputVolumeBar.style.background = outputColor; 281 | }); 282 | } 283 | 284 | // Update the available ringtone and speaker devices 285 | function updateDevices(selectEl, selectedDevices) { 286 | selectEl.innerHTML = ""; 287 | 288 | device.audio.availableOutputDevices.forEach(function (device, id) { 289 | var isActive = selectedDevices.size === 0 && id === "default"; 290 | selectedDevices.forEach(function (device) { 291 | if (device.deviceId === id) { 292 | isActive = true; 293 | } 294 | }); 295 | 296 | var option = document.createElement("option"); 297 | option.label = device.label; 298 | option.setAttribute("data-id", id); 299 | if (isActive) { 300 | option.setAttribute("selected", "selected"); 301 | } 302 | selectEl.appendChild(option); 303 | }); 304 | } 305 | }); 306 | -------------------------------------------------------------------------------- /static/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: 250px; 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 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | 5 | from app import app as flask_app 6 | 7 | 8 | @pytest.fixture 9 | def app(): 10 | yield flask_app 11 | 12 | 13 | @pytest.fixture 14 | def client(app): 15 | return app.test_client() 16 | -------------------------------------------------------------------------------- /tests/test_routes.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | 3 | 4 | def test_index(app, client): 5 | res = client.get('/') 6 | assert res.status_code == 200 7 | assert 'Make a Call' in res.get_data(as_text=True) 8 | 9 | 10 | def test_voice_has_to_parameter(app, client): 11 | res = client.post('/voice', data=dict(phone='+593XXXXXXX')) 12 | assert res.status_code == 200 13 | assert '+593XXXXXXX' in res.get_data(as_text=True) 14 | 15 | 16 | def test_voice_has_not_to_parameter(app, client): 17 | res = client.post('/voice') 18 | assert res.status_code == 200 19 | assert 'Thanks for calling!' in res.get_data(as_text=True) 20 | 21 | 22 | @patch('app.fake', **{'user_name.return_value': 'nezuko'}) 23 | def test_generate_token(app, client): 24 | res = client.get('/token') 25 | assert '"identity":"nezuko"' in res.get_data(as_text=True) 26 | assert '"token"' in res.get_data(as_text=True) 27 | --------------------------------------------------------------------------------