Notification Service Native URL Support; refs #109

This commit is contained in:
Chris Caron 2019-05-31 20:56:54 -04:00 committed by GitHub
parent d5dfbf74fa
commit 6d9069e106
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 243 additions and 14 deletions

View File

@ -124,12 +124,20 @@ class Apprise(object):
# Some basic validation # Some basic validation
if schema not in plugins.SCHEMA_MAP: if schema not in plugins.SCHEMA_MAP:
logger.error('Unsupported schema {}.'.format(schema)) # Give the user the benefit of the doubt that the user may be
return None # 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 else:
# containing all of the information parsed from our URL # Parse our url details of the server object as dictionary
results = plugins.SCHEMA_MAP[schema].parse_url(_url) # containing all of the information parsed from our URL
results = plugins.SCHEMA_MAP[schema].parse_url(_url)
if results is None: if results is None:
# Failed to parse the server URL # Failed to parse the server URL

View File

@ -402,3 +402,21 @@ class NotifyBase(URLBase):
del results['overflow'] del results['overflow']
return results 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

View File

@ -395,6 +395,29 @@ class NotifyDiscord(NotifyBase):
return results 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<webhook_id>[0-9]+)/'
r'(?P<webhook_token>[A-Z0-9_-]+)/?'
r'(?P<args>\?[.+])?$', 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 @staticmethod
def extract_markdown_sections(markdown): def extract_markdown_sections(markdown):
""" """

View File

@ -358,3 +358,24 @@ class NotifyFlock(NotifyBase):
parse_bool(results['qsd'].get('image', True)) parse_bool(results['qsd'].get('image', True))
return results 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<token>[a-z0-9-]{24})/?'
r'(?P<args>\?[.+])?$', 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

View File

@ -39,6 +39,7 @@
# #
# For each event you create you will assign it a name (this will be known as # For each event you create you will assign it a name (this will be known as
# the {event} when building your URL. # the {event} when building your URL.
import re
import requests import requests
from json import dumps from json import dumps
@ -344,3 +345,27 @@ class NotifyIFTTT(NotifyBase):
NotifyIFTTT.parse_list(results['qsd']['to']) NotifyIFTTT.parse_list(results['qsd']['to'])
return results 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<webhook_id>[A-Z0-9_-]+)'
r'/?(?P<events>([A-Z0-9_-]+/?)+)?'
r'/?(?P<args>\?[.+])?$', 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

View File

@ -74,7 +74,6 @@ from ..AppriseLocale import gettext_lazy as _
# Used to prepare our UUID regex matching # Used to prepare our UUID regex matching
UUID4_RE = \ 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}-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 # Token required as part of the API request
# /AAAAAAAAA@AAAAAAAAA/........./......... # /AAAAAAAAA@AAAAAAAAA/........./.........
@ -364,3 +363,33 @@ class NotifyMSTeams(NotifyBase):
parse_bool(results['qsd'].get('image', True)) parse_bool(results['qsd'].get('image', True))
return results 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<token_a>[A-Z0-9-]+@[A-Z0-9-]+)/'
r'IncomingWebhook/'
r'(?P<token_b>[A-Z0-9]+)/'
r'(?P<token_c>[A-Z0-9-]+)/?'
r'(?P<args>\?[.+])?$', 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

View File

@ -43,10 +43,10 @@ from ..utils import parse_bool
from ..AppriseLocale import gettext_lazy as _ from ..AppriseLocale import gettext_lazy as _
# Token required as part of the API request # 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 # 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): class RyverWebhookMode(object):
@ -353,3 +353,25 @@ class NotifyRyver(NotifyBase):
parse_bool(results['qsd'].get('image', True)) parse_bool(results['qsd'].get('image', True))
return results return results
@staticmethod
def parse_native_url(url):
"""
Support https://RYVER_ORG.ryver.com/application/webhook/TOKEN
"""
result = re.match(
r'^https?://(?P<org>[A-Z0-9_-]+)\.ryver\.com/application/webhook/'
r'(?P<webhook_token>[A-Z0-9]+)/?'
r'(?P<args>\?[.+])?$', 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

View File

@ -472,3 +472,28 @@ class NotifySlack(NotifyBase):
parse_bool(results['qsd'].get('image', True)) parse_bool(results['qsd'].get('image', True))
return results 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<token_a>[A-Z0-9]{9})/'
r'(?P<token_b>[A-Z0-9]{9})/'
r'(?P<token_c>[A-Z0-9]{24})/?'
r'(?P<args>\?[.+])?$', 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

View File

@ -245,3 +245,24 @@ class NotifyWebexTeams(NotifyBase):
results['token'] = NotifyWebexTeams.unquote(results['host']) results['token'] = NotifyWebexTeams.unquote(results['host'])
return results 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<webhook_token>[A-Z0-9_-]+)/?'
r'(?P<args>\?[.+])?$', 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

