# BSD 2-Clause License # # Apprise - Push Notification Library. # Copyright (c) 2025, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # 2. Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. from json import dumps, loads # Disable logging for a cleaner testing output import logging import os from unittest import mock from helpers import AppriseURLTester import pytest import requests from apprise import ( Apprise, AppriseAsset, AppriseAttachment, NotifyFormat, NotifyType, ) from apprise.plugins.telegram import NotifyTelegram logging.disable(logging.CRITICAL) # Attachment Directory TEST_VAR_DIR = os.path.join(os.path.dirname(__file__), "var") # Our Testing URLs apprise_url_tests = ( ################################## # NotifyTelegram ################################## ( "tgram://", { "instance": None, }, ), # Simple Message ( "tgram://123456789:abcdefg_hijklmnop/lead2gold/", { "instance": NotifyTelegram, }, ), # Simple Message (no images) ( "tgram://123456789:abcdefg_hijklmnop/lead2gold/", { "instance": NotifyTelegram, # don't include an image by default "include_image": False, }, ), # Simple Message with multiple chat names ( "tgram://123456789:abcdefg_hijklmnop/id1/id2/", { "instance": NotifyTelegram, }, ), # Simple Message with multiple chat names ( "tgram://123456789:abcdefg_hijklmnop/?to=id1,id2", { "instance": NotifyTelegram, }, ), # Simple Message with an invalid chat ID ( "tgram://123456789:abcdefg_hijklmnop/%$/", { "instance": NotifyTelegram, # Notify will fail "response": False, }, ), # Simple Message with multiple chat ids ( "tgram://123456789:abcdefg_hijklmnop/id1/id2/23423/-30/", { "instance": NotifyTelegram, }, ), # Simple Message with multiple chat ids (no images) ( "tgram://123456789:abcdefg_hijklmnop/id1/id2/23423/-30/", { "instance": NotifyTelegram, # don't include an image by default "include_image": False, }, ), # Support bot keyword prefix ( "tgram://bottest@123456789:abcdefg_hijklmnop/lead2gold/", { "instance": NotifyTelegram, }, ), # Support Thread Topics ( "tgram://bottest@123456789:abcdefg_hijklmnop/id1/?topic=12345", { "instance": NotifyTelegram, }, ), # Thread is just an alias of topic ( "tgram://bottest@123456789:abcdefg_hijklmnop/id1/?thread=12345", { "instance": NotifyTelegram, }, ), # Threads must be numeric ( "tgram://bottest@123456789:abcdefg_hijklmnop/id1/?topic=invalid", { "instance": TypeError, }, ), # content must be 'before' or 'after' ( "tgram://bottest@123456789:abcdefg_hijklmnop/id1/?content=invalid", { "instance": TypeError, }, ), ( "tgram://bottest@123456789:abcdefg_hijklmnop/id1:invalid/?thread=12345", { "instance": NotifyTelegram, # Notify will fail (bad target) "response": False, }, ), # Testing image ( "tgram://123456789:abcdefg_hijklmnop/lead2gold/?image=Yes", { "instance": NotifyTelegram, }, ), # Testing invalid format (fall's back to html) ( "tgram://123456789:abcdefg_hijklmnop/lead2gold/?format=invalid", { "instance": NotifyTelegram, }, ), # Testing empty format (falls back to html) ( "tgram://123456789:abcdefg_hijklmnop/lead2gold/?format=", { "instance": NotifyTelegram, }, ), # Testing valid formats ( "tgram://123456789:abcdefg_hijklmnop/lead2gold/?format=markdown", { "instance": NotifyTelegram, }, ), ( "tgram://123456789:abcdefg_hijklmnop/lead2gold/?format=markdown&mdv=v1", { "instance": NotifyTelegram, }, ), ( "tgram://123456789:abcdefg_hijklmnop/lead2gold/?format=markdown&mdv=v2", { "instance": NotifyTelegram, }, ), ( "tgram://123456789:abcdefg_hijklmnop/l2g/?format=markdown&mdv=bad", { # Defaults to v2 "instance": NotifyTelegram, }, ), ( "tgram://123456789:abcdefg_hijklmnop/lead2gold/?format=html", { "instance": NotifyTelegram, }, ), ( "tgram://123456789:abcdefg_hijklmnop/lead2gold/?format=text", { "instance": NotifyTelegram, }, ), # Test Silent Settings ( "tgram://123456789:abcdefg_hijklmnop/lead2gold/?silent=yes", { "instance": NotifyTelegram, }, ), ( "tgram://123456789:abcdefg_hijklmnop/lead2gold/?silent=no", { "instance": NotifyTelegram, }, ), # Test Web Page Preview Settings ( "tgram://123456789:abcdefg_hijklmnop/lead2gold/?preview=yes", { "instance": NotifyTelegram, }, ), ( "tgram://123456789:abcdefg_hijklmnop/lead2gold/?preview=no", { "instance": NotifyTelegram, }, ), # Simple Message without image ( "tgram://123456789:abcdefg_hijklmnop/lead2gold/", { "instance": NotifyTelegram, # don't include an image by default "include_image": False, }, ), # Invalid Bot Token ( "tgram://alpha:abcdefg_hijklmnop/lead2gold/", { "instance": None, }, ), # AuthToken + bad url ( "tgram://:@/", { "instance": None, }, ), ( "tgram://123456789:abcdefg_hijklmnop/lead2gold/", { "instance": NotifyTelegram, # force a failure "response": False, "requests_response_code": requests.codes.internal_server_error, }, ), ( "tgram://123456789:abcdefg_hijklmnop/lead2gold/?image=Yes", { "instance": NotifyTelegram, # force a failure without an image specified "include_image": False, "response": False, "requests_response_code": requests.codes.internal_server_error, }, ), ( "tgram://123456789:abcdefg_hijklmnop/id1/id2/", { "instance": NotifyTelegram, # force a failure with multiple chat_ids "response": False, "requests_response_code": requests.codes.internal_server_error, }, ), ( "tgram://123456789:abcdefg_hijklmnop/id1/id2/", { "instance": NotifyTelegram, # force a failure without an image specified "include_image": False, "response": False, "requests_response_code": requests.codes.internal_server_error, }, ), ( "tgram://123456789:abcdefg_hijklmnop/lead2gold/", { "instance": NotifyTelegram, # throw a bizzare code forcing us to fail to look it up "response": False, "requests_response_code": 999, }, ), ( "tgram://123456789:abcdefg_hijklmnop/lead2gold/", { "instance": NotifyTelegram, # throw a bizzare code forcing us to fail to look it up without # having an image included "include_image": False, "response": False, "requests_response_code": 999, }, ), # Test with image set ( "tgram://123456789:abcdefg_hijklmnop/lead2gold/?image=Yes", { "instance": NotifyTelegram, # throw a bizzare code forcing us to fail to look it up without # having an image included "include_image": True, "response": False, "requests_response_code": 999, }, ), ( "tgram://123456789:abcdefg_hijklmnop/lead2gold/", { "instance": NotifyTelegram, # Throws a series of i/o exceptions with this flag # is set and tests that we gracfully handle them "test_requests_exceptions": True, }, ), ( "tgram://123456789:abcdefg_hijklmnop/lead2gold/?image=Yes", { "instance": NotifyTelegram, # Throws a series of i/o exceptions with this flag # is set and tests that we gracfully handle them without images set "include_image": True, "test_requests_exceptions": True, }, ), ) def test_plugin_telegram_urls(): """NotifyTelegram() Apprise URLs.""" # Run our general tests AppriseURLTester(tests=apprise_url_tests).run_all() @mock.patch("requests.post") def test_plugin_telegram_general(mock_post): """NotifyTelegram() General Tests.""" # Bot Token bot_token = "123456789:abcdefg_hijklmnop" invalid_bot_token = "abcd:123" # Chat ID chat_ids = "l2g:1234, lead2gold" # Prepare Mock mock_post.return_value = requests.Request() mock_post.return_value.status_code = requests.codes.ok mock_post.return_value.content = "{}" # Exception should be thrown about the fact no bot token was specified with pytest.raises(TypeError): NotifyTelegram(bot_token=None, targets=chat_ids) # Invalid JSON while trying to detect bot owner mock_post.return_value.content = "}" obj = NotifyTelegram(bot_token=bot_token, targets=None) obj.notify(title="hello", body="world") # Invalid JSON while trying to detect bot owner + 400 error mock_post.return_value.status_code = requests.codes.internal_server_error obj = NotifyTelegram(bot_token=bot_token, targets=None) obj.notify(title="hello", body="world") # Return status back to how they were mock_post.return_value.status_code = requests.codes.ok # Exception should be thrown about the fact an invalid bot token was # specifed with pytest.raises(TypeError): NotifyTelegram(bot_token=invalid_bot_token, targets=chat_ids) obj = NotifyTelegram( bot_token=bot_token, targets=chat_ids, include_image=True ) assert isinstance(obj, NotifyTelegram) is True assert len(obj.targets) == 2 # Test Image Sending Exceptions mock_post.side_effect = OSError() assert not obj.send_media(obj.targets[0], NotifyType.INFO) # Test our other objects mock_post.side_effect = requests.HTTPError assert not obj.send_media(obj.targets[0], NotifyType.INFO) # Restore their entries mock_post.side_effect = None mock_post.return_value.content = "{}" # test url call assert isinstance(obj.url(), str) is True # test privacy version of url assert isinstance(obj.url(privacy=True), str) is True assert obj.url(privacy=True).startswith("tgram://1...p/") is True # Test that we can load the string we generate back: obj = NotifyTelegram(**NotifyTelegram.parse_url(obj.url())) assert isinstance(obj, NotifyTelegram) is True # Prepare Mock to fail response = mock.Mock() response.status_code = requests.codes.internal_server_error # a error response response.content = dumps({ "description": "test", }) mock_post.return_value = response # No image asset nimg_obj = NotifyTelegram(bot_token=bot_token, targets=chat_ids) nimg_obj.asset = AppriseAsset(image_path_mask=False, image_url_mask=False) # Test that our default settings over-ride base settings since they are # not the same as the one specified in the base; this check merely # ensures our plugin inheritance is working properly assert obj.body_maxlen == NotifyTelegram.body_maxlen # This tests erroneous messages involving multiple chat ids assert ( obj.notify(body="body", title="title", notify_type=NotifyType.INFO) is False ) assert ( obj.notify(body="body", title="title", notify_type=NotifyType.INFO) is False ) assert ( nimg_obj.notify( body="body", title="title", notify_type=NotifyType.INFO ) is False ) # This tests erroneous messages involving a single chat id obj = NotifyTelegram(bot_token=bot_token, targets="l2g") nimg_obj = NotifyTelegram(bot_token=bot_token, targets="l2g") nimg_obj.asset = AppriseAsset(image_path_mask=False, image_url_mask=False) assert ( obj.notify(body="body", title="title", notify_type=NotifyType.INFO) is False ) assert ( nimg_obj.notify( body="body", title="title", notify_type=NotifyType.INFO ) is False ) # Bot Token Detection # Just to make it clear to people reading this code and trying to learn # what is going on. Apprise tries to detect the bot owner if you don't # specify a user to message. The idea is to just default to messaging # the bot owner himself (it makes it easier for people). So we're testing # the creating of a Telegram Notification without providing a chat ID. # We're testing the error handling of this bot detection section of the # code mock_post.return_value.content = dumps({ "ok": True, "result": [ { "update_id": 645421319, # Entry without `message` in it }, { # Entry without `from` in `message` "update_id": 645421320, "message": { "message_id": 2, "chat": { "id": 532389719, "first_name": "Chris", "type": "private", }, "date": 1519694394, "text": "/start", "entities": [{ "offset": 0, "length": 6, "type": "bot_command", }], }, }, { "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", }], }, }, ], }) mock_post.return_value.status_code = requests.codes.ok obj = NotifyTelegram(bot_token=bot_token, targets="12345") assert len(obj.targets) == 1 assert obj.targets[0] == (12345, None) # Test the escaping of characters since Telegram escapes stuff for us to # which we need to consider mock_post.reset_mock() body = "

