├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── AUTHORS.md ├── CHANGELOG.md ├── Dockerfile ├── LICENSE.md ├── Makefile ├── Pipfile ├── Pipfile.lock ├── README.md ├── examples ├── laml │ ├── laml.py │ ├── make_call.py │ └── send_sms.py └── relay │ ├── call-connect.py │ ├── call-detect.py │ ├── call-fax-receive.py │ ├── call-fax-send.py │ ├── call-record.py │ ├── call-tap.py │ ├── consumer.py │ ├── create-task.py │ └── raw_client.py ├── fixtures └── cassettes │ ├── test_accounts │ ├── test_applications │ ├── test_conference_members │ ├── test_conferences │ ├── test_fetch_fax │ ├── test_fetch_fax_media │ ├── test_fetch_fax_media_instance │ ├── test_incoming_phone_numbers │ ├── test_list_fax │ ├── test_local_numbers │ ├── test_media │ ├── test_messages │ ├── test_queue_members │ ├── test_queues │ ├── test_recordings │ ├── test_send_fax │ ├── test_toll_free_numbers │ └── test_transcriptions ├── pyproject.toml ├── setup.py └── signalwire ├── __init__.py ├── blade ├── __init__.py ├── connection.py ├── handler.py ├── helpers.py └── messages │ ├── __init__.py │ ├── connect.py │ ├── execute.py │ ├── message.py │ ├── ping.py │ └── subscription.py ├── fax_response └── __init__.py ├── messaging_response └── __init__.py ├── relay ├── __init__.py ├── calling │ ├── __init__.py │ ├── actions │ │ ├── __init__.py │ │ ├── connect_action.py │ │ ├── detect_action.py │ │ ├── fax_action.py │ │ ├── play_action.py │ │ ├── prompt_action.py │ │ ├── record_action.py │ │ ├── send_digits_action.py │ │ └── tap_action.py │ ├── call.py │ ├── components │ │ ├── __init__.py │ │ ├── answer.py │ │ ├── awaiter.py │ │ ├── base_fax.py │ │ ├── connect.py │ │ ├── decorators.py │ │ ├── detect.py │ │ ├── dial.py │ │ ├── disconnect.py │ │ ├── fax_receive.py │ │ ├── fax_send.py │ │ ├── hangup.py │ │ ├── play.py │ │ ├── prompt.py │ │ ├── record.py │ │ ├── send_digits.py │ │ └── tap.py │ ├── constants.py │ ├── helpers.py │ └── results │ │ ├── __init__.py │ │ ├── answer_result.py │ │ ├── connect_result.py │ │ ├── detect_result.py │ │ ├── dial_result.py │ │ ├── disconnect_result.py │ │ ├── fax_result.py │ │ ├── hangup_result.py │ │ ├── play_result.py │ │ ├── prompt_result.py │ │ ├── record_result.py │ │ ├── send_digits_result.py │ │ ├── stop_result.py │ │ └── tap_result.py ├── client.py ├── constants.py ├── consumer.py ├── event.py ├── helpers.py ├── message_handler.py ├── messaging │ ├── __init__.py │ ├── constants.py │ ├── message.py │ └── send_result.py ├── task.py └── tasking │ └── __init__.py ├── request_validator.py ├── rest └── __init__.py ├── tests ├── __init__.py ├── blade │ ├── test_blade_messages.py │ └── test_handler.py ├── relay │ ├── calling │ │ ├── conftest.py │ │ ├── test_actions.py │ │ ├── test_call.py │ │ ├── test_call_connect.py │ │ ├── test_call_detect.py │ │ ├── test_call_disconnect.py │ │ ├── test_call_fax_receive.py │ │ ├── test_call_fax_send.py │ │ ├── test_call_play.py │ │ ├── test_call_prompt.py │ │ ├── test_call_record.py │ │ ├── test_call_send_digits.py │ │ ├── test_call_tap.py │ │ ├── test_call_wait_for.py │ │ └── test_calling.py │ ├── conftest.py │ ├── messaging │ │ ├── conftest.py │ │ └── test_messaging.py │ ├── tasking │ │ ├── conftest.py │ │ └── test_tasking.py │ ├── test_client.py │ ├── test_consumer.py │ └── test_helpers.py ├── test_fax_response.py ├── test_messaging_response.py ├── test_request_validator.py ├── test_requests.py ├── test_sdk.py └── test_voice_response.py └── voice_response └── __init__.py /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | 2 | version: 2 3 | 4 | updates: 5 | # Maintain dependencies for GitHub Actions 6 | - package-ecosystem: "github-actions" 7 | directory: "/" 8 | schedule: 9 | interval: "daily" 10 | 11 | - package-ecosystem: "pip" 12 | directory: "/" 13 | schedule: 14 | interval: "daily" 15 | allow: 16 | - dependency-type: "all" 17 | 18 | - package-ecosystem: "docker" 19 | directory: "/" 20 | schedule: 21 | interval: "daily" -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Run Test 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout Code 14 | uses: actions/checkout@v4 15 | - name: Build Image 16 | run: docker build -t signalwire-python-sdk . 17 | - name: Run Tests 18 | run: docker run signalwire-python-sdk -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .eggs 2 | SignalWire_Python_SDK.egg-info 3 | __pycache__ 4 | 5 | # Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore: 6 | # 7 | # * Create a file at ~/.gitignore 8 | # * Include files you want ignored 9 | # * Run: git config --global core.excludesfile ~/.gitignore 10 | # 11 | # After doing this, these files will be ignored in all your git projects, 12 | # saving you from having to 'pollute' every project you touch with them 13 | # 14 | # Not sure what to needs to be ignored for particular editors/OSes? Here's some ideas to get you started. (Remember, remove the leading # of the line) 15 | # 16 | # For MacOS: 17 | # 18 | #.DS_Store 19 | 20 | # For TextMate 21 | #*.tmproj 22 | #tmtags 23 | 24 | # For emacs: 25 | #*~ 26 | #\#* 27 | #.\#* 28 | 29 | # For vim: 30 | #*.swp 31 | 32 | # For redcar: 33 | #.redcar 34 | 35 | .pytest_cache/ 36 | # Packages 37 | *.egg 38 | *.egg-info 39 | dist 40 | build 41 | eggs 42 | parts 43 | bin 44 | develop-eggs 45 | .installed.cfg 46 | scratch 47 | env 48 | venv* 49 | *.egg-info 50 | Dockerfile.dev 51 | tmp 52 | *.pyc 53 | .vscode 54 | .devcontainer 55 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | Authors 2 | ======= 3 | 4 | A huge thanks to all of our contributors: 5 | 6 | 7 | - Luca Pradovera 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 5 | 6 | ## [2.1.0] - 2023-11-23 7 | ### Added 8 | - Webhook `RequestValidator` 9 | 10 | ## [2.0.5] - 2023-06-21 11 | ### Updated 12 | - Update dependencies. 13 | 14 | ## [2.0.4] - 2021-11-29 15 | ### Updated 16 | - Update compatibility SDK versions. 17 | 18 | ## [2.0.2] - 2020-03-10 19 | ### Fixed 20 | - Delete the `_request` dict key once the request has been completed. 21 | 22 | ## [2.0.1] - 2019-12-05 23 | ### Fixed 24 | - Call prompt `media_list` parameter without nested _params_ property. 25 | 26 | ## [2.0.0] - 2019-11-25 27 | ### Added 28 | - Call `disconnect()` method. 29 | 30 | ### Fixed 31 | - Check signals supported by the environment. On Windows there is no `SIGHUP`. 32 | - Detect half-open connection and force close connection to update Client/Consumer properly. 33 | 34 | ## [2.0.0rc1] - 2019-10-28 35 | ### Added 36 | - Support for Calling `tap`, `tap_async` methods. 37 | - Support for Calling `send_digits`, `send_digits_async` methods. 38 | - Support to send/receive faxes on the Call: `fax_receive`, `fax_receive_async`, `fax_send`, `fax_send_async`. 39 | - Methods to start detectors on the Call: `detect`, `detect_async`, `detect_answering_machine`, `detect_answering_machine_async`, `detect_digit`, `detect_digit_async`, `detect_fax`, `detect_fax_async`. 40 | - Set logging level via LOG_LEVEL env variable. 41 | - Add `playRingtone` and `playRingtoneAsync` methods to simplify play a ringtone. 42 | 43 | ## [2.0.0b3] - 2019-10-14 44 | ### Added 45 | - Support for Relay Messaging 46 | - Support for Calling `connect` and `play` methods. 47 | - Support for Calling `record` methods. 48 | - Methods to wait some events on the Call object: `wait_for_ringing`, `wait_for_answered`, `wait_for_ending`, `wait_for_ended`. 49 | 50 | ## [2.0.0b2] - 2019-10-03 51 | ### Fixed 52 | - Minor fix if using wrong SignalWire number. 53 | 54 | ### Changed 55 | - Default log level to INFO 56 | 57 | ## [2.0.0b1] - 2019-10-03 58 | ### Added 59 | - Beta version of `Relay Client` 60 | - `Relay Consumer` and `Tasks` 61 | 62 | ### Fixed 63 | - Possible issue on WebSocket reconnect due to a race condition on the EventLoop. 64 | 65 | ## [1.5.0] - 2019-04-12 66 | ### Fixed 67 | - Allow initialization via ENV variable 68 | 69 | ## [1.4.2] - 2019-02-02 70 | ### Added 71 | - Python 2 support 72 | ### Fixed 73 | - Importing LaML subclasses 74 | 75 | ## [1.4.0] - 2019-01-16 76 | ### Added 77 | - Fax REST API 78 | 79 | ## [1.0.0] - 2018-10-04 80 | 81 | Initial release 82 | 83 | 90 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.13-slim 2 | 3 | COPY . /app 4 | WORKDIR /app 5 | 6 | RUN apt-get update && apt-get install -y build-essential 7 | 8 | RUN pip install --upgrade pip && pip install pipenv && pipenv install --dev 9 | 10 | CMD ["pipenv", "run", "pytest"] 11 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 SignalWire Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | authors: 2 | echo "Authors\n=======\n\nA huge thanks to all of our contributors:\n\n" > AUTHORS.md 3 | git log --raw | grep "^Author: " | cut -d ' ' -f2- | cut -d '<' -f1 | sed 's/^/- /' | sort | uniq >> AUTHORS.md 4 | 5 | build_wheel: 6 | pipenv run python setup.py bdist_wheel 7 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | pylint = "*" 8 | pytest = "*" 9 | pytest-asyncio = "*" 10 | pytest-runner = "*" 11 | vcrpy = "*" 12 | 13 | [packages] 14 | aiohttp = "*" 15 | signalwire = {editable = true,path = "."} 16 | six = "*" 17 | twilio = ">=7.0.0,<7.6.0" 18 | 19 | [requires] 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SignalWire Python 2 | 3 | 4 | ![PyPI Version](https://img.shields.io/pypi/v/signalwire.svg?color=brightgreen) 5 | ![Drone CI](https://ci.signalwire.com/api/badges/signalwire/signalwire-python/status.svg) 6 | 7 | The Relay SDK for Python enables developers to connect and use SignalWire's Relay APIs within their own Python code. Our Relay SDK allows developers to build or add robust and innovative communication services to their applications. 8 | 9 | ## Getting Started 10 | 11 | Read the implementation documentation, guides and API Reference at the official [Relay SDK for Python Documentation](https://docs.signalwire.com/topics/relay-sdk-python) site. 12 | 13 | --- 14 | 15 | ## Contributing 16 | 17 | Relay SDK for Python is open source and maintained by the SignalWire team, but we are very grateful for [everyone](https://github.com/signalwire/signalwire-python/contributors) who has contributed and assisted so far. 18 | 19 | If you'd like to contribute, feel free to visit our [Slack channel](https://signalwire.community/) and read our developer section to get the code running in your local environment. 20 | 21 | ## Developers 22 | 23 | To setup the dev environment follow these steps: 24 | 25 | 1. Fork this repository and clone it. 26 | 2. Create a new branch from `master` for your change. 27 | 3. Make changes! 28 | 29 | ## Versioning 30 | 31 | Relay SDK for Python follows Semantic Versioning 2.0 as defined at . 32 | 33 | ## License 34 | 35 | Relay SDK for Python is copyright © 2018-2019 36 | [SignalWire](http://signalwire.com). It is free software, and may be redistributed under the terms specified in the [MIT-LICENSE](https://github.com/signalwire/signalwire-python/blob/master/LICENSE) file. 37 | -------------------------------------------------------------------------------- /examples/laml/laml.py: -------------------------------------------------------------------------------- 1 | from signalwire.voice_response import VoiceResponse 2 | 3 | r = VoiceResponse() 4 | r.say("Welcome to SignalWire!") 5 | 6 | print(str(r)) 7 | -------------------------------------------------------------------------------- /examples/laml/make_call.py: -------------------------------------------------------------------------------- 1 | from signalwire.rest import Client as signalwire_client 2 | 3 | account = "YOURACCOUNT" 4 | token = "YOURTOKEN" 5 | client = signalwire_client(account, token, signalwire_space_url = 'yourspace.signalwire.com') 6 | 7 | call = client.calls.create( 8 | to="+1xxx", 9 | from_="+1yyy", #must be a number in your Signalwire account 10 | url="https://cdn.signalwire.com/default-music/playlist.xml", 11 | method="GET" 12 | ) 13 | 14 | print(call) 15 | 16 | # get the latest 5 calls and print their statuses 17 | callrec = client.calls.list() 18 | for record in callrec[:5]: 19 | print(record.sid) 20 | print(record.status) 21 | -------------------------------------------------------------------------------- /examples/laml/send_sms.py: -------------------------------------------------------------------------------- 1 | from signalwire.rest import Client as signalwire_client 2 | 3 | account = "YOURACCOUNT" 4 | token = "YOURTOKEN" 5 | client = signalwire_client(account, token, signalwire_space_url = 'yourspace.signalwire.com') 6 | 7 | message = client.messages.create( 8 | to="+1xxx", 9 | from_="+1yyy", #must be a number in your Signalwire account 10 | body="Hello, how are you?" 11 | ) 12 | 13 | print(message) 14 | -------------------------------------------------------------------------------- /examples/relay/call-connect.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | from signalwire.relay.consumer import Consumer 4 | 5 | class CustomConsumer(Consumer): 6 | def setup(self): 7 | self.project = os.getenv('PROJECT', '') 8 | self.token = os.getenv('TOKEN', '') 9 | self.contexts = ['office', 'home'] 10 | 11 | async def ready(self): 12 | logging.info('Consumer Ready') 13 | 14 | def teardown(self): 15 | logging.info('Consumer teardown..') 16 | 17 | async def on_incoming_call(self, call): 18 | await call.answer() 19 | devices = [ 20 | { 'to_number': '+1xxx', 'timeout': 10 }, 21 | { 'to_number': '+1yyy', 'timeout': 20 } 22 | ] 23 | ringback = [ 24 | { 'type': 'ringtone', 'name': 'us' } 25 | ] 26 | result = await call.connect(device_list=devices, ringback_list=ringback) 27 | if result.successful: 28 | print('Call Connected!') 29 | remote_call = result.call 30 | 31 | consumer = CustomConsumer() 32 | consumer.run() 33 | -------------------------------------------------------------------------------- /examples/relay/call-detect.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | from signalwire.relay.consumer import Consumer 4 | 5 | class CustomConsumer(Consumer): 6 | def setup(self): 7 | self.project = os.getenv('PROJECT', '') 8 | self.token = os.getenv('TOKEN', '') 9 | self.contexts = ['office', 'home'] 10 | 11 | async def ready(self): 12 | logging.info('Consumer Ready') 13 | dial_result = await self.client.calling.dial(to_number='+1xxx', from_number='+1yyy') 14 | if dial_result.successful is False: 15 | logging.info('Outboud call failed.') 16 | return 17 | 18 | amd = await dial_result.call.amd() 19 | logging.info(f'AMD Result: {amd.result}') 20 | 21 | if amd.successful and amd.result == 'HUMAN': 22 | # If we detect a HUMAN, say hello and play an audio file. 23 | await dial_result.call.play_tts(text='Hey human! How you doing?') 24 | await dial_result.call.play_audio(url='https://cdn.signalwire.com/default-music/welcome.mp3') 25 | 26 | await dial_result.call.hangup() 27 | logging.info('Outbound call hanged up!') 28 | 29 | def teardown(self): 30 | logging.info('Consumer teardown..') 31 | 32 | consumer = CustomConsumer() 33 | consumer.run() 34 | -------------------------------------------------------------------------------- /examples/relay/call-fax-receive.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | from signalwire.relay.consumer import Consumer 4 | 5 | class Receiver(Consumer): 6 | def setup(self): 7 | self.project = os.getenv('PROJECT', '') 8 | self.token = os.getenv('TOKEN', '') 9 | self.contexts = ['office', 'home'] 10 | 11 | async def on_incoming_call(self, call): 12 | await call.answer() 13 | fax = await call.fax_receive() 14 | print(f"Fax received: {fax.document}") 15 | await call.hangup() 16 | 17 | r = Receiver() 18 | r.run() 19 | -------------------------------------------------------------------------------- /examples/relay/call-fax-send.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | from signalwire.relay.consumer import Consumer 4 | 5 | class Sender(Consumer): 6 | def setup(self): 7 | self.project = os.getenv('PROJECT', '') 8 | self.token = os.getenv('TOKEN', '') 9 | self.contexts = ['default'] 10 | 11 | async def ready(self): 12 | logging.info('Sender Ready') 13 | dial_result = await self.client.calling.dial(to_number='+1xxx', from_number='+1yyy') 14 | if dial_result.successful is False: 15 | logging.info('Outboud call failed.') 16 | return 17 | 18 | fax = await dial_result.call.fax_send(url='https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf', header='Custom Header') 19 | logging.info(f'Send fax Result: {fax.document} with pages {fax.pages}') 20 | 21 | await dial_result.call.hangup() 22 | logging.info('Outbound call completed!') 23 | 24 | def teardown(self): 25 | logging.info('Consumer teardown..') 26 | 27 | s = Sender() 28 | s.run() 29 | -------------------------------------------------------------------------------- /examples/relay/call-record.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import os 4 | from signalwire.relay.consumer import Consumer 5 | 6 | class CustomConsumer(Consumer): 7 | def setup(self): 8 | self.project = os.getenv('PROJECT', '') 9 | self.token = os.getenv('TOKEN', '') 10 | self.contexts = ['office', 'home'] 11 | 12 | async def ready(self): 13 | logging.info('Consumer Ready') 14 | 15 | def teardown(self): 16 | logging.info('Consumer teardown..') 17 | 18 | async def on_incoming_call(self, call): 19 | await call.answer() 20 | action = await call.record_async(beep=True, terminators='#') 21 | await asyncio.sleep(10) # Record 10seconds... 22 | await action.stop() 23 | print(f"Recording file at: {action.url}") 24 | await call.hangup() 25 | 26 | consumer = CustomConsumer() 27 | consumer.run() 28 | -------------------------------------------------------------------------------- /examples/relay/call-tap.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import os 4 | from signalwire.relay.consumer import Consumer 5 | 6 | class CustomConsumer(Consumer): 7 | def setup(self): 8 | self.project = os.getenv('PROJECT', '') 9 | self.token = os.getenv('TOKEN', '') 10 | self.contexts = ['office', 'home'] 11 | 12 | async def ready(self): 13 | logging.info('Consumer Ready') 14 | result = await self.client.calling.dial(from_number='+1xxx', to_number='+1yyy') 15 | if result.successful: 16 | logging.info('Start tapping') 17 | tap = await result.call.tap_async(audio_direction='both', target_type='rtp', target_addr='', target_port=16394) 18 | await asyncio.sleep(20) 19 | logging.info('Stop tapping') 20 | await tap.stop() 21 | await result.call.hangup() 22 | 23 | def teardown(self): 24 | logging.info('Consumer teardown..') 25 | 26 | consumer = CustomConsumer() 27 | consumer.run() 28 | -------------------------------------------------------------------------------- /examples/relay/consumer.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | from signalwire.relay.consumer import Consumer 4 | 5 | class CustomConsumer(Consumer): 6 | def setup(self): 7 | self.project = os.getenv('PROJECT', '') 8 | self.token = os.getenv('TOKEN', '') 9 | self.contexts = ['office', 'home'] 10 | 11 | async def ready(self): 12 | logging.info('Consumer Ready') 13 | result = await self.client.calling.dial(from_number='+1xxx', to_number='+1yyy') 14 | if result.successful: 15 | logging.info('Call answered') 16 | 17 | def teardown(self): 18 | logging.info('Consumer teardown..') 19 | 20 | async def on_incoming_call(self, call): 21 | result = await call.answer() 22 | if result.successful: 23 | logging.info('Call answered') 24 | 25 | async def on_task(self, message): 26 | logging.info('Handle inbound task') 27 | logging.info(message) 28 | 29 | def on_incoming_message(self, message): 30 | logging.info('on_incoming_message') 31 | logging.info(message) 32 | 33 | def on_message_state_change(self, message): 34 | logging.info('on_message_state_change') 35 | logging.info(message) 36 | 37 | consumer = CustomConsumer() 38 | consumer.run() 39 | -------------------------------------------------------------------------------- /examples/relay/create-task.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | from signalwire.relay.task import Task 4 | 5 | project = os.getenv('PROJECT', '') 6 | token = os.getenv('TOKEN', '') 7 | task = Task(project=project, token=token) 8 | success = task.deliver('office', { 'key': 'value', 'data': 'random stuff' }) 9 | if success: 10 | print('Task delivered') 11 | else: 12 | print('Task NOT delivered') 13 | -------------------------------------------------------------------------------- /examples/relay/raw_client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | from signalwire.relay.client import Client 4 | 5 | async def inbound_call(call): 6 | result = await call.answer() 7 | if result.successful: 8 | print('Inbound call answered successful') 9 | await asyncio.sleep(5) 10 | hangup = await call.hangup() 11 | if hangup.successful: 12 | print('Call hangup') 13 | else: 14 | print('Call hangup failed') 15 | else: 16 | print('Inbound call failed or not answered') 17 | 18 | async def ready(client): 19 | print('Client ready!') 20 | await client.calling.receive(['home', 'office'], inbound_call) 21 | # call = client.calling.new_call(from_number='+1xxx', to_number='+1yyy') 22 | # result = await call.dial() 23 | # if result.successful: 24 | # print('Outbound call answered successful') 25 | # else: 26 | # print('Outbound call failed or not answered') 27 | 28 | def socket_open(): 29 | print('socket_open') 30 | 31 | def socket_error(error): 32 | print('socket_error') 33 | print(error) 34 | 35 | def socket_message(msg): 36 | print('socket_message') 37 | print(msg) 38 | 39 | def socket_close(): 40 | print('socket_close') 41 | 42 | def main(): 43 | project = os.getenv('PROJECT', '') 44 | token = os.getenv('TOKEN', '') 45 | client = Client(project=project, token=token) 46 | client.on('ready', ready) 47 | client.on('signalwire.socket.open', socket_open) 48 | client.on('signalwire.socket.error', socket_error) 49 | client.on('signalwire.socket.message', socket_message) 50 | client.on('signalwire.socket.close', socket_close) 51 | client.connect() 52 | 53 | main() 54 | -------------------------------------------------------------------------------- /fixtures/cassettes/test_accounts: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: [application/json] 6 | Accept-Charset: [utf-8] 7 | Accept-Encoding: ['gzip, deflate'] 8 | Connection: [keep-alive] 9 | method: GET 10 | uri: https://myaccount.signalwire.com/2010-04-01/Accounts/signalwire-account-123.json 11 | response: 12 | body: {string: '{"sid":"signalwire-account-123","friendly_name":"LAML 13 | testing","status":"active","auth_token":"redacted","date_created":"Mon, 20 14 | Aug 2018 12:54:10 +0000","date_updated":"Mon, 20 Aug 2018 12:54:10 +0000","type":"Full","uri":"/api/laml/2010-04-01/Accounts/signalwire-account-123","subresource_uris":{"addresses":null,"available_phone_numbers":"/api/laml/2010-04-01/Accounts/signalwire-account-123/AvailablePhoneNumbers","applications":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Applications","authorized_connect_apps":null,"calls":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Calls","conferences":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Conferences","connect_apps":null,"incoming_phone_numbers":"/api/laml/2010-04-01/Accounts/signalwire-account-123/IncomingPhoneNumbers","keys":null,"notifications":null,"outgoing_caller_ids":null,"queues":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Queues","recordings":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Recordings","sandbox":null,"signing_keys":null,"sip":null,"short_codes":null,"messages":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Messages","transcriptions":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Transcriptions","usage":null}}'} 15 | headers: {} 16 | status: {code: 200, message: OK} 17 | version: 1 18 | -------------------------------------------------------------------------------- /fixtures/cassettes/test_applications: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: [application/json] 6 | Accept-Charset: [utf-8] 7 | Accept-Encoding: ['gzip, deflate'] 8 | Connection: [keep-alive] 9 | method: GET 10 | uri: https://myaccount.signalwire.com/2010-04-01/Accounts/signalwire-account-123/Applications.json 11 | response: 12 | body: {string: '{"uri":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Applications?Page=0\u0026PageSize=50","first_page_uri":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Applications?Page=0\u0026PageSize=50","next_page_uri":null,"previous_page_uri":null,"page":0,"page_size":50,"applications":[{"sid":"34f49a97-a863-4a11-8fef-bc399c6f0928","date_created":"Wed, 13 | 19 Dec 2018 14:23:36 +0000","date_updated":"Wed, 19 Dec 2018 14:23:36 +0000","account_sid":"signalwire-account-123","friendly_name":"Test 14 | App","voice_url":"","voice_method":"POST","voice_fallback_url":"","voice_fallback_method":"POST","status_callback":"","status_callback_method":"POST","voice_caller_id_lookup":null,"sms_url":"","sms_method":"POST","sms_fallback_url":"","sms_fallback_method":"POST","sms_status_callback":"","sms_status_callback_method":"POST","api_version":"2010-04-01","uri":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Calls/34f49a97-a863-4a11-8fef-bc399c6f0928"}]}'} 15 | headers: {} 16 | status: {code: 200, message: OK} 17 | version: 1 18 | -------------------------------------------------------------------------------- /fixtures/cassettes/test_conference_members: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: [application/json] 6 | Accept-Charset: [utf-8] 7 | Accept-Encoding: ['gzip, deflate'] 8 | method: GET 9 | uri: https://myaccount.signalwire.com/2010-04-01/Accounts/signalwire-account-123/Conferences/a811cb2c-9e5a-415d-a951-701f8e884fb5/Participants.json 10 | response: 11 | body: {string: '{"uri":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Conferences.a811cb2c-9e5a-415d-a951-701f8e884fb5?Page=0\u0026PageSize=50","first_page_uri":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Conferences.a811cb2c-9e5a-415d-a951-701f8e884fb5?Page=0\u0026PageSize=50","next_page_uri":null,"previous_page_uri":null,"page":0,"page_size":50,"participants":[{"account_sid":"signalwire-account-123","call_sid":"7a520324-684d-435c-87c2-ea7975f371d0","conference_sid":"a811cb2c-9e5a-415d-a951-701f8e884fb5","date_created":"Wed, 12 | 19 Dec 2018 15:48:13 +0000","date_updated":"Wed, 19 Dec 2018 15:48:13 +0000","start_conference_on_enter":true,"end_conference_on_exit":false,"muted":false,"hold":false,"status":"completed","uri":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Conferences/a811cb2c-9e5a-415d-a951-701f8e884fb5/Participants/7a520324-684d-435c-87c2-ea7975f371d0.json"}]}'} 13 | headers: {} 14 | status: {code: 200, message: OK} 15 | version: 1 16 | -------------------------------------------------------------------------------- /fixtures/cassettes/test_conferences: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: [application/json] 6 | Accept-Charset: [utf-8] 7 | Accept-Encoding: ['gzip, deflate'] 8 | method: GET 9 | uri: https://myaccount.signalwire.com/2010-04-01/Accounts/signalwire-account-123/Conferences.json 10 | response: 11 | body: {string: '{"uri":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Conferences?Page=0\u0026PageSize=50","first_page_uri":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Conferences?Page=0\u0026PageSize=50","next_page_uri":null,"previous_page_uri":null,"page":0,"page_size":50,"conferences":[{"sid":"a811cb2c-9e5a-415d-a951-701f8e884fb5","date_created":"Wed, 12 | 19 Dec 2018 15:48:13 +0000","date_updated":"Wed, 19 Dec 2018 15:48:13 +0000","account_sid":"signalwire-account-123","friendly_name":"Room 13 | 1234","status":"init","api_version":"2010-04-01","region":"us1","uri":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Conferences/a811cb2c-9e5a-415d-a951-701f8e884fb5.json","subresource_uris":{"participants":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Conferences/a811cb2c-9e5a-415d-a951-701f8e884fb5/Participants.json","recordings":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Conferences/a811cb2c-9e5a-415d-a951-701f8e884fb5/Recordings.json"}},{"sid":"e68ad3cf-67d2-4be1-9634-63e56ba9b100","date_created":"Thu, 14 | 18 Oct 2018 13:47:01 +0000","date_updated":"Thu, 18 Oct 2018 13:47:01 +0000","account_sid":"signalwire-account-123","friendly_name":"555-555-5555","status":"init","api_version":"2010-04-01","region":"us1","uri":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Conferences/e68ad3cf-67d2-4be1-9634-63e56ba9b100.json","subresource_uris":{"participants":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Conferences/e68ad3cf-67d2-4be1-9634-63e56ba9b100/Participants.json","recordings":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Conferences/e68ad3cf-67d2-4be1-9634-63e56ba9b100/Recordings.json"}}]}'} 15 | headers: {} 16 | status: {code: 200, message: OK} 17 | version: 1 18 | -------------------------------------------------------------------------------- /fixtures/cassettes/test_fetch_fax: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: [application/json] 6 | Accept-Charset: [utf-8] 7 | Accept-Encoding: ['gzip, deflate'] 8 | method: GET 9 | uri: https://myaccount.signalwire.com/2010-04-01/Accounts/signalwire-account-123/Faxes/831455c6-574e-4d8b-b6ee-2418140bf4cd 10 | response: 11 | body: {string: '{"account_sid":"signalwire-account-123","api_version":"v1","date_created":"2019-01-04T16:28:33Z","date_updated":"2019-01-04T16:29:20Z","direction":"outbound","from":"+15556677888","media_url":"https://s3.us-east-2.amazonaws.com/signalwire-assets/faxes/20190104162834-831455c6-574e-4d8b-b6ee-2418140bf4cd.tiff","media_sid":"aff0684c-3445-49bc-802b-3a0a488139f5","num_pages":1,"price":0.0105,"price_unit":"USD","quality":"fine","sid":"831455c6-574e-4d8b-b6ee-2418140bf4cd","status":"delivered","to":"+15556677999","duration":41,"links":{"media":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Faxes/831455c6-574e-4d8b-b6ee-2418140bf4cd/Media"},"url":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Faxes/831455c6-574e-4d8b-b6ee-2418140bf4cd"}'} 12 | headers: {} 13 | status: {code: 200, message: OK} 14 | version: 1 15 | -------------------------------------------------------------------------------- /fixtures/cassettes/test_fetch_fax_media: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: [application/json] 6 | Accept-Charset: [utf-8] 7 | Accept-Encoding: ['gzip, deflate'] 8 | Connection: [keep-alive] 9 | method: GET 10 | uri: https://myaccount.signalwire.com/2010-04-01/Accounts/signalwire-account-123/Faxes/831455c6-574e-4d8b-b6ee-2418140bf4cd/Media 11 | response: 12 | body: {string: !!python/unicode '{"uri":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Faxes/831455c6-574e-4d8b-b6ee-2418140bf4cd/Media?Page=0\u0026PageSize=50","first_page_uri":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Faxes/831455c6-574e-4d8b-b6ee-2418140bf4cd/Media?Page=0\u0026PageSize=50","next_page_uri":null,"previous_page_uri":null,"page":0,"page_size":50,"fax_media":[{"sid":"aff0684c-3445-49bc-802b-3a0a488139f5","date_created":"2019-01-04T16:28:33Z","date_updated":"2019-01-04T16:29:20Z","account_sid":"signalwire-account-123","fax_sid":"831455c6-574e-4d8b-b6ee-2418140bf4cd","content_type":"image/tiff","uri":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Faxes/831455c6-574e-4d8b-b6ee-2418140bf4cd/Media/aff0684c-3445-49bc-802b-3a0a488139f5.json","url":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Faxes/831455c6-574e-4d8b-b6ee-2418140bf4cd/Media/aff0684c-3445-49bc-802b-3a0a488139f5.json"}]}'} 13 | headers: {} 14 | status: {code: 200, message: OK} 15 | version: 1 16 | -------------------------------------------------------------------------------- /fixtures/cassettes/test_fetch_fax_media_instance: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: [application/json] 6 | Accept-Charset: [utf-8] 7 | Accept-Encoding: ['gzip, deflate'] 8 | Connection: [keep-alive] 9 | method: GET 10 | uri: https://myaccount.signalwire.com/2010-04-01/Accounts/signalwire-account-123/Faxes/831455c6-574e-4d8b-b6ee-2418140bf4cd/Media/aff0684c-3445-49bc-802b-3a0a488139f5 11 | response: 12 | body: {string: !!python/unicode '{"sid":"aff0684c-3445-49bc-802b-3a0a488139f5","date_created":"2019-01-04T16:28:33Z","date_updated":"2019-01-04T16:29:20Z","account_sid":"signalwire-account-123","fax_sid":"831455c6-574e-4d8b-b6ee-2418140bf4cd","content_type":"image/tiff","uri":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Faxes/831455c6-574e-4d8b-b6ee-2418140bf4cd/Media/aff0684c-3445-49bc-802b-3a0a488139f5.json","url":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Faxes/831455c6-574e-4d8b-b6ee-2418140bf4cd/Media/aff0684c-3445-49bc-802b-3a0a488139f5.json"}'} 13 | headers: {} 14 | status: {code: 200, message: OK} 15 | version: 1 16 | -------------------------------------------------------------------------------- /fixtures/cassettes/test_incoming_phone_numbers: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: [application/json] 6 | Accept-Charset: [utf-8] 7 | Accept-Encoding: ['gzip, deflate'] 8 | method: GET 9 | uri: https://myaccount.signalwire.com/2010-04-01/Accounts/signalwire-account-123/IncomingPhoneNumbers.json 10 | response: 11 | body: {string: '{"uri":"/api/laml/2010-04-01/Accounts/signalwire-account-123/IncomingPhoneNumbers?Page=0\u0026PageSize=50","first_page_uri":"/api/laml/2010-04-01/Accounts/signalwire-account-123/IncomingPhoneNumbers?Page=0\u0026PageSize=50","next_page_uri":null,"previous_page_uri":null,"page":0,"page_size":50,"incoming_phone_numbers":[{"sid":"4472cc03-1f97-436e-9f15-8752dfdbc00b","friendly_name":"+1 12 | (899) 000-0001","phone_number":"+18990000001","voice_url":"http://adhearsion:8080/laml","voice_method":"GET","voice_fallback_url":"","voice_fallback_method":"POST","voice_caller_id_lookup":null,"voice_application_sid":null,"date_created":"Thu, 13 | 11 Oct 2018 20:37:41 +0000","date_updated":"Mon, 10 Dec 2018 18:31:21 +0000","sms_url":"","sms_method":"POST","sms_fallback_url":"","sms_fallback_method":"POST","sms_application_sid":null,"capabilities":{"voice":true,"sms":false,"mms":false},"beta":false,"status_callback":"","status_callback_method":"POST","api_version":"2010-04-01","uri":"/api/laml/2010-04-01/Accounts/signalwire-account-123/IncomingPhoneNumbers/4472cc03-1f97-436e-9f15-8752dfdbc00b"},{"sid":"9c14695d-06c8-4790-b467-5ac83f235d33","friendly_name":"+1 14 | (404) 328-7382","phone_number":"+14043287382","voice_url":"https://myaccount.signalwire.com/laml-bins/edfb6a6e-a60a-4a56-9a9c-04babd511882","voice_method":"GET","voice_fallback_url":"","voice_fallback_method":"POST","voice_caller_id_lookup":null,"voice_application_sid":"34f49a97-a863-4a11-8fef-bc399c6f0928","date_created":"Wed, 15 | 10 Oct 2018 18:31:01 +0000","date_updated":"Wed, 19 Dec 2018 15:47:30 +0000","sms_url":"","sms_method":"POST","sms_fallback_url":"","sms_fallback_method":"POST","sms_application_sid":null,"capabilities":{"voice":true,"sms":true,"mms":true},"beta":false,"status_callback":"","status_callback_method":"POST","api_version":"2010-04-01","uri":"/api/laml/2010-04-01/Accounts/signalwire-account-123/IncomingPhoneNumbers/9c14695d-06c8-4790-b467-5ac83f235d33"},{"sid":"2eb34bc9-5108-41e7-aeb1-848f3ca90dcf","friendly_name":"+1 16 | (404) 328-7360","phone_number":"+14043287360","voice_url":"https://signalwire.ngrok.io/laml","voice_method":"GET","voice_fallback_url":"","voice_fallback_method":"POST","voice_caller_id_lookup":null,"voice_application_sid":null,"date_created":"Mon, 17 | 20 Aug 2018 12:54:34 +0000","date_updated":"Thu, 13 Sep 2018 13:32:02 +0000","sms_url":"","sms_method":"POST","sms_fallback_url":"","sms_fallback_method":"POST","sms_application_sid":null,"capabilities":{"voice":true,"sms":true,"mms":true},"beta":false,"status_callback":"","status_callback_method":"POST","api_version":"2010-04-01","uri":"/api/laml/2010-04-01/Accounts/signalwire-account-123/IncomingPhoneNumbers/2eb34bc9-5108-41e7-aeb1-848f3ca90dcf"}]}'} 18 | headers: {} 19 | status: {code: 200, message: OK} 20 | version: 1 21 | -------------------------------------------------------------------------------- /fixtures/cassettes/test_list_fax: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: [application/json] 6 | Accept-Charset: [utf-8] 7 | Accept-Encoding: ['gzip, deflate'] 8 | Connection: [keep-alive] 9 | method: GET 10 | uri: https://myaccount.signalwire.com/2010-04-01/Accounts/signalwire-account-123/Faxes 11 | response: 12 | body: {string: '{"uri":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Faxes?Page=0\u0026PageSize=50","first_page_uri":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Faxes?Page=0\u0026PageSize=50","next_page_uri":null,"previous_page_uri":null,"page":0,"page_size":50,"faxes":[{"account_sid":"signalwire-account-123","api_version":"v1","date_created":"2019-01-07T16:51:22Z","date_updated":"2019-01-07T16:52:00Z","direction":"outbound","from":"+15556677888","media_url":"https://s3.us-east-2.amazonaws.com/signalwire-assets/faxes/20190107165123-dd3e1ac4-50c9-4241-933a-5d4e9a2baf31.tiff","media_sid":"ceab8d12-359b-4c0c-86fc-75e5d500a1c0","num_pages":1,"price":0.0105,"price_unit":"USD","quality":"fine","sid":"dd3e1ac4-50c9-4241-933a-5d4e9a2baf31","status":"delivered","to":"+15556677999","duration":34,"links":{"media":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Faxes/dd3e1ac4-50c9-4241-933a-5d4e9a2baf31/Media"},"url":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Faxes/dd3e1ac4-50c9-4241-933a-5d4e9a2baf31"},{"account_sid":"signalwire-account-123","api_version":"v1","date_created":"2019-01-07T16:46:42Z","date_updated":"2019-01-07T16:47:11Z","direction":"outbound","from":"+14043287382","media_url":"https://s3.us-east-2.amazonaws.com/signalwire-assets/faxes/20190107164643-8dc7f5aa-c9f9-44cf-817b-fcd0ccc801db.tiff","media_sid":"7ad80d05-8dfe-44bf-9a51-8ecae439e05e","num_pages":1,"price":0.0105,"price_unit":"USD","quality":"fine","sid":"8dc7f5aa-c9f9-44cf-817b-fcd0ccc801db","status":"delivered","to":"+14043287360","duration":26,"links":{"media":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Faxes/8dc7f5aa-c9f9-44cf-817b-fcd0ccc801db/Media"},"url":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Faxes/8dc7f5aa-c9f9-44cf-817b-fcd0ccc801db"},{"account_sid":"signalwire-account-123","api_version":"v1","date_created":"2019-01-07T16:44:43Z","date_updated":"2019-01-07T16:45:23Z","direction":"outbound","from":"+14043287382","media_url":"https://s3.us-east-2.amazonaws.com/signalwire-assets/faxes/20190107164443-ab77c13e-13a6-475e-bf8a-e21d57060537.tiff","media_sid":"5d3a0dd4-7061-461d-a274-f68d7e6e940c","num_pages":1,"price":0.0105,"price_unit":"USD","quality":"fine","sid":"ab77c13e-13a6-475e-bf8a-e21d57060537","status":"delivered","to":"+14043287360","duration":34,"links":{"media":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Faxes/ab77c13e-13a6-475e-bf8a-e21d57060537/Media"},"url":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Faxes/ab77c13e-13a6-475e-bf8a-e21d57060537"},{"account_sid":"signalwire-account-123","api_version":"v1","date_created":"2019-01-05T10:56:25Z","date_updated":"2019-01-05T10:57:11Z","direction":"outbound","from":"+14043287382","media_url":"https://s3.us-east-2.amazonaws.com/signalwire-assets/faxes/20190105105625-2b7a9801-1739-410e-961b-9d589d4a76e5.tiff","media_sid":"464c5e5d-e87b-4673-939f-383b6cc61f51","num_pages":1,"price":0.0105,"price_unit":"USD","quality":"fine","sid":"2b7a9801-1739-410e-961b-9d589d4a76e5","status":"delivered","to":"+14043287360","duration":40,"links":{"media":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Faxes/2b7a9801-1739-410e-961b-9d589d4a76e5/Media"},"url":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Faxes/2b7a9801-1739-410e-961b-9d589d4a76e5"},{"account_sid":"signalwire-account-123","api_version":"v1","date_created":"2019-01-04T16:28:33Z","date_updated":"2019-01-04T16:29:20Z","direction":"outbound","from":"+14043287382","media_url":"https://s3.us-east-2.amazonaws.com/signalwire-assets/faxes/20190104162834-831455c6-574e-4d8b-b6ee-2418140bf4cd.tiff","media_sid":"aff0684c-3445-49bc-802b-3a0a488139f5","num_pages":1,"price":0.0105,"price_unit":"USD","quality":"fine","sid":"831455c6-574e-4d8b-b6ee-2418140bf4cd","status":"delivered","to":"+14043287360","duration":41,"links":{"media":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Faxes/831455c6-574e-4d8b-b6ee-2418140bf4cd/Media"},"url":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Faxes/831455c6-574e-4d8b-b6ee-2418140bf4cd"},{"account_sid":"signalwire-account-123","api_version":"v1","date_created":"2019-01-04T16:05:18Z","date_updated":"2019-01-04T16:05:45Z","direction":"outbound","from":"+14043287382","media_url":"https://s3.us-east-2.amazonaws.com/signalwire-assets/faxes/20190104160520-5ed234e3-0e6b-4c49-869a-6c0ef3c30884.tiff","media_sid":"77643eca-a413-48c7-ad34-6e703fc77ca7","num_pages":0,"price":0.0105,"price_unit":"USD","quality":"fine","sid":"5ed234e3-0e6b-4c49-869a-6c0ef3c30884","status":"failed","to":"+14043287360","duration":17,"links":{"media":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Faxes/5ed234e3-0e6b-4c49-869a-6c0ef3c30884/Media"},"url":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Faxes/5ed234e3-0e6b-4c49-869a-6c0ef3c30884"},{"account_sid":"signalwire-account-123","api_version":"v1","date_created":"2019-01-04T16:03:11Z","date_updated":"2019-01-04T16:03:11Z","direction":"outbound","from":"+14043287382","media_url":null,"media_sid":"a9d56213-1ec6-4618-adac-969d4d11c09a","num_pages":null,"price":null,"price_unit":"USD","quality":"fine","sid":"ce501cac-3144-4540-a6c7-a1c7963501f7","status":"failed","to":"+14043287360","duration":0,"links":{"media":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Faxes/ce501cac-3144-4540-a6c7-a1c7963501f7/Media"},"url":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Faxes/ce501cac-3144-4540-a6c7-a1c7963501f7"}]}'} 13 | headers: {} 14 | status: {code: 200, message: OK} 15 | version: 1 16 | -------------------------------------------------------------------------------- /fixtures/cassettes/test_media: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: [application/json] 6 | Accept-Charset: [utf-8] 7 | Accept-Encoding: ['gzip, deflate'] 8 | method: GET 9 | uri: https://myaccount.signalwire.com/2010-04-01/Accounts/signalwire-account-123/Messages/0da01046-5cca-462f-bc50-adae4e1307e1/Media.json 10 | response: 11 | body: {string: '{"uri":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Messages/0da01046-5cca-462f-bc50-adae4e1307e1/Media?Page=0\u0026PageSize=50","first_page_uri":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Messages/0da01046-5cca-462f-bc50-adae4e1307e1/Media?Page=0\u0026PageSize=50","next_page_uri":null,"previous_page_uri":null,"page":0,"page_size":50,"media_list":[{"sid":"a1ee4484-99a4-4996-b7df-fd3ceef2e9ec","date_created":"Wed, 12 | 19 Dec 2018 16:23:50 +0000","date_updated":"Wed, 19 Dec 2018 16:23:50 +0000","account_sid":"signalwire-account-123","parent_sid":"0da01046-5cca-462f-bc50-adae4e1307e1","content_type":"image/png","uri":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Messages/0da01046-5cca-462f-bc50-adae4e1307e1/Media/a1ee4484-99a4-4996-b7df-fd3ceef2e9ec.json"}]}'} 13 | headers: {} 14 | status: {code: 200, message: OK} 15 | version: 1 16 | -------------------------------------------------------------------------------- /fixtures/cassettes/test_messages: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: To=%2B14044754849&From=%2B18990000001&Body=Hello+World%21 4 | headers: 5 | Accept: [application/json] 6 | Accept-Charset: [utf-8] 7 | Accept-Encoding: ['gzip, deflate'] 8 | Connection: [keep-alive] 9 | Content-Length: ['57'] 10 | Content-Type: [application/x-www-form-urlencoded] 11 | method: POST 12 | uri: https://myaccount.signalwire.com/2010-04-01/Accounts/signalwire-account-123/Messages.json 13 | response: 14 | body: {string: '{"account_sid":"signalwire-account-123","api_version":"2010-04-01","body":"Hello 15 | World!","num_segments":1,"num_media":0,"date_created":"Wed, 19 Dec 2018 16:06:46 16 | +0000","date_sent":null,"date_updated":"Wed, 19 Dec 2018 16:06:46 +0000","direction":"outbound-api","error_code":"21601","error_message":"Phone 17 | number is not a SMS-capable phone number.","from":"+18990000001","price":null,"price_unit":"USD","sid":"cbad786b-fdcd-4d2a-bcb2-fff9df045008","status":"failed","to":"+14044754849","uri":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Messages/cbad786b-fdcd-4d2a-bcb2-fff9df045008","subresource_uris":{"media":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Messages/cbad786b-fdcd-4d2a-bcb2-fff9df045008/Media"},"messaging_service_sid":null}'} 18 | headers: {} 19 | status: {code: 201, message: Created} 20 | version: 1 21 | -------------------------------------------------------------------------------- /fixtures/cassettes/test_queue_members: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: [application/json] 6 | Accept-Charset: [utf-8] 7 | Accept-Encoding: ['gzip, deflate'] 8 | Connection: [keep-alive] 9 | method: GET 10 | uri: https://myaccount.signalwire.com/2010-04-01/Accounts/signalwire-account-123/Queues/2fd1bc9b-2e1f-41ac-988f-06842700c10d/Members.json 11 | response: 12 | body: {string: '{"uri":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Queues/2fd1bc9b-2e1f-41ac-988f-06842700c10d/Members?Page=0\u0026PageSize=50","first_page_uri":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Queues/2fd1bc9b-2e1f-41ac-988f-06842700c10d/Members?Page=0\u0026PageSize=50","next_page_uri":null,"previous_page_uri":null,"page":0,"page_size":50,"queue_members":[{"date_enqueued":"Wed, 13 | 19 Dec 2018 17:20:27 +0000","account_sid":"signalwire-account-123","call_sid":"24c0f807-2663-4080-acef-c0874f45274d","queue_sid":"2fd1bc9b-2e1f-41ac-988f-06842700c10d","position":1,"wait_time":74,"uri":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Queues/2fd1bc9b-2e1f-41ac-988f-06842700c10d/Members/24c0f807-2663-4080-acef-c0874f45274d.json"}]}'} 14 | headers: {} 15 | status: {code: 200, message: OK} 16 | version: 1 17 | -------------------------------------------------------------------------------- /fixtures/cassettes/test_queues: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: [application/json] 6 | Accept-Charset: [utf-8] 7 | Accept-Encoding: ['gzip, deflate'] 8 | Connection: [keep-alive] 9 | method: GET 10 | uri: https://myaccount.signalwire.com/2010-04-01/Accounts/signalwire-account-123/Queues.json 11 | response: 12 | body: {string: '{"uri":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Queues?Page=0\u0026PageSize=50","first_page_uri":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Queues?Page=0\u0026PageSize=50","next_page_uri":null,"previous_page_uri":null,"page":0,"page_size":50,"queues":[{"sid":"2fd1bc9b-2e1f-41ac-988f-06842700c10d","date_created":"Wed, 13 | 19 Dec 2018 16:51:49 +0000","date_updated":"Wed, 19 Dec 2018 16:51:49 +0000","account_sid":"signalwire-account-123","friendly_name":"support","max_size":null,"current_size":0,"average_wait_time":0,"api_version":"2010-04-01","uri":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Queues/2fd1bc9b-2e1f-41ac-988f-06842700c10d.json"}]}'} 14 | headers: {} 15 | status: {code: 200, message: OK} 16 | version: 1 17 | -------------------------------------------------------------------------------- /fixtures/cassettes/test_recordings: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: [application/json] 6 | Accept-Charset: [utf-8] 7 | Accept-Encoding: ['gzip, deflate'] 8 | Connection: [keep-alive] 9 | method: GET 10 | uri: https://myaccount.signalwire.com/2010-04-01/Accounts/signalwire-account-123/Recordings.json 11 | response: 12 | body: {string: '{"uri":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Recordings?Page=0\u0026PageSize=50","first_page_uri":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Recordings?Page=0\u0026PageSize=50","next_page_uri":null,"previous_page_uri":null,"page":0,"page_size":50,"recordings":[{"sid":"e4c78e17-c0e2-441d-b5dd-39a6dad496f8","api_version":"2010-04-01","channel":"1","account_sid":"signalwire-account-123","call_sid":"d411976d-d319-4fbd-923c-57c62b6f677a","conference_sid":null,"date_created":"Wed, 13 | 19 Dec 2018 16:28:16 +0000","date_updated":"Wed, 19 Dec 2018 16:28:16 +0000","duration":14,"start_time":"Wed, 14 | 19 Dec 2018 16:27:56 +0000","end_time":"Wed, 19 Dec 2018 16:28:16 +0000","error_code":null,"price":0.0,"price_unit":"USD","source":"RecordVerb","status":"completed","uri":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Calls/d411976d-d319-4fbd-923c-57c62b6f677a/Recordings/e4c78e17-c0e2-441d-b5dd-39a6dad496f8.json","subresource_uris":{"transcriptions":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Recordings/e4c78e17-c0e2-441d-b5dd-39a6dad496f8/Transcriptions.json"}}]}'} 15 | headers: {} 16 | status: {code: 200, message: OK} 17 | version: 1 18 | -------------------------------------------------------------------------------- /fixtures/cassettes/test_send_fax: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: To=%2B15556677999&MediaUrl=https%3A%2F%2Fwww.w3.org%2FWAI%2FER%2Ftests%2Fxhtml%2Ftestfiles%2Fresources%2Fpdf%2Fdummy.pdf&From=%2B15556677888 4 | headers: 5 | Accept: [application/json] 6 | Accept-Charset: [utf-8] 7 | Accept-Encoding: ['gzip, deflate'] 8 | Connection: [keep-alive] 9 | Content-Length: ['140'] 10 | Content-Type: [application/x-www-form-urlencoded] 11 | method: POST 12 | uri: https://myaccount.signalwire.com/2010-04-01/Accounts/signalwire-account-123/Faxes 13 | response: 14 | body: {string: '{"account_sid":"signalwire-account-123","api_version":"v1","date_created":"2019-01-07T16:51:22Z","date_updated":"2019-01-07T16:51:22Z","direction":"outbound","from":"+14043287382","media_url":null,"media_sid":"ceab8d12-359b-4c0c-86fc-75e5d500a1c0","num_pages":null,"price":null,"price_unit":"USD","quality":"fine","sid":"dd3e1ac4-50c9-4241-933a-5d4e9a2baf31","status":"queued","to":"+14043287360","duration":0,"links":{"media":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Faxes/dd3e1ac4-50c9-4241-933a-5d4e9a2baf31/Media"},"url":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Faxes/dd3e1ac4-50c9-4241-933a-5d4e9a2baf31"}'} 15 | headers: {} 16 | status: {code: 201, message: Created} 17 | version: 1 -------------------------------------------------------------------------------- /fixtures/cassettes/test_toll_free_numbers: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: [application/json] 6 | Accept-Charset: [utf-8] 7 | Accept-Encoding: ['gzip, deflate'] 8 | Connection: [keep-alive] 9 | method: GET 10 | uri: https://myaccount.signalwire.com/2010-04-01/Accounts/signalwire-account-123/AvailablePhoneNumbers/US/TollFree.json?AreaCode=310 11 | response: 12 | body: {string: '{"uri":"/api/laml/2010-04-01/Accounts/signalwire-account-123/AvailablePhoneNumbers/US/TollFree?AreaCode=310","available_phone_numbers":[{"friendly_name":"310-359-0741","phone_number":"+13103590741","lata":null,"rate_center":"MALIBU","latitude":null,"longitude":null,"region":"CA","postal_code":null,"iso_country":"US","capabilities":{"voice":true,"SMS":true,"MMS":true},"beta":false},{"friendly_name":"310-359-0835","phone_number":"+13103590835","lata":null,"rate_center":"MALIBU","latitude":null,"longitude":null,"region":"CA","postal_code":null,"iso_country":"US","capabilities":{"voice":true,"SMS":true,"MMS":true},"beta":false},{"friendly_name":"310-359-0839","phone_number":"+13103590839","lata":null,"rate_center":"MALIBU","latitude":null,"longitude":null,"region":"CA","postal_code":null,"iso_country":"US","capabilities":{"voice":true,"SMS":true,"MMS":true},"beta":false},{"friendly_name":"310-359-1453","phone_number":"+13103591453","lata":null,"rate_center":"MALIBU","latitude":null,"longitude":null,"region":"CA","postal_code":null,"iso_country":"US","capabilities":{"voice":true,"SMS":true,"MMS":true},"beta":false},{"friendly_name":"310-359-1457","phone_number":"+13103591457","lata":null,"rate_center":"MALIBU","latitude":null,"longitude":null,"region":"CA","postal_code":null,"iso_country":"US","capabilities":{"voice":true,"SMS":true,"MMS":true},"beta":false},{"friendly_name":"310-359-1498","phone_number":"+13103591498","lata":null,"rate_center":"MALIBU","latitude":null,"longitude":null,"region":"CA","postal_code":null,"iso_country":"US","capabilities":{"voice":true,"SMS":true,"MMS":true},"beta":false},{"friendly_name":"310-359-1531","phone_number":"+13103591531","lata":null,"rate_center":"MALIBU","latitude":null,"longitude":null,"region":"CA","postal_code":null,"iso_country":"US","capabilities":{"voice":true,"SMS":true,"MMS":true},"beta":false},{"friendly_name":"310-359-1532","phone_number":"+13103591532","lata":null,"rate_center":"MALIBU","latitude":null,"longitude":null,"region":"CA","postal_code":null,"iso_country":"US","capabilities":{"voice":true,"SMS":true,"MMS":true},"beta":false},{"friendly_name":"310-359-1541","phone_number":"+13103591541","lata":null,"rate_center":"MALIBU","latitude":null,"longitude":null,"region":"CA","postal_code":null,"iso_country":"US","capabilities":{"voice":true,"SMS":true,"MMS":true},"beta":false},{"friendly_name":"310-359-1542","phone_number":"+13103591542","lata":null,"rate_center":"MALIBU","latitude":null,"longitude":null,"region":"CA","postal_code":null,"iso_country":"US","capabilities":{"voice":true,"SMS":true,"MMS":true},"beta":false},{"friendly_name":"310-359-1543","phone_number":"+13103591543","lata":null,"rate_center":"MALIBU","latitude":null,"longitude":null,"region":"CA","postal_code":null,"iso_country":"US","capabilities":{"voice":true,"SMS":true,"MMS":true},"beta":false},{"friendly_name":"310-359-1546","phone_number":"+13103591546","lata":null,"rate_center":"MALIBU","latitude":null,"longitude":null,"region":"CA","postal_code":null,"iso_country":"US","capabilities":{"voice":true,"SMS":true,"MMS":true},"beta":false},{"friendly_name":"310-359-1547","phone_number":"+13103591547","lata":null,"rate_center":"MALIBU","latitude":null,"longitude":null,"region":"CA","postal_code":null,"iso_country":"US","capabilities":{"voice":true,"SMS":true,"MMS":true},"beta":false},{"friendly_name":"310-359-1548","phone_number":"+13103591548","lata":null,"rate_center":"MALIBU","latitude":null,"longitude":null,"region":"CA","postal_code":null,"iso_country":"US","capabilities":{"voice":true,"SMS":true,"MMS":true},"beta":false},{"friendly_name":"310-359-1552","phone_number":"+13103591552","lata":null,"rate_center":"MALIBU","latitude":null,"longitude":null,"region":"CA","postal_code":null,"iso_country":"US","capabilities":{"voice":true,"SMS":true,"MMS":true},"beta":false},{"friendly_name":"310-359-1553","phone_number":"+13103591553","lata":null,"rate_center":"MALIBU","latitude":null,"longitude":null,"region":"CA","postal_code":null,"iso_country":"US","capabilities":{"voice":true,"SMS":true,"MMS":true},"beta":false},{"friendly_name":"310-359-1554","phone_number":"+13103591554","lata":null,"rate_center":"MALIBU","latitude":null,"longitude":null,"region":"CA","postal_code":null,"iso_country":"US","capabilities":{"voice":true,"SMS":true,"MMS":true},"beta":false},{"friendly_name":"310-359-1556","phone_number":"+13103591556","lata":null,"rate_center":"MALIBU","latitude":null,"longitude":null,"region":"CA","postal_code":null,"iso_country":"US","capabilities":{"voice":true,"SMS":true,"MMS":true},"beta":false},{"friendly_name":"310-359-1557","phone_number":"+13103591557","lata":null,"rate_center":"MALIBU","latitude":null,"longitude":null,"region":"CA","postal_code":null,"iso_country":"US","capabilities":{"voice":true,"SMS":true,"MMS":true},"beta":false},{"friendly_name":"310-359-1558","phone_number":"+13103591558","lata":null,"rate_center":"MALIBU","latitude":null,"longitude":null,"region":"CA","postal_code":null,"iso_country":"US","capabilities":{"voice":true,"SMS":true,"MMS":true},"beta":false},{"friendly_name":"310-359-1559","phone_number":"+13103591559","lata":null,"rate_center":"MALIBU","latitude":null,"longitude":null,"region":"CA","postal_code":null,"iso_country":"US","capabilities":{"voice":true,"SMS":true,"MMS":true},"beta":false},{"friendly_name":"310-359-1560","phone_number":"+13103591560","lata":null,"rate_center":"MALIBU","latitude":null,"longitude":null,"region":"CA","postal_code":null,"iso_country":"US","capabilities":{"voice":true,"SMS":true,"MMS":true},"beta":false},{"friendly_name":"310-359-1561","phone_number":"+13103591561","lata":null,"rate_center":"MALIBU","latitude":null,"longitude":null,"region":"CA","postal_code":null,"iso_country":"US","capabilities":{"voice":true,"SMS":true,"MMS":true},"beta":false},{"friendly_name":"310-359-1564","phone_number":"+13103591564","lata":null,"rate_center":"MALIBU","latitude":null,"longitude":null,"region":"CA","postal_code":null,"iso_country":"US","capabilities":{"voice":true,"SMS":true,"MMS":true},"beta":false},{"friendly_name":"310-359-1566","phone_number":"+13103591566","lata":null,"rate_center":"MALIBU","latitude":null,"longitude":null,"region":"CA","postal_code":null,"iso_country":"US","capabilities":{"voice":true,"SMS":true,"MMS":true},"beta":false},{"friendly_name":"310-359-1567","phone_number":"+13103591567","lata":null,"rate_center":"MALIBU","latitude":null,"longitude":null,"region":"CA","postal_code":null,"iso_country":"US","capabilities":{"voice":true,"SMS":true,"MMS":true},"beta":false},{"friendly_name":"310-359-1568","phone_number":"+13103591568","lata":null,"rate_center":"MALIBU","latitude":null,"longitude":null,"region":"CA","postal_code":null,"iso_country":"US","capabilities":{"voice":true,"SMS":true,"MMS":true},"beta":false},{"friendly_name":"310-359-1569","phone_number":"+13103591569","lata":null,"rate_center":"MALIBU","latitude":null,"longitude":null,"region":"CA","postal_code":null,"iso_country":"US","capabilities":{"voice":true,"SMS":true,"MMS":true},"beta":false},{"friendly_name":"310-359-1572","phone_number":"+13103591572","lata":null,"rate_center":"MALIBU","latitude":null,"longitude":null,"region":"CA","postal_code":null,"iso_country":"US","capabilities":{"voice":true,"SMS":true,"MMS":true},"beta":false},{"friendly_name":"310-359-1573","phone_number":"+13103591573","lata":null,"rate_center":"MALIBU","latitude":null,"longitude":null,"region":"CA","postal_code":null,"iso_country":"US","capabilities":{"voice":true,"SMS":true,"MMS":true},"beta":false},{"friendly_name":"310-359-1576","phone_number":"+13103591576","lata":null,"rate_center":"MALIBU","latitude":null,"longitude":null,"region":"CA","postal_code":null,"iso_country":"US","capabilities":{"voice":true,"SMS":true,"MMS":true},"beta":false},{"friendly_name":"310-359-1577","phone_number":"+13103591577","lata":null,"rate_center":"MALIBU","latitude":null,"longitude":null,"region":"CA","postal_code":null,"iso_country":"US","capabilities":{"voice":true,"SMS":true,"MMS":true},"beta":false},{"friendly_name":"310-359-1578","phone_number":"+13103591578","lata":null,"rate_center":"MALIBU","latitude":null,"longitude":null,"region":"CA","postal_code":null,"iso_country":"US","capabilities":{"voice":true,"SMS":true,"MMS":true},"beta":false},{"friendly_name":"310-359-1579","phone_number":"+13103591579","lata":null,"rate_center":"MALIBU","latitude":null,"longitude":null,"region":"CA","postal_code":null,"iso_country":"US","capabilities":{"voice":true,"SMS":true,"MMS":true},"beta":false},{"friendly_name":"310-359-1580","phone_number":"+13103591580","lata":null,"rate_center":"MALIBU","latitude":null,"longitude":null,"region":"CA","postal_code":null,"iso_country":"US","capabilities":{"voice":true,"SMS":true,"MMS":true},"beta":false},{"friendly_name":"310-359-1582","phone_number":"+13103591582","lata":null,"rate_center":"MALIBU","latitude":null,"longitude":null,"region":"CA","postal_code":null,"iso_country":"US","capabilities":{"voice":true,"SMS":true,"MMS":true},"beta":false},{"friendly_name":"310-359-1583","phone_number":"+13103591583","lata":null,"rate_center":"MALIBU","latitude":null,"longitude":null,"region":"CA","postal_code":null,"iso_country":"US","capabilities":{"voice":true,"SMS":true,"MMS":true},"beta":false},{"friendly_name":"310-359-1584","phone_number":"+13103591584","lata":null,"rate_center":"MALIBU","latitude":null,"longitude":null,"region":"CA","postal_code":null,"iso_country":"US","capabilities":{"voice":true,"SMS":true,"MMS":true},"beta":false},{"friendly_name":"310-359-1598","phone_number":"+13103591598","lata":null,"rate_center":"MALIBU","latitude":null,"longitude":null,"region":"CA","postal_code":null,"iso_country":"US","capabilities":{"voice":true,"SMS":true,"MMS":true},"beta":false},{"friendly_name":"310-359-1841","phone_number":"+13103591841","lata":null,"rate_center":"MALIBU","latitude":null,"longitude":null,"region":"CA","postal_code":null,"iso_country":"US","capabilities":{"voice":true,"SMS":true,"MMS":true},"beta":false},{"friendly_name":"310-359-1842","phone_number":"+13103591842","lata":null,"rate_center":"MALIBU","latitude":null,"longitude":null,"region":"CA","postal_code":null,"iso_country":"US","capabilities":{"voice":true,"SMS":true,"MMS":true},"beta":false},{"friendly_name":"310-359-1843","phone_number":"+13103591843","lata":null,"rate_center":"MALIBU","latitude":null,"longitude":null,"region":"CA","postal_code":null,"iso_country":"US","capabilities":{"voice":true,"SMS":true,"MMS":true},"beta":false},{"friendly_name":"310-359-1845","phone_number":"+13103591845","lata":null,"rate_center":"MALIBU","latitude":null,"longitude":null,"region":"CA","postal_code":null,"iso_country":"US","capabilities":{"voice":true,"SMS":true,"MMS":true},"beta":false},{"friendly_name":"310-359-1846","phone_number":"+13103591846","lata":null,"rate_center":"MALIBU","latitude":null,"longitude":null,"region":"CA","postal_code":null,"iso_country":"US","capabilities":{"voice":true,"SMS":true,"MMS":true},"beta":false},{"friendly_name":"310-359-1847","phone_number":"+13103591847","lata":null,"rate_center":"MALIBU","latitude":null,"longitude":null,"region":"CA","postal_code":null,"iso_country":"US","capabilities":{"voice":true,"SMS":true,"MMS":true},"beta":false},{"friendly_name":"310-359-1853","phone_number":"+13103591853","lata":null,"rate_center":"MALIBU","latitude":null,"longitude":null,"region":"CA","postal_code":null,"iso_country":"US","capabilities":{"voice":true,"SMS":true,"MMS":true},"beta":false},{"friendly_name":"310-359-1859","phone_number":"+13103591859","lata":null,"rate_center":"MALIBU","latitude":null,"longitude":null,"region":"CA","postal_code":null,"iso_country":"US","capabilities":{"voice":true,"SMS":true,"MMS":true},"beta":false},{"friendly_name":"310-359-1860","phone_number":"+13103591860","lata":null,"rate_center":"MALIBU","latitude":null,"longitude":null,"region":"CA","postal_code":null,"iso_country":"US","capabilities":{"voice":true,"SMS":true,"MMS":true},"beta":false},{"friendly_name":"310-359-1861","phone_number":"+13103591861","lata":null,"rate_center":"MALIBU","latitude":null,"longitude":null,"region":"CA","postal_code":null,"iso_country":"US","capabilities":{"voice":true,"SMS":true,"MMS":true},"beta":false},{"friendly_name":"310-359-1862","phone_number":"+13103591862","lata":null,"rate_center":"MALIBU","latitude":null,"longitude":null,"region":"CA","postal_code":null,"iso_country":"US","capabilities":{"voice":true,"SMS":true,"MMS":true},"beta":false}]}'} 13 | headers: {} 14 | status: {code: 200, message: OK} 15 | version: 1 16 | -------------------------------------------------------------------------------- /fixtures/cassettes/test_transcriptions: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: [application/json] 6 | Accept-Charset: [utf-8] 7 | Accept-Encoding: ['gzip, deflate'] 8 | Connection: [keep-alive] 9 | method: GET 10 | uri: https://myaccount.signalwire.com/2010-04-01/Accounts/signalwire-account-123/Transcriptions.json 11 | response: 12 | body: {string: '{"uri":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Transcriptions?Page=0\u0026PageSize=50","first_page_uri":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Transcriptions?Page=0\u0026PageSize=50","next_page_uri":null,"previous_page_uri":null,"page":0,"page_size":50,"transcriptions":[{"sid":"425e8224-c4b0-4725-ab2c-ec9d41adbbe9","api_version":"2010-04-01","account_sid":"signalwire-account-123","recording_sid":"e4c78e17-c0e2-441d-b5dd-39a6dad496f8","date_created":"Wed, 13 | 19 Dec 2018 16:28:16 +0000","date_updated":"Wed, 19 Dec 2018 16:30:04 +0000","duration":14,"price":0.0,"price_unit":"USD","status":"completed","transcription_text":"Hello. 14 | Hello? Hello? Hello? Hello? Hello? Recording. Recording, Recording, Recording, 15 | recording. Hang up. Hello?","uri":"/api/laml/2010-04-01/Accounts/signalwire-account-123/Transcriptions/425e8224-c4b0-4725-ab2c-ec9d41adbbe9.json"}]}'} 16 | headers: {} 17 | status: {code: 200, message: OK} 18 | version: 1 19 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel"] 3 | build-backend = "setuptools.build_meta:__legacy__" -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os, codecs, re 2 | from setuptools import setup, find_packages 3 | 4 | HERE = os.path.abspath(os.path.dirname(__file__)) 5 | 6 | def read(*file_paths): 7 | """Read file data.""" 8 | with codecs.open(os.path.join(HERE, *file_paths), "r") as file_in: 9 | return file_in.read() 10 | 11 | def get_version(): 12 | content = read('signalwire/__init__.py') 13 | match = re.search(r"^__version__ = '(.*)'$", content, re.MULTILINE) 14 | if match: 15 | return match.group(1) 16 | raise RuntimeError('Unable to find package version!') 17 | 18 | CLASSIFIERS = [ 19 | 'Development Status :: 5 - Production/Stable', 20 | 'Intended Audience :: Developers', 21 | 'Intended Audience :: Information Technology', 22 | 'Intended Audience :: Telecommunications Industry', 23 | 'License :: OSI Approved :: MIT License', 24 | 'Natural Language :: English', 25 | 'Topic :: Communications', 26 | 'Topic :: Software Development' 27 | ] 28 | 29 | setup( 30 | name='signalwire', 31 | version=get_version(), 32 | description='Client library for connecting to SignalWire.', 33 | long_description=read('README.md'), 34 | long_description_content_type="text/markdown", 35 | classifiers=CLASSIFIERS, 36 | url='https://github.com/signalwire/signalwire-python', 37 | author='SignalWire Team', 38 | author_email='open.source@signalwire.com', 39 | license='MIT', 40 | packages=find_packages(exclude=['tests', 'tests.*']), 41 | install_requires=[ 42 | 'aiohttp', 43 | 'six', 44 | 'twilio>=7.0.0,<7.6.0', 45 | ], 46 | python_requires='>=3.9', 47 | zip_safe=False 48 | ) 49 | -------------------------------------------------------------------------------- /signalwire/__init__.py: -------------------------------------------------------------------------------- 1 | name = "signalwire" 2 | 3 | __version__ = '2.1.1' 4 | -------------------------------------------------------------------------------- /signalwire/blade/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/signalwire/signalwire-python/cf7d2b4bb680e1ec9b33f60ce1c013389f490efb/signalwire/blade/__init__.py -------------------------------------------------------------------------------- /signalwire/blade/connection.py: -------------------------------------------------------------------------------- 1 | from aiohttp import WSMsgType, ClientSession, ClientWebSocketResponse 2 | import logging 3 | import re 4 | from signalwire.blade.messages.message import Message 5 | 6 | class Connection: 7 | def __init__(self, client, session=ClientSession): 8 | self._session = session() 9 | self.client = client 10 | self.host = self._checkHost(client.host) 11 | self.ws = None 12 | self._requests = {} 13 | 14 | @property 15 | def connected(self): 16 | return self.ws is not None and self.ws.closed == False 17 | 18 | def _checkHost(self, host): 19 | # TODO: move to a helper 20 | protocol = '' if re.match(r"^(ws|wss):\/\/", host) else 'wss://' 21 | return protocol + host 22 | 23 | async def send(self, message): 24 | logging.debug('SEND: \n' + message.to_json(indent=2)) 25 | await self.ws.send_str(message.to_json()) 26 | 27 | async def connect(self): 28 | logging.debug('Connecting to: {0}'.format(self.host)) 29 | self.ws = await self._session.ws_connect(self.host) 30 | 31 | async def read(self): 32 | async for msg in self.ws: 33 | logging.debug('RECV: \n' + msg.data) 34 | if msg.type == WSMsgType.TEXT: 35 | self.client.message_handler(Message.from_json(msg.data)) 36 | elif msg.type == WSMsgType.CLOSED: 37 | logging.info('WebSocket Closed!') 38 | break 39 | elif msg.type == WSMsgType.ERROR: 40 | logging.info('WebSocket Error!') 41 | break 42 | 43 | async def close(self): 44 | if self.connected: 45 | await self.ws.close() 46 | -------------------------------------------------------------------------------- /signalwire/blade/handler.py: -------------------------------------------------------------------------------- 1 | from .helpers import safe_invoke_callback 2 | 3 | GLOBAL = 'GLOBAL' 4 | _queue = {} 5 | 6 | def register(*, event, callback, suffix=GLOBAL): 7 | global _queue 8 | 9 | event = build_event_name(event, suffix) 10 | if event not in _queue: 11 | _queue[event] = [] 12 | _queue[event].append(callback) 13 | 14 | def register_once(*, event, callback, suffix=GLOBAL): 15 | global _queue 16 | 17 | def cb(*args): 18 | unregister(event=event, callback=cb, suffix=suffix) 19 | callback(*args) 20 | 21 | register(event=event, callback=cb, suffix=suffix) 22 | 23 | def unregister(*, event, callback=None, suffix=GLOBAL): 24 | global _queue 25 | 26 | if is_queued(event, suffix) is False: 27 | return False 28 | event = build_event_name(event, suffix) 29 | if callback is None: 30 | _queue[event] = [] 31 | else: 32 | for index, handler in enumerate(_queue[event]): 33 | if callback == handler: 34 | del _queue[event][index] 35 | 36 | if (_queue[event]) == 0: 37 | del _queue[event] 38 | 39 | return True 40 | 41 | def unregister_all(event): 42 | global _queue 43 | 44 | target = build_event_name(event, '') 45 | for event in list(_queue.keys()): 46 | if event.find(target) == 0: 47 | del _queue[event] 48 | 49 | def trigger(event, *args, suffix=GLOBAL, **kwargs): 50 | global _queue 51 | 52 | if is_queued(event, suffix) is False: 53 | return False 54 | event = build_event_name(event, suffix) 55 | for callback in _queue[event]: 56 | safe_invoke_callback(callback, *args, **kwargs) 57 | return True 58 | 59 | def is_queued(event, suffix=GLOBAL): 60 | global _queue 61 | 62 | event = build_event_name(event, suffix) 63 | return event in _queue and len(_queue[event]) > 0 64 | 65 | def queue_size(event, suffix=GLOBAL): 66 | global _queue 67 | 68 | if is_queued(event, suffix) is False: 69 | return 0 70 | event = build_event_name(event, suffix) 71 | return len(_queue[event]) 72 | 73 | def clear(): 74 | global _queue 75 | 76 | _queue = {} 77 | 78 | def build_event_name(event, suffix): 79 | return "{0}|{1}".format(event.strip(), suffix.strip()) 80 | -------------------------------------------------------------------------------- /signalwire/blade/helpers.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | def safe_invoke_callback(callback, *args, **kwargs): 4 | if asyncio.iscoroutinefunction(callback): 5 | asyncio.create_task(callback(*args, **kwargs)) 6 | else: 7 | callback(*args, **kwargs) 8 | -------------------------------------------------------------------------------- /signalwire/blade/messages/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/signalwire/signalwire-python/cf7d2b4bb680e1ec9b33f60ce1c013389f490efb/signalwire/blade/messages/__init__.py -------------------------------------------------------------------------------- /signalwire/blade/messages/connect.py: -------------------------------------------------------------------------------- 1 | from signalwire import __version__ 2 | from signalwire.blade.messages.message import Message 3 | 4 | class Connect(Message): 5 | 6 | MAJOR = 2 7 | MINOR = 3 8 | REVISION = 0 9 | 10 | def __init__(self, project, token): 11 | self.method = 'blade.connect' 12 | params = { 13 | 'version': { 14 | 'major': self.MAJOR, 15 | 'minor': self.MINOR, 16 | 'revision': self.REVISION 17 | }, 18 | 'authentication': { 19 | 'project': project, 20 | 'token': token 21 | }, 22 | 'agent': f'Python SDK/{__version__}' 23 | } 24 | super().__init__(params=params) 25 | -------------------------------------------------------------------------------- /signalwire/blade/messages/execute.py: -------------------------------------------------------------------------------- 1 | from signalwire.blade.messages.message import Message 2 | 3 | class Execute(Message): 4 | 5 | def __init__(self, params): 6 | self.method = 'blade.execute' 7 | super().__init__(params=params) 8 | -------------------------------------------------------------------------------- /signalwire/blade/messages/message.py: -------------------------------------------------------------------------------- 1 | import json 2 | from uuid import uuid4 3 | 4 | class Message: 5 | def __init__(self, **kwargs): 6 | self.jsonrpc = '2.0' 7 | self.id = kwargs.pop('id', str(uuid4())) 8 | if 'method' in kwargs: 9 | self.method = kwargs.pop('method') 10 | if 'params' in kwargs: 11 | self.params = kwargs.pop('params') 12 | if 'error' in kwargs: 13 | self.error = kwargs.pop('error') 14 | if 'result' in kwargs: 15 | self.result = kwargs.pop('result') 16 | 17 | def to_json(self, **kwargs): 18 | return json.dumps(self.__dict__, separators=(',', ':'), **kwargs) 19 | 20 | @classmethod 21 | def from_json(cls, json_str): 22 | json_dict = json.loads(json_str) 23 | return cls(**json_dict) 24 | -------------------------------------------------------------------------------- /signalwire/blade/messages/ping.py: -------------------------------------------------------------------------------- 1 | from signalwire.blade.messages.message import Message 2 | 3 | class Ping(Message): 4 | 5 | def __init__(self, timestamp=None): 6 | self.method = 'blade.ping' 7 | params = { 'timestamp': timestamp } if timestamp else {} 8 | super().__init__(params=params) 9 | -------------------------------------------------------------------------------- /signalwire/blade/messages/subscription.py: -------------------------------------------------------------------------------- 1 | from signalwire.blade.messages.message import Message 2 | 3 | class Subscription(Message): 4 | 5 | def __init__(self, params): 6 | self.method = 'blade.subscription' 7 | super().__init__(params=params) 8 | -------------------------------------------------------------------------------- /signalwire/fax_response/__init__.py: -------------------------------------------------------------------------------- 1 | from twilio.twiml.fax_response import FaxResponse as TwilioFaxResponse 2 | from twilio.twiml import TwiML 3 | 4 | class FaxResponse(TwilioFaxResponse): 5 | def reject(self, **kwargs): 6 | """ 7 | Create a element 8 | :param kwargs: additional attributes 9 | :returns: element 10 | """ 11 | return self.nest(Reject(**kwargs)) 12 | 13 | class Reject(TwiML): 14 | """ TwiML Verb """ 15 | 16 | def __init__(self, **kwargs): 17 | super(Reject, self).__init__(**kwargs) 18 | self.name = 'Reject' 19 | -------------------------------------------------------------------------------- /signalwire/messaging_response/__init__.py: -------------------------------------------------------------------------------- 1 | from twilio.twiml.messaging_response import MessagingResponse as TwilioMessagingResponse 2 | 3 | class MessagingResponse(TwilioMessagingResponse): 4 | pass 5 | 6 | 7 | -------------------------------------------------------------------------------- /signalwire/relay/__init__.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod, abstractproperty 2 | import logging 3 | from signalwire.blade.handler import register 4 | from .helpers import receive_contexts 5 | 6 | class BaseRelay(ABC): 7 | def __init__(self, client): 8 | self.client = client 9 | 10 | @abstractproperty 11 | def service(self): 12 | pass 13 | 14 | @abstractmethod 15 | def notification_handler(self, notification): 16 | pass 17 | 18 | def ctx_receive_unique(self, context): 19 | return f"{self.service}.ctx_receive.{context}" 20 | 21 | def ctx_state_unique(self, context): 22 | return f"{self.service}.ctx_state.{context}" 23 | 24 | async def receive(self, contexts, handler): 25 | try: 26 | await receive_contexts(self.client, contexts) 27 | for context in contexts: 28 | register(event=self.client.protocol, callback=handler, suffix=self.ctx_receive_unique(context)) 29 | except Exception as error: 30 | logging.error('receive error: {0}'.format(str(error))) 31 | 32 | async def state_change(self, contexts, handler): 33 | try: 34 | await receive_contexts(self.client, contexts) 35 | for context in contexts: 36 | register(event=self.client.protocol, callback=handler, suffix=self.ctx_state_unique(context)) 37 | except Exception as error: 38 | logging.error('state_change error: {0}'.format(str(error))) 39 | -------------------------------------------------------------------------------- /signalwire/relay/calling/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from signalwire.blade.handler import trigger 3 | from signalwire.relay import BaseRelay 4 | from .call import Call 5 | from .constants import Notification, DetectState, ConnectState 6 | 7 | class Calling(BaseRelay): 8 | def __init__(self, client): 9 | super().__init__(client) 10 | self.calls = [] 11 | 12 | @property 13 | def service(self): 14 | return 'calling' 15 | 16 | def notification_handler(self, notification): 17 | notification['params']['event_type'] = notification['event_type'] 18 | if notification['event_type'] == Notification.STATE: 19 | self._on_state(notification['params']) 20 | elif notification['event_type'] == Notification.RECEIVE: 21 | self._on_receive(notification['params']) 22 | elif notification['event_type'] == Notification.CONNECT: 23 | self._on_connect(notification['params']) 24 | elif notification['event_type'] == Notification.PLAY: 25 | self._on_play(notification['params']) 26 | elif notification['event_type'] == Notification.COLLECT: 27 | self._on_collect(notification['params']) 28 | elif notification['event_type'] == Notification.RECORD: 29 | self._on_record(notification['params']) 30 | elif notification['event_type'] == Notification.FAX: 31 | self._on_fax(notification['params']) 32 | elif notification['event_type'] == Notification.SEND_DIGITS: 33 | self._on_send_digits(notification['params']) 34 | elif notification['event_type'] == Notification.TAP: 35 | self._on_tap(notification['params']) 36 | elif notification['event_type'] == Notification.DETECT: 37 | self._on_detect(notification['params']) 38 | 39 | def new_call(self, *, call_type='phone', from_number, to_number, timeout=None): 40 | call = Call(calling=self) 41 | call.call_type = call_type 42 | call.from_number = from_number 43 | call.to_number = to_number 44 | call.timeout = timeout 45 | return call 46 | 47 | async def dial(self, *, call_type='phone', from_number, to_number, timeout=None): 48 | call = Call(calling=self) 49 | call.call_type = call_type 50 | call.from_number = from_number 51 | call.to_number = to_number 52 | call.timeout = timeout 53 | return await call.dial() 54 | 55 | def add_call(self, call): 56 | self.calls.append(call) 57 | 58 | def remove_call(self, call): 59 | try: 60 | self.calls.remove(call) 61 | except ValueError: 62 | logging.warn('Call to remove not found') 63 | 64 | def _get_call_by_id(self, call_id): 65 | for call in self.calls: 66 | if call.id == call_id: 67 | return call 68 | return None 69 | 70 | def _get_call_by_tag(self, tag): 71 | for call in self.calls: 72 | if call.tag == tag: 73 | return call 74 | return None 75 | 76 | def _on_state(self, params): 77 | call = self._get_call_by_id(params['call_id']) 78 | tag = params.get('tag', None) 79 | if call is None and tag is not None: 80 | call = self._get_call_by_tag(tag) 81 | 82 | if call is not None: 83 | if call.id is None: 84 | call.id = params['call_id'] 85 | call.node_id = params['node_id'] 86 | trigger(Notification.STATE, params, suffix=call.tag) # Notify components listening on State and Tag 87 | call._state_changed(params) 88 | elif 'call_id' in params and 'peer' in params: 89 | call = Call(calling=self, **params) 90 | else: 91 | logging.error('Unknown call {0}'.format(params['call_id'])) 92 | 93 | def _on_receive(self, params): 94 | call = Call(calling=self, **params) 95 | trigger(self.client.protocol, call, suffix=self.ctx_receive_unique(call.context)) 96 | 97 | def _on_connect(self, params): 98 | call = self._get_call_by_id(params['call_id']) 99 | state = params['connect_state'] 100 | if call is not None: 101 | try: 102 | if state == ConnectState.CONNECTED: 103 | call.peer = self._get_call_by_id(params['peer']['call_id']) 104 | else: 105 | call.peer = None 106 | except KeyError: 107 | pass 108 | trigger(Notification.CONNECT, params, suffix=call.tag) # Notify components listening on Connect and Tag 109 | trigger(call.tag, params, suffix='connect.stateChange') 110 | trigger(call.tag, params, suffix=f"connect.{state}") 111 | 112 | def _on_play(self, params): 113 | call = self._get_call_by_id(params['call_id']) 114 | if call is not None: 115 | trigger(Notification.PLAY, params, suffix=params['control_id']) # Notify components listening on Play and control_id 116 | trigger(call.tag, params, suffix='play.stateChange') 117 | trigger(call.tag, params, suffix=f"play.{params['state']}") 118 | 119 | def _on_collect(self, params): 120 | call = self._get_call_by_id(params['call_id']) 121 | if call is not None: 122 | trigger(Notification.COLLECT, params, suffix=params['control_id']) # Notify components listening on Collect and control_id 123 | trigger(call.tag, params, suffix='prompt') 124 | 125 | def _on_record(self, params): 126 | call = self._get_call_by_id(params['call_id']) 127 | if call is not None: 128 | trigger(Notification.RECORD, params, suffix=params['control_id']) # Notify components listening on Record and control_id 129 | trigger(call.tag, params, suffix='record.stateChange') 130 | trigger(call.tag, params, suffix=f"record.{params['state']}") 131 | 132 | def _on_fax(self, params): 133 | call = self._get_call_by_id(params['call_id']) 134 | if call is not None: 135 | trigger(Notification.FAX, params, suffix=params['control_id']) # Notify components listening on Fax and control_id 136 | trigger(call.tag, params, suffix='fax.stateChange') 137 | try: 138 | trigger(call.tag, params, suffix=f"fax.{params['fax']['type']}") 139 | except KeyError: 140 | pass 141 | 142 | def _on_send_digits(self, params): 143 | call = self._get_call_by_id(params['call_id']) 144 | if call is not None: 145 | trigger(Notification.SEND_DIGITS, params, suffix=params['control_id']) # Notify components listening on SendDigits and control_id 146 | trigger(call.tag, params, suffix='sendDigits.stateChange') 147 | trigger(call.tag, params, suffix=f"sendDigits.{params['state']}") 148 | 149 | def _on_tap(self, params): 150 | call = self._get_call_by_id(params['call_id']) 151 | if call is not None: 152 | trigger(Notification.TAP, params, suffix=params['control_id']) # Notify components listening on Tap and control_id 153 | trigger(call.tag, params, suffix='tap.stateChange') 154 | trigger(call.tag, params, suffix=f"tap.{params['state']}") 155 | 156 | def _on_detect(self, params): 157 | call = self._get_call_by_id(params['call_id']) 158 | if call is not None: 159 | trigger(Notification.DETECT, params, suffix=params['control_id']) # Notify components listening on Detect and control_id 160 | try: 161 | event = params['detect']['params']['event'] 162 | suffix = event if event == DetectState.FINISHED or event == DetectState.ERROR else 'update' 163 | trigger(call.tag, params['detect'], suffix=f"detect.{suffix}") 164 | except KeyError: 165 | pass 166 | -------------------------------------------------------------------------------- /signalwire/relay/calling/actions/__init__.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from ..components import BaseComponent 3 | 4 | class BaseAction(ABC): 5 | def __init__(self, component: BaseComponent): 6 | self.component = component 7 | 8 | @property 9 | def control_id(self): 10 | return self.component.control_id 11 | 12 | @property 13 | def payload(self): 14 | return self.component.payload 15 | 16 | @property 17 | def completed(self): 18 | return self.component.completed 19 | 20 | @property 21 | def state(self): 22 | return self.component.state 23 | -------------------------------------------------------------------------------- /signalwire/relay/calling/actions/connect_action.py: -------------------------------------------------------------------------------- 1 | from . import BaseAction 2 | from ..results.connect_result import ConnectResult 3 | 4 | class ConnectAction(BaseAction): 5 | 6 | @property 7 | def result(self): 8 | return ConnectResult(self.component) 9 | -------------------------------------------------------------------------------- /signalwire/relay/calling/actions/detect_action.py: -------------------------------------------------------------------------------- 1 | from . import BaseAction 2 | from ..results.detect_result import DetectResult 3 | from ..results.stop_result import StopResult 4 | 5 | class DetectAction(BaseAction): 6 | 7 | @property 8 | def result(self): 9 | return DetectResult(self.component) 10 | 11 | async def stop(self): 12 | result = await self.component.stop() 13 | return StopResult(result) 14 | -------------------------------------------------------------------------------- /signalwire/relay/calling/actions/fax_action.py: -------------------------------------------------------------------------------- 1 | from . import BaseAction 2 | from ..results.fax_result import FaxResult 3 | from ..results.stop_result import StopResult 4 | 5 | class FaxAction(BaseAction): 6 | 7 | @property 8 | def result(self): 9 | return FaxResult(self.component) 10 | 11 | async def stop(self): 12 | result = await self.component.stop() 13 | return StopResult(result) 14 | -------------------------------------------------------------------------------- /signalwire/relay/calling/actions/play_action.py: -------------------------------------------------------------------------------- 1 | from . import BaseAction 2 | from ..results.play_result import PlayResult, PlayPauseResult, PlayResumeResult, PlayVolumeResult 3 | from ..results.stop_result import StopResult 4 | 5 | class PlayAction(BaseAction): 6 | 7 | @property 8 | def result(self): 9 | return PlayResult(self.component) 10 | 11 | async def stop(self): 12 | result = await self.component.stop() 13 | return StopResult(result) 14 | 15 | async def pause(self): 16 | result = await self.component.pause() 17 | return PlayPauseResult(result) 18 | 19 | async def resume(self): 20 | result = await self.component.resume() 21 | return PlayResumeResult(result) 22 | 23 | async def volume(self, value): 24 | result = await self.component.volume(value) 25 | return PlayVolumeResult(result) 26 | -------------------------------------------------------------------------------- /signalwire/relay/calling/actions/prompt_action.py: -------------------------------------------------------------------------------- 1 | from . import BaseAction 2 | from ..results.prompt_result import PromptResult, PromptVolumeResult 3 | from ..results.stop_result import StopResult 4 | 5 | class PromptAction(BaseAction): 6 | 7 | @property 8 | def result(self): 9 | return PromptResult(self.component) 10 | 11 | async def stop(self): 12 | result = await self.component.stop() 13 | return StopResult(result) 14 | 15 | async def volume(self, value): 16 | result = await self.component.volume(value) 17 | return PromptVolumeResult(result) 18 | -------------------------------------------------------------------------------- /signalwire/relay/calling/actions/record_action.py: -------------------------------------------------------------------------------- 1 | from . import BaseAction 2 | from ..results.record_result import RecordResult 3 | from ..results.stop_result import StopResult 4 | 5 | class RecordAction(BaseAction): 6 | 7 | @property 8 | def result(self): 9 | return RecordResult(self.component) 10 | 11 | @property 12 | def url(self): 13 | return self.component.url 14 | 15 | async def stop(self): 16 | result = await self.component.stop() 17 | return StopResult(result) 18 | -------------------------------------------------------------------------------- /signalwire/relay/calling/actions/send_digits_action.py: -------------------------------------------------------------------------------- 1 | from . import BaseAction 2 | from ..results.send_digits_result import SendDigitsResult 3 | 4 | class SendDigitsAction(BaseAction): 5 | 6 | @property 7 | def result(self): 8 | return SendDigitsResult(self.component) 9 | -------------------------------------------------------------------------------- /signalwire/relay/calling/actions/tap_action.py: -------------------------------------------------------------------------------- 1 | from . import BaseAction 2 | from ..results.tap_result import TapResult 3 | from ..results.stop_result import StopResult 4 | 5 | class TapAction(BaseAction): 6 | 7 | @property 8 | def result(self): 9 | return TapResult(self.component) 10 | 11 | @property 12 | def source_device(self): 13 | return self.component.source_device 14 | 15 | async def stop(self): 16 | result = await self.component.stop() 17 | return StopResult(result) 18 | -------------------------------------------------------------------------------- /signalwire/relay/calling/components/__init__.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | from abc import ABC, abstractmethod, abstractproperty 4 | from uuid import uuid4 5 | from signalwire.blade.handler import register, unregister 6 | from signalwire.blade.messages.execute import Execute 7 | from ..constants import CallState 8 | from ...event import Event 9 | 10 | class BaseComponent(ABC): 11 | def __init__(self, call): 12 | self.call = call 13 | self.control_id = str(uuid4()) 14 | self.state = '' 15 | self.completed = False 16 | self.successful = False 17 | self.event = None 18 | self._future = None 19 | self._execute_result = None 20 | self._events_to_await = [] 21 | 22 | @abstractproperty 23 | def event_type(self): 24 | pass 25 | 26 | @abstractproperty 27 | def method(self): 28 | pass 29 | 30 | @abstractproperty 31 | def payload(self): 32 | pass 33 | 34 | @abstractmethod 35 | def notification_handler(self, params): 36 | pass 37 | 38 | def register(self): 39 | register(event=self.event_type, callback=self.notification_handler, suffix=self.control_id) 40 | check_id = self.call.id if self.call.id else self.call.tag 41 | register(event=check_id, callback=self.terminate, suffix=CallState.ENDED) 42 | 43 | def unregister(self): 44 | unregister(event=self.event_type, callback=self.notification_handler, suffix=self.control_id) 45 | if self.call.id: 46 | unregister(event=self.call.id, callback=self.terminate, suffix=CallState.ENDED) 47 | unregister(event=self.call.tag, callback=self.terminate, suffix=CallState.ENDED) 48 | 49 | async def execute(self): 50 | if self.call.ended == True: 51 | return self.terminate() 52 | self.register() 53 | if self.method is None: 54 | return 55 | msg = Execute({ 56 | 'protocol': self.call.calling.client.protocol, 57 | 'method': self.method, 58 | 'params': self.payload 59 | }) 60 | try: 61 | response = await self.call.calling.client.execute(msg) 62 | self._execute_result = response.get('result', {}) 63 | return self._execute_result 64 | except Exception as error: 65 | logging.error('Relay command failed: {0}'.format(str(error))) 66 | self.terminate() 67 | 68 | async def wait_for(self, *events): 69 | self._events_to_await = events 70 | self._future = asyncio.get_event_loop().create_future() 71 | await self.execute() 72 | try: 73 | await self._future 74 | except asyncio.CancelledError: 75 | pass 76 | 77 | def terminate(self, params={}): 78 | self.unregister() 79 | self.completed = True 80 | self.successful = False 81 | self.state = 'failed' 82 | if 'call_state' in params: 83 | self.event = Event(params['call_state'], params) 84 | if self.has_future(): 85 | self._future.cancel() 86 | 87 | def has_future(self): 88 | return isinstance(self._future, asyncio.Future) 89 | -------------------------------------------------------------------------------- /signalwire/relay/calling/components/answer.py: -------------------------------------------------------------------------------- 1 | from . import BaseComponent 2 | from ..constants import Method, Notification, CallState 3 | from ...event import Event 4 | 5 | class Answer(BaseComponent): 6 | def __init__(self, call): 7 | super().__init__(call) 8 | self.control_id = call.tag 9 | 10 | @property 11 | def event_type(self): 12 | return Notification.STATE 13 | 14 | @property 15 | def method(self): 16 | return Method.ANSWER 17 | 18 | @property 19 | def payload(self): 20 | return { 21 | 'node_id': self.call.node_id, 22 | 'call_id': self.call.id 23 | } 24 | 25 | def notification_handler(self, params): 26 | self.state = params.get('call_state', None) 27 | if self.state is None: 28 | return 29 | 30 | if self.state in self._events_to_await: 31 | self.unregister() 32 | self.completed = True 33 | self.successful = True 34 | self.event = Event(self.state, params) 35 | if self.has_future(): 36 | self._future.set_result(True) 37 | -------------------------------------------------------------------------------- /signalwire/relay/calling/components/awaiter.py: -------------------------------------------------------------------------------- 1 | from . import BaseComponent 2 | from ..constants import Notification, CallState 3 | from ...event import Event 4 | 5 | class Awaiter(BaseComponent): 6 | 7 | def __init__(self, call): 8 | super().__init__(call) 9 | self.control_id = call.tag 10 | 11 | @property 12 | def event_type(self): 13 | return Notification.STATE 14 | 15 | @property 16 | def method(self): 17 | return None 18 | 19 | @property 20 | def payload(self): 21 | return None 22 | 23 | def notification_handler(self, params): 24 | self.state = params.get('call_state', None) 25 | if self.state is None: 26 | return 27 | 28 | if self.state in self._events_to_await: 29 | self.unregister() 30 | self.completed = True 31 | self.successful = True 32 | self.event = Event(self.state, params) 33 | if self.has_future(): 34 | self._future.set_result(True) 35 | -------------------------------------------------------------------------------- /signalwire/relay/calling/components/base_fax.py: -------------------------------------------------------------------------------- 1 | from . import BaseComponent 2 | from ..constants import Notification, CallFaxState 3 | from ...event import Event 4 | from .decorators import stoppable 5 | 6 | @stoppable 7 | class BaseFax(BaseComponent): 8 | 9 | def __init__(self, call): 10 | super().__init__(call) 11 | self.direction = None 12 | self.identity = None 13 | self.remote_identity = None 14 | self.document = None 15 | self.pages = None 16 | 17 | @property 18 | def event_type(self): 19 | return Notification.FAX 20 | 21 | def notification_handler(self, params): 22 | try: 23 | fax = params['fax'] 24 | self.state = fax['type'] 25 | except KeyError: 26 | return 27 | 28 | self.completed = self.state != CallFaxState.PAGE 29 | if self.completed: 30 | self.unregister() 31 | self.event = Event(self.state, fax) 32 | self.successful = fax['params'].get('success', False) 33 | if self.successful: 34 | self.direction = fax['params'].get('direction', None) 35 | self.identity = fax['params'].get('identity', None) 36 | self.remote_identity = fax['params'].get('remote_identity', None) 37 | self.document = fax['params'].get('document', None) 38 | self.pages = fax['params'].get('pages', None) 39 | if self.has_future(): 40 | self._future.set_result(True) 41 | -------------------------------------------------------------------------------- /signalwire/relay/calling/components/connect.py: -------------------------------------------------------------------------------- 1 | from signalwire.relay.calling.components import BaseComponent 2 | from ..helpers import prepare_connect_devices, prepare_media_list 3 | from ..constants import Method, Notification, ConnectState 4 | from ...event import Event 5 | 6 | class Connect(BaseComponent): 7 | def __init__(self, call, devices, ringback=[]): 8 | super().__init__(call) 9 | self.control_id = call.tag 10 | self.devices = prepare_connect_devices(devices, call.from_number, call.timeout) 11 | self.ringback = prepare_media_list(ringback) 12 | 13 | @property 14 | def event_type(self): 15 | return Notification.CONNECT 16 | 17 | @property 18 | def method(self): 19 | return Method.CONNECT 20 | 21 | @property 22 | def payload(self): 23 | tmp = { 24 | 'node_id': self.call.node_id, 25 | 'call_id': self.call.id, 26 | 'devices': self.devices 27 | } 28 | if len(self.ringback) > 0: 29 | tmp['ringback'] = self.ringback 30 | return tmp 31 | 32 | def notification_handler(self, params): 33 | self.state = params.get('connect_state', None) 34 | if self.state is None: 35 | return 36 | self.completed = self.state != ConnectState.CONNECTING 37 | if self.completed: 38 | self.unregister() 39 | self.successful = self.state == ConnectState.CONNECTED 40 | self.event = Event(self.state, params) 41 | if self.has_future(): 42 | self._future.set_result(True) 43 | -------------------------------------------------------------------------------- /signalwire/relay/calling/components/decorators.py: -------------------------------------------------------------------------------- 1 | from signalwire.blade.messages.execute import Execute 2 | 3 | async def _execute(self, method): 4 | msg = Execute({ 5 | 'protocol': self.call.calling.client.protocol, 6 | 'method': method, 7 | 'params': { 8 | 'node_id': self.call.node_id, 9 | 'call_id': self.call.id, 10 | 'control_id': self.control_id 11 | } 12 | }) 13 | try: 14 | await self.call.calling.client.execute(msg) 15 | return True 16 | except Exception: 17 | return False 18 | 19 | def stoppable(cls): 20 | def stop(self): 21 | return _execute(self, f'{self.method}.stop') 22 | setattr(cls, 'stop', stop) 23 | return cls 24 | 25 | def pausable(cls): 26 | def pause(self): 27 | return _execute(self, f'{self.method}.pause') 28 | setattr(cls, 'pause', pause) 29 | return cls 30 | 31 | def resumable(cls): 32 | def resume(self): 33 | return _execute(self, f'{self.method}.resume') 34 | setattr(cls, 'resume', resume) 35 | return cls 36 | 37 | def has_volume_control(cls): 38 | async def volume(self, volume): 39 | msg = Execute({ 40 | 'protocol': self.call.calling.client.protocol, 41 | 'method': f'{self.method}.volume', 42 | 'params': { 43 | 'node_id': self.call.node_id, 44 | 'call_id': self.call.id, 45 | 'control_id': self.control_id, 46 | 'volume': float(volume) 47 | } 48 | }) 49 | try: 50 | await self.call.calling.client.execute(msg) 51 | return True 52 | except Exception: 53 | return False 54 | 55 | setattr(cls, 'volume', volume) 56 | return cls 57 | -------------------------------------------------------------------------------- /signalwire/relay/calling/components/detect.py: -------------------------------------------------------------------------------- 1 | from . import BaseComponent 2 | from ..constants import Method, Notification, DetectState, DetectType 3 | from ...event import Event 4 | from .decorators import stoppable 5 | 6 | @stoppable 7 | class Detect(BaseComponent): 8 | 9 | def __init__(self, call, detect_type, wait_for_beep=False, timeout=None, initial_timeout=None, end_silence_timeout=None, machine_voice_threshold=None, machine_words_threshold=None, tone=None, digits=None): 10 | super().__init__(call) 11 | self.detect_type = detect_type 12 | params = {} 13 | if initial_timeout is not None: 14 | params['initial_timeout'] = initial_timeout 15 | if end_silence_timeout is not None: 16 | params['end_silence_timeout'] = end_silence_timeout 17 | if machine_voice_threshold is not None: 18 | params['machine_voice_threshold'] = machine_voice_threshold 19 | if machine_words_threshold is not None: 20 | params['machine_words_threshold'] = machine_words_threshold 21 | if tone is not None: 22 | params['tone'] = tone 23 | if digits is not None: 24 | params['digits'] = digits 25 | self.detect = { 26 | 'type': self.detect_type, 27 | 'params': params 28 | } 29 | self.timeout = timeout 30 | self._wait_for_beep = wait_for_beep 31 | self._waiting_for_ready = False 32 | self.result = None 33 | self._results = [] 34 | 35 | @property 36 | def event_type(self): 37 | return Notification.DETECT 38 | 39 | @property 40 | def method(self): 41 | return Method.DETECT 42 | 43 | @property 44 | def payload(self): 45 | tmp = { 46 | 'node_id': self.call.node_id, 47 | 'call_id': self.call.id, 48 | 'control_id': self.control_id, 49 | 'detect': self.detect 50 | } 51 | if self.timeout: 52 | tmp['timeout'] = self.timeout 53 | return tmp 54 | 55 | def notification_handler(self, params): 56 | try: 57 | detect = params['detect'] 58 | self.detect_type = detect['type'] 59 | self.state = detect['params']['event'] 60 | except KeyError: 61 | return 62 | 63 | if self.state in [DetectState.FINISHED, DetectState.ERROR]: 64 | return self._complete(detect) 65 | 66 | if not self.has_future(): 67 | self._results.append(self.state) 68 | return 69 | 70 | if self.detect_type == DetectType.DIGIT: 71 | return self._complete(detect) 72 | 73 | if self._waiting_for_ready: 74 | if self.state == DetectState.READY: 75 | return self._complete(detect) 76 | return 77 | 78 | if self._wait_for_beep and self.state == DetectState.MACHINE: 79 | self._waiting_for_ready = True 80 | return 81 | 82 | if self.state in self._events_to_await: 83 | return self._complete(detect) 84 | 85 | def _complete(self, detect): 86 | self.unregister() 87 | self.completed = True 88 | self.event = Event(self.state, detect) 89 | if self.has_future(): 90 | # force READY/NOT_READY to MACHINE 91 | self.result = DetectState.MACHINE if self.state in [DetectState.READY, DetectState.NOT_READY] else self.state 92 | self.successful = self.state not in [DetectState.FINISHED, DetectState.ERROR] 93 | self._future.set_result(True) 94 | else: 95 | self.result = ','.join(self._results) 96 | self.successful = self.state != DetectState.ERROR 97 | -------------------------------------------------------------------------------- /signalwire/relay/calling/components/dial.py: -------------------------------------------------------------------------------- 1 | from signalwire.relay.calling.components import BaseComponent 2 | from ..constants import Method, Notification, CallState 3 | from ...event import Event 4 | 5 | class Dial(BaseComponent): 6 | def __init__(self, call): 7 | super().__init__(call) 8 | self.control_id = call.tag 9 | 10 | @property 11 | def event_type(self): 12 | return Notification.STATE 13 | 14 | @property 15 | def method(self): 16 | return Method.BEGIN 17 | 18 | @property 19 | def payload(self): 20 | return { 21 | 'tag': self.call.tag, 22 | 'device': self.call.device 23 | } 24 | 25 | def notification_handler(self, params): 26 | self.state = params.get('call_state', None) 27 | if self.state is None: 28 | return 29 | 30 | if self.state in self._events_to_await: 31 | self.unregister() 32 | self.completed = True 33 | self.successful = self.state == CallState.ANSWERED 34 | self.event = Event(self.state, params) 35 | if self.has_future(): 36 | self._future.set_result(True) 37 | -------------------------------------------------------------------------------- /signalwire/relay/calling/components/disconnect.py: -------------------------------------------------------------------------------- 1 | from signalwire.relay.calling.components import BaseComponent 2 | from ..constants import Method, Notification, ConnectState 3 | from ...event import Event 4 | 5 | class Disconnect(BaseComponent): 6 | def __init__(self, call): 7 | super().__init__(call) 8 | self.control_id = call.tag 9 | 10 | @property 11 | def event_type(self): 12 | return Notification.CONNECT 13 | 14 | @property 15 | def method(self): 16 | return Method.DISCONNECT 17 | 18 | @property 19 | def payload(self): 20 | return { 21 | 'node_id': self.call.node_id, 22 | 'call_id': self.call.id 23 | } 24 | 25 | def notification_handler(self, params): 26 | self.state = params.get('connect_state', None) 27 | if self.state is None: 28 | return 29 | self.completed = self.state != ConnectState.CONNECTING 30 | if self.completed: 31 | self.unregister() 32 | self.successful = self.state == ConnectState.DISCONNECTED 33 | self.event = Event(self.state, params) 34 | if self.has_future(): 35 | self._future.set_result(True) 36 | -------------------------------------------------------------------------------- /signalwire/relay/calling/components/fax_receive.py: -------------------------------------------------------------------------------- 1 | from .base_fax import BaseFax 2 | from ..constants import Method 3 | 4 | class FaxReceive(BaseFax): 5 | 6 | @property 7 | def method(self): 8 | return Method.RECEIVE_FAX 9 | 10 | @property 11 | def payload(self): 12 | return { 13 | 'node_id': self.call.node_id, 14 | 'call_id': self.call.id, 15 | 'control_id': self.control_id 16 | } 17 | -------------------------------------------------------------------------------- /signalwire/relay/calling/components/fax_send.py: -------------------------------------------------------------------------------- 1 | from .base_fax import BaseFax 2 | from ..constants import Method 3 | 4 | class FaxSend(BaseFax): 5 | 6 | def __init__(self, call, document, identity=None, header=None): 7 | super().__init__(call) 8 | self._document = document 9 | self._identity = identity 10 | self._header = header 11 | 12 | @property 13 | def method(self): 14 | return Method.SEND_FAX 15 | 16 | @property 17 | def payload(self): 18 | tmp = { 19 | 'node_id': self.call.node_id, 20 | 'call_id': self.call.id, 21 | 'control_id': self.control_id, 22 | 'document': self._document 23 | } 24 | if self._identity: 25 | tmp['identity'] = self._identity 26 | if self._header: 27 | tmp['header_info'] = self._header 28 | return tmp 29 | -------------------------------------------------------------------------------- /signalwire/relay/calling/components/hangup.py: -------------------------------------------------------------------------------- 1 | from . import BaseComponent 2 | from ..constants import Method, Notification, CallState 3 | from ...event import Event 4 | 5 | class Hangup(BaseComponent): 6 | def __init__(self, call, reason: str): 7 | super().__init__(call) 8 | self.control_id = call.tag 9 | self.reason = reason 10 | 11 | @property 12 | def event_type(self): 13 | return Notification.STATE 14 | 15 | @property 16 | def method(self): 17 | return Method.END 18 | 19 | @property 20 | def payload(self): 21 | return { 22 | 'node_id': self.call.node_id, 23 | 'call_id': self.call.id, 24 | 'reason': self.reason 25 | } 26 | 27 | def notification_handler(self, params): 28 | self.state = params.get('call_state', None) 29 | if self.state is None: 30 | return 31 | 32 | if self.state in self._events_to_await: 33 | self.unregister() 34 | self.completed = True 35 | self.successful = self.state == CallState.ENDED 36 | self.event = Event(self.state, params) 37 | if 'end_reason' in params: 38 | self.reason = params['end_reason'] 39 | if self.has_future(): 40 | self._future.set_result(True) 41 | -------------------------------------------------------------------------------- /signalwire/relay/calling/components/play.py: -------------------------------------------------------------------------------- 1 | from . import BaseComponent 2 | from ..constants import Method, Notification, CallPlayState 3 | from ..helpers import prepare_media_list 4 | from ...event import Event 5 | from .decorators import stoppable, pausable, resumable, has_volume_control 6 | 7 | @stoppable 8 | @pausable 9 | @resumable 10 | @has_volume_control 11 | class Play(BaseComponent): 12 | 13 | def __init__(self, call, play, volume=0): 14 | super().__init__(call) 15 | self.play = prepare_media_list(play) 16 | self.volume_value = float(volume) 17 | 18 | @property 19 | def event_type(self): 20 | return Notification.PLAY 21 | 22 | @property 23 | def method(self): 24 | return Method.PLAY 25 | 26 | @property 27 | def payload(self): 28 | tmp = { 29 | 'node_id': self.call.node_id, 30 | 'call_id': self.call.id, 31 | 'control_id': self.control_id, 32 | 'play': self.play 33 | } 34 | if self.volume_value != 0: 35 | tmp['volume'] = self.volume_value 36 | return tmp 37 | 38 | def notification_handler(self, params): 39 | self.state = params.get('state', None) 40 | if self.state is None: 41 | return 42 | 43 | self.completed = self.state != CallPlayState.PLAYING 44 | if self.completed: 45 | self.unregister() 46 | self.successful = self.state == CallPlayState.FINISHED 47 | self.event = Event(self.state, params) 48 | if self.has_future(): 49 | self._future.set_result(True) 50 | -------------------------------------------------------------------------------- /signalwire/relay/calling/components/prompt.py: -------------------------------------------------------------------------------- 1 | from . import BaseComponent 2 | from ..constants import Method, Notification, PromptState 3 | from ..helpers import prepare_collect_params, prepare_media_list 4 | from ...event import Event 5 | from .decorators import stoppable, has_volume_control 6 | 7 | @stoppable 8 | @has_volume_control 9 | class Prompt(BaseComponent): 10 | 11 | def __init__(self, call, prompt_type, play, **kwargs): 12 | super().__init__(call) 13 | self._collect = prepare_collect_params(prompt_type, kwargs) 14 | self.play = prepare_media_list(play) 15 | self.volume_value = kwargs.get('volume', None) 16 | self.prompt_type = prompt_type 17 | self.confidence = None 18 | self.input = None 19 | self.terminator = None 20 | 21 | @property 22 | def event_type(self): 23 | return Notification.COLLECT 24 | 25 | @property 26 | def method(self): 27 | return Method.PLAY_AND_COLLECT 28 | 29 | @property 30 | def payload(self): 31 | tmp = { 32 | 'node_id': self.call.node_id, 33 | 'call_id': self.call.id, 34 | 'control_id': self.control_id, 35 | 'play': self.play, 36 | 'collect': self._collect, 37 | } 38 | if self.volume_value is not None: 39 | tmp['volume'] = float(self.volume_value) 40 | return tmp 41 | 42 | def notification_handler(self, params): 43 | self.completed = True 44 | self.unregister() 45 | result = params['result'] 46 | if result['type'] == PromptState.SPEECH: 47 | self.state = 'successful' 48 | self.successful = True 49 | self.input = result['params']['text'] 50 | self.confidence = result['params']['confidence'] 51 | elif result['type'] == PromptState.DIGIT: 52 | self.state = 'successful' 53 | self.successful = True 54 | self.input = result['params']['digits'] 55 | self.terminator = result['params']['terminator'] 56 | else: 57 | self.state = result['type'] 58 | self.successful = False 59 | self.prompt_type = result['type'] 60 | self.event = Event(self.prompt_type, result) 61 | if self.has_future(): 62 | self._future.set_result(True) 63 | -------------------------------------------------------------------------------- /signalwire/relay/calling/components/record.py: -------------------------------------------------------------------------------- 1 | from . import BaseComponent 2 | from ..constants import Method, Notification, CallRecordState, RecordType 3 | from ..helpers import prepare_record_params 4 | from ...event import Event 5 | from .decorators import stoppable 6 | 7 | @stoppable 8 | class Record(BaseComponent): 9 | 10 | def __init__(self, call, record_type=RecordType.AUDIO, beep=None, record_format=None, stereo=None, direction=None, initial_timeout=None, end_silence_timeout=None, terminators=None): 11 | super().__init__(call) 12 | self.url = '' 13 | self.duration = 0 14 | self.size = 0 15 | self._record = prepare_record_params(record_type, beep, record_format, stereo, direction, initial_timeout, end_silence_timeout, terminators) 16 | 17 | @property 18 | def event_type(self): 19 | return Notification.RECORD 20 | 21 | @property 22 | def method(self): 23 | return Method.RECORD 24 | 25 | @property 26 | def payload(self): 27 | return { 28 | 'node_id': self.call.node_id, 29 | 'call_id': self.call.id, 30 | 'control_id': self.control_id, 31 | 'record': self._record 32 | } 33 | 34 | def notification_handler(self, params): 35 | self.state = params.get('state', None) 36 | if self.state is None: 37 | return 38 | 39 | self.completed = self.state != CallRecordState.RECORDING 40 | if self.completed: 41 | self.unregister() 42 | self.successful = self.state == CallRecordState.FINISHED 43 | self.event = Event(self.state, params) 44 | self.url = params.get('url', '') 45 | self.duration = params.get('duration', 0) 46 | self.size = params.get('size', 0) 47 | if self.has_future(): 48 | self._future.set_result(True) 49 | -------------------------------------------------------------------------------- /signalwire/relay/calling/components/send_digits.py: -------------------------------------------------------------------------------- 1 | from . import BaseComponent 2 | from ..constants import Method, Notification, CallSendDigitsState 3 | from ...event import Event 4 | 5 | class SendDigits(BaseComponent): 6 | 7 | def __init__(self, call, digits): 8 | super().__init__(call) 9 | self.digits = digits 10 | 11 | @property 12 | def event_type(self): 13 | return Notification.SEND_DIGITS 14 | 15 | @property 16 | def method(self): 17 | return Method.SEND_DIGITS 18 | 19 | @property 20 | def payload(self): 21 | return { 22 | 'node_id': self.call.node_id, 23 | 'call_id': self.call.id, 24 | 'control_id': self.control_id, 25 | 'digits': self.digits 26 | } 27 | 28 | def notification_handler(self, params): 29 | self.state = params.get('state', None) 30 | if self.state is None: 31 | return 32 | 33 | self.completed = self.state == CallSendDigitsState.FINISHED 34 | self.successful = self.completed 35 | self.unregister() 36 | self.event = Event(self.state, params) 37 | if self.has_future(): 38 | self._future.set_result(True) 39 | -------------------------------------------------------------------------------- /signalwire/relay/calling/components/tap.py: -------------------------------------------------------------------------------- 1 | from . import BaseComponent 2 | from ..constants import Method, Notification, CallTapState, TapType 3 | from ...event import Event 4 | from .decorators import stoppable 5 | 6 | @stoppable 7 | class Tap(BaseComponent): 8 | 9 | def __init__(self, call, audio_direction, target_type, target_addr=None, target_port=None, target_ptime=None, target_uri=None, rate=None, codec=None): 10 | super().__init__(call) 11 | self.tap = { 12 | 'type': TapType.AUDIO, 13 | 'params': { 'direction': audio_direction } 14 | } 15 | self.device = { 16 | 'type': target_type, 17 | 'params': {} 18 | } 19 | if target_addr is not None: 20 | self.device['params']['addr'] = target_addr 21 | if target_port is not None: 22 | self.device['params']['port'] = int(target_port) 23 | if target_ptime is not None: 24 | self.device['params']['ptime'] = int(target_ptime) 25 | if target_uri is not None: 26 | self.device['params']['uri'] = target_uri 27 | if rate is not None: 28 | self.device['params']['rate'] = int(rate) 29 | if codec is not None: 30 | self.device['params']['codec'] = codec 31 | 32 | @property 33 | def event_type(self): 34 | return Notification.TAP 35 | 36 | @property 37 | def method(self): 38 | return Method.TAP 39 | 40 | @property 41 | def payload(self): 42 | return { 43 | 'node_id': self.call.node_id, 44 | 'call_id': self.call.id, 45 | 'control_id': self.control_id, 46 | 'tap': self.tap, 47 | 'device': self.device 48 | } 49 | 50 | @property 51 | def source_device(self): 52 | try: 53 | return self._execute_result['source_device'] 54 | except Exception: 55 | return {} 56 | 57 | def notification_handler(self, params): 58 | self.state = params.get('state', None) 59 | if self.state is None: 60 | return 61 | if 'tap' in params: 62 | self.tap = params['tap'] 63 | if 'device' in params: 64 | self.device = params['device'] 65 | 66 | self.completed = self.state == CallTapState.FINISHED 67 | if self.completed: 68 | self.unregister() 69 | self.successful = True 70 | self.event = Event(self.state, params) 71 | if self.has_future(): 72 | self._future.set_result(True) 73 | -------------------------------------------------------------------------------- /signalwire/relay/calling/constants.py: -------------------------------------------------------------------------------- 1 | class Method: 2 | BEGIN = 'calling.begin' 3 | ANSWER = 'calling.answer' 4 | END = 'calling.end' 5 | CONNECT = 'calling.connect' 6 | DISCONNECT = 'calling.disconnect' 7 | PLAY = 'calling.play' 8 | RECORD = 'calling.record' 9 | RECEIVE_FAX = 'calling.receive_fax' 10 | SEND_FAX = 'calling.send_fax' 11 | SEND_DIGITS = 'calling.send_digits' 12 | TAP = 'calling.tap' 13 | DETECT = 'calling.detect' 14 | PLAY_AND_COLLECT = 'calling.play_and_collect' 15 | 16 | class Notification: 17 | STATE = 'calling.call.state' 18 | CONNECT = 'calling.call.connect' 19 | RECORD = 'calling.call.record' 20 | PLAY = 'calling.call.play' 21 | COLLECT = 'calling.call.collect' 22 | RECEIVE = 'calling.call.receive' 23 | FAX = 'calling.call.fax' 24 | DETECT = 'calling.call.detect' 25 | TAP = 'calling.call.tap' 26 | SEND_DIGITS = 'calling.call.send_digits' 27 | 28 | class CallState: 29 | ALL = ['created', 'ringing', 'answered', 'ending', 'ended'] 30 | NONE = 'none' 31 | CREATED = 'created' 32 | RINGING = 'ringing' 33 | ANSWERED = 'answered' 34 | ENDING = 'ending' 35 | ENDED = 'ended' 36 | 37 | class ConnectState: 38 | DISCONNECTED = 'disconnected' 39 | CONNECTING = 'connecting' 40 | CONNECTED = 'connected' 41 | FAILED = 'failed' 42 | 43 | class DisconnectReason: 44 | ERROR = 'error' 45 | BUSY = 'busy' 46 | 47 | class CallPlayState: 48 | PLAYING = 'playing' 49 | ERROR = 'error' 50 | FINISHED = 'finished' 51 | 52 | class PromptState: 53 | ERROR = 'error' 54 | NO_INPUT = 'no_input' 55 | NO_MATCH = 'no_match' 56 | DIGIT = 'digit' 57 | SPEECH = 'speech' 58 | 59 | class MediaType: 60 | AUDIO = 'audio' 61 | TTS = 'tts' 62 | SILENCE = 'silence' 63 | RINGTONE = 'ringtone' 64 | 65 | class CallRecordState: 66 | RECORDING = 'recording' 67 | NO_INPUT = 'no_input' 68 | FINISHED = 'finished' 69 | 70 | class RecordType: 71 | AUDIO = 'audio' 72 | 73 | class CallFaxState: 74 | PAGE = 'page' 75 | ERROR = 'error' 76 | FINISHED = 'finished' 77 | 78 | class CallSendDigitsState: 79 | FINISHED = 'finished' 80 | 81 | class CallTapState: 82 | TAPPING = 'tapping' 83 | FINISHED = 'finished' 84 | 85 | class TapType: 86 | AUDIO = 'audio' 87 | 88 | class DetectType: 89 | FAX = 'fax' 90 | MACHINE = 'machine' 91 | DIGIT = 'digit' 92 | 93 | class DetectState: 94 | ERROR = 'error' 95 | FINISHED = 'finished' 96 | CED = 'CED' 97 | CNG = 'CNG' 98 | MACHINE = 'MACHINE' 99 | HUMAN = 'HUMAN' 100 | UNKNOWN = 'UNKNOWN' 101 | READY = 'READY' 102 | NOT_READY = 'NOT_READY' 103 | -------------------------------------------------------------------------------- /signalwire/relay/calling/helpers.py: -------------------------------------------------------------------------------- 1 | def prepare_connect_devices(devices, default_from, default_timeout=None, nested=False): 2 | final = [] 3 | for device in devices: 4 | if isinstance(device, list): 5 | final.append(prepare_connect_devices(device, default_from, default_timeout, True)) 6 | elif isinstance(device, dict): 7 | params = { 8 | 'from_number': device.get('from_number', default_from), 9 | 'to_number': device.get('to_number', '') 10 | } 11 | timeout = device.get('timeout', default_timeout) 12 | if timeout: 13 | params['timeout'] = int(timeout) 14 | tmp = { 15 | 'type': device.get('call_type', 'phone'), 16 | 'params': params 17 | } 18 | final.append(tmp if nested else [tmp]) 19 | return final 20 | 21 | def prepare_media_list(media_list): 22 | final = [] 23 | for media in media_list: 24 | if not isinstance(media, dict): 25 | continue 26 | media_type = media.pop('type', '') 27 | final.append({ 28 | 'type': media_type, 29 | 'params': media 30 | }) 31 | return final 32 | 33 | def prepare_record_params(record_type, beep, record_format, stereo, direction, initial_timeout, end_silence_timeout, terminators): 34 | params = {} 35 | if isinstance(beep, bool): 36 | params['beep'] = beep 37 | if record_format is not None: 38 | params['format'] = record_format 39 | if isinstance(stereo, bool): 40 | params['stereo'] = stereo 41 | if direction is not None: 42 | params['direction'] = direction 43 | if initial_timeout is not None: 44 | params['initial_timeout'] = initial_timeout 45 | if end_silence_timeout is not None: 46 | params['end_silence_timeout'] = end_silence_timeout 47 | if terminators is not None: 48 | params['terminators'] = terminators 49 | return { record_type: params } 50 | 51 | def prepare_collect_params(prompt_type, params): 52 | collect = {} 53 | if prompt_type == 'both': 54 | collect['speech'] = {} 55 | collect['digits'] = {} 56 | elif prompt_type == 'digits': 57 | collect['digits'] = {} 58 | elif prompt_type == 'speech': 59 | collect['speech'] = {} 60 | 61 | # TODO: support partial_results 62 | if 'initial_timeout' in params: 63 | collect['initial_timeout'] = params['initial_timeout'] 64 | if 'digits_max' in params: 65 | collect['digits']['max'] = params['digits_max'] 66 | if 'digits_terminators' in params: 67 | collect['digits']['terminators'] = params['digits_terminators'] 68 | if 'digits_timeout' in params: 69 | collect['digits']['digit_timeout'] = params['digits_timeout'] 70 | if 'end_silence_timeout' in params: 71 | collect['speech']['end_silence_timeout'] = params['end_silence_timeout'] 72 | if 'speech_timeout' in params: 73 | collect['speech']['speech_timeout'] = params['speech_timeout'] 74 | if 'speech_language' in params: 75 | collect['speech']['language'] = params['speech_language'] 76 | if 'speech_hints' in params: 77 | collect['speech']['hints'] = params['speech_hints'] 78 | 79 | return collect 80 | 81 | def prepare_prompt_media_list(params, kwargs): 82 | # helper method to build media_list for prompt_ringtone and prompt_tts 83 | for k in ['duration', 'language', 'gender']: 84 | if k in kwargs: 85 | params[k] = kwargs[k] 86 | return params 87 | -------------------------------------------------------------------------------- /signalwire/relay/calling/results/__init__.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from ..components import BaseComponent 3 | 4 | class BaseResult(ABC): 5 | def __init__(self, component: BaseComponent): 6 | self.component = component 7 | 8 | @property 9 | def successful(self): 10 | return self.component.successful 11 | 12 | @property 13 | def event(self): 14 | return self.component.event 15 | -------------------------------------------------------------------------------- /signalwire/relay/calling/results/answer_result.py: -------------------------------------------------------------------------------- 1 | from . import BaseResult 2 | 3 | class AnswerResult(BaseResult): 4 | def __init__(self, component): 5 | super().__init__(component) 6 | -------------------------------------------------------------------------------- /signalwire/relay/calling/results/connect_result.py: -------------------------------------------------------------------------------- 1 | from . import BaseResult 2 | 3 | class ConnectResult(BaseResult): 4 | def __init__(self, component): 5 | super().__init__(component) 6 | 7 | @property 8 | def call(self): 9 | return self.component.call.peer 10 | -------------------------------------------------------------------------------- /signalwire/relay/calling/results/detect_result.py: -------------------------------------------------------------------------------- 1 | from . import BaseResult 2 | 3 | class DetectResult(BaseResult): 4 | 5 | @property 6 | def detect_type(self): 7 | return self.component.detect_type 8 | 9 | @property 10 | def result(self): 11 | return self.component.result 12 | -------------------------------------------------------------------------------- /signalwire/relay/calling/results/dial_result.py: -------------------------------------------------------------------------------- 1 | from . import BaseResult 2 | 3 | class DialResult(BaseResult): 4 | def __init__(self, component): 5 | super().__init__(component) 6 | 7 | @property 8 | def call(self): 9 | return self.component.call 10 | -------------------------------------------------------------------------------- /signalwire/relay/calling/results/disconnect_result.py: -------------------------------------------------------------------------------- 1 | from . import BaseResult 2 | 3 | class DisconnectResult(BaseResult): 4 | def __init__(self, component): 5 | super().__init__(component) 6 | -------------------------------------------------------------------------------- /signalwire/relay/calling/results/fax_result.py: -------------------------------------------------------------------------------- 1 | from . import BaseResult 2 | 3 | class FaxResult(BaseResult): 4 | 5 | @property 6 | def direction(self): 7 | return self.component.direction 8 | 9 | @property 10 | def identity(self): 11 | return self.component.identity 12 | 13 | @property 14 | def remote_identity(self): 15 | return self.component.remote_identity 16 | 17 | @property 18 | def document(self): 19 | return self.component.document 20 | 21 | @property 22 | def pages(self): 23 | return self.component.pages 24 | -------------------------------------------------------------------------------- /signalwire/relay/calling/results/hangup_result.py: -------------------------------------------------------------------------------- 1 | from . import BaseResult 2 | 3 | class HangupResult(BaseResult): 4 | def __init__(self, component): 5 | super().__init__(component) 6 | 7 | @property 8 | def reason(self): 9 | return self.component.reason 10 | -------------------------------------------------------------------------------- /signalwire/relay/calling/results/play_result.py: -------------------------------------------------------------------------------- 1 | from . import BaseResult 2 | 3 | class PlayResult(BaseResult): 4 | pass 5 | 6 | class PlayPauseResult: 7 | def __init__(self, successful): 8 | self.successful = successful 9 | 10 | class PlayResumeResult: 11 | def __init__(self, successful): 12 | self.successful = successful 13 | 14 | class PlayVolumeResult: 15 | def __init__(self, successful): 16 | self.successful = successful 17 | -------------------------------------------------------------------------------- /signalwire/relay/calling/results/prompt_result.py: -------------------------------------------------------------------------------- 1 | from . import BaseResult 2 | 3 | class PromptResult(BaseResult): 4 | 5 | @property 6 | def prompt_type(self): 7 | return self.component.prompt_type 8 | 9 | @property 10 | def result(self): 11 | return self.component.input 12 | 13 | @property 14 | def terminator(self): 15 | return self.component.terminator 16 | 17 | @property 18 | def confidence(self): 19 | return self.component.confidence 20 | 21 | class PromptVolumeResult: 22 | def __init__(self, successful): 23 | self.successful = successful 24 | -------------------------------------------------------------------------------- /signalwire/relay/calling/results/record_result.py: -------------------------------------------------------------------------------- 1 | from . import BaseResult 2 | 3 | class RecordResult(BaseResult): 4 | 5 | @property 6 | def url(self): 7 | return self.component.url 8 | 9 | @property 10 | def duration(self): 11 | return self.component.duration 12 | 13 | @property 14 | def size(self): 15 | return self.component.size 16 | -------------------------------------------------------------------------------- /signalwire/relay/calling/results/send_digits_result.py: -------------------------------------------------------------------------------- 1 | from . import BaseResult 2 | 3 | class SendDigitsResult(BaseResult): 4 | pass 5 | -------------------------------------------------------------------------------- /signalwire/relay/calling/results/stop_result.py: -------------------------------------------------------------------------------- 1 | class StopResult: 2 | def __init__(self, successful): 3 | self.successful = successful 4 | -------------------------------------------------------------------------------- /signalwire/relay/calling/results/tap_result.py: -------------------------------------------------------------------------------- 1 | from . import BaseResult 2 | 3 | class TapResult(BaseResult): 4 | 5 | @property 6 | def tap(self): 7 | return self.component.tap 8 | 9 | @property 10 | def source_device(self): 11 | return self.component.source_device 12 | 13 | @property 14 | def destination_device(self): 15 | return self.component.device 16 | -------------------------------------------------------------------------------- /signalwire/relay/client.py: -------------------------------------------------------------------------------- 1 | import os 2 | import asyncio 3 | import logging 4 | import signal 5 | import aiohttp 6 | from uuid import uuid4 7 | from signalwire.blade.connection import Connection 8 | from signalwire.blade.messages.connect import Connect 9 | from signalwire.blade.messages.ping import Ping 10 | from signalwire.blade.handler import register, unregister, trigger 11 | from .helpers import setup_protocol 12 | from .calling import Calling 13 | from .tasking import Tasking 14 | from .messaging import Messaging 15 | from .message_handler import handle_inbound_message 16 | from .constants import Constants, WebSocketEvents 17 | 18 | class Client: 19 | PING_DELAY = 10 20 | 21 | def __init__(self, project, token, host=Constants.HOST, connection=Connection): 22 | self.loop = asyncio.get_event_loop() 23 | self.host = host 24 | self.project = project 25 | self.token = token 26 | self.attach_signals() 27 | self.connection = connection(self) 28 | self.uuid = str(uuid4()) 29 | self._reconnect = True 30 | self.session_id = None 31 | self.signature = None 32 | self.protocol = None 33 | self.contexts = [] 34 | self._calling = None 35 | self._tasking = None 36 | self._messaging = None 37 | self._requests = {} 38 | self._idle = False 39 | self._executeQueue = asyncio.Queue() 40 | self._pingInterval = None 41 | log_level = os.getenv('LOG_LEVEL', 'INFO').upper() 42 | logging.basicConfig(level=log_level) 43 | 44 | @property 45 | def connected(self): 46 | return self.connection.connected 47 | 48 | @property 49 | def calling(self): 50 | if self._calling is None: 51 | self._calling = Calling(self) 52 | return self._calling 53 | 54 | @property 55 | def tasking(self): 56 | if self._tasking is None: 57 | self._tasking = Tasking(self) 58 | return self._tasking 59 | 60 | @property 61 | def messaging(self): 62 | if self._messaging is None: 63 | self._messaging = Messaging(self) 64 | return self._messaging 65 | 66 | async def execute(self, message): 67 | if message.id not in self._requests: 68 | self._requests[message.id] = self.loop.create_future() 69 | 70 | if self._idle == True or self.connected == False: 71 | await self._executeQueue.put(message) 72 | else: 73 | await self.connection.send(message) 74 | result = await self._requests[message.id] 75 | del self._requests[message.id] 76 | return result 77 | 78 | def connect(self): 79 | while self._reconnect: 80 | self.loop.run_until_complete(self._connect()) 81 | 82 | async def _connect(self): 83 | try: 84 | await self.connection.connect() 85 | asyncio.create_task(self.on_socket_open()) 86 | await self.connection.read() 87 | self.on_socket_close() 88 | except aiohttp.client_exceptions.ClientConnectorError as error: 89 | trigger(WebSocketEvents.ERROR, error, suffix=self.uuid) 90 | logging.warn(f"{self.host} seems down..") 91 | try: 92 | logging.info('Connection closed..') 93 | await asyncio.sleep(5) 94 | except asyncio.CancelledError: 95 | pass 96 | 97 | async def disconnect(self): 98 | logging.info('Disconnection..') 99 | self._idle = True 100 | self._reconnect = False 101 | await self.connection.close() 102 | await self.cancel_pending_tasks() 103 | logging.info(f"Bye bye!") 104 | self.loop.stop() 105 | 106 | def on(self, event, callback): 107 | register(event=event, callback=callback, suffix=self.uuid) 108 | return self 109 | 110 | def off(self, event, callback=None): 111 | unregister(event=event, callback=callback, suffix=self.uuid) 112 | return self 113 | 114 | async def cancel_pending_tasks(self): 115 | tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()] 116 | logging.info(f"Cancelling {len(tasks)} outstanding tasks..") 117 | [task.cancel() for task in tasks] 118 | await asyncio.gather(*tasks, return_exceptions=True) 119 | 120 | def handle_signal(self): 121 | asyncio.create_task(self.disconnect()) 122 | 123 | def attach_signals(self): 124 | for s in ('SIGHUP', 'SIGTERM', 'SIGINT'): 125 | try: 126 | self.loop.add_signal_handler(getattr(signal, s), self.handle_signal) 127 | except: 128 | pass 129 | 130 | def on_socket_close(self): 131 | if self._pingInterval: 132 | self._pingInterval.cancel() 133 | self.contexts = [] 134 | trigger(WebSocketEvents.CLOSE, suffix=self.uuid) 135 | 136 | async def on_socket_open(self): 137 | try: 138 | self._idle = False 139 | trigger(WebSocketEvents.OPEN, suffix=self.uuid) 140 | result = await self.execute(Connect(project=self.project, token=self.token)) 141 | self.session_id = result['sessionid'] 142 | self.signature = result['authorization']['signature'] 143 | self.protocol = await setup_protocol(self) 144 | await self._clearExecuteQueue() 145 | self._pong = True 146 | self.keepalive() 147 | logging.info('Client connected!') 148 | trigger(Constants.READY, self, suffix=self.uuid) 149 | except Exception as error: 150 | logging.error('Client setup error: {0}'.format(str(error))) 151 | await self.connection.close() 152 | 153 | def keepalive(self): 154 | async def send_ping(): 155 | if self._pong is False: 156 | return await self.connection.close() 157 | self._pong = False 158 | await self.execute(Ping()) 159 | self._pong = True 160 | asyncio.create_task(send_ping()) 161 | self._pingInterval = self.loop.call_later(self.PING_DELAY, self.keepalive) 162 | 163 | async def _clearExecuteQueue(self): 164 | while True: 165 | if self._executeQueue.empty(): 166 | break 167 | message = self._executeQueue.get_nowait() 168 | asyncio.create_task(self.connection.send(message)) 169 | 170 | def message_handler(self, msg): 171 | trigger(WebSocketEvents.MESSAGE, msg, suffix=self.uuid) 172 | if msg.id not in self._requests: 173 | return handle_inbound_message(self, msg) 174 | 175 | if hasattr(msg, 'error'): 176 | self._set_exception(msg.id, msg.error) 177 | elif hasattr(msg, 'result'): 178 | try: 179 | if msg.result['result']['code'] == '200': 180 | self._set_result(msg.id, msg.result) 181 | else: 182 | self._set_exception(msg.id, msg.result['result']) 183 | except KeyError: # not a Relay with "result.result.code" 184 | self._set_result(msg.id, msg.result) 185 | 186 | def _set_result(self, uuid, result): 187 | self._requests[uuid].set_result(result) 188 | 189 | def _set_exception(self, uuid, error): 190 | # TODO: replace with a custom exception 191 | self._requests[uuid].set_exception(Exception(error['message'])) 192 | -------------------------------------------------------------------------------- /signalwire/relay/constants.py: -------------------------------------------------------------------------------- 1 | class Constants: 2 | HOST = 'relay.signalwire.com' 3 | READY = 'ready' 4 | 5 | class WebSocketEvents: 6 | OPEN = 'signalwire.socket.open' 7 | CLOSE = 'signalwire.socket.close' 8 | MESSAGE = 'signalwire.socket.message' 9 | ERROR = 'signalwire.socket.error' 10 | 11 | class BladeMethod: 12 | NETCAST = 'blade.netcast' 13 | BROADCAST = 'blade.broadcast' 14 | DISCONNECT = 'blade.disconnect' 15 | -------------------------------------------------------------------------------- /signalwire/relay/consumer.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from signalwire.blade.helpers import safe_invoke_callback 3 | from .client import Client 4 | from .constants import Constants 5 | 6 | class Consumer: 7 | def __init__(self, **kwargs): 8 | self.project = kwargs.pop('project', None) 9 | self.token = kwargs.pop('token', None) 10 | self.host = kwargs.pop('host', Constants.HOST) 11 | self.contexts = kwargs.pop('contexts', []) 12 | self.Connection = kwargs.pop('Connection', None) 13 | self.client = None 14 | 15 | def setup(self): 16 | pass 17 | 18 | def ready(self): 19 | pass 20 | 21 | def teardown(self): 22 | pass 23 | 24 | def on_incoming_call(self, call): 25 | pass 26 | 27 | def on_task(self, message): 28 | pass 29 | 30 | def on_incoming_message(self, message): 31 | pass 32 | 33 | def on_message_state_change(self, message): 34 | pass 35 | 36 | def run(self): 37 | self.setup() 38 | self.check_requirements() 39 | self.client = Client(**self._build_relay_client_params()) 40 | self.client.on('ready', self._client_ready) 41 | self.client.connect() 42 | self.teardown() 43 | 44 | def check_requirements(self): 45 | if self.project is None or self.token is None or len(self.contexts) <= 0: 46 | raise Exception('project, token and contexts instance attributes are required!') 47 | 48 | def _build_relay_client_params(self): 49 | params = { 50 | 'project': self.project, 51 | 'token': self.token 52 | } 53 | if self.host is not None: 54 | params['host'] = self.host 55 | if self.Connection is not None: 56 | params['connection'] = self.Connection 57 | return params 58 | 59 | async def _client_ready(self, client): 60 | await self.client.calling.receive(self.contexts, self.on_incoming_call) 61 | await self.client.tasking.receive(self.contexts, self.on_task) 62 | await self.client.messaging.receive(self.contexts, self.on_incoming_message) 63 | await self.client.messaging.state_change(self.contexts, self.on_message_state_change) 64 | safe_invoke_callback(self.ready) 65 | -------------------------------------------------------------------------------- /signalwire/relay/event.py: -------------------------------------------------------------------------------- 1 | class Event: 2 | def __init__(self, name, payload): 3 | self.name = name 4 | self.payload = payload 5 | -------------------------------------------------------------------------------- /signalwire/relay/helpers.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from signalwire.blade.messages.execute import Execute 3 | from signalwire.blade.messages.subscription import Subscription 4 | 5 | # TODO: move protocols/methods to a constants file 6 | 7 | async def setup_protocol(client): 8 | setup_message = Execute({ 9 | 'protocol': 'signalwire', 10 | 'method': 'setup', 11 | 'params': {} 12 | }) 13 | response = await client.execute(setup_message) 14 | protocol = response['result']['protocol'] 15 | subscribe_message = Subscription({ 16 | 'command': 'add', 17 | 'protocol': protocol, 18 | 'channels': ['notifications'] 19 | }) 20 | response = await client.execute(subscribe_message) 21 | return protocol 22 | 23 | async def receive_contexts(client, contexts): 24 | contexts = list(set(contexts) - set(client.contexts)) 25 | if len(contexts) == 0: 26 | return True 27 | logging.info(f'Trying to receive contexts: {contexts}') 28 | message = Execute({ 29 | 'protocol': client.protocol, 30 | 'method': 'signalwire.receive', 31 | 'params': { 32 | 'contexts': contexts 33 | } 34 | }) 35 | response = await client.execute(message) 36 | logging.info(response['result']['message']) 37 | client.contexts = list(set(client.contexts + contexts)) 38 | return response['result'] 39 | -------------------------------------------------------------------------------- /signalwire/relay/message_handler.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from .constants import BladeMethod 3 | 4 | def handle_inbound_message(client, message): 5 | if message.method == BladeMethod.NETCAST: 6 | pass 7 | elif message.method == BladeMethod.BROADCAST: 8 | _blade_broadcast(client, message.params) 9 | elif message.method == BladeMethod.DISCONNECT: 10 | client._idle = True 11 | 12 | def _blade_broadcast(client, params): 13 | if client.protocol != params['protocol']: 14 | logging.warn('Client protocol mismatch.') 15 | return 16 | 17 | if params['event'] == 'queuing.relay.events': 18 | # FIXME: at the moment all these events are for calling. In the future we'll change the case 19 | client.calling.notification_handler(params['params']) 20 | elif params['event'] == 'queuing.relay.tasks': 21 | client.tasking.notification_handler(params['params']) 22 | elif params['event'] == 'queuing.relay.messaging': 23 | client.messaging.notification_handler(params['params']) 24 | -------------------------------------------------------------------------------- /signalwire/relay/messaging/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from signalwire.blade.messages.execute import Execute 3 | from signalwire.blade.handler import trigger 4 | from signalwire.relay import BaseRelay 5 | from .message import Message 6 | from .send_result import SendResult 7 | from .constants import Notification, Method 8 | 9 | class Messaging(BaseRelay): 10 | 11 | @property 12 | def service(self): 13 | return 'messaging' 14 | 15 | def notification_handler(self, notification): 16 | notification['params']['event_type'] = notification['event_type'] 17 | message = Message(notification['params']) 18 | if notification['event_type'] == Notification.STATE: 19 | trigger(self.client.protocol, message, suffix=self.ctx_state_unique(message.context)) 20 | elif notification['event_type'] == Notification.RECEIVE: 21 | trigger(self.client.protocol, message, suffix=self.ctx_receive_unique(message.context)) 22 | 23 | async def send(self, *, from_number, to_number, context, body=None, media=None, tags=None): 24 | params = { 25 | 'from_number': from_number, 26 | 'to_number': to_number, 27 | 'context': context 28 | } 29 | if body: 30 | params['body'] = body 31 | if media: 32 | params['media'] = media 33 | if tags: 34 | params['tags'] = tags 35 | 36 | message = Execute({ 37 | 'protocol': self.client.protocol, 38 | 'method': Method.SEND, 39 | 'params': params 40 | }) 41 | try: 42 | response = await self.client.execute(message) 43 | logging.info(response['result']['message']) 44 | return SendResult(response['result']) 45 | except Exception as error: 46 | logging.error(f'Messaging send error: {str(error)}') 47 | return SendResult() 48 | -------------------------------------------------------------------------------- /signalwire/relay/messaging/constants.py: -------------------------------------------------------------------------------- 1 | class Method: 2 | SEND = 'messaging.send' 3 | 4 | class Notification: 5 | STATE = 'messaging.state' 6 | RECEIVE = 'messaging.receive' 7 | 8 | class MessageState: 9 | QUEUED = 'queued' 10 | INITIATED = 'initiated' 11 | SENT = 'sent' 12 | DELIVERED = 'delivered' 13 | UNDELIVERED = 'undelivered' 14 | FAILED = 'failed' 15 | 16 | class DisconnectReason: 17 | ERROR = 'error' 18 | BUSY = 'busy' 19 | -------------------------------------------------------------------------------- /signalwire/relay/messaging/message.py: -------------------------------------------------------------------------------- 1 | class Message: 2 | def __init__(self, params={}): 3 | self.id = params.get('message_id', None) 4 | self.state = params.get('message_state', None) 5 | self.context = params.get('context', None) 6 | self.from_number = params.get('from_number', None) 7 | self.to_number = params.get('to_number', None) 8 | self.body = params.get('body', None) 9 | self.direction = params.get('direction', None) 10 | self.media = params.get('media', None) 11 | self.segments = params.get('segments', None) 12 | self.tags = params.get('tags', None) 13 | self.reason = params.get('reason', None) 14 | -------------------------------------------------------------------------------- /signalwire/relay/messaging/send_result.py: -------------------------------------------------------------------------------- 1 | class SendResult: 2 | def __init__(self, result={}): 3 | self.successful = result.get('code', None) == '200' 4 | self.message_id = result.get('message_id', None) 5 | -------------------------------------------------------------------------------- /signalwire/relay/task.py: -------------------------------------------------------------------------------- 1 | import json 2 | from base64 import b64encode 3 | from urllib.parse import urlencode 4 | from urllib.request import Request, urlopen 5 | from urllib.error import HTTPError 6 | from .constants import Constants 7 | 8 | class Task: 9 | def __init__(self, project, token, host=Constants.HOST): 10 | self.project = project 11 | self.token = token 12 | self.host = host 13 | 14 | def _authorization(self): 15 | data = f'{self.project}:{self.token}'.encode('utf-8') 16 | return 'Basic ' + str(b64encode(data), 'utf-8') 17 | 18 | def deliver(self, context, message): 19 | uri = f"https://{self.host}/api/relay/rest/tasks" 20 | data = json.dumps({ 'context': context, 'message': message }).encode('utf8') 21 | headers = { 22 | 'Authorization': self._authorization(), 23 | 'Content-Type': 'application/json', 24 | 'Content-Length': len(data) 25 | } 26 | req = Request(uri, data=data, headers=headers) 27 | try: 28 | response = urlopen(req) 29 | return response.getcode() == 204 30 | except HTTPError as error: 31 | print('Task deliver error: {0}'.format(str(error))) 32 | return False 33 | -------------------------------------------------------------------------------- /signalwire/relay/tasking/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from signalwire.blade.handler import trigger 3 | from signalwire.relay import BaseRelay 4 | 5 | class Tasking(BaseRelay): 6 | 7 | @property 8 | def service(self): 9 | return 'tasking' 10 | 11 | def notification_handler(self, notification): 12 | context = notification['context'] 13 | logging.info(f'Receive task in context: {context}') 14 | trigger(self.client.protocol, notification['message'], suffix=self.ctx_receive_unique(context)) 15 | -------------------------------------------------------------------------------- /signalwire/request_validator.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import hmac 3 | import json 4 | from hashlib import sha1 5 | from urllib.parse import urlparse 6 | from twilio.request_validator import compare, remove_port, add_port, RequestValidator as TwilioRequestValidator 7 | 8 | 9 | class RequestValidator(object): 10 | def __init__(self, token): 11 | self.compatibility_validator = TwilioRequestValidator(token) 12 | self.token = token.encode() 13 | 14 | def build_signature_with_compatibility(self, uri, params): 15 | return self.compatibility_validator.compute_signature(uri, params) 16 | 17 | def validate_with_compatibility(self, uri, params, signature): 18 | return self.compatibility_validator.validate(uri, params, signature) 19 | 20 | def build_signature(self, uri, params): 21 | s = uri 22 | if params: 23 | s += params 24 | 25 | hmac_buffer = hmac.new(self.token, s.encode(), sha1) 26 | result = hmac_buffer.digest().hex() 27 | 28 | return result.strip() 29 | 30 | def validate(self, uri, params, signature): 31 | 32 | if isinstance(params, str): 33 | parsed_uri = urlparse(uri) 34 | with_port = add_port(parsed_uri) 35 | without_port = remove_port(parsed_uri) 36 | 37 | valid_signature_without_port = compare( 38 | self.build_signature(without_port, params), 39 | signature 40 | ) 41 | valid_signature_with_port = compare( 42 | self.build_signature(with_port, params), 43 | signature 44 | ) 45 | 46 | if valid_signature_without_port or valid_signature_with_port: 47 | return True 48 | 49 | try: 50 | parsed_params = json.loads(params) 51 | return self.validate_with_compatibility(uri, parsed_params, signature) 52 | except json.JSONDecodeError as e: 53 | return False 54 | 55 | return self.validate_with_compatibility(uri, params, signature) 56 | -------------------------------------------------------------------------------- /signalwire/tests/__init__.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from unittest.mock import MagicMock 3 | from signalwire.blade.messages.message import Message 4 | 5 | def AsyncMock(*args, **kwargs): 6 | m = MagicMock(*args, **kwargs) 7 | 8 | async def mock_coro(*args, **kwargs): 9 | return m(*args, **kwargs) 10 | 11 | mock_coro.mock = m 12 | return mock_coro 13 | 14 | class MockedConnection: 15 | def __init__(self, client): 16 | self.client = client 17 | self.connect = AsyncMock() 18 | self.queue = asyncio.Queue() 19 | self.responses = [] 20 | self.close = AsyncMock() 21 | self.connected = True 22 | 23 | async def send(self, message): 24 | await self.queue.put(message.id) 25 | 26 | async def read(self): 27 | for response in self.responses: 28 | msg = Message.from_json(response) 29 | msg.id = await self.queue.get() 30 | self.client.message_handler(msg) 31 | -------------------------------------------------------------------------------- /signalwire/tests/blade/test_blade_messages.py: -------------------------------------------------------------------------------- 1 | from time import time 2 | from unittest import TestCase 3 | from signalwire import __version__ 4 | from signalwire.blade.messages.message import Message 5 | from signalwire.blade.messages.connect import Connect 6 | from signalwire.blade.messages.execute import Execute 7 | from signalwire.blade.messages.subscription import Subscription 8 | from signalwire.blade.messages.ping import Ping 9 | 10 | class TestBladeMessages(TestCase): 11 | def test_from_json_with_result(self): 12 | j_string = '{"jsonrpc":"2.0","id":"c0b3cabb-e7f8-445d-b3eb-49add1519300","result":{"protocol":"signalwire_xxx","command":"add","subscribe_channels":["notifications"]}}' 13 | msg = Message.from_json(j_string) 14 | 15 | self.assertEqual(msg.id, 'c0b3cabb-e7f8-445d-b3eb-49add1519300') 16 | self.assertEqual(msg.result['protocol'], 'signalwire_xxx') 17 | self.assertEqual(msg.result['subscribe_channels'], ['notifications']) 18 | 19 | def test_from_json_with_error(self): 20 | j_string = '{"jsonrpc":"2.0","id":"9ca3c540-e622-420a-a975-58d0abd54abc","error":{"code":-32002,"message":"error description"}}' 21 | msg = Message.from_json(j_string) 22 | 23 | self.assertEqual(msg.id, '9ca3c540-e622-420a-a975-58d0abd54abc') 24 | self.assertEqual(msg.error['code'], -32002) 25 | self.assertEqual(msg.error['message'], 'error description') 26 | 27 | def test_from_json_with_params(self): 28 | j_string = '{"jsonrpc":"2.0","id":"a691b004-1fc7-4e88-8428-9f1bfed6bf96","method":"blade.broadcast","params":{"event":"relay.test"}}' 29 | msg = Message.from_json(j_string) 30 | 31 | self.assertEqual(msg.id, 'a691b004-1fc7-4e88-8428-9f1bfed6bf96') 32 | self.assertEqual(msg.params['event'], 'relay.test') 33 | self.assertEqual(msg.method, 'blade.broadcast') 34 | 35 | def test_to_json(self): 36 | msg = Message(params={'test': 'hello world!'}) 37 | msg.id = 'mocked' 38 | 39 | self.assertEqual(msg.to_json(), '{"jsonrpc":"2.0","id":"mocked","params":{"test":"hello world!"}}') 40 | 41 | def test_connect(self): 42 | msg = Connect(project='project', token='token') 43 | msg.id = 'mocked' 44 | 45 | self.assertEqual(msg.method, 'blade.connect') 46 | self.assertEqual(msg.params['authentication']['project'], 'project') 47 | self.assertEqual(msg.params['authentication']['token'], 'token') 48 | self.assertEqual(msg.to_json(), '{{"method":"blade.connect","jsonrpc":"2.0","id":"mocked","params":{{"version":{{"major":{0},"minor":{1},"revision":{2}}},"authentication":{{"project":"project","token":"token"}},"agent":"Python SDK/{3}"}}}}'.format(Connect.MAJOR, Connect.MINOR, Connect.REVISION, __version__)) 49 | 50 | def test_connect_kwargs(self): 51 | msg = Connect(token='token', project='project') 52 | msg.id = 'mocked' 53 | self.assertEqual(msg.to_json(), '{{"method":"blade.connect","jsonrpc":"2.0","id":"mocked","params":{{"version":{{"major":{0},"minor":{1},"revision":{2}}},"authentication":{{"project":"project","token":"token"}},"agent":"Python SDK/{3}"}}}}'.format(Connect.MAJOR, Connect.MINOR, Connect.REVISION, __version__)) 54 | 55 | def test_execute(self): 56 | msg = Execute({ 57 | 'protocol': 'proto', 58 | 'method': 'py.test', 59 | 'params': { 60 | 'nested': True 61 | } 62 | }) 63 | msg.id = 'mocked' 64 | 65 | self.assertEqual(msg.method, 'blade.execute') 66 | self.assertEqual(msg.to_json(), '{"method":"blade.execute","jsonrpc":"2.0","id":"mocked","params":{"protocol":"proto","method":"py.test","params":{"nested":true}}}') 67 | 68 | def test_subscription(self): 69 | msg = Subscription({ 70 | 'protocol': 'proto', 71 | 'command': 'add', 72 | 'channels': ['notif'] 73 | }) 74 | msg.id = 'mocked' 75 | 76 | self.assertEqual(msg.method, 'blade.subscription') 77 | self.assertEqual(msg.to_json(), '{"method":"blade.subscription","jsonrpc":"2.0","id":"mocked","params":{"protocol":"proto","command":"add","channels":["notif"]}}') 78 | 79 | def test_ping_without_ts(self): 80 | msg = Ping() 81 | msg.id = 'mocked' 82 | 83 | self.assertEqual(msg.method, 'blade.ping') 84 | self.assertEqual(msg.to_json(), '{"method":"blade.ping","jsonrpc":"2.0","id":"mocked","params":{}}') 85 | 86 | def test_ping_with_ts(self): 87 | ts = time() 88 | msg = Ping(ts) 89 | msg.id = 'mocked' 90 | 91 | self.assertEqual(msg.method, 'blade.ping') 92 | self.assertEqual(msg.to_json(), f'{{"method":"blade.ping","jsonrpc":"2.0","id":"mocked","params":{{"timestamp":{ts}}}}}') 93 | -------------------------------------------------------------------------------- /signalwire/tests/blade/test_handler.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from unittest.mock import Mock 3 | from signalwire.blade.handler import register, register_once, unregister, unregister_all, trigger, queue_size, is_queued, clear 4 | 5 | class TestBladeMessages(TestCase): 6 | def setUp(self): 7 | clear() 8 | 9 | def test_register(self): 10 | register(event='event_name', callback=lambda x: print(x)) 11 | register(event='event_name', callback=lambda x: print(x)) 12 | register(event='event_name', callback=lambda x: print(x)) 13 | 14 | self.assertTrue(is_queued('event_name')) 15 | self.assertEqual(queue_size('event_name'), 3) 16 | 17 | def test_register_with_unique_id(self): 18 | register(event='event_name', callback=lambda x: print(x), suffix='xxx') 19 | register(event='event_name', callback=lambda x: print(x), suffix='yyy') 20 | register(event='event_name', callback=lambda x: print(x), suffix='zzz') 21 | 22 | self.assertFalse(is_queued('event_name')) 23 | self.assertEqual(queue_size('event_name', 'xxx'), 1) 24 | self.assertEqual(queue_size('event_name', 'yyy'), 1) 25 | 26 | def test_register_once(self): 27 | mock = Mock() 28 | register_once(event='event_name', callback=mock) 29 | self.assertEqual(queue_size('event_name'), 1) 30 | trigger('event_name', 'custom data') 31 | self.assertEqual(queue_size('event_name'), 0) 32 | trigger('event_name', 'custom data') 33 | trigger('event_name', 'custom data') 34 | mock.assert_called_once() 35 | 36 | def test_register_once_with_unique_id(self): 37 | mock = Mock() 38 | register_once(event='event_name', callback=mock, suffix='uuid') 39 | self.assertEqual(queue_size('event_name', 'uuid'), 1) 40 | trigger('event_name', 'custom data', suffix='uuid') 41 | self.assertEqual(queue_size('event_name'), 0) 42 | trigger('event_name', 'custom data', suffix='uuid') 43 | trigger('event_name', 'custom data', suffix='uuid') 44 | mock.assert_called_once() 45 | 46 | def test_unregister(self): 47 | mock = Mock() 48 | register(event='event_name', callback=mock) 49 | self.assertEqual(queue_size('event_name'), 1) 50 | 51 | unregister(event='event_name', callback=mock) 52 | self.assertEqual(queue_size('event_name'), 0) 53 | trigger('event_name', 'custom data') 54 | mock.assert_not_called() 55 | 56 | def test_unregister_with_unique_id(self): 57 | mock = Mock() 58 | register(event='event_name', callback=mock, suffix='xxx') 59 | self.assertEqual(queue_size('event_name', 'xxx'), 1) 60 | 61 | unregister(event='event_name', callback=mock, suffix='xxx') 62 | self.assertEqual(queue_size('event_name', 'xxx'), 0) 63 | trigger('event_name', 'custom data', 'xxx') 64 | mock.assert_not_called() 65 | 66 | def test_unregister_without_callbak(self): 67 | mock = Mock() 68 | register(event='event_name', callback=mock) 69 | self.assertEqual(queue_size('event_name'), 1) 70 | 71 | unregister(event='event_name') 72 | self.assertEqual(queue_size('event_name'), 0) 73 | trigger('event_name', 'custom data') 74 | mock.assert_not_called() 75 | 76 | def test_unregister_all(self): 77 | mock = Mock() 78 | register(event='event_name', callback=mock) 79 | register(event='event_name', callback=mock, suffix='t1') 80 | register(event='event_name', callback=mock, suffix='t2') 81 | unregister_all('event_name') 82 | trigger('event_name', 'custom data') 83 | trigger('event_name', 'custom data', suffix='t1') 84 | trigger('event_name', 'custom data', suffix='t2') 85 | mock.assert_not_called() 86 | 87 | def test_clear(self): 88 | register(event='event_name', callback=lambda x: print(x)) 89 | self.assertEqual(queue_size('event_name'), 1) 90 | clear() 91 | self.assertEqual(queue_size('event_name'), 0) 92 | 93 | def test_trigger(self): 94 | mock = Mock() 95 | register(event='event_name', callback=mock) 96 | mock.assert_not_called() 97 | trigger('event_name', 'custom data') 98 | mock.assert_called_once() 99 | 100 | def test_trigger_with_unique_id(self): 101 | mock1 = Mock() 102 | register(event='event_name', callback=mock1, suffix='some_uuid') 103 | mock2 = Mock() 104 | register(event='event_name', callback=mock2, suffix='other_uuid') 105 | mock1.assert_not_called() 106 | mock2.assert_not_called() 107 | trigger('event_name', 'custom data', suffix='some_uuid') 108 | mock1.assert_called_once() 109 | mock2.assert_not_called() 110 | -------------------------------------------------------------------------------- /signalwire/tests/relay/calling/conftest.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pytest 3 | from signalwire.tests import MockedConnection, AsyncMock 4 | from signalwire.relay.calling import Call 5 | 6 | @pytest.fixture(scope='function') 7 | def relay_calling(relay_client): 8 | relay_client.protocol = 'signalwire-proto-test' 9 | response = json.loads('{"requester_nodeid":"uuid","responder_nodeid":"uuid","result":{"code":"200","message":"Receiving all inbound related to the requested relay contexts and available scopes"}}') 10 | relay_client.execute = AsyncMock(return_value=response) 11 | return relay_client.calling 12 | 13 | @pytest.fixture() 14 | def relay_call(relay_calling): 15 | params = json.loads('{"call_state":"created","context":"office","device":{"type":"phone","params":{"from_number":"+12029999999","to_number":"+12028888888"}},"direction":"inbound","call_id":"call-id","node_id":"node-id","tag":"call-tag"}') 16 | return Call(calling=relay_calling, **params) 17 | 18 | @pytest.fixture(scope='function') 19 | def success_response(): 20 | response = json.loads('{"requester_nodeid":"uuid","responder_nodeid":"uuid","result":{"code":"200","message":"Message"}}') 21 | return AsyncMock(return_value=response) 22 | 23 | @pytest.fixture(scope='function') 24 | def fail_response(): 25 | # response = json.loads('{"requester_nodeid":"uuid","responder_nodeid":"uuid","result":{"code":"400","message":"Some error"}}') 26 | return AsyncMock(side_effect=Exception('Some error')) 27 | -------------------------------------------------------------------------------- /signalwire/tests/relay/calling/test_call.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | import pytest 4 | from unittest.mock import Mock 5 | 6 | def test_init_options(relay_call): 7 | assert relay_call.id == 'call-id' 8 | assert relay_call.node_id == 'node-id' 9 | assert relay_call.call_type == 'phone' 10 | assert relay_call.from_number == '+12029999999' 11 | assert relay_call.to_number == '+12028888888' 12 | assert relay_call.state == 'created' 13 | assert relay_call.context == 'office' 14 | assert relay_call.timeout is None 15 | 16 | def test_device(relay_call): 17 | assert relay_call.device == {'type':'phone','params':{'from_number':'+12029999999','to_number':'+12028888888'}} 18 | 19 | async def _fire(calling, notification): 20 | calling.notification_handler(notification) 21 | 22 | def test_on_method(relay_call): 23 | on_answered = Mock() 24 | relay_call.on('stateChange', on_answered) 25 | relay_call.on('answered', on_answered) 26 | on_ended = Mock() 27 | relay_call.on('ended', on_ended) 28 | answered_event = json.loads('{"event_type":"calling.call.state","event_channel":"signalwire-proto-test","timestamp":1570204684.1133151,"project_id":"project-uuid","space_id":"space-uuid","params":{"call_state":"answered","direction":"outbound","device":{"type":"phone","params":{"from_number":"+12029999999","to_number":"+12028888888"}},"call_id":"call-id","node_id":"node-id","tag":"call-tag"}}') 29 | relay_call.calling.notification_handler(answered_event) 30 | assert on_answered.call_count == 2 31 | on_ended.assert_not_called() 32 | ended_event = json.loads('{"event_type":"calling.call.state","event_channel":"signalwire-proto-test","timestamp":1570204684.1133151,"project_id":"project-uuid","space_id":"space-uuid","params":{"call_state":"ended","direction":"outbound","device":{"type":"phone","params":{"from_number":"+12029999999","to_number":"+12028888888"}},"end_reason":"noAnswer","call_id":"call-id","node_id":"node-id","tag":"call-tag"}}') 33 | relay_call.calling.notification_handler(ended_event) 34 | on_ended.assert_called_once() 35 | 36 | def test_off_method(relay_call): 37 | on_answered = Mock() 38 | relay_call.on('stateChange', on_answered) 39 | relay_call.on('answered', on_answered) 40 | 41 | relay_call.off('stateChange', on_answered) 42 | relay_call.off('answered') 43 | answered_event = json.loads('{"event_type":"calling.call.state","event_channel":"signalwire-proto-test","timestamp":1570204684.1133151,"project_id":"project-uuid","space_id":"space-uuid","params":{"call_state":"answered","direction":"outbound","device":{"type":"phone","params":{"from_number":"+12029999999","to_number":"+12028888888"}},"call_id":"call-id","node_id":"node-id","tag":"call-tag"}}') 44 | relay_call.calling.notification_handler(answered_event) 45 | on_answered.assert_not_called() 46 | 47 | @pytest.mark.asyncio 48 | async def test_answer_with_success(success_response, relay_call): 49 | relay_call.calling.client.execute = success_response 50 | payload = json.loads('{"event_type":"calling.call.state","event_channel":"signalwire-proto-test","timestamp":1570204684.1133151,"project_id":"project-uuid","space_id":"space-uuid","params":{"call_state":"answered","direction":"outbound","device":{"type":"phone","params":{"from_number":"+12029999999","to_number":"+12028888888"}},"call_id":"call-id","node_id":"node-id","tag":"call-tag"}}') 51 | asyncio.create_task(_fire(relay_call.calling, payload)) 52 | result = await relay_call.answer() 53 | 54 | assert result.successful 55 | msg = relay_call.calling.client.execute.mock.call_args[0][0] 56 | assert msg.params == json.loads('{"protocol":"signalwire-proto-test","method":"calling.answer","params":{"call_id":"call-id","node_id":"node-id"}}') 57 | relay_call.calling.client.execute.mock.assert_called_once() 58 | 59 | @pytest.mark.asyncio 60 | async def test_answer_with_failure(fail_response, relay_call): 61 | relay_call.calling.client.execute = fail_response 62 | result = await relay_call.answer() 63 | assert not result.successful 64 | relay_call.calling.client.execute.mock.assert_called_once() 65 | 66 | @pytest.mark.asyncio 67 | async def test_hangup(success_response, relay_call): 68 | relay_call.calling.client.execute = success_response 69 | payload = json.loads('{"event_type":"calling.call.state","event_channel":"signalwire-proto-test","timestamp":1570204684.1133151,"project_id":"project-uuid","space_id":"space-uuid","params":{"call_state":"ended","direction":"outbound","device":{"type":"phone","params":{"from_number":"+12029999999","to_number":"+12028888888"}},"end_reason":"noAnswer","call_id":"call-id","node_id":"node-id","tag":"call-tag"}}') 70 | asyncio.create_task(_fire(relay_call.calling, payload)) 71 | result = await relay_call.hangup() 72 | 73 | assert result.successful 74 | msg = relay_call.calling.client.execute.mock.call_args[0][0] 75 | assert msg.params == json.loads('{"protocol":"signalwire-proto-test","method":"calling.end","params":{"call_id":"call-id","node_id":"node-id","reason":"hangup"}}') 76 | relay_call.calling.client.execute.mock.assert_called_once() 77 | 78 | @pytest.mark.asyncio 79 | async def test_hangup_with_failure(fail_response, relay_call): 80 | relay_call.calling.client.execute = fail_response 81 | result = await relay_call.hangup() 82 | assert not result.successful 83 | relay_call.calling.client.execute.mock.assert_called_once() 84 | 85 | @pytest.mark.asyncio 86 | async def test_dial(success_response, relay_call): 87 | relay_call.calling.client.execute = success_response 88 | payload = json.loads('{"event_type":"calling.call.state","event_channel":"signalwire-proto-test","timestamp":1570204684.1133151,"project_id":"project-uuid","space_id":"space-uuid","params":{"call_state":"answered","direction":"outbound","device":{"type":"phone","params":{"from_number":"+12029999999","to_number":"+12028888888"}},"end_reason":"noAnswer","call_id":"call-id","node_id":"node-id","tag":"call-tag"}}') 89 | asyncio.create_task(_fire(relay_call.calling, payload)) 90 | result = await relay_call.dial() 91 | 92 | assert result.successful 93 | msg = relay_call.calling.client.execute.mock.call_args[0][0] 94 | assert msg.params == json.loads('{"protocol":"signalwire-proto-test","method":"calling.begin","params":{"tag":"'+relay_call.tag+'","device":{"type":"phone","params":{"from_number":"+12029999999","to_number":"+12028888888"}}}}') 95 | relay_call.calling.client.execute.mock.assert_called_once() 96 | 97 | @pytest.mark.asyncio 98 | async def test_dial_with_failure(fail_response, relay_call): 99 | relay_call.calling.client.execute = fail_response 100 | result = await relay_call.dial() 101 | assert not result.successful 102 | relay_call.calling.client.execute.mock.assert_called_once() 103 | -------------------------------------------------------------------------------- /signalwire/tests/relay/calling/test_call_disconnect.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | import pytest 4 | from unittest.mock import Mock 5 | from signalwire.relay.calling.call import Call 6 | 7 | async def _fire(calling, notification): 8 | calling.notification_handler(notification) 9 | 10 | def test_disconnect_events(relay_call): 11 | mock = Mock() 12 | relay_call.on('connect.stateChange', mock) 13 | relay_call.on('connect.disconnected', mock) 14 | payload = json.loads('{"event_type":"calling.call.connect","params":{"connect_state":"disconnected","peer":{"call_id":"peer-call-id","node_id":"peer-node-id","device":{"type":"phone","params":{"from_number":"+12029999999","to_number":"+12029999991"}}},"call_id":"call-id","node_id":"node-id","tag":"call-tag"}}') 15 | relay_call.calling.notification_handler(payload) 16 | assert mock.call_count == 2 17 | 18 | @pytest.mark.asyncio 19 | async def test_disconnect_with_success(success_response, relay_call): 20 | relay_call.calling.client.execute = success_response 21 | peer_created = json.loads('{"event_type":"calling.call.state","event_channel":"signalwire-proto-test","timestamp":1569517309.4546909,"project_id":"project-uuid","space_id":"space-uuid","params":{"call_state":"created","direction":"outbound","peer":{"call_id":"call-id","node_id":"node-id"},"device":{"type":"phone","params":{"from_number":"+12029999999","to_number":"+12028888888"}},"call_id":"peer-call-id","node_id":"peer-node-id"}}') 22 | await _fire(relay_call.calling, peer_created) 23 | 24 | payload = json.loads('{"event_type":"calling.call.connect","params":{"connect_state":"connected","peer":{"call_id":"peer-call-id","node_id":"peer-node-id","device":{"type":"phone","params":{"from_number":"+12029999999","to_number":"+12029999991"}}},"call_id":"call-id","node_id":"node-id","tag":"call-tag"}}') 25 | await _fire(relay_call.calling, payload) 26 | assert isinstance(relay_call.peer, Call) 27 | 28 | payload['params']['connect_state'] = 'disconnected' 29 | asyncio.create_task(_fire(relay_call.calling, payload)) 30 | result = await relay_call.disconnect() 31 | assert result.successful 32 | assert relay_call.peer is None 33 | msg = relay_call.calling.client.execute.mock.call_args[0][0] 34 | assert msg.params == json.loads('{"protocol":"signalwire-proto-test","method":"calling.disconnect","params":{"call_id":"call-id","node_id":"node-id"}}') 35 | relay_call.calling.client.execute.mock.assert_called_once() 36 | -------------------------------------------------------------------------------- /signalwire/tests/relay/calling/test_call_fax_receive.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | import pytest 4 | from unittest.mock import Mock, patch 5 | 6 | async def _fire(calling, notification): 7 | calling.notification_handler(notification) 8 | 9 | def mock_uuid(): 10 | return 'control-id' 11 | 12 | def test_fax_events(relay_call): 13 | mock = Mock() 14 | on_page = Mock() 15 | on_finished = Mock() 16 | relay_call.on('fax.stateChange', mock) 17 | relay_call.on('fax.page', on_page) 18 | relay_call.on('fax.finished', on_finished) 19 | page_event = json.loads('{"event_type":"calling.call.fax","params":{"control_id":"control-id","call_id":"call-id","node_id":"node-id","fax":{"type":"page","params":{"direction":"send","pages":"1"}}}}') 20 | relay_call.calling.notification_handler(page_event) 21 | finished_event = json.loads('{"event_type":"calling.call.fax","params":{"control_id":"control-id","call_id":"call-id","node_id":"node-id","fax":{"type":"finished","params":{"direction":"send","identity":"+1xxx","remote_identity":"+1yyy","document":"file.pdf","success":true,"result":"1231","result_text":"","pages":"1"}}}}') 22 | relay_call.calling.notification_handler(finished_event) 23 | assert mock.call_count == 2 24 | assert on_page.call_count == 1 25 | assert on_finished.call_count == 1 26 | 27 | @pytest.mark.asyncio 28 | async def test_fax_receive_with_success(success_response, relay_call): 29 | with patch('signalwire.relay.calling.components.uuid4', mock_uuid): 30 | relay_call.calling.client.execute = success_response 31 | payload = json.loads('{"event_type":"calling.call.fax","params":{"control_id":"control-id","call_id":"call-id","node_id":"node-id","fax":{"type":"finished","params":{"direction":"receive","identity":"+1xxx","remote_identity":"+1yyy","document":"file.pdf","success":true,"result":"1231","result_text":"","pages":"1"}}}}') 32 | asyncio.create_task(_fire(relay_call.calling, payload)) 33 | result = await relay_call.fax_receive() 34 | assert result.successful 35 | assert result.direction == 'receive' 36 | assert result.identity == '+1xxx' 37 | assert result.remote_identity == '+1yyy' 38 | assert result.document == 'file.pdf' 39 | assert result.pages == '1' 40 | assert result.event.payload['type'] == 'finished' 41 | msg = relay_call.calling.client.execute.mock.call_args[0][0] 42 | assert msg.params == json.loads('{"protocol":"signalwire-proto-test","method":"calling.receive_fax","params":{"call_id":"call-id","node_id":"node-id","control_id":"control-id"}}') 43 | relay_call.calling.client.execute.mock.assert_called_once() 44 | 45 | @pytest.mark.asyncio 46 | async def test_fax_receive_with_failure(fail_response, relay_call): 47 | relay_call.calling.client.execute = fail_response 48 | result = await relay_call.fax_receive() 49 | assert not result.successful 50 | relay_call.calling.client.execute.mock.assert_called_once() 51 | 52 | @pytest.mark.asyncio 53 | async def test_fax_receive_async_with_success(success_response, relay_call): 54 | with patch('signalwire.relay.calling.components.uuid4', mock_uuid): 55 | relay_call.calling.client.execute = success_response 56 | action = await relay_call.fax_receive_async() 57 | assert not action.completed 58 | # Complete the action now.. 59 | payload = json.loads('{"event_type":"calling.call.fax","params":{"control_id":"control-id","call_id":"call-id","node_id":"node-id","fax":{"type":"finished","params":{"direction":"receive","identity":"+1xxx","remote_identity":"+1yyy","document":"file.pdf","success":true,"result":"1231","result_text":"","pages":"1"}}}}') 60 | await _fire(relay_call.calling, payload) 61 | assert action.completed 62 | msg = relay_call.calling.client.execute.mock.call_args[0][0] 63 | assert msg.params == json.loads('{"protocol":"signalwire-proto-test","method":"calling.receive_fax","params":{"call_id":"call-id","node_id":"node-id","control_id":"control-id"}}') 64 | relay_call.calling.client.execute.mock.assert_called_once() 65 | 66 | @pytest.mark.asyncio 67 | async def test_fax_receive_async_with_failure(fail_response, relay_call): 68 | relay_call.calling.client.execute = fail_response 69 | action = await relay_call.fax_receive_async() 70 | assert action.completed 71 | assert not action.result.successful 72 | relay_call.calling.client.execute.mock.assert_called_once() 73 | -------------------------------------------------------------------------------- /signalwire/tests/relay/calling/test_call_fax_send.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | import pytest 4 | from unittest.mock import patch 5 | 6 | async def _fire(calling, notification): 7 | calling.notification_handler(notification) 8 | 9 | def mock_uuid(): 10 | return 'control-id' 11 | 12 | @pytest.mark.asyncio 13 | async def test_fax_send_with_success(success_response, relay_call): 14 | with patch('signalwire.relay.calling.components.uuid4', mock_uuid): 15 | relay_call.calling.client.execute = success_response 16 | payload = json.loads('{"event_type":"calling.call.fax","params":{"control_id":"control-id","call_id":"call-id","node_id":"node-id","fax":{"type":"finished","params":{"direction":"send","identity":"+1xxx","remote_identity":"+1yyy","document":"file.pdf","success":true,"result":"1231","result_text":"","pages":2}}}}') 17 | asyncio.create_task(_fire(relay_call.calling, payload)) 18 | result = await relay_call.fax_send(url='file.pdf', header='custom header') 19 | assert result.successful 20 | assert result.direction == 'send' 21 | assert result.identity == '+1xxx' 22 | assert result.remote_identity == '+1yyy' 23 | assert result.document == 'file.pdf' 24 | assert result.pages == 2 25 | assert result.event.payload['type'] == 'finished' 26 | msg = relay_call.calling.client.execute.mock.call_args[0][0] 27 | assert msg.params == json.loads('{"protocol":"signalwire-proto-test","method":"calling.send_fax","params":{"call_id":"call-id","node_id":"node-id","control_id":"control-id","document":"file.pdf","header_info":"custom header"}}') 28 | relay_call.calling.client.execute.mock.assert_called_once() 29 | 30 | @pytest.mark.asyncio 31 | async def test_fax_send_with_failure(fail_response, relay_call): 32 | relay_call.calling.client.execute = fail_response 33 | result = await relay_call.fax_send(url='file.pdf', header='custom header') 34 | assert not result.successful 35 | relay_call.calling.client.execute.mock.assert_called_once() 36 | 37 | @pytest.mark.asyncio 38 | async def test_fax_send_async_with_success(success_response, relay_call): 39 | with patch('signalwire.relay.calling.components.uuid4', mock_uuid): 40 | relay_call.calling.client.execute = success_response 41 | action = await relay_call.fax_send_async(url='file.pdf', header='custom header') 42 | assert not action.completed 43 | # Complete the action now.. 44 | payload = json.loads('{"event_type":"calling.call.fax","params":{"control_id":"control-id","call_id":"call-id","node_id":"node-id","fax":{"type":"finished","params":{"direction":"send","identity":"+1xxx","remote_identity":"+1yyy","document":"file.pdf","success":true,"result":"1231","result_text":"","pages":2}}}}') 45 | await _fire(relay_call.calling, payload) 46 | assert action.completed 47 | msg = relay_call.calling.client.execute.mock.call_args[0][0] 48 | assert msg.params == json.loads('{"protocol":"signalwire-proto-test","method":"calling.send_fax","params":{"call_id":"call-id","node_id":"node-id","control_id":"control-id","document":"file.pdf","header_info":"custom header"}}') 49 | relay_call.calling.client.execute.mock.assert_called_once() 50 | 51 | @pytest.mark.asyncio 52 | async def test_fax_send_async_with_failure(fail_response, relay_call): 53 | relay_call.calling.client.execute = fail_response 54 | action = await relay_call.fax_send_async(url='file.pdf', header='custom header') 55 | assert action.completed 56 | assert not action.result.successful 57 | relay_call.calling.client.execute.mock.assert_called_once() 58 | -------------------------------------------------------------------------------- /signalwire/tests/relay/calling/test_call_record.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | import pytest 4 | from unittest.mock import Mock, patch 5 | from signalwire.tests import AsyncMock 6 | 7 | async def _fire(calling, notification): 8 | calling.notification_handler(notification) 9 | 10 | def mock_uuid(): 11 | return 'control-id' 12 | 13 | def test_record_events(relay_call): 14 | mock = Mock() 15 | relay_call.on('record.stateChange', mock) 16 | relay_call.on('record.finished', mock) 17 | payload = json.loads('{"event_type":"calling.call.record","params":{"state":"finished","record":{"audio":{"format":"mp3","direction":"speak","stereo":false}},"url":"record.mp3","control_id":"control-id","size":4096,"duration":4,"call_id":"call-id","node_id":"node-id"}}') 18 | relay_call.calling.notification_handler(payload) 19 | assert mock.call_count == 2 20 | 21 | @pytest.mark.asyncio 22 | async def test_record_with_success(success_response, relay_call): 23 | with patch('signalwire.relay.calling.components.uuid4', mock_uuid): 24 | relay_call.calling.client.execute = success_response 25 | payload = json.loads('{"event_type":"calling.call.record","params":{"state":"recording","record":{"audio":{"format":"mp3","direction":"speak","stereo":true}},"url":"record.mp3","control_id":"control-id","size":4096,"duration":4,"call_id":"call-id","node_id":"node-id"}}') 26 | asyncio.create_task(_fire(relay_call.calling, payload)) # Test 'recording' event before 'finished' 27 | payload = json.loads('{"event_type":"calling.call.record","params":{"state":"finished","record":{"audio":{"format":"mp3","direction":"speak","stereo":true}},"url":"record.mp3","control_id":"control-id","size":4096,"duration":4,"call_id":"call-id","node_id":"node-id"}}') 28 | asyncio.create_task(_fire(relay_call.calling, payload)) 29 | result = await relay_call.record(beep=True, direction='both', terminators='#') 30 | assert result.successful 31 | assert result.url == 'record.mp3' 32 | assert result.duration == 4 33 | assert result.size == 4096 34 | assert result.event.payload['state'] == 'finished' 35 | msg = relay_call.calling.client.execute.mock.call_args[0][0] 36 | assert msg.params == json.loads('{"protocol":"signalwire-proto-test","method":"calling.record","params":{"call_id":"call-id","node_id":"node-id","control_id":"control-id","record":{"audio":{"beep":true,"direction":"both","terminators":"#"}}}}') 37 | relay_call.calling.client.execute.mock.assert_called_once() 38 | 39 | @pytest.mark.asyncio 40 | async def test_record_with_failure(fail_response, relay_call): 41 | relay_call.calling.client.execute = fail_response 42 | result = await relay_call.record(beep=True, direction='both', terminators='#') 43 | assert not result.successful 44 | relay_call.calling.client.execute.mock.assert_called_once() 45 | 46 | @pytest.mark.asyncio 47 | async def test_record_async_with_success(success_response, relay_call): 48 | with patch('signalwire.relay.calling.components.uuid4', mock_uuid): 49 | relay_response = json.loads('{"requester_nodeid":"uuid","responder_nodeid":"uuid","result":{"code":"200","message":"Message","url":"record.mp3"}}') 50 | relay_call.calling.client.execute = AsyncMock(return_value=relay_response) 51 | # relay_call.calling.client.execute = success_response 52 | action = await relay_call.record_async(terminators='#') 53 | assert not action.completed 54 | assert action.url == 'record.mp3' 55 | # Complete the action now.. 56 | payload = json.loads('{"event_type":"calling.call.record","params":{"state":"finished","record":{"audio":{"format":"mp3","direction":"speak","stereo":true}},"url":"record.mp3","control_id":"control-id","size":4096,"duration":4,"call_id":"call-id","node_id":"node-id"}}') 57 | await _fire(relay_call.calling, payload) 58 | assert action.completed 59 | msg = relay_call.calling.client.execute.mock.call_args[0][0] 60 | assert msg.params == json.loads('{"protocol":"signalwire-proto-test","method":"calling.record","params":{"call_id":"call-id","node_id":"node-id","control_id":"control-id","record":{"audio":{"terminators":"#"}}}}') 61 | relay_call.calling.client.execute.mock.assert_called_once() 62 | 63 | @pytest.mark.asyncio 64 | async def test_record_async_with_failure(fail_response, relay_call): 65 | relay_call.calling.client.execute = fail_response 66 | action = await relay_call.record_async(beep=True, direction='both', terminators='#') 67 | assert action.completed 68 | assert not action.result.successful 69 | relay_call.calling.client.execute.mock.assert_called_once() 70 | 71 | @pytest.mark.asyncio 72 | async def test_record_with_no_input_event(success_response, relay_call): 73 | with patch('signalwire.relay.calling.components.uuid4', mock_uuid): 74 | relay_call.calling.client.execute = success_response 75 | payload = json.loads('{"event_type":"calling.call.record","params":{"state":"no_input","control_id":"control-id","call_id":"call-id","node_id":"node-id"}}') 76 | asyncio.create_task(_fire(relay_call.calling, payload)) 77 | result = await relay_call.record(beep=True, direction='both', terminators='#') 78 | assert not result.successful 79 | assert result.event.payload['state'] == 'no_input' 80 | relay_call.calling.client.execute.mock.assert_called_once() 81 | 82 | @pytest.mark.asyncio 83 | async def test_record_async_with_no_input_event(success_response, relay_call): 84 | with patch('signalwire.relay.calling.components.uuid4', mock_uuid): 85 | relay_call.calling.client.execute = success_response 86 | action = await relay_call.record_async(terminators='#') 87 | assert not action.completed 88 | payload = json.loads('{"event_type":"calling.call.record","params":{"state":"no_input","control_id":"control-id","call_id":"call-id","node_id":"node-id"}}') 89 | await _fire(relay_call.calling, payload) 90 | assert action.completed 91 | assert not action.result.successful 92 | relay_call.calling.client.execute.mock.assert_called_once() 93 | -------------------------------------------------------------------------------- /signalwire/tests/relay/calling/test_call_send_digits.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | import pytest 4 | from unittest.mock import Mock, patch 5 | 6 | async def _fire(calling, notification): 7 | calling.notification_handler(notification) 8 | 9 | def mock_uuid(): 10 | return 'control-id' 11 | 12 | def test_send_digits_events(relay_call): 13 | mock = Mock() 14 | relay_call.on('sendDigits.stateChange', mock) 15 | relay_call.on('sendDigits.finished', mock) 16 | payload = json.loads('{"event_type":"calling.call.send_digits","params":{"control_id":"control-id","call_id":"call-id","node_id":"node-id","state":"finished"}}') 17 | relay_call.calling.notification_handler(payload) 18 | assert mock.call_count == 2 19 | 20 | @pytest.mark.asyncio 21 | async def test_send_digits_with_success(success_response, relay_call): 22 | with patch('signalwire.relay.calling.components.uuid4', mock_uuid): 23 | relay_call.calling.client.execute = success_response 24 | payload = json.loads('{"event_type":"calling.call.send_digits","params":{"control_id":"control-id","call_id":"call-id","node_id":"node-id","state":"finished"}}') 25 | asyncio.create_task(_fire(relay_call.calling, payload)) 26 | result = await relay_call.send_digits(digits='1234') 27 | assert result.successful 28 | assert result.event.payload['state'] == 'finished' 29 | msg = relay_call.calling.client.execute.mock.call_args[0][0] 30 | assert msg.params == json.loads('{"protocol":"signalwire-proto-test","method":"calling.send_digits","params":{"call_id":"call-id","node_id":"node-id","control_id":"control-id","digits":"1234"}}') 31 | relay_call.calling.client.execute.mock.assert_called_once() 32 | 33 | @pytest.mark.asyncio 34 | async def test_send_digits_with_failure(fail_response, relay_call): 35 | relay_call.calling.client.execute = fail_response 36 | result = await relay_call.send_digits(digits='1234') 37 | assert not result.successful 38 | relay_call.calling.client.execute.mock.assert_called_once() 39 | 40 | @pytest.mark.asyncio 41 | async def test_send_digits_async_with_success(success_response, relay_call): 42 | with patch('signalwire.relay.calling.components.uuid4', mock_uuid): 43 | relay_call.calling.client.execute = success_response 44 | action = await relay_call.send_digits_async(digits='1234') 45 | assert not action.completed 46 | # Complete the action now.. 47 | payload = json.loads('{"event_type":"calling.call.send_digits","params":{"control_id":"control-id","call_id":"call-id","node_id":"node-id","state":"finished"}}') 48 | await _fire(relay_call.calling, payload) 49 | assert action.completed 50 | msg = relay_call.calling.client.execute.mock.call_args[0][0] 51 | assert msg.params == json.loads('{"protocol":"signalwire-proto-test","method":"calling.send_digits","params":{"call_id":"call-id","node_id":"node-id","control_id":"control-id","digits":"1234"}}') 52 | relay_call.calling.client.execute.mock.assert_called_once() 53 | 54 | @pytest.mark.asyncio 55 | async def test_send_digits_async_with_failure(fail_response, relay_call): 56 | relay_call.calling.client.execute = fail_response 57 | action = await relay_call.send_digits_async(digits='1234') 58 | assert action.completed 59 | assert not action.result.successful 60 | relay_call.calling.client.execute.mock.assert_called_once() 61 | -------------------------------------------------------------------------------- /signalwire/tests/relay/calling/test_call_tap.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | import pytest 4 | from unittest.mock import Mock, patch 5 | from signalwire.tests import AsyncMock 6 | 7 | @pytest.fixture() 8 | def tap_success_response(): 9 | response = json.loads('{"requester_nodeid":"uuid","responder_nodeid":"uuid","result":{"code":"200","message":"Tapping","call_id":"call-id","control_id":"control-id","source_device":{"type":"rtp","params":{"addr":"10.10.10.10","port":30030,"codec":"PCMU","ptime":20,"rate":8000}}}}') 10 | return AsyncMock(return_value=response) 11 | 12 | async def _fire(calling, notification): 13 | calling.notification_handler(notification) 14 | 15 | def mock_uuid(): 16 | return 'control-id' 17 | 18 | def test_tap_events(relay_call): 19 | mock = Mock() 20 | relay_call.on('tap.stateChange', mock) 21 | relay_call.on('tap.finished', mock) 22 | payload = json.loads('{"event_type":"calling.call.tap","params":{"control_id":"control-id","call_id":"call-id","node_id":"node-id","state":"finished","tap":{"type":"audio","params":{"direction":"listen"}},"device":{"type":"rtp","params":{"addr":"127.0.0.1","port":1234,"codec":"PCMU","ptime":20}}}}') 23 | relay_call.calling.notification_handler(payload) 24 | assert mock.call_count == 2 25 | 26 | @pytest.mark.asyncio 27 | async def test_tap_with_success(tap_success_response, relay_call): 28 | with patch('signalwire.relay.calling.components.uuid4', mock_uuid): 29 | relay_call.calling.client.execute = tap_success_response 30 | payload = json.loads('{"event_type":"calling.call.tap","params":{"control_id":"control-id","call_id":"call-id","node_id":"node-id","state":"finished","tap":{"type":"audio","params":{"direction":"listen"}},"device":{"type":"rtp","params":{"addr":"127.0.0.1","port":1234,"codec":"PCMU","ptime":20}}}}') 31 | asyncio.create_task(_fire(relay_call.calling, payload)) 32 | result = await relay_call.tap(audio_direction='listen', target_type='rtp', target_addr='127.0.0.1', target_port='1234') 33 | assert result.successful 34 | assert result.source_device == json.loads('{"type":"rtp","params":{"addr":"10.10.10.10","port":30030,"codec":"PCMU","ptime":20,"rate":8000}}') 35 | assert result.destination_device == json.loads('{"type":"rtp","params":{"addr":"127.0.0.1","port":1234,"codec":"PCMU","ptime":20}}') 36 | assert result.event.payload['state'] == 'finished' 37 | msg = relay_call.calling.client.execute.mock.call_args[0][0] 38 | assert msg.params == json.loads('{"protocol":"signalwire-proto-test","method":"calling.tap","params":{"call_id":"call-id","node_id":"node-id","control_id":"control-id","tap":{"type":"audio","params":{"direction":"listen"}},"device":{"type":"rtp","params":{"addr":"127.0.0.1","port":1234}}}}') 39 | relay_call.calling.client.execute.mock.assert_called_once() 40 | 41 | @pytest.mark.asyncio 42 | async def test_tap_with_failure(fail_response, relay_call): 43 | relay_call.calling.client.execute = fail_response 44 | result = await relay_call.tap(audio_direction='listen', target_type='rtp', target_addr='127.0.0.1', target_port='1234') 45 | assert not result.successful 46 | relay_call.calling.client.execute.mock.assert_called_once() 47 | 48 | @pytest.mark.asyncio 49 | async def test_tap_async_with_success(tap_success_response, relay_call): 50 | with patch('signalwire.relay.calling.components.uuid4', mock_uuid): 51 | relay_call.calling.client.execute = tap_success_response 52 | action = await relay_call.tap_async(audio_direction='listen', target_type='rtp', target_addr='127.0.0.1', target_port='1234') 53 | assert not action.completed 54 | assert action.source_device == json.loads('{"type":"rtp","params":{"addr":"10.10.10.10","port":30030,"codec":"PCMU","ptime":20,"rate":8000}}') 55 | # Complete the action now.. 56 | payload = json.loads('{"event_type":"calling.call.tap","params":{"control_id":"control-id","call_id":"call-id","node_id":"node-id","state":"finished","tap":{"type":"audio","params":{"direction":"listen"}},"device":{"type":"rtp","params":{"addr":"127.0.0.1","port":1234,"codec":"PCMU","ptime":20}}}}') 57 | await _fire(relay_call.calling, payload) 58 | assert action.completed 59 | msg = relay_call.calling.client.execute.mock.call_args[0][0] 60 | assert msg.params == json.loads('{"protocol":"signalwire-proto-test","method":"calling.tap","params":{"call_id":"call-id","node_id":"node-id","control_id":"control-id","tap":{"type":"audio","params":{"direction":"listen"}},"device":{"type":"rtp","params":{"addr":"127.0.0.1","port":1234}}}}') 61 | relay_call.calling.client.execute.mock.assert_called_once() 62 | 63 | @pytest.mark.asyncio 64 | async def test_tap_async_with_failure(fail_response, relay_call): 65 | relay_call.calling.client.execute = fail_response 66 | action = await relay_call.tap_async(audio_direction='listen', target_type='rtp', target_addr='127.0.0.1', target_port='1234') 67 | assert action.completed 68 | assert not action.result.successful 69 | relay_call.calling.client.execute.mock.assert_called_once() 70 | -------------------------------------------------------------------------------- /signalwire/tests/relay/calling/test_call_wait_for.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | import pytest 4 | from signalwire.tests import AsyncMock 5 | 6 | async def _fire(calling, notification): 7 | calling.notification_handler(notification) 8 | 9 | @pytest.mark.asyncio 10 | async def test_wait_for_ringing(relay_call): 11 | relay_call.calling.client.execute = AsyncMock() 12 | payload = json.loads('{"event_type":"calling.call.state","event_channel":"signalwire-proto-test","timestamp":1570204684.1133151,"project_id":"project-uuid","space_id":"space-uuid","params":{"call_state":"ringing","direction":"outbound","device":{"type":"phone","params":{"from_number":"+12029999999","to_number":"+12028888888"}},"call_id":"call-id","node_id":"node-id","tag":"call-tag"}}') 13 | asyncio.create_task(_fire(relay_call.calling, payload)) 14 | result = await relay_call.wait_for_ringing() 15 | assert result 16 | assert relay_call.state == 'ringing' 17 | relay_call.calling.client.execute.mock.assert_not_called() 18 | 19 | @pytest.mark.asyncio 20 | async def test_wait_for_answered(relay_call): 21 | relay_call.calling.client.execute = AsyncMock() 22 | payload = json.loads('{"event_type":"calling.call.state","event_channel":"signalwire-proto-test","timestamp":1570204684.1133151,"project_id":"project-uuid","space_id":"space-uuid","params":{"call_state":"answered","direction":"outbound","device":{"type":"phone","params":{"from_number":"+12029999999","to_number":"+12028888888"}},"call_id":"call-id","node_id":"node-id","tag":"call-tag"}}') 23 | asyncio.create_task(_fire(relay_call.calling, payload)) 24 | result = await relay_call.wait_for_answered() 25 | assert result 26 | assert relay_call.state == 'answered' 27 | relay_call.calling.client.execute.mock.assert_not_called() 28 | 29 | @pytest.mark.asyncio 30 | async def test_wait_for_answered_on_ending_call(relay_call): 31 | relay_call.state = 'ending' 32 | result = await relay_call.wait_for_answered() 33 | assert result 34 | 35 | @pytest.mark.asyncio 36 | async def test_wait_for_ending(relay_call): 37 | relay_call.calling.client.execute = AsyncMock() 38 | payload = json.loads('{"event_type":"calling.call.state","event_channel":"signalwire-proto-test","timestamp":1570204684.1133151,"project_id":"project-uuid","space_id":"space-uuid","params":{"call_state":"ending","direction":"outbound","device":{"type":"phone","params":{"from_number":"+12029999999","to_number":"+12028888888"}},"call_id":"call-id","node_id":"node-id","tag":"call-tag"}}') 39 | asyncio.create_task(_fire(relay_call.calling, payload)) 40 | result = await relay_call.wait_for_ending() 41 | assert result 42 | assert relay_call.state == 'ending' 43 | relay_call.calling.client.execute.mock.assert_not_called() 44 | 45 | @pytest.mark.asyncio 46 | async def test_wait_for_ended(relay_call): 47 | relay_call.calling.client.execute = AsyncMock() 48 | payload = json.loads('{"event_type":"calling.call.state","event_channel":"signalwire-proto-test","timestamp":1570204684.1133151,"project_id":"project-uuid","space_id":"space-uuid","params":{"call_state":"ended","direction":"outbound","device":{"type":"phone","params":{"from_number":"+12029999999","to_number":"+12028888888"}},"call_id":"call-id","node_id":"node-id","tag":"call-tag"}}') 49 | asyncio.create_task(_fire(relay_call.calling, payload)) 50 | result = await relay_call.wait_for_ended() 51 | assert result 52 | assert relay_call.state == 'ended' 53 | relay_call.calling.client.execute.mock.assert_not_called() 54 | -------------------------------------------------------------------------------- /signalwire/tests/relay/calling/test_calling.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from signalwire.blade.messages.message import Message 3 | from signalwire.relay.calling import Calling, Call 4 | from unittest.mock import Mock 5 | 6 | def test_add_call(relay_client): 7 | instance = Calling(relay_client) 8 | c1 = Call(calling=instance) 9 | c2 = Call(calling=instance) 10 | c3 = Call(calling=instance) 11 | assert len(instance.calls) == 3 12 | 13 | def test_remove_call(relay_client): 14 | instance = Calling(relay_client) 15 | c1 = Call(calling=instance) 16 | instance.remove_call(c1) 17 | assert len(instance.calls) == 0 18 | 19 | def test_get_call_by_id(relay_client): 20 | instance = Calling(relay_client) 21 | c1 = Call(calling=instance, **{ 'call_id': '1234' }) 22 | assert instance._get_call_by_id('1234') is c1 23 | 24 | def test_get_call_by_tag(relay_client): 25 | instance = Calling(relay_client) 26 | c1 = Call(calling=instance, **{ 'call_id': '1234' }) 27 | assert instance._get_call_by_tag(c1.tag) is c1 28 | 29 | @pytest.mark.asyncio 30 | async def test_on_receive(relay_calling): 31 | handler = Mock() 32 | await relay_calling.receive(['home', 'office'], handler) 33 | 34 | message = Message.from_json('{"jsonrpc":"2.0","id":"uuid","method":"blade.broadcast","params":{"broadcaster_nodeid":"uuid","protocol":"signalwire-proto-test","channel":"notifications","event":"queuing.relay.events","params":{"event_type":"calling.call.receive","timestamp":1569514183.0130031,"project_id":"project-uuid","space_id":"space-uuid","params":{"call_state":"created","context":"office","device":{"type":"phone","params":{"from_number":"+12029999999","to_number":"+12028888888"}},"direction":"inbound","call_id":"call-id","node_id":"node-id"},"context":"office","event_channel":"signalwire-proto-test"}}}') 35 | relay_calling.client.message_handler(message) 36 | 37 | call = handler.call_args[0][0] 38 | assert isinstance(call, Call) 39 | assert call.from_number == '+12029999999' 40 | assert call.to_number == '+12028888888' 41 | handler.assert_called_once() 42 | 43 | @pytest.mark.asyncio 44 | async def test_on_state_created(relay_calling): 45 | call = Call(calling=relay_calling) 46 | assert call.id is None 47 | message = Message.from_json('{"jsonrpc":"2.0","id":"uuid","method":"blade.broadcast","params":{"broadcaster_nodeid":"uuid","protocol":"signalwire-proto-test","channel":"notifications","event":"queuing.relay.events","params":{"event_type":"calling.call.state","event_channel":"signalwire-proto-test","timestamp":1569517309.4546909,"project_id":"project-uuid","space_id":"space-uuid","params":{"call_state":"created","direction":"outbound","device":{"type":"phone","params":{"from_number":"+12029999999","to_number":"+12028888888"}},"call_id":"call-id","node_id":"node-id","tag":"'+call.tag+'"}}}}') 48 | relay_calling.client.message_handler(message) 49 | 50 | assert call.state == 'created' 51 | assert call.id == 'call-id' 52 | assert call.node_id == 'node-id' 53 | 54 | @pytest.mark.asyncio 55 | async def test_on_state_ringing(relay_calling): 56 | call = Call(calling=relay_calling) 57 | call.id = 'call-id' 58 | message = Message.from_json('{"jsonrpc":"2.0","id":"uuid","method":"blade.broadcast","params":{"broadcaster_nodeid":"uuid","protocol":"signalwire-proto-test","channel":"notifications","event":"queuing.relay.events","params":{"event_type":"calling.call.state","event_channel":"signalwire-proto-test","timestamp":1569517309.4546909,"project_id":"project-uuid","space_id":"space-uuid","params":{"call_state":"ringing","direction":"outbound","device":{"type":"phone","params":{"from_number":"+12029999999","to_number":"+12028888888"}},"call_id":"call-id","node_id":"node-id","tag":"'+call.tag+'"}}}}') 59 | relay_calling.client.message_handler(message) 60 | assert call.state == 'ringing' 61 | -------------------------------------------------------------------------------- /signalwire/tests/relay/conftest.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import pytest 3 | from signalwire.tests import MockedConnection, AsyncMock 4 | 5 | @pytest.fixture(scope='function') 6 | def relay_client(event_loop): 7 | from signalwire.relay.client import Client 8 | client = Client(project='project', token='token', connection=MockedConnection) 9 | client.loop = event_loop 10 | return client 11 | 12 | @pytest.fixture(scope='function') 13 | def relay_client_to_connect(relay_client): 14 | asyncio.sleep = AsyncMock() # Mock sleep 15 | relay_client.connection.responses = [ 16 | '{"jsonrpc":"2.0","id":"uuid","result":{"session_restored":false,"sessionid":"87cf6699-7a89-4491-b732-b51144155d46","nodeid":"uuid_node","master_nodeid":"00000000-0000-0000-0000-000000000000","authorization":{"project":"project","expires_at":null,"scopes":["calling"],"signature":"random_signature"},"routes":[],"protocols":[],"subscriptions":[],"authorities":[],"authorizations":[],"accesses":[],"protocols_uncertified":["signalwire"]}}', 17 | '{"jsonrpc":"2.0","id":"uuid","result":{"requester_nodeid":"uuid","responder_nodeid":"uuid","result":{"protocol":"signalwire_random_proto"}}}', 18 | '{"jsonrpc":"2.0","id":"uuid","result":{"protocol":"signalwire_random_proto","command":"add","subscribe_channels":["notifications"]}}' 19 | ] 20 | return relay_client 21 | -------------------------------------------------------------------------------- /signalwire/tests/relay/messaging/conftest.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pytest 3 | from signalwire.tests import MockedConnection, AsyncMock 4 | 5 | @pytest.fixture(scope='function') 6 | def relay_messaging(relay_client): 7 | relay_client.protocol = 'signalwire-proto-test' 8 | response = json.loads('{"requester_nodeid":"uuid","responder_nodeid":"uuid","result":{"code":"200","message":"Receiving all inbound related to the requested relay contexts and available scopes"}}') 9 | relay_client.execute = AsyncMock(return_value=response) 10 | return relay_client.messaging 11 | -------------------------------------------------------------------------------- /signalwire/tests/relay/messaging/test_messaging.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pytest 3 | from signalwire.blade.messages.message import Message as BladeMessage 4 | from signalwire.relay.messaging import Messaging, Message, SendResult 5 | from unittest.mock import Mock 6 | from signalwire.tests import AsyncMock 7 | 8 | @pytest.mark.asyncio 9 | async def test_receive(relay_messaging): 10 | handler = Mock() 11 | await relay_messaging.receive(['home', 'office'], handler) 12 | 13 | event = BladeMessage.from_json('{"jsonrpc":"2.0","id":"uuid","method":"blade.broadcast","params":{"broadcaster_nodeid":"uuid","protocol":"signalwire-proto-test","channel":"notifications","event":"queuing.relay.messaging","params":{"event_type":"messaging.receive","space_id":"space-uuid","project_id":"project-uuid","context":"office","timestamp":1570191488.717304,"params":{"message_id":"875029c0-92cf-44ef-b6c0-d250123e833b","context":"office","direction":"outbound","tags":["t1","t2"],"from_number":"+12029999999","to_number":"+12028888888","body":"Hey There, Welcome at SignalWire!","media":[],"segments":1,"message_state":"received"},"event_channel":"signalwire-proto-test"}}}') 14 | relay_messaging.client.message_handler(event) 15 | 16 | message = handler.call_args[0][0] 17 | assert isinstance(message, Message) 18 | assert message.id == '875029c0-92cf-44ef-b6c0-d250123e833b' 19 | assert message.from_number == '+12029999999' 20 | assert message.to_number == '+12028888888' 21 | assert message.state == 'received' 22 | assert message.tags == ['t1', 't2'] 23 | handler.assert_called_once() 24 | 25 | @pytest.mark.asyncio 26 | async def test_state_change(relay_messaging): 27 | handler = Mock() 28 | await relay_messaging.state_change(['home', 'office'], handler) 29 | 30 | event = BladeMessage.from_json('{"jsonrpc":"2.0","id":"uuid","method":"blade.broadcast","params":{"broadcaster_nodeid":"uuid","protocol":"signalwire-proto-test","channel":"notifications","event":"queuing.relay.messaging","params":{"event_type":"messaging.state","space_id":"space-uuid","project_id":"project-uuid","context":"office","timestamp":1570191488.717304,"params":{"message_id":"875029c0-92cf-44ef-b6c0-d250123e833b","context":"office","direction":"outbound","tags":[],"from_number":"+12029999999","to_number":"+12028888888","body":"Hey There, Welcome at SignalWire!","media":[],"segments":1,"message_state":"queued"},"event_channel":"signalwire-proto-test"}}}') 31 | relay_messaging.client.message_handler(event) 32 | 33 | message = handler.call_args[0][0] 34 | assert isinstance(message, Message) 35 | assert message.id == '875029c0-92cf-44ef-b6c0-d250123e833b' 36 | assert message.from_number == '+12029999999' 37 | assert message.to_number == '+12028888888' 38 | assert message.state == 'queued' 39 | assert message.tags == [] 40 | handler.assert_called_once() 41 | 42 | @pytest.mark.asyncio 43 | async def test_send_success(relay_messaging): 44 | response = json.loads('{"requester_nodeid":"uuid","responder_nodeid":"uuid","result":{"message":"Message accepted","code":"200","message_id":"2c0e265d-4597-470e-9d5d-00581e0874a2"}}') 45 | relay_messaging.client.execute = AsyncMock(return_value=response) 46 | result = await relay_messaging.send(from_number='+12029999999', to_number='+12028888888', context='office', body='Hey There, Welcome at SignalWire!') 47 | 48 | assert isinstance(result, SendResult) 49 | assert result.successful 50 | assert result.message_id == '2c0e265d-4597-470e-9d5d-00581e0874a2' 51 | msg = relay_messaging.client.execute.mock.call_args[0][0] 52 | params = msg.params.pop('params') 53 | assert msg.params.pop('protocol') == 'signalwire-proto-test' 54 | assert msg.params.pop('method') == 'messaging.send' 55 | assert params['from_number'] == '+12029999999' 56 | assert params['body'] == 'Hey There, Welcome at SignalWire!' 57 | relay_messaging.client.execute.mock.assert_called_once() 58 | 59 | @pytest.mark.asyncio 60 | async def test_send_fail(relay_messaging): 61 | response = json.loads('{"requester_nodeid":"uuid","responder_nodeid":"uuid","result":{"message":"Some error","code":"400"}}') 62 | relay_messaging.client.execute = AsyncMock(return_value=response) 63 | result = await relay_messaging.send(from_number='+12029999999', to_number='+12028888888', context='office', body='Hey There, Welcome at SignalWire!') 64 | 65 | assert isinstance(result, SendResult) 66 | assert not result.successful 67 | assert result.message_id == None 68 | relay_messaging.client.execute.mock.assert_called_once() 69 | -------------------------------------------------------------------------------- /signalwire/tests/relay/tasking/conftest.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pytest 3 | from signalwire.tests import MockedConnection, AsyncMock 4 | 5 | @pytest.fixture(scope='function') 6 | def relay_tasking(relay_client): 7 | relay_client.protocol = 'signalwire-proto-test' 8 | response = json.loads('{"requester_nodeid":"uuid","responder_nodeid":"uuid","result":{"code":"200","message":"Receiving all inbound related to the requested relay contexts and available scopes"}}') 9 | relay_client.execute = AsyncMock(return_value=response) 10 | return relay_client.tasking 11 | -------------------------------------------------------------------------------- /signalwire/tests/relay/tasking/test_tasking.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from signalwire.blade.messages.message import Message 3 | from unittest.mock import Mock 4 | 5 | @pytest.mark.asyncio 6 | async def test_on_receive(relay_tasking): 7 | handler = Mock() 8 | await relay_tasking.receive(['home', 'office'], handler) 9 | 10 | message = Message.from_json('{"jsonrpc":"2.0","id":"uuid","method":"blade.broadcast","params":{"broadcaster_nodeid":"uuid","protocol":"signalwire-proto-test","channel":"notifications","event":"queuing.relay.tasks","params":{"project_id":"project-uuid","space_id":"space-uuid","context":"office","message":{"key":"value","data":"random stuff"},"timestamp":1569859833,"event_channel":"signalwire-proto-test"}}}') 11 | relay_tasking.client.message_handler(message) 12 | 13 | message = handler.call_args[0][0] 14 | assert isinstance(message, dict) 15 | assert message['key'] == 'value' 16 | assert message['data'] == 'random stuff' 17 | handler.assert_called_once() 18 | -------------------------------------------------------------------------------- /signalwire/tests/relay/test_client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import pytest 3 | from signalwire.blade.messages.message import Message 4 | from signalwire.blade.messages.execute import Execute 5 | from signalwire.relay.client import Client 6 | from unittest.mock import Mock 7 | from signalwire.tests import AsyncMock, MockedConnection 8 | 9 | @pytest.mark.asyncio 10 | async def test_connect(relay_client_to_connect): 11 | ready_callback = Mock() 12 | relay_client_to_connect.on('ready', ready_callback) 13 | await relay_client_to_connect._connect() 14 | 15 | pending_tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()] 16 | await asyncio.gather(*pending_tasks) 17 | 18 | assert relay_client_to_connect.session_id == '87cf6699-7a89-4491-b732-b51144155d46' 19 | assert relay_client_to_connect.protocol == 'signalwire_random_proto' 20 | ready_callback.assert_called_once() 21 | 22 | @pytest.mark.asyncio 23 | async def test_blade_disconnect(relay_client_to_connect): 24 | assert relay_client_to_connect._idle == False 25 | blade_disconnect = Message.from_json('{"id":"378d7dea-e581-4305-a7e7-d29173797f32","jsonrpc":"2.0","method":"blade.disconnect","params":{}}') 26 | relay_client_to_connect.message_handler(blade_disconnect) 27 | assert relay_client_to_connect._idle == True 28 | 29 | async def _reconnect(): 30 | assert relay_client_to_connect._executeQueue.qsize() == 1 31 | relay_client_to_connect.connection.responses.append('{"jsonrpc":"2.0","id":"uuid","result":{"test":"done"}}') 32 | await relay_client_to_connect._connect() 33 | 34 | asyncio.create_task(_reconnect()) 35 | message = Execute({ 'protocol': 'fake', 'method': 'testing', 'params': {} }) 36 | result = await relay_client_to_connect.execute(message) 37 | 38 | assert relay_client_to_connect._idle == False 39 | assert relay_client_to_connect._executeQueue.qsize() == 0 40 | assert result['test'] == 'done' 41 | -------------------------------------------------------------------------------- /signalwire/tests/relay/test_consumer.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from signalwire.relay.consumer import Consumer 3 | from signalwire.tests import MockedConnection 4 | 5 | class InvalidConsumer(Consumer): 6 | def setup(self): 7 | self.Connection = MockedConnection 8 | self.project = 'project' 9 | self.token = 'token' 10 | 11 | def test_invalid_consumer(): 12 | with pytest.raises(Exception): 13 | invalid = InvalidConsumer() 14 | invalid.run() 15 | -------------------------------------------------------------------------------- /signalwire/tests/relay/test_helpers.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pytest 3 | from signalwire.relay.client import Client 4 | from signalwire.relay.helpers import setup_protocol, receive_contexts 5 | from signalwire.tests import AsyncMock, MockedConnection 6 | 7 | @pytest.mark.asyncio 8 | async def test_setup_protocol(): 9 | client = Client(project='project', token='token', connection=MockedConnection) 10 | responses = [ 11 | json.loads('{"requester_nodeid":"uuid","responder_nodeid":"uuid","result":{"protocol":"signalwire-proto-test"}}'), 12 | json.loads('{"protocol":"signalwire-proto-test","command":"add","subscribe_channels":["notifications"]}') 13 | ] 14 | client.execute = AsyncMock(side_effect=responses) 15 | new_protocol = await setup_protocol(client) 16 | 17 | assert new_protocol == 'signalwire-proto-test' 18 | assert client.execute.mock.call_count == 2 19 | setup, subscription = client.execute.mock.call_args_list 20 | assert setup[0][0].params == {'protocol': 'signalwire', 'method': 'setup', 'params': {}} 21 | assert subscription[0][0].params == {'command': 'add', 'protocol': 'signalwire-proto-test', 'channels': ['notifications']} 22 | 23 | 24 | @pytest.mark.asyncio 25 | async def test_receive_contexts(): 26 | client = Client(project='project', token='token', connection=MockedConnection) 27 | client.protocol = 'signalwire-proto-test' 28 | response = json.loads('{"requester_nodeid":"uuid","responder_nodeid":"uuid","result":{"code":"200","message":"Receiving all inbound related to the requested relay contexts and available scopes"}}') 29 | client.execute = AsyncMock(return_value=response) 30 | await receive_contexts(client, ['default']) 31 | 32 | msg = client.execute.mock.call_args[0][0] 33 | assert msg.params.pop('protocol') == 'signalwire-proto-test' 34 | assert msg.params.pop('method') == 'signalwire.receive' 35 | assert msg.params.pop('params') == {'contexts': ['default']} 36 | assert client.contexts == ['default'] 37 | client.execute.mock.assert_called_once() 38 | 39 | @pytest.mark.asyncio 40 | async def test_receive_contexts_already_present(): 41 | client = Client(project='project', token='token', connection=MockedConnection) 42 | client.contexts = ['already_there'] 43 | client.execute = AsyncMock() 44 | await receive_contexts(client, ['already_there']) 45 | assert client.contexts == ['already_there'] 46 | client.execute.mock.assert_not_called() 47 | 48 | 49 | @pytest.mark.asyncio 50 | async def test_receive_contexts_with_mixed_contexts(): 51 | client = Client(project='project', token='token', connection=MockedConnection) 52 | client.protocol = 'signalwire-proto-test' 53 | client.contexts = ['already_there'] 54 | response = json.loads('{"requester_nodeid":"uuid","responder_nodeid":"uuid","result":{"code":"200","message":"Receiving all inbound related to the requested relay contexts and available scopes"}}') 55 | client.execute = AsyncMock(return_value=response) 56 | await receive_contexts(client, ['another_one']) 57 | 58 | msg = client.execute.mock.call_args[0][0] 59 | assert msg.params.pop('params') == {'contexts': ['another_one']} 60 | client.contexts.sort() 61 | assert client.contexts == ['already_there', 'another_one'] 62 | client.execute.mock.assert_called_once() 63 | -------------------------------------------------------------------------------- /signalwire/tests/test_fax_response.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | class TestFaxResponse(TestCase): 4 | def test_returns_laml(self): 5 | from signalwire.fax_response import FaxResponse 6 | r = FaxResponse() 7 | r.receive(action = '/foo/bar') 8 | self.assertEqual(str(r), '') 9 | 10 | def test_reject_laml(self): 11 | from signalwire.fax_response import FaxResponse 12 | r = FaxResponse() 13 | obj = r.reject() 14 | self.assertEqual(str(r), '') 15 | 16 | 17 | -------------------------------------------------------------------------------- /signalwire/tests/test_messaging_response.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | class TestMessagingResponse(TestCase): 4 | def test_returns_laml(self): 5 | from signalwire.messaging_response import MessagingResponse 6 | r = MessagingResponse() 7 | r.message("This is a message from SignalWire!") 8 | self.assertEqual(str(r), 'This is a message from SignalWire!') 9 | 10 | -------------------------------------------------------------------------------- /signalwire/tests/test_request_validator.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from multidict import MultiDict 3 | 4 | class TestRequestValidator(TestCase): 5 | def test_should_validate_no_compatibility(self): 6 | from signalwire.request_validator import RequestValidator 7 | 8 | url = 'https://81f2-2-45-18-191.ngrok-free.app/' 9 | token = 'PSK_7TruNcSNTxp4zNrykMj4EPzF' 10 | signature = 'b18500437ebb010220ddd770cbe6fd531ea0ba0d' 11 | body = '{"call":{"call_id":"b5d63b2e-f75b-4dc8-b6d4-269b635f96c0","node_id":"fa3570ae-f8bd-42c2-83f4-9950d906c91b@us-west","segment_id":"b5d63b2e-f75b-4dc8-b6d4-269b635f96c0","call_state":"created","direction":"inbound","type":"phone","from":"+12135877632","to":"+12089806814","from_number":"+12135877632","to_number":"+12089806814","project_id":"4b7ae78a-d02e-4889-a63b-08b156d5916e","space_id":"62615f44-2a34-4235-b38b-76b5a1de6ef8"},"vars":{}}' 12 | 13 | validator = RequestValidator(token) 14 | computed = validator.build_signature(url, body) 15 | self.assertIsInstance(computed, str) 16 | self.assertEqual(signature, computed) 17 | valid = validator.validate(url, body, signature) 18 | self.assertTrue(valid) 19 | 20 | def test_should_validate_with_compatibity(self): 21 | from signalwire.request_validator import RequestValidator 22 | 23 | url = 'https://mycompany.com/myapp.php?foo=1&bar=2' 24 | token = '12345' 25 | signature = 'RSOYDt4T1cUTdK1PDd93/VVr8B8=' 26 | body = { 27 | 'CallSid': 'CA1234567890ABCDE', 28 | 'Caller': '+14158675309', 29 | 'Digits': '1234', 30 | 'From': '+14158675309', 31 | 'To': '+18005551212', 32 | } 33 | 34 | validator = RequestValidator(token) 35 | valid = validator.validate(url, body, signature) 36 | self.assertTrue(valid) 37 | 38 | def test_should_validate_with_compatibity_flask(self): 39 | from signalwire.request_validator import RequestValidator 40 | 41 | url = 'https://mycompany.com/myapp.php?foo=1&bar=2' 42 | token = '12345' 43 | signature = 'RSOYDt4T1cUTdK1PDd93/VVr8B8=' 44 | body = MultiDict ( 45 | [ 46 | ('CallSid', 'CA1234567890ABCDE'), 47 | ('Caller', '+14158675309'), 48 | ('Digits', '1234'), 49 | ('From', '+14158675309'), 50 | ('To', '+18005551212') 51 | ] 52 | ) 53 | 54 | validator = RequestValidator(token) 55 | valid = validator.validate(url, body, signature) 56 | self.assertTrue(valid) 57 | 58 | def test_should_validate_from_signalwire_http_request(self): 59 | from signalwire.request_validator import RequestValidator 60 | 61 | url = 'http://0aac-189-71-169-171.ngrok-free.app/voice' 62 | token = 'PSK_V3bF8oyeRNpJWGoRWHNYQMUU' 63 | signature = 'lf3nWPmUr2y6jSeeoMW4mg58vgI=' #From Lib 64 | body = { 65 | "AccountSid": "6bfbbe86-a901-4197-8759-2a0de1fa319d", 66 | "ApiVersion": "2010-04-01", 67 | "CallbackSource": "call-progress-events", 68 | "CallSid": "0703574f-b151-465d-aedb-28972eb513c7", 69 | "CallStatus": "busy", 70 | "Direction": "outbound-api", 71 | "From": "sip:+17063958228@sip.swire.io", 72 | "HangupBy": "sip:jpsantos@joaosantos-2a0de1fa319d.sip.swire.io", 73 | "HangupDirection": "inbound", 74 | "Timestamp": "Thu, 09 Nov 2023 17:05:04 +0000", 75 | "To": "sip:jpsantos@joaosantos-2a0de1fa319d.sip.swire.io", 76 | "SipResultCode": "486" 77 | } 78 | 79 | validator = RequestValidator(token) 80 | valid = validator.validate(url, body, signature) 81 | self.assertTrue(valid) 82 | 83 | def test_should_validate_from_signalwire_https_request(self): 84 | from signalwire.request_validator import RequestValidator 85 | 86 | url = 'https://675d-189-71-169-171.ngrok-free.app/voice' 87 | token = 'PSK_V3bF8oyeRNpJWGoRWHNYQMUU' 88 | signature = 'muUMpldcBHlzuXGZ5gbw1ETZCYA=' 89 | body = { 90 | "CallSid": "a97d4e8a-6047-4e2b-be48-fb96b33b5642", 91 | "AccountSid": "6bfbbe86-a901-4197-8759-2a0de1fa319d", 92 | "ApiVersion": "2010-04-01", 93 | "Direction": "outbound-api", 94 | "From": "sip:+17063958228@sip.swire.io", 95 | "To": "sip:jpsantos@joaosantos-2a0de1fa319d.sip.swire.io", 96 | "Timestamp": "Thu, 09 Nov 2023 14:40:55 +0000", 97 | "CallStatus": "no-answer", 98 | "CallbackSource": "call-progress-events", 99 | "HangupDirection": "outbound", 100 | "HangupBy": "sip:+17063958228@sip.swire.io", 101 | "SipResultCode": "487" 102 | } 103 | 104 | 105 | validator = RequestValidator(token) 106 | valid = validator.validate(url, body, signature) 107 | self.assertTrue(valid) 108 | 109 | def test_should_validate_from_raw_json(self): 110 | from signalwire.request_validator import RequestValidator 111 | 112 | url = 'https://675d-189-71-169-171.ngrok-free.app/voice' 113 | token = 'PSK_V3bF8oyeRNpJWGoRWHNYQMUU' 114 | signature = 'muUMpldcBHlzuXGZ5gbw1ETZCYA=' 115 | body = '''{ 116 | "CallSid": "a97d4e8a-6047-4e2b-be48-fb96b33b5642", 117 | "AccountSid": "6bfbbe86-a901-4197-8759-2a0de1fa319d", 118 | "ApiVersion": "2010-04-01", 119 | "Direction": "outbound-api", 120 | "From": "sip:+17063958228@sip.swire.io", 121 | "To": "sip:jpsantos@joaosantos-2a0de1fa319d.sip.swire.io", 122 | "Timestamp": "Thu, 09 Nov 2023 14:40:55 +0000", 123 | "CallStatus": "no-answer", 124 | "CallbackSource": "call-progress-events", 125 | "HangupDirection": "outbound", 126 | "HangupBy": "sip:+17063958228@sip.swire.io", 127 | "SipResultCode": "487" 128 | }''' 129 | 130 | 131 | validator = RequestValidator(token) 132 | valid = validator.validate(url, body, signature) 133 | self.assertTrue(valid) 134 | 135 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /signalwire/tests/test_requests.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | import os, pytest 3 | from signalwire.rest import Client as signalwire_client 4 | import vcr 5 | 6 | my_vcr = vcr.VCR( 7 | cassette_library_dir='fixtures/cassettes', 8 | record_mode='once', 9 | ) 10 | 11 | @pytest.fixture(scope="module") 12 | def client(): 13 | client = signalwire_client(os.getenv('SIGNALWIRE_ACCOUNT','signalwire-account-123'), os.getenv('SIGNALWIRE_TOKEN', '123456'), signalwire_space_url = os.getenv('SIGNALWIRE_SPACE', 'myaccount.signalwire.com')) 14 | return client 15 | 16 | @my_vcr.use_cassette() 17 | def test_accounts(client): 18 | account = client.api.accounts(os.getenv('SIGNALWIRE_ACCOUNT','signalwire-account-123')).fetch() 19 | assert(account.friendly_name == 'LAML testing') 20 | 21 | 22 | @my_vcr.use_cassette() 23 | def test_applications(client): 24 | applications = client.applications.list() 25 | assert(applications[0].sid == '34f49a97-a863-4a11-8fef-bc399c6f0928') 26 | 27 | @my_vcr.use_cassette() 28 | def test_local_numbers(client): 29 | numbers = client.available_phone_numbers("US") \ 30 | .local \ 31 | .list(in_region="WA") 32 | assert(numbers[0].phone_number == '+12064015921') 33 | 34 | @my_vcr.use_cassette() 35 | def test_toll_free_numbers(client): 36 | numbers = client.available_phone_numbers("US") \ 37 | .toll_free \ 38 | .list(area_code="310") 39 | assert(numbers[0].phone_number == '+13103590741') 40 | 41 | @my_vcr.use_cassette() 42 | def test_conferences(client): 43 | conferences = client.conferences.list() 44 | 45 | assert(conferences[0].sid == 'a811cb2c-9e5a-415d-a951-701f8e884fb5') 46 | 47 | @my_vcr.use_cassette() 48 | def test_conference_members(client): 49 | participants = client.conferences('a811cb2c-9e5a-415d-a951-701f8e884fb5') \ 50 | .participants \ 51 | .list() 52 | 53 | assert(participants[0].call_sid == '7a520324-684d-435c-87c2-ea7975f371d0') 54 | 55 | @my_vcr.use_cassette() 56 | def test_incoming_phone_numbers(client): 57 | incoming_phone_numbers = client.incoming_phone_numbers.list() 58 | 59 | assert(incoming_phone_numbers[0].phone_number == '+18990000001') 60 | 61 | @my_vcr.use_cassette() 62 | def test_messages(client): 63 | message = client.messages.create( 64 | from_='+15059999999', 65 | to='+15058888888', 66 | media_url='https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png' 67 | ) 68 | 69 | assert(message.sid == 'cbad786b-fdcd-4d2a-bcb2-fff9df045008') 70 | 71 | @my_vcr.use_cassette() 72 | def test_media(client): 73 | media = client.messages('0da01046-5cca-462f-bc50-adae4e1307e1').media.list() 74 | assert(media[0].sid == 'a1ee4484-99a4-4996-b7df-fd3ceef2e9ec') 75 | 76 | @my_vcr.use_cassette() 77 | def test_recordings(client): 78 | recordings = client.recordings.list() 79 | assert(recordings[0].call_sid == 'd411976d-d319-4fbd-923c-57c62b6f677a') 80 | 81 | @my_vcr.use_cassette() 82 | def test_transcriptions(client): 83 | transcriptions = client.transcriptions.list() 84 | assert(transcriptions[0].recording_sid == 'e4c78e17-c0e2-441d-b5dd-39a6dad496f8') 85 | 86 | @my_vcr.use_cassette() 87 | def test_queues(client): 88 | queues = client.queues.list() 89 | assert(queues[0].sid == '2fd1bc9b-2e1f-41ac-988f-06842700c10d') 90 | 91 | @my_vcr.use_cassette() 92 | def test_queue_members(client): 93 | members = client.queues('2fd1bc9b-2e1f-41ac-988f-06842700c10d').members.list() 94 | assert(members[0].call_sid == '24c0f807-2663-4080-acef-c0874f45274d') 95 | 96 | @my_vcr.use_cassette() 97 | def test_send_fax(client): 98 | fax = client.fax.faxes.create( 99 | from_='+15556677888', 100 | to='+15556677999', 101 | media_url='https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf' 102 | ) 103 | assert(fax.sid == 'dd3e1ac4-50c9-4241-933a-5d4e9a2baf31') 104 | 105 | @my_vcr.use_cassette() 106 | def test_list_fax(client): 107 | faxes = client.fax.faxes.list() 108 | assert(faxes[0].sid == 'dd3e1ac4-50c9-4241-933a-5d4e9a2baf31') 109 | 110 | @my_vcr.use_cassette() 111 | def test_fetch_fax(client): 112 | fax = client.fax.faxes('831455c6-574e-4d8b-b6ee-2418140bf4cd').fetch() 113 | assert(fax.to == '+15556677999') 114 | assert(fax.media_url == 'https://s3.us-east-2.amazonaws.com/signalwire-assets/faxes/20190104162834-831455c6-574e-4d8b-b6ee-2418140bf4cd.tiff') 115 | 116 | @my_vcr.use_cassette() 117 | def test_fetch_fax_media(client): 118 | media = client.fax.faxes('831455c6-574e-4d8b-b6ee-2418140bf4cd').media.list() 119 | assert(media[0].sid == 'aff0684c-3445-49bc-802b-3a0a488139f5') 120 | 121 | @my_vcr.use_cassette() 122 | def test_fetch_fax_media_instance(client): 123 | media = client.fax.faxes('831455c6-574e-4d8b-b6ee-2418140bf4cd').media('aff0684c-3445-49bc-802b-3a0a488139f5').fetch() 124 | assert(media.url == '/api/laml/2010-04-01/Accounts/signalwire-account-123/Faxes/831455c6-574e-4d8b-b6ee-2418140bf4cd/Media/aff0684c-3445-49bc-802b-3a0a488139f5.json') 125 | -------------------------------------------------------------------------------- /signalwire/tests/test_sdk.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | import os 3 | 4 | class TestConfigurable(TestCase): 5 | def test_api_url(self): 6 | from signalwire.rest import Client as signalwire_client 7 | client = signalwire_client('account', 'token', signalwire_space_url = 'myname.signalwire.com') 8 | 9 | self.assertEqual(client.api.base_url, 'https://myname.signalwire.com') 10 | 11 | def test_api_url_scheme(self): 12 | from signalwire.rest import Client as signalwire_client 13 | client = signalwire_client('account', 'token', signalwire_space_url = 'https://myname.signalwire.com') 14 | 15 | self.assertEqual(client.api.base_url, 'https://myname.signalwire.com') 16 | 17 | def test_api_url_environnment(self): 18 | from signalwire.rest import Client as signalwire_client 19 | os.environ['SIGNALWIRE_SPACE_URL'] = 'myname.signalwire.com' 20 | client = signalwire_client('account', 'token') 21 | 22 | self.assertEqual(client.api.base_url, 'https://myname.signalwire.com') 23 | -------------------------------------------------------------------------------- /signalwire/tests/test_voice_response.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | class TestVoiceResponse(TestCase): 4 | def test_returns_laml(self): 5 | from signalwire.voice_response import VoiceResponse, Dial 6 | r = VoiceResponse() 7 | r.say("Welcome to SignalWire!") 8 | self.assertEqual(str(r), 'Welcome to SignalWire!') 9 | 10 | def test_supports_virtual_agent(self): 11 | from signalwire.voice_response import VoiceResponse 12 | r = VoiceResponse() 13 | connect = r.connect(action='http://example.com/action') 14 | connect.virtual_agent(connectorName='project', statusCallback='https://mycallbackurl.com') 15 | self.assertEqual(str(r), '') 16 | 17 | -------------------------------------------------------------------------------- /signalwire/voice_response/__init__.py: -------------------------------------------------------------------------------- 1 | from twilio.twiml.voice_response import * 2 | --------------------------------------------------------------------------------