Skip to content

Commit

Permalink
Add Twilio API key support (#440)
Browse files Browse the repository at this point in the history
  • Loading branch information
mnkhouri committed Sep 15, 2021
1 parent e0f928f commit 3c552a6
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 7 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ The table below identifies the services this tool supports and some example serv
| [MSG91](https://github.com/caronc/apprise/wiki/Notify_msg91) | msg91:// | (TCP) 443 | msg91://AuthKey/ToPhoneNo<br/>msg91://SenderID@AuthKey/ToPhoneNo<br/>msg91://AuthKey/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/
| [Nexmo](https://github.com/caronc/apprise/wiki/Notify_nexmo) | nexmo:// | (TCP) 443 | nexmo://ApiKey:ApiSecret@FromPhoneNo<br/>nexmo://ApiKey:ApiSecret@FromPhoneNo/ToPhoneNo<br/>nexmo://ApiKey:ApiSecret@FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/
| [Sinch](https://github.com/caronc/apprise/wiki/Notify_sinch) | sinch:// | (TCP) 443 | sinch://ServicePlanId:ApiToken@FromPhoneNo<br/>sinch://ServicePlanId:ApiToken@FromPhoneNo/ToPhoneNo<br/>sinch://ServicePlanId:ApiToken@FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/<br/>sinch://ServicePlanId:ApiToken@ShortCode/ToPhoneNo<br/>sinch://ServicePlanId:ApiToken@ShortCode/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/
| [Twilio](https://github.com/caronc/apprise/wiki/Notify_twilio) | twilio:// | (TCP) 443 | twilio://AccountSid:AuthToken@FromPhoneNo<br/>twilio://AccountSid:AuthToken@FromPhoneNo/ToPhoneNo<br/>twilio://AccountSid:AuthToken@FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/<br/>twilio://AccountSid:AuthToken@ShortCode/ToPhoneNo<br/>twilio://AccountSid:AuthToken@ShortCode/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/
| [Twilio](https://github.com/caronc/apprise/wiki/Notify_twilio) | twilio:// | (TCP) 443 | twilio://AccountSid:AuthToken@FromPhoneNo<br/>twilio://AccountSid:AuthToken@FromPhoneNo/ToPhoneNo<br/>twilio://AccountSid:AuthToken@FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/<br/>twilio://AccountSid:AuthToken@FromPhoneNo/ToPhoneNo?apikey=Key<br/>twilio://AccountSid:AuthToken@ShortCode/ToPhoneNo<br/>twilio://AccountSid:AuthToken@ShortCode/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/

## Desktop Notification Support
| Notification Service | Service ID | Default Port | Example Syntax |
Expand Down
28 changes: 23 additions & 5 deletions apprise/plugins/NotifyTwilio.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ class NotifyTwilio(NotifyBase):
'type': 'string',
'private': True,
'required': True,
'regex': (r'^[a-f0-9]+$', 'i'),
'regex': (r'^[a-z0-9]+$', 'i'),
},
'from_phone': {
'name': _('From Phone No'),
Expand Down Expand Up @@ -150,10 +150,16 @@ class NotifyTwilio(NotifyBase):
'token': {
'alias_of': 'auth_token',
},
'apikey': {
'name': _('API Key'),
'type': 'string',
'private': True,
'regex': (r'^SK[a-f0-9]+$', 'i'),
},
})

def __init__(self, account_sid, auth_token, source, targets=None,
**kwargs):
apikey=None, ** kwargs):
"""
Initialize Twilio Object
"""
Expand All @@ -177,6 +183,10 @@ def __init__(self, account_sid, auth_token, source, targets=None,
self.logger.warning(msg)
raise TypeError(msg)

# The API Key associated with the account (optional)
self.apikey = validate_regex(
apikey, *self.template_args['apikey']['regex'])

result = is_phone_no(source, min_len=5)
if not result:
msg = 'The Account (From) Phone # or Short-code specified ' \
Expand Down Expand Up @@ -218,7 +228,7 @@ def __init__(self, account_sid, auth_token, source, targets=None,
continue

# store valid phone number
self.targets.append('+{}'.format(result))
self.targets.append('+{}'.format(result['full']))

return

Expand Down Expand Up @@ -259,8 +269,8 @@ def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
# Create a copy of the targets list
targets = list(self.targets)

# Set up our authentication
auth = (self.account_sid, self.auth_token)
# Set up our authentication. Prefer the API Key if provided.
auth = (self.apikey or self.account_sid, self.auth_token)

if len(targets) == 0:
# No sources specified, use our own phone no
Expand Down Expand Up @@ -354,6 +364,10 @@ def url(self, privacy=False, *args, **kwargs):
# Our URL parameters
params = self.url_parameters(privacy=privacy, *args, **kwargs)

if self.apikey is not None:
# apikey specified; pass it back on the url
params['apikey'] = self.apikey

return '{schema}://{sid}:{token}@{source}/{targets}/?{params}'.format(
schema=self.secure_protocol,
sid=self.pprint(
Expand Down Expand Up @@ -400,6 +414,10 @@ def parse_url(url):
results['account_sid'] = \
NotifyTwilio.unquote(results['qsd']['sid'])

# API Key
if 'apikey' in results['qsd'] and len(results['qsd']['apikey']):
results['apikey'] = results['qsd']['apikey']

# Support the 'from' and 'source' variable so that we can support
# targets this way too.
# The 'from' makes it easier to use yaml configuration
Expand Down
2 changes: 1 addition & 1 deletion apprise/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ def is_phone_no(phone, min_len=11):
Returns:
bool: Returns False if the address specified is not a phone number
and a dictionary of the parsed email if it is as:
and a dictionary of the parsed phone number if it is as:
{
'country': '1',
'area': '800',
Expand Down
105 changes: 105 additions & 0 deletions test/test_twilio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

import six
import mock
import requests
from apprise import plugins
from apprise import Apprise

# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)


@mock.patch('requests.post')
def test_twilio_auth(mock_post):
"""
API: NotifyTwilio() Tests using:
- account-wide auth token
- API key and its own auth token
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0

response = mock.Mock()
response.content = ''
response.status_code = requests.codes.ok

# Prepare Mock
mock_post.return_value = response

# Initialize some generic (but valid) tokens
account_sid = 'AC{}'.format('b' * 32)
apikey = 'SK{}'.format('b' * 32)
auth_token = '{}'.format('b' * 32)
source = '+1 (555) 123-3456'
dest = '+1 (555) 987-6543'
message_contents = "test"

# Variation of initialization without API key
obj = Apprise.instantiate(
'twilio://{}:{}@{}/{}'
.format(account_sid, auth_token, source, dest))
assert isinstance(obj, plugins.NotifyTwilio) is True
assert isinstance(obj.url(), six.string_types) is True

# Send Notification
assert obj.send(body=message_contents) is True

# Variation of initialization with API key
obj = Apprise.instantiate(
'twilio://{}:{}@{}/{}?apikey={}'
.format(account_sid, auth_token, source, dest, apikey))
assert isinstance(obj, plugins.NotifyTwilio) is True
assert isinstance(obj.url(), six.string_types) is True

# Send Notification
assert obj.send(body=message_contents) is True

# Validate expected call parameters
assert mock_post.call_count == 2
first_call = mock_post.call_args_list[0]
second_call = mock_post.call_args_list[1]

# URL and message parameters are the same for both calls
assert first_call[0][0] == \
second_call[0][0] == \
'https://api.twilio.com/2010-04-01/Accounts/{}/Messages.json'.format(
account_sid)
assert first_call[1]['data']['Body'] == \
second_call[1]['data']['Body'] == \
message_contents
assert first_call[1]['data']['From'] == \
second_call[1]['data']['From'] == \
'+15551233456'
assert first_call[1]['data']['To'] == \
second_call[1]['data']['To'] == \
'+15559876543'

# Auth differs depending on if API Key is used
assert first_call[1]['auth'] == (account_sid, auth_token)
assert second_call[1]['auth'] == (apikey, auth_token)

0 comments on commit 3c552a6

Please sign in to comment.