Fixed MSTeams webhook handling for new format (#380)

This commit is contained in:
Chris Caron 2021-05-02 10:11:51 -04:00 committed by GitHub
parent 0b60e99aa4
commit 975b1721af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 145 additions and 43 deletions

View File

@ -43,16 +43,16 @@
#
# When you've completed this, it will generate you a (webhook) URL that
# looks like:
# https://team-name.office.com/webhook/ \
# https://team-name.webhook.office.com/webhookb2/ \
# abcdefgf8-2f4b-4eca-8f61-225c83db1967@abcdefg2-5a99-4849-8efc-\
# c9e78d28e57d/IncomingWebhook/291289f63a8abd3593e834af4d79f9fe/\
# a2329f43-0ffb-46ab-948b-c9abdad9d643
#
# Yes... The URL is that big... But it looks like this (greatly simplified):
# https://TEAM-NAME.office.com/webhook/ABCD/IncomingWebhook/DEFG/HIJK
# https://TEAM-NAME.webhook.office.com/webhookb2/ABCD/IncomingWebhook/DEFG/HIJK
# ^ ^ ^ ^
# | | | |
# These are important <----------------^--------------------^----^
# These are important <--------------------------^--------------------^----^
#
# The Legacy format didn't have the team name identified and reads 'outlook'
@ -114,7 +114,11 @@ class NotifyMSTeams(NotifyBase):
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_msteams'
# MSTeams uses the http protocol with JSON requests
notify_url = 'https://{team}.office.com/webhook/' \
notify_url_v1 = 'https://outlook.office.com/webhook/' \
'{token_a}/IncomingWebhook/{token_b}/{token_c}'
# New MSTeams webhook (as of April 11th, 2021)
notify_url_v2 = 'https://{team}.webhook.office.com/webhookb2/' \
'{token_a}/IncomingWebhook/{token_b}/{token_c}'
# Allows the user to specify the NotifyImageSize object
@ -185,6 +189,12 @@ class NotifyMSTeams(NotifyBase):
'default': False,
'map_to': 'include_image',
},
'version': {
'name': _('Version'),
'type': 'choice:int',
'values': (1, 2),
'default': 2,
},
'template': {
'name': _('Template Path'),
'type': 'string',
@ -200,7 +210,7 @@ class NotifyMSTeams(NotifyBase):
},
}
def __init__(self, token_a, token_b, token_c, team=None,
def __init__(self, token_a, token_b, token_c, team=None, version=None,
include_image=True, template=None, tokens=None, **kwargs):
"""
Initialize Microsoft Teams Object
@ -212,6 +222,24 @@ class NotifyMSTeams(NotifyBase):
"""
super(NotifyMSTeams, self).__init__(**kwargs)
try:
self.version = int(version)
except TypeError:
# None was specified... take on default
self.version = self.template_args['version']['default']
except ValueError:
# invalid content was provided; let this get caught in the next
# validation check for the version
self.version = None
if self.version not in self.template_args['version']['values']:
msg = 'An invalid MSTeams Version ' \
'({}) was specified.'.format(version)
self.logger.warning(msg)
raise TypeError(msg)
self.team = validate_regex(team)
if not self.team:
NotifyBase.logger.deprecate(
@ -373,12 +401,16 @@ class NotifyMSTeams(NotifyBase):
'Content-Type': 'application/json',
}
notify_url = self.notify_url.format(
notify_url = self.notify_url_v2.format(
team=self.team,
token_a=self.token_a,
token_b=self.token_b,
token_c=self.token_c,
)
) if self.version > 1 else \
self.notify_url_v1.format(
token_a=self.token_a,
token_b=self.token_b,
token_c=self.token_c)
# Generate our payload if it's possible
payload = self.gen_payload(
@ -444,6 +476,9 @@ class NotifyMSTeams(NotifyBase):
'image': 'yes' if self.include_image else 'no',
}
if self.version != self.template_args['version']['default']:
params['version'] = str(self.version)
if self.template:
params['template'] = NotifyMSTeams.quote(
self.template[0].url(), safe='')
@ -453,6 +488,7 @@ class NotifyMSTeams(NotifyBase):
# Store any template entries if specified
params.update({':{}'.format(k): v for k, v in self.tokens.items()})
if self.version > 1:
return '{schema}://{team}/{token_a}/{token_b}/{token_c}/'\
'?{params}'.format(
schema=self.secure_protocol,
@ -463,6 +499,16 @@ class NotifyMSTeams(NotifyBase):
params=NotifyMSTeams.urlencode(params),
)
else: # Version 1
return '{schema}://{token_a}/{token_b}/{token_c}/'\
'?{params}'.format(
schema=self.secure_protocol,
token_a=self.pprint(self.token_a, privacy, safe='@'),
token_b=self.pprint(self.token_b, privacy, safe=''),
token_c=self.pprint(self.token_c, privacy, safe=''),
params=NotifyMSTeams.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
@ -470,6 +516,7 @@ class NotifyMSTeams(NotifyBase):
us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
@ -511,6 +558,15 @@ class NotifyMSTeams(NotifyBase):
results['template'] = \
NotifyMSTeams.unquote(results['qsd']['template'])
# Override version if defined
if 'version' in results['qsd'] and results['qsd']['version']:
results['version'] = \
NotifyMSTeams.unquote(results['qsd']['version'])
else:
# Set our version if not otherwise set
results['version'] = 1 if not results.get('team') else 2
# Store our tokens
results['tokens'] = results['qsd:']
@ -530,7 +586,8 @@ class NotifyMSTeams(NotifyBase):
# 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?://(?P<team>[^.]+)\.office\.com/webhook/'
r'^https?://(?P<team>[^.]+)(?P<v2a>\.webhook)?\.office\.com/'
r'webhook(?P<v2b>b2)?/'
r'(?P<token_a>[A-Z0-9-]+@[A-Z0-9-]+)/'
r'IncomingWebhook/'
r'(?P<token_b>[A-Z0-9]+)/'
@ -538,6 +595,8 @@ class NotifyMSTeams(NotifyBase):
r'(?P<params>\?.+)?$', url, re.I)
if result:
if result.group('v2a'):
# Version 2 URL
return NotifyMSTeams.parse_url(
'{schema}://{team}/{token_a}/{token_b}/{token_c}'
'/{params}'.format(
@ -548,5 +607,16 @@ class NotifyMSTeams(NotifyBase):
token_c=result.group('token_c'),
params='' if not result.group('params')
else result.group('params')))
else:
# Version 1 URLs
# team is also set to 'outlook' in this case
return NotifyMSTeams.parse_url(
'{schema}://{token_a}/{token_b}/{token_c}'
'/{params}'.format(
schema=NotifyMSTeams.secure_protocol,
token_a=result.group('token_a'),
token_b=result.group('token_b'),
token_c=result.group('token_c'),
params='' if not result.group('params')
else result.group('params')))
return None

View File

@ -2015,62 +2015,94 @@ TEST_URLS = (
# Just 2 tokens provided
'instance': TypeError,
}),
('msteams://{}@{}/{}/{}?t1'.format(UUID4, UUID4, 'a' * 32, UUID4), {
('msteams://{}@{}/{}/{}?t1'.format(UUID4, UUID4, 'b' * 32, UUID4), {
# All tokens provided - we're good
'instance': plugins.NotifyMSTeams,
}),
# Support native URLs
('https://outlook.office.com/webhook/{}@{}/IncomingWebhook/{}/{}'
.format(UUID4, UUID4, 'a' * 32, UUID4), {
.format(UUID4, UUID4, 'k' * 32, UUID4), {
# All tokens provided - we're good
'instance': plugins.NotifyMSTeams}),
'instance': plugins.NotifyMSTeams,
# Our expected url(privacy=True) startswith() response (v1 format)
'privacy_url': 'msteams://8...2/k...k/8...2/'}),
# Support New Native URLs
('https://myteam.webhook.office.com/webhookb2/{}@{}/IncomingWebhook/{}/{}'
.format(UUID4, UUID4, 'm' * 32, UUID4), {
# All tokens provided - we're good
'instance': plugins.NotifyMSTeams,
# Our expected url(privacy=True) startswith() response (v2 format):
'privacy_url': 'msteams://myteam/8...2/m...m/8...2/'}),
# Legacy URL Formatting
('msteams://{}@{}/{}/{}?t2'.format(UUID4, UUID4, 'a' * 32, UUID4), {
('msteams://{}@{}/{}/{}?t2'.format(UUID4, UUID4, 'c' * 32, UUID4), {
# All tokens provided - we're good
'instance': plugins.NotifyMSTeams,
# don't include an image by default
'include_image': False,
}),
# Legacy URL Formatting
('msteams://{}@{}/{}/{}?image=No'.format(UUID4, UUID4, 'a' * 32, UUID4), {
('msteams://{}@{}/{}/{}?image=No'.format(UUID4, UUID4, 'd' * 32, UUID4), {
# All tokens provided - we're good no image
'instance': plugins.NotifyMSTeams,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'msteams://outlook/8...2/a...a/8...2/',
'privacy_url': 'msteams://8...2/d...d/8...2/',
}),
# New 2021 URL formatting
('msteams://apprise/{}@{}/{}/{}'.format(
UUID4, UUID4, 'a' * 32, UUID4), {
UUID4, UUID4, 'e' * 32, UUID4), {
# All tokens provided - we're good no image
'instance': plugins.NotifyMSTeams,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'msteams://apprise/8...2/a...a/8...2/',
'privacy_url': 'msteams://apprise/8...2/e...e/8...2/',
}),
# New 2021 URL formatting; support team= argument
('msteams://{}@{}/{}/{}?team=teamname'.format(
UUID4, UUID4, 'a' * 32, UUID4), {
UUID4, UUID4, 'f' * 32, UUID4), {
# All tokens provided - we're good no image
'instance': plugins.NotifyMSTeams,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'msteams://teamname/8...2/a...a/8...2/',
'privacy_url': 'msteams://teamname/8...2/f...f/8...2/',
}),
('msteams://{}@{}/{}/{}?tx'.format(UUID4, UUID4, 'a' * 32, UUID4), {
# New 2021 URL formatting (forcing v1)
('msteams://apprise/{}@{}/{}/{}?version=1'.format(
UUID4, UUID4, 'e' * 32, UUID4), {
# All tokens provided - we're good
'instance': plugins.NotifyMSTeams,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'msteams://8...2/e...e/8...2/',
}),
# Invalid versioning
('msteams://apprise/{}@{}/{}/{}?version=999'.format(
UUID4, UUID4, 'e' * 32, UUID4), {
# invalid version
'instance': TypeError,
}),
('msteams://apprise/{}@{}/{}/{}?version=invalid'.format(
UUID4, UUID4, 'e' * 32, UUID4), {
# invalid version
'instance': TypeError,
}),
('msteams://{}@{}/{}/{}?tx'.format(UUID4, UUID4, 'x' * 32, UUID4), {
'instance': plugins.NotifyMSTeams,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('msteams://{}@{}/{}/{}?ty'.format(UUID4, UUID4, 'a' * 32, UUID4), {
('msteams://{}@{}/{}/{}?ty'.format(UUID4, UUID4, 'y' * 32, UUID4), {
'instance': plugins.NotifyMSTeams,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('msteams://{}@{}/{}/{}?tz'.format(UUID4, UUID4, 'a' * 32, UUID4), {
('msteams://{}@{}/{}/{}?tz'.format(UUID4, UUID4, 'z' * 32, UUID4), {
'instance': plugins.NotifyMSTeams,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them