'\"This can't\t\r\nfail us\"'

" assert ( obj.notify( body=body, title="special characters", notify_type=NotifyType.INFO ) is True ) assert mock_post.call_count == 1 payload = loads(mock_post.call_args_list[0][1]["data"]) # Test our payload assert ( payload["text"] == "special characters\r\n'\"This can't\t\r\nfail us\"'\r\n" ) for content in ("before", "after"): # Test our content settings obj = NotifyTelegram( bot_token=bot_token, targets="12345", content=content ) # Reset our mock mock_post.reset_mock() # Test sending attachments attach = AppriseAttachment( os.path.join(TEST_VAR_DIR, "apprise-test.gif") ) assert ( obj.notify( body="body", title="title", notify_type=NotifyType.INFO, attach=attach, ) is True ) # Test large messages assert ( obj.notify( body="a" * (obj.telegram_caption_maxlen + 1), title="title", notify_type=NotifyType.INFO, attach=attach, ) is True ) # An invalid attachment will cause a failure path = os.path.join( TEST_VAR_DIR, "/invalid/path/to/an/invalid/file.jpg" ) attach = AppriseAttachment(path) assert ( obj.notify( body="body", title="title", notify_type=NotifyType.INFO, attach=path, ) is False ) # Test large messages assert ( obj.notify( body="a" * (obj.telegram_caption_maxlen + 1), title="title", notify_type=NotifyType.INFO, attach=path, ) is False ) obj = NotifyTelegram(bot_token=bot_token, targets=None) # No user detected; this happens after our firsst notification assert len(obj.targets) == 0 assert obj.notify(title="hello", body="world") is True assert len(obj.targets) == 1 assert obj.targets[0] == ("532389719", None) # Do the test again, but without the expected (parsed response) mock_post.return_value.content = dumps({ "ok": True, "result": [], }) # No user will be detected now obj = NotifyTelegram(bot_token=bot_token, targets=None) # No user detected; this happens after our firsst notification assert len(obj.targets) == 0 assert obj.notify(title="hello", body="world") is False assert len(obj.targets) == 0 # Do the test again, but with ok not set to True mock_post.return_value.content = dumps({ "ok": False, "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", }], }, }, ], }) # No user will be detected now obj = NotifyTelegram(bot_token=bot_token, targets=None) # No user detected; this happens after our firsst notification assert len(obj.targets) == 0 assert obj.notify(title="hello", body="world") is False assert len(obj.targets) == 0 # An edge case where no results were provided; this will probably never # happen, but it helps with test coverage completeness mock_post.return_value.content = dumps({ "ok": True, }) # No user will be detected now obj = NotifyTelegram(bot_token=bot_token, targets=None) # No user detected; this happens after our firsst notification assert len(obj.targets) == 0 assert obj.notify(title="hello", body="world") is False assert len(obj.targets) == 0 # Detect the bot with a bad response mock_post.return_value.content = dumps({}) obj.detect_bot_owner() # Test our bot detection with a internal server error mock_post.return_value.status_code = requests.codes.internal_server_error # internal server error prevents notification from being sent obj = NotifyTelegram(bot_token=bot_token, targets=None) assert len(obj.targets) == 0 assert obj.notify(title="hello", body="world") is False assert len(obj.targets) == 0 # Test our bot detection with an unmappable html error mock_post.return_value.status_code = 999 NotifyTelegram(bot_token=bot_token, targets=None) assert len(obj.targets) == 0 assert obj.notify(title="hello", body="world") is False assert len(obj.targets) == 0 # Do it again but this time provide a failure message mock_post.return_value.content = dumps({"description": "Failure Message"}) NotifyTelegram(bot_token=bot_token, targets=None) assert len(obj.targets) == 0 assert obj.notify(title="hello", body="world") is False assert len(obj.targets) == 0 # Do it again but this time provide a failure message and perform a # notification without a bot detection by providing at least 1 chat id obj = NotifyTelegram(bot_token=bot_token, targets=["@abcd"]) assert ( nimg_obj.notify( body="body", title="title", notify_type=NotifyType.INFO ) is False ) # iterate over our exceptions and test them mock_post.side_effect = requests.HTTPError # No chat_ids specified obj = NotifyTelegram(bot_token=bot_token, targets=None) assert len(obj.targets) == 0 assert obj.notify(title="hello", body="world") is False assert len(obj.targets) == 0 # Test Telegram Group obj = Apprise.instantiate( "tgram://123456789:ABCdefghijkl123456789opqyz/-123456789525" ) assert isinstance(obj, NotifyTelegram) assert len(obj.targets) == 1 assert (-123456789525, None) in obj.targets @mock.patch("requests.post") def test_plugin_telegram_formatting(mock_post): """NotifyTelegram() formatting tests.""" # Prepare Mock mock_post.return_value = requests.Request() mock_post.return_value.status_code = requests.codes.ok mock_post.return_value.content = "{}" # 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", }], }, }, ], }) mock_post.return_value.status_code = requests.codes.ok results = NotifyTelegram.parse_url("tgram://123456789:abcdefg_hijklmnop/") instance = NotifyTelegram(**results) assert isinstance(instance, NotifyTelegram) response = instance.send(title="title", body="body") assert response is True # 1 call to look up bot owner, and second for notification 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" ) # Reset our values mock_post.reset_mock() # Now test our HTML Conversion as TEXT) aobj = Apprise() aobj.add("tgram://123456789:abcdefg_hijklmnop/") assert len(aobj) == 1 title = "🚨 Change detected for Apprise Test Title" body = ( 'Apprise Body Title' ' had a change' ) 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/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 TEXT mode assert ( payload["text"] == "🚨 Change detected for <i>Apprise Test Title</i>" '\r\n<a href="http://localhost"><i>' "Apprise Body Title</i></a> had <" 'a href="http://127.0.0.1">a change</a>' ) # Reset our values mock_post.reset_mock() # Now test our HTML Conversion as TEXT) aobj = Apprise() aobj.add("tgram://123456789:abcdefg_hijklmnop/?format=html") assert len(aobj) == 1 assert aobj.notify(title=title, body=body, body_format=NotifyFormat.HTML) # 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 had ' 'a change' ) # 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=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 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() # Upstream to use HTML but input specified as Markdown aobj = Apprise() aobj.add("tgram://987654321:abcdefg_hijklmnop/?format=html") assert len(aobj) == 1 # 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 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/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"] == "\r\n🚨 Another Change detected for " "Apprise Test Title\r\n\r\n" 'Apprise Body Title' ' had a change\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 v1 aobj = Apprise() aobj.add("tgram://987654321:abcdefg_hijklmnop/?format=markdown&mdv=1") 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 MARKDOWN mode assert payload["text"] == body # 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 # 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 MARKDOWN 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 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)" ) # Reset our values mock_post.reset_mock() # # Now test that
is correctly escaped # title = "Test Message Title" body = "Test Message Body
ok
" aobj = Apprise() aobj.add("tgram://1234:aaaaaaaaa/-1123456245134") assert len(aobj) == 1 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/bot1234:aaaaaaaaa/sendMessage" ) payload = loads(mock_post.call_args_list[0][1]["data"]) # Test that everything is escaped properly in a HTML mode assert ( payload["text"] == "Test Message Title\r\n\r\nTest Message Body\r\nok\r\n" ) # Reset our values mock_post.reset_mock() # # Now test that
is correctly escaped as it would have been via the # CLI mode where the body_format is TEXT # aobj = Apprise() aobj.add("tgram://1234:aaaaaaaaa/-1123456245134") assert len(aobj) == 1 assert aobj.notify(title=title, body=body, body_format=NotifyFormat.TEXT) # Test our calls assert mock_post.call_count == 1 assert ( mock_post.call_args_list[0][0][0] == "https://api.telegram.org/bot1234:aaaaaaaaa/sendMessage" ) payload = loads(mock_post.call_args_list[0][1]["data"]) # Test that everything is escaped properly in a HTML mode assert ( payload["text"] == "Test Message Title\r\n" "Test Message Body <br/> ok</br>" ) # Reset our values mock_post.reset_mock() # # Now test that
is correctly escaped if fed as HTML # aobj = Apprise() aobj.add("tgram://1234:aaaaaaaaa/-1123456245134") assert len(aobj) == 1 assert aobj.notify(title=title, body=body, body_format=NotifyFormat.HTML) # Test our calls assert mock_post.call_count == 1 assert ( mock_post.call_args_list[0][0][0] == "https://api.telegram.org/bot1234:aaaaaaaaa/sendMessage" ) payload = loads(mock_post.call_args_list[0][1]["data"]) # Test that everything is escaped properly in a HTML mode assert ( payload["text"] == "Test Message Title\r\nTest Message Body\r\nok\r\n" ) @mock.patch("requests.post") def test_plugin_telegram_html_formatting(mock_post): """NotifyTelegram() HTML Formatting.""" # 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/") assert len(aobj) == 1 assert isinstance(aobj[0], NotifyTelegram) # Test our HTML Conversion title = "'information&apos" body = ( ""This is in Italic"
" "
 &emspHeadings are dropped and" " converted to bold
