From 8d2a53cedf3265731fe1e7e79fe418a7a5bb99e5 Mon Sep 17 00:00:00 2001 From: lockhead <37371518+lkhd@users.noreply.github.com> Date: Mon, 23 Dec 2019 01:10:54 +0100 Subject: [PATCH] Support for t2bot.io webhooks for Matrix (#189) --- apprise/plugins/NotifyMatrix.py | 125 +++++++++++++++++++++++++++----- test/test_rest_plugins.py | 35 ++++++++- 2 files changed, 140 insertions(+), 20 deletions(-) diff --git a/apprise/plugins/NotifyMatrix.py b/apprise/plugins/NotifyMatrix.py index 97ab127c..13e7fbd3 100644 --- a/apprise/plugins/NotifyMatrix.py +++ b/apprise/plugins/NotifyMatrix.py @@ -41,6 +41,7 @@ from ..common import NotifyImageSize from ..common import NotifyFormat from ..utils import parse_bool from ..utils import parse_list +from ..utils import validate_regex from ..AppriseLocale import gettext_lazy as _ # Define default path @@ -74,12 +75,16 @@ class MatrixWebhookMode(object): # Support the slack webhook plugin SLACK = "slack" + # Support the t2bot webhook plugin + T2BOT = "t2bot" + # webhook modes are placed ito this list for validation purposes MATRIX_WEBHOOK_MODES = ( MatrixWebhookMode.DISABLED, MatrixWebhookMode.MATRIX, MatrixWebhookMode.SLACK, + MatrixWebhookMode.T2BOT, ) @@ -122,6 +127,11 @@ class NotifyMatrix(NotifyBase): # Define object templates templates = ( + # Targets are ignored when using t2bot mode; only a token is required + '{schema}://{token}', + '{schema}://{user}@{token}', + + # All other non-t2bot setups require targets '{schema}://{user}:{password}@{host}/{targets}', '{schema}://{user}:{password}@{host}:{port}/{targets}', '{schema}://{token}:{password}@{host}/{targets}', @@ -199,8 +209,7 @@ class NotifyMatrix(NotifyBase): }, }) - def __init__(self, targets=None, mode=None, include_image=False, - **kwargs): + def __init__(self, targets=None, mode=None, include_image=False, **kwargs): """ Initialize Matrix Object """ @@ -233,6 +242,16 @@ class NotifyMatrix(NotifyBase): self.logger.warning(msg) raise TypeError(msg) + if self.mode == MatrixWebhookMode.T2BOT: + # t2bot configuration requires that a webhook id is specified + self.access_token = validate_regex( + self.host, r'^[a-z0-9]{64}$', 'i') + if not self.access_token: + msg = 'An invalid T2Bot/Matrix Webhook ID ' \ + '({}) was specified.'.format(self.host) + self.logger.warning(msg) + raise TypeError(msg) + def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): """ Perform Matrix Notification @@ -257,20 +276,30 @@ class NotifyMatrix(NotifyBase): 'Content-Type': 'application/json', } - # Acquire our access token from our URL - access_token = self.password if self.password else self.user + if self.mode != MatrixWebhookMode.T2BOT: + # Acquire our access token from our URL + access_token = self.password if self.password else self.user - default_port = 443 if self.secure else 80 + default_port = 443 if self.secure else 80 - # Prepare our URL - url = '{schema}://{hostname}:{port}/{webhook_path}/{token}'.format( - schema='https' if self.secure else 'http', - hostname=self.host, - port='' if self.port is None - or self.port == default_port else self.port, - webhook_path=MATRIX_V1_WEBHOOK_PATH, - token=access_token, - ) + # Prepare our URL + url = '{schema}://{hostname}:{port}/{webhook_path}/{token}'.format( + schema='https' if self.secure else 'http', + hostname=self.host, + port='' if self.port is None + or self.port == default_port else self.port, + webhook_path=MATRIX_V1_WEBHOOK_PATH, + token=access_token, + ) + + else: + # + # t2bot Setup + # + + # Prepare our URL + url = 'https://webhooks.t2bot.io/api/v1/matrix/hook/' \ + '{token}'.format(token=self.access_token) # Retrieve our payload payload = getattr(self, '_{}_webhook_payload'.format(self.mode))( @@ -381,7 +410,7 @@ class NotifyMatrix(NotifyBase): payload = { 'displayName': - self.user if self.user else self.matrix_default_user, + self.user if self.user else self.app_id, 'format': 'html', } @@ -399,6 +428,27 @@ class NotifyMatrix(NotifyBase): return payload + def _t2bot_webhook_payload(self, body, title='', + notify_type=NotifyType.INFO, **kwargs): + """ + Format the payload for a T2Bot Matrix based messages + + """ + + # Retrieve our payload + payload = self._matrix_webhook_payload( + body=body, title=title, notify_type=notify_type, **kwargs) + + # Acquire our image url if we're configured to do so + image_url = None if not self.include_image else \ + self.image_url(notify_type) + + if image_url: + # t2bot can take an avatarUrl Entry + payload['avatarUrl'] = image_url + + return payload + def _send_server_notification(self, body, title='', notify_type=NotifyType.INFO, **kwargs): """ @@ -867,6 +917,9 @@ class NotifyMatrix(NotifyBase): )) self.logger.debug('Matrix Payload: %s' % str(payload)) + # Initialize our response object + r = None + try: r = fn( url, @@ -948,7 +1001,8 @@ class NotifyMatrix(NotifyBase): """ Ensure we relinquish our token """ - self._logout() + if self.mode != MatrixWebhookMode.T2BOT: + self._logout() def url(self, privacy=False, *args, **kwargs): """ @@ -997,12 +1051,14 @@ class NotifyMatrix(NotifyBase): us to substantiate this object. """ - results = NotifyBase.parse_url(url) - + results = NotifyBase.parse_url(url, verify_host=False) if not results: # We're done early as we couldn't load the results return results + if not results.get('host'): + return None + # Get our rooms results['targets'] = NotifyMatrix.split_path(results['fullpath']) @@ -1040,4 +1096,37 @@ class NotifyMatrix(NotifyBase): results['mode'] = results['qsd'].get( 'mode', results['qsd'].get('webhook')) + # t2bot detection... look for just a hostname, and/or just a user/host + # if we match this; we can go ahead and set the mode (but only if + # it was otherwise not set) + if results['mode'] is None \ + and not results['password'] \ + and not results['targets']: + + # Default mode to t2bot + results['mode'] = MatrixWebhookMode.T2BOT + return results + + @staticmethod + def parse_native_url(url): + """ + Support https://webhooks.t2bot.io/api/v1/matrix/hook/WEBHOOK_TOKEN/ + """ + + result = re.match( + r'^https?://webhooks\.t2bot\.io/api/v1/matrix/hook/' + r'(?P[A-Z0-9_-]+)/?' + r'(?P\?.+)?$', url, re.I) + + if result: + mode = 'mode={}'.format(MatrixWebhookMode.T2BOT) + + return NotifyMatrix.parse_url( + '{schema}://{webhook_token}/{args}'.format( + schema=NotifyMatrix.secure_protocol, + webhook_token=result.group('webhook_token'), + args='?{}'.format(mode) if not result.group('args') + else '{}&{}'.format(result.group('args'), mode))) + + return None diff --git a/test/test_rest_plugins.py b/test/test_rest_plugins.py index 2797d3d5..a7953921 100644 --- a/test/test_rest_plugins.py +++ b/test/test_rest_plugins.py @@ -1241,12 +1241,17 @@ TEST_URLS = ( ('matrixs://', { 'instance': None, }), - ('matrix://localhost', { + ('matrix://localhost?mode=off', { # treats it as a anonymous user to register 'instance': plugins.NotifyMatrix, # response is false because we have nothing to notify 'response': False, }), + ('matrix://localhost', { + # response is TypeError because we'll try to initialize as + # a t2bot and fail (localhost is too short of a api key) + 'instance': TypeError + }), ('matrix://user:pass@localhost/#room1/#room2/#room3', { 'instance': plugins.NotifyMatrix, 'response': False, @@ -1310,6 +1315,27 @@ TEST_URLS = ( # user and token specified; image set to True 'instance': plugins.NotifyMatrix, }), + ('matrixs://user@{}?mode=t2bot&format=markdown&image=True' + .format('a' * 64), { + # user and token specified; image set to True + 'instance': plugins.NotifyMatrix}), + ('matrix://user@{}?mode=t2bot&format=markdown&image=False' + .format('z' * 64), { + # user and token specified; image set to True + 'instance': plugins.NotifyMatrix}), + # This will default to t2bot because no targets were specified and no + # password + ('matrixs://{}'.format('c' * 64), { + 'instance': plugins.NotifyMatrix, + # Throws a series of connection and transfer exceptions when this flag + # is set and tests that we gracfully handle them + 'test_requests_exceptions': True, + }), + # Test Native URL + ('https://webhooks.t2bot.io/api/v1/matrix/hook/{}/'.format('d' * 64), { + # user and token specified; image set to True + 'instance': plugins.NotifyMatrix, + }), # Legacy (Depricated) image reference ('matrixs://user:token@localhost?mode=slack&thumbnail=False', { # user and token specified; image set to True @@ -1340,7 +1366,12 @@ TEST_URLS = ( # is set and tests that we gracfully handle them 'test_requests_exceptions': True, }), - + ('matrix://{}/?mode=t2bot'.format('b' * 64), { + 'instance': plugins.NotifyMatrix, + # Throws a series of connection and transfer exceptions when this flag + # is set and tests that we gracfully handle them + 'test_requests_exceptions': True, + }), ################################## # NotifyMatterMost