From 6d9069e106038f4e309c02e3a2358add366b9e22 Mon Sep 17 00:00:00 2001 From: Chris Caron Date: Fri, 31 May 2019 20:56:54 -0400 Subject: [PATCH] Notification Service Native URL Support; refs #109 --- apprise/Apprise.py | 18 +++++++++---- apprise/plugins/NotifyBase.py | 18 +++++++++++++ apprise/plugins/NotifyDiscord.py | 23 +++++++++++++++++ apprise/plugins/NotifyFlock.py | 21 ++++++++++++++++ apprise/plugins/NotifyIFTTT.py | 25 ++++++++++++++++++ apprise/plugins/NotifyMSTeams.py | 31 ++++++++++++++++++++++- apprise/plugins/NotifyRyver.py | 26 +++++++++++++++++-- apprise/plugins/NotifySlack.py | 25 ++++++++++++++++++ apprise/plugins/NotifyWebexTeams.py | 21 ++++++++++++++++ apprise/plugins/__init__.py | 10 ++++---- test/test_rest_plugins.py | 39 ++++++++++++++++++++++++++++- 11 files changed, 243 insertions(+), 14 deletions(-) diff --git a/apprise/Apprise.py b/apprise/Apprise.py index 4731b521..ac20f014 100644 --- a/apprise/Apprise.py +++ b/apprise/Apprise.py @@ -124,12 +124,20 @@ class Apprise(object): # Some basic validation if schema not in plugins.SCHEMA_MAP: - logger.error('Unsupported schema {}.'.format(schema)) - return None + # Give the user the benefit of the doubt that the user may be + # using one of the URLs provided to them by their notification + # service. Before we fail for good, just scan all the plugins + # that support he native_url() parse function + results = \ + next((r['plugin'].parse_native_url(_url) + for r in plugins.MODULE_MAP.values() + if r['plugin'].parse_native_url(_url) is not None), + None) - # Parse our url details of the server object as dictionary - # containing all of the information parsed from our URL - results = plugins.SCHEMA_MAP[schema].parse_url(_url) + else: + # Parse our url details of the server object as dictionary + # containing all of the information parsed from our URL + results = plugins.SCHEMA_MAP[schema].parse_url(_url) if results is None: # Failed to parse the server URL diff --git a/apprise/plugins/NotifyBase.py b/apprise/plugins/NotifyBase.py index da3de4b0..c0183c9f 100644 --- a/apprise/plugins/NotifyBase.py +++ b/apprise/plugins/NotifyBase.py @@ -402,3 +402,21 @@ class NotifyBase(URLBase): del results['overflow'] return results + + @staticmethod + def parse_native_url(url): + """ + This is a base class that can be optionally over-ridden by child + classes who can build their Apprise URL based on the one provided + by the notification service they choose to use. + + The intent of this is to make Apprise a little more userfriendly + to people who aren't familiar with constructing URLs and wish to + use the ones that were just provied by their notification serivice + that they're using. + + This function will return None if the passed in URL can't be matched + as belonging to the notification service. Otherwise this function + should return the same set of results that parse_url() does. + """ + return None diff --git a/apprise/plugins/NotifyDiscord.py b/apprise/plugins/NotifyDiscord.py index 30d6bbeb..6db65c8d 100644 --- a/apprise/plugins/NotifyDiscord.py +++ b/apprise/plugins/NotifyDiscord.py @@ -395,6 +395,29 @@ class NotifyDiscord(NotifyBase): return results + @staticmethod + def parse_native_url(url): + """ + Support https://discordapp.com/api/webhooks/WEBHOOK_ID/WEBHOOK_TOKEN + """ + + result = re.match( + r'^https?://discordapp\.com/api/webhooks/' + r'(?P[0-9]+)/' + r'(?P[A-Z0-9_-]+)/?' + r'(?P\?[.+])?$', url, re.I) + + if result: + return NotifyDiscord.parse_url( + '{schema}://{webhook_id}/{webhook_token}/{args}'.format( + schema=NotifyDiscord.secure_protocol, + webhook_id=result.group('webhook_id'), + webhook_token=result.group('webhook_token'), + args='' if not result.group('args') + else result.group('args'))) + + return None + @staticmethod def extract_markdown_sections(markdown): """ diff --git a/apprise/plugins/NotifyFlock.py b/apprise/plugins/NotifyFlock.py index 4259d010..96ea52b4 100644 --- a/apprise/plugins/NotifyFlock.py +++ b/apprise/plugins/NotifyFlock.py @@ -358,3 +358,24 @@ class NotifyFlock(NotifyBase): parse_bool(results['qsd'].get('image', True)) return results + + @staticmethod + def parse_native_url(url): + """ + Support https://api.flock.com/hooks/sendMessage/TOKEN + """ + + result = re.match( + r'^https?://api\.flock\.com/hooks/sendMessage/' + r'(?P[a-z0-9-]{24})/?' + r'(?P\?[.+])?$', url, re.I) + + if result: + return NotifyFlock.parse_url( + '{schema}://{token}/{args}'.format( + schema=NotifyFlock.secure_protocol, + token=result.group('token'), + args='' if not result.group('args') + else result.group('args'))) + + return None diff --git a/apprise/plugins/NotifyIFTTT.py b/apprise/plugins/NotifyIFTTT.py index b7bded1c..85c913ad 100644 --- a/apprise/plugins/NotifyIFTTT.py +++ b/apprise/plugins/NotifyIFTTT.py @@ -39,6 +39,7 @@ # # For each event you create you will assign it a name (this will be known as # the {event} when building your URL. +import re import requests from json import dumps @@ -344,3 +345,27 @@ class NotifyIFTTT(NotifyBase): NotifyIFTTT.parse_list(results['qsd']['to']) return results + + @staticmethod + def parse_native_url(url): + """ + Support https://maker.ifttt.com/use/WEBHOOK_ID/EVENT_ID + """ + + result = re.match( + r'^https?://maker\.ifttt\.com/use/' + r'(?P[A-Z0-9_-]+)' + r'/?(?P([A-Z0-9_-]+/?)+)?' + r'/?(?P\?[.+])?$', url, re.I) + + if result: + return NotifyIFTTT.parse_url( + '{schema}://{webhook_id}{events}{args}'.format( + schema=NotifyIFTTT.secure_protocol, + webhook_id=result.group('webhook_id'), + events='' if not result.group('events') + else '@{}'.format(result.group('events')), + args='' if not result.group('args') + else result.group('args'))) + + return None diff --git a/apprise/plugins/NotifyMSTeams.py b/apprise/plugins/NotifyMSTeams.py index b47e9f53..b75be7de 100644 --- a/apprise/plugins/NotifyMSTeams.py +++ b/apprise/plugins/NotifyMSTeams.py @@ -74,7 +74,6 @@ from ..AppriseLocale import gettext_lazy as _ # Used to prepare our UUID regex matching UUID4_RE = \ r'[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}' -# r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' # Token required as part of the API request # /AAAAAAAAA@AAAAAAAAA/........./......... @@ -364,3 +363,33 @@ class NotifyMSTeams(NotifyBase): parse_bool(results['qsd'].get('image', True)) return results + + @staticmethod + def parse_native_url(url): + """ + Support: + https://outlook.office.com/webhook/ABCD/IncomingWebhook/DEFG/HIJK + """ + + # We don't need to do incredibly details token matching as the purpose + # of this is just to detect that were dealing with an msteams url + # token parsing will occur once we initialize the function + result = re.match( + r'^https?://outlook\.office\.com/webhook/' + r'(?P[A-Z0-9-]+@[A-Z0-9-]+)/' + r'IncomingWebhook/' + r'(?P[A-Z0-9]+)/' + r'(?P[A-Z0-9-]+)/?' + r'(?P\?[.+])?$', url, re.I) + + if result: + return NotifyMSTeams.parse_url( + '{schema}://{token_a}/{token_b}/{token_c}/{args}'.format( + schema=NotifyMSTeams.secure_protocol, + token_a=result.group('token_a'), + token_b=result.group('token_b'), + token_c=result.group('token_c'), + args='' if not result.group('args') + else result.group('args'))) + + return None diff --git a/apprise/plugins/NotifyRyver.py b/apprise/plugins/NotifyRyver.py index 9754b69c..ebc67de1 100644 --- a/apprise/plugins/NotifyRyver.py +++ b/apprise/plugins/NotifyRyver.py @@ -43,10 +43,10 @@ from ..utils import parse_bool from ..AppriseLocale import gettext_lazy as _ # Token required as part of the API request -VALIDATE_TOKEN = re.compile(r'[A-Za-z0-9]{15}') +VALIDATE_TOKEN = re.compile(r'[A-Z0-9]{15}', re.I) # Organization required as part of the API request -VALIDATE_ORG = re.compile(r'[A-Za-z0-9-]{3,32}') +VALIDATE_ORG = re.compile(r'[A-Z0-9_-]{3,32}', re.I) class RyverWebhookMode(object): @@ -353,3 +353,25 @@ class NotifyRyver(NotifyBase): parse_bool(results['qsd'].get('image', True)) return results + + @staticmethod + def parse_native_url(url): + """ + Support https://RYVER_ORG.ryver.com/application/webhook/TOKEN + """ + + result = re.match( + r'^https?://(?P[A-Z0-9_-]+)\.ryver\.com/application/webhook/' + r'(?P[A-Z0-9]+)/?' + r'(?P\?[.+])?$', url, re.I) + + if result: + return NotifyRyver.parse_url( + '{schema}://{org}/{webhook_token}/{args}'.format( + schema=NotifyRyver.secure_protocol, + org=result.group('org'), + webhook_token=result.group('webhook_token'), + args='' if not result.group('args') + else result.group('args'))) + + return None diff --git a/apprise/plugins/NotifySlack.py b/apprise/plugins/NotifySlack.py index 13d33908..690952ef 100644 --- a/apprise/plugins/NotifySlack.py +++ b/apprise/plugins/NotifySlack.py @@ -472,3 +472,28 @@ class NotifySlack(NotifyBase): parse_bool(results['qsd'].get('image', True)) return results + + @staticmethod + def parse_native_url(url): + """ + Support https://hooks.slack.com/services/TOKEN_A/TOKEN_B/TOKEN_C + """ + + result = re.match( + r'^https?://hooks\.slack\.com/services/' + r'(?P[A-Z0-9]{9})/' + r'(?P[A-Z0-9]{9})/' + r'(?P[A-Z0-9]{24})/?' + r'(?P\?[.+])?$', url, re.I) + + if result: + return NotifySlack.parse_url( + '{schema}://{token_a}/{token_b}/{token_c}/{args}'.format( + schema=NotifySlack.secure_protocol, + token_a=result.group('token_a'), + token_b=result.group('token_b'), + token_c=result.group('token_c'), + args='' if not result.group('args') + else result.group('args'))) + + return None diff --git a/apprise/plugins/NotifyWebexTeams.py b/apprise/plugins/NotifyWebexTeams.py index b76df127..687ff0b0 100644 --- a/apprise/plugins/NotifyWebexTeams.py +++ b/apprise/plugins/NotifyWebexTeams.py @@ -245,3 +245,24 @@ class NotifyWebexTeams(NotifyBase): results['token'] = NotifyWebexTeams.unquote(results['host']) return results + + @staticmethod + def parse_native_url(url): + """ + Support https://api.ciscospark.com/v1/webhooks/incoming/WEBHOOK_TOKEN + """ + + result = re.match( + r'^https?://api\.ciscospark\.com/v[1-9][0-9]*/webhooks/incoming/' + r'(?P[A-Z0-9_-]+)/?' + r'(?P\?[.+])?$', url, re.I) + + if result: + return NotifyWebexTeams.parse_url( + '{schema}://{webhook_token}/{args}'.format( + schema=NotifyWebexTeams.secure_protocol, + webhook_token=result.group('webhook_token'), + args='' if not result.group('args') + else result.group('args'))) + + return None diff --git a/apprise/plugins/__init__.py b/apprise/plugins/__init__.py index f9ceb04d..d441494c 100644 --- a/apprise/plugins/__init__.py +++ b/apprise/plugins/__init__.py @@ -73,7 +73,7 @@ __all__ = [ # we mirror our base purely for the ability to reset everything; this # is generally only used in testing and should not be used by developers -__MODULE_MAP = {} +MODULE_MAP = {} # Load our Lookup Matrix @@ -117,12 +117,12 @@ def __load_matrix(path=abspath(dirname(__file__)), name='apprise.plugins'): # Filter out non-notification modules continue - elif plugin_name in __MODULE_MAP: + elif plugin_name in MODULE_MAP: # we're already handling this object continue # Add our plugin name to our module map - __MODULE_MAP[plugin_name] = { + MODULE_MAP[plugin_name] = { 'plugin': plugin, 'module': module, } @@ -171,7 +171,7 @@ def __reset_matrix(): SCHEMA_MAP.clear() # Iterate over our module map so we can clear out our __all__ and globals - for plugin_name in __MODULE_MAP.keys(): + for plugin_name in MODULE_MAP.keys(): # Clear out globals del globals()[plugin_name] @@ -179,7 +179,7 @@ def __reset_matrix(): __all__.remove(plugin_name) # Clear out our module map - __MODULE_MAP.clear() + MODULE_MAP.clear() # Dynamically build our schema base diff --git a/test/test_rest_plugins.py b/test/test_rest_plugins.py index 42b7b0ed..b1c90282 100644 --- a/test/test_rest_plugins.py +++ b/test/test_rest_plugins.py @@ -177,7 +177,12 @@ TEST_URLS = ( # don't include an image by default 'include_image': True, }), - + ('https://discordapp.com/api/webhooks/{}/{}'.format( + '0' * 10, 'B' * 40), { + # Native URL Support, take the discord URL and still build from it + 'instance': plugins.NotifyDiscord, + 'requests_response_code': requests.codes.no_content, + }), ('discord://%s/%s?format=markdown&avatar=No&footer=No' % ( 'i' * 24, 't' * 64), { 'instance': plugins.NotifyDiscord, @@ -331,6 +336,10 @@ TEST_URLS = ( ('flock://%s?format=text' % ('i' * 24), { 'instance': plugins.NotifyFlock, }), + # Native URL Support, take the slack URL and still build from it + ('https://api.flock.com/hooks/sendMessage/{}/'.format('i' * 24), { + 'instance': plugins.NotifyFlock, + }), # Bot API presumed if one or more targets are specified # Provide markdown format ('flock://%s/u:%s?format=markdown' % ('i' * 24, 'u' * 12), { @@ -534,6 +543,14 @@ TEST_URLS = ( ('ifttt://WebHookID@EventID/EventID2/', { 'instance': plugins.NotifyIFTTT, }), + # Support native URL references + ('https://maker.ifttt.com/use/WebHookID/', { + # No EventID specified + 'instance': TypeError, + }), + ('https://maker.ifttt.com/use/WebHookID/EventID/', { + 'instance': plugins.NotifyIFTTT, + }), # Test website connection failures ('ifttt://WebHookID@EventID', { 'instance': plugins.NotifyIFTTT, @@ -1052,6 +1069,12 @@ TEST_URLS = ( # All tokens provided - we're good 'instance': plugins.NotifyMSTeams, }), + # Support native URLs + ('https://outlook.office.com/webhook/{}@{}/IncomingWebhook/{}/{}' + .format(UUID4, UUID4, 'a' * 32, UUID4), { + # All tokens provided - we're good + 'instance': plugins.NotifyMSTeams}), + ('msteams://{}@{}/{}/{}?t2'.format(UUID4, UUID4, 'a' * 32, UUID4), { # All tokens provided - we're good 'instance': plugins.NotifyMSTeams, @@ -1647,6 +1670,10 @@ TEST_URLS = ( # the user told the webhook to use; set our ryver mode 'instance': plugins.NotifyRyver, }), + # Support Native URLs + ('https://apprise.ryver.com/application/webhook/ckhrjW8w672m6HG', { + 'instance': plugins.NotifyRyver, + }), ('ryver://caronc@apprise/ckhrjW8w672m6HG', { 'instance': plugins.NotifyRyver, # don't include an image by default @@ -1719,6 +1746,11 @@ TEST_URLS = ( # Missing a channel, falls back to webhook channel bindings 'instance': plugins.NotifySlack, }), + # Native URL Support, take the slack URL and still build from it + ('https://hooks.slack.com/services/{}/{}/{}'.format( + 'A' * 9, 'B' * 9, 'c' * 24), { + 'instance': plugins.NotifySlack, + }), ('slack://username@INVALID/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/#cool', { # invalid 1st Token 'instance': TypeError, @@ -2041,6 +2073,11 @@ TEST_URLS = ( # token provided - we're good 'instance': plugins.NotifyWebexTeams, }), + # Support Native URLs + ('https://api.ciscospark.com/v1/webhooks/incoming/{}'.format('a' * 80), { + # token provided - we're good + 'instance': plugins.NotifyWebexTeams, + }), ('wxteams://{}'.format('a' * 80), { 'instance': plugins.NotifyWebexTeams, # force a failure