mirror of
https://github.com/caronc/apprise.git
synced 2025-01-07 14:39:51 +01:00
Apprise API FORM based Attachment Support Added (#877)
This commit is contained in:
parent
b0e64126e6
commit
80ae7228e9
@ -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
|
||||
|
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user