Apprise API FORM based Attachment Support Added (#877)

This commit is contained in:
Chris Caron 2023-05-15 15:21:03 -04:00 committed by GitHub
parent b0e64126e6
commit 80ae7228e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 156 additions and 62 deletions

View File

@ -43,6 +43,20 @@ from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _
class AppriseAPIMethod:
"""
Defines the method to post data tot he remote server
"""
JSON = 'json'
FORM = 'form'
APPRISE_API_METHODS = (
AppriseAPIMethod.FORM,
AppriseAPIMethod.JSON,
)
class NotifyAppriseAPI(NotifyBase):
"""
A wrapper for Apprise (Persistent) API Notifications
@ -65,7 +79,7 @@ class NotifyAppriseAPI(NotifyBase):
# Depending on the number of transactions/notifications taking place, this
# could take a while. 30 seconds should be enough to perform the task
socket_connect_timeout = 30.0
socket_read_timeout = 30.0
# Disable throttle rate for Apprise API requests since they are normally
# local anyway
@ -120,6 +134,12 @@ class NotifyAppriseAPI(NotifyBase):
'name': _('Tags'),
'type': 'string',
},
'method': {
'name': _('Query Method'),
'type': 'choice:string',
'values': APPRISE_API_METHODS,
'default': APPRISE_API_METHODS[0],
},
'to': {
'alias_of': 'token',
},
@ -133,7 +153,8 @@ class NotifyAppriseAPI(NotifyBase):
},
}
def __init__(self, token=None, tags=None, headers=None, **kwargs):
def __init__(self, token=None, tags=None, method=None, headers=None,
**kwargs):
"""
Initialize Apprise API Object
@ -155,6 +176,14 @@ class NotifyAppriseAPI(NotifyBase):
self.logger.warning(msg)
raise TypeError(msg)
self.method = self.template_args['method']['default'] \
if not isinstance(method, str) else method.lower()
if self.method not in APPRISE_API_METHODS:
msg = 'The method specified ({}) is invalid.'.format(method)
self.logger.warning(msg)
raise TypeError(msg)
# Build list of tags
self.__tags = parse_list(tags)
@ -170,8 +199,13 @@ class NotifyAppriseAPI(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
# Our URL parameters
params = self.url_parameters(privacy=privacy, *args, **kwargs)
# Define any URL parameters
params = {
'method': self.method,
}
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
# Append our headers into our parameters
params.update({'+{}'.format(k): v for k, v in self.headers.items()})
@ -219,16 +253,15 @@ class NotifyAppriseAPI(NotifyBase):
# Prepare HTTP Headers
headers = {
'User-Agent': self.app_id,
'Content-Type': 'application/json'
}
# Apply any/all header over-rides defined
headers.update(self.headers)
# Track our potential attachments
attachments = []
files = []
if attach:
for attachment in attach:
for no, attachment in enumerate(attach, start=1):
# Perform some simple error checking
if not attachment:
# We could not access the attachment
@ -238,15 +271,26 @@ class NotifyAppriseAPI(NotifyBase):
return False
try:
with open(attachment.path, 'rb') as f:
# Output must be in a DataURL format (that's what
# PushSafer calls it):
attachments.append({
'filename': attachment.name,
'base64': base64.b64encode(f.read())
.decode('utf-8'),
'mimetype': attachment.mimetype,
})
if self.method == AppriseAPIMethod.JSON:
with open(attachment.path, 'rb') as f:
# Output must be in a DataURL format (that's what
# PushSafer calls it):
attachments.append({
'filename': attachment.name,
'base64': base64.b64encode(f.read())
.decode('utf-8'),
'mimetype': attachment.mimetype,
})
else: # AppriseAPIMethod.FORM
files.append((
'file{:02d}'.format(no),
(
attachment.name,
open(attachment.path, 'rb'),
attachment.mimetype,
)
))
except (OSError, IOError) as e:
self.logger.warning(
@ -262,9 +306,13 @@ class NotifyAppriseAPI(NotifyBase):
'body': body,
'type': notify_type,
'format': self.notify_format,
'attachments': attachments,
}
if self.method == AppriseAPIMethod.JSON:
headers['Content-Type'] = 'application/json'
payload['attachments'] = attachments
payload = dumps(payload)
if self.__tags:
payload['tag'] = self.__tags
@ -285,8 +333,8 @@ class NotifyAppriseAPI(NotifyBase):
# Some entries can not be over-ridden
headers.update({
'User-Agent': self.app_id,
'Content-Type': 'application/json',
# Our response to be in JSON format always
'Accept': 'application/json',
# Pass our Source UUID4 Identifier
'X-Apprise-ID': self.asset._uid,
# Pass our current recursion count to our upstream server
@ -304,9 +352,10 @@ class NotifyAppriseAPI(NotifyBase):
try:
r = requests.post(
url,
data=dumps(payload),
data=payload,
headers=headers,
auth=auth,
files=files if files else None,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
@ -328,7 +377,8 @@ class NotifyAppriseAPI(NotifyBase):
return False
else:
self.logger.info('Sent Apprise API notification.')
self.logger.info(
'Sent Apprise API notification; method=%s.', self.method)
except requests.RequestException as e:
self.logger.warning(
@ -339,6 +389,18 @@ class NotifyAppriseAPI(NotifyBase):
# Return; we're done
return False
except (OSError, IOError) as e:
self.logger.warning(
'An I/O error occurred while reading one of the '
'attached files.')
self.logger.debug('I/O Exception: %s' % str(e))
return False
finally:
for file in files:
# Ensure all files are closed
file[1][1].close()
return True
@staticmethod
@ -415,4 +477,9 @@ class NotifyAppriseAPI(NotifyBase):
# re-assemble our full path
results['fullpath'] = '/'.join(entries)
# Set method if specified
if 'method' in results['qsd'] and len(results['qsd']['method']):
results['method'] = \
NotifyAppriseAPI.unquote(results['qsd']['method'])
return results

View File

@ -142,6 +142,20 @@ apprise_url_tests = (
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'apprises://localhost:8080/m...4/',
}),
('apprises://localhost:8080/abc123/?method=json', {
'instance': NotifyAppriseAPI,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'apprises://localhost:8080/a...3/',
}),
('apprises://localhost:8080/abc123/?method=form', {
'instance': NotifyAppriseAPI,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'apprises://localhost:8080/a...3/',
}),
# Invalid method specified
('apprises://localhost:8080/abc123/?method=invalid', {
'instance': TypeError,
}),
('apprises://user:password@localhost:8080/mytoken5/', {
'instance': NotifyAppriseAPI,
@ -190,52 +204,65 @@ def test_notify_apprise_api_attachments(mock_post):
"""
okay_response = requests.Request()
okay_response.status_code = requests.codes.ok
okay_response.content = ""
# Assign our mock object our return value
mock_post.return_value = okay_response
for method in ('json', 'form'):
okay_response.status_code = requests.codes.ok
okay_response.content = ""
obj = Apprise.instantiate('apprise://user@localhost/mytoken1/')
assert isinstance(obj, NotifyAppriseAPI)
# Assign our mock object our return value
mock_post.return_value = okay_response
# Test 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
obj = Apprise.instantiate(
'apprise://user@localhost/mytoken1/?method={}'.format(method))
assert isinstance(obj, NotifyAppriseAPI)
# Test invalid attachment
path = os.path.join(TEST_VAR_DIR, '/invalid/path/to/an/invalid/file.jpg')
assert obj.notify(
body='body', title='title', notify_type=NotifyType.INFO,
attach=path) is False
# Test Valid Attachment (load 3)
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)
# Return our good configuration
mock_post.side_effect = None
mock_post.return_value = okay_response
with mock.patch('builtins.open', side_effect=OSError()):
# We can't send the message we can't open the attachment for reading
# Test 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 False
attach=attach) is True
# test the handling of our batch modes
obj = Apprise.instantiate('apprise://user@localhost/mytoken1/')
assert isinstance(obj, NotifyAppriseAPI)
# Test invalid attachment
path = os.path.join(
TEST_VAR_DIR, '/invalid/path/to/an/invalid/file.jpg')
assert obj.notify(
body='body', title='title', notify_type=NotifyType.INFO,
attach=path) is False
# Now send an attachment normally without issues
mock_post.reset_mock()
assert obj.notify(
body='body', title='title', notify_type=NotifyType.INFO,
attach=attach) is True
assert mock_post.call_count == 1
# Test Valid Attachment (load 3)
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)
# Return our good configuration
mock_post.side_effect = None
mock_post.return_value = okay_response
with mock.patch('builtins.open', side_effect=OSError()):
# We can't send the message we can't open the attachment for
# reading
assert obj.notify(
body='body', title='title', notify_type=NotifyType.INFO,
attach=attach) is False
with mock.patch('requests.post', side_effect=OSError()):
# Attachment issue
assert obj.notify(
body='body', title='title', notify_type=NotifyType.INFO,
attach=attach) is False
# test the handling of our batch modes
obj = Apprise.instantiate('apprise://user@localhost/mytoken1/')
assert isinstance(obj, NotifyAppriseAPI)
# Now send an attachment normally without issues
mock_post.reset_mock()
assert obj.notify(
body='body', title='title', notify_type=NotifyType.INFO,
attach=attach) is True
assert mock_post.call_count == 1
mock_post.reset_mock()