mirror of
https://github.com/caronc/apprise.git
synced 2025-01-05 21:49:02 +01:00
Notification Service Native URL Support; refs #109
This commit is contained in:
parent
d5dfbf74fa
commit
6d9069e106
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user