From 51a5ce344ad182e64624409b6fcf222fd8e17614 Mon Sep 17 00:00:00 2001 From: Chris Caron Date: Sat, 11 May 2019 15:02:56 -0400 Subject: [PATCH] Support Slack URLs missing channel references (#108) --- README.md | 2 +- apprise/plugins/NotifySlack.py | 105 ++++++++++++++++----------------- test/test_rest_plugins.py | 15 +---- 3 files changed, 54 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index 4a82a582..4be81b7a 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ The table below identifies the services this tool supports and some example serv | [Pushover](https://github.com/caronc/apprise/wiki/Notify_pushover) | pover:// | (TCP) 443 | pover://user@token
pover://user@token/DEVICE
pover://user@token/DEVICE1/DEVICE2/DEVICEN
**Note**: you must specify both your user_id and token | [Rocket.Chat](https://github.com/caronc/apprise/wiki/Notify_rocketchat) | rocket:// or rockets:// | (TCP) 80 or 443 | rocket://user:password@hostname/RoomID/Channel
rockets://user:password@hostname:443/Channel1/Channel1/RoomID
rocket://user:password@hostname/Channel | [Ryver](https://github.com/caronc/apprise/wiki/Notify_ryver) | ryver:// | (TCP) 443 | ryver://Organization/Token
ryver://botname@Organization/Token -| [Slack](https://github.com/caronc/apprise/wiki/Notify_slack) | slack:// | (TCP) 443 | slack://TokenA/TokenB/TokenC/Channel
slack://botname@TokenA/TokenB/TokenC/Channel
slack://user@TokenA/TokenB/TokenC/Channel1/Channel2/ChannelN +| [Slack](https://github.com/caronc/apprise/wiki/Notify_slack) | slack:// | (TCP) 443 | slack://TokenA/TokenB/TokenC/
slack://TokenA/TokenB/TokenC/Channel
slack://botname@TokenA/TokenB/TokenC/Channel
slack://user@TokenA/TokenB/TokenC/Channel1/Channel2/ChannelN | [Telegram](https://github.com/caronc/apprise/wiki/Notify_telegram) | tgram:// | (TCP) 443 | tgram://bottoken/ChatID
tgram://bottoken/ChatID1/ChatID2/ChatIDN | [Twilio](https://github.com/caronc/apprise/wiki/Notify_twilio) | twilio:// | (TCP) 443 | twilio://AccountSid:AuthToken@FromPhoneNo
twilio://AccountSid:AuthToken@FromPhoneNo/ToPhoneNo
twilio://AccountSid:AuthToken@FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/
twilio://AccountSid:AuthToken@ShortCode/ToPhoneNo
twilio://AccountSid:AuthToken@ShortCode/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/ | [Twitter](https://github.com/caronc/apprise/wiki/Notify_twitter) | tweet:// | (TCP) 443 | tweet://user@CKey/CSecret/AKey/ASecret diff --git a/apprise/plugins/NotifySlack.py b/apprise/plugins/NotifySlack.py index 77c81023..0aad4657 100644 --- a/apprise/plugins/NotifySlack.py +++ b/apprise/plugins/NotifySlack.py @@ -36,7 +36,6 @@ # # import re -import six import requests from json import dumps from time import time @@ -46,6 +45,7 @@ from ..common import NotifyImageSize from ..common import NotifyType from ..common import NotifyFormat from ..utils import parse_bool +from ..utils import parse_list # Token required as part of the API request # /AAAAAAAAA/........./........................ @@ -155,21 +155,13 @@ class NotifySlack(NotifyBase): self.logger.warning( 'No user was specified; using %s.' % SLACK_DEFAULT_USER) - if isinstance(targets, six.string_types): - self.channels = [x for x in filter(bool, CHANNEL_LIST_DELIM.split( - targets, - ))] - - elif isinstance(targets, (set, tuple, list)): - self.channels = targets - - else: - self.channels = list() - + # Build list of channels + self.channels = parse_list(targets) if len(self.channels) == 0: - msg = 'No channel(s) were specified.' - self.logger.warning(msg) - raise TypeError(msg) + # No problem; the webhook is smart enough to just notify the + # channel it was created for; adding 'None' is just used as + # a flag lower to not set the channels + self.channels.append(None) # Formatting requirements are defined here: # https://api.slack.com/docs/message-formatting @@ -218,47 +210,48 @@ class NotifySlack(NotifyBase): self.token_c, ) + # prepare JSON Object + payload = { + 'username': self.user if self.user else SLACK_DEFAULT_USER, + # Use Markdown language + 'mrkdwn': (self.notify_format == NotifyFormat.MARKDOWN), + 'attachments': [{ + 'title': title, + 'text': body, + 'color': self.color(notify_type), + # Time + 'ts': time(), + 'footer': self.app_id, + }], + } + # Create a copy of the channel list channels = list(self.channels) while len(channels): channel = channels.pop(0) - if not IS_CHANNEL_RE.match(channel): - self.logger.warning( - "The specified channel '%s' is invalid; skipping." % ( - channel, - ) - ) - # Mark our failure - has_error = True - continue - if len(channel) > 1 and channel[0] == '+': - # Treat as encoded id if prefixed with a + - _channel = channel[1:] + if channel is not None: + # Channel over-ride was specified + if not IS_CHANNEL_RE.match(channel): + self.logger.warning( + "The specified target {} is invalid;" + "skipping.".format(channel)) - elif len(channel) > 1 and channel[0] == '@': - # Treat @ value 'as is' - _channel = channel + # Mark our failure + has_error = True + continue - else: - # Prefix with channel hash tag - _channel = '#%s' % channel + if len(channel) > 1 and channel[0] == '+': + # Treat as encoded id if prefixed with a + + payload['channel'] = channel[1:] - # prepare JSON Object - payload = { - 'channel': _channel, - 'username': self.user if self.user else SLACK_DEFAULT_USER, - # Use Markdown language - 'mrkdwn': (self.notify_format == NotifyFormat.MARKDOWN), - 'attachments': [{ - 'title': title, - 'text': body, - 'color': self.color(notify_type), - # Time - 'ts': time(), - 'footer': self.app_id, - }], - } + elif len(channel) > 1 and channel[0] == '@': + # Treat @ value 'as is' + payload['channel'] = channel + + else: + # Prefix with channel hash tag + payload['channel'] = '#%s' % channel # Acquire our to-be footer icon if configured to do so image_url = None if not self.include_image \ @@ -288,9 +281,10 @@ class NotifySlack(NotifyBase): r.status_code, SLACK_HTTP_ERROR_MAP) self.logger.warning( - 'Failed to send Slack notification to {}: ' + 'Failed to send Slack notification{}: ' '{}{}error={}.'.format( - channel, + ' to {}'.format(channel) + if channel is not None else '', status_str, ', ' if status_str else '', r.status_code)) @@ -304,13 +298,16 @@ class NotifySlack(NotifyBase): else: self.logger.info( - 'Sent Slack notification to {}.'.format(channel)) + 'Sent Slack notification{}.'.format( + ' to {}'.format(channel) + if channel is not None else '')) except requests.RequestException as e: self.logger.warning( - 'A Connection error occured sending Slack:%s ' % ( - channel) + 'notification.' - ) + 'A Connection error occured sending Slack ' + 'notification{}.'.format( + ' to {}'.format(channel) + if channel is not None else '')) self.logger.debug('Socket Exception: %s' % str(e)) # Mark our failure diff --git a/test/test_rest_plugins.py b/test/test_rest_plugins.py index 7d5194a1..ef1c77a1 100644 --- a/test/test_rest_plugins.py +++ b/test/test_rest_plugins.py @@ -1671,8 +1671,8 @@ TEST_URLS = ( 'instance': plugins.NotifySlack, }), ('slack://username@T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ', { - # Missing a channel - 'instance': TypeError, + # Missing a channel, falls back to webhook channel bindings + 'instance': plugins.NotifySlack, }), ('slack://username@INVALID/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/#cool', { # invalid 1st Token @@ -3157,17 +3157,6 @@ def test_notify_slack_plugin(mock_post, mock_get): mock_post.return_value.status_code = requests.codes.ok mock_get.return_value.status_code = requests.codes.ok - # Empty Channel list - try: - plugins.NotifySlack( - token_a=token_a, token_b=token_b, token_c=token_c, - targets=None) - assert False - - except TypeError: - # we'll thrown because an empty list of channels was provided - assert True - # Missing first Token try: plugins.NotifySlack(