From 4c57d76ee7d88b04b3ac2667f91d9e7062486415 Mon Sep 17 00:00:00 2001 From: Chris Caron Date: Sat, 18 Jun 2022 22:18:42 -0400 Subject: [PATCH] Nexmo is now Vonage (#609) --- KEYWORDS | 1 + README.md | 2 +- .../{NotifyNexmo.py => NotifyVonage.py} | 67 ++++++------ packaging/redhat/python-apprise.spec | 3 +- ..._plugin_nexmo.py => test_plugin_vonage.py} | 100 +++++++++++++++--- 5 files changed, 121 insertions(+), 52 deletions(-) rename apprise/plugins/{NotifyNexmo.py => NotifyVonage.py} (84%) rename test/{test_plugin_nexmo.py => test_plugin_vonage.py} (59%) diff --git a/KEYWORDS b/KEYWORDS index 58dd2ee8..822a0c07 100644 --- a/KEYWORDS +++ b/KEYWORDS @@ -78,6 +78,7 @@ Telegram Twilio Twist Twitter +Vonage Webex Windows XBMC diff --git a/README.md b/README.md index 9a3a3f2a..91101d97 100644 --- a/README.md +++ b/README.md @@ -112,9 +112,9 @@ The table below identifies the services this tool supports and some example serv | [Kavenegar](https://github.com/caronc/apprise/wiki/Notify_kavenegar) | kavenegar:// | (TCP) 443 | kavenegar://ApiKey/ToPhoneNo
kavenegar://FromPhoneNo@ApiKey/ToPhoneNo
kavenegar://ApiKey/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN | [MessageBird](https://github.com/caronc/apprise/wiki/Notify_messagebird) | msgbird:// | (TCP) 443 | msgbird://ApiKey/FromPhoneNo
msgbird://ApiKey/FromPhoneNo/ToPhoneNo
msgbird://ApiKey/FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/ | [MSG91](https://github.com/caronc/apprise/wiki/Notify_msg91) | msg91:// | (TCP) 443 | msg91://AuthKey/ToPhoneNo
msg91://SenderID@AuthKey/ToPhoneNo
msg91://AuthKey/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/ -| [Nexmo](https://github.com/caronc/apprise/wiki/Notify_nexmo) | nexmo:// | (TCP) 443 | nexmo://ApiKey:ApiSecret@FromPhoneNo
nexmo://ApiKey:ApiSecret@FromPhoneNo/ToPhoneNo
nexmo://ApiKey:ApiSecret@FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/ | [Sinch](https://github.com/caronc/apprise/wiki/Notify_sinch) | sinch:// | (TCP) 443 | sinch://ServicePlanId:ApiToken@FromPhoneNo
sinch://ServicePlanId:ApiToken@FromPhoneNo/ToPhoneNo
sinch://ServicePlanId:ApiToken@FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/
sinch://ServicePlanId:ApiToken@ShortCode/ToPhoneNo
sinch://ServicePlanId:ApiToken@ShortCode/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/ | [Twilio](https://github.com/caronc/apprise/wiki/Notify_twilio) | twilio:// | (TCP) 443 | twilio://AccountSid:AuthToken@FromPhoneNo
twilio://AccountSid:AuthToken@FromPhoneNo/ToPhoneNo
twilio://AccountSid:AuthToken@FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/
twilio://AccountSid:AuthToken@FromPhoneNo/ToPhoneNo?apikey=Key
twilio://AccountSid:AuthToken@ShortCode/ToPhoneNo
twilio://AccountSid:AuthToken@ShortCode/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/ +| [Vonage](https://github.com/caronc/apprise/wiki/Notify_nexmo) (formerly Nexmo) | nexmo:// | (TCP) 443 | nexmo://ApiKey:ApiSecret@FromPhoneNo
nexmo://ApiKey:ApiSecret@FromPhoneNo/ToPhoneNo
nexmo://ApiKey:ApiSecret@FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/ ## Desktop Notification Support | Notification Service | Service ID | Default Port | Example Syntax | diff --git a/apprise/plugins/NotifyNexmo.py b/apprise/plugins/NotifyVonage.py similarity index 84% rename from apprise/plugins/NotifyNexmo.py rename to apprise/plugins/NotifyVonage.py index 1c423aad..1dea3157 100644 --- a/apprise/plugins/NotifyNexmo.py +++ b/apprise/plugins/NotifyVonage.py @@ -39,24 +39,24 @@ from ..utils import validate_regex from ..AppriseLocale import gettext_lazy as _ -class NotifyNexmo(NotifyBase): +class NotifyVonage(NotifyBase): """ - A wrapper for Nexmo Notifications + A wrapper for Vonage Notifications """ # The default descriptive name associated with the Notification - service_name = 'Nexmo' + service_name = 'Vonage' # The services URL service_url = 'https://dashboard.nexmo.com/' - # The default protocol - secure_protocol = 'nexmo' + # The default protocol (nexmo kept for backwards compatibility) + secure_protocol = ('vonage', 'nexmo') # A URL that takes you to the setup/help of the specific protocol setup_url = 'https://github.com/caronc/apprise/wiki/Notify_nexmo' - # Nexmo uses the http protocol with JSON requests + # Vonage uses the http protocol with JSON requests notify_url = 'https://rest.nexmo.com/sms/json' # The maximum length of the body @@ -124,7 +124,7 @@ class NotifyNexmo(NotifyBase): }, # Default Time To Live - # By default Nexmo attempt delivery for 72 hours, however the maximum + # By default Vonage attempt delivery for 72 hours, however the maximum # effective value depends on the operator and is typically 24 - 48 # hours. We recommend this value should be kept at its default or at # least 30 minutes. @@ -140,15 +140,15 @@ class NotifyNexmo(NotifyBase): def __init__(self, apikey, secret, source, targets=None, ttl=None, **kwargs): """ - Initialize Nexmo Object + Initialize Vonage Object """ - super(NotifyNexmo, self).__init__(**kwargs) + super(NotifyVonage, self).__init__(**kwargs) # API Key (associated with project) self.apikey = validate_regex( apikey, *self.template_tokens['apikey']['regex']) if not self.apikey: - msg = 'An invalid Nexmo API Key ' \ + msg = 'An invalid Vonage API Key ' \ '({}) was specified.'.format(apikey) self.logger.warning(msg) raise TypeError(msg) @@ -157,7 +157,7 @@ class NotifyNexmo(NotifyBase): self.secret = validate_regex( secret, *self.template_tokens['secret']['regex']) if not self.secret: - msg = 'An invalid Nexmo API Secret ' \ + msg = 'An invalid Vonage API Secret ' \ '({}) was specified.'.format(secret) self.logger.warning(msg) raise TypeError(msg) @@ -173,7 +173,7 @@ class NotifyNexmo(NotifyBase): if self.ttl < self.template_args['ttl']['min'] or \ self.ttl > self.template_args['ttl']['max']: - msg = 'The Nexmo TTL specified ({}) is out of range.'\ + msg = 'The Vonage TTL specified ({}) is out of range.'\ .format(self.ttl) self.logger.warning(msg) raise TypeError(msg) @@ -211,7 +211,7 @@ class NotifyNexmo(NotifyBase): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): """ - Perform Nexmo Notification + Perform Vonage Notification """ # error tracking (used for function return) @@ -250,9 +250,9 @@ class NotifyNexmo(NotifyBase): payload['to'] = target # Some Debug Logging - self.logger.debug('Nexmo POST URL: {} (cert_verify={})'.format( + self.logger.debug('Vonage POST URL: {} (cert_verify={})'.format( self.notify_url, self.verify_certificate)) - self.logger.debug('Nexmo Payload: {}' .format(payload)) + self.logger.debug('Vonage Payload: {}' .format(payload)) # Always call throttle before any remote server i/o is made self.throttle() @@ -269,11 +269,11 @@ class NotifyNexmo(NotifyBase): if r.status_code != requests.codes.ok: # We had a problem status_str = \ - NotifyNexmo.http_response_code_lookup( + NotifyVonage.http_response_code_lookup( r.status_code) self.logger.warning( - 'Failed to send Nexmo notification to {}: ' + 'Failed to send Vonage notification to {}: ' '{}{}error={}.'.format( target, status_str, @@ -288,11 +288,12 @@ class NotifyNexmo(NotifyBase): continue else: - self.logger.info('Sent Nexmo notification to %s.' % target) + self.logger.info( + 'Sent Vonage notification to %s.' % target) except requests.RequestException as e: self.logger.warning( - 'A Connection error occurred sending Nexmo:%s ' + 'A Connection error occurred sending Vonage:%s ' 'notification.' % target ) self.logger.debug('Socket Exception: %s' % str(e)) @@ -317,14 +318,14 @@ class NotifyNexmo(NotifyBase): params.update(self.url_parameters(privacy=privacy, *args, **kwargs)) return '{schema}://{key}:{secret}@{source}/{targets}/?{params}'.format( - schema=self.secure_protocol, + schema=self.secure_protocol[0], key=self.pprint(self.apikey, privacy, safe=''), secret=self.pprint( self.secret, privacy, mode=PrivacyMode.Secret, safe=''), - source=NotifyNexmo.quote(self.source, safe=''), + source=NotifyVonage.quote(self.source, safe=''), targets='/'.join( - [NotifyNexmo.quote(x, safe='') for x in self.targets]), - params=NotifyNexmo.urlencode(params)) + [NotifyVonage.quote(x, safe='') for x in self.targets]), + params=NotifyVonage.urlencode(params)) @staticmethod def parse_url(url): @@ -340,46 +341,46 @@ class NotifyNexmo(NotifyBase): # Get our entries; split_path() looks after unquoting content for us # by default - results['targets'] = NotifyNexmo.split_path(results['fullpath']) + results['targets'] = NotifyVonage.split_path(results['fullpath']) # The hostname is our source number - results['source'] = NotifyNexmo.unquote(results['host']) + results['source'] = NotifyVonage.unquote(results['host']) # Get our account_side and auth_token from the user/pass config - results['apikey'] = NotifyNexmo.unquote(results['user']) - results['secret'] = NotifyNexmo.unquote(results['password']) + results['apikey'] = NotifyVonage.unquote(results['user']) + results['secret'] = NotifyVonage.unquote(results['password']) # API Key if 'key' in results['qsd'] and len(results['qsd']['key']): # Extract the API Key from an argument results['apikey'] = \ - NotifyNexmo.unquote(results['qsd']['key']) + NotifyVonage.unquote(results['qsd']['key']) # API Secret if 'secret' in results['qsd'] and len(results['qsd']['secret']): # Extract the API Secret from an argument results['secret'] = \ - NotifyNexmo.unquote(results['qsd']['secret']) + NotifyVonage.unquote(results['qsd']['secret']) # Support the 'from' and 'source' variable so that we can support # targets this way too. # The 'from' makes it easier to use yaml configuration if 'from' in results['qsd'] and len(results['qsd']['from']): results['source'] = \ - NotifyNexmo.unquote(results['qsd']['from']) + NotifyVonage.unquote(results['qsd']['from']) if 'source' in results['qsd'] and len(results['qsd']['source']): results['source'] = \ - NotifyNexmo.unquote(results['qsd']['source']) + NotifyVonage.unquote(results['qsd']['source']) # Support the 'ttl' variable if 'ttl' in results['qsd'] and len(results['qsd']['ttl']): results['ttl'] = \ - NotifyNexmo.unquote(results['qsd']['ttl']) + NotifyVonage.unquote(results['qsd']['ttl']) # Support the 'to' variable so that we can support rooms this way too # The 'to' makes it easier to use yaml configuration if 'to' in results['qsd'] and len(results['qsd']['to']): results['targets'] += \ - NotifyNexmo.parse_phone_no(results['qsd']['to']) + NotifyVonage.parse_phone_no(results['qsd']['to']) return results diff --git a/packaging/redhat/python-apprise.spec b/packaging/redhat/python-apprise.spec index 3f265421..48841a89 100644 --- a/packaging/redhat/python-apprise.spec +++ b/packaging/redhat/python-apprise.spec @@ -56,7 +56,8 @@ Notifico, ntfy, Office365, OneSignal, Opsgenie, PagerDuty, ParsePlatform, PopcornNotify, Prowl, Pushalot, PushBullet, Pushjet, Pushover, PushSafer, Reddit, Rocket.Chat, SendGrid, ServerChan, Signal, SimplePush, Sinch, Slack, SMTP2Go, Spontit, SparkPost, Super Toasty, Streamlabs, Stride, Syslog, -Techulus Push, Telegram, Twilio, Twitter, Twist, XBMC, XMPP, Webex Teams} +Techulus Push, Telegram, Twilio, Twitter, Twist, XBMC, XMPP, Vonage, Webex +Teams} Name: python-%{pypi_name} Version: 0.9.9 diff --git a/test/test_plugin_nexmo.py b/test/test_plugin_vonage.py similarity index 59% rename from test/test_plugin_nexmo.py rename to test/test_plugin_vonage.py index 77bbd4b3..a8f79a8c 100644 --- a/test/test_plugin_nexmo.py +++ b/test/test_plugin_vonage.py @@ -35,6 +35,72 @@ logging.disable(logging.CRITICAL) # Our Testing URLs apprise_url_tests = ( + ('vonage://', { + # No API Key specified + 'instance': TypeError, + }), + ('vonage://:@/', { + # invalid Auth key + 'instance': TypeError, + }), + ('vonage://AC{}@12345678'.format('a' * 8), { + # Just a key provided + 'instance': TypeError, + }), + ('vonage://AC{}:{}@{}'.format('a' * 8, 'b' * 16, '3' * 9), { + # key and secret provided and from but invalid from no + 'instance': TypeError, + }), + ('vonage://AC{}:{}@{}/?ttl=0'.format('b' * 8, 'c' * 16, '3' * 11), { + # Invalid ttl defined + 'instance': TypeError, + }), + ('vonage://AC{}:{}@{}'.format('d' * 8, 'e' * 16, 'a' * 11), { + # Invalid source number + 'instance': TypeError, + }), + ('vonage://AC{}:{}@{}/123/{}/abcd/'.format( + 'f' * 8, 'g' * 16, '3' * 11, '9' * 15), { + # valid everything but target numbers + 'instance': plugins.NotifyVonage, + + # Our expected url(privacy=True) startswith() response: + 'privacy_url': 'vonage://A...f:****@', + }), + ('vonage://AC{}:{}@{}'.format('h' * 8, 'i' * 16, '5' * 11), { + # using phone no with no target - we text ourselves in + # this case + 'instance': plugins.NotifyVonage, + }), + ('vonage://_?key=AC{}&secret={}&from={}'.format( + 'a' * 8, 'b' * 16, '5' * 11), { + # use get args to acomplish the same thing + 'instance': plugins.NotifyVonage, + }), + ('vonage://_?key=AC{}&secret={}&source={}'.format( + 'a' * 8, 'b' * 16, '5' * 11), { + # use get args to acomplish the same thing (use source instead of from) + 'instance': plugins.NotifyVonage, + }), + ('vonage://_?key=AC{}&secret={}&from={}&to={}'.format( + 'a' * 8, 'b' * 16, '5' * 11, '7' * 13), { + # use to= + 'instance': plugins.NotifyVonage, + }), + ('vonage://AC{}:{}@{}'.format('a' * 8, 'b' * 16, '6' * 11), { + 'instance': plugins.NotifyVonage, + # throw a bizzare code forcing us to fail to look it up + 'response': False, + 'requests_response_code': 999, + }), + ('vonage://AC{}:{}@{}'.format('a' * 8, 'b' * 16, '6' * 11), { + 'instance': plugins.NotifyVonage, + # Throws a series of connection and transfer exceptions when this flag + # is set and tests that we gracfully handle them + 'test_requests_exceptions': True, + }), + + # Nexmo Backwards Support ('nexmo://', { # No API Key specified 'instance': TypeError, @@ -62,39 +128,39 @@ apprise_url_tests = ( ('nexmo://AC{}:{}@{}/123/{}/abcd/'.format( 'f' * 8, 'g' * 16, '3' * 11, '9' * 15), { # valid everything but target numbers - 'instance': plugins.NotifyNexmo, + 'instance': plugins.NotifyVonage, # Our expected url(privacy=True) startswith() response: - 'privacy_url': 'nexmo://A...f:****@', + 'privacy_url': 'vonage://A...f:****@', }), ('nexmo://AC{}:{}@{}'.format('h' * 8, 'i' * 16, '5' * 11), { # using phone no with no target - we text ourselves in # this case - 'instance': plugins.NotifyNexmo, + 'instance': plugins.NotifyVonage, }), ('nexmo://_?key=AC{}&secret={}&from={}'.format( 'a' * 8, 'b' * 16, '5' * 11), { # use get args to acomplish the same thing - 'instance': plugins.NotifyNexmo, + 'instance': plugins.NotifyVonage, }), ('nexmo://_?key=AC{}&secret={}&source={}'.format( 'a' * 8, 'b' * 16, '5' * 11), { # use get args to acomplish the same thing (use source instead of from) - 'instance': plugins.NotifyNexmo, + 'instance': plugins.NotifyVonage, }), ('nexmo://_?key=AC{}&secret={}&from={}&to={}'.format( 'a' * 8, 'b' * 16, '5' * 11, '7' * 13), { # use to= - 'instance': plugins.NotifyNexmo, + 'instance': plugins.NotifyVonage, }), ('nexmo://AC{}:{}@{}'.format('a' * 8, 'b' * 16, '6' * 11), { - 'instance': plugins.NotifyNexmo, + 'instance': plugins.NotifyVonage, # throw a bizzare code forcing us to fail to look it up 'response': False, 'requests_response_code': 999, }), ('nexmo://AC{}:{}@{}'.format('a' * 8, 'b' * 16, '6' * 11), { - 'instance': plugins.NotifyNexmo, + 'instance': plugins.NotifyVonage, # Throws a series of connection and transfer exceptions when this flag # is set and tests that we gracfully handle them 'test_requests_exceptions': True, @@ -102,9 +168,9 @@ apprise_url_tests = ( ) -def test_plugin_nexmo_urls(): +def test_plugin_vonage_urls(): """ - NotifyNexmo() Apprise URLs + NotifyVonage() Apprise URLs """ @@ -113,9 +179,9 @@ def test_plugin_nexmo_urls(): @mock.patch('requests.post') -def test_plugin_nexmo_edge_cases(mock_post): +def test_plugin_vonage_edge_cases(mock_post): """ - NotifyNexmo() Edge Cases + NotifyVonage() Edge Cases """ # Disable Throttling to speed testing @@ -135,17 +201,17 @@ def test_plugin_nexmo_edge_cases(mock_post): # No apikey specified with pytest.raises(TypeError): - plugins.NotifyNexmo(apikey=None, secret=secret, source=source) + plugins.NotifyVonage(apikey=None, secret=secret, source=source) with pytest.raises(TypeError): - plugins.NotifyNexmo(apikey=" ", secret=secret, source=source) + plugins.NotifyVonage(apikey=" ", secret=secret, source=source) # No secret specified with pytest.raises(TypeError): - plugins.NotifyNexmo(apikey=apikey, secret=None, source=source) + plugins.NotifyVonage(apikey=apikey, secret=None, source=source) with pytest.raises(TypeError): - plugins.NotifyNexmo(apikey=apikey, secret=" ", source=source) + plugins.NotifyVonage(apikey=apikey, secret=" ", source=source) # a error response response.status_code = 400 @@ -156,7 +222,7 @@ def test_plugin_nexmo_edge_cases(mock_post): mock_post.return_value = response # Initialize our object - obj = plugins.NotifyNexmo( + obj = plugins.NotifyVonage( apikey=apikey, secret=secret, source=source) # We will fail with the above error code