From 6ad013b8fbd166aad13c052e22ac3c11662d4a24 Mon Sep 17 00:00:00 2001 From: Mark Wilson Date: Thu, 12 Dec 2024 14:54:11 +0000 Subject: [PATCH] feat: Add support for latest MSTeams webhook URL format --- apprise/plugins/msteams.py | 113 ++++++++++++++++++++++++++++++------ setup.cfg | 2 +- test/test_plugin_msteams.py | 9 +++ 3 files changed, 104 insertions(+), 20 deletions(-) diff --git a/apprise/plugins/msteams.py b/apprise/plugins/msteams.py index 83f85c79b..8c56fdf12 100644 --- a/apprise/plugins/msteams.py +++ b/apprise/plugins/msteams.py @@ -118,6 +118,9 @@ class NotifyMSTeams(NotifyBase): notify_url_v2 = 'https://{team}.webhook.office.com/webhookb2/' \ '{token_a}/IncomingWebhook/{token_b}/{token_c}' + notify_url_v3 = 'https://{team}.webhook.office.com/webhookb2/' \ + '{token_a}/IncomingWebhook/{token_b}/{token_c}/{token_d}' + # Allows the user to specify the NotifyImageSize object image_size = NotifyImageSize.XY_72 @@ -176,6 +179,15 @@ class NotifyMSTeams(NotifyBase): 'required': True, 'regex': (r'^[a-z0-9-]+$', 'i'), }, + # Token required as part of the API request + # /........./........./........./DDDDDDDDDDDDDDDDD + 'token_d': { + 'name': _('Token D'), + 'type': 'string', + 'private': True, + 'required': False, + 'regex': (r'^V2[a-zA-Z0-9-_]+$', 'i'), + }, }) # Define our template arguments @@ -189,7 +201,7 @@ class NotifyMSTeams(NotifyBase): 'version': { 'name': _('Version'), 'type': 'choice:int', - 'values': (1, 2), + 'values': (1, 2, 3), 'default': 2, }, 'template': { @@ -207,8 +219,9 @@ class NotifyMSTeams(NotifyBase): }, } - def __init__(self, token_a, token_b, token_c, team=None, version=None, - include_image=True, template=None, tokens=None, **kwargs): + def __init__(self, token_a, token_b, token_c, token_d=None, team=None, + version=None, include_image=True, template=None, tokens=None, + **kwargs): """ Initialize Microsoft Teams Object @@ -271,6 +284,9 @@ def __init__(self, token_a, token_b, token_c, team=None, version=None, self.logger.warning(msg) raise TypeError(msg) + self.token_d = validate_regex( + token_d, *self.template_tokens['token_d']['regex']) + # Place a thumbnail image inline with the message body self.include_image = include_image @@ -294,12 +310,11 @@ def __init__(self, token_a, token_b, token_c, team=None, version=None, raise TypeError(msg) self.logger.deprecate( - "Microsoft is depricating their MSTeams webhooks on " + "Microsoft is deprecating their MSTeams webhooks on " "December 31, 2024. It is advised that you switch to " "Microsoft Power Automate (already supported by Apprise as " "workflows://. For more information visit: " "https://github.com/caronc/apprise/wiki/Notify_workflows") - return def gen_payload(self, body, title='', notify_type=NotifyType.INFO, **kwargs): @@ -403,17 +418,28 @@ def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): 'Content-Type': 'application/json', } - 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( + if self.version == 1: + notify_url = self.notify_url_v1.format( token_a=self.token_a, token_b=self.token_b, token_c=self.token_c) + if self.version == 2: + 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 == 3: + notify_url = self.notify_url_v3.format( + team=self.team, + token_a=self.token_a, + token_b=self.token_b, + token_c=self.token_c, + token_d=self.token_d, + ) + # Generate our payload if it's possible payload = self.gen_payload( body=body, title=title, notify_type=notify_type, **kwargs) @@ -503,8 +529,20 @@ def url(self, privacy=False, *args, **kwargs): # 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}/'\ + result = None + + if self.version == 1: + result = '{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), + ) + + if self.version == 2: + result = '{schema}://{team}/{token_a}/{token_b}/{token_c}/'\ '?{params}'.format( schema=self.secure_protocol, team=NotifyMSTeams.quote(self.team, safe=''), @@ -514,15 +552,18 @@ def url(self, privacy=False, *args, **kwargs): params=NotifyMSTeams.urlencode(params), ) - else: # Version 1 - return '{schema}://{token_a}/{token_b}/{token_c}/'\ - '?{params}'.format( + if self.version == 3: + result = '{schema}://{team}/{token_a}/{token_b}/{token_c}/'\ + '{token_d}/?{params}'.format( schema=self.secure_protocol, - token_a=self.pprint(self.token_a, privacy, safe='@'), + team=NotifyMSTeams.quote(self.team, safe=''), + 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=''), + token_d=self.pprint(self.token_d, privacy, safe=''), params=NotifyMSTeams.urlencode(params), ) + return result @staticmethod def parse_url(url): @@ -561,6 +602,8 @@ def parse_url(url): else NotifyMSTeams.unquote(entries.pop(0)) results['token_c'] = None if not entries \ else NotifyMSTeams.unquote(entries.pop(0)) + results['token_d'] = None if not entries \ + else NotifyMSTeams.unquote(entries.pop(0)) # Get Image results['include_image'] = \ @@ -582,8 +625,13 @@ def parse_url(url): NotifyMSTeams.unquote(results['qsd']['version']) else: + version = 1 + if results.get('team'): + version = 2 + if results.get('token_d'): + version = 3 # Set our version if not otherwise set - results['version'] = 1 if not results.get('team') else 2 + results['version'] = version # Store our tokens results['tokens'] = results['qsd:'] @@ -598,11 +646,38 @@ def parse_native_url(url): New Hook Support: https://team-name.office.com/webhook/ABCD/IncomingWebhook/DEFG/HIJK + + Newer Hook Support: + https://team-name.office.com/webhook/ABCD/IncomingWebhook/DEFG/HIJK/V2LMNOP """ # 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?://(?P[^.]+)(?P\.webhook)?\.office\.com/' + r'webhook(?Pb2)?/' + r'(?P[A-Z0-9-]+@[A-Z0-9-]+)/' + r'IncomingWebhook/' + r'(?P[A-Z0-9]+)/' + r'(?P[A-Z0-9-]+)/' + r'(?PV2[A-Z0-9-_]+)/?' + r'(?P\?.+)?$', url, re.I) + + if result: + # Version 3 URL + return NotifyMSTeams.parse_url( + '{schema}://{team}/{token_a}/{token_b}/{token_c}/{token_d}' + '/{params}'.format( + schema=NotifyMSTeams.secure_protocol, + team=result.group('team'), + token_a=result.group('token_a'), + token_b=result.group('token_b'), + token_c=result.group('token_c'), + token_d=result.group('token_d'), + params='' if not result.group('params') + else result.group('params'))) + result = re.match( r'^https?://(?P[^.]+)(?P\.webhook)?\.office\.com/' r'webhook(?Pb2)?/' diff --git a/setup.cfg b/setup.cfg index 253d6c63a..d96ac296c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,7 +7,7 @@ license_files = LICENSE [flake8] # We exclude packages we don't maintain -exclude = .eggs,.tox,.local,dist +exclude = .eggs,.tox,.local,dist,.venv,venv ignore = E741,E722,W503,W504,W605 statistics = true builtins = _ diff --git a/test/test_plugin_msteams.py b/test/test_plugin_msteams.py index 377c7467c..9c940c4e7 100644 --- a/test/test_plugin_msteams.py +++ b/test/test_plugin_msteams.py @@ -92,6 +92,15 @@ # Our expected url(privacy=True) startswith() response (v2 format): 'privacy_url': 'msteams://myteam/8...2/m...m/8...2/'}), + # Support Newer Native URLs with 4 tokens, introduced in 2024 + ('https://myteam.webhook.office.com/webhookb2/{}@{}/IncomingWebhook/{}/{}' + '/{}' + .format(UUID4, UUID4, 'm' * 32, UUID4, 'V2-_' + 'n' * 43), { + # All tokens provided - we're good + 'instance': NotifyMSTeams, + + # Our expected url(privacy=True) startswith() response (v2 format): + 'privacy_url': 'msteams://myteam/8...2/m...m/8...2/V...n'}), # Legacy URL Formatting ('msteams://{}@{}/{}/{}?t2'.format(UUID4, UUID4, 'c' * 32, UUID4), {