From 3d16cbf3d3f5c7257569f2036c57970016563cfd Mon Sep 17 00:00:00 2001 From: Chris Caron Date: Sat, 12 Aug 2023 16:32:07 -0400 Subject: [PATCH] Title & body not required if attachment specified (#916) --- apprise/Apprise.py | 7 +- apprise/plugins/NotifyAppriseAPI.py | 10 +- apprise/plugins/NotifyBase.py | 36 ++++++- apprise/plugins/NotifyDiscord.py | 145 +++++++++++++++------------- apprise/plugins/NotifyEmail.py | 5 +- apprise/plugins/NotifyForm.py | 5 +- apprise/plugins/NotifyJSON.py | 5 +- apprise/plugins/NotifyMailgun.py | 5 +- apprise/plugins/NotifyMastodon.py | 9 +- apprise/plugins/NotifyNtfy.py | 18 ++-- apprise/plugins/NotifyPushBullet.py | 20 ++-- apprise/plugins/NotifyPushSafer.py | 5 +- apprise/plugins/NotifyPushover.py | 18 ++-- apprise/plugins/NotifySES.py | 8 +- apprise/plugins/NotifySMSEagle.py | 5 +- apprise/plugins/NotifySMTP2Go.py | 7 +- apprise/plugins/NotifySignalAPI.py | 7 +- apprise/plugins/NotifySlack.py | 10 +- apprise/plugins/NotifySparkPost.py | 5 +- apprise/plugins/NotifyStreamlabs.py | 3 +- apprise/plugins/NotifyTelegram.py | 17 +++- apprise/plugins/NotifyTwitter.py | 9 +- apprise/plugins/NotifyXML.py | 5 +- docker-compose.yml | 18 ++++ test/helpers/rest.py | 46 +++++++++ test/test_api.py | 6 +- test/test_plugin_boxcar.py | 8 +- 27 files changed, 317 insertions(+), 125 deletions(-) diff --git a/apprise/Apprise.py b/apprise/Apprise.py index 86fabb7115..67f44b44ca 100644 --- a/apprise/Apprise.py +++ b/apprise/Apprise.py @@ -458,7 +458,7 @@ def _create_notify_gen(self, body, title='', logger.error(msg) raise TypeError(msg) - if not (title or body): + if not (title or body or attach): msg = "No message content specified to deliver" logger.error(msg) raise TypeError(msg) @@ -689,6 +689,11 @@ def details(self, lang=None, show_requirements=False, show_disabled=False): # Placeholder - populated below 'details': None, + # Let upstream service know of the plugins that support + # attachments + 'attachment_support': getattr( + plugin, 'attachment_support', False), + # Differentiat between what is a custom loaded plugin and # which is native. 'category': getattr(plugin, 'category', None) diff --git a/apprise/plugins/NotifyAppriseAPI.py b/apprise/plugins/NotifyAppriseAPI.py index b8765496f9..a1247c249b 100644 --- a/apprise/plugins/NotifyAppriseAPI.py +++ b/apprise/plugins/NotifyAppriseAPI.py @@ -77,6 +77,9 @@ class NotifyAppriseAPI(NotifyBase): # A URL that takes you to the setup/help of the specific protocol setup_url = 'https://github.com/caronc/apprise/wiki/Notify_apprise_api' + # Support attachments + attachment_support = True + # Depending on the number of transactions/notifications taking place, this # could take a while. 30 seconds should be enough to perform the task socket_read_timeout = 30.0 @@ -260,7 +263,7 @@ def send(self, body, title='', notify_type=NotifyType.INFO, attach=None, attachments = [] files = [] - if attach: + if attach and self.attachment_support: for no, attachment in enumerate(attach, start=1): # Perform some simple error checking if not attachment: @@ -310,7 +313,10 @@ def send(self, body, title='', notify_type=NotifyType.INFO, attach=None, if self.method == AppriseAPIMethod.JSON: headers['Content-Type'] = 'application/json' - payload['attachments'] = attachments + + if attachments: + payload['attachments'] = attachments + payload = dumps(payload) if self.__tags: diff --git a/apprise/plugins/NotifyBase.py b/apprise/plugins/NotifyBase.py index 1b07baa715..5297b9331e 100644 --- a/apprise/plugins/NotifyBase.py +++ b/apprise/plugins/NotifyBase.py @@ -139,6 +139,18 @@ class NotifyBase(URLBase): # Default Overflow Mode overflow_mode = OverflowMode.UPSTREAM + # Support Attachments; this defaults to being disabled. + # Since apprise allows you to send attachments without a body or title + # defined, by letting Apprise know the plugin won't support attachments + # up front, it can quickly pass over and ignore calls to these end points. + + # You must set this to true if your application can handle attachments. + # You must also consider a flow change to your notification if this is set + # to True as well as now there will be cases where both the body and title + # may not be set. There will never be a case where a body, or attachment + # isn't set in the same call to your notify() function. + attachment_support = False + # Default Title HTML Tagging # When a title is specified for a notification service that doesn't accept # titles, by default apprise tries to give a plesant view and convert the @@ -316,7 +328,7 @@ async def do_send(**kwargs2): the_cors = (do_send(**kwargs2) for kwargs2 in send_calls) return all(await asyncio.gather(*the_cors)) - def _build_send_calls(self, body, title=None, + def _build_send_calls(self, body=None, title=None, notify_type=NotifyType.INFO, overflow=None, attach=None, body_format=None, **kwargs): """ @@ -339,6 +351,28 @@ def _build_send_calls(self, body, title=None, # bad attachments raise + # Handle situations where the body is None + body = '' if not body else body + + elif not (body or attach): + # If there is not an attachment at the very least, a body must be + # present + msg = "No message body or attachment was specified." + self.logger.warning(msg) + raise TypeError(msg) + + if not body and not self.attachment_support: + # If no body was specified, then we know that an attachment + # was. This is logic checked earlier in the code. + # + # Knowing this, if the plugin itself doesn't support sending + # attachments, there is nothing further to do here, just move + # along. + msg = f"{self.service_name} does not support attachments; " \ + " service skipped" + self.logger.warning(msg) + raise TypeError(msg) + # Handle situations where the title is None title = '' if not title else title diff --git a/apprise/plugins/NotifyDiscord.py b/apprise/plugins/NotifyDiscord.py index 408c86f540..af0344a06d 100644 --- a/apprise/plugins/NotifyDiscord.py +++ b/apprise/plugins/NotifyDiscord.py @@ -84,6 +84,9 @@ class NotifyDiscord(NotifyBase): # Discord Webhook notify_url = 'https://discord.com/api/webhooks' + # Support attachments + attachment_support = True + # Allows the user to specify the NotifyImageSize object image_size = NotifyImageSize.XY_256 @@ -255,61 +258,6 @@ def send(self, body, title='', notify_type=NotifyType.INFO, attach=None, # Acquire image_url image_url = self.image_url(notify_type) - # our fields variable - fields = [] - - if self.notify_format == NotifyFormat.MARKDOWN: - # Use embeds for payload - payload['embeds'] = [{ - 'author': { - 'name': self.app_id, - 'url': self.app_url, - }, - 'title': title, - 'description': body, - - # Our color associated with our notification - 'color': self.color(notify_type, int), - }] - - if self.footer: - # Acquire logo URL - logo_url = self.image_url(notify_type, logo=True) - - # Set Footer text to our app description - payload['embeds'][0]['footer'] = { - 'text': self.app_desc, - } - - if self.footer_logo and logo_url: - payload['embeds'][0]['footer']['icon_url'] = logo_url - - if self.include_image and image_url: - payload['embeds'][0]['thumbnail'] = { - 'url': image_url, - 'height': 256, - 'width': 256, - } - - if self.fields: - # Break titles out so that we can sort them in embeds - description, fields = self.extract_markdown_sections(body) - - # Swap first entry for description - payload['embeds'][0]['description'] = description - if fields: - # Apply our additional parsing for a better presentation - payload['embeds'][0]['fields'] = \ - fields[:self.discord_max_fields] - - # Remove entry from head of fields - fields = fields[self.discord_max_fields:] - - else: - # not markdown - payload['content'] = \ - body if not title else "{}\r\n{}".format(title, body) - if self.avatar and (image_url or self.avatar_url): payload['avatar_url'] = \ self.avatar_url if self.avatar_url else image_url @@ -318,22 +266,81 @@ def send(self, body, title='', notify_type=NotifyType.INFO, attach=None, # Optionally override the default username of the webhook payload['username'] = self.user + # Associate our thread_id with our message params = {'thread_id': self.thread_id} if self.thread_id else None - if not self._send(payload, params=params): - # We failed to post our message - return False - # Process any remaining fields IF set - if fields: - payload['embeds'][0]['description'] = '' - for i in range(0, len(fields), self.discord_max_fields): - payload['embeds'][0]['fields'] = \ - fields[i:i + self.discord_max_fields] - if not self._send(payload): - # We failed to post our message - return False + if body: + # our fields variable + fields = [] + + if self.notify_format == NotifyFormat.MARKDOWN: + # Use embeds for payload + payload['embeds'] = [{ + 'author': { + 'name': self.app_id, + 'url': self.app_url, + }, + 'title': title, + 'description': body, + + # Our color associated with our notification + 'color': self.color(notify_type, int), + }] + + if self.footer: + # Acquire logo URL + logo_url = self.image_url(notify_type, logo=True) + + # Set Footer text to our app description + payload['embeds'][0]['footer'] = { + 'text': self.app_desc, + } + + if self.footer_logo and logo_url: + payload['embeds'][0]['footer']['icon_url'] = logo_url + + if self.include_image and image_url: + payload['embeds'][0]['thumbnail'] = { + 'url': image_url, + 'height': 256, + 'width': 256, + } + + if self.fields: + # Break titles out so that we can sort them in embeds + description, fields = self.extract_markdown_sections(body) + + # Swap first entry for description + payload['embeds'][0]['description'] = description + if fields: + # Apply our additional parsing for a better + # presentation + payload['embeds'][0]['fields'] = \ + fields[:self.discord_max_fields] + + # Remove entry from head of fields + fields = fields[self.discord_max_fields:] + + else: + # not markdown + payload['content'] = \ + body if not title else "{}\r\n{}".format(title, body) + + if not self._send(payload, params=params): + # We failed to post our message + return False + + # Process any remaining fields IF set + if fields: + payload['embeds'][0]['description'] = '' + for i in range(0, len(fields), self.discord_max_fields): + payload['embeds'][0]['fields'] = \ + fields[i:i + self.discord_max_fields] + if not self._send(payload): + # We failed to post our message + return False - if attach: + if attach and self.attachment_support: # Update our payload; the idea is to preserve it's other detected # and assigned values for re-use here too payload.update({ @@ -356,7 +363,7 @@ def send(self, body, title='', notify_type=NotifyType.INFO, attach=None, for attachment in attach: self.logger.info( 'Posting Discord Attachment {}'.format(attachment.name)) - if not self._send(payload, attach=attachment): + if not self._send(payload, params=params, attach=attachment): # We failed to post our message return False diff --git a/apprise/plugins/NotifyEmail.py b/apprise/plugins/NotifyEmail.py index aadf027cb5..cc69489bda 100644 --- a/apprise/plugins/NotifyEmail.py +++ b/apprise/plugins/NotifyEmail.py @@ -341,6 +341,9 @@ class NotifyEmail(NotifyBase): # A URL that takes you to the setup/help of the specific protocol setup_url = 'https://github.com/caronc/apprise/wiki/Notify_email' + # Support attachments + attachment_support = True + # Default Notify Format notify_format = NotifyFormat.HTML @@ -770,7 +773,7 @@ def send(self, body, title='', notify_type=NotifyType.INFO, attach=None, else: base = MIMEText(body, 'plain', 'utf-8') - if attach: + if attach and self.attachment_support: mixed = MIMEMultipart("mixed") mixed.attach(base) # Now store our attachments diff --git a/apprise/plugins/NotifyForm.py b/apprise/plugins/NotifyForm.py index 3ef8d21b41..505319ea31 100644 --- a/apprise/plugins/NotifyForm.py +++ b/apprise/plugins/NotifyForm.py @@ -99,6 +99,9 @@ class NotifyForm(NotifyBase): # A URL that takes you to the setup/help of the specific protocol setup_url = 'https://github.com/caronc/apprise/wiki/Notify_Custom_Form' + # Support attachments + attachment_support = True + # Allows the user to specify the NotifyImageSize object image_size = NotifyImageSize.XY_128 @@ -345,7 +348,7 @@ def send(self, body, title='', notify_type=NotifyType.INFO, attach=None, # Track our potential attachments files = [] - if attach: + if attach and self.attachment_support: for no, attachment in enumerate(attach, start=1): # Perform some simple error checking if not attachment: diff --git a/apprise/plugins/NotifyJSON.py b/apprise/plugins/NotifyJSON.py index f1a9cc04e4..0a36f4dcd5 100644 --- a/apprise/plugins/NotifyJSON.py +++ b/apprise/plugins/NotifyJSON.py @@ -80,6 +80,9 @@ class NotifyJSON(NotifyBase): # A URL that takes you to the setup/help of the specific protocol setup_url = 'https://github.com/caronc/apprise/wiki/Notify_Custom_JSON' + # Support attachments + attachment_support = True + # Allows the user to specify the NotifyImageSize object image_size = NotifyImageSize.XY_128 @@ -289,7 +292,7 @@ def send(self, body, title='', notify_type=NotifyType.INFO, attach=None, # Track our potential attachments attachments = [] - if attach: + if attach and self.attachment_support: for attachment in attach: # Perform some simple error checking if not attachment: diff --git a/apprise/plugins/NotifyMailgun.py b/apprise/plugins/NotifyMailgun.py index dd3171855b..218a3a73fa 100644 --- a/apprise/plugins/NotifyMailgun.py +++ b/apprise/plugins/NotifyMailgun.py @@ -121,6 +121,9 @@ class NotifyMailgun(NotifyBase): # A URL that takes you to the setup/help of the specific protocol setup_url = 'https://github.com/caronc/apprise/wiki/Notify_mailgun' + # Support attachments + attachment_support = True + # Default Notify Format notify_format = NotifyFormat.HTML @@ -371,7 +374,7 @@ def send(self, body, title='', notify_type=NotifyType.INFO, attach=None, # Track our potential files files = {} - if attach: + if attach and self.attachment_support: for idx, attachment in enumerate(attach): # Perform some simple error checking if not attachment: diff --git a/apprise/plugins/NotifyMastodon.py b/apprise/plugins/NotifyMastodon.py index 18170e8fb1..2fb5c41a42 100644 --- a/apprise/plugins/NotifyMastodon.py +++ b/apprise/plugins/NotifyMastodon.py @@ -111,6 +111,10 @@ class NotifyMastodon(NotifyBase): # A URL that takes you to the setup/help of the specific protocol setup_url = 'https://github.com/caronc/apprise/wiki/Notify_mastodon' + # Support attachments + attachment_support = True + + # Allows the user to specify the NotifyImageSize object # Allows the user to specify the NotifyImageSize object; this is supported # through the webhook image_size = NotifyImageSize.XY_128 @@ -414,11 +418,10 @@ def send(self, body, title='', notify_type=NotifyType.INFO, attach=None, else: targets.add(myself) - if attach: + if attach and self.attachment_support: # We need to upload our payload first so that we can source it # in remaining messages for attachment in attach: - # Perform some simple error checking if not attachment: # We could not access the attachment @@ -578,7 +581,7 @@ def send(self, body, title='', notify_type=NotifyType.INFO, attach=None, _payload = deepcopy(payload) _payload['media_ids'] = media_ids - if no: + if no or not body: # strip text and replace it with the image representation _payload['status'] = \ '{:02d}/{:02d}'.format(no + 1, len(batches)) diff --git a/apprise/plugins/NotifyNtfy.py b/apprise/plugins/NotifyNtfy.py index 2aaae20133..e686a311c4 100644 --- a/apprise/plugins/NotifyNtfy.py +++ b/apprise/plugins/NotifyNtfy.py @@ -172,6 +172,9 @@ class NotifyNtfy(NotifyBase): # Default upstream/cloud host if none is defined cloud_notify_url = 'https://ntfy.sh' + # Support attachments + attachment_support = True + # Allows the user to specify the NotifyImageSize object image_size = NotifyImageSize.XY_256 @@ -405,14 +408,14 @@ def send(self, body, title='', notify_type=NotifyType.INFO, attach=None, # Retrieve our topic topic = topics.pop() - if attach: + if attach and self.attachment_support: # We need to upload our payload first so that we can source it # in remaining messages for no, attachment in enumerate(attach): - # First message only includes the text - _body = body if not no else None - _title = title if not no else None + # First message only includes the text (if defined) + _body = body if not no and body else None + _title = title if not no and title else None # Perform some simple error checking if not attachment: @@ -543,11 +546,8 @@ def _send(self, topic, body=None, title=None, attach=None, image_url=None, # Default response type response = None - if data: - data = data if attach else dumps(data) - - else: # not data: - data = None + if not attach: + data = dumps(data) try: r = requests.post( diff --git a/apprise/plugins/NotifyPushBullet.py b/apprise/plugins/NotifyPushBullet.py index 07b2a43a05..6502b6c747 100644 --- a/apprise/plugins/NotifyPushBullet.py +++ b/apprise/plugins/NotifyPushBullet.py @@ -75,6 +75,9 @@ class NotifyPushBullet(NotifyBase): # PushBullet uses the http protocol with JSON requests notify_url = 'https://api.pushbullet.com/v2/{}' + # Support attachments + attachment_support = True + # Define object templates templates = ( '{schema}://{accesstoken}', @@ -150,7 +153,7 @@ def send(self, body, title='', notify_type=NotifyType.INFO, attach=None, # Build a list of our attachments attachments = [] - if attach: + if attach and self.attachment_support: # We need to upload our payload first so that we can source it # in remaining messages for attachment in attach: @@ -261,14 +264,15 @@ def send(self, body, title='', notify_type=NotifyType.INFO, attach=None, "PushBullet recipient {} parsed as a device" .format(recipient)) - okay, response = self._send( - self.notify_url.format('pushes'), payload) - if not okay: - has_error = True - continue + if body: + okay, response = self._send( + self.notify_url.format('pushes'), payload) + if not okay: + has_error = True + continue - self.logger.info( - 'Sent PushBullet notification to "%s".' % (recipient)) + self.logger.info( + 'Sent PushBullet notification to "%s".' % (recipient)) for attach_payload in attachments: # Send our attachments to our same user (already prepared as diff --git a/apprise/plugins/NotifyPushSafer.py b/apprise/plugins/NotifyPushSafer.py index 19bff2bd0b..f6415c88ec 100644 --- a/apprise/plugins/NotifyPushSafer.py +++ b/apprise/plugins/NotifyPushSafer.py @@ -336,6 +336,9 @@ class NotifyPushSafer(NotifyBase): # The default secure protocol secure_protocol = 'psafers' + # Support attachments + attachment_support = True + # Number of requests to a allow per second request_rate_per_sec = 1.2 @@ -546,7 +549,7 @@ def send(self, body, title='', notify_type=NotifyType.INFO, attach=None, # Initialize our list of attachments attachments = [] - if attach: + if attach and self.attachment_support: # We need to upload our payload first so that we can source it # in remaining messages for attachment in attach: diff --git a/apprise/plugins/NotifyPushover.py b/apprise/plugins/NotifyPushover.py index 01214808cd..922ead25f4 100644 --- a/apprise/plugins/NotifyPushover.py +++ b/apprise/plugins/NotifyPushover.py @@ -164,6 +164,9 @@ class NotifyPushover(NotifyBase): # Pushover uses the http protocol with JSON requests notify_url = 'https://api.pushover.net/1/messages.json' + # Support attachments + attachment_support = True + # The maximum allowable characters allowed in the body per message body_maxlen = 1024 @@ -381,22 +384,25 @@ def send(self, body, title='', notify_type=NotifyType.INFO, attach=None, if self.priority == PushoverPriority.EMERGENCY: payload.update({'retry': self.retry, 'expire': self.expire}) - if attach: + if attach and self.attachment_support: # Create a copy of our payload _payload = payload.copy() # Send with attachments - for attachment in attach: - # Simple send + for no, attachment in enumerate(attach): + if no or not body: + # To handle multiple attachments, clean up our message + _payload['message'] = attachment.name + if not self._send(_payload, attachment): # Mark our failure has_error = True # clean exit from our attachment loop break - # To handle multiple attachments, clean up our message - _payload['title'] = '...' - _payload['message'] = attachment.name + # Clear our title if previously set + _payload['title'] = '' + # No need to alarm for each consecutive attachment uploaded # afterwards _payload['sound'] = PushoverSound.NONE diff --git a/apprise/plugins/NotifySES.py b/apprise/plugins/NotifySES.py index 2f4a23c3b0..6b08263127 100644 --- a/apprise/plugins/NotifySES.py +++ b/apprise/plugins/NotifySES.py @@ -136,6 +136,9 @@ class NotifySES(NotifyBase): # A URL that takes you to the setup/help of the specific protocol setup_url = 'https://github.com/caronc/apprise/wiki/Notify_ses' + # Support attachments + attachment_support = True + # AWS is pretty good for handling data load so request limits # can occur in much shorter bursts request_rate_per_sec = 2.5 @@ -427,7 +430,8 @@ def send(self, body, title='', notify_type=NotifyType.INFO, attach=None, content = MIMEText(body, 'plain', 'utf-8') # Create a Multipart container if there is an attachment - base = MIMEMultipart() if attach else content + base = MIMEMultipart() \ + if attach and self.attachment_support else content # TODO: Deduplicate with `NotifyEmail`? base['Subject'] = Header(title, 'utf-8') @@ -443,7 +447,7 @@ def send(self, body, title='', notify_type=NotifyType.INFO, attach=None, timezone.utc).strftime("%a, %d %b %Y %H:%M:%S +0000") base['X-Application'] = self.app_id - if attach: + if attach and self.attachment_support: # First attach our body to our content as the first element base.attach(content) diff --git a/apprise/plugins/NotifySMSEagle.py b/apprise/plugins/NotifySMSEagle.py index ac29838f58..8fb6615822 100644 --- a/apprise/plugins/NotifySMSEagle.py +++ b/apprise/plugins/NotifySMSEagle.py @@ -112,6 +112,9 @@ class NotifySMSEagle(NotifyBase): # The path we send our notification to notify_path = '/jsonrpc/sms' + # Support attachments + attachment_support = True + # The maxumum length of the text message # The actual limit is 160 but SMSEagle looks after the handling # of large messages in it's upstream service @@ -340,7 +343,7 @@ def send(self, body, title='', notify_type=NotifyType.INFO, attach=None, has_error = False attachments = [] - if attach: + if attach and self.attachment_support: for attachment in attach: # Perform some simple error checking if not attachment: diff --git a/apprise/plugins/NotifySMTP2Go.py b/apprise/plugins/NotifySMTP2Go.py index 3634ba6a88..57c4cc9dc6 100644 --- a/apprise/plugins/NotifySMTP2Go.py +++ b/apprise/plugins/NotifySMTP2Go.py @@ -91,6 +91,9 @@ class NotifySMTP2Go(NotifyBase): # Notify URL notify_url = 'https://api.smtp2go.com/v3/email/send' + # Support attachments + attachment_support = True + # Default Notify Format notify_format = NotifyFormat.HTML @@ -294,8 +297,8 @@ def send(self, body, title='', notify_type=NotifyType.INFO, attach=None, # Track our potential attachments attachments = [] - if attach: - for idx, attachment in enumerate(attach): + if attach and self.attachment_support: + for attachment in attach: # Perform some simple error checking if not attachment: # We could not access the attachment diff --git a/apprise/plugins/NotifySignalAPI.py b/apprise/plugins/NotifySignalAPI.py index 589499f8dc..35fad739d5 100644 --- a/apprise/plugins/NotifySignalAPI.py +++ b/apprise/plugins/NotifySignalAPI.py @@ -68,6 +68,9 @@ class NotifySignalAPI(NotifyBase): # A URL that takes you to the setup/help of the specific protocol setup_url = 'https://github.com/caronc/apprise/wiki/Notify_signal' + # Support attachments + attachment_support = True + # The maximum targets to include when doing batch transfers default_batch_size = 10 @@ -234,7 +237,7 @@ def send(self, body, title='', notify_type=NotifyType.INFO, attach=None, has_error = False attachments = [] - if attach: + if attach and self.attachment_support: for attachment in attach: # Perform some simple error checking if not attachment: @@ -281,7 +284,7 @@ def send(self, body, title='', notify_type=NotifyType.INFO, attach=None, payload = { 'message': "{}{}".format( '' if not self.status else '{} '.format( - self.asset.ascii(notify_type)), body), + self.asset.ascii(notify_type)), body).rstrip(), "number": self.source, "recipients": [] } diff --git a/apprise/plugins/NotifySlack.py b/apprise/plugins/NotifySlack.py index 64038e746f..7c6a114317 100644 --- a/apprise/plugins/NotifySlack.py +++ b/apprise/plugins/NotifySlack.py @@ -143,6 +143,10 @@ class NotifySlack(NotifyBase): # A URL that takes you to the setup/help of the specific protocol setup_url = 'https://github.com/caronc/apprise/wiki/Notify_slack' + # Support attachments + attachment_support = True + + # The maximum targets to include when doing batch transfers # Slack Webhook URL webhook_url = 'https://hooks.slack.com/services' @@ -522,7 +526,8 @@ def send(self, body, title='', notify_type=NotifyType.INFO, attach=None, # Include the footer only if specified to do so payload['attachments'][0]['footer'] = self.app_id - if attach and self.mode is SlackMode.WEBHOOK: + if attach and self.attachment_support \ + and self.mode is SlackMode.WEBHOOK: # Be friendly; let the user know why they can't send their # attachments if using the Webhook mode self.logger.warning( @@ -600,7 +605,8 @@ def send(self, body, title='', notify_type=NotifyType.INFO, attach=None, ' to {}'.format(channel) if channel is not None else '')) - if attach and self.mode is SlackMode.BOT and attach_channel_list: + if attach and self.attachment_support and \ + self.mode is SlackMode.BOT and attach_channel_list: # Send our attachments (can only be done in bot mode) for attachment in attach: diff --git a/apprise/plugins/NotifySparkPost.py b/apprise/plugins/NotifySparkPost.py index 9e89113b21..52e5ef9271 100644 --- a/apprise/plugins/NotifySparkPost.py +++ b/apprise/plugins/NotifySparkPost.py @@ -118,6 +118,9 @@ class NotifySparkPost(NotifyBase): # The services URL service_url = 'https://sparkpost.com/' + # Support attachments + attachment_support = True + # All notification requests are secure secure_protocol = 'sparkpost' @@ -543,7 +546,7 @@ def send(self, body, title='', notify_type=NotifyType.INFO, attach=None, else: payload['content']['text'] = body - if attach: + if attach and self.attachment_support: # Prepare ourselves an attachment object payload['content']['attachments'] = [] diff --git a/apprise/plugins/NotifyStreamlabs.py b/apprise/plugins/NotifyStreamlabs.py index 3489519a57..b8a6696c61 100644 --- a/apprise/plugins/NotifyStreamlabs.py +++ b/apprise/plugins/NotifyStreamlabs.py @@ -277,8 +277,7 @@ def __init__(self, access_token, return - def send(self, body, title='', notify_type=NotifyType.INFO, attach=None, - **kwargs): + def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): """ Perform Streamlabs notification call (either donation or alert) """ diff --git a/apprise/plugins/NotifyTelegram.py b/apprise/plugins/NotifyTelegram.py index 3abbd6e9b7..2404b0abed 100644 --- a/apprise/plugins/NotifyTelegram.py +++ b/apprise/plugins/NotifyTelegram.py @@ -123,6 +123,9 @@ class NotifyTelegram(NotifyBase): # Telegram uses the http protocol with JSON requests notify_url = 'https://api.telegram.org/bot' + # Support attachments + attachment_support = True + # Allows the user to specify the NotifyImageSize object image_size = NotifyImageSize.XY_256 @@ -715,6 +718,10 @@ def send(self, body, title='', notify_type=NotifyType.INFO, attach=None, # Prepare our payload based on HTML or TEXT payload['text'] = body + # Handle payloads without a body specified (but an attachment present) + attach_content = \ + TelegramContentPlacement.AFTER if not body else self.content + # Create a copy of the chat_ids list targets = list(self.targets) while len(targets): @@ -748,7 +755,8 @@ def send(self, body, title='', notify_type=NotifyType.INFO, attach=None, 'Failed to send Telegram type image to {}.', payload['chat_id']) - if attach and self.content == TelegramContentPlacement.AFTER: + if attach and self.attachment_support and \ + attach_content == TelegramContentPlacement.AFTER: # Send our attachments now (if specified and if it exists) if not self._send_attachments( chat_id=payload['chat_id'], notify_type=notify_type, @@ -757,6 +765,10 @@ def send(self, body, title='', notify_type=NotifyType.INFO, attach=None, has_error = True continue + if not body: + # Nothing more to do; move along to the next attachment + continue + # 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. @@ -819,7 +831,8 @@ def send(self, body, title='', notify_type=NotifyType.INFO, attach=None, self.logger.info('Sent Telegram notification.') - if attach and self.content == TelegramContentPlacement.BEFORE: + if attach and self.attachment_support \ + and attach_content == TelegramContentPlacement.BEFORE: # Send our attachments now (if specified and if it exists) as # it was identified to send the content before the attachments # which is now done. diff --git a/apprise/plugins/NotifyTwitter.py b/apprise/plugins/NotifyTwitter.py index 9743a589ce..0adefddb3f 100644 --- a/apprise/plugins/NotifyTwitter.py +++ b/apprise/plugins/NotifyTwitter.py @@ -88,6 +88,9 @@ class NotifyTwitter(NotifyBase): # A URL that takes you to the setup/help of the specific protocol setup_url = 'https://github.com/caronc/apprise/wiki/Notify_twitter' + # Support attachments + attachment_support = True + # Do not set body_maxlen as it is set in a property value below # since the length varies depending if we are doing a direct message # or a tweet @@ -285,7 +288,7 @@ def send(self, body, title='', notify_type=NotifyType.INFO, attach=None, # Build a list of our attachments attachments = [] - if attach: + if attach and self.attachment_support: # We need to upload our payload first so that we can source it # in remaining messages for attachment in attach: @@ -414,7 +417,7 @@ def _send_tweet(self, body, title='', notify_type=NotifyType.INFO, _payload = deepcopy(payload) _payload['media_ids'] = media_ids - if no: + if no or not body: # strip text and replace it with the image representation _payload['status'] = \ '{:02d}/{:02d}'.format(no + 1, len(batches)) @@ -514,7 +517,7 @@ def _send_dm(self, body, title='', notify_type=NotifyType.INFO, 'additional_owners': ','.join([str(x) for x in targets.values()]) } - if no: + if no or not body: # strip text and replace it with the image representation _data['text'] = \ '{:02d}/{:02d}'.format(no + 1, len(attachments)) diff --git a/apprise/plugins/NotifyXML.py b/apprise/plugins/NotifyXML.py index 04cdac10e8..d1607e896b 100644 --- a/apprise/plugins/NotifyXML.py +++ b/apprise/plugins/NotifyXML.py @@ -79,6 +79,9 @@ class NotifyXML(NotifyBase): # A URL that takes you to the setup/help of the specific protocol setup_url = 'https://github.com/caronc/apprise/wiki/Notify_Custom_XML' + # Support attachments + attachment_support = True + # Allows the user to specify the NotifyImageSize object image_size = NotifyImageSize.XY_128 @@ -340,7 +343,7 @@ def send(self, body, title='', notify_type=NotifyType.INFO, attach=None, ['<{}>{}'.format(k, v, k) for k, v in payload_base.items()]) attachments = [] - if attach: + if attach and self.attachment_support: for attachment in attach: # Perform some simple error checking if not attachment: diff --git a/docker-compose.yml b/docker-compose.yml index f2d22f0533..cf652300c6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,6 +14,13 @@ services: volumes: - ./:/apprise + test.py311: + build: + context: . + dockerfile: Dockerfile.py311 + volumes: + - ./:/apprise + rpmbuild.el8: build: context: . @@ -36,6 +43,17 @@ services: - ./:/apprise +# +# Every Day testing +# +# Connect to web and create a new project using the manage script +# -> docker-compose run --rm test.py311 bash +# bin/apprise - +# bin/checkdone.sh + +# +# Other Testing +# # Connect to web and create a new project using the manage script # -> docker-compose run --rm test.py36 bash # bin/apprise - diff --git a/test/helpers/rest.py b/test/helpers/rest.py index 72edc28200..b4bf853e37 100644 --- a/test/helpers/rest.py +++ b/test/helpers/rest.py @@ -477,6 +477,52 @@ def __notify(self, url, obj, meta, asset, mock_patch, mock_del, mock_put, notify_type=notify_type, attach=attach) == attach_response + if obj.attachment_support: + # + # Services that support attachments should support + # sending a attachment (or more) without a body or + # title specified: + # + assert obj.notify( + body=None, title=None, + notify_type=notify_type, + attach=attach) == attach_response + + # Turn off attachment support on the notifications + # that support it so we can test that any logic we + # have ot test against this flag is ran + obj.attachment_support = False + + # + # Notifications should still transmit as normal if + # Attachment support is flipped off + # + assert obj.notify( + body=self.body, title=self.title, + notify_type=notify_type, + attach=attach) == notify_response + + # + # We should not be able to send a message without a + # body or title in this circumstance + # + assert obj.notify( + body=None, title=None, + notify_type=notify_type, + attach=attach) is False + + # Toggle Back + obj.attachment_support = True + + else: # No Attachment support + # + # We should not be able to send a message without a + # body or title in this circumstance + # + assert obj.notify( + body=None, title=None, + notify_type=notify_type, + attach=attach) is False else: for _exception in self.req_exceptions: diff --git a/test/test_api.py b/test/test_api.py index bc25a17323..0b53656dd3 100644 --- a/test/test_api.py +++ b/test/test_api.py @@ -257,9 +257,11 @@ def parse_url(url, *args, **kwargs): assert do_notify(a, title=object(), body=b'bytes') is False assert do_notify(a, title=b"bytes", body=object()) is False - # As long as one is present, we're good + # A Body must be present + assert do_notify(a, title='present', body=None) is False + + # Other combinations work fine assert do_notify(a, title=None, body='present') is True - assert do_notify(a, title='present', body=None) is True assert do_notify(a, title="present", body="present") is True # Send Attachment with success diff --git a/test/test_plugin_boxcar.py b/test/test_plugin_boxcar.py index 32ceee58cb..f59c8fab2c 100644 --- a/test/test_plugin_boxcar.py +++ b/test/test_plugin_boxcar.py @@ -170,7 +170,13 @@ def test_plugin_boxcar_edge_cases(mock_post, mock_get): # Test notifications without a body or a title p = NotifyBoxcar(access=access, secret=secret, targets=None) - assert p.notify(body=None, title=None, notify_type=NotifyType.INFO) is True + # Neither a title or body was specified + assert p.notify( + body=None, title=None, notify_type=NotifyType.INFO) is False + + # Acceptable when data is provided: + assert p.notify( + body="Test", title=None, notify_type=NotifyType.INFO) is True # Test comma, separate values device = 'a' * 64