mirror of
https://github.com/caronc/apprise.git
synced 2024-11-08 09:14:53 +01:00
Discord markdown enhancements (#295)
This commit is contained in:
parent
49faa9a201
commit
784e073eea
@ -80,6 +80,11 @@ class NotifyDiscord(NotifyBase):
|
||||
# The maximum allowable characters allowed in the body per message
|
||||
body_maxlen = 2000
|
||||
|
||||
# Discord has a limit of the number of fields you can include in an
|
||||
# embeds message. This value allows the discord message to safely
|
||||
# break into multiple messages to handle these cases.
|
||||
discord_max_fields = 10
|
||||
|
||||
# Define object templates
|
||||
templates = (
|
||||
'{schema}://{webhook_id}/{webhook_token}',
|
||||
@ -133,6 +138,11 @@ class NotifyDiscord(NotifyBase):
|
||||
'type': 'bool',
|
||||
'default': True,
|
||||
},
|
||||
'fields': {
|
||||
'name': _('Use Fields'),
|
||||
'type': 'bool',
|
||||
'default': True,
|
||||
},
|
||||
'image': {
|
||||
'name': _('Include Image'),
|
||||
'type': 'bool',
|
||||
@ -143,7 +153,7 @@ class NotifyDiscord(NotifyBase):
|
||||
|
||||
def __init__(self, webhook_id, webhook_token, tts=False, avatar=True,
|
||||
footer=False, footer_logo=True, include_image=False,
|
||||
avatar_url=None, **kwargs):
|
||||
fields=True, avatar_url=None, **kwargs):
|
||||
"""
|
||||
Initialize Discord Object
|
||||
|
||||
@ -181,6 +191,9 @@ class NotifyDiscord(NotifyBase):
|
||||
# Place a thumbnail image inline with the message body
|
||||
self.include_image = include_image
|
||||
|
||||
# Use Fields
|
||||
self.fields = fields
|
||||
|
||||
# Avatar URL
|
||||
# This allows a user to provide an over-ride to the otherwise
|
||||
# dynamically generated avatar url images
|
||||
@ -206,32 +219,23 @@ class NotifyDiscord(NotifyBase):
|
||||
# Acquire image_url
|
||||
image_url = self.image_url(notify_type)
|
||||
|
||||
# our fields variable
|
||||
fields = []
|
||||
|
||||
if self.notify_format == NotifyFormat.MARKDOWN:
|
||||
# Use embeds for payload
|
||||
payload['embeds'] = [{
|
||||
'provider': {
|
||||
'author': {
|
||||
'name': self.app_id,
|
||||
'url': self.app_url,
|
||||
},
|
||||
'title': title,
|
||||
'type': 'rich',
|
||||
'description': body,
|
||||
|
||||
# Our color associated with our notification
|
||||
'color': self.color(notify_type, int),
|
||||
}]
|
||||
|
||||
# Break titles out so that we can sort them in embeds
|
||||
fields = self.extract_markdown_sections(body)
|
||||
|
||||
if len(fields) > 0:
|
||||
# Apply our additional parsing for a better presentation
|
||||
|
||||
# Swap first entry for description
|
||||
payload['embeds'][0]['description'] = \
|
||||
fields[0].get('name') + fields[0].get('value')
|
||||
payload['embeds'][0]['fields'] = fields[1:]
|
||||
|
||||
if self.footer:
|
||||
# Acquire logo URL
|
||||
logo_url = self.image_url(notify_type, logo=True)
|
||||
@ -251,6 +255,20 @@ class NotifyDiscord(NotifyBase):
|
||||
'width': 256,
|
||||
}
|
||||
|
||||
if self.fields:
|
||||
# Break titles out so that we can sort them in embeds
|
||||
description, fields = self.extract_markdown_sections(body)
|
||||
|
||||
# Swap first entry for description
|
||||
payload['embeds'][0]['description'] = description
|
||||
if fields:
|
||||
# Apply our additional parsing for a better presentation
|
||||
payload['embeds'][0]['fields'] = \
|
||||
fields[:self.discord_max_fields]
|
||||
|
||||
# Remove entry from head of fields
|
||||
fields = fields[self.discord_max_fields:]
|
||||
|
||||
else:
|
||||
# not markdown
|
||||
payload['content'] = \
|
||||
@ -268,6 +286,16 @@ class NotifyDiscord(NotifyBase):
|
||||
# We failed to post our message
|
||||
return False
|
||||
|
||||
# Process any remaining fields IF set
|
||||
if fields:
|
||||
payload['embeds'][0]['description'] = ''
|
||||
for i in range(0, len(fields), self.discord_max_fields):
|
||||
payload['embeds'][0]['fields'] = \
|
||||
fields[i:i + self.discord_max_fields]
|
||||
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
|
||||
@ -413,6 +441,7 @@ class NotifyDiscord(NotifyBase):
|
||||
'footer': 'yes' if self.footer else 'no',
|
||||
'footer_logo': 'yes' if self.footer_logo else 'no',
|
||||
'image': 'yes' if self.include_image else 'no',
|
||||
'fields': 'yes' if self.fields else 'no',
|
||||
}
|
||||
|
||||
# Extend our parameters
|
||||
@ -459,6 +488,11 @@ class NotifyDiscord(NotifyBase):
|
||||
# Text To Speech
|
||||
results['tts'] = parse_bool(results['qsd'].get('tts', False))
|
||||
|
||||
# Use sections
|
||||
# effectively detect multiple fields and break them off
|
||||
# into sections
|
||||
results['fields'] = parse_bool(results['qsd'].get('fields', True))
|
||||
|
||||
# Use Footer
|
||||
results['footer'] = parse_bool(results['qsd'].get('footer', False))
|
||||
|
||||
@ -513,6 +547,18 @@ class NotifyDiscord(NotifyBase):
|
||||
fields that get passed as an embed entry to Discord.
|
||||
|
||||
"""
|
||||
# Search for any header information found without it's own section
|
||||
# identifier
|
||||
match = re.match(
|
||||
r'^\s*(?P<desc>[^\s#]+.*?)(?=\s*$|[\r\n]+\s*#)',
|
||||
markdown, flags=re.S)
|
||||
|
||||
description = match.group('desc').strip() if match else ''
|
||||
if description:
|
||||
# Strip description from our string since it has been handled
|
||||
# now.
|
||||
markdown = re.sub(description, '', markdown, count=1)
|
||||
|
||||
regex = re.compile(
|
||||
r'\s*#[# \t\v]*(?P<name>[^\n]+)(\n|\s*$)'
|
||||
r'\s*((?P<value>[^#].+?)(?=\s*$|[\r\n]+\s*#))?', flags=re.S)
|
||||
@ -523,9 +569,11 @@ class NotifyDiscord(NotifyBase):
|
||||
d = el.groupdict()
|
||||
|
||||
fields.append({
|
||||
'name': d.get('name', '').strip('# \r\n\t\v'),
|
||||
'value': '```md\n' +
|
||||
(d.get('value').strip() if d.get('value') else '') + '\n```'
|
||||
'name': d.get('name', '').strip('#`* \r\n\t\v'),
|
||||
'value': '```{}\n{}```'.format(
|
||||
'md' if d.get('value') else '',
|
||||
d.get('value').strip() + '\n' if d.get('value') else '',
|
||||
),
|
||||
})
|
||||
|
||||
return fields
|
||||
return description, fields
|
||||
|
@ -84,6 +84,41 @@ def test_discord_plugin(mock_post):
|
||||
assert obj.notify(
|
||||
body='body', title='title', notify_type=NotifyType.INFO) is True
|
||||
|
||||
# Simple Markdown Single line of text
|
||||
test_markdown = "body"
|
||||
desc, results = obj.extract_markdown_sections(test_markdown)
|
||||
assert isinstance(results, list) is True
|
||||
assert len(results) == 0
|
||||
|
||||
# Test our header parsing when not lead with a header
|
||||
test_markdown = """
|
||||
A section of text that has no header at the top.
|
||||
It also has a hash tag # <- in the middle of a
|
||||
string.
|
||||
|
||||
## Heading 1
|
||||
body
|
||||
|
||||
# Heading 2
|
||||
|
||||
more content
|
||||
on multi-lines
|
||||
"""
|
||||
|
||||
desc, results = obj.extract_markdown_sections(test_markdown)
|
||||
# we have a description
|
||||
assert isinstance(desc, six.string_types) is True
|
||||
assert desc.startswith('A section of text that has no header at the top.')
|
||||
assert desc.endswith('string.')
|
||||
|
||||
assert isinstance(results, list) is True
|
||||
assert len(results) == 2
|
||||
assert results[0]['name'] == 'Heading 1'
|
||||
assert results[0]['value'] == '```md\nbody\n```'
|
||||
assert results[1]['name'] == 'Heading 2'
|
||||
assert results[1]['value'] == \
|
||||
'```md\nmore content\n on multi-lines\n```'
|
||||
|
||||
# Test our header parsing
|
||||
test_markdown = "## Heading one\nbody body\n\n" + \
|
||||
"# Heading 2 ##\n\nTest\n\n" + \
|
||||
@ -94,8 +129,12 @@ def test_discord_plugin(mock_post):
|
||||
"# heading 4\n" + \
|
||||
"#### Heading 5"
|
||||
|
||||
results = obj.extract_markdown_sections(test_markdown)
|
||||
desc, results = obj.extract_markdown_sections(test_markdown)
|
||||
assert isinstance(results, list) is True
|
||||
# No desc details filled out
|
||||
assert isinstance(desc, six.string_types) is True
|
||||
assert not desc
|
||||
|
||||
# We should have 5 sections (since there are 5 headers identified above)
|
||||
assert len(results) == 5
|
||||
assert results[0]['name'] == 'Heading one'
|
||||
@ -107,29 +146,67 @@ def test_discord_plugin(mock_post):
|
||||
assert results[2]['value'] == \
|
||||
'```md\nnormal content\n```'
|
||||
assert results[3]['name'] == 'heading 4'
|
||||
assert results[3]['value'] == '```md\n\n```'
|
||||
assert results[3]['value'] == '```\n```'
|
||||
assert results[4]['name'] == 'Heading 5'
|
||||
assert results[4]['value'] == '```md\n\n```'
|
||||
assert results[4]['value'] == '```\n```'
|
||||
|
||||
# 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:
|
||||
plugins.NotifyDiscord.discord_max_fields = 1
|
||||
|
||||
assert a.notify(body=test_markdown, title='title',
|
||||
notify_type=NotifyType.INFO,
|
||||
body_format=NotifyFormat.TEXT) is True
|
||||
|
||||
# Throw an exception on the forth call to requests.post()
|
||||
# This allows to test our batch field processing
|
||||
response = mock.Mock()
|
||||
response.content = ''
|
||||
response.status_code = requests.codes.ok
|
||||
mock_post.return_value = response
|
||||
mock_post.side_effect = [
|
||||
response, response, response, requests.RequestException()]
|
||||
|
||||
# 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
|
||||
body=test_markdown, title='title',
|
||||
notify_type=NotifyType.INFO) is False
|
||||
mock_post.side_effect = None
|
||||
|
||||
# Empty String
|
||||
results = obj.extract_markdown_sections("")
|
||||
desc, results = obj.extract_markdown_sections("")
|
||||
assert isinstance(results, list) is True
|
||||
assert len(results) == 0
|
||||
|
||||
# No desc details filled out
|
||||
assert isinstance(desc, six.string_types) is True
|
||||
assert not desc
|
||||
|
||||
# String without Heading
|
||||
test_markdown = "Just a string without any header entries.\n" + \
|
||||
"A second line"
|
||||
results = obj.extract_markdown_sections(test_markdown)
|
||||
desc, results = obj.extract_markdown_sections(test_markdown)
|
||||
assert isinstance(results, list) is True
|
||||
assert len(results) == 0
|
||||
|
||||
# No desc details filled out
|
||||
assert isinstance(desc, six.string_types) is True
|
||||
assert desc == 'Just a string without any header entries.\n' + \
|
||||
'A second line'
|
||||
|
||||
# Use our test markdown string during a notification
|
||||
assert obj.notify(
|
||||
body=test_markdown, title='title', notify_type=NotifyType.INFO) is True
|
||||
|
@ -289,7 +289,7 @@ TEST_URLS = (
|
||||
# don't include an image by default
|
||||
'include_image': False,
|
||||
}),
|
||||
('discord://%s/%s?format=markdown&footer=Yes&image=No' % (
|
||||
('discord://%s/%s?format=markdown&footer=Yes&image=No&fields=no' % (
|
||||
'i' * 24, 't' * 64), {
|
||||
'instance': plugins.NotifyDiscord,
|
||||
'requests_response_code': requests.codes.no_content,
|
||||
|
Loading…
Reference in New Issue
Block a user