diff --git a/apprise/plugins/NotifyMatrix.py b/apprise/plugins/NotifyMatrix.py index 78019715a1..4d99e9213a 100644 --- a/apprise/plugins/NotifyMatrix.py +++ b/apprise/plugins/NotifyMatrix.py @@ -53,8 +53,11 @@ from ..AppriseLocale import gettext_lazy as _ # Define default path -MATRIX_V2_API_PATH = '/_matrix/client/r0' MATRIX_V1_WEBHOOK_PATH = '/api/v1/matrix/hook' +MATRIX_V2_API_PATH = '/_matrix/client/r0' +MATRIX_V3_API_PATH = '/_matrix/client/v3' +MATRIX_V3_MEDIA_PATH = '/_matrix/media/v3' +MATRIX_V2_MEDIA_PATH = '/_matrix/media/r0' # Extend HTTP Error Messages MATRIX_HTTP_ERROR_MAP = { @@ -88,6 +91,21 @@ class MatrixMessageType: ) +class MatrixVersion: + # Version 2 + V2 = "2" + + # Version 3 + V3 = "3" + + +# webhook modes are placed into this list for validation purposes +MATRIX_VERSIONS = ( + MatrixVersion.V2, + MatrixVersion.V3, +) + + class MatrixWebhookMode: # Webhook Mode is disabled DISABLED = "off" @@ -147,6 +165,9 @@ class NotifyMatrix(NotifyBase): # Throttle a wee-bit to avoid thrashing request_rate_per_sec = 0.5 + # Our Matrix API Version + matrix_api_version = '3' + # How many retry attempts we'll make in the event the server asks us to # throttle back. default_retries = 2 @@ -234,6 +255,12 @@ class NotifyMatrix(NotifyBase): 'values': MATRIX_WEBHOOK_MODES, 'default': MatrixWebhookMode.DISABLED, }, + 'version': { + 'name': _('Matrix API Verion'), + 'type': 'choice:string', + 'values': MATRIX_VERSIONS, + 'default': MatrixVersion.V3, + }, 'msgtype': { 'name': _('Message Type'), 'type': 'choice:string', @@ -248,7 +275,7 @@ class NotifyMatrix(NotifyBase): }, }) - def __init__(self, targets=None, mode=None, msgtype=None, + def __init__(self, targets=None, mode=None, msgtype=None, version=None, include_image=False, **kwargs): """ Initialize Matrix Object @@ -282,6 +309,14 @@ def __init__(self, targets=None, mode=None, msgtype=None, self.logger.warning(msg) raise TypeError(msg) + # Setup our version + self.version = self.template_args['version']['default'] \ + if not isinstance(version, str) else version.lower() + if self.version and self.version not in MATRIX_VERSIONS: + msg = 'The version specified ({}) is invalid.'.format(version) + self.logger.warning(msg) + raise TypeError(msg) + # Setup our message type self.msgtype = self.template_args['msgtype']['default'] \ if not isinstance(msgtype, str) else msgtype.lower() @@ -521,7 +556,8 @@ def _t2bot_webhook_payload(self, body, title='', return payload def _send_server_notification(self, body, title='', - notify_type=NotifyType.INFO, **kwargs): + notify_type=NotifyType.INFO, attach=None, + **kwargs): """ Perform Direct Matrix Server Notification (no webhook) """ @@ -548,6 +584,10 @@ def _send_server_notification(self, body, title='', # Initiaize our error tracking has_error = False + attachments = None + if attach: + attachments = self._send_attachments(attach) + while len(rooms) > 0: # Get our room @@ -568,6 +608,10 @@ def _send_server_notification(self, body, title='', image_url = None if not self.include_image else \ self.image_url(notify_type) + # Build our path + path = '/rooms/{}/send/m.room.message'.format( + NotifyMatrix.quote(room_id)) + if image_url: # Define our payload image_payload = { @@ -575,9 +619,6 @@ def _send_server_notification(self, body, title='', 'url': image_url, 'body': '{}'.format(notify_type if not title else title), } - # Build our path - path = '/rooms/{}/send/m.room.message'.format( - NotifyMatrix.quote(room_id)) # Post our content postokay, response = self._fetch(path, payload=image_payload) @@ -586,6 +627,14 @@ def _send_server_notification(self, body, title='', has_error = True continue + if attachments: + for attachment in attachments: + postokay, response = self._fetch(path, payload=attachment) + if not postokay: + # Mark our failure + has_error = True + continue + # Define our payload payload = { 'msgtype': 'm.{}'.format(self.msgtype), @@ -615,10 +664,6 @@ def _send_server_notification(self, body, title='', ) }) - # Build our path - path = '/rooms/{}/send/m.room.message'.format( - NotifyMatrix.quote(room_id)) - # Post our content postokay, response = self._fetch(path, payload=payload) if not postokay: @@ -632,6 +677,40 @@ def _send_server_notification(self, body, title='', return not has_error + def _send_attachments(self, attach): + """ + Posts all of the provided attachments + """ + + payloads = [] + for attachment in attach: + if not re.match(r'^image/', attachment.mimetype, re.I): + # unsuppored at this time + continue + + postokay, response = \ + self._fetch('/upload', attachment=attachment) + if not (postokay and isinstance(response, dict)): + # Failed to perform upload + return False + + # If we get here, we'll have a response that looks like: + # { + # "content_uri": "mxc://example.com/a-unique-key" + # } + + # Prepare our payload + payloads.append({ + "info": { + "mimetype": attachment.mimetype, + }, + "msgtype": "m.image", + "body": "tta.webp", + "url": response.get('content_uri'), + }) + + return payloads + def _register(self): """ Register with the service if possible. @@ -970,7 +1049,8 @@ def _room_id(self, room): return None - def _fetch(self, path, payload=None, params=None, method='POST'): + def _fetch(self, path, payload=None, params=None, attachment=None, + method='POST'): """ Wrapper to request.post() to manage it's response better and make the send() function cleaner and easier to maintain. @@ -983,6 +1063,7 @@ def _fetch(self, path, payload=None, params=None, method='POST'): headers = { 'User-Agent': self.app_id, 'Content-Type': 'application/json', + 'Accept': 'application/json', } if self.access_token is not None: @@ -991,13 +1072,32 @@ def _fetch(self, path, payload=None, params=None, method='POST'): default_port = 443 if self.secure else 80 url = \ - '{schema}://{hostname}{port}{matrix_api}{path}'.format( + '{schema}://{hostname}{port}'.format( schema='https' if self.secure else 'http', hostname=self.host, port='' if self.port is None - or self.port == default_port else f':{self.port}', - matrix_api=MATRIX_V2_API_PATH, - path=path) + or self.port == default_port else f':{self.port}') + + if path == '/upload': + if self.version == MatrixVersion.V3: + url += MATRIX_V3_MEDIA_PATH + path + + else: + url += MATRIX_V2_MEDIA_PATH + path + + params = {'filename': attachment.name} + with open(attachment.path, 'rb') as fp: + payload = fp.read() + + # Update our content type + headers['Content-Type'] = attachment.mimetype + + else: + if self.version == MatrixVersion.V3: + url += MATRIX_V3_API_PATH + path + + else: + url += MATRIX_V2_API_PATH + path # Our response object response = {} @@ -1024,7 +1124,7 @@ def _fetch(self, path, payload=None, params=None, method='POST'): try: r = fn( url, - data=dumps(payload), + data=dumps(payload) if not attachment else payload, params=params, headers=headers, verify=self.verify_certificate, @@ -1161,6 +1261,7 @@ def url(self, privacy=False, *args, **kwargs): params = { 'image': 'yes' if self.include_image else 'no', 'mode': self.mode, + 'version': self.version, 'msgtype': self.msgtype, } @@ -1258,6 +1359,14 @@ def parse_url(url): if 'token' in results['qsd'] and len(results['qsd']['token']): results['password'] = NotifyMatrix.unquote(results['qsd']['token']) + # Support the use of the version= or v= keyword + if 'version' in results['qsd'] and len(results['qsd']['version']): + results['version'] = \ + NotifyMatrix.unquote(results['qsd']['version']) + + elif 'v' in results['qsd'] and len(results['qsd']['v']): + results['version'] = NotifyMatrix.unquote(results['qsd']['v']) + return results @staticmethod @@ -1267,7 +1376,7 @@ def parse_native_url(url): """ result = re.match( - r'^https?://webhooks\.t2bot\.io/api/v1/matrix/hook/' + r'^https?://webhooks\.t2bot\.io/api/v[0-9]+/matrix/hook/' r'(?P[A-Z0-9_-]+)/?' r'(?P\?.+)?$', url, re.I)