From 6f5b064841f325fee65dbc6935a29643ad07def7 Mon Sep 17 00:00:00 2001 From: Chris Caron Date: Fri, 6 Oct 2023 16:19:50 -0400 Subject: [PATCH 1/3] adjusted Matrix login URL using api v3 --- apprise/plugins/NotifyMatrix.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/apprise/plugins/NotifyMatrix.py b/apprise/plugins/NotifyMatrix.py index 9f0a2c130a..907b569608 100644 --- a/apprise/plugins/NotifyMatrix.py +++ b/apprise/plugins/NotifyMatrix.py @@ -780,12 +780,23 @@ def _login(self): 'user/pass combo is missing.') return False - # Prepare our Registration Payload - payload = { - 'type': 'm.login.password', - 'user': self.user, - 'password': self.password, - } + # Prepare our Authentication Payload + if self.version == MatrixVersion.V3: + payload = { + 'type': 'm.login.password', + 'identifier': { + 'type': 'm.id.user', + 'user': self.user, + }, + 'password': self.password, + } + + else: + payload = { + 'type': 'm.login.password', + 'user': self.user, + 'password': self.password, + } # Build our URL postokay, response = self._fetch('/login', payload=payload) From c9ef17f1c16016f81731c5ae4cfa632037e62a5c Mon Sep 17 00:00:00 2001 From: Chris Caron Date: Fri, 13 Oct 2023 21:41:46 -0400 Subject: [PATCH 2/3] updated to include findings in PR --- apprise/plugins/NotifyMatrix.py | 64 +++++++++++++++++++++++++-------- test/test_plugin_matrix.py | 50 +++++++++++++++++--------- 2 files changed, 82 insertions(+), 32 deletions(-) diff --git a/apprise/plugins/NotifyMatrix.py b/apprise/plugins/NotifyMatrix.py index 907b569608..e26fcca13f 100644 --- a/apprise/plugins/NotifyMatrix.py +++ b/apprise/plugins/NotifyMatrix.py @@ -151,6 +151,9 @@ class NotifyMatrix(NotifyBase): # Allows the user to specify the NotifyImageSize object image_size = NotifyImageSize.XY_32 + # Matrix is_image check + __matrix_is_image = re.compile(r'^image/.*', re.I) + # The maximum allowable characters allowed in the body per message # https://spec.matrix.org/v1.6/client-server-api/#size-limits # The complete event MUST NOT be larger than 65536 bytes, when formatted @@ -611,10 +614,15 @@ def _send_server_notification(self, body, title='', self.image_url(notify_type) # Build our path - path = '/rooms/{}/send/m.room.message'.format( - NotifyMatrix.quote(room_id)) + if self.version == MatrixVersion.V3: + path = '/rooms/{}/send/m.room.message/0'.format( + NotifyMatrix.quote(room_id)) + + else: + path = '/rooms/{}/send/m.room.message'.format( + NotifyMatrix.quote(room_id)) - if image_url: + if image_url and self.version == MatrixVersion.V2: # Define our payload image_payload = { 'msgtype': 'm.image', @@ -631,6 +639,10 @@ def _send_server_notification(self, body, title='', if attachments: for attachment in attachments: + if self.version == MatrixVersion.V3: + attachment['room_id'] = room_id + attachment['type'] = 'm.room.message' + postokay, response = self._fetch(path, payload=attachment) if not postokay: # Mark our failure @@ -667,7 +679,9 @@ def _send_server_notification(self, body, title='', }) # Post our content - postokay, response = self._fetch(path, payload=payload) + method = 'PUT' if self.version == MatrixVersion.V3 else 'POST' + postokay, response = self._fetch( + path, payload=payload, method=method) if not postokay: # Notify our user self.logger.warning( @@ -705,15 +719,32 @@ def _send_attachments(self, attach): # "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'), - }) + if self.version == MatrixVersion.V3: + # Prepare our payload + is_image = self.__matrix_is_image.match(attachment.mimetype) + payloads.append({ + "body": attachment.name, + "info": { + "mimetype": attachment.mimetype, + "size": len(attachment), + }, + "msgtype": "m.image" if is_image else "m.file", + "url": response.get('content_uri'), + }) + if not is_image: + # Setup `m.file' + payloads[-1]['filename'] = attachment.name + + else: + # Prepare our payload + payloads.append({ + "info": { + "mimetype": attachment.mimetype, + }, + "msgtype": "m.image", + "body": "tta.webp", + "url": response.get('content_uri'), + }) return payloads @@ -1120,7 +1151,8 @@ def _fetch(self, path, payload=None, params=None, attachment=None, response = {} # fetch function - fn = requests.post if method == 'POST' else requests.get + fn = requests.post if method == 'POST' else ( + requests.put if method == 'PUT' else requests.get) # Define how many attempts we'll make if we get caught in a throttle # event @@ -1148,7 +1180,9 @@ def _fetch(self, path, payload=None, params=None, attachment=None, timeout=self.request_timeout, ) - self.logger.debug('Matrix Response: %s' % str(r.content)) + self.logger.debug( + 'Matrix Response: code=%d, %s' % ( + r.status_code, str(r.content))) response = loads(r.content) if r.status_code == 429: diff --git a/test/test_plugin_matrix.py b/test/test_plugin_matrix.py index 76d8624e2f..85ab33f4f6 100644 --- a/test/test_plugin_matrix.py +++ b/test/test_plugin_matrix.py @@ -27,7 +27,6 @@ # POSSIBILITY OF SUCH DAMAGE. from unittest import mock - import os import requests import pytest @@ -228,9 +227,10 @@ def test_plugin_matrix_urls(): AppriseURLTester(tests=apprise_url_tests).run_all() +@mock.patch('requests.put') @mock.patch('requests.get') @mock.patch('requests.post') -def test_plugin_matrix_general(mock_post, mock_get): +def test_plugin_matrix_general(mock_post, mock_get, mock_put): """ NotifyMatrix() General Tests @@ -250,6 +250,7 @@ def test_plugin_matrix_general(mock_post, mock_get): # Prepare Mock mock_get.return_value = request mock_post.return_value = request + mock_put.return_value = request # Variation Initializations obj = NotifyMatrix(host='host', targets='#abcd') @@ -383,9 +384,10 @@ def test_plugin_matrix_general(mock_post, mock_get): assert obj.send(user='test', password='passwd', body="test") is True +@mock.patch('requests.put') @mock.patch('requests.get') @mock.patch('requests.post') -def test_plugin_matrix_fetch(mock_post, mock_get): +def test_plugin_matrix_fetch(mock_post, mock_get, mock_put): """ NotifyMatrix() Server Fetch/API Tests @@ -419,6 +421,7 @@ def fetch_failed(url, *args, **kwargs): return request + mock_put.side_effect = fetch_failed mock_get.side_effect = fetch_failed mock_post.side_effect = fetch_failed @@ -449,12 +452,14 @@ def fetch_failed(url, *args, **kwargs): # Default configuration mock_get.side_effect = None mock_post.side_effect = None + mock_put.side_effect = None request = mock.Mock() request.status_code = requests.codes.ok request.content = dumps(response_obj) mock_post.return_value = request mock_get.return_value = request + mock_put.return_value = request obj = NotifyMatrix(host='host', include_image=True) assert isinstance(obj, NotifyMatrix) is True @@ -467,6 +472,7 @@ def fetch_failed(url, *args, **kwargs): request.content = dumps({ 'retry_after_ms': 1, }) + code, response = obj._fetch('/retry/apprise/unit/test') assert code is False @@ -485,9 +491,10 @@ def fetch_failed(url, *args, **kwargs): assert code is False +@mock.patch('requests.put') @mock.patch('requests.get') @mock.patch('requests.post') -def test_plugin_matrix_auth(mock_post, mock_get): +def test_plugin_matrix_auth(mock_post, mock_get, mock_put): """ NotifyMatrix() Server Authentication @@ -506,6 +513,7 @@ def test_plugin_matrix_auth(mock_post, mock_get): request.content = dumps(response_obj) mock_post.return_value = request mock_get.return_value = request + mock_put.return_value = request obj = NotifyMatrix(host='localhost') assert isinstance(obj, NotifyMatrix) is True @@ -579,9 +587,10 @@ def test_plugin_matrix_auth(mock_post, mock_get): assert obj.access_token is None +@mock.patch('requests.put') @mock.patch('requests.get') @mock.patch('requests.post') -def test_plugin_matrix_rooms(mock_post, mock_get): +def test_plugin_matrix_rooms(mock_post, mock_get, mock_put): """ NotifyMatrix() Room Testing @@ -606,6 +615,7 @@ def test_plugin_matrix_rooms(mock_post, mock_get): request.content = dumps(response_obj) mock_post.return_value = request mock_get.return_value = request + mock_put.return_value = request obj = NotifyMatrix(host='host') assert isinstance(obj, NotifyMatrix) is True @@ -789,9 +799,10 @@ def test_plugin_matrix_url_parsing(): assert '#room3' in result['targets'] +@mock.patch('requests.put') @mock.patch('requests.get') @mock.patch('requests.post') -def test_plugin_matrix_image_errors(mock_post, mock_get): +def test_plugin_matrix_image_errors(mock_post, mock_get, mock_put): """ NotifyMatrix() Image Error Handling @@ -822,8 +833,9 @@ def mock_function_handing(url, data, **kwargs): # Prepare Mock mock_get.side_effect = mock_function_handing mock_post.side_effect = mock_function_handing + mock_put.side_effect = mock_function_handing - obj = NotifyMatrix(host='host', include_image=True) + obj = NotifyMatrix(host='host', include_image=True, version='2') assert isinstance(obj, NotifyMatrix) is True assert obj.access_token is None @@ -831,7 +843,7 @@ def mock_function_handing(url, data, **kwargs): # we had post errors (of any kind) we still report a failure. assert obj.notify('test', 'test') is False - obj = NotifyMatrix(host='host', include_image=False) + obj = NotifyMatrix(host='host', include_image=False, version='2') assert isinstance(obj, NotifyMatrix) is True assert obj.access_token is None @@ -862,6 +874,7 @@ def mock_function_handing(url, data, **kwargs): # Prepare Mock mock_get.side_effect = mock_function_handing + mock_put.side_effect = mock_function_handing mock_post.side_effect = mock_function_handing obj = NotifyMatrix(host='host', include_image=True) assert isinstance(obj, NotifyMatrix) is True @@ -879,9 +892,10 @@ def mock_function_handing(url, data, **kwargs): del obj +@mock.patch('requests.put') @mock.patch('requests.get') @mock.patch('requests.post') -def test_plugin_matrix_attachments_api_v3(mock_post, mock_get): +def test_plugin_matrix_attachments_api_v3(mock_post, mock_get, mock_put): """ NotifyMatrix() Attachment Checks (v3) @@ -899,6 +913,7 @@ def test_plugin_matrix_attachments_api_v3(mock_post, mock_get): # Prepare Mock return object mock_post.return_value = response mock_get.return_value = response + mock_put.return_value = response # Instantiate our object obj = Apprise.instantiate('matrix://user:pass@localhost/#general?v=3') @@ -913,7 +928,8 @@ def test_plugin_matrix_attachments_api_v3(mock_post, mock_get): attach = AppriseAttachment(os.path.join(TEST_VAR_DIR, 'apprise-test.gif')) # Test our call count - assert mock_post.call_count == 5 + assert mock_put.call_count == 1 + assert mock_post.call_count == 4 assert mock_post.call_args_list[0][0][0] == \ 'http://localhost/_matrix/client/v3/login' assert mock_post.call_args_list[1][0][0] == \ @@ -922,10 +938,10 @@ def test_plugin_matrix_attachments_api_v3(mock_post, mock_get): 'http://localhost/_matrix/client/v3/join/%23general%3Alocalhost' assert mock_post.call_args_list[3][0][0] == \ 'http://localhost/_matrix/client/v3/rooms/%21abc123%3Alocalhost/' \ - 'send/m.room.message' - assert mock_post.call_args_list[4][0][0] == \ + 'send/m.room.message/0' + assert mock_put.call_args_list[0][0][0] == \ 'http://localhost/_matrix/client/v3/rooms/%21abc123%3Alocalhost/' \ - 'send/m.room.message' + 'send/m.room.message/0' # Attach an unsupported file type attach = AppriseAttachment( @@ -993,7 +1009,7 @@ def test_plugin_matrix_attachments_api_v2(mock_post, mock_get): mock_get.return_value = response # Instantiate our object - obj = Apprise.instantiate('matrix://user:pass@localhost/#general?v=3') + obj = Apprise.instantiate('matrix://user:pass@localhost/#general?v=2') # attach our content attach = AppriseAttachment(os.path.join(TEST_VAR_DIR, 'apprise-test.gif')) @@ -1013,13 +1029,13 @@ def test_plugin_matrix_attachments_api_v2(mock_post, mock_get): # Force a object removal (thus a logout call) del obj + # Instantiate our object + obj = Apprise.instantiate('matrixs://user:pass@localhost/#general?v=2') + # Reset our object mock_post.reset_mock() mock_get.reset_mock() - # Instantiate our object - obj = Apprise.instantiate('matrixs://user:pass@localhost/#general?v=2') - assert obj.notify( body='body', title='title', notify_type=NotifyType.INFO, attach=attach) is True From cd197a97e5082a37051f4fc17517cdcf506a9bd7 Mon Sep 17 00:00:00 2001 From: Chris Caron Date: Sun, 15 Oct 2023 14:57:24 -0400 Subject: [PATCH 3/3] attachment support for v3 removed --- apprise/plugins/NotifyMatrix.py | 68 +++++++++++++++++---------------- test/test_plugin_matrix.py | 66 ++++++++++++++++++++++---------- 2 files changed, 82 insertions(+), 52 deletions(-) diff --git a/apprise/plugins/NotifyMatrix.py b/apprise/plugins/NotifyMatrix.py index e26fcca13f..8f3e77ff99 100644 --- a/apprise/plugins/NotifyMatrix.py +++ b/apprise/plugins/NotifyMatrix.py @@ -151,9 +151,6 @@ class NotifyMatrix(NotifyBase): # Allows the user to specify the NotifyImageSize object image_size = NotifyImageSize.XY_32 - # Matrix is_image check - __matrix_is_image = re.compile(r'^image/.*', re.I) - # The maximum allowable characters allowed in the body per message # https://spec.matrix.org/v1.6/client-server-api/#size-limits # The complete event MUST NOT be larger than 65536 bytes, when formatted @@ -589,7 +586,7 @@ def _send_server_notification(self, body, title='', attachments = None if attach and self.attachment_support: attachments = self._send_attachments(attach) - if not attachments: + if attachments is False: # take an early exit return False @@ -622,32 +619,38 @@ def _send_server_notification(self, body, title='', path = '/rooms/{}/send/m.room.message'.format( NotifyMatrix.quote(room_id)) - if image_url and self.version == MatrixVersion.V2: - # Define our payload - image_payload = { - 'msgtype': 'm.image', - 'url': image_url, - 'body': '{}'.format(notify_type if not title else title), - } - - # Post our content - postokay, response = self._fetch(path, payload=image_payload) - if not postokay: - # Mark our failure - has_error = True - continue - - if attachments: - for attachment in attachments: - if self.version == MatrixVersion.V3: + if self.version == MatrixVersion.V2: + # + # Attachments don't work beyond V2 at this time + # + if image_url: + # Define our payload + image_payload = { + 'msgtype': 'm.image', + 'url': image_url, + 'body': '{}'.format( + notify_type if not title else title), + } + + # Post our content + postokay, response = self._fetch( + path, payload=image_payload) + if not postokay: + # Mark our failure + has_error = True + continue + + if attachments: + for attachment in attachments: attachment['room_id'] = room_id attachment['type'] = 'm.room.message' - postokay, response = self._fetch(path, payload=attachment) - if not postokay: - # Mark our failure - has_error = True - continue + postokay, response = self._fetch( + path, payload=attachment) + if not postokay: + # Mark our failure + has_error = True + continue # Define our payload payload = { @@ -699,6 +702,11 @@ def _send_attachments(self, attach): """ payloads = [] + if self.version != MatrixVersion.V2: + self.logger.warning( + 'Add ?v=2 to Apprise URL to support Attachments') + return next((False for a in attach if not a), []) + for attachment in attach: if not attachment: # invalid attachment (bad file) @@ -721,19 +729,15 @@ def _send_attachments(self, attach): if self.version == MatrixVersion.V3: # Prepare our payload - is_image = self.__matrix_is_image.match(attachment.mimetype) payloads.append({ "body": attachment.name, "info": { "mimetype": attachment.mimetype, "size": len(attachment), }, - "msgtype": "m.image" if is_image else "m.file", + "msgtype": "m.image", "url": response.get('content_uri'), }) - if not is_image: - # Setup `m.file' - payloads[-1]['filename'] = attachment.name else: # Prepare our payload diff --git a/test/test_plugin_matrix.py b/test/test_plugin_matrix.py index 85ab33f4f6..93e42d0154 100644 --- a/test/test_plugin_matrix.py +++ b/test/test_plugin_matrix.py @@ -929,26 +929,21 @@ def test_plugin_matrix_attachments_api_v3(mock_post, mock_get, mock_put): # Test our call count assert mock_put.call_count == 1 - assert mock_post.call_count == 4 + assert mock_post.call_count == 2 assert mock_post.call_args_list[0][0][0] == \ 'http://localhost/_matrix/client/v3/login' assert mock_post.call_args_list[1][0][0] == \ - 'http://localhost/_matrix/media/v3/upload' - assert mock_post.call_args_list[2][0][0] == \ 'http://localhost/_matrix/client/v3/join/%23general%3Alocalhost' - assert mock_post.call_args_list[3][0][0] == \ - 'http://localhost/_matrix/client/v3/rooms/%21abc123%3Alocalhost/' \ - 'send/m.room.message/0' assert mock_put.call_args_list[0][0][0] == \ 'http://localhost/_matrix/client/v3/rooms/%21abc123%3Alocalhost/' \ 'send/m.room.message/0' - # Attach an unsupported file type + # Attach an unsupported file type (it's just skipped) attach = AppriseAttachment( os.path.join(TEST_VAR_DIR, 'apprise-archive.zip')) assert obj.notify( body='body', title='title', notify_type=NotifyType.INFO, - attach=attach) is False + attach=attach) is True # An invalid attachment will cause a failure path = os.path.join(TEST_VAR_DIR, '/invalid/path/to/an/invalid/file.jpg') @@ -965,23 +960,23 @@ def test_plugin_matrix_attachments_api_v3(mock_post, mock_get, mock_put): for side_effect in (requests.RequestException(), OSError(), bad_response): mock_post.side_effect = [side_effect] - # We'll fail now because of our error handling - assert obj.send(body="test", attach=attach) is False + # We'll never fail because files are not attached + assert obj.send(body="test", attach=attach) is True # Throw an exception on the second call to requests.post() for side_effect in (requests.RequestException(), OSError(), bad_response): mock_post.side_effect = [response, side_effect] - # We'll fail now because of our error handling - assert obj.send(body="test", attach=attach) is False + # Attachment support does not exist vor v3 at time, so this will + # work nicely + assert obj.send(body="test", attach=attach) is True # handle a bad response - bad_response = mock.Mock() - bad_response.status_code = requests.codes.internal_server_error mock_post.side_effect = [response, bad_response, response] - # We'll fail now because of an internal exception - assert obj.send(body="test", attach=attach) is False + # Attachment support does not exist vor v3 at time, so this will + # work nicely + assert obj.send(body="test", attach=attach) is True # Force a object removal (thus a logout call) del obj @@ -1055,12 +1050,12 @@ def test_plugin_matrix_attachments_api_v2(mock_post, mock_get): 'https://localhost/_matrix/client/r0/rooms/%21abc123%3Alocalhost/' \ 'send/m.room.message' - # Attach an unsupported file type + # Attach an unsupported file type; these are skipped attach = AppriseAttachment( os.path.join(TEST_VAR_DIR, 'apprise-archive.zip')) assert obj.notify( body='body', title='title', notify_type=NotifyType.INFO, - attach=attach) is False + attach=attach) is True # An invalid attachment will cause a failure path = os.path.join(TEST_VAR_DIR, '/invalid/path/to/an/invalid/file.jpg') @@ -1099,8 +1094,6 @@ def test_plugin_matrix_attachments_api_v2(mock_post, mock_get): assert obj.send(body="test", attach=attach) is False # handle a bad response - bad_response = mock.Mock() - bad_response.status_code = requests.codes.internal_server_error mock_post.side_effect = \ [response, bad_response, response, response, response, response] mock_get.side_effect = \ @@ -1109,5 +1102,38 @@ def test_plugin_matrix_attachments_api_v2(mock_post, mock_get): # We'll fail now because of an internal exception assert obj.send(body="test", attach=attach) is False + # Force a object removal (thus a logout call) + del obj + + # Instantiate our object + obj = Apprise.instantiate( + 'matrixs://user:pass@localhost/#general?v=2&image=y') + + # Reset our object + mock_post.reset_mock() + mock_get.reset_mock() + + mock_post.return_value = None + mock_get.return_value = None + mock_post.side_effect = \ + [response, response, bad_response, response, response, response, + response] + mock_get.side_effect = \ + [response, response, bad_response, response, response, response, + response] + + # image attachment didn't succeed + assert obj.notify( + body='body', title='title', notify_type=NotifyType.INFO) is False + + # Error during image post + mock_post.return_value = response + mock_get.return_value = response + mock_post.side_effect = None + mock_get.side_effect = None + + # We'll fail now because of an internal exception + assert obj.send(body="test", attach=attach) is True + # Force __del__() call del obj