Added attach-as option to form:// for upstream filename over-ride (#827)

pull/831/head
Chris Caron 2023-02-17 16:29:22 -05:00 committed by GitHub
parent f7cc732c31
commit 704f7db53a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 178 additions and 13 deletions

View File

@ -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'])

View File

@ -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

View File

@ -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}'

View File

@ -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

View File

@ -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(

View File

@ -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')

View File

@ -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', {

View File

@ -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', {