Discord markdown enhancements (#295)

This commit is contained in:
Chris Caron 2020-09-13 23:06:41 -04:00 committed by GitHub
parent 49faa9a201
commit 784e073eea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 150 additions and 25 deletions

View File

@ -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

View File

@ -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

View File

@ -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,