View File

@ -73,7 +73,7 @@ __all__ = [
# we mirror our base purely for the ability to reset everything; this # 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 # is generally only used in testing and should not be used by developers
__MODULE_MAP = {} MODULE_MAP = {}
# Load our Lookup Matrix # Load our Lookup Matrix
@ -117,12 +117,12 @@ def __load_matrix(path=abspath(dirname(__file__)), name='apprise.plugins'):
# Filter out non-notification modules # Filter out non-notification modules
continue continue
elif plugin_name in __MODULE_MAP: elif plugin_name in MODULE_MAP:
# we're already handling this object # we're already handling this object
continue continue
# Add our plugin name to our module map # Add our plugin name to our module map
__MODULE_MAP[plugin_name] = { MODULE_MAP[plugin_name] = {
'plugin': plugin, 'plugin': plugin,
'module': module, 'module': module,
} }
@ -171,7 +171,7 @@ def __reset_matrix():
SCHEMA_MAP.clear() SCHEMA_MAP.clear()
# Iterate over our module map so we can clear out our __all__ and globals # 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 # Clear out globals
del globals()[plugin_name] del globals()[plugin_name]
@ -179,7 +179,7 @@ def __reset_matrix():
__all__.remove(plugin_name) __all__.remove(plugin_name)
# Clear out our module map # Clear out our module map
__MODULE_MAP.clear() MODULE_MAP.clear()
# Dynamically build our schema base # Dynamically build our schema base

View File

@ -177,7 +177,12 @@ TEST_URLS = (
# don't include an image by default # don't include an image by default
'include_image': True, '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' % ( ('discord://%s/%s?format=markdown&avatar=No&footer=No' % (
'i' * 24, 't' * 64), { 'i' * 24, 't' * 64), {
'instance': plugins.NotifyDiscord, 'instance': plugins.NotifyDiscord,
@ -331,6 +336,10 @@ TEST_URLS = (
('flock://%s?format=text' % ('i' * 24), { ('flock://%s?format=text' % ('i' * 24), {
'instance': plugins.NotifyFlock, '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 # Bot API presumed if one or more targets are specified
# Provide markdown format # Provide markdown format
('flock://%s/u:%s?format=markdown' % ('i' * 24, 'u' * 12), { ('flock://%s/u:%s?format=markdown' % ('i' * 24, 'u' * 12), {
@ -534,6 +543,14 @@ TEST_URLS = (
('ifttt://WebHookID@EventID/EventID2/', { ('ifttt://WebHookID@EventID/EventID2/', {
'instance': plugins.NotifyIFTTT, '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 # Test website connection failures
('ifttt://WebHookID@EventID', { ('ifttt://WebHookID@EventID', {
'instance': plugins.NotifyIFTTT, 'instance': plugins.NotifyIFTTT,
@ -1052,6 +1069,12 @@ TEST_URLS = (
# All tokens provided - we're good # All tokens provided - we're good
'instance': plugins.NotifyMSTeams, '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), { ('msteams://{}@{}/{}/{}?t2'.format(UUID4, UUID4, 'a' * 32, UUID4), {
# All tokens provided - we're good # All tokens provided - we're good
'instance': plugins.NotifyMSTeams, 'instance': plugins.NotifyMSTeams,
@ -1647,6 +1670,10 @@ TEST_URLS = (
# the user told the webhook to use; set our ryver mode # the user told the webhook to use; set our ryver mode
'instance': plugins.NotifyRyver, 'instance': plugins.NotifyRyver,
}), }),
# Support Native URLs
('https://apprise.ryver.com/application/webhook/ckhrjW8w672m6HG', {
'instance': plugins.NotifyRyver,
}),
('ryver://caronc@apprise/ckhrjW8w672m6HG', { ('ryver://caronc@apprise/ckhrjW8w672m6HG', {
'instance': plugins.NotifyRyver, 'instance': plugins.NotifyRyver,
# don't include an image by default # don't include an image by default
@ -1719,6 +1746,11 @@ TEST_URLS = (
# Missing a channel, falls back to webhook channel bindings # Missing a channel, falls back to webhook channel bindings
'instance': plugins.NotifySlack, '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', { ('slack://username@INVALID/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/#cool', {
# invalid 1st Token # invalid 1st Token
'instance': TypeError, 'instance': TypeError,
@ -2041,6 +2073,11 @@ TEST_URLS = (
# token provided - we're good # token provided - we're good
'instance': plugins.NotifyWebexTeams, '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), { ('wxteams://{}'.format('a' * 80), {
'instance': plugins.NotifyWebexTeams, 'instance': plugins.NotifyWebexTeams,
# force a failure # force a failure