Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Apprise API FORM based Attachment Support Added #877

Merged
merged 1 commit into from
May 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 88 additions & 21 deletions apprise/plugins/NotifyAppriseAPI.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,20 @@
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
Expand All @@ -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
Expand Down Expand Up @@ -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',
},
Expand All @@ -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

Expand All @@ -155,6 +176,14 @@ def __init__(self, token=None, tags=None, headers=None, **kwargs):
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)

Expand All @@ -170,8 +199,13 @@ def url(self, privacy=False, *args, **kwargs):
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()})
Expand Down Expand Up @@ -219,16 +253,15 @@ def send(self, body, title='', notify_type=NotifyType.INFO, attach=None,
# 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
Expand All @@ -238,15 +271,26 @@ def send(self, body, title='', notify_type=NotifyType.INFO, attach=None,
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(
Expand All @@ -262,9 +306,13 @@ def send(self, body, title='', notify_type=NotifyType.INFO, attach=None,
'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

Expand All @@ -285,8 +333,8 @@ def send(self, body, title='', notify_type=NotifyType.INFO, attach=None,

# 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
Expand All @@ -304,9 +352,10 @@ def send(self, body, title='', notify_type=NotifyType.INFO, attach=None,
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,
)
Expand All @@ -328,7 +377,8 @@ def send(self, body, title='', notify_type=NotifyType.INFO, attach=None,
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(
Expand All @@ -339,6 +389,18 @@ def send(self, body, title='', notify_type=NotifyType.INFO, attach=None,
# 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
Expand Down Expand Up @@ -415,4 +477,9 @@ def parse_url(url):
# 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
121 changes: 74 additions & 47 deletions test/test_plugin_apprise_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,20 @@
# 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,

Expand Down Expand Up @@ -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

obj = Apprise.instantiate('apprise://user@localhost/mytoken1/')
assert isinstance(obj, NotifyAppriseAPI)

# 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

# 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

for method in ('json', 'form'):
okay_response.status_code = requests.codes.ok
okay_response.content = ""

# Assign our mock object our return value
mock_post.return_value = okay_response

obj = Apprise.instantiate(
'apprise://user@localhost/mytoken1/?method={}'.format(method))
assert isinstance(obj, NotifyAppriseAPI)

# 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

# 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
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 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
attach=attach) is True
assert mock_post.call_count == 1
mock_post.reset_mock()