diff --git a/apprise/plugins/NotifyMatrix.py b/apprise/plugins/NotifyMatrix.py index 9f0a2c130a..8f3e77ff99 100644 --- a/apprise/plugins/NotifyMatrix.py +++ b/apprise/plugins/NotifyMatrix.py @@ -586,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 @@ -611,31 +611,46 @@ 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 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 self.version == MatrixVersion.V3: + path = '/rooms/{}/send/m.room.message/0'.format( + NotifyMatrix.quote(room_id)) - if attachments: - for attachment in attachments: - postokay, response = self._fetch(path, payload=attachment) - if not postokay: - # Mark our failure - has_error = True - continue + else: + path = '/rooms/{}/send/m.room.message'.format( + NotifyMatrix.quote(room_id)) + + 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 # Define our payload payload = { @@ -667,7 +682,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( @@ -685,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) @@ -705,15 +727,28 @@ 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 + payloads.append({ + "body": attachment.name, + "info": { + "mimetype": attachment.mimetype, + "size": len(attachment), + }, + "msgtype": "m.image", + "url": response.get('content_uri'), + }) + + else: + # Prepare our payload + payloads.append({ + "info": { + "mimetype": attachment.mimetype, + }, + "msgtype": "m.image", + "body": "tta.webp", + "url": response.get('content_uri'), + }) return payloads @@ -780,12 +815,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) @@ -1109,7 +1155,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 @@ -1137,7 +1184,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..93e42d0154 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,26 +928,22 @@ 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 == 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] == \ + assert mock_put.call_args_list[0][0][0] == \ 'http://localhost/_matrix/client/v3/rooms/%21abc123%3Alocalhost/' \ - 'send/m.room.message' - assert mock_post.call_args_list[4][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 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') @@ -949,23 +960,23 @@ def test_plugin_matrix_attachments_api_v3(mock_post, mock_get): 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 @@ -993,7 +1004,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 +1024,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 @@ -1039,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') @@ -1083,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 = \ @@ -1093,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