From 704f7db53a52bf60b1e8e3a5d55dd0ce692b9489 Mon Sep 17 00:00:00 2001 From: Chris Caron <lead2gold@gmail.com> Date: Fri, 17 Feb 2023 16:29:22 -0500 Subject: [PATCH] Added attach-as option to form:// for upstream filename over-ride (#827) --- apprise/plugins/NotifyForm.py | 84 +++++++++++++++++++++++++++++++-- apprise/plugins/NotifyJSON.py | 6 ++- apprise/plugins/NotifyVoipms.py | 7 +-- apprise/plugins/NotifyXML.py | 6 ++- test/helpers/rest.py | 12 ++++- test/test_plugin_custom_form.py | 70 +++++++++++++++++++++++++++ test/test_plugin_custom_json.py | 3 ++ test/test_plugin_custom_xml.py | 3 ++ 8 files changed, 178 insertions(+), 13 deletions(-) diff --git a/apprise/plugins/NotifyForm.py b/apprise/plugins/NotifyForm.py index ccf7e3a5..b14ae5ef 100644 --- a/apprise/plugins/NotifyForm.py +++ b/apprise/plugins/NotifyForm.py @@ -30,6 +30,7 @@ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. +import re import requests from .NotifyBase import NotifyBase @@ -45,7 +46,8 @@ METHODS = ( 'GET', 'DELETE', 'PUT', - 'HEAD' + 'HEAD', + 'PATCH' ) @@ -54,6 +56,27 @@ class NotifyForm(NotifyBase): A wrapper for Form Notifications """ + # Support + # - file* + # - file? + # - file*name + # - file?name + # - ?file + # - *file + # - file + # The code will convert the ? or * to the digit increments + __attach_as_re = re.compile( + r'((?P<match1>(?P<id1a>[a-z0-9_-]+)?' + r'(?P<wc1>[*?+$:.%]+)(?P<id1b>[a-z0-9_-]+))' + r'|(?P<match2>(?P<id2>[a-z0-9_-]+)(?P<wc2>[*?+$:.%]?)))', + re.IGNORECASE) + + # Our count + attach_as_count = '{:02d}' + + # the default attach_as value + attach_as_default = f'file{attach_as_count}' + # The default descriptive name associated with the Notification service_name = 'Form' @@ -118,6 +141,12 @@ class NotifyForm(NotifyBase): 'values': METHODS, 'default': METHODS[0], }, + 'attach-as': { + 'name': _('Attach File As'), + 'type': 'string', + 'default': 'file*', + 'map_to': 'attach_as', + }, }) # Define any kwargs we're using @@ -137,7 +166,7 @@ class NotifyForm(NotifyBase): } def __init__(self, headers=None, method=None, payload=None, params=None, - **kwargs): + attach_as=None, **kwargs): """ Initialize Form Object @@ -159,6 +188,36 @@ class NotifyForm(NotifyBase): self.logger.warning(msg) raise TypeError(msg) + # Custom File Attachment Over-Ride Support + if not isinstance(attach_as, str): + # Default value + self.attach_as = self.attach_as_default + self.attach_multi_support = True + + else: + result = self.__attach_as_re.match(attach_as.strip()) + if not result: + msg = 'The attach-as specified ({}) is invalid.'.format( + attach_as) + self.logger.warning(msg) + raise TypeError(msg) + + self.attach_as = '' + self.attach_multi_support = False + if result.group('match1'): + if result.group('id1a'): + self.attach_as += result.group('id1a') + + self.attach_as += self.attach_as_count + self.attach_multi_support = True + self.attach_as += result.group('id1b') + + else: # result.group('match2'): + self.attach_as += result.group('id2') + if result.group('wc2'): + self.attach_as += self.attach_as_count + self.attach_multi_support = True + self.params = {} if params: # Store our extra headers @@ -199,6 +258,10 @@ class NotifyForm(NotifyBase): params.update( {':{}'.format(k): v for k, v in self.payload_extras.items()}) + if self.attach_as != self.attach_as_default: + # Provide Attach-As extension details + params['attach-as'] = self.attach_as + # Determine Authentication auth = '' if self.user and self.password: @@ -254,7 +317,8 @@ class NotifyForm(NotifyBase): try: files.append(( - 'file{:02d}'.format(no), ( + self.attach_as.format(no) + if self.attach_multi_support else self.attach_as, ( attachment.name, open(attachment.path, 'rb'), attachment.mimetype) @@ -267,6 +331,11 @@ class NotifyForm(NotifyBase): self.logger.debug('I/O Exception: %s' % str(e)) return False + if not self.attach_multi_support and no > 1: + self.logger.warning( + 'Multiple attachments provided while ' + 'form:// Multi-Attachment Support not enabled') + # prepare Form Object payload = { # Version: Major.Minor, Major is only updated if the entire @@ -309,6 +378,9 @@ class NotifyForm(NotifyBase): elif self.method == 'PUT': method = requests.put + elif self.method == 'PATCH': + method = requests.patch + elif self.method == 'DELETE': method = requests.delete @@ -397,6 +469,12 @@ class NotifyForm(NotifyBase): results['params'] = {NotifyForm.unquote(x): NotifyForm.unquote(y) for x, y in results['qsd-'].items()} + # Allow Attach-As Support which over-rides the name of the filename + # posted with the form:// + # the default is file01, file02, file03, etc + if 'attach-as' in results['qsd'] and len(results['qsd']['attach-as']): + results['attach_as'] = results['qsd']['attach-as'] + # Set method if not otherwise set if 'method' in results['qsd'] and len(results['qsd']['method']): results['method'] = NotifyForm.unquote(results['qsd']['method']) diff --git a/apprise/plugins/NotifyJSON.py b/apprise/plugins/NotifyJSON.py index 7af4939b..509c7627 100644 --- a/apprise/plugins/NotifyJSON.py +++ b/apprise/plugins/NotifyJSON.py @@ -47,7 +47,8 @@ METHODS = ( 'GET', 'DELETE', 'PUT', - 'HEAD' + 'HEAD', + 'PATCH' ) @@ -315,6 +316,9 @@ class NotifyJSON(NotifyBase): elif self.method == 'PUT': method = requests.put + elif self.method == 'PATCH': + method = requests.patch + elif self.method == 'DELETE': method = requests.delete diff --git a/apprise/plugins/NotifyVoipms.py b/apprise/plugins/NotifyVoipms.py index 8580ce50..42379b6b 100644 --- a/apprise/plugins/NotifyVoipms.py +++ b/apprise/plugins/NotifyVoipms.py @@ -316,12 +316,7 @@ class NotifyVoipms(NotifyBase): """ # Define any URL parameters - params = { - 'method': 'sendSMS' - } - - # Extend our parameters - params.update(self.url_parameters(privacy=privacy, *args, **kwargs)) + params = self.url_parameters(privacy=privacy, *args, **kwargs) schemaStr = \ '{schema}://{password}:{email}/{from_phone}/{targets}/?{params}' diff --git a/apprise/plugins/NotifyXML.py b/apprise/plugins/NotifyXML.py index 8f71892e..bbb3046a 100644 --- a/apprise/plugins/NotifyXML.py +++ b/apprise/plugins/NotifyXML.py @@ -47,7 +47,8 @@ METHODS = ( 'GET', 'DELETE', 'PUT', - 'HEAD' + 'HEAD', + 'PATCH' ) @@ -367,6 +368,9 @@ class NotifyXML(NotifyBase): elif self.method == 'PUT': method = requests.put + elif self.method == 'PATCH': + method = requests.patch + elif self.method == 'DELETE': method = requests.delete diff --git a/test/helpers/rest.py b/test/helpers/rest.py index 624dfcd1..672a6b7e 100644 --- a/test/helpers/rest.py +++ b/test/helpers/rest.py @@ -268,8 +268,9 @@ class AppriseURLTester: @mock.patch('requests.head') @mock.patch('requests.put') @mock.patch('requests.delete') - def __notify(self, url, obj, meta, asset, mock_del, mock_put, mock_head, - mock_post, mock_get): + @mock.patch('requests.patch') + def __notify(self, url, obj, meta, asset, mock_patch, mock_del, mock_put, + mock_head, mock_post, mock_get): """ Perform notification testing against object specified """ @@ -326,6 +327,7 @@ class AppriseURLTester: mock_get.return_value = robj mock_post.return_value = robj mock_head.return_value = robj + mock_patch.return_value = robj mock_del.return_value = robj mock_put.return_value = robj @@ -336,6 +338,7 @@ class AppriseURLTester: mock_del.return_value.status_code = requests_response_code mock_post.return_value.status_code = requests_response_code mock_get.return_value.status_code = requests_response_code + mock_patch.return_value.status_code = requests_response_code # Handle our default text response mock_get.return_value.content = requests_response_content @@ -343,12 +346,14 @@ class AppriseURLTester: mock_del.return_value.content = requests_response_content mock_put.return_value.content = requests_response_content mock_head.return_value.content = requests_response_content + mock_patch.return_value.content = requests_response_content mock_get.return_value.text = requests_response_text mock_post.return_value.text = requests_response_text mock_put.return_value.text = requests_response_text mock_del.return_value.text = requests_response_text mock_head.return_value.text = requests_response_text + mock_patch.return_value.text = requests_response_text # Ensure there is no side effect set mock_post.side_effect = None @@ -356,6 +361,7 @@ class AppriseURLTester: mock_put.side_effect = None mock_head.side_effect = None mock_get.side_effect = None + mock_patch.side_effect = None else: # Handle exception testing; first we turn the boolean flag @@ -454,6 +460,7 @@ class AppriseURLTester: mock_del.side_effect = _exception mock_put.side_effect = _exception mock_get.side_effect = _exception + mock_patch.side_effect = _exception try: assert obj.notify( @@ -498,6 +505,7 @@ class AppriseURLTester: mock_put.side_effect = _exception mock_head.side_effect = _exception mock_get.side_effect = _exception + mock_patch.side_effect = _exception try: assert obj.notify( diff --git a/test/test_plugin_custom_form.py b/test/test_plugin_custom_form.py index bee440ff..138a0196 100644 --- a/test/test_plugin_custom_form.py +++ b/test/test_plugin_custom_form.py @@ -91,6 +91,9 @@ apprise_url_tests = ( ('form://user@localhost?method=delete', { 'instance': NotifyForm, }), + ('form://user@localhost?method=patch', { + 'instance': NotifyForm, + }), # Custom payload options ('form://localhost:8080?:key=value&:key2=value2', { @@ -230,6 +233,73 @@ def test_plugin_custom_form_attachments(mock_post): body='body', title='title', notify_type=NotifyType.INFO, attach=attach) is False + # + # Test attach-as + # + + # Assign our mock object our return value + mock_post.return_value = okay_response + mock_post.side_effect = None + + obj = Apprise.instantiate( + 'form://user@localhost.localdomain/?attach-as=file') + assert isinstance(obj, NotifyForm) + + # Test Single Valid Attachment + path = os.path.join(TEST_VAR_DIR, 'apprise-test.gif') + attach = AppriseAttachment(path) + assert obj.notify( + body='body', title='title', notify_type=NotifyType.INFO, + attach=attach) is True + + # Test Valid Attachment (load 3) (produces a warning) + path = ( + os.path.join(TEST_VAR_DIR, 'apprise-test.gif'), + os.path.join(TEST_VAR_DIR, 'apprise-test.gif'), + os.path.join(TEST_VAR_DIR, 'apprise-test.gif'), + ) + attach = AppriseAttachment(path) + assert obj.notify( + body='body', title='title', notify_type=NotifyType.INFO, + attach=attach) is True + + # Test our other variations of accepted values + # we support *, :, ?, ., +, %, and $ + for attach_as in ( + 'file*', '*file', 'file*file', + 'file:', ':file', 'file:file', + 'file?', '?file', 'file?file', + 'file.', '.file', 'file.file', + 'file+', '+file', 'file+file', + 'file$', '$file', 'file$file'): + + obj = Apprise.instantiate( + f'form://user@localhost.localdomain/?attach-as={attach_as}') + assert isinstance(obj, NotifyForm) + + # Test Single Valid Attachment + path = os.path.join(TEST_VAR_DIR, 'apprise-test.gif') + attach = AppriseAttachment(path) + assert obj.notify( + body='body', title='title', notify_type=NotifyType.INFO, + attach=attach) is True + + # Test Valid Attachment (load 3) (produces a warning) + path = ( + os.path.join(TEST_VAR_DIR, 'apprise-test.gif'), + os.path.join(TEST_VAR_DIR, 'apprise-test.gif'), + os.path.join(TEST_VAR_DIR, 'apprise-test.gif'), + ) + attach = AppriseAttachment(path) + assert obj.notify( + body='body', title='title', notify_type=NotifyType.INFO, + attach=attach) is True + + # Test invalid attach-as input + obj = Apprise.instantiate( + 'form://user@localhost.localdomain/?attach-as={') + assert obj is None + @mock.patch('requests.post') @mock.patch('requests.get') diff --git a/test/test_plugin_custom_json.py b/test/test_plugin_custom_json.py index ccbb2135..459776ef 100644 --- a/test/test_plugin_custom_json.py +++ b/test/test_plugin_custom_json.py @@ -93,6 +93,9 @@ apprise_url_tests = ( ('json://user@localhost?method=delete', { 'instance': NotifyJSON, }), + ('json://user@localhost?method=patch', { + 'instance': NotifyJSON, + }), # Continue testing other cases ('json://localhost:8080', { diff --git a/test/test_plugin_custom_xml.py b/test/test_plugin_custom_xml.py index ea0e7b07..69fc14bf 100644 --- a/test/test_plugin_custom_xml.py +++ b/test/test_plugin_custom_xml.py @@ -92,6 +92,9 @@ apprise_url_tests = ( ('xml://user@localhost?method=delete', { 'instance': NotifyXML, }), + ('xml://user@localhost?method=patch', { + 'instance': NotifyXML, + }), # Continue testing other cases ('xml://localhost:8080', {