Microsoft Teams Webhook to include team name in URL (#361)

pull/240/head
Chris Caron 2021-02-22 14:35:28 -05:00 committed by GitHub
parent 221e304a73
commit 9e9b140642
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 95 additions and 36 deletions

View File

@ -43,22 +43,35 @@
# #
# When you've completed this, it will generate you a (webhook) URL that # When you've completed this, it will generate you a (webhook) URL that
# looks like: # looks like:
# https://outlook.office.com/webhook/ \ # https://team-name.office.com/webhook/ \
# abcdefgf8-2f4b-4eca-8f61-225c83db1967@abcdefg2-5a99-4849-8efc-\ # abcdefgf8-2f4b-4eca-8f61-225c83db1967@abcdefg2-5a99-4849-8efc-\
# c9e78d28e57d/IncomingWebhook/291289f63a8abd3593e834af4d79f9fe/\ # c9e78d28e57d/IncomingWebhook/291289f63a8abd3593e834af4d79f9fe/\
# a2329f43-0ffb-46ab-948b-c9abdad9d643 # a2329f43-0ffb-46ab-948b-c9abdad9d643
# #
# Yes... The URL is that big... But it looks like this (greatly simplified): # Yes... The URL is that big... But it looks like this (greatly simplified):
# https://TEAM-NAME.office.com/webhook/ABCD/IncomingWebhook/DEFG/HIJK
# ^ ^ ^ ^
# | | | |
# These are important <----------------^--------------------^----^
#
# The Legacy format didn't have the team name identified and reads 'outlook'
# While this still works, consider that Microsoft will be dropping support
# for this soon, so you may need to update your IncomingWebhook. Here is
# what a legacy URL looked like:
# https://outlook.office.com/webhook/ABCD/IncomingWebhook/DEFG/HIJK # https://outlook.office.com/webhook/ABCD/IncomingWebhook/DEFG/HIJK
# ^ ^ ^ # ^ ^ ^ ^
# | | | |
# legacy team reference: 'outlook' | | |
# | | | # | | |
# These are important <--------------^--------------------^----^ # These are important <--------------^--------------------^----^
# #
# You'll notice that the first token is actually 2 separated by an @ symbol # You'll notice that the first token is actually 2 separated by an @ symbol
# But lets just ignore that and assume it's one great big token instead. # But lets just ignore that and assume it's one great big token instead.
# #
# These 3 tokens is what you'll need to build your URL with: # These 3 tokens need to be placed in the URL after the Team
# msteams://ABCD/DEFG/HIJK # msteams://TEAM/ABCD/DEFG/HIJK
# #
import re import re
import requests import requests
@ -101,7 +114,8 @@ class NotifyMSTeams(NotifyBase):
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_msteams' setup_url = 'https://github.com/caronc/apprise/wiki/Notify_msteams'
# MSTeams uses the http protocol with JSON requests # MSTeams uses the http protocol with JSON requests
notify_url = 'https://outlook.office.com/webhook' notify_url = 'https://{team}.office.com/webhook/' \
'{token_a}/IncomingWebhook/{token_b}/{token_c}'
# Allows the user to specify the NotifyImageSize object # Allows the user to specify the NotifyImageSize object
image_size = NotifyImageSize.XY_72 image_size = NotifyImageSize.XY_72
@ -118,11 +132,22 @@ class NotifyMSTeams(NotifyBase):
# Define object templates # Define object templates
templates = ( templates = (
'{schema}://{token_a}/{token_b}{token_c}', # New required format
'{schema}://{team}/{token_a}/{token_b}/{token_c}',
# Deprecated
'{schema}://{token_a}/{token_b}/{token_c}',
) )
# Define our template tokens # Define our template tokens
template_tokens = dict(NotifyBase.template_tokens, **{ template_tokens = dict(NotifyBase.template_tokens, **{
# The Microsoft Team Name
'team': {
'name': _('Team Name'),
'type': 'string',
'required': True,
'regex': (r'^[A-Z0-9_-]+$', 'i'),
},
# Token required as part of the API request # Token required as part of the API request
# /AAAAAAAAA@AAAAAAAAA/........./......... # /AAAAAAAAA@AAAAAAAAA/........./.........
'token_a': { 'token_a': {
@ -175,8 +200,8 @@ class NotifyMSTeams(NotifyBase):
}, },
} }
def __init__(self, token_a, token_b, token_c, include_image=True, def __init__(self, token_a, token_b, token_c, team=None,
template=None, tokens=None, **kwargs): include_image=True, template=None, tokens=None, **kwargs):
""" """
Initialize Microsoft Teams Object Initialize Microsoft Teams Object
@ -187,6 +212,16 @@ class NotifyMSTeams(NotifyBase):
""" """
super(NotifyMSTeams, self).__init__(**kwargs) super(NotifyMSTeams, self).__init__(**kwargs)
self.team = validate_regex(team)
if not self.team:
NotifyBase.logger.deprecate(
"Apprise requires you to identify your Microsoft Team name as "
"part of the URL. e.g.: "
"msteams://TEAM-NAME/{token_a}/{token_b}/{token_c}")
# Fallback
self.team = 'outlook'
self.token_a = validate_regex( self.token_a = validate_regex(
token_a, *self.template_tokens['token_a']['regex']) token_a, *self.template_tokens['token_a']['regex'])
if not self.token_a: if not self.token_a:
@ -338,11 +373,11 @@ class NotifyMSTeams(NotifyBase):
'Content-Type': 'application/json', 'Content-Type': 'application/json',
} }
url = '%s/%s/IncomingWebhook/%s/%s' % ( notify_url = self.notify_url.format(
self.notify_url, team=self.team,
self.token_a, token_a=self.token_a,
self.token_b, token_b=self.token_b,
self.token_c, token_c=self.token_c,
) )
# Generate our payload if it's possible # Generate our payload if it's possible
@ -354,7 +389,7 @@ class NotifyMSTeams(NotifyBase):
return False return False
self.logger.debug('MSTeams POST URL: %s (cert_verify=%r)' % ( self.logger.debug('MSTeams POST URL: %s (cert_verify=%r)' % (
url, self.verify_certificate, notify_url, self.verify_certificate,
)) ))
self.logger.debug('MSTeams Payload: %s' % str(payload)) self.logger.debug('MSTeams Payload: %s' % str(payload))
@ -362,7 +397,7 @@ class NotifyMSTeams(NotifyBase):
self.throttle() self.throttle()
try: try:
r = requests.post( r = requests.post(
url, notify_url,
data=json.dumps(payload), data=json.dumps(payload),
headers=headers, headers=headers,
verify=self.verify_certificate, verify=self.verify_certificate,
@ -418,9 +453,10 @@ class NotifyMSTeams(NotifyBase):
# Store any template entries if specified # Store any template entries if specified
params.update({':{}'.format(k): v for k, v in self.tokens.items()}) params.update({':{}'.format(k): v for k, v in self.tokens.items()})
return '{schema}://{token_a}/{token_b}/{token_c}/'\ return '{schema}://{team}/{token_a}/{token_b}/{token_c}/'\
'?{params}'.format( '?{params}'.format(
schema=self.secure_protocol, schema=self.secure_protocol,
team=NotifyMSTeams.quote(self.team, safe=''),
token_a=self.pprint(self.token_a, privacy, safe=''), token_a=self.pprint(self.token_a, privacy, safe=''),
token_b=self.pprint(self.token_b, privacy, safe=''), token_b=self.pprint(self.token_b, privacy, safe=''),
token_c=self.pprint(self.token_c, privacy, safe=''), token_c=self.pprint(self.token_c, privacy, safe=''),
@ -442,6 +478,7 @@ class NotifyMSTeams(NotifyBase):
# Get unquoted entries # Get unquoted entries
entries = NotifyMSTeams.split_path(results['fullpath']) entries = NotifyMSTeams.split_path(results['fullpath'])
# Deprecated mode (backwards compatibility)
if results.get('user'): if results.get('user'):
# If a user was found, it's because it's still part of the first # If a user was found, it's because it's still part of the first
# token, so we concatinate them # token, so we concatinate them
@ -451,28 +488,25 @@ class NotifyMSTeams(NotifyBase):
) )
else: else:
# The first token is stored in the hostname # Get the Team from the hostname
results['token_a'] = NotifyMSTeams.unquote(results['host']) results['team'] = NotifyMSTeams.unquote(results['host'])
# Now fetch the remaining tokens # Get the token from the path
try: results['token_a'] = None if not entries else entries.pop(0)
results['token_b'] = entries.pop(0)
except IndexError: results['token_b'] = None if not entries else entries.pop(0)
# We're done results['token_c'] = None if not entries else entries.pop(0)
results['token_b'] = None
try:
results['token_c'] = entries.pop(0)
except IndexError:
# We're done
results['token_c'] = None
# Get Image # Get Image
results['include_image'] = \ results['include_image'] = \
parse_bool(results['qsd'].get('image', True)) parse_bool(results['qsd'].get('image', True))
# Get Team name if defined
if 'team' in results['qsd'] and results['qsd']['team']:
results['team'] = \
NotifyMSTeams.unquote(results['qsd']['team'])
# Template Handling
if 'template' in results['qsd'] and results['qsd']['template']: if 'template' in results['qsd'] and results['qsd']['template']:
results['template'] = \ results['template'] = \
NotifyMSTeams.unquote(results['qsd']['template']) NotifyMSTeams.unquote(results['qsd']['template'])
@ -485,15 +519,18 @@ class NotifyMSTeams(NotifyBase):
@staticmethod @staticmethod
def parse_native_url(url): def parse_native_url(url):
""" """
Support: Legacy Support:
https://outlook.office.com/webhook/ABCD/IncomingWebhook/DEFG/HIJK https://outlook.office.com/webhook/ABCD/IncomingWebhook/DEFG/HIJK
New Hook Support:
https://team-name.office.com/webhook/ABCD/IncomingWebhook/DEFG/HIJK
""" """
# We don't need to do incredibly details token matching as the purpose # 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 # of this is just to detect that were dealing with an msteams url
# token parsing will occur once we initialize the function # token parsing will occur once we initialize the function
result = re.match( result = re.match(
r'^https?://outlook\.office\.com/webhook/' r'^https?://(?P<team>[^.]+)\.office\.com/webhook/'
r'(?P<token_a>[A-Z0-9-]+@[A-Z0-9-]+)/' r'(?P<token_a>[A-Z0-9-]+@[A-Z0-9-]+)/'
r'IncomingWebhook/' r'IncomingWebhook/'
r'(?P<token_b>[A-Z0-9]+)/' r'(?P<token_b>[A-Z0-9]+)/'
@ -502,8 +539,10 @@ class NotifyMSTeams(NotifyBase):
if result: if result:
return NotifyMSTeams.parse_url( return NotifyMSTeams.parse_url(
'{schema}://{token_a}/{token_b}/{token_c}/{params}'.format( '{schema}://{team}/{token_a}/{token_b}/{token_c}'
'/{params}'.format(
schema=NotifyMSTeams.secure_protocol, schema=NotifyMSTeams.secure_protocol,
team=result.group('team'),
token_a=result.group('token_a'), token_a=result.group('token_a'),
token_b=result.group('token_b'), token_b=result.group('token_b'),
token_c=result.group('token_c'), token_c=result.group('token_c'),

View File

@ -1339,7 +1339,7 @@ def test_apprise_details_plugin_verification():
if six.PY2: if six.PY2:
# inspect our object # inspect our object
# getargspec() is depricated in Python v3 # getargspec() is deprecated in Python v3
spec = inspect.getargspec(SCHEMA_MAP[protocols[0]].__init__) spec = inspect.getargspec(SCHEMA_MAP[protocols[0]].__init__)
function_args = \ function_args = \

View File

@ -2025,18 +2025,38 @@ TEST_URLS = (
# All tokens provided - we're good # All tokens provided - we're good
'instance': plugins.NotifyMSTeams}), 'instance': plugins.NotifyMSTeams}),
# Legacy URL Formatting
('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,
# don't include an image by default # don't include an image by default
'include_image': False, 'include_image': False,
}), }),
# Legacy URL Formatting
('msteams://{}@{}/{}/{}?image=No'.format(UUID4, UUID4, 'a' * 32, UUID4), { ('msteams://{}@{}/{}/{}?image=No'.format(UUID4, UUID4, 'a' * 32, UUID4), {
# All tokens provided - we're good no image # All tokens provided - we're good no image
'instance': plugins.NotifyMSTeams, 'instance': plugins.NotifyMSTeams,
# Our expected url(privacy=True) startswith() response: # Our expected url(privacy=True) startswith() response:
'privacy_url': 'msteams://8...2/a...a/8...2/', 'privacy_url': 'msteams://outlook/8...2/a...a/8...2/',
}),
# New 2021 URL formatting
('msteams://apprise/{}@{}/{}/{}'.format(
UUID4, UUID4, 'a' * 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/',
}),
# New 2021 URL formatting; support team= argument
('msteams://{}@{}/{}/{}?team=teamname'.format(
UUID4, UUID4, 'a' * 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/',
}), }),
('msteams://{}@{}/{}/{}?tx'.format(UUID4, UUID4, 'a' * 32, UUID4), { ('msteams://{}@{}/{}/{}?tx'.format(UUID4, UUID4, 'a' * 32, UUID4), {
'instance': plugins.NotifyMSTeams, 'instance': plugins.NotifyMSTeams,