From 988601b10e02394e18f05fafeaf649b4aa27bfee Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Wed, 19 Aug 2020 23:30:15 +0200 Subject: [PATCH] Added Twilio integration [closes #132] --- docs/source/conf.py | 1 + platypush/plugins/twilio.py | 646 ++++++++++++++++++++++++++++++++++++ requirements.txt | 5 +- setup.py | 2 + 4 files changed, 653 insertions(+), 1 deletion(-) create mode 100644 platypush/plugins/twilio.py diff --git a/docs/source/conf.py b/docs/source/conf.py index 502811e8..ce661e6c 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -257,6 +257,7 @@ autodoc_mock_imports = ['googlesamples.assistant.grpc.audio_helpers', 'dbus', 'gi', 'gi.repository', + 'twilio', ] sys.path.insert(0, os.path.abspath('../..')) diff --git a/platypush/plugins/twilio.py b/platypush/plugins/twilio.py new file mode 100644 index 00000000..0780e9d6 --- /dev/null +++ b/platypush/plugins/twilio.py @@ -0,0 +1,646 @@ +import datetime +import enum +import json +from typing import List, Dict, Optional + +import requests + +from twilio.rest import Client +from twilio.base import values + +from platypush.message import Message +from platypush.plugins import Plugin, action + + +class TwilioPhoneNumberType(enum.Enum): + MOBILE = 'mobile' + LOCAL = 'local' + TOLL_FREE = 'toll_free' + + +class TwilioPlugin(Plugin): + """ + The Twilio plugin allows you to send messages and WhatsApp texts and make programmable phone call by using a Twilio + account. Note that some features may require a Premium account. + + Requires: + + * **twilio** (``pip install twilio``) + """ + + _api_base_url = 'https://api.twilio.com' + + def __init__(self, + account_sid: str, + auth_token: str, + address_sid: Optional[str] = None, + phone_number: Optional[str] = None, + address_book: Optional[Dict[str, str]] = None, + **kwargs): + """ + :param account_sid: Account SID. + :param auth_token: Account authentication token. + :param address_sid: SID of the default physical address - required to register a new number in some countries. + :param phone_number: Default phone number associated to the account to be used for messages and calls. + :param address_book: ``name``->``phone_number`` mapping of contacts. You can use directly these names to send + messages and make calls instead of the full phone number. + """ + super().__init__(**kwargs) + self.account_sid = account_sid + self.auth_token = auth_token + self.address_sid = address_sid + self.phone_number = phone_number + self.address_book = address_book or {} + self.client = Client(account_sid, auth_token) + + @action + def get_available_phone_numbers(self, country: str, number_type: str) -> List[dict]: + """ + Get a list of phone numbers of a certain type available for a certain country. + + :param country: Country code (e.g. ``US`` or ``NL``). + :param number_type: Phone number type - e.g. ``mobile``, ``local`` or ``toll_free``. + :return: A list of the available phone numbers with their properties and capabilities. Example:: + + .. code-block:: json + + [ + { + "friendly_name": "+311234567890", + "phone_number": "+311234567890", + "lata": null, + "rate_center": null, + "latitude": null, + "longitude": null, + "locality": null, + "region": null, + "postal_code": null, + "iso_country": "NL", + "address_requirements": "any", + "beta": false, + "capabilities": { + "voice": true, + "SMS": true, + "MMS": false, + "fax": false + } + }, + ... + ] + """ + phone_numbers = self.client.available_phone_numbers(country.upper()).fetch() + resp = requests.get(self._api_base_url + phone_numbers.uri, auth=(self.account_sid, self.auth_token)).json() + assert 'subresource_uris' in resp, 'No available phone numbers found for the country {}'.format(country) + assert number_type in resp['subresource_uris'], 'No "{}" phone numbers available - available types: {}'.format( + number_type, list(resp['subresource_uris'].keys()) + ) + + resp = requests.get(self._api_base_url + resp['subresource_uris'][number_type], + auth=(self.account_sid, self.auth_token)).json() + + phone_numbers = resp['available_phone_numbers'] + assert len(phone_numbers), 'No phone numbers available' + return phone_numbers + + @action + def create_address(self, + customer_name: str, + street: str, + city: str, + region: str, + postal_code: str, + iso_country: str): + # noinspection SpellCheckingInspection + """ + Create a new address associated to your account. + + :param customer_name: Full name of the customer. + :param street: Street name. + :param city: City name. + :param region: Region name. + :param postal_code: Postal code. + :param iso_country: ISO code of the country. + :return: Details of the newly created address. Example: + + .. code-block:: json + + { + "account_sid": "ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "city": "city", + "customer_name": "customer_name", + "date_created": "Tue, 18 Aug 2015 17:07:30 +0000", + "date_updated": "Tue, 18 Aug 2015 17:07:30 +0000", + "emergency_enabled": false, + "friendly_name": null, + "iso_country": "US", + "postal_code": "postal_code", + "region": "region", + "sid": "ADXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "street": "street", + "validated": false, + "verified": false, + "uri": "/2010-04-01/Accounts/ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Addresses/ADXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.json" + } + + """ + address = self.client.addresses.create(customer_name=customer_name, + street=street, + city=city, + region=region, + postal_code=postal_code, + iso_country=iso_country) + + # noinspection PyProtectedMember + return address._properties + + @action + def register_phone_number(self, + phone_number: str, + friendly_name: str = values.unset, + address_sid: str = values.unset, + sms_url: str = values.unset, + sms_fallback_url: str = values.unset, + status_callback: str = values.unset, + voice_caller_id_lookup: bool = True, + voice_url: str = values.unset, + voice_fallback_url: str = values.unset, + area_code: str = values.unset) -> dict: + # noinspection SpellCheckingInspection + """ + Request to allocate a phone number on your Twilio account. The phone number should first be displayed as + available in :meth:`get_available_phone_numbers`. + + :param phone_number: Phone number to be allocated. + :param friendly_name: A string used to identify your new phone number. + :param address_sid: Address SID. NOTE: some countries may require you to specify a valid address in + order to register a new phone number (see meth:`create_address`). If none is specified then the + configured ``address_sid`` (if available) will be applied. + :param sms_url: URL to call when an SMS is received. + :param sms_fallback_url: URL to call when an error occurs on SMS delivery/receipt. + :param status_callback: URL to call when a status change occurs. + :param voice_caller_id_lookup: Whether to perform ID lookup for incoming caller numbers. + :param voice_url: URL to call when the number receives a call. + :param voice_fallback_url: URL to call when a call fails. + :param area_code: Override the area code for the new number. + :return: Status of the newly created number. Example: + + .. code-block:: json + + { + "account_sid": "ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "address_requirements": "none", + "address_sid": "ADXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "api_version": "2010-04-01", + "beta": false, + "capabilities": { + "voice": true, + "sms": false, + "mms": true, + "fax": false + }, + "date_created": "Thu, 30 Jul 2015 23:19:04 +0000", + "date_updated": "Thu, 30 Jul 2015 23:19:04 +0000", + "emergency_status": "Active", + "emergency_address_sid": "ADXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "friendly_name": "friendly_name", + "identity_sid": "RIXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "origin": "origin", + "phone_number": "+18089255327", + "sid": "PNXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "sms_application_sid": "APXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "sms_fallback_method": "GET", + "sms_fallback_url": "https://example.com", + "sms_method": "GET", + "sms_url": "https://example.com", + "status_callback": "https://example.com", + "status_callback_method": "GET", + "trunk_sid": null, + "uri": "/2010-04-01/Accounts/ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/IncomingPhoneNumbers/PNXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.json", + "voice_application_sid": "APXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "voice_caller_id_lookup": false, + "voice_fallback_method": "GET", + "voice_fallback_url": "https://example.com", + "voice_method": "GET", + "voice_url": "https://example.com", + "bundle_sid": "BUXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "voice_receive_mode": "voice", + "status": "in-use" + } + + """ + status = self.client.incoming_phone_numbers.create(phone_number=phone_number, + friendly_name=friendly_name, + sms_url=sms_url, + sms_fallback_url=sms_fallback_url, + status_callback=status_callback, + voice_caller_id_lookup=voice_caller_id_lookup, + voice_url=voice_url, + voice_fallback_url=voice_fallback_url, + area_code=area_code, + address_sid=address_sid or self.address_sid) + + # noinspection PyProtectedMember + return status._properties + + @action + def send_message(self, + body: str, + to: str, + from_: Optional[str] = None, + status_callback: Optional[str] = None, + max_price: Optional[str] = None, + attempt: Optional[int] = None, + validity_period: Optional[int] = None, + smart_encoded: bool = True, + media_url: Optional[str] = None) -> dict: + # noinspection SpellCheckingInspection + """ + Send an SMS/MMS. + Note: WhatsApp messages are also supported (and free of charge), although the functionality is currently quite + limited. Full support is only available to WhatsApp Business profiles and indipendent software vendors approved + by WhatsApp. If that's not the case, you can send WhatsApp messages through the Twilio Test account/number - + as of now the ``from_`` field should be ``whatsapp:+14155238886`` and the ``to`` field should be + ``whatsapp:+``. More information `here `_. + + :param body: Message body. + :param to: Recipient number or address book name. + :param from_: Sender number. If none is specified then the default configured ``phone_number`` will be + used if available. + :param status_callback: The URL to call to send status information to the application. + :param max_price: The total maximum price up to 4 decimal places in US dollars acceptable for the message to be + delivered. + :param attempt: Total numer of attempts made , this inclusive to send out the message. + :param validity_period: The number of seconds that the message can remain in our outgoing queue. + :param smart_encoded: Whether to detect Unicode characters that have a similar GSM-7 character and replace them. + :param media_url: The URL of the media to send with the message. + :return: A mapping representing the status of the delivery. Example: + + .. code-block:: json + + { + "account_sid": "ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "api_version": "2010-04-01", + "body": "Sent from your Twilio trial account - It works!", + "date_created": "2020-08-17T16:32:09.341", + "date_updated": "2020-08-17T16:32:09.526", + "date_sent": null, + "direction": "outbound-api", + "error_code": null, + "error_message": null, + "from_": "+XXXXXXXXX", + "messaging_service_sid": null, + "num_media": "0", + "num_segments": "1", + "price": null, + "price_unit": "USD", + "sid": "XXXXXXXXXXXXX", + "status": "queued", + "subresource_uris": { + "media": "/2010-04-01/Accounts/ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Messages/SMXXXXXXXXXXXXXXXXXX/Media.json" + }, + "to": "+XXXXXXXXXXXXXX", + "uri": "/2010-04-01/Accounts/ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Messages/SMXXXXXXXXXXXXXXXXXX.json" + } + + """ + # noinspection SpellCheckingInspection + assert from_ or self.phone_number, 'No valid sender phone number specified nor configured' + if to in self.address_book: + to = self.address_book[to] + + status = self.client.messages.create( + body=body, + from_=from_ or self.phone_number, + to=to, + media_url=media_url or values.unset, + max_price=max_price or values.unset, + attempt=str(attempt) if attempt else values.unset, + status_callback=status_callback or values.unset, + validity_period=str(validity_period) if validity_period else values.unset, + smart_encoded=smart_encoded, + ) + + # noinspection PyProtectedMember + return status._properties + + @action + def list_messages(self, + to: Optional[str] = None, + from_: Optional[str] = None, + date_sent_before: Optional[str] = None, + date_sent: Optional[str] = None, + date_sent_after: Optional[str] = None, + limit: Optional[int] = None, + page_size: Optional[int] = None) -> List[dict]: + # noinspection SpellCheckingInspection + """ + List all messages matching the specified criteria. + + :param to: Recipient phone number or address book name. + :param from_: Sender phone number. + :param date_sent_before: Maximum date filter (ISO format: YYYYMMDD with or without time). + :param date_sent: Date filter (ISO format: YYYYMMDD with or without time). + :param date_sent_after: Minimum date filter (ISO format: YYYYMMDD with or without time). + :param limit: Maximum number of messages to be returned. + :param page_size: Maximum number of messages per page. + :return: List of selected messages. Example: + + .. code-block:: json + + { + "account_sid": "ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "api_version": "2010-04-01", + "body": "testing", + "date_created": "Fri, 24 May 2019 17:44:46 +0000", + "date_sent": "Fri, 24 May 2019 17:44:50 +0000", + "date_updated": "Fri, 24 May 2019 17:44:50 +0000", + "direction": "outbound-api", + "error_code": null, + "error_message": null, + "from": "+12019235161", + "messaging_service_sid": null, + "num_media": "0", + "num_segments": "1", + "price": "-0.00750", + "price_unit": "USD", + "sid": "SMded05904ccb347238880ca9264e8fe1c", + "status": "sent", + "subresource_uris": { + "media": "/2010-04-01/Accounts/ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Messages/SMded05904ccb347238880ca9264e8fe1c/Media.json", + "feedback": "/2010-04-01/Accounts/ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Messages/SMded05904ccb347238880ca9264e8fe1c/Feedback.json" + }, + "to": "+18182008801", + "uri": "/2010-04-01/Accounts/ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Messages/SMded05904ccb347238880ca9264e8fe1c.json" + } + """ + if to in self.address_book: + to = self.address_book[to] + + # noinspection PyTypeChecker + messages = self.client.messages.list(to=to, from_=from_, + date_sent_before=datetime.datetime.fromisoformat(date_sent_before) + if date_sent_before else None, + date_sent_after=datetime.datetime.fromisoformat(date_sent_after) + if date_sent_after else None, + date_sent=datetime.datetime.fromisoformat(date_sent) + if date_sent else None, + limit=limit, page_size=page_size) + + # noinspection PyProtectedMember + return json.loads(json.dumps([msg._properties for msg in messages], indent=2, cls=Message.Encoder)) + + @action + def get_message(self, message_sid: str) -> dict: + """ + Get the details of a stored message. + + :param message_sid: Message SID to be retrieved. + :return: Message with its properties - see :meth:`send_message`. + """ + msg = self.client.messages(message_sid).fetch() + # noinspection PyProtectedMember + return msg._properties + + @action + def update_message(self, message_sid: str, body: str) -> dict: + """ + Update/redact the body of a message. + + :param message_sid: Message SID to be updated. + :param body: New message body. + :return: Updated message with its properties - see :meth:`send_message`. + """ + msg = self.client.messages(message_sid).update(body) + # noinspection PyProtectedMember + return msg._properties + + @action + def delete_message(self, message_sid: str): + """ + Delete a send/received message. + + :param message_sid: Message SID to be deleted. + """ + self.client.messages(message_sid).delete() + + @action + def make_call(self, + twiml: str, + to: str, + from_: Optional[str] = None, + method: Optional[str] = None, + status_callback: Optional[str] = None, + status_callback_event: Optional[str] = None, + status_callback_method: Optional[str] = None, + fallback_url: Optional[str] = None, + fallback_method: Optional[str] = None, + send_digits: Optional[str] = None, + timeout: Optional[int] = 30, + record: bool = False, + recording_channels: Optional[int] = None, + recording_status_callback: Optional[str] = None, + recording_status_callback_method: Optional[str] = None, + recording_status_callback_event: Optional[str] = None, + sip_auth_username: Optional[str] = None, + sip_auth_password: Optional[str] = None, + caller_id: Optional[str] = None, + call_reason: Optional[str] = None) -> dict: + # noinspection SpellCheckingInspection + """ + Make an automated phone call from a registered Twilio number. + + :param twiml: TwiML containing the logic to be executed in the call (see https://www.twilio.com/docs/voice/twiml). + :param to: Recipient phone number or address book name. + :param from_: Registered Twilio phone number that will perform the call (default: default configured phone number). + :param method: HTTP method to use to fetch TwiML if it's provided remotely. + :param status_callback: The URL that should be called to send status information to your application. + :param status_callback_event: The call progress events to be sent to the ``status_callback`` URL. + :param status_callback_method: HTTP Method to use with status_callback. + :param fallback_url: Fallback URL in case of error. + :param fallback_method: HTTP Method to use with fallback_url. + :param send_digits: The digits to dial after connecting to the number. + :param timeout: Number of seconds to wait for an answer. + :param record: Whether to record the call. + :param recording_channels: The number of channels in the final recording. + :param recording_status_callback: The URL that we call when the recording is available to be accessed. + :param recording_status_callback_method: The HTTP method to use when calling the `recording_status_callback` URL. + :param recording_status_callback_event: The recording status events that will trigger calls to the URL specified + in `recording_status_callback` + :param sip_auth_username: The username used to authenticate the caller making a SIP call. + :param sip_auth_password: The password required to authenticate the user account specified in `sip_auth_username`. + :param caller_id: The phone number, SIP address, or Client identifier that made this call. Phone numbers are in + E.164 format (e.g., +16175551212). SIP addresses are formatted as `name@company.com`. + :param call_reason: Reason for the call (Branded Calls Beta). + :return: The call properties and details, as a dictionary. Example: + + .. code-block:: json + + { + "account_sid": "ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "annotation": null, + "answered_by": null, + "api_version": "2010-04-01", + "caller_name": null, + "date_created": "Tue, 31 Aug 2010 20:36:28 +0000", + "date_updated": "Tue, 31 Aug 2010 20:36:44 +0000", + "direction": "inbound", + "duration": "15", + "end_time": "Tue, 31 Aug 2010 20:36:44 +0000", + "forwarded_from": "+141586753093", + "from": "+15017122661", + "from_formatted": "(501) 712-2661", + "group_sid": null, + "parent_call_sid": null, + "phone_number_sid": "PNXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "price": "-0.03000", + "price_unit": "USD", + "sid": "CAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "start_time": "Tue, 31 Aug 2010 20:36:29 +0000", + "status": "completed", + "subresource_uris": { + "notifications": "/2010-04-01/Accounts/ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Calls/CAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Notifications.json", + "recordings": "/2010-04-01/Accounts/ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Calls/CAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Recordings.json", + "feedback": "/2010-04-01/Accounts/ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Calls/CAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Feedback.json", + "feedback_summaries": "/2010-04-01/Accounts/ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Calls/FeedbackSummary.json", + "payments": "/2010-04-01/Accounts/ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Calls/CAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Payments.json" + }, + "to": "+14155551212", + "to_formatted": "(415) 555-1212", + "trunk_sid": null, + "uri": "/2010-04-01/Accounts/ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Calls/CAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.json", + "queue_time": "1000" + } + + """ + if to in self.address_book: + to = self.address_book[to] + + call = self.client.calls.create(to=to, + from_=from_ or self.phone_number, + twiml=twiml, + method=method, + status_callback=status_callback, + status_callback_event=status_callback_event, + status_callback_method=status_callback_method, + fallback_url=fallback_url, + fallback_method=fallback_method, + send_digits=send_digits, + timeout=str(timeout) if timeout else None, + record=record, + recording_channels=str(recording_channels) if recording_channels else None, + recording_status_callback=recording_status_callback, + recording_status_callback_method=recording_status_callback_method, + recording_status_callback_event=recording_status_callback_event, + sip_auth_username=sip_auth_username, + sip_auth_password=sip_auth_password, + caller_id=caller_id, + call_reason=call_reason) + + # noinspection PyProtectedMember + return call._properties + + @action + def list_calls(self, + to: Optional[str] = None, + from_: Optional[str] = None, + parent_call_sid: Optional[str] = None, + status: Optional[str] = None, + start_time_before: Optional[str] = None, + start_time: Optional[str] = None, + start_time_after: Optional[str] = None, + end_time_before: Optional[str] = None, + end_time: Optional[str] = None, + end_time_after: Optional[str] = None, + limit: Optional[int] = None, + page_size: Optional[int] = None) -> List[dict]: + # noinspection SpellCheckingInspection + """ + List the calls performed by the account, either the full list or those that match some filter. + + :param to: Phone number or Client identifier of calls to include + :param from_: Phone number or Client identifier to filter `from` on + :param parent_call_sid: Parent call SID to filter on + :param status: The status of the resources to read + :param start_time_before: Only include calls that started on this date + :param start_time: Only include calls that started on this date + :param start_time_after: Only include calls that started on this date + :param end_time_before: Only include calls that ended on this date + :param end_time: Only include calls that ended on this date + :param end_time_after: Only include calls that ended on this date + :param limit: Upper limit for the number of records to return. list() guarantees + never to return more than limit. Default is no limit + :param page_size: Number of records to fetch per request, when not set will use + the default value of 50 records. If no page_size is defined + but a limit is defined, list() will attempt to read the limit + with the most efficient page size, i.e. min(limit, 1000) + :return: A list of dictionaries, each representing the information of a call. Example: + + .. code-block:: json + + [ + { + "account_sid": "ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "annotation": "billingreferencetag1", + "answered_by": "machine_start", + "api_version": "2010-04-01", + "caller_name": "callerid1", + "date_created": "Fri, 18 Oct 2019 17:00:00 +0000", + "date_updated": "Fri, 18 Oct 2019 17:01:00 +0000", + "direction": "outbound-api", + "duration": "4", + "end_time": "Fri, 18 Oct 2019 17:03:00 +0000", + "forwarded_from": "calledvia1", + "from": "+13051416799", + "from_formatted": "(305) 141-6799", + "group_sid": "GPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "parent_call_sid": "CAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "phone_number_sid": "PNXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "price": "-0.200", + "price_unit": "USD", + "sid": "CAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "start_time": "Fri, 18 Oct 2019 17:02:00 +0000", + "status": "completed", + "subresource_uris": { + "feedback": "/2010-04-01/Accounts/ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Calls/CAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Feedback.json", + "feedback_summaries": "/2010-04-01/Accounts/ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Calls/FeedbackSummary.json", + "notifications": "/2010-04-01/Accounts/ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Calls/CAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Notifications.json", + "recordings": "/2010-04-01/Accounts/ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Calls/CAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Recordings.json", + "payments": "/2010-04-01/Accounts/ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Calls/CAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Payments.json" + }, + "to": "+13051913581", + "to_formatted": "(305) 191-3581", + "trunk_sid": "TKXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "uri": "/2010-04-01/Accounts/ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Calls/CAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.json", + "queue_time": "1000" + } + ] + + """ + + # noinspection PyTypeChecker + call_list = self.client.calls.list( + to=to, + from_=from_, + parent_call_sid=parent_call_sid, + status=status, + start_time_before=datetime.datetime.fromisoformat(start_time_before) + if start_time_before else None, + start_time=datetime.datetime.fromisoformat(start_time) + if start_time else None, + start_time_after=datetime.datetime.fromisoformat(start_time_after) + if start_time_after else None, + end_time_before=datetime.datetime.fromisoformat(end_time_before) + if end_time_before else None, + end_time=datetime.datetime.fromisoformat(end_time) + if end_time else None, + end_time_after=datetime.datetime.fromisoformat(end_time_after) + if end_time_after else None, + limit=limit, + page_size=page_size + ) + + # noinspection PyProtectedMember + return json.loads(json.dumps([call._properties for call in call_list], indent=2, cls=Message.Encoder)) + + +# vim:sw=4:ts=4:et: diff --git a/requirements.txt b/requirements.txt index 9b50b300..176242e2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -278,4 +278,7 @@ croniter # git+https://github.com/rm-hull/luma.oled # Support for DBus integration -# python-dbus \ No newline at end of file +# python-dbus + +# Support for Twilio +# twilio \ No newline at end of file diff --git a/setup.py b/setup.py index 541feef9..475ce4d8 100755 --- a/setup.py +++ b/setup.py @@ -321,5 +321,7 @@ setup( 'luma-oled': ['luma.oled @ git+https://github.com/rm-hull/luma.oled'], # Support for DBus integration 'dbus': ['dbus-python'], + # Support for Twilio integration + 'twilio': ['twilio'], }, )