mirror of
https://github.com/caronc/apprise-api.git
synced 2024-12-04 14:03:16 +01:00
attachment keyword in payload now supports Web Based URLs (#164)
This commit is contained in:
parent
68fd580b84
commit
2b21ab71ef
31
README.md
31
README.md
@ -147,16 +147,47 @@ curl -X POST \
|
||||
-F attach2=@/my/path/to/Apprise.doc \
|
||||
http://localhost:8000/notify
|
||||
|
||||
# This example shows how you can place the body among other parameters
|
||||
# in the GET parameter and not the payload as another option.
|
||||
curl -X POST -d 'urls=mailto://user:pass@gmail.com&body=test message' \
|
||||
-F @/path/to/your/attachment \
|
||||
http://localhost:8000/notify
|
||||
|
||||
# The body is not required if an attachment is provided:
|
||||
curl -X POST -d 'urls=mailto://user:pass@gmail.com' \
|
||||
-F @/path/to/your/attachment \
|
||||
http://localhost:8000/notify
|
||||
|
||||
# Send your notifications directly using JSON
|
||||
curl -X POST -d '{"urls": "mailto://user:pass@gmail.com", "body":"test message"}' \
|
||||
-H "Content-Type: application/json" \
|
||||
http://localhost:8000/notify
|
||||
```
|
||||
|
||||
You can also send notifications that are URLs. Apprise will download the item so that it can send it along to all end points that should be notified about it.
|
||||
```bash
|
||||
# Use the 'attachment' parameter and send along a web request
|
||||
curl -X POST \
|
||||
-F 'urls=mailto://user:pass@gmail.com' \
|
||||
-F attachment=https://i.redd.it/my2t4d2fx0u31.jpg \
|
||||
http://localhost:8000/notify
|
||||
|
||||
# To send more then one URL, the following would work:
|
||||
curl -X POST \
|
||||
-F 'urls=mailto://user:pass@gmail.com' \
|
||||
-F attachment=https://i.redd.it/my2t4d2fx0u31.jpg \
|
||||
-F attachment=https://path/to/another/remote/file.pdf \
|
||||
http://localhost:8000/notify
|
||||
|
||||
# Finally feel free to mix and match local files with external ones:
|
||||
curl -X POST \
|
||||
-F 'urls=mailto://user:pass@gmail.com' \
|
||||
-F attachment=https://i.redd.it/my2t4d2fx0u31.jpg \
|
||||
-F attachment=https://path/to/another/remote/file.pdf \
|
||||
-F @/path/to/your/local/file/attachment \
|
||||
http://localhost:8000/notify
|
||||
```
|
||||
|
||||
### Persistent Storage Solution
|
||||
|
||||
You can pre-save all of your Apprise configuration and/or set of Apprise URLs and associate them with a `{KEY}` of your choosing. Once set, the configuration persists for retrieval by the `apprise` [CLI tool](https://github.com/caronc/apprise/wiki/CLI_Usage) or any other custom integration you've set up. The built in website with comes with a user interface that you can use to leverage these API calls as well. Those who wish to build their own application around this can use the following API end points:
|
||||
|
@ -22,12 +22,21 @@
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
import requests
|
||||
from django.test import SimpleTestCase
|
||||
from unittest import mock
|
||||
from ..utils import Attachment
|
||||
from unittest.mock import mock_open
|
||||
from ..utils import Attachment, HTTPAttachment
|
||||
from ..utils import parse_attachments
|
||||
from django.test.utils import override_settings
|
||||
from tempfile import TemporaryDirectory
|
||||
from shutil import rmtree
|
||||
import base64
|
||||
from unittest.mock import patch
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from os.path import dirname, join, getsize
|
||||
|
||||
SAMPLE_FILE = join(dirname(dirname(dirname(__file__))), 'static', 'logo.png')
|
||||
|
||||
|
||||
class AttachmentTests(SimpleTestCase):
|
||||
@ -54,10 +63,324 @@ class AttachmentTests(SimpleTestCase):
|
||||
with mock.patch('os.makedirs', side_effect=OSError):
|
||||
with self.assertRaises(ValueError):
|
||||
Attachment('file')
|
||||
with self.assertRaises(ValueError):
|
||||
HTTPAttachment('web')
|
||||
|
||||
with mock.patch('tempfile.mkstemp', side_effect=FileNotFoundError):
|
||||
with self.assertRaises(ValueError):
|
||||
Attachment('file')
|
||||
with self.assertRaises(ValueError):
|
||||
HTTPAttachment('web')
|
||||
|
||||
with mock.patch('os.remove', side_effect=FileNotFoundError):
|
||||
a = Attachment('file')
|
||||
# Force __del__ call to throw an exception which we gracefully
|
||||
# handle
|
||||
del a
|
||||
|
||||
a = HTTPAttachment('web')
|
||||
a._path = 'abcd'
|
||||
assert a.filename == 'web'
|
||||
# Force __del__ call to throw an exception which we gracefully
|
||||
# handle
|
||||
del a
|
||||
|
||||
a = Attachment('file')
|
||||
assert a.filename
|
||||
|
||||
def test_form_file_attachment_parsing(self):
|
||||
"""
|
||||
Test the parsing of file attachments
|
||||
"""
|
||||
# Get ourselves a file to work with
|
||||
|
||||
files_request = {
|
||||
'file1': SimpleUploadedFile(
|
||||
"attach.txt", b"content here", content_type="text/plain")
|
||||
}
|
||||
result = parse_attachments(None, files_request)
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 1
|
||||
|
||||
# Test case where no filename was specified
|
||||
files_request = {
|
||||
'file1': SimpleUploadedFile(
|
||||
" ", b"content here", content_type="text/plain")
|
||||
}
|
||||
result = parse_attachments(None, files_request)
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 1
|
||||
|
||||
# Test our case where we throw an error trying to open/read/write our
|
||||
# attachment to disk
|
||||
m = mock_open()
|
||||
m.side_effect = OSError()
|
||||
with patch('builtins.open', m):
|
||||
with self.assertRaises(ValueError):
|
||||
parse_attachments(None, files_request)
|
||||
|
||||
# Test a case where our attachment exceeds the maximum size we allow
|
||||
# for
|
||||
with override_settings(APPRISE_MAX_ATTACHMENT_SIZE=1):
|
||||
files_request = {
|
||||
'file1': SimpleUploadedFile(
|
||||
"attach.txt",
|
||||
b"some content more then 1 byte in length to pass along.",
|
||||
content_type="text/plain")
|
||||
}
|
||||
with self.assertRaises(ValueError):
|
||||
parse_attachments(None, files_request)
|
||||
|
||||
# Bad data provided in filename field
|
||||
files_request = {
|
||||
'file1': SimpleUploadedFile(
|
||||
None, b"content here", content_type="text/plain")
|
||||
}
|
||||
with self.assertRaises(ValueError):
|
||||
parse_attachments(None, files_request)
|
||||
|
||||
@patch('requests.get')
|
||||
def test_direct_attachment_parsing(self, mock_get):
|
||||
"""
|
||||
Test the parsing of file attachments
|
||||
"""
|
||||
# Test the processing of file attachments
|
||||
result = parse_attachments([], {})
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 0
|
||||
|
||||
# Response object
|
||||
response = mock.Mock()
|
||||
response.status_code = requests.codes.ok
|
||||
response.raise_for_status.return_value = True
|
||||
response.headers = {
|
||||
'Content-Length': getsize(SAMPLE_FILE),
|
||||
}
|
||||
ref = {
|
||||
'io': None,
|
||||
}
|
||||
|
||||
def iter_content(chunk_size=1024, *args, **kwargs):
|
||||
if not ref['io']:
|
||||
ref['io'] = open(SAMPLE_FILE)
|
||||
block = ref['io'].read(chunk_size)
|
||||
if not block:
|
||||
# Close for re-use
|
||||
ref['io'].close()
|
||||
ref['io'] = None
|
||||
yield block
|
||||
response.iter_content = iter_content
|
||||
|
||||
def test(*args, **kwargs):
|
||||
return response
|
||||
response.__enter__ = test
|
||||
response.__exit__ = test
|
||||
mock_get.return_value = response
|
||||
|
||||
# Support base64 encoding
|
||||
attachment_payload = {
|
||||
'base64': base64.b64encode(b'data to be encoded').decode('utf-8')
|
||||
}
|
||||
result = parse_attachments(attachment_payload, {})
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 1
|
||||
|
||||
# Support multi entries
|
||||
attachment_payload = [
|
||||
{
|
||||
'base64': base64.b64encode(
|
||||
b'data to be encoded 1').decode('utf-8'),
|
||||
}, {
|
||||
'base64': base64.b64encode(
|
||||
b'data to be encoded 2').decode('utf-8'),
|
||||
}, {
|
||||
'base64': base64.b64encode(
|
||||
b'data to be encoded 3').decode('utf-8'),
|
||||
}
|
||||
]
|
||||
result = parse_attachments(attachment_payload, {})
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 3
|
||||
|
||||
# Support multi entries
|
||||
attachment_payload = [
|
||||
{
|
||||
'url': 'http://localhost/my.attachment.3',
|
||||
}, {
|
||||
'url': 'http://localhost/my.attachment.2',
|
||||
}, {
|
||||
'url': 'http://localhost/my.attachment.1',
|
||||
}
|
||||
]
|
||||
result = parse_attachments(attachment_payload, {})
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 3
|
||||
|
||||
# Garbage handling (integer, float, object, etc is invalid)
|
||||
attachment_payload = 5
|
||||
result = parse_attachments(attachment_payload, {})
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 0
|
||||
attachment_payload = 5.5
|
||||
result = parse_attachments(attachment_payload, {})
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 0
|
||||
attachment_payload = object()
|
||||
result = parse_attachments(attachment_payload, {})
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 0
|
||||
|
||||
# filename provided, but its empty (and/or contains whitespace)
|
||||
attachment_payload = {
|
||||
'base64': base64.b64encode(b'data to be encoded').decode('utf-8'),
|
||||
'filename': ' '
|
||||
}
|
||||
result = parse_attachments(attachment_payload, {})
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 1
|
||||
|
||||
# filename too long
|
||||
attachment_payload = {
|
||||
'base64': base64.b64encode(b'data to be encoded').decode('utf-8'),
|
||||
'filename': 'a' * 1000,
|
||||
}
|
||||
with self.assertRaises(ValueError):
|
||||
parse_attachments(attachment_payload, {})
|
||||
|
||||
# filename invalid
|
||||
attachment_payload = {
|
||||
'base64': base64.b64encode(b'data to be encoded').decode('utf-8'),
|
||||
'filename': 1,
|
||||
}
|
||||
with self.assertRaises(ValueError):
|
||||
parse_attachments(attachment_payload, {})
|
||||
|
||||
attachment_payload = {
|
||||
'base64': base64.b64encode(b'data to be encoded').decode('utf-8'),
|
||||
'filename': None,
|
||||
}
|
||||
with self.assertRaises(ValueError):
|
||||
parse_attachments(attachment_payload, {})
|
||||
|
||||
attachment_payload = {
|
||||
'base64': base64.b64encode(b'data to be encoded').decode('utf-8'),
|
||||
'filename': object(),
|
||||
}
|
||||
with self.assertRaises(ValueError):
|
||||
parse_attachments(attachment_payload, {})
|
||||
|
||||
# List Entry with bad data
|
||||
attachment_payload = [
|
||||
None,
|
||||
]
|
||||
with self.assertRaises(ValueError):
|
||||
parse_attachments(attachment_payload, {})
|
||||
|
||||
# We expect at least a 'base64' or something in our dict
|
||||
attachment_payload = [
|
||||
{},
|
||||
]
|
||||
with self.assertRaises(ValueError):
|
||||
parse_attachments(attachment_payload, {})
|
||||
|
||||
# We can't parse entries that are not base64 but specified as
|
||||
# though they are
|
||||
attachment_payload = {
|
||||
'base64': 'not-base-64',
|
||||
}
|
||||
with self.assertRaises(ValueError):
|
||||
parse_attachments(attachment_payload, {})
|
||||
|
||||
# Support string; these become web requests
|
||||
attachment_payload = \
|
||||
"https://avatars.githubusercontent.com/u/850374?v=4"
|
||||
result = parse_attachments(attachment_payload, {})
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 1
|
||||
|
||||
# Local files are not allowed
|
||||
attachment_payload = "file:///etc/hosts"
|
||||
with self.assertRaises(ValueError):
|
||||
parse_attachments(attachment_payload, {})
|
||||
attachment_payload = "/etc/hosts"
|
||||
with self.assertRaises(ValueError):
|
||||
parse_attachments(attachment_payload, {})
|
||||
attachment_payload = "simply invalid"
|
||||
with self.assertRaises(ValueError):
|
||||
parse_attachments(attachment_payload, {})
|
||||
|
||||
# Test our case where we throw an error trying to write our attachment
|
||||
# to disk
|
||||
m = mock_open()
|
||||
m.side_effect = OSError()
|
||||
with patch('builtins.open', m):
|
||||
with self.assertRaises(ValueError):
|
||||
attachment_payload = b"some data to work with."
|
||||
parse_attachments(attachment_payload, {})
|
||||
|
||||
# Test a case where our attachment exceeds the maximum size we allow
|
||||
# for
|
||||
with override_settings(APPRISE_MAX_ATTACHMENT_SIZE=1):
|
||||
attachment_payload = \
|
||||
b"some content more then 1 byte in length to pass along."
|
||||
with self.assertRaises(ValueError):
|
||||
parse_attachments(attachment_payload, {})
|
||||
|
||||
# Support byte data
|
||||
attachment_payload = b"some content to pass along as an attachment."
|
||||
result = parse_attachments(attachment_payload, {})
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 1
|
||||
|
||||
attachment_payload = [
|
||||
# Request several images
|
||||
"https://localhost/myotherfile.png",
|
||||
"https://localhost/myfile.png"
|
||||
]
|
||||
result = parse_attachments(attachment_payload, {})
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 2
|
||||
|
||||
attachment_payload = [{
|
||||
# Request several images
|
||||
'url': "https://localhost/myotherfile.png",
|
||||
}, {
|
||||
'url': "https://localhost/myfile.png"
|
||||
}]
|
||||
result = parse_attachments(attachment_payload, {})
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 2
|
||||
|
||||
# Test pure binary payload (raw)
|
||||
attachment_payload = [
|
||||
b"some content to pass along as an attachment.",
|
||||
b"some more content to pass along as an attachment.",
|
||||
]
|
||||
result = parse_attachments(attachment_payload, {})
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 2
|
||||
|
||||
def test_direct_attachment_parsing_nw(self):
|
||||
"""
|
||||
Test the parsing of file attachments with network availability
|
||||
We test web requests that do not work or in accessible to access
|
||||
this part of the test cases
|
||||
"""
|
||||
attachment_payload = [
|
||||
# While we have a network in place, we're intentionally requesting
|
||||
# URLs that do not exist (hopefully they don't anyway) as we want
|
||||
# this test to fail.
|
||||
"https://localhost/garbage/abcd1.png",
|
||||
"https://localhost/garbage/abcd2.png",
|
||||
]
|
||||
with self.assertRaises(ValueError):
|
||||
parse_attachments(attachment_payload, {})
|
||||
|
||||
# Support url encoding
|
||||
attachment_payload = [{
|
||||
'url': "https://localhost/garbage/abcd1.png",
|
||||
}, {
|
||||
'url': "https://localhost/garbage/abcd2.png",
|
||||
}]
|
||||
with self.assertRaises(ValueError):
|
||||
parse_attachments(attachment_payload, {})
|
||||
|
@ -828,6 +828,25 @@ class NotifyTests(SimpleTestCase):
|
||||
assert response.status_code == 424
|
||||
assert mock_notify.call_count == 2
|
||||
|
||||
# One more test but we test our URL fetching
|
||||
mock_notify.side_effect = True
|
||||
|
||||
# Reset our mock object
|
||||
mock_notify.reset_mock()
|
||||
|
||||
# Preare our form data
|
||||
form_data = {
|
||||
'body': 'test notifiction',
|
||||
'attachment': 'https://localhost/invalid/path/to/image.png',
|
||||
}
|
||||
|
||||
# Send our notification
|
||||
response = self.client.post(
|
||||
'/notify/{}'.format(key), form_data)
|
||||
# We fail because we couldn't retrieve our attachment
|
||||
assert response.status_code == 400
|
||||
assert mock_notify.call_count == 0
|
||||
|
||||
@mock.patch('apprise.Apprise.notify')
|
||||
def test_notify_by_loaded_urls_with_json(self, mock_notify):
|
||||
"""
|
||||
|
@ -231,6 +231,25 @@ class StatelessNotifyTests(SimpleTestCase):
|
||||
assert response.status_code == 424
|
||||
assert mock_notify.call_count == 2
|
||||
|
||||
# Reset our mock object
|
||||
mock_notify.reset_mock()
|
||||
|
||||
# Preare our form data
|
||||
form_data = {
|
||||
'body': 'test notifiction',
|
||||
'urls': ', '.join([
|
||||
'mailto://user:pass@hotmail.com',
|
||||
'mailto://user:pass@gmail.com',
|
||||
]),
|
||||
'attachment': 'https://localhost/invalid/path/to/image.png',
|
||||
}
|
||||
|
||||
# Send our notification
|
||||
response = self.client.post('/notify', form_data)
|
||||
# We fail because we couldn't retrieve our attachment
|
||||
assert response.status_code == 400
|
||||
assert mock_notify.call_count == 0
|
||||
|
||||
@override_settings(APPRISE_RECURSION_MAX=1)
|
||||
@mock.patch('apprise.Apprise.notify')
|
||||
def test_stateless_notify_recursion(self, mock_notify):
|
||||
|
@ -60,6 +60,17 @@ class AppriseStoreMode(object):
|
||||
DISABLED = 'disabled'
|
||||
|
||||
|
||||
class AttachmentPayload(object):
|
||||
"""
|
||||
Defines the supported Attachment Payload Types
|
||||
"""
|
||||
# BASE64
|
||||
BASE64 = 'base64'
|
||||
|
||||
# URL request
|
||||
URL = 'url'
|
||||
|
||||
|
||||
STORE_MODES = (
|
||||
AppriseStoreMode.HASH,
|
||||
AppriseStoreMode.SIMPLE,
|
||||
@ -79,7 +90,7 @@ class Attachment(A_MGR['file']):
|
||||
Attachments
|
||||
"""
|
||||
|
||||
def __init__(self, filename, path=None, delete=True):
|
||||
def __init__(self, filename, path=None, delete=True, **kwargs):
|
||||
"""
|
||||
Initialize our attachment
|
||||
"""
|
||||
@ -108,7 +119,7 @@ class Attachment(A_MGR['file']):
|
||||
self._path = path
|
||||
|
||||
# Prepare our item
|
||||
super().__init__(path=self._path, name=filename)
|
||||
super().__init__(path=self._path, name=filename, **kwargs)
|
||||
|
||||
# Update our file size based on the settings value
|
||||
self.max_file_size = settings.APPRISE_ATTACH_SIZE
|
||||
@ -136,6 +147,67 @@ class Attachment(A_MGR['file']):
|
||||
pass
|
||||
|
||||
|
||||
class HTTPAttachment(A_MGR['http']):
|
||||
"""
|
||||
A Light Weight Attachment Object for Auto-cleanup that wraps the Apprise
|
||||
Web Attachments
|
||||
"""
|
||||
|
||||
def __init__(self, filename, delete=True, **kwargs):
|
||||
"""
|
||||
Initialize our attachment
|
||||
"""
|
||||
self._filename = filename
|
||||
self.delete = delete
|
||||
self._path = None
|
||||
try:
|
||||
os.makedirs(settings.APPRISE_ATTACH_DIR, exist_ok=True)
|
||||
|
||||
except OSError:
|
||||
# Permission error
|
||||
raise ValueError('Could not create directory {}'.format(
|
||||
settings.APPRISE_ATTACH_DIR))
|
||||
|
||||
try:
|
||||
d, self._path = tempfile.mkstemp(dir=settings.APPRISE_ATTACH_DIR)
|
||||
# Close our file descriptor
|
||||
os.close(d)
|
||||
|
||||
except FileNotFoundError:
|
||||
raise ValueError(
|
||||
'Could not prepare {} attachment in {}'.format(
|
||||
filename, settings.APPRISE_ATTACH_DIR))
|
||||
|
||||
|
||||
# Prepare our item
|
||||
super().__init__(name=filename, **kwargs)
|
||||
|
||||
# Update our file size based on the settings value
|
||||
self.max_file_size = settings.APPRISE_ATTACH_SIZE
|
||||
|
||||
@property
|
||||
def filename(self):
|
||||
return self._filename
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
"""
|
||||
Return filesize
|
||||
"""
|
||||
return 0 if not self else os.stat(self._path).st_size
|
||||
|
||||
def __del__(self):
|
||||
"""
|
||||
De-Construtor is used to tidy up files during garbage collection
|
||||
"""
|
||||
if self.delete and self._path:
|
||||
try:
|
||||
os.remove(self._path)
|
||||
except FileNotFoundError:
|
||||
# no problem
|
||||
pass
|
||||
|
||||
|
||||
def parse_attachments(attachment_payload, files_request):
|
||||
"""
|
||||
Takes the payload provided in a `/notify` call and extracts the
|
||||
@ -148,11 +220,16 @@ def parse_attachments(attachment_payload, files_request):
|
||||
|
||||
# Attachment Count
|
||||
count = sum([
|
||||
0 if not isinstance(attachment_payload, (tuple, list))
|
||||
0 if not isinstance(attachment_payload, (set, tuple, list))
|
||||
else len(attachment_payload),
|
||||
0 if not isinstance(files_request, dict) else len(files_request),
|
||||
])
|
||||
|
||||
if isinstance(attachment_payload, (dict, str, bytes)):
|
||||
# Convert and adjust counter
|
||||
attachment_payload = (attachment_payload, )
|
||||
count += 1
|
||||
|
||||
if settings.APPRISE_MAX_ATTACHMENTS <= 0 or \
|
||||
(settings.APPRISE_MAX_ATTACHMENTS > 0 and
|
||||
count > settings.APPRISE_MAX_ATTACHMENTS):
|
||||
@ -161,10 +238,9 @@ def parse_attachments(attachment_payload, files_request):
|
||||
settings.APPRISE_MAX_ATTACHMENTS
|
||||
if settings.APPRISE_MAX_ATTACHMENTS > 0 else 0)
|
||||
|
||||
if isinstance(attachment_payload, (tuple, list)):
|
||||
if isinstance(attachment_payload, (tuple, list, set)):
|
||||
for no, entry in enumerate(attachment_payload, start=1):
|
||||
|
||||
if isinstance(entry, str):
|
||||
if isinstance(entry, (str, bytes)):
|
||||
filename = "attachment.%.3d" % no
|
||||
|
||||
elif isinstance(entry, dict):
|
||||
@ -180,7 +256,8 @@ def parse_attachments(attachment_payload, files_request):
|
||||
elif not filename:
|
||||
filename = "attachment.%.3d" % no
|
||||
|
||||
except TypeError:
|
||||
except AttributeError:
|
||||
# not a string that was provided
|
||||
raise ValueError(
|
||||
"An invalid filename was provided for attachment %d" %
|
||||
no)
|
||||
@ -194,30 +271,70 @@ def parse_attachments(attachment_payload, files_request):
|
||||
#
|
||||
# Prepare our Attachment
|
||||
#
|
||||
attachment = Attachment(filename)
|
||||
if isinstance(entry, str):
|
||||
if not re.match(r'^https?://.+', entry[:10], re.I):
|
||||
# We failed to retrieve the product
|
||||
raise ValueError(
|
||||
"Failed to load attachment "
|
||||
"%d (not web request): %s" % (no, entry))
|
||||
|
||||
try:
|
||||
with open(attachment.path, 'wb') as f:
|
||||
# Write our content to disk
|
||||
f.write(base64.b64decode(entry["base64"]))
|
||||
attachment = HTTPAttachment(
|
||||
filename, **A_MGR['http'].parse_url(entry))
|
||||
if not attachment:
|
||||
# We failed to retrieve the attachment
|
||||
raise ValueError(
|
||||
"Failed to retrieve attachment %d: %s" % (no, entry))
|
||||
|
||||
except binascii.Error:
|
||||
# The file ws not base64 encoded
|
||||
raise ValueError(
|
||||
"Invalid filecontent was provided for attachment %s" %
|
||||
filename)
|
||||
else: # web, base64 or raw
|
||||
attachment = Attachment(filename)
|
||||
try:
|
||||
with open(attachment.path, 'wb') as f:
|
||||
# Write our content to disk
|
||||
if isinstance(entry, dict) and \
|
||||
AttachmentPayload.BASE64 in entry:
|
||||
# BASE64
|
||||
f.write(
|
||||
base64.b64decode(
|
||||
entry[AttachmentPayload.BASE64]))
|
||||
|
||||
except OSError:
|
||||
raise ValueError(
|
||||
"Could not write attachment %s to disk" % filename)
|
||||
elif isinstance(entry, dict) and \
|
||||
AttachmentPayload.URL in entry:
|
||||
|
||||
#
|
||||
# Some Validation
|
||||
#
|
||||
if settings.APPRISE_MAX_ATTACHMENT_SIZE > 0 and \
|
||||
attachment.size > settings.APPRISE_MAX_ATTACHMENT_SIZE:
|
||||
raise ValueError(
|
||||
"attachment %s's filesize is to large" % filename)
|
||||
attachment = HTTPAttachment(
|
||||
filename, **A_MGR['http']
|
||||
.parse_url(entry[AttachmentPayload.URL]))
|
||||
if not attachment:
|
||||
# We failed to retrieve the attachment
|
||||
raise ValueError(
|
||||
"Failed to retrieve attachment "
|
||||
"%d: %s" % (no, entry))
|
||||
|
||||
elif isinstance(entry, bytes):
|
||||
# RAW
|
||||
f.write(entry)
|
||||
|
||||
else:
|
||||
raise ValueError(
|
||||
"Invalid filetype was provided for "
|
||||
"attachment %s" % filename)
|
||||
|
||||
except binascii.Error:
|
||||
# The file ws not base64 encoded
|
||||
raise ValueError(
|
||||
"Invalid filecontent was provided for attachment %s" %
|
||||
filename)
|
||||
|
||||
except OSError:
|
||||
raise ValueError(
|
||||
"Could not write attachment %s to disk" % filename)
|
||||
|
||||
#
|
||||
# Some Validation
|
||||
#
|
||||
if settings.APPRISE_MAX_ATTACHMENT_SIZE > 0 and \
|
||||
attachment.size > settings.APPRISE_MAX_ATTACHMENT_SIZE:
|
||||
raise ValueError(
|
||||
"attachment %s's filesize is to large" % filename)
|
||||
|
||||
# Add our attachment
|
||||
attachments.append(attachment)
|
||||
@ -245,8 +362,7 @@ def parse_attachments(attachment_payload, files_request):
|
||||
|
||||
except (AttributeError, TypeError):
|
||||
raise ValueError(
|
||||
"An invalid filename was provided for attachment %d" %
|
||||
no)
|
||||
"An invalid filename was provided for attachment %d" % no)
|
||||
|
||||
#
|
||||
# Prepare our Attachment
|
||||
|
@ -610,6 +610,10 @@ class NotifyView(View):
|
||||
|
||||
# Handle Attachments
|
||||
attach = None
|
||||
if not content.get('attachment') and 'attachment' in request.POST:
|
||||
# Acquire attachments to work with them
|
||||
content['attachment'] = request.POST.getlist('attachment')
|
||||
|
||||
if 'attachment' in content or request.FILES:
|
||||
try:
|
||||
attach = parse_attachments(
|
||||
@ -974,8 +978,9 @@ class NotifyView(View):
|
||||
)
|
||||
|
||||
logger.info(
|
||||
'NOTIFY - %s - Proccessed%s KEY: %s', request.META['REMOTE_ADDR'],
|
||||
'' if not tag else f' (Tags: {tag}),', key)
|
||||
'NOTIFY - %s - Delivered Notification(s) - %sKEY: %s',
|
||||
request.META['REMOTE_ADDR'],
|
||||
'' if not tag else f'Tags: {tag}, ', key)
|
||||
|
||||
# Return our retrieved content
|
||||
return HttpResponse(
|
||||
@ -1011,14 +1016,22 @@ class StatelessNotifyView(View):
|
||||
|
||||
except (AttributeError, ValueError):
|
||||
# could not parse JSON response...
|
||||
logger.warning(
|
||||
'NOTIFY - %s - Invalid JSON Payload provided',
|
||||
request.META['REMOTE_ADDR'])
|
||||
|
||||
return HttpResponse(
|
||||
_('Invalid JSON specified.'),
|
||||
status=ResponseCode.bad_request)
|
||||
|
||||
if not content:
|
||||
# We could not handle the Content-Type
|
||||
logger.warning(
|
||||
'NOTIFY - %s - Invalid FORM Payload provided',
|
||||
request.META['REMOTE_ADDR'])
|
||||
|
||||
return HttpResponse(
|
||||
_('The message format is not supported.'),
|
||||
_('Bad FORM Payload provided.'),
|
||||
status=ResponseCode.bad_request)
|
||||
|
||||
if not content.get('urls') and settings.APPRISE_STATELESS_URLS:
|
||||
@ -1031,15 +1044,22 @@ class StatelessNotifyView(View):
|
||||
content.get('type', apprise.NotifyType.INFO) \
|
||||
not in apprise.NOTIFY_TYPES:
|
||||
|
||||
logger.warning(
|
||||
'NOTIFY - %s - Payload lacks minimum requirements',
|
||||
request.META['REMOTE_ADDR'])
|
||||
|
||||
return HttpResponse(
|
||||
_('An invalid payload was specified.'),
|
||||
_('Payload lacks minimum requirements.'),
|
||||
status=ResponseCode.bad_request)
|
||||
|
||||
# Acquire our body format (if identified)
|
||||
body_format = content.get('format', apprise.NotifyFormat.TEXT)
|
||||
if body_format and body_format not in apprise.NOTIFY_FORMATS:
|
||||
logger.warning(
|
||||
'NOTIFY - %s - Format parameter contains an unsupported '
|
||||
'value (%s)', request.META['REMOTE_ADDR'], str(body_format))
|
||||
return HttpResponse(
|
||||
_('An invalid (body) format was specified.'),
|
||||
_('An invalid body input format was specified.'),
|
||||
status=ResponseCode.bad_request)
|
||||
|
||||
# Prepare our keyword arguments (to be passed into an AppriseAsset
|
||||
@ -1053,15 +1073,19 @@ class StatelessNotifyView(View):
|
||||
kwargs['body_format'] = body_format
|
||||
|
||||
# Acquire our recursion count (if defined)
|
||||
recursion = request.headers.get('X-Apprise-Recursion-Count', 0)
|
||||
try:
|
||||
recursion = \
|
||||
int(request.headers.get('X-Apprise-Recursion-Count', 0))
|
||||
recursion = int(recursion)
|
||||
|
||||
if recursion < 0:
|
||||
# We do not accept negative numbers
|
||||
raise TypeError("Invalid Recursion Value")
|
||||
|
||||
if recursion > settings.APPRISE_RECURSION_MAX:
|
||||
logger.warning(
|
||||
'NOTIFY - %s - Recursion limit reached (%d > %d)',
|
||||
request.META['REMOTE_ADDR'], recursion,
|
||||
settings.APPRISE_RECURSION_MAX)
|
||||
return HttpResponse(
|
||||
_('The recursion limit has been reached.'),
|
||||
status=ResponseCode.method_not_accepted)
|
||||
@ -1070,6 +1094,9 @@ class StatelessNotifyView(View):
|
||||
kwargs['_recursion'] = recursion
|
||||
|
||||
except (TypeError, ValueError):
|
||||
logger.warning(
|
||||
'NOTIFY - %s - Invalid recursion value (%s) provided',
|
||||
request.META['REMOTE_ADDR'], str(recursion))
|
||||
return HttpResponse(
|
||||
_('An invalid recursion value was specified.'),
|
||||
status=ResponseCode.bad_request)
|
||||
@ -1100,6 +1127,10 @@ class StatelessNotifyView(View):
|
||||
|
||||
# Handle Attachments
|
||||
attach = None
|
||||
if not content.get('attachment') and 'attachment' in request.POST:
|
||||
# Acquire attachments to work with them
|
||||
content['attachment'] = request.POST.getlist('attachment')
|
||||
|
||||
if 'attachment' in content or request.FILES:
|
||||
try:
|
||||
attach = parse_attachments(
|
||||
@ -1178,6 +1209,10 @@ class StatelessNotifyView(View):
|
||||
_('One or more notification could not be sent.'),
|
||||
status=ResponseCode.failed_dependency)
|
||||
|
||||
logger.info(
|
||||
'NOTIFY - %s - Delivered Stateless Notification(s)',
|
||||
request.META['REMOTE_ADDR'])
|
||||
|
||||
# Return our retrieved content
|
||||
return HttpResponse(
|
||||
_('Notification(s) sent.'),
|
||||
|
Loading…
Reference in New Issue
Block a user