From 76700bfa1ddcb2812d9ed14dfa7736125bcd346e Mon Sep 17 00:00:00 2001 From: Chris Caron Date: Sat, 13 Nov 2021 08:33:38 -0500 Subject: [PATCH] Refactored Slack plugin to allow users to switch between payload types (#482) --- apprise/plugins/NotifySlack.py | 192 +++++++++++++++++++++------------ apprise/utils.py | 5 +- test/test_rest_plugins.py | 27 +++++ 3 files changed, 154 insertions(+), 70 deletions(-) diff --git a/apprise/plugins/NotifySlack.py b/apprise/plugins/NotifySlack.py index 692968c6..ff7907a3 100644 --- a/apprise/plugins/NotifySlack.py +++ b/apprise/plugins/NotifySlack.py @@ -73,6 +73,7 @@ import re import requests from json import dumps from json import loads +from time import time from .NotifyBase import NotifyBase from ..common import NotifyImageSize @@ -254,6 +255,14 @@ class NotifySlack(NotifyBase): 'default': True, 'map_to': 'include_footer', }, + # Use Payload in Blocks (vs legacy way): + # See: https://api.slack.com/reference/messaging/payload + 'blocks': { + 'name': _('Use Blocks'), + 'type': 'bool', + 'default': False, + 'map_to': 'use_blocks', + }, 'to': { 'alias_of': 'targets', }, @@ -265,7 +274,7 @@ class NotifySlack(NotifyBase): def __init__(self, access_token=None, token_a=None, token_b=None, token_c=None, targets=None, include_image=True, - include_footer=True, **kwargs): + include_footer=True, use_blocks=None, **kwargs): """ Initialize Slack Object """ @@ -316,6 +325,11 @@ class NotifySlack(NotifyBase): # specify a full email as a recipient via slack self._lookup_users = {} + self.use_blocks = parse_bool( + use_blocks, self.template_args['blocks']['default']) \ + if use_blocks is not None \ + else self.template_args['blocks']['default'] + # Build list of channels self.channels = parse_list(targets) if len(self.channels) == 0: @@ -359,45 +373,117 @@ class NotifySlack(NotifyBase): # error tracking (used for function return) has_error = False - # Perform Formatting - title = self._re_formatting_rules.sub( # pragma: no branch - lambda x: self._re_formatting_map[x.group()], title, - ) - # Only for NONE markdown, otherwise eg links wont work - if self.notify_format != NotifyFormat.MARKDOWN: - body = self._re_formatting_rules.sub( # pragma: no branch - lambda x: self._re_formatting_map[x.group()], body, + # + # Prepare JSON Object (applicable to both WEBHOOK and BOT mode) + # + if self.use_blocks: + # Our slack format + _slack_format = 'mrkdwn' \ + if self.notify_format == NotifyFormat.MARKDOWN \ + else 'plain_text' + + payload = { + 'username': self.user if self.user else self.app_id, + 'attachments': [{ + 'blocks': [{ + 'type': 'section', + 'text': { + 'type': _slack_format, + 'text': body + } + }], + 'color': self.color(notify_type), + }] + } + + # Slack only accepts non-empty header sections + if title: + payload['attachments'][0]['blocks'].insert(0, { + 'type': 'header', + 'text': { + 'type': 'plain_text', + 'text': title, + 'emoji': True + } + }) + + # Include the footer only if specified to do so + if self.include_footer: + + # Acquire our to-be footer icon if configured to do so + image_url = None if not self.include_image \ + else self.image_url(notify_type) + + # Prepare our footer based on the block structure + _footer = { + 'type': 'context', + 'elements': [{ + 'type': _slack_format, + 'text': self.app_id + }] + } + + if image_url: + payload['icon_url'] = image_url + + _footer['elements'].insert(0, { + 'type': 'image', + 'image_url': image_url, + 'alt_text': notify_type + }) + + payload['attachments'][0]['blocks'].append(_footer) + + else: + # + # Legacy API Formatting + # + if self.notify_format == NotifyFormat.MARKDOWN: + body = self._re_formatting_rules.sub( # pragma: no branch + lambda x: self._re_formatting_map[x.group()], body, + ) + + # Perform Formatting on title here; this is not needed for block + # mode above + title = self._re_formatting_rules.sub( # pragma: no branch + lambda x: self._re_formatting_map[x.group()], title, ) - # Prepare JSON Object (applicable to both WEBHOOK and BOT mode) - _slack_format = 'mrkdwn' \ - if self.notify_format == NotifyFormat.MARKDOWN else 'plain_text' - payload = { - 'username': self.user if self.user else self.app_id, - 'attachments': [{ - 'blocks': [{ - 'type': 'section', - 'text': { - 'type': _slack_format, - 'text': body - } + # Prepare JSON Object (applicable to both WEBHOOK and BOT mode) + payload = { + 'username': self.user if self.user else self.app_id, + # Use Markdown language + 'mrkdwn': (self.notify_format == NotifyFormat.MARKDOWN), + 'attachments': [{ + 'title': title, + 'text': body, + 'color': self.color(notify_type), + # Time + 'ts': time(), }], - 'color': self.color(notify_type), - }] - } + } + # Acquire our to-be footer icon if configured to do so + image_url = None if not self.include_image \ + else self.image_url(notify_type) - # Slack only accepts non-empty header sections - if title: - payload['attachments'][0]['blocks'].insert(0, { - 'type': 'header', - 'text': { - 'type': 'plain_text', - 'text': title, - 'emoji': True - } - }) + if image_url: + payload['icon_url'] = image_url - # Prepare our URL (depends on mode) + # Include the footer only if specified to do so + if self.include_footer: + if image_url: + payload['attachments'][0]['footer_icon'] = image_url + + # Include the footer only if specified to do so + payload['attachments'][0]['footer'] = self.app_id + + if attach 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( + 'Slack Webhooks do not support attachments.') + + # Prepare our Slack URL (depends on mode) if self.mode is SlackMode.WEBHOOK: url = '{}/{}/{}/{}'.format( self.webhook_url, @@ -409,37 +495,6 @@ class NotifySlack(NotifyBase): else: # SlackMode.BOT url = self.api_url.format('chat.postMessage') - # Include the footer only if specified to do so - if self.include_footer: - _footer = { - 'type': 'context', - 'elements': [{ - 'type': _slack_format, - 'text': self.app_id - }] - } - - # Acquire our to-be footer icon if configured to do so - image_url = None if not self.include_image \ - else self.image_url(notify_type) - - if image_url: - payload['icon_url'] = image_url - - _footer['elements'].insert(0, { - 'type': 'image', - 'image_url': image_url, - 'alt_text': notify_type - }) - - payload['attachments'][0]['blocks'].append(_footer) - - if attach 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( - 'Slack Webhooks do not support attachments.') - # Create a copy of the channel list channels = list(self.channels) @@ -875,6 +930,7 @@ class NotifySlack(NotifyBase): params = { 'image': 'yes' if self.include_image else 'no', 'footer': 'yes' if self.include_footer else 'no', + 'blocks': 'yes' if self.use_blocks else 'no', } # Extend our parameters @@ -978,6 +1034,10 @@ class NotifySlack(NotifyBase): results['include_image'] = \ parse_bool(results['qsd'].get('image', True)) + # Get Payload structure (use blocks?) + if 'blocks' in results['qsd'] and len(results['qsd']['blocks']): + results['use_blocks'] = parse_bool(results['qsd']['blocks']) + # Get Footer Flag results['include_footer'] = \ parse_bool(results['qsd'].get('footer', True)) diff --git a/apprise/utils.py b/apprise/utils.py index b08462c9..27b263c3 100644 --- a/apprise/utils.py +++ b/apprise/utils.py @@ -694,10 +694,7 @@ def parse_url(url, default_schema='http', verify_host=True): def parse_bool(arg, default=False): """ - NZBGet uses 'yes' and 'no' as well as other strings such as 'on' or - 'off' etch to handle boolean operations from it's control interface. - - This method can just simplify checks to these variables. + Support string based boolean settings. If the content could not be parsed, then the default is returned. """ diff --git a/test/test_rest_plugins.py b/test/test_rest_plugins.py index cafcb43b..1e4936b1 100644 --- a/test/test_rest_plugins.py +++ b/test/test_rest_plugins.py @@ -4859,6 +4859,33 @@ TEST_URLS = ( }, }, }), + # Test blocks mode + ('slack://?token=T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/' + '&to=#chan&blocks=yes&footer=yes', + { + 'instance': plugins.NotifySlack, + 'requests_response_text': 'ok'}), + ('slack://?token=T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/' + '&to=#chan&blocks=yes&footer=no', + { + 'instance': plugins.NotifySlack, + 'requests_response_text': 'ok'}), + ('slack://?token=T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/' + '&to=#chan&blocks=yes&footer=yes&image=no', + { + 'instance': plugins.NotifySlack, + 'requests_response_text': 'ok'}), + ('slack://?token=T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/' + '&to=#chan&blocks=yes&format=text', + { + 'instance': plugins.NotifySlack, + 'requests_response_text': 'ok'}), + ('slack://?token=T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/' + '&to=#chan&blocks=no&format=text', + { + 'instance': plugins.NotifySlack, + 'requests_response_text': 'ok'}), + # Test using a bot-token as argument ('slack://?token=xoxb-1234-1234-abc124&to=#nuxref&footer=no&user=test', { 'instance': plugins.NotifySlack,