Refactored Slack plugin to allow users to switch between payload types (#482)

pull/486/head
Chris Caron 2021-11-13 08:33:38 -05:00 committed by GitHub
parent a9ce6b3556
commit 76700bfa1d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 154 additions and 70 deletions

View File

@ -73,6 +73,7 @@ import re
import requests import requests
from json import dumps from json import dumps
from json import loads from json import loads
from time import time
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from ..common import NotifyImageSize from ..common import NotifyImageSize
@ -254,6 +255,14 @@ class NotifySlack(NotifyBase):
'default': True, 'default': True,
'map_to': 'include_footer', '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': { 'to': {
'alias_of': 'targets', 'alias_of': 'targets',
}, },
@ -265,7 +274,7 @@ class NotifySlack(NotifyBase):
def __init__(self, access_token=None, token_a=None, token_b=None, def __init__(self, access_token=None, token_a=None, token_b=None,
token_c=None, targets=None, include_image=True, token_c=None, targets=None, include_image=True,
include_footer=True, **kwargs): include_footer=True, use_blocks=None, **kwargs):
""" """
Initialize Slack Object Initialize Slack Object
""" """
@ -316,6 +325,11 @@ class NotifySlack(NotifyBase):
# specify a full email as a recipient via slack # specify a full email as a recipient via slack
self._lookup_users = {} 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 # Build list of channels
self.channels = parse_list(targets) self.channels = parse_list(targets)
if len(self.channels) == 0: if len(self.channels) == 0:
@ -359,45 +373,117 @@ class NotifySlack(NotifyBase):
# error tracking (used for function return) # error tracking (used for function return)
has_error = False has_error = False
# Perform Formatting #
title = self._re_formatting_rules.sub( # pragma: no branch # Prepare JSON Object (applicable to both WEBHOOK and BOT mode)
lambda x: self._re_formatting_map[x.group()], title, #
) if self.use_blocks:
# Only for NONE markdown, otherwise eg links wont work # Our slack format
if self.notify_format != NotifyFormat.MARKDOWN: _slack_format = 'mrkdwn' \
body = self._re_formatting_rules.sub( # pragma: no branch if self.notify_format == NotifyFormat.MARKDOWN \
lambda x: self._re_formatting_map[x.group()], body, 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) # Prepare JSON Object (applicable to both WEBHOOK and BOT mode)
_slack_format = 'mrkdwn' \ payload = {
if self.notify_format == NotifyFormat.MARKDOWN else 'plain_text' 'username': self.user if self.user else self.app_id,
payload = { # Use Markdown language
'username': self.user if self.user else self.app_id, 'mrkdwn': (self.notify_format == NotifyFormat.MARKDOWN),
'attachments': [{ 'attachments': [{
'blocks': [{ 'title': title,
'type': 'section', 'text': body,
'text': { 'color': self.color(notify_type),
'type': _slack_format, # Time
'text': body '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 image_url:
if title: payload['icon_url'] = image_url
payload['attachments'][0]['blocks'].insert(0, {
'type': 'header',
'text': {
'type': 'plain_text',
'text': title,
'emoji': True
}
})
# 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: if self.mode is SlackMode.WEBHOOK:
url = '{}/{}/{}/{}'.format( url = '{}/{}/{}/{}'.format(
self.webhook_url, self.webhook_url,
@ -409,37 +495,6 @@ class NotifySlack(NotifyBase):
else: # SlackMode.BOT else: # SlackMode.BOT
url = self.api_url.format('chat.postMessage') 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 # Create a copy of the channel list
channels = list(self.channels) channels = list(self.channels)
@ -875,6 +930,7 @@ class NotifySlack(NotifyBase):
params = { params = {
'image': 'yes' if self.include_image else 'no', 'image': 'yes' if self.include_image else 'no',
'footer': 'yes' if self.include_footer else 'no', 'footer': 'yes' if self.include_footer else 'no',
'blocks': 'yes' if self.use_blocks else 'no',
} }
# Extend our parameters # Extend our parameters
@ -978,6 +1034,10 @@ class NotifySlack(NotifyBase):
results['include_image'] = \ results['include_image'] = \
parse_bool(results['qsd'].get('image', True)) 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 # Get Footer Flag
results['include_footer'] = \ results['include_footer'] = \
parse_bool(results['qsd'].get('footer', True)) parse_bool(results['qsd'].get('footer', True))

View File

@ -694,10 +694,7 @@ def parse_url(url, default_schema='http', verify_host=True):
def parse_bool(arg, default=False): def parse_bool(arg, default=False):
""" """
NZBGet uses 'yes' and 'no' as well as other strings such as 'on' or Support string based boolean settings.
'off' etch to handle boolean operations from it's control interface.
This method can just simplify checks to these variables.
If the content could not be parsed, then the default is returned. If the content could not be parsed, then the default is returned.
""" """

View File

@ -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 # Test using a bot-token as argument
('slack://?token=xoxb-1234-1234-abc124&to=#nuxref&footer=no&user=test', { ('slack://?token=xoxb-1234-1234-abc124&to=#nuxref&footer=no&user=test', {
'instance': plugins.NotifySlack, 'instance': plugins.NotifySlack,