diff --git a/apprise/plugins/NotifyTelegram.py b/apprise/plugins/NotifyTelegram.py index d04bbfd0..62ab7675 100644 --- a/apprise/plugins/NotifyTelegram.py +++ b/apprise/plugins/NotifyTelegram.py @@ -51,6 +51,7 @@ # - https://core.telegram.org/bots/api import requests import re +import os from json import loads from json import dumps @@ -63,6 +64,7 @@ from ..utils import parse_bool from ..utils import parse_list from ..utils import validate_regex from ..AppriseLocale import gettext_lazy as _ +from ..attachment.AttachBase import AttachBase TELEGRAM_IMAGE_XY = NotifyImageSize.XY_256 @@ -100,12 +102,71 @@ class NotifyTelegram(NotifyBase): # The maximum allowable characters allowed in the body per message body_maxlen = 4096 + # Telegram is limited to sending a maximum of 100 requests per second. + request_rate_per_sec = 0.001 + # Define object templates templates = ( '{schema}://{bot_token}', '{schema}://{bot_token}/{targets}', ) + # Telegram Attachment Support + mime_lookup = ( + # This list is intentionally ordered so that it can be scanned + # from top to bottom. The last entry is a catch-all + + # Animations are documented to only support gif or H.264/MPEG-4 + # Source: https://core.telegram.org/bots/api#sendanimation + { + 'regex': re.compile(r'^(image/gif|video/H264)', re.I), + 'function_name': 'sendAnimation', + 'key': 'animation', + }, + + # This entry is intentially placed below the sendAnimiation allowing + # it to catch gif files. This then becomes a catch all to remaining + # image types. + # Source: https://core.telegram.org/bots/api#sendphoto + { + 'regex': re.compile(r'^image/.*', re.I), + 'function_name': 'sendPhoto', + 'key': 'photo', + }, + + # Video is documented to only support .mp4 + # Source: https://core.telegram.org/bots/api#sendvideo + { + 'regex': re.compile(r'^video/mp4', re.I), + 'function_name': 'sendVideo', + 'key': 'video', + }, + + # Voice supports ogg + # Source: https://core.telegram.org/bots/api#sendvoice + { + 'regex': re.compile(r'^(application|audio)/ogg', re.I), + 'function_name': 'sendVoice', + 'key': 'voice', + }, + + # Audio supports mp3 and m4a only + # Source: https://core.telegram.org/bots/api#sendaudio + { + 'regex': re.compile(r'^audio/(mpeg|mp4a-latm)', re.I), + 'function_name': 'sendAudio', + 'key': 'audio', + }, + + # Catch All (all other types) + # Source: https://core.telegram.org/bots/api#senddocument + { + 'regex': re.compile(r'.*', re.I), + 'function_name': 'sendDocument', + 'key': 'document', + }, + ) + # Define our template tokens template_tokens = dict(NotifyBase.template_tokens, **{ 'bot_token': { @@ -189,82 +250,101 @@ class NotifyTelegram(NotifyBase): # or not. self.include_image = include_image - def send_image(self, chat_id, notify_type): + def send_media(self, chat_id, notify_type, attach=None): """ Sends a sticker based on the specified notify type """ - # The URL; we do not set headers because the api doesn't seem to like - # when we set one. + # Prepare our Headers + headers = { + 'User-Agent': self.app_id, + } + + # Our function name and payload are determined on the path + function_name = 'SendPhoto' + key = 'photo' + path = None + + if isinstance(attach, AttachBase): + # Store our path to our file + path = attach.path + file_name = attach.name + mimetype = attach.mimetype + + if not path: + # Could not load attachment + return False + + # Process our attachment + function_name, key = \ + next(((x['function_name'], x['key']) for x in self.mime_lookup + if x['regex'].match(mimetype))) # pragma: no cover + + else: + attach = self.image_path(notify_type) if attach is None else attach + if attach is None: + # Nothing specified to send + return True + + # Take on specified attachent as path + path = attach + file_name = os.path.basename(path) + url = '%s%s/%s' % ( self.notify_url, self.bot_token, - 'sendPhoto' + function_name, ) - # Acquire our image path if configured to do so; we don't bother - # checking to see if selfinclude_image is set here because the - # send_image() function itself (this function) checks this flag - # already - path = self.image_path(notify_type) - - if not path: - # No image to send - self.logger.debug( - 'Telegram image does not exist for %s' % (notify_type)) - - # No need to fail; we may have been configured this way through - # the apprise.AssetObject() - return True + # Always call throttle before any remote server i/o is made; + # Telegram throttles to occur before sending the image so that + # content can arrive together. + self.throttle() try: with open(path, 'rb') as f: # Configure file payload (for upload) - files = { - 'photo': f, - } - - payload = { - 'chat_id': chat_id, - } + files = {key: (file_name, f)} + payload = {'chat_id': chat_id} self.logger.debug( - 'Telegram image POST URL: %s (cert_verify=%r)' % ( + 'Telegram attachment POST URL: %s (cert_verify=%r)' % ( url, self.verify_certificate)) - try: - r = requests.post( - url, - files=files, - data=payload, - verify=self.verify_certificate, - ) + r = requests.post( + url, + headers=headers, + files=files, + data=payload, + verify=self.verify_certificate, + ) - if r.status_code != requests.codes.ok: - # We had a problem - status_str = NotifyTelegram\ - .http_response_code_lookup(r.status_code) + if r.status_code != requests.codes.ok: + # We had a problem + status_str = NotifyTelegram\ + .http_response_code_lookup(r.status_code) - self.logger.warning( - 'Failed to send Telegram image: ' - '{}{}error={}.'.format( - status_str, - ', ' if status_str else '', - r.status_code)) - - self.logger.debug( - 'Response Details:\r\n{}'.format(r.content)) - - return False - - except requests.RequestException as e: self.logger.warning( - 'A connection error occured posting Telegram image.') - self.logger.debug('Socket Exception: %s' % str(e)) + 'Failed to send Telegram attachment: ' + '{}{}error={}.'.format( + status_str, + ', ' if status_str else '', + r.status_code)) + + self.logger.debug( + 'Response Details:\r\n{}'.format(r.content)) + return False - return True + # Content was sent successfully if we got here + return True + + except requests.RequestException as e: + self.logger.warning( + 'A connection error occured posting Telegram ' + 'attachment.') + self.logger.debug('Socket Exception: %s' % str(e)) except (IOError, OSError): # IOError is present for backwards compatibility with Python @@ -364,26 +444,20 @@ class NotifyTelegram(NotifyBase): # Load our response and attempt to fetch our userid response = loads(r.content) - if 'ok' in response and response['ok'] is True: - start = re.compile(r'^\s*\/start', re.I) - for _msg in iter(response['result']): - # Find /start - if not start.search(_msg['message']['text']): - continue - - _id = _msg['message']['from'].get('id', 0) - _user = _msg['message']['from'].get('first_name') - self.logger.info('Detected telegram user %s (userid=%d)' % ( - _user, _id)) - # Return our detected userid - return _id - - self.logger.warning( - 'Could not detect bot owner. Is it running (/start)?') + if 'ok' in response and response['ok'] is True \ + and 'result' in response and len(response['result']): + entry = response['result'][0] + _id = entry['message']['from'].get('id', 0) + _user = entry['message']['from'].get('first_name') + self.logger.info('Detected telegram user %s (userid=%d)' % ( + _user, _id)) + # Return our detected userid + return _id return 0 - def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): + def send(self, body, title='', notify_type=NotifyType.INFO, attach=None, + **kwargs): """ Perform Telegram Notification """ @@ -476,15 +550,20 @@ class NotifyTelegram(NotifyBase): # ID payload['chat_id'] = int(chat_id.group('idno')) + if self.include_image is True: + # Define our path + if not self.send_media(payload['chat_id'], notify_type): + # We failed to send the image associated with our + notify_type + self.logger.warning( + 'Failed to send Telegram type image to {}.', + payload['chat_id']) + # Always call throttle before any remote server i/o is made; # Telegram throttles to occur before sending the image so that # content can arrive together. self.throttle() - if self.include_image is True: - # Send an image - self.send_image(payload['chat_id'], notify_type) - self.logger.debug('Telegram POST URL: %s (cert_verify=%r)' % ( url, self.verify_certificate, )) @@ -524,9 +603,6 @@ class NotifyTelegram(NotifyBase): has_error = True continue - else: - self.logger.info('Sent Telegram notification.') - except requests.RequestException as e: self.logger.warning( 'A connection error occured sending Telegram:%s ' % ( @@ -538,6 +614,22 @@ class NotifyTelegram(NotifyBase): has_error = True continue + self.logger.info('Sent Telegram notification.') + + if attach: + # Send our attachments now (if specified and if it exists) + for attachment in attach: + sent_attachment = self.send_media( + payload['chat_id'], notify_type, attach=attachment) + + if not sent_attachment: + # We failed; don't continue + has_error = True + break + + self.logger.info( + 'Sent Telegram attachment: {}.'.format(attachment)) + return not has_error def url(self, privacy=False, *args, **kwargs): diff --git a/test/test_rest_plugins.py b/test/test_rest_plugins.py index a5a45fdd..65971678 100644 --- a/test/test_rest_plugins.py +++ b/test/test_rest_plugins.py @@ -5114,203 +5114,6 @@ def test_notify_rocketchat_plugin(mock_post, mock_get): assert obj.logout() is False -@mock.patch('requests.get') -@mock.patch('requests.post') -def test_notify_telegram_plugin(mock_post, mock_get): - """ - API: NotifyTelegram() Extra Checks - - """ - # Disable Throttling to speed testing - plugins.NotifyBase.request_rate_per_sec = 0 - - # Bot Token - bot_token = '123456789:abcdefg_hijklmnop' - invalid_bot_token = 'abcd:123' - - # Chat ID - chat_ids = 'l2g, lead2gold' - - # Prepare Mock - mock_get.return_value = requests.Request() - mock_post.return_value = requests.Request() - mock_post.return_value.status_code = requests.codes.ok - mock_get.return_value.status_code = requests.codes.ok - mock_get.return_value.content = '{}' - mock_post.return_value.content = '{}' - - # Exception should be thrown about the fact no bot token was specified - with pytest.raises(TypeError): - plugins.NotifyTelegram(bot_token=None, targets=chat_ids) - - # Exception should be thrown about the fact an invalid bot token was - # specifed - with pytest.raises(TypeError): - plugins.NotifyTelegram(bot_token=invalid_bot_token, targets=chat_ids) - - obj = plugins.NotifyTelegram( - bot_token=bot_token, targets=chat_ids, include_image=True) - assert isinstance(obj, plugins.NotifyTelegram) is True - assert len(obj.targets) == 2 - - # Test Image Sending Exceptions - mock_get.side_effect = IOError() - mock_post.side_effect = IOError() - obj.send_image(obj.targets[0], NotifyType.INFO) - - # Restore their entries - mock_get.side_effect = None - mock_post.side_effect = None - mock_get.return_value.content = '{}' - mock_post.return_value.content = '{}' - - # test url call - assert isinstance(obj.url(), six.string_types) is True - - # test privacy version of url - assert isinstance(obj.url(privacy=True), six.string_types) is True - assert obj.url(privacy=True).startswith('tgram://1...p/') is True - - # Test that we can load the string we generate back: - obj = plugins.NotifyTelegram(**plugins.NotifyTelegram.parse_url(obj.url())) - assert isinstance(obj, plugins.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_get.return_value = response - mock_post.return_value = response - - # No image asset - nimg_obj = plugins.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 == 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 - 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 = plugins.NotifyTelegram(bot_token=bot_token, targets='l2g') - nimg_obj = plugins.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": 645421321, - "message": { - "message_id": 1, - "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 = plugins.NotifyTelegram(bot_token=bot_token, targets=None) - assert len(obj.targets) == 1 - assert obj.targets[0] == '532389719' - - # Do the test again, but without the expected (parsed response) - mock_post.return_value.content = dumps({ - "ok": True, - "result": [{ - "message": { - "text": "/ignored.entry", - }}, - ], - }) - - # Exception should be thrown about the fact no bot token was specified - with pytest.raises(TypeError): - plugins.NotifyTelegram(bot_token=bot_token, targets=None) - - # 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 - - # Exception should be thrown over internal server error caused - with pytest.raises(TypeError): - plugins.NotifyTelegram(bot_token=bot_token, targets=None) - - # Test our bot detection with an unmappable html error - mock_post.return_value.status_code = 999 - # Exception should be thrown over invali internal error no - with pytest.raises(TypeError): - plugins.NotifyTelegram(bot_token=bot_token, targets=None) - - # Do it again but this time provide a failure message - mock_post.return_value.content = dumps({'description': 'Failure Message'}) - - # Exception should be thrown about the fact no bot token was specified - with pytest.raises(TypeError): - plugins.NotifyTelegram(bot_token=bot_token, targets=None) - - # 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 = plugins.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 - for _exception in REQUEST_EXCEPTIONS: - mock_post.side_effect = _exception - - # No chat_ids specified - with pytest.raises(TypeError): - obj = plugins.NotifyTelegram(bot_token=bot_token, targets=None) - - def test_notify_overflow_truncate(): """ API: Overflow Truncate Functionality Testing diff --git a/test/test_telegram.py b/test/test_telegram.py new file mode 100644 index 00000000..d5a25783 --- /dev/null +++ b/test/test_telegram.py @@ -0,0 +1,252 @@ +# -*- coding: utf-8 -*- +# +# All rights reserved. +# +# This code is licensed under the MIT License. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files(the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions : +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import os +import six +import pytest +import mock +import requests +from json import dumps +from apprise import AppriseAttachment +from apprise import AppriseAsset +from apprise import NotifyType +from apprise import plugins + +# Disable logging for a cleaner testing output +import logging +logging.disable(logging.CRITICAL) + +# Attachment Directory +TEST_VAR_DIR = os.path.join(os.path.dirname(__file__), 'var') + + +@mock.patch('requests.get') +@mock.patch('requests.post') +def test_notify_telegram_plugin(mock_post, mock_get, tmpdir): + """ + API: NotifyTelegram() Tests + + """ + # Disable Throttling to speed testing + plugins.NotifyBase.request_rate_per_sec = 0 + + # Bot Token + bot_token = '123456789:abcdefg_hijklmnop' + invalid_bot_token = 'abcd:123' + + # Chat ID + chat_ids = 'l2g, lead2gold' + + # Prepare Mock + mock_get.return_value = requests.Request() + mock_post.return_value = requests.Request() + mock_post.return_value.status_code = requests.codes.ok + mock_get.return_value.status_code = requests.codes.ok + mock_get.return_value.content = '{}' + mock_post.return_value.content = '{}' + + # Exception should be thrown about the fact no bot token was specified + with pytest.raises(TypeError): + plugins.NotifyTelegram(bot_token=None, targets=chat_ids) + + # Exception should be thrown about the fact an invalid bot token was + # specifed + with pytest.raises(TypeError): + plugins.NotifyTelegram(bot_token=invalid_bot_token, targets=chat_ids) + + obj = plugins.NotifyTelegram( + bot_token=bot_token, targets=chat_ids, include_image=True) + assert isinstance(obj, plugins.NotifyTelegram) is True + assert len(obj.targets) == 2 + + # Test Image Sending Exceptions + mock_post.side_effect = IOError() + 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_get.side_effect = None + mock_post.side_effect = None + mock_get.return_value.content = '{}' + mock_post.return_value.content = '{}' + + # test url call + assert isinstance(obj.url(), six.string_types) is True + + # test privacy version of url + assert isinstance(obj.url(privacy=True), six.string_types) is True + assert obj.url(privacy=True).startswith('tgram://1...p/') is True + + # Test that we can load the string we generate back: + obj = plugins.NotifyTelegram(**plugins.NotifyTelegram.parse_url(obj.url())) + assert isinstance(obj, plugins.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_get.return_value = response + mock_post.return_value = response + + # No image asset + nimg_obj = plugins.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 == 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 + 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 = plugins.NotifyTelegram(bot_token=bot_token, targets='l2g') + nimg_obj = plugins.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": 645421321, + "message": { + "message_id": 1, + "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 + + # Test sending attachments + obj = plugins.NotifyTelegram(bot_token=bot_token, targets='12345') + assert len(obj.targets) == 1 + assert obj.targets[0] == '12345' + + path = os.path.join(TEST_VAR_DIR, 'apprise-test.gif') + attach = AppriseAttachment(path) + assert obj.notify( + body='body', title='title', notify_type=NotifyType.INFO, + attach=attach) is True + + path = os.path.join(TEST_VAR_DIR, '/invalid/path/to/an/invalid/file.jpg') + assert obj.notify( + body='body', title='title', notify_type=NotifyType.INFO, + attach=path) is False + + obj = plugins.NotifyTelegram(bot_token=bot_token, targets=None) + assert len(obj.targets) == 1 + assert obj.targets[0] == '532389719' + + # Do the test again, but without the expected (parsed response) + mock_post.return_value.content = dumps({ + "ok": True, + "result": [], + }) + + # Exception should be thrown about the fact no bot token was specified + with pytest.raises(TypeError): + plugins.NotifyTelegram(bot_token=bot_token, targets=None) + + # 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 + + # Exception should be thrown over internal server error caused + with pytest.raises(TypeError): + plugins.NotifyTelegram(bot_token=bot_token, targets=None) + + # Test our bot detection with an unmappable html error + mock_post.return_value.status_code = 999 + # Exception should be thrown over invali internal error no + with pytest.raises(TypeError): + plugins.NotifyTelegram(bot_token=bot_token, targets=None) + + # Do it again but this time provide a failure message + mock_post.return_value.content = dumps({'description': 'Failure Message'}) + + # Exception should be thrown about the fact no bot token was specified + with pytest.raises(TypeError): + plugins.NotifyTelegram(bot_token=bot_token, targets=None) + + # 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 = plugins.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 + with pytest.raises(TypeError): + obj = plugins.NotifyTelegram(bot_token=bot_token, targets=None) diff --git a/test/var/apprise-test.png b/test/var/apprise-test.png index 6703efe8..9a1ffc10 100644 Binary files a/test/var/apprise-test.png and b/test/var/apprise-test.png differ