" ) assert aobj.notify(title=title, body=body, body_format=NotifyFormat.HTML) # 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"]) # Test that everything is escaped properly in a HTML mode assert ( payload["text"] == "\r\n'information'\r\n\r\n\"This is in Italic\"" "\r\n Headings are dropped and converted to bold\r\n" ) mock_post.reset_mock() assert aobj.notify(title=title, body=body, body_format=NotifyFormat.TEXT) # owner has already been looked up, so only one call is made assert mock_post.call_count == 1 payload = loads(mock_post.call_args_list[0][1]["data"]) assert ( payload["text"] == "<title>&apos;information&apos</title>" "\r\n<em>&quot;This is in Italic&quot</em><" "br/><h5>&emsp;&emspHeadings&nbsp;are " "dropped and&nbspconverted to bold</h5>" ) # Lest test more complex HTML examples now mock_post.reset_mock() test_file_01 = os.path.join(TEST_VAR_DIR, "01_test_example.html") with open(test_file_01) as html_file: assert aobj.notify( body=html_file.read(), body_format=NotifyFormat.HTML ) # owner has already been looked up, so only one call is made assert mock_post.call_count == 1 payload = loads(mock_post.call_args_list[0][1]["data"]) assert ( payload["text"] == "\r\nBootstrap 101 Template\r\nMy Title\r\n" "Heading 1\r\n-Bullet 1\r\n-Bullet 2\r\n-Bullet 3\r\n" "-Bullet 1\r\n-Bullet 2\r\n-Bullet 3\r\nHeading 2\r\n" "A div entry\r\nA div entry\r\nA pre entry\r\n" "Heading 3\r\nHeading 4\r\nHeading 5\r\n" "Heading 6\r\nA set of text\r\n" "Another line after the set of text\r\nMore text\r\nlabel" ) @mock.patch("requests.post") def test_plugin_telegram_threads(mock_post): """NotifyTelegram() Threads/Topics.""" # 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/?thread=1234") assert len(aobj) == 1 assert isinstance(aobj[0], NotifyTelegram) body = "my threaded message" assert aobj.notify(body=body) # 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"]) assert "message_thread_id" in payload assert payload["message_thread_id"] == 1234 mock_post.reset_mock() aobj = Apprise() aobj.add("tgram://123456789:abcdefg_hijklmnop/?topic=1234") assert len(aobj) == 1 assert isinstance(aobj[0], NotifyTelegram) body = "my message" assert aobj.notify(body=body) # 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"]) assert "message_thread_id" in payload assert payload["message_thread_id"] == 1234 mock_post.reset_mock() aobj = Apprise() aobj.add("tgram://123456789:abcdefg_hijklmnop/9876:1234/9876:1111") assert len(aobj) == 1 assert isinstance(aobj[0], NotifyTelegram) body = "my message" assert aobj.notify(body=body) # 1 call to look up bot owner, and second for notification assert mock_post.call_count == 2 payload = loads(mock_post.call_args_list[0][1]["data"]) assert "message_thread_id" in payload assert payload["message_thread_id"] == 1111 payload = loads(mock_post.call_args_list[1][1]["data"]) assert "message_thread_id" in payload assert payload["message_thread_id"] == 1234 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()