Escape unsupported Telegram MarkdownV2 characters if required (#1181)

This commit is contained in:
Chris Caron 2024-08-05 16:49:05 -04:00 committed by GitHub
parent 1ee08d14fe
commit 5cee11ac84
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 236 additions and 17 deletions

View File

@ -368,7 +368,7 @@ class NotifyTelegram(NotifyBase):
'name': _('Markdown Version'), 'name': _('Markdown Version'),
'type': 'choice:string', 'type': 'choice:string',
'values': ('v1', 'v2'), 'values': ('v1', 'v2'),
'default': 'v2', 'default': 'v1',
}, },
'to': { 'to': {
'alias_of': 'targets', 'alias_of': 'targets',
@ -764,6 +764,16 @@ class NotifyTelegram(NotifyBase):
# Prepare Message Body # Prepare Message Body
if self.notify_format == NotifyFormat.MARKDOWN: if self.notify_format == NotifyFormat.MARKDOWN:
if body_format not in (None, NotifyFormat.MARKDOWN) \
and self.markdown_ver == TelegramMarkdownVersion.TWO:
# Telegram Markdown v2 is not very accomodating to some
# characters such as the hashtag (#) which is fine in v1.
# To try and be accomodating we escape them in advance
# See: https://stackoverflow.com/a/69892704/355584
# Also: https://core.telegram.org/bots/api#markdownv2-style
body = re.sub(r'(?<!\\)([_*[\]()~`>#+=|{}.!-])', r'\\\1', body)
_payload['parse_mode'] = self.markdown_ver _payload['parse_mode'] = self.markdown_ver
_payload['text'] = body _payload['text'] = body

View File

@ -695,7 +695,38 @@ def test_plugin_telegram_formatting(mock_post):
' had [a change](http://127.0.0.1)' ' had [a change](http://127.0.0.1)'
aobj = Apprise() aobj = Apprise()
aobj.add('tgram://123456789:abcdefg_hijklmnop/?format=markdown') aobj.add('tgram://123456789:abcdefg_hijklmnop/?format=markdown&mdv=2')
assert len(aobj) == 1
assert aobj.notify(
title=title, body=body, body_format=NotifyFormat.MARKDOWN)
# Test our calls
assert mock_post.call_count == 2
assert mock_post.call_args_list[0][0][0] == \
'https://api.telegram.org/bot123456789:abcdefg_hijklmnop/getUpdates'
assert mock_post.call_args_list[1][0][0] == \
'https://api.telegram.org/bot123456789: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'] == \
'# 🚨 Change detected for _Apprise Test Title_\r\n' \
'_[Apprise Body Title](http://localhost)_ had ' \
'[a change](http://127.0.0.1)'
# Reset our values
mock_post.reset_mock()
# Now test our MARKDOWN Handling
title = '# 🚨 Change detected for _Apprise Test Title_'
body = '_[Apprise Body Title](http://localhost)_' \
' had [a change](http://127.0.0.1)'
aobj = Apprise()
aobj.add('tgram://123456789:abcdefg_hijklmnop/?format=markdown&mdv=1')
assert len(aobj) == 1 assert len(aobj) == 1
assert aobj.notify( assert aobj.notify(
@ -759,9 +790,9 @@ def test_plugin_telegram_formatting(mock_post):
# Reset our values # Reset our values
mock_post.reset_mock() mock_post.reset_mock()
# Upstream to use HTML but input specified as Markdown # Upstream to use HTML but input specified as Markdown v1
aobj = Apprise() aobj = Apprise()
aobj.add('tgram://987654321:abcdefg_hijklmnop/?format=markdown') aobj.add('tgram://987654321:abcdefg_hijklmnop/?format=markdown&mdv=1')
assert len(aobj) == 1 assert len(aobj) == 1
# Now test our MARKDOWN Handling (no title defined... not really anyway) # Now test our MARKDOWN Handling (no title defined... not really anyway)
@ -784,24 +815,17 @@ def test_plugin_telegram_formatting(mock_post):
payload = loads(mock_post.call_args_list[1][1]['data']) payload = loads(mock_post.call_args_list[1][1]['data'])
# Test that everything is escaped properly in a HTML mode # Test that everything is escaped properly in a MARKDOWN mode
assert payload['text'] == \ assert payload['text'] == body
'_[Apprise Body Title](http://localhost)_ had ' \
'[a change](http://127.0.0.2)'
# Reset our values # Reset our values
mock_post.reset_mock() mock_post.reset_mock()
# Upstream to use HTML but input specified as Markdown # Upstream to use HTML but input specified as Markdown v2
aobj = Apprise() aobj = Apprise()
aobj.add('tgram://987654321:abcdefg_hijklmnop/?format=markdown') aobj.add('tgram://987654321:abcdefg_hijklmnop/?format=markdown&mdv=2')
assert len(aobj) == 1 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 # MARKDOWN forced by the command line, but TEXT specified as
# upstream mode # upstream mode
assert aobj.notify( assert aobj.notify(
@ -817,9 +841,115 @@ def test_plugin_telegram_formatting(mock_post):
payload = loads(mock_post.call_args_list[1][1]['data']) payload = loads(mock_post.call_args_list[1][1]['data'])
# Test that everything is escaped properly in a HTML mode # Test that everything is escaped properly in a MARKDOWN mode
assert payload['text'] == \ assert payload['text'] == \
'# A Great Title\r\n_[Apprise Body Title](http://localhost)_ had ' \ '\\_\\[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 v1
aobj = Apprise()
aobj.add('tgram://987654321:abcdefg_hijklmnop/?format=markdown&mdv=1')
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)'
# TEXT forced by the command line, but MARKDOWN 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 MARKDOWN mode
assert payload['text'] == \
'# A Great Title\r\n' \
'_[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 v2
aobj = Apprise()
aobj.add('tgram://987654321:abcdefg_hijklmnop/?format=markdown&mdv=2')
assert len(aobj) == 1
# TEXT forced by the command line, but MARKDOWN 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 MARKDOWN mode
assert payload['text'] == \
'\\# A Great Title\r\n' \
'\\_\\[Apprise Body Title\\]\\(http://localhost\\)\\_ had ' \
'\\[a change\\]\\(http://127\\.0\\.0\\.2\\)'
# Reset our values
mock_post.reset_mock()
# If input is markdown and output is v2, it is expected the user knows
# what he is doing... no esaping takes place
assert aobj.notify(
title=title, body=body, body_format=NotifyFormat.MARKDOWN)
# Test our calls
assert mock_post.call_count == 1
assert mock_post.call_args_list[0][0][0] == \
'https://api.telegram.org/bot987654321:abcdefg_hijklmnop/sendMessage'
payload = loads(mock_post.call_args_list[0][1]['data'])
# No escaping in this circumstance
assert payload['text'] == \
'# A Great Title\r\n' \
'_[Apprise Body Title](http://localhost)_ had ' \
'[a change](http://127.0.0.2)'
# Reset our values
mock_post.reset_mock()
# No body format specified at all... user definitely must know what
# they are doing... still no escaping in this circumstance
assert aobj.notify(title=title, body=body)
# Test our calls
assert mock_post.call_count == 1
assert mock_post.call_args_list[0][0][0] == \
'https://api.telegram.org/bot987654321:abcdefg_hijklmnop/sendMessage'
payload = loads(mock_post.call_args_list[0][1]['data'])
# No escaping in this circumstance
assert payload['text'] == \
'# A Great Title\r\n' \
'_[Apprise Body Title](http://localhost)_ had ' \
'[a change](http://127.0.0.2)' '[a change](http://127.0.0.2)'
# Reset our values # Reset our values
@ -1118,3 +1248,82 @@ def test_plugin_telegram_threads(mock_post):
assert payload['message_thread_id'] == 1234 assert payload['message_thread_id'] == 1234
mock_post.reset_mock() mock_post.reset_mock()
@mock.patch('requests.post')
def test_plugin_telegram_markdown_v2(mock_post):
"""
NotifyTelegram() MarkdownV2
"""
# Prepare Mock
mock_post.return_value = requests.Request()
mock_post.return_value.status_code = requests.codes.ok
# Simple success response
mock_post.return_value.content = dumps({
"ok": True,
"result": [{
"update_id": 645421321,
"message": {
"message_id": 2,
"from": {
"id": 532389719,
"is_bot": False,
"first_name": "Chris",
"language_code": "en-US"
},
"chat": {
"id": 532389719,
"first_name": "Chris",
"type": "private"
},
"date": 1519694394,
"text": "/start",
"entities": [{
"offset": 0,
"length": 6,
"type": "bot_command",
}],
}},
],
})
aobj = Apprise()
aobj.add('tgram://123456789:abcdefg_hijklmnop/?mdv=2&format=markdown')
assert len(aobj) == 1
assert isinstance(aobj[0], NotifyTelegram)
body = '# my message\r\n## more content\r\n\\# already escaped hashtag'
# Test with body format set to markdown
assert aobj.notify(body=body, body_format=NotifyFormat.TEXT)
# 1 call to look up bot owner, and second for notification
assert mock_post.call_count == 2
payload = loads(mock_post.call_args_list[1][1]['data'])
# Our content is escapped properly
assert payload['text'] == '\\# my message\r\n' \
'\\#\\# more content\r\n\\# already escaped hashtag'
mock_post.reset_mock()
# We'll iterate over all of the bad unsupported characters
mdv2_unsupported = (
'_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '=', '|', '{',
'}', '.', '!', '-')
for c in mdv2_unsupported:
body = f'bad character: {c}, and already escapped \\{c}'
# Test with body format set to markdown
assert aobj.notify(body=body, body_format=NotifyFormat.TEXT)
assert mock_post.call_count == 1
payload = loads(mock_post.call_args_list[0][1]['data'])
# Our content is escapped properly
assert payload['text'] == \
f'bad character: \\{c}, and already escapped \\{c}'
mock_post.reset_mock()