Apprise HTML/MARKDOWN/TEXT Translation Handling Refactoring (#575)

This commit is contained in:
Chris Caron 2022-04-28 19:14:49 -04:00 committed by GitHub
parent 344b2153e0
commit 4a87d45879
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 378 additions and 224 deletions

View File

@ -43,6 +43,7 @@ from .AppriseLocale import AppriseLocale
from .config.ConfigBase import ConfigBase from .config.ConfigBase import ConfigBase
from .plugins.NotifyBase import NotifyBase from .plugins.NotifyBase import NotifyBase
from . import plugins from . import plugins
from . import __version__ from . import __version__
@ -542,11 +543,23 @@ class Apprise(object):
# determined we need to notify the service it's associated with # determined we need to notify the service it's associated with
if server.notify_format not in conversion_body_map: if server.notify_format not in conversion_body_map:
# Perform Conversion # Perform Conversion
(conversion_title_map[server.notify_format], conversion_body_map[server.notify_format] = \
conversion_body_map[server.notify_format]) = \
convert_between( convert_between(
body_format, server.notify_format, body=body, body_format, server.notify_format, content=body)
title=title, title_format=server.title_format)
# Prepare our title
conversion_title_map[server.notify_format] = \
'' if not title else title
# Tidy Title IF required (hence it will become part of the
# body)
if server.title_maxlen <= 0 and \
conversion_title_map[server.notify_format]:
conversion_title_map[server.notify_format] = \
convert_between(
body_format, server.notify_format,
content=conversion_title_map[server.notify_format])
if interpret_escapes: if interpret_escapes:
# #
@ -587,14 +600,6 @@ class Apprise(object):
if six.PY2: if six.PY2:
# Python 2.7 strings must be encoded as utf-8 for # Python 2.7 strings must be encoded as utf-8 for
# consistency across all platforms # consistency across all platforms
if conversion_title_map[server.notify_format] and \
isinstance(
conversion_title_map[server.notify_format],
unicode): # noqa: F821
conversion_title_map[server.notify_format] = \
conversion_title_map[server.notify_format]\
.encode('utf-8')
if conversion_body_map[server.notify_format] and \ if conversion_body_map[server.notify_format] and \
isinstance( isinstance(
conversion_body_map[server.notify_format], conversion_body_map[server.notify_format],
@ -603,12 +608,21 @@ class Apprise(object):
conversion_body_map[server.notify_format]\ conversion_body_map[server.notify_format]\
.encode('utf-8') .encode('utf-8')
if conversion_title_map[server.notify_format] and \
isinstance(
conversion_title_map[server.notify_format],
unicode): # noqa: F821
conversion_title_map[server.notify_format] = \
conversion_title_map[server.notify_format]\
.encode('utf-8')
yield handler( yield handler(
server, server,
body=conversion_body_map[server.notify_format], body=conversion_body_map[server.notify_format],
title=conversion_title_map[server.notify_format], title=conversion_title_map[server.notify_format],
notify_type=notify_type, notify_type=notify_type,
attach=attach attach=attach,
body_format=body_format,
) )
def details(self, lang=None, show_requirements=False, show_disabled=False): def details(self, lang=None, show_requirements=False, show_disabled=False):

View File

@ -28,6 +28,7 @@ import re
import six import six
from markdown import markdown from markdown import markdown
from .common import NotifyFormat from .common import NotifyFormat
from .URLBase import URLBase
if six.PY2: if six.PY2:
from HTMLParser import HTMLParser from HTMLParser import HTMLParser
@ -36,13 +37,12 @@ else:
from html.parser import HTMLParser from html.parser import HTMLParser
def convert_between(from_format, to_format, body, title=None, def convert_between(from_format, to_format, content):
title_format=NotifyFormat.TEXT):
""" """
Converts between different notification formats. If no conversion exists, Converts between different suported formats. If no conversion exists,
or the selected one fails, the original text will be returned. or the selected one fails, the original text will be returned.
This function returns a tuple as (title, body) This function returns the content translated (if required)
""" """
converters = { converters = {
@ -53,106 +53,39 @@ def convert_between(from_format, to_format, body, title=None,
(NotifyFormat.HTML, NotifyFormat.MARKDOWN): html_to_text, (NotifyFormat.HTML, NotifyFormat.MARKDOWN): html_to_text,
} }
if NotifyFormat.MARKDOWN in (from_format, to_format):
# Tidy any exising pre-formating configuration
title = '' if not title else title.lstrip('\r\n \t\v\b*#-')
else:
title = '' if not title else title
convert = converters.get((from_format, to_format)) convert = converters.get((from_format, to_format))
title, body = convert(title=title, body=body, title_format=title_format) \ return convert(content) if convert else content
if convert is not None else (title, body)
return (title, body)
def markdown_to_html(body, title=None, title_format=None): def markdown_to_html(content):
""" """
Handle Markdown conversions Converts specified content from markdown to HTML.
""" """
if title_format == NotifyFormat.HTML and title: return markdown(content)
# perform conversion if otherwise told to do so
title = markdown(title)
return (
# Title
'' if not title else title,
# Body
markdown(body),
)
def text_to_html(body, title=None, title_format=None): def text_to_html(content):
""" """
Converts a notification body from plain text to HTML. Converts specified content from plain text to HTML.
""" """
# Basic TEXT to HTML format map; supports keys only return URLBase.escape_html(content)
re_map = {
# Support Ampersand
r'&': '&amp;',
# Spaces to &nbsp; for formatting purposes since
# multiple spaces are treated as one an this may
# not be the callers intention
r' ': '&nbsp;',
# Tab support
r'\t': '&nbsp;&nbsp;&nbsp;',
# Greater than and Less than Characters
r'>': '&gt;',
r'<': '&lt;',
}
# Compile our map
re_table = re.compile(
r'(' + '|'.join(
map(re.escape, re_map.keys())) + r')',
re.IGNORECASE,
)
# Execute our map against our body in addition to
# swapping out new lines and replacing them with <br/>
return (
# Title; swap whitespace with space
'' if not title else re.sub(
r'[\r\n]+', ' ', re_table.sub(
lambda x: re_map[x.group()], title)),
# Body Formatting
re.sub(
r'\r*\n', '<br/>\n', re_table.sub(
lambda x: re_map[x.group()], body)))
def html_to_text(body, title=None, title_format=None): def html_to_text(content):
""" """
Converts a notification body from HTML to plain text. Converts a content from HTML to plain text.
""" """
parser = HTMLConverter() parser = HTMLConverter()
if six.PY2: if six.PY2:
# Python 2.7 requires an additional parsing to un-escape characters # Python 2.7 requires an additional parsing to un-escape characters
body = parser.unescape(body) content = parser.unescape(content)
if title: parser.feed(content)
if six.PY2:
# Python 2.7 requires an additional parsing to un-escape characters
title = parser.unescape(title)
parser.feed(title)
parser.close() parser.close()
title = parser.converted return parser.converted
parser.feed(body)
parser.close()
body = parser.converted
return ('' if not title else title, body)
class HTMLConverter(HTMLParser, object): class HTMLConverter(HTMLParser, object):

View File

@ -121,13 +121,6 @@ class NotifyBase(BASE_OBJECT):
# automatically placed into the body # automatically placed into the body
title_maxlen = 250 title_maxlen = 250
# Set this to HTML for services that support the conversion of HTML in
# the title. For example; services like Telegram support HTML in the
# title, however services like Email (where this goes in the Subject line)
# do not (but the body does). By default we do not convert titles but
# allow those who wish to over-ride this to do so.
title_format = NotifyFormat.TEXT
# Set the maximum line count; if this is set to anything larger then zero # Set the maximum line count; if this is set to anything larger then zero
# the message (prior to it being sent) will be truncated to this number # the message (prior to it being sent) will be truncated to this number
# of lines. Setting this to zero disables this feature. # of lines. Setting this to zero disables this feature.
@ -272,7 +265,7 @@ class NotifyBase(BASE_OBJECT):
) )
def notify(self, body, title=None, notify_type=NotifyType.INFO, def notify(self, body, title=None, notify_type=NotifyType.INFO,
overflow=None, attach=None, **kwargs): overflow=None, attach=None, body_format=None, **kwargs):
""" """
Performs notification Performs notification
@ -298,18 +291,22 @@ class NotifyBase(BASE_OBJECT):
title = '' if not title else title title = '' if not title else title
# Apply our overflow (if defined) # Apply our overflow (if defined)
for chunk in self._apply_overflow(body=body, title=title, for chunk in self._apply_overflow(
overflow=overflow): body=body, title=title, overflow=overflow,
body_format=body_format):
# Send notification # Send notification
if not self.send(body=chunk['body'], title=chunk['title'], if not self.send(body=chunk['body'], title=chunk['title'],
notify_type=notify_type, attach=attach): notify_type=notify_type, attach=attach,
body_format=body_format):
# Toggle our return status flag # Toggle our return status flag
return False return False
return True return True
def _apply_overflow(self, body, title=None, overflow=None): def _apply_overflow(self, body, title=None, overflow=None,
body_format=None):
""" """
Takes the message body and title as input. This function then Takes the message body and title as input. This function then
applies any defined overflow restrictions associated with the applies any defined overflow restrictions associated with the
@ -341,18 +338,24 @@ class NotifyBase(BASE_OBJECT):
overflow = self.overflow_mode overflow = self.overflow_mode
if self.title_maxlen <= 0 and len(title) > 0: if self.title_maxlen <= 0 and len(title) > 0:
if self.notify_format == NotifyFormat.MARKDOWN:
# Content is appended to body as markdown
body = '**{}**\r\n{}'.format(title, body)
elif self.notify_format == NotifyFormat.HTML: if self.notify_format == NotifyFormat.HTML:
# Content is appended to body as html # Content is appended to body as html
body = '<{open_tag}>{title}</{close_tag}>' \ body = '<{open_tag}>{title}</{close_tag}>' \
'<br />\r\n{body}'.format( '<br />\r\n{body}'.format(
open_tag=self.default_html_tag_id, open_tag=self.default_html_tag_id,
title=self.escape_html(title), title=title,
close_tag=self.default_html_tag_id, close_tag=self.default_html_tag_id,
body=body) body=body)
elif self.notify_format == NotifyFormat.MARKDOWN and \
body_format == NotifyFormat.TEXT:
# Content is appended to body as markdown
title = title.lstrip('\r\n \t\v\f#-')
if title:
# Content is appended to body as text
body = '# {}\r\n{}'.format(title, body)
else: else:
# Content is appended to body as text # Content is appended to body as text
body = '{}\r\n{}'.format(title, body) body = '{}\r\n{}'.format(title, body)

View File

@ -105,8 +105,8 @@ class NotifyTelegram(NotifyBase):
# The maximum allowable characters allowed in the body per message # The maximum allowable characters allowed in the body per message
body_maxlen = 4096 body_maxlen = 4096
# Allow the title to support HTML character sets # Title is to be part of body
title_format = NotifyFormat.HTML title_maxlen = 0
# Telegram is limited to sending a maximum of 100 requests per second. # Telegram is limited to sending a maximum of 100 requests per second.
request_rate_per_sec = 0.001 request_rate_per_sec = 0.001
@ -173,6 +173,49 @@ class NotifyTelegram(NotifyBase):
}, },
) )
# Telegram's HTML support doesn't like having HTML escaped
# characters passed into it. to handle this situation, we need to
# search the body for these sequences and convert them to the
# output the user expected
__telegram_escape_html_dict = {
# New Lines
re.compile(r'<\s*/?br\s*/?>\r*\n?', re.I): '\r\n',
re.compile(r'<\s*/(br|p|div|li)[^>]*>\r*\n?', re.I): '\r\n',
# The following characters can be altered to become supported
re.compile(r'<\s*pre[^>]*>', re.I): '<code>',
re.compile(r'<\s*/pre[^>]*>', re.I): '</code>',
# the following tags are not supported
re.compile(
r'<\s*(br|p|div|span|body|script|meta|html|font'
r'|label|iframe|li|ol|ul|source|script)[^>]*>', re.I): '',
re.compile(
r'<\s*/(span|body|script|meta|html|font'
r'|label|iframe|ol|ul|source|script)[^>]*>', re.I): '',
# Italic
re.compile(r'<\s*(caption|em)[^>]*>', re.I): '<i>',
re.compile(r'<\s*/(caption|em)[^>]*>', re.I): '</i>',
# Bold
re.compile(r'<\s*(h[1-6]|title|strong)[^>]*>', re.I): '<b>',
re.compile(r'<\s*/(h[1-6]|title|strong)[^>]*>', re.I): '</b>',
# HTML Spaces (&nbsp;) and tabs (&emsp;) aren't supported
# See https://core.telegram.org/bots/api#html-style
re.compile(r'\&nbsp;?', re.I): ' ',
# Tabs become 3 spaces
re.compile(r'\&emsp;?', re.I): ' ',
# Some characters get re-escaped by the Telegram upstream
# service so we need to convert these back,
re.compile(r'\&apos;?', re.I): '\'',
re.compile(r'\&quot;?', re.I): '"',
}
# Define our template tokens # Define our template tokens
template_tokens = dict(NotifyBase.template_tokens, **{ template_tokens = dict(NotifyBase.template_tokens, **{
'bot_token': { 'bot_token': {
@ -505,7 +548,7 @@ class NotifyTelegram(NotifyBase):
return 0 return 0
def send(self, body, title='', notify_type=NotifyType.INFO, attach=None, def send(self, body, title='', notify_type=NotifyType.INFO, attach=None,
**kwargs): body_format=None, **kwargs):
""" """
Perform Telegram Notification Perform Telegram Notification
""" """
@ -548,93 +591,43 @@ class NotifyTelegram(NotifyBase):
if self.notify_format == NotifyFormat.MARKDOWN: if self.notify_format == NotifyFormat.MARKDOWN:
payload['parse_mode'] = 'MARKDOWN' payload['parse_mode'] = 'MARKDOWN'
payload['text'] = '{}{}'.format( payload['text'] = body
'# {}\r\n'.format(title) if title else '',
body, else: # HTML
)
elif self.notify_format == NotifyFormat.HTML:
# Use Telegram's HTML mode # Use Telegram's HTML mode
payload['parse_mode'] = 'HTML' payload['parse_mode'] = 'HTML'
for r, v in self.__telegram_escape_html_dict.items():
body = r.sub(v, body, re.I)
# Telegram's HTML support doesn't like having HTML escaped # Prepare our payload based on HTML or TEXT
# characters passed into it. to handle this situation, we need to payload['text'] = body
# search the body for these sequences and convert them to the
# output the user expected
telegram_escape_html_dict = {
# HTML Spaces (&nbsp;) and tabs (&emsp;) aren't supported
# See https://core.telegram.org/bots/api#html-style
r'\&nbsp;?': ' ',
# Tabs become 3 spaces # else: # self.notify_format == NotifyFormat.TEXT:
r'\&emsp;?': ' ', # # Use Telegram's HTML mode
# payload['parse_mode'] = 'HTML'
# Some characters get re-escaped by the Telegram upstream # # Further html escaping required...
# service so we need to convert these back, # telegram_escape_text_dict = {
r'\&apos;?': '\'', # # We need to escape characters that conflict with html
r'\&quot;?': '"', # # entity blocks (< and >) when displaying text
# r'>': '&gt;',
# r'<': '&lt;',
# r'\&': '&amp;',
# }
# the following tags are not supported # # Create a regular expression from the dictionary keys
r'<[ \t]*/?(br|p|div|span|body|script|meta|html|font' # text_regex = re.compile("(%s)" % "|".join(
r'|label|iframe|li|ol|ul)[^>]*>': '', # map(re.escape, telegram_escape_text_dict.keys())).lower(),
# re.I)
# The following characters can be altered to become supported # # For each match, look-up corresponding value in dictionary
r'<[ \t]*pre[^>]*>': '<code>', # body = text_regex.sub( # pragma: no branch
r'<[ \t]*/pre[^>]*>': '</code>', # lambda mo: telegram_escape_text_dict[
# mo.string[mo.start():mo.end()]], body)
# Bold # # prepare our payload based on HTML or TEXT
r'<[ \t]*(h[0-9]+|title|strong)[^>]*>': '<b>', # payload['text'] = body
r'<[ \t]*/(h[0-9]+|title|strong)[^>]*>': '</b>',
# Italic
r'<[ \t]*(caption|em)[^>]*>': '<i>',
r'<[ \t]*/(caption|em)[^>]*>': '</i>',
}
for k, v in telegram_escape_html_dict.items():
body = re.sub(k, v, body, re.I)
if title:
title = re.sub(k, v, title, re.I)
# prepare our payload based on HTML or TEXT
payload['text'] = '{}{}'.format(
'<b>{}</b>\r\n'.format(title) if title else '',
body,
)
else: # self.notify_format == NotifyFormat.TEXT:
# Use Telegram's HTML mode
payload['parse_mode'] = 'HTML'
# Further html escaping required...
telegram_escape_text_dict = {
# We need to escape characters that conflict with html
# entity blocks (< and >) when displaying text
r'>': '&gt;',
r'<': '&lt;',
}
# Create a regular expression from the dictionary keys
text_regex = re.compile("(%s)" % "|".join(
map(re.escape, telegram_escape_text_dict.keys())).lower(),
re.I)
# For each match, look-up corresponding value in dictionary
body = text_regex.sub( # pragma: no branch
lambda mo: telegram_escape_text_dict[
mo.string[mo.start():mo.end()]], body)
if title:
# For each match, look-up corresponding value in dictionary
title = text_regex.sub( # pragma: no branch
lambda mo: telegram_escape_text_dict[
mo.string[mo.start():mo.end()]], title)
# prepare our payload based on HTML or TEXT
payload['text'] = '{}{}'.format(
'<b>{}</b>\r\n'.format(title) if title else '',
body,
)
# Create a copy of the chat_ids list # Create a copy of the chat_ids list
targets = list(self.targets) targets = list(self.targets)

View File

@ -702,7 +702,7 @@ def parse_url(url, default_schema='http', verify_host=True, strict_port=False):
# Port Parsing # Port Parsing
pmatch = re.search( pmatch = re.search(
r'^(?P<host>([[0-9a-f:]+]|[^:]+)):(?P<port>[^:]*)$', r'^(?P<host>(\[[0-9a-f:]+\]|[^:]+)):(?P<port>[^:]*)$',
result['host']) result['host'])
if pmatch: if pmatch:

View File

@ -32,7 +32,7 @@ import logging
logging.disable(logging.CRITICAL) logging.disable(logging.CRITICAL)
def test_html_to_text(): def test_conversion_html_to_text():
"""conversion: Test HTML to plain text """conversion: Test HTML to plain text
""" """
@ -40,7 +40,7 @@ def test_html_to_text():
""" """
A function to simply html conversion tests A function to simply html conversion tests
""" """
return convert_between(NotifyFormat.HTML, NotifyFormat.TEXT, body)[1] return convert_between(NotifyFormat.HTML, NotifyFormat.TEXT, body)
assert to_html("No HTML code here.") == "No HTML code here." assert to_html("No HTML code here.") == "No HTML code here."
@ -134,3 +134,16 @@ def test_html_to_text():
with pytest.raises(TypeError): with pytest.raises(TypeError):
# Invalid input # Invalid input
assert to_html(object) assert to_html(object)
def test_conversion_text_to():
"""conversion: Test Text to all types
"""
response = convert_between(
NotifyFormat.TEXT, NotifyFormat.HTML,
"<title>Test Message</title><body>Body</body>")
assert response == \
'&lt;title&gt;Test&nbsp;Message&lt;/title&gt;&lt;body&gt;Body&lt;'\
'/body&gt;'

View File

@ -311,10 +311,6 @@ def test_plugin_telegram_general(mock_post):
# ensures our plugin inheritance is working properly # ensures our plugin inheritance is working properly
assert obj.body_maxlen == plugins.NotifyTelegram.body_maxlen assert obj.body_maxlen == plugins.NotifyTelegram.body_maxlen
# We don't override the title maxlen so we should be set to the same
# as our parent class in this case
assert obj.title_maxlen == plugins.NotifyBase.title_maxlen
# This tests erroneous messages involving multiple chat ids # This tests erroneous messages involving multiple chat ids
assert obj.notify( assert obj.notify(
body='body', title='title', notify_type=NotifyType.INFO) is False body='body', title='title', notify_type=NotifyType.INFO) is False
@ -407,7 +403,7 @@ def test_plugin_telegram_general(mock_post):
# Test our payload # Test our payload
assert payload['text'] == \ assert payload['text'] == \
'<b>special characters</b>\r\n\'"This can\'t\t\r\nfail us"\'' '<b>special characters</b>\r\n\'"This can\'t\t\r\nfail us"\'\r\n'
# Test sending attachments # Test sending attachments
attach = AppriseAttachment(os.path.join(TEST_VAR_DIR, 'apprise-test.gif')) attach = AppriseAttachment(os.path.join(TEST_VAR_DIR, 'apprise-test.gif'))
@ -629,10 +625,11 @@ def test_plugin_telegram_formating_py3(mock_post):
# Test that everything is escaped properly in a TEXT mode # Test that everything is escaped properly in a TEXT mode
assert payload['text'] == \ assert payload['text'] == \
'<b>🚨 Change detected&nbsp;for&nbsp;&lt;i&gt;Apprise&nbsp;Test' \ '<b>🚨 Change detected&nbsp;for&nbsp;&lt;i&gt;Apprise&nbsp;' \
'&nbsp;Title&lt;/i&gt;</b>\r\n&lt;a href="http://localhost"&gt;' \ 'Test&nbsp;Title&lt;/i&gt;</b>\r\n&lt;a&nbsp;href=' \
'&lt;i&gt;Apprise Body&nbsp;Title&lt;/i&gt;&lt;/a&gt;&nbsp;had' \ '"http://localhost"&gt;&lt;i&gt;Apprise&nbsp;Body&nbsp;Title&lt;' \
'&nbsp;&lt;a&nbsp;href="http://127.0.0.1"&gt;a&nbsp;change&lt;/a&gt;' '/i&gt;&lt;/a&gt;&nbsp;had&nbsp;&lt;a&nbsp;href=&quot;http://' \
'127.0.0.1&quot;&gt;a&nbsp;change&lt;/a&gt;'
# Reset our values # Reset our values
mock_post.reset_mock() mock_post.reset_mock()
@ -699,7 +696,12 @@ def test_plugin_telegram_formating_py3(mock_post):
aobj.add('tgram://987654321:abcdefg_hijklmnop/?format=html') aobj.add('tgram://987654321:abcdefg_hijklmnop/?format=html')
assert len(aobj) == 1 assert len(aobj) == 1
# HTML forced by the command line, but MARKDOWN spacified as # Now test our MARKDOWN Handling
title = '# 🚨 Another Change detected for _Apprise Test Title_'
body = '_[Apprise Body Title](http://localhost)_' \
' had [a change](http://127.0.0.2)'
# HTML forced by the command line, but MARKDOWN specified as
# upstream mode # upstream mode
assert aobj.notify( assert aobj.notify(
title=title, body=body, body_format=NotifyFormat.MARKDOWN) title=title, body=body, body_format=NotifyFormat.MARKDOWN)
@ -716,9 +718,79 @@ def test_plugin_telegram_formating_py3(mock_post):
# Test that everything is escaped properly in a HTML mode # Test that everything is escaped properly in a HTML mode
assert payload['text'] == \ assert payload['text'] == \
'<b>🚨 Change detected for <i>Apprise Test Title</i></b>\r\n<i>' \ '<b><b>🚨 Another Change detected for <i>Apprise Test Title</i>' \
'<a href="http://localhost">Apprise Body Title</a></i> ' \ '</b></b>\r\n<i><a href="http://localhost">Apprise Body Title</a>' \
'had <a href="http://127.0.0.1">a change</a>' '</i> had <a href="http://127.0.0.2">a change</a>\r\n'
# Now we'll test an edge case where a title was defined, but after
# processing it, it was determiend there really wasn't anything there
# at all at the end of the day.
# Reset our values
mock_post.reset_mock()
# Upstream to use HTML but input specified as Markdown
aobj = Apprise()
aobj.add('tgram://987654321:abcdefg_hijklmnop/?format=markdown')
assert len(aobj) == 1
# Now test our MARKDOWN Handling (no title defined... not really anyway)
title = '# '
body = '_[Apprise Body Title](http://localhost)_' \
' had [a change](http://127.0.0.2)'
# MARKDOWN forced by the command line, but TEXT specified as
# upstream mode
assert aobj.notify(
title=title, body=body, body_format=NotifyFormat.TEXT)
# Test our calls
assert mock_post.call_count == 2
assert mock_post.call_args_list[0][0][0] == \
'https://api.telegram.org/bot987654321:abcdefg_hijklmnop/getUpdates'
assert mock_post.call_args_list[1][0][0] == \
'https://api.telegram.org/bot987654321:abcdefg_hijklmnop/sendMessage'
payload = loads(mock_post.call_args_list[1][1]['data'])
# Test that everything is escaped properly in a HTML mode
assert payload['text'] == \
'_[Apprise Body Title](http://localhost)_ had ' \
'[a change](http://127.0.0.2)'
# Reset our values
mock_post.reset_mock()
# Upstream to use HTML but input specified as Markdown
aobj = Apprise()
aobj.add('tgram://987654321:abcdefg_hijklmnop/?format=markdown')
assert len(aobj) == 1
# Set an actual title this time
title = '# A Great Title'
body = '_[Apprise Body Title](http://localhost)_' \
' had [a change](http://127.0.0.2)'
# MARKDOWN forced by the command line, but TEXT specified as
# upstream mode
assert aobj.notify(
title=title, body=body, body_format=NotifyFormat.TEXT)
# Test our calls
assert mock_post.call_count == 2
assert mock_post.call_args_list[0][0][0] == \
'https://api.telegram.org/bot987654321:abcdefg_hijklmnop/getUpdates'
assert mock_post.call_args_list[1][0][0] == \
'https://api.telegram.org/bot987654321:abcdefg_hijklmnop/sendMessage'
payload = loads(mock_post.call_args_list[1][1]['data'])
# Test that everything is escaped properly in a HTML mode
assert payload['text'] == \
'# A Great Title\r\n_[Apprise Body Title](http://localhost)_ had ' \
'[a change](http://127.0.0.2)'
@pytest.mark.skipif(sys.version_info.major >= 3, reason="Requires Python 2.x+") @pytest.mark.skipif(sys.version_info.major >= 3, reason="Requires Python 2.x+")
@ -809,11 +881,11 @@ def test_plugin_telegram_formating_py2(mock_post):
# Test that everything is escaped properly in a TEXT mode # Test that everything is escaped properly in a TEXT mode
assert payload['text'].encode('utf-8') == \ assert payload['text'].encode('utf-8') == \
'<b>\xf0\x9f\x9a\xa8 Change detected&nbsp;for&nbsp;' \ '<b>\xf0\x9f\x9a\xa8 Change detected&nbsp;for&nbsp;&lt;i&gt;' \
'&lt;i&gt;Apprise&nbsp;Test&nbsp;Title&lt;/i&gt;</b>\r\n' \ 'Apprise&nbsp;Test&nbsp;Title&lt;/i&gt;</b>\r\n&lt;a&nbsp;' \
'&lt;a href="http://localhost"&gt;&lt;i&gt;Apprise Body&nbsp;' \ 'href="http://localhost"&gt;&lt;i&gt;Apprise&nbsp;Body&nbsp;' \
'Title&lt;/i&gt;&lt;/a&gt;&nbsp;had&nbsp;&lt;a&nbsp;' \ 'Title&lt;/i&gt;&lt;/a&gt;&nbsp;had&nbsp;&lt;a&nbsp;href=&quot;' \
'href="http://127.0.0.1"&gt;a&nbsp;change&lt;/a&gt;' 'http://127.0.0.1&quot;&gt;a&nbsp;change&lt;/a&gt;'
# Reset our values # Reset our values
mock_post.reset_mock() mock_post.reset_mock()
@ -880,7 +952,7 @@ def test_plugin_telegram_formating_py2(mock_post):
aobj.add('tgram://987654321:abcdefg_hijklmnop/?format=html') aobj.add('tgram://987654321:abcdefg_hijklmnop/?format=html')
assert len(aobj) == 1 assert len(aobj) == 1
# HTML forced by the command line, but MARKDOWN spacified as # HTML forced by the command line, but MARKDOWN specified as
# upstream mode # upstream mode
assert aobj.notify( assert aobj.notify(
title=title, body=body, body_format=NotifyFormat.MARKDOWN) title=title, body=body, body_format=NotifyFormat.MARKDOWN)
@ -897,9 +969,10 @@ def test_plugin_telegram_formating_py2(mock_post):
# Test that everything is escaped properly in a HTML mode # Test that everything is escaped properly in a HTML mode
assert payload['text'].encode('utf-8') == \ assert payload['text'].encode('utf-8') == \
'<b>\xf0\x9f\x9a\xa8 Change detected for <i>Apprise Test Title</i>' \ '<b><b>\xf0\x9f\x9a\xa8 Change detected for ' \
'</b>\r\n<i><a href="http://localhost">Apprise Body Title</a></i> ' \ '<i>Apprise Test Title</i></b></b>\r\n<i>' \
'had <a href="http://127.0.0.1">a change</a>' '<a href="http://localhost">Apprise Body Title</a>'\
'</i> had <a href="http://127.0.0.1">a change</a>\r\n'
# Reset our values # Reset our values
mock_post.reset_mock() mock_post.reset_mock()
@ -951,6 +1024,76 @@ def test_plugin_telegram_formating_py2(mock_post):
'\xd7\xa0\xd7\xa4\xd7\x9c\xd7\x90\xd7\x94</b>\r\n[_[\xd7\x96\xd7\x95 '\ '\xd7\xa0\xd7\xa4\xd7\x9c\xd7\x90\xd7\x94</b>\r\n[_[\xd7\x96\xd7\x95 '\
'\xd7\x94\xd7\x95\xd7\x93\xd7\xa2\xd7\x94](http://localhost)_' '\xd7\x94\xd7\x95\xd7\x93\xd7\xa2\xd7\x94](http://localhost)_'
# Now we'll test an edge case where a title was defined, but after
# processing it, it was determiend there really wasn't anything there
# at all at the end of the day.
# Reset our values
mock_post.reset_mock()
# Upstream to use HTML but input specified as Markdown
aobj = Apprise()
aobj.add('tgram://987654321:abcdefg_hijklmnop/?format=markdown')
assert len(aobj) == 1
# Now test our MARKDOWN Handling (no title defined... not really anyway)
title = '# '
body = '_[Apprise Body Title](http://localhost)_' \
' had [a change](http://127.0.0.2)'
# MARKDOWN forced by the command line, but TEXT specified as
# upstream mode
assert aobj.notify(
title=title, body=body, body_format=NotifyFormat.TEXT)
# Test our calls
assert mock_post.call_count == 2
assert mock_post.call_args_list[0][0][0] == \
'https://api.telegram.org/bot987654321:abcdefg_hijklmnop/getUpdates'
assert mock_post.call_args_list[1][0][0] == \
'https://api.telegram.org/bot987654321:abcdefg_hijklmnop/sendMessage'
payload = loads(mock_post.call_args_list[1][1]['data'])
# Test that everything is escaped properly in a HTML mode
assert payload['text'] == \
'_[Apprise Body Title](http://localhost)_ had ' \
'[a change](http://127.0.0.2)'
# Reset our values
mock_post.reset_mock()
# Upstream to use HTML but input specified as Markdown
aobj = Apprise()
aobj.add('tgram://987654321:abcdefg_hijklmnop/?format=markdown')
assert len(aobj) == 1
# Set an actual title this time
title = '# A Great Title'
body = '_[Apprise Body Title](http://localhost)_' \
' had [a change](http://127.0.0.2)'
# MARKDOWN forced by the command line, but TEXT specified as
# upstream mode
assert aobj.notify(
title=title, body=body, body_format=NotifyFormat.TEXT)
# Test our calls
assert mock_post.call_count == 2
assert mock_post.call_args_list[0][0][0] == \
'https://api.telegram.org/bot987654321:abcdefg_hijklmnop/getUpdates'
assert mock_post.call_args_list[1][0][0] == \
'https://api.telegram.org/bot987654321:abcdefg_hijklmnop/sendMessage'
payload = loads(mock_post.call_args_list[1][1]['data'])
# Test that everything is escaped properly in a HTML mode
assert payload['text'] == \
'# A Great Title\r\n_[Apprise Body Title](http://localhost)_ had ' \
'[a change](http://127.0.0.2)'
@mock.patch('requests.post') @mock.patch('requests.post')
def test_plugin_telegram_html_formatting(mock_post): def test_plugin_telegram_html_formatting(mock_post):
@ -1020,8 +1163,8 @@ def test_plugin_telegram_html_formatting(mock_post):
# Test that everything is escaped properly in a HTML mode # Test that everything is escaped properly in a HTML mode
assert payload['text'] == \ assert payload['text'] == \
'<b><b>\'information\'</b></b>\r\n<i>"This is in Italic"</i>' \ '<b><b>\'information\'</b></b>\r\n<i>"This is in Italic"' \
'<b> Headings are dropped and converted to bold</b>' '</i>\r\n<b> Headings are dropped and converted to bold</b>'
mock_post.reset_mock() mock_post.reset_mock()
@ -1034,6 +1177,7 @@ def test_plugin_telegram_html_formatting(mock_post):
assert payload['text'] == \ assert payload['text'] == \
'<b>&lt;title&gt;&amp;apos;information&amp;apos&lt;/title&gt;</b>' \ '<b>&lt;title&gt;&amp;apos;information&amp;apos&lt;/title&gt;</b>' \
'\r\n&lt;em&gt;&amp;quot;This is in&nbsp;Italic&amp;quot&lt;/em&gt;' \ '\r\n&lt;em&gt;&amp;quot;This is in&nbsp;Italic&amp;quot&lt;/em' \
'&lt;br/&gt;&lt;h5&gt;&amp;emsp;&amp;emspHeadings&amp;nbsp;are' \ '&gt;&lt;br/&gt;&lt;h5&gt;&amp;emsp;&amp;emspHeadings&amp;nbsp;' \
'&nbsp;dropped&nbsp;and&amp;nbspconverted&nbsp;to&nbsp;bold&lt;/h5&gt;' 'are&nbsp;dropped&nbsp;and&amp;nbspconverted&nbsp;to&nbsp;bold&lt;' \
'/h5&gt;'

View File

@ -386,3 +386,57 @@ def test_notify_overflow_split():
_body = chunk.get('body') _body = chunk.get('body')
assert bulk[offset: len(_body) + offset] == _body assert bulk[offset: len(_body) + offset] == _body
offset += len(_body) offset += len(_body)
def test_notify_overflow_general():
"""
API: Overflow General Testing
"""
#
# A little preparation
#
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
#
# First Test: Truncated Title
#
class TestMarkdownNotification(NotifyBase):
# Force our title to wrap
title_maxlen = 0
# Default Notify Format
notify_format = NotifyFormat.MARKDOWN
def __init__(self, *args, **kwargs):
super(TestMarkdownNotification, self).__init__(**kwargs)
def notify(self, *args, **kwargs):
# Pretend everything is okay
return True
# Load our object
obj = TestMarkdownNotification()
assert obj is not None
# A bad header
title = " # "
body = "**Test Body**"
chunks = obj._apply_overflow(body=body, title=title)
assert len(chunks) == 1
# whitspace is trimmed
assert '#\r\n**Test Body**' == chunks[0].get('body')
assert chunks[0].get('title') == ""
# If we know our input is text however, we perform manipulation
chunks = obj._apply_overflow(
body=body, title=title, body_format=NotifyFormat.TEXT)
assert len(chunks) == 1
# Our title get's stripped off since it's not of valid markdown
assert body == chunks[0].get('body')
assert chunks[0].get('title') == ""