mirror of https://github.com/caronc/apprise
Discord Attachment Support
parent
0175da1583
commit
cb9b4fd103
|
@ -42,7 +42,6 @@
|
|||
#
|
||||
import re
|
||||
import requests
|
||||
from json import dumps
|
||||
|
||||
from .NotifyBase import NotifyBase
|
||||
from ..common import NotifyImageSize
|
||||
|
@ -178,17 +177,12 @@ class NotifyDiscord(NotifyBase):
|
|||
|
||||
return
|
||||
|
||||
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||
def send(self, body, title='', notify_type=NotifyType.INFO, attach=None,
|
||||
**kwargs):
|
||||
"""
|
||||
Perform Discord Notification
|
||||
"""
|
||||
|
||||
headers = {
|
||||
'User-Agent': self.app_id,
|
||||
'Content-Type': 'multipart/form-data',
|
||||
}
|
||||
|
||||
# Prepare JSON Object
|
||||
payload = {
|
||||
# Text-To-Speech
|
||||
'tts': self.tts,
|
||||
|
@ -258,6 +252,50 @@ class NotifyDiscord(NotifyBase):
|
|||
# Optionally override the default username of the webhook
|
||||
payload['username'] = self.user
|
||||
|
||||
if not self._send(payload):
|
||||
# We failed to post our message
|
||||
return False
|
||||
|
||||
if attach:
|
||||
# Update our payload; the idea is to preserve it's other detected
|
||||
# and assigned values for re-use here too
|
||||
payload.update({
|
||||
# Text-To-Speech
|
||||
'tts': False,
|
||||
# Wait until the upload has posted itself before continuing
|
||||
'wait': True,
|
||||
})
|
||||
|
||||
# Remove our text/title based content for attachment use
|
||||
if 'embeds' in payload:
|
||||
# Markdown
|
||||
del payload['embeds']
|
||||
|
||||
if 'content' in payload:
|
||||
# Markdown
|
||||
del payload['content']
|
||||
|
||||
# Send our attachments
|
||||
for attachment in attach:
|
||||
self.logger.info(
|
||||
'Posting Discord Attachment {}'.format(attachment.name))
|
||||
if not self._send(payload, attach=attachment):
|
||||
# We failed to post our message
|
||||
return False
|
||||
|
||||
# Otherwise return
|
||||
return True
|
||||
|
||||
def _send(self, payload, attach=None, **kwargs):
|
||||
"""
|
||||
Wrapper to the requests (post) object
|
||||
"""
|
||||
|
||||
# Our headers
|
||||
headers = {
|
||||
'User-Agent': self.app_id,
|
||||
}
|
||||
|
||||
# Construct Notify URL
|
||||
notify_url = '{0}/{1}/{2}'.format(
|
||||
self.notify_url,
|
||||
|
@ -273,11 +311,19 @@ class NotifyDiscord(NotifyBase):
|
|||
# Always call throttle before any remote server i/o is made
|
||||
self.throttle()
|
||||
|
||||
# Our attachment path (if specified)
|
||||
files = None
|
||||
try:
|
||||
|
||||
# Open our attachment path if required:
|
||||
if attach:
|
||||
files = (attach.name, open(attach.path, 'rb'))
|
||||
|
||||
r = requests.post(
|
||||
notify_url,
|
||||
data=dumps(payload),
|
||||
data=payload,
|
||||
headers=headers,
|
||||
files=files,
|
||||
verify=self.verify_certificate,
|
||||
)
|
||||
if r.status_code not in (
|
||||
|
@ -288,8 +334,9 @@ class NotifyDiscord(NotifyBase):
|
|||
NotifyBase.http_response_code_lookup(r.status_code)
|
||||
|
||||
self.logger.warning(
|
||||
'Failed to send Discord notification: '
|
||||
'Failed to send {}to Discord notification: '
|
||||
'{}{}error={}.'.format(
|
||||
attach.name if attach else '',
|
||||
status_str,
|
||||
', ' if status_str else '',
|
||||
r.status_code))
|
||||
|
@ -304,12 +351,24 @@ class NotifyDiscord(NotifyBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured sending Discord '
|
||||
'notification.'
|
||||
)
|
||||
'A Connection error occured posting {}to Discord.'.format(
|
||||
attach.name if attach else ''))
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
return False
|
||||
|
||||
except (OSError, IOError) as e:
|
||||
self.logger.warning(
|
||||
'An I/O error occured while reading {}.'.format(
|
||||
attach.name if attach else 'attachment'))
|
||||
self.logger.debug('I/O Exception: %s' % str(e))
|
||||
return False
|
||||
|
||||
finally:
|
||||
# Close our file (if it's open) stored in the second element
|
||||
# of our files tuple (index 1)
|
||||
if files:
|
||||
files[1].close()
|
||||
|
||||
return True
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
|
|
|
@ -0,0 +1,191 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# All rights reserved.
|
||||
#
|
||||
# This code is licensed under the MIT License.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files(the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions :
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# 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 os
|
||||
import six
|
||||
import mock
|
||||
import pytest
|
||||
import requests
|
||||
from apprise import Apprise
|
||||
from apprise import AppriseAttachment
|
||||
from apprise import plugins
|
||||
from apprise import NotifyType
|
||||
from apprise import NotifyFormat
|
||||
|
||||
# Disable logging for a cleaner testing output
|
||||
import logging
|
||||
logging.disable(logging.CRITICAL)
|
||||
|
||||
# Attachment Directory
|
||||
TEST_VAR_DIR = os.path.join(os.path.dirname(__file__), 'var')
|
||||
|
||||
|
||||
@mock.patch('requests.post')
|
||||
def test_discord_plugin(mock_post):
|
||||
"""
|
||||
API: NotifyDiscord() General Checks
|
||||
|
||||
"""
|
||||
# Disable Throttling to speed testing
|
||||
plugins.NotifyBase.request_rate_per_sec = 0
|
||||
|
||||
# Initialize some generic (but valid) tokens
|
||||
webhook_id = 'A' * 24
|
||||
webhook_token = 'B' * 64
|
||||
|
||||
# Prepare Mock
|
||||
mock_post.return_value = requests.Request()
|
||||
mock_post.return_value.status_code = requests.codes.ok
|
||||
|
||||
# Invalid webhook id
|
||||
with pytest.raises(TypeError):
|
||||
plugins.NotifyDiscord(webhook_id=None, webhook_token=webhook_token)
|
||||
# Invalid webhook id (whitespace)
|
||||
with pytest.raises(TypeError):
|
||||
plugins.NotifyDiscord(webhook_id=" ", webhook_token=webhook_token)
|
||||
|
||||
# Invalid webhook token
|
||||
with pytest.raises(TypeError):
|
||||
plugins.NotifyDiscord(webhook_id=webhook_id, webhook_token=None)
|
||||
# Invalid webhook token (whitespace)
|
||||
with pytest.raises(TypeError):
|
||||
plugins.NotifyDiscord(webhook_id=webhook_id, webhook_token=" ")
|
||||
|
||||
obj = plugins.NotifyDiscord(
|
||||
webhook_id=webhook_id,
|
||||
webhook_token=webhook_token,
|
||||
footer=True, thumbnail=False)
|
||||
|
||||
# Test that we get a string response
|
||||
assert isinstance(obj.url(), six.string_types) is True
|
||||
|
||||
# This call includes an image with it's payload:
|
||||
assert obj.notify(
|
||||
body='body', title='title', notify_type=NotifyType.INFO) is True
|
||||
|
||||
# Test our header parsing
|
||||
test_markdown = "## Heading one\nbody body\n\n" + \
|
||||
"# Heading 2 ##\n\nTest\n\n" + \
|
||||
"more content\n" + \
|
||||
"even more content \t\r\n\n\n" + \
|
||||
"# Heading 3 ##\n\n\n" + \
|
||||
"normal content\n" + \
|
||||
"# heading 4\n" + \
|
||||
"#### Heading 5"
|
||||
|
||||
results = obj.extract_markdown_sections(test_markdown)
|
||||
assert isinstance(results, list) is True
|
||||
# We should have 5 sections (since there are 5 headers identified above)
|
||||
assert len(results) == 5
|
||||
assert results[0]['name'] == 'Heading one'
|
||||
assert results[0]['value'] == '```md\nbody body\n```'
|
||||
assert results[1]['name'] == 'Heading 2'
|
||||
assert results[1]['value'] == \
|
||||
'```md\nTest\n\nmore content\neven more content\n```'
|
||||
assert results[2]['name'] == 'Heading 3'
|
||||
assert results[2]['value'] == \
|
||||
'```md\nnormal content\n```'
|
||||
assert results[3]['name'] == 'heading 4'
|
||||
assert results[3]['value'] == '```md\n\n```'
|
||||
assert results[4]['name'] == 'Heading 5'
|
||||
assert results[4]['value'] == '```md\n\n```'
|
||||
|
||||
# Test our markdown
|
||||
obj = Apprise.instantiate(
|
||||
'discord://{}/{}/?format=markdown'.format(webhook_id, webhook_token))
|
||||
assert isinstance(obj, plugins.NotifyDiscord)
|
||||
assert obj.notify(
|
||||
body=test_markdown, title='title', notify_type=NotifyType.INFO) is True
|
||||
|
||||
# Empty String
|
||||
results = obj.extract_markdown_sections("")
|
||||
assert isinstance(results, list) is True
|
||||
assert len(results) == 0
|
||||
|
||||
# String without Heading
|
||||
test_markdown = "Just a string without any header entries.\n" + \
|
||||
"A second line"
|
||||
results = obj.extract_markdown_sections(test_markdown)
|
||||
assert isinstance(results, list) is True
|
||||
assert len(results) == 0
|
||||
|
||||
# Use our test markdown string during a notification
|
||||
assert obj.notify(
|
||||
body=test_markdown, title='title', notify_type=NotifyType.INFO) is True
|
||||
|
||||
# Create an apprise instance
|
||||
a = Apprise()
|
||||
|
||||
# Our processing is slightly different when we aren't using markdown
|
||||
# as we do not pre-parse content during our notifications
|
||||
assert a.add(
|
||||
'discord://{webhook_id}/{webhook_token}/'
|
||||
'?format=markdown&footer=Yes'.format(
|
||||
webhook_id=webhook_id,
|
||||
webhook_token=webhook_token)) is True
|
||||
|
||||
# This call includes an image with it's payload:
|
||||
assert a.notify(body=test_markdown, title='title',
|
||||
notify_type=NotifyType.INFO,
|
||||
body_format=NotifyFormat.TEXT) is True
|
||||
|
||||
assert a.notify(body=test_markdown, title='title',
|
||||
notify_type=NotifyType.INFO,
|
||||
body_format=NotifyFormat.MARKDOWN) is True
|
||||
|
||||
# Toggle our logo availability
|
||||
a.asset.image_url_logo = None
|
||||
assert a.notify(
|
||||
body='body', title='title', notify_type=NotifyType.INFO) is True
|
||||
|
||||
|
||||
@mock.patch('requests.post')
|
||||
def test_discord_attachments(mock_post):
|
||||
"""
|
||||
API: NotifyDiscord() Attachment Checks
|
||||
|
||||
"""
|
||||
# Disable Throttling to speed testing
|
||||
plugins.NotifyBase.request_rate_per_sec = 0
|
||||
|
||||
# Initialize some generic (but valid) tokens
|
||||
webhook_id = 'C' * 24
|
||||
webhook_token = 'D' * 64
|
||||
|
||||
# Prepare Mock return object
|
||||
response = mock.Mock()
|
||||
response.status_code = requests.codes.ok
|
||||
|
||||
# Throw an exception on the second call to requests.post()
|
||||
mock_post.side_effect = [response, OSError()]
|
||||
|
||||
# Test our markdown
|
||||
obj = Apprise.instantiate(
|
||||
'discord://{}/{}/?format=markdown'.format(webhook_id, webhook_token))
|
||||
|
||||
# attach our content
|
||||
attach = AppriseAttachment(os.path.join(TEST_VAR_DIR, 'apprise-test.gif'))
|
||||
|
||||
# We'll fail now because of an internal exception
|
||||
assert obj.send(body="test", attach=attach) is False
|
|
@ -3849,125 +3849,6 @@ def test_notify_boxcar_plugin(mock_post, mock_get):
|
|||
assert len(p.device_tokens) == 3
|
||||
|
||||
|
||||
@mock.patch('requests.get')
|
||||
@mock.patch('requests.post')
|
||||
def test_notify_discord_plugin(mock_post, mock_get):
|
||||
"""
|
||||
API: NotifyDiscord() Extra Checks
|
||||
|
||||
"""
|
||||
# Disable Throttling to speed testing
|
||||
plugins.NotifyBase.request_rate_per_sec = 0
|
||||
|
||||
# Initialize some generic (but valid) tokens
|
||||
webhook_id = 'A' * 24
|
||||
webhook_token = 'B' * 64
|
||||
|
||||
# Prepare Mock
|
||||
mock_get.return_value = requests.Request()
|
||||
mock_post.return_value = requests.Request()
|
||||
mock_post.return_value.status_code = requests.codes.ok
|
||||
mock_get.return_value.status_code = requests.codes.ok
|
||||
|
||||
# Invalid webhook id
|
||||
with pytest.raises(TypeError):
|
||||
plugins.NotifyDiscord(webhook_id=None, webhook_token=webhook_token)
|
||||
# Invalid webhook id (whitespace)
|
||||
with pytest.raises(TypeError):
|
||||
plugins.NotifyDiscord(webhook_id=" ", webhook_token=webhook_token)
|
||||
|
||||
# Invalid webhook token
|
||||
with pytest.raises(TypeError):
|
||||
plugins.NotifyDiscord(webhook_id=webhook_id, webhook_token=None)
|
||||
# Invalid webhook token (whitespace)
|
||||
with pytest.raises(TypeError):
|
||||
plugins.NotifyDiscord(webhook_id=webhook_id, webhook_token=" ")
|
||||
|
||||
obj = plugins.NotifyDiscord(
|
||||
webhook_id=webhook_id,
|
||||
webhook_token=webhook_token,
|
||||
footer=True, thumbnail=False)
|
||||
|
||||
# This call includes an image with it's payload:
|
||||
assert obj.notify(
|
||||
body='body', title='title', notify_type=NotifyType.INFO) is True
|
||||
|
||||
# Test our header parsing
|
||||
test_markdown = "## Heading one\nbody body\n\n" + \
|
||||
"# Heading 2 ##\n\nTest\n\n" + \
|
||||
"more content\n" + \
|
||||
"even more content \t\r\n\n\n" + \
|
||||
"# Heading 3 ##\n\n\n" + \
|
||||
"normal content\n" + \
|
||||
"# heading 4\n" + \
|
||||
"#### Heading 5"
|
||||
|
||||
results = obj.extract_markdown_sections(test_markdown)
|
||||
assert isinstance(results, list) is True
|
||||
# We should have 5 sections (since there are 5 headers identified above)
|
||||
assert len(results) == 5
|
||||
assert results[0]['name'] == 'Heading one'
|
||||
assert results[0]['value'] == '```md\nbody body\n```'
|
||||
assert results[1]['name'] == 'Heading 2'
|
||||
assert results[1]['value'] == \
|
||||
'```md\nTest\n\nmore content\neven more content\n```'
|
||||
assert results[2]['name'] == 'Heading 3'
|
||||
assert results[2]['value'] == \
|
||||
'```md\nnormal content\n```'
|
||||
assert results[3]['name'] == 'heading 4'
|
||||
assert results[3]['value'] == '```md\n\n```'
|
||||
assert results[4]['name'] == 'Heading 5'
|
||||
assert results[4]['value'] == '```md\n\n```'
|
||||
|
||||
# Test our markdown
|
||||
obj = Apprise.instantiate(
|
||||
'discord://{}/{}/?format=markdown'.format(webhook_id, webhook_token))
|
||||
assert isinstance(obj, plugins.NotifyDiscord)
|
||||
assert obj.notify(
|
||||
body=test_markdown, title='title', notify_type=NotifyType.INFO) is True
|
||||
|
||||
# Empty String
|
||||
results = obj.extract_markdown_sections("")
|
||||
assert isinstance(results, list) is True
|
||||
assert len(results) == 0
|
||||
|
||||
# String without Heading
|
||||
test_markdown = "Just a string without any header entries.\n" + \
|
||||
"A second line"
|
||||
results = obj.extract_markdown_sections(test_markdown)
|
||||
assert isinstance(results, list) is True
|
||||
assert len(results) == 0
|
||||
|
||||
# Use our test markdown string during a notification
|
||||
assert obj.notify(
|
||||
body=test_markdown, title='title', notify_type=NotifyType.INFO) is True
|
||||
|
||||
# Create an apprise instance
|
||||
a = Apprise()
|
||||
|
||||
# Our processing is slightly different when we aren't using markdown
|
||||
# as we do not pre-parse content during our notifications
|
||||
assert a.add(
|
||||
'discord://{webhook_id}/{webhook_token}/'
|
||||
'?format=markdown&footer=Yes'.format(
|
||||
webhook_id=webhook_id,
|
||||
webhook_token=webhook_token)) is True
|
||||
|
||||
# This call includes an image with it's payload:
|
||||
assert a.notify(body=test_markdown, title='title',
|
||||
notify_type=NotifyType.INFO,
|
||||
body_format=NotifyFormat.TEXT) is True
|
||||
|
||||
assert a.notify(body=test_markdown, title='title',
|
||||
notify_type=NotifyType.INFO,
|
||||
body_format=NotifyFormat.MARKDOWN) is True
|
||||
|
||||
# Toggle our logo availability
|
||||
a.asset.image_url_logo = None
|
||||
assert a.notify(
|
||||
body='body', title='title', notify_type=NotifyType.INFO) is True
|
||||
|
||||
|
||||
@mock.patch('requests.get')
|
||||
@mock.patch('requests.post')
|
||||
def test_notify_emby_plugin_login(mock_post, mock_get):
|
||||
|
|
Loading…
Reference in New Issue