Skip to content

Commit

Permalink
Add support for latest MSTeams webhook URL format (#1253)
Browse files Browse the repository at this point in the history
  • Loading branch information
anothermwilson authored Dec 15, 2024
1 parent 551fa0d commit 12f8da9
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 20 deletions.
113 changes: 94 additions & 19 deletions apprise/plugins/msteams.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,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

Expand Down Expand Up @@ -174,6 +177,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
Expand All @@ -187,7 +199,7 @@ class NotifyMSTeams(NotifyBase):
'version': {
'name': _('Version'),
'type': 'choice:int',
'values': (1, 2),
'values': (1, 2, 3),
'default': 2,
},
'template': {
Expand All @@ -205,8 +217,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
Expand Down Expand Up @@ -269,6 +282,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

Expand All @@ -292,12 +308,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):
Expand Down Expand Up @@ -401,17 +416,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)
Expand Down Expand Up @@ -501,8 +527,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=''),
Expand All @@ -512,15 +550,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):
Expand Down Expand Up @@ -559,6 +600,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'] = \
Expand All @@ -580,8 +623,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:']
Expand All @@ -596,11 +644,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<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]+)/'
r'(?P<token_c>[A-Z0-9-]+)/'
r'(?P<token_d>V2[A-Z0-9-_]+)/?'
r'(?P<params>\?.+)?$', 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<team>[^.]+)(?P<v2a>\.webhook)?\.office\.com/'
r'webhook(?P<v2b>b2)?/'
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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 = _
Expand Down
9 changes: 9 additions & 0 deletions test/test_plugin_msteams.py
Original file line number Diff line number Diff line change
Expand Up @@ -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), {
Expand Down

0 comments on commit 12f8da9

Please sign in to comment.