From 87e6eb6cf23d12fb25bf443acdbdfb99a2912d87 Mon Sep 17 00:00:00 2001 From: Chris Caron Date: Sun, 25 Feb 2018 19:59:37 -0500 Subject: [PATCH] Added Discord Support; refs #4 --- README.md | 1 + apprise/AppriseAsset.py | 76 +++++- .../assets/themes/default/apprise-logo.png | Bin 0 -> 42935 bytes apprise/plugins/NotifyBase.py | 33 ++- apprise/plugins/NotifyDiscord.py | 251 ++++++++++++++++++ apprise/plugins/NotifySlack.py | 2 +- apprise/plugins/__init__.py | 3 +- test/test_api.py | 26 +- test/test_notify_base.py | 10 + test/test_rest_plugins.py | 104 ++++++++ 10 files changed, 490 insertions(+), 16 deletions(-) create mode 100644 apprise/assets/themes/default/apprise-logo.png create mode 100644 apprise/plugins/NotifyDiscord.py diff --git a/README.md b/README.md index 3c3c54bb..9c43c41d 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ The table below identifies the services this tool supports and some example serv | Notification Service | Service ID | Default Port | Example Syntax | | -------------------- | ---------- | ------------ | -------------- | | [Boxcar](https://github.com/caronc/apprise/wiki/Notify_boxcar) | boxcar:// | (TCP) 443 | boxcar://hostname
boxcar://hostname/@tag
boxcar://hostname/device_token
boxcar://hostname/device_token1/device_token2/device_tokenN
boxcar://hostname/@tag/@tag2/device_token +| [Discord](https://github.com/caronc/apprise/wiki/Notify_discord) | discord:// | (TCP) 443 | discord://webhook_id/webhook_token
discord://avatar@webhook_id/webhook_token | [Faast](https://github.com/caronc/apprise/wiki/Notify_faast) | faast:// | (TCP) 443 | faast://authorizationtoken | [Growl](https://github.com/caronc/apprise/wiki/Notify_growl) | growl:// | (UDP) 23053 | growl://hostname
growl://hostname:portno
growl://password@hostname
growl://password@hostname:port
_Note: you can also use the get parameter _version_ which can allow the growl request to behave using the older v1.x protocol. An example would look like: growl://hostname?version=1 | [Join](https://github.com/caronc/apprise/wiki/Notify_join) | join:// | (TCP) 443 | join://apikey/device
join://apikey/device1/device2/deviceN/
join://apikey/group
join://apikey/groupA/groupB/groupN
join://apikey/DeviceA/groupA/groupN/DeviceN/ diff --git a/apprise/AppriseAsset.py b/apprise/AppriseAsset.py index 691ca05d..5353357a 100644 --- a/apprise/AppriseAsset.py +++ b/apprise/AppriseAsset.py @@ -33,6 +33,15 @@ class AppriseAsset(object): URL masks. """ + # Application Identifier + app_id = 'Apprise' + + # Application Description + app_desc = 'Apprise Notifications' + + # Provider URL + app_url = 'https://github.com/caronc/apprise' + # A Simple Mapping of Colors; For every NOTIFY_TYPE identified, # there should be a mapping to it's color here: html_notify_map = { @@ -52,6 +61,10 @@ class AppriseAsset(object): image_url_mask = \ 'http://nuxref.com/apprise/themes/{THEME}/apprise-{TYPE}-{XY}.png' + # Application Logo + image_url_logo = \ + 'http://nuxref.com/apprise/themes/{THEME}/apprise-logo.png' + # Image Path Mask image_path_mask = abspath(join( dirname(__file__), @@ -76,20 +89,48 @@ class AppriseAsset(object): if image_url_mask is not None: self.image_url_mask = image_url_mask - def html_color(self, notify_type): + def color(self, notify_type, color_type=None): """ Returns an HTML mapped color based on passed in notify type + + if color_type is: + None then a standard hex string is returned as + a string format ('#000000'). + + int then the integer representation is returned + tuple then the the red, green, blue is returned in a tuple + """ + # Attempt to get the type, otherwise return a default grey # if we couldn't look up the entry - return self.html_notify_map.get(notify_type, self.default_html_color) + color = self.html_notify_map.get(notify_type, self.default_html_color) + if color_type is None: + # This is the default return type + return color - def image_url(self, notify_type, image_size): + elif color_type is int: + # Convert the color to integer + return AppriseAsset.hex_to_int(color) + + # The only other type is tuple + elif color_type is tuple: + return AppriseAsset.hex_to_rgb(color) + + # Unsupported type + raise ValueError( + 'AppriseAsset html_color(): An invalid color_type was specified.') + + def image_url(self, notify_type, image_size, logo=False): """ Apply our mask to our image URL + if logo is set to True, then the logo_url is used instead + """ - if not self.image_url_mask: + + url_mask = self.image_url_logo if logo else self.image_url_mask + if not url_mask: # No image to return return None @@ -105,7 +146,7 @@ class AppriseAsset(object): re.IGNORECASE, ) - return re_table.sub(lambda x: re_map[x.group()], self.image_url_mask) + return re_table.sub(lambda x: re_map[x.group()], url_mask) def image_path(self, notify_type, image_size, must_exist=True): """ @@ -154,3 +195,28 @@ class AppriseAsset(object): return None return None + + @staticmethod + def hex_to_rgb(value): + """ + Takes a hex string (such as #00ff00) and returns a tuple in the form + of (red, green, blue) + + eg: #00ff00 becomes : (0, 65535, 0) + + """ + value = value.lstrip('#') + lv = len(value) + return tuple(int(value[i:i + lv // 3], 16) + for i in range(0, lv, lv // 3)) + + @staticmethod + def hex_to_int(value): + """ + Takes a hex string (such as #00ff00) and returns its integer + equivalent + + eg: #00000f becomes : 15 + + """ + return int(value.lstrip('#'), 16) diff --git a/apprise/assets/themes/default/apprise-logo.png b/apprise/assets/themes/default/apprise-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..44df0c7cd2957ce04aad18f5574c52bc11f034f7 GIT binary patch literal 42935 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 literal 0 HcmV?d00001 diff --git a/apprise/plugins/NotifyBase.py b/apprise/plugins/NotifyBase.py index cb62093a..a9ecf20d 100644 --- a/apprise/plugins/NotifyBase.py +++ b/apprise/plugins/NotifyBase.py @@ -95,12 +95,6 @@ class NotifyBase(object): # This value can be the same as the defined protocol. secure_protocol = '' - # our Application identifier - app_id = 'Apprise' - - # our Application description - app_desc = 'Apprise Notifications' - # Most Servers do not like more then 1 request per 5 seconds, so 5.5 gives # us a safe play range... throttle_attempt = 5.5 @@ -177,7 +171,7 @@ class NotifyBase(object): return - def image_url(self, notify_type): + def image_url(self, notify_type, logo=False): """ Returns Image URL if possible """ @@ -191,6 +185,7 @@ class NotifyBase(object): return self.asset.image_url( notify_type=notify_type, image_size=self.image_size, + logo=logo, ) def image_path(self, notify_type): @@ -223,6 +218,30 @@ class NotifyBase(object): image_size=self.image_size, ) + def color(self, notify_type, color_type=None): + """ + Returns the html color (hex code) associated with the notify_type + """ + if notify_type not in NOTIFY_TYPES: + return None + + return self.asset.color( + notify_type=notify_type, + color_type=color_type, + ) + + @property + def app_id(self): + return self.asset.app_id + + @property + def app_desc(self): + return self.asset.app_desc + + @property + def app_url(self): + return self.asset.app_url + @staticmethod def escape_html(html, convert_new_lines=False): """ diff --git a/apprise/plugins/NotifyDiscord.py b/apprise/plugins/NotifyDiscord.py new file mode 100644 index 00000000..6022a920 --- /dev/null +++ b/apprise/plugins/NotifyDiscord.py @@ -0,0 +1,251 @@ +# -*- coding: utf-8 -*- +# +# Discord Notify Wrapper +# +# Copyright (C) 2018 Chris Caron +# +# This file is part of apprise. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. + +# For this to work correctly you need to create a webhook. To do this just +# click on the little gear icon next to the channel you're part of. From +# here you'll be able to access the Webhooks menu and create a new one. +# +# When you've completed, you'll get a URL that looks a little like this: +# https://discordapp.com/api/webhooks/417429632418316298/\ +# JHZ7lQml277CDHmQKMHI8qBe7bk2ZwO5UKjCiOAF7711o33MyqU344Qpgv7YTpadV_js +# +# Simplified, it looks like this: +# https://discordapp.com/api/webhooks/WEBHOOK_ID/WEBHOOK_TOKEN +# +# This plugin will simply work using the url of: +# discord://WEBHOOK_ID/WEBHOOK_TOKEN +# +# API Documentation on Webhooks: +# - https://discordapp.com/developers/docs/resources/webhook +# +import requests +from json import dumps + +from .NotifyBase import NotifyBase +from .NotifyBase import HTTP_ERROR_MAP +from ..common import NotifyImageSize +from ..utils import parse_bool + +# Image Support (256x256) +DISCORD_IMAGE_XY = NotifyImageSize.XY_256 + + +class NotifyDiscord(NotifyBase): + """ + A wrapper to Discord Notifications + + """ + + # The default secure protocol + secure_protocol = 'discord' + + # Discord Webhook + notify_url = 'https://discordapp.com/api/webhooks' + + def __init__(self, webhook_id, webhook_token, tts=False, avatar=True, + footer=False, thumbnail=True, **kwargs): + """ + Initialize Discord Object + + """ + super(NotifyDiscord, self).__init__( + title_maxlen=250, body_maxlen=2000, + image_size=DISCORD_IMAGE_XY, **kwargs) + + if not webhook_id: + raise TypeError( + 'An invalid Client ID was specified.' + ) + + if not webhook_token: + raise TypeError( + 'An invalid Webhook Token was specified.' + ) + + # Store our data + self.webhook_id = webhook_id + self.webhook_token = webhook_token + + # Text To Speech + self.tts = tts + + # Over-ride Avatar Icon + self.avatar = avatar + + # Place a footer icon + self.footer = footer + + # Place a thumbnail image inline with the message body + self.thumbnail = thumbnail + + return + + def notify(self, title, body, notify_type, **kwargs): + """ + Perform Discord Notification + """ + + headers = { + 'User-Agent': self.app_id, + 'Content-Type': 'multipart/form-data', + } + + # Prepare JSON Object + payload = { + # Text-To-Speech + 'tts': self.tts, + + # If Text-To-Speech is set to True, then we do not want to wait + # for the whole message before continuing. Otherwise, we wait + 'wait': self.tts is False, + + # Our color associated with our notification + 'color': self.color(notify_type, int), + + 'embeds': [{ + 'provider': { + 'name': self.app_id, + 'url': self.app_url, + }, + 'title': title, + 'type': 'rich', + 'description': body, + }] + } + + if self.footer: + logo_url = self.image_url(notify_type, logo=True) + payload['embeds'][0]['footer'] = { + 'text': self.app_desc, + } + if logo_url: + payload['embeds'][0]['footer']['icon_url'] = logo_url + + image_url = self.image_url(notify_type) + if image_url: + if self.thumbnail: + payload['embeds'][0]['thumbnail'] = { + 'url': image_url, + 'height': 256, + 'width': 256, + } + + if self.avatar: + payload['avatar_url'] = image_url + + if self.user: + # Optionally override the default username of the webhook + payload['username'] = self.user + + # Construct Notify URL + notify_url = '{0}/{1}/{2}'.format( + self.notify_url, + self.webhook_id, + self.webhook_token, + ) + + self.logger.debug('Discord POST URL: %s (cert_verify=%r)' % ( + notify_url, self.verify_certificate, + )) + self.logger.debug('Discord Payload: %s' % str(payload)) + try: + r = requests.post( + notify_url, + data=dumps(payload), + headers=headers, + verify=self.verify_certificate, + ) + if r.status_code not in ( + requests.codes.ok, requests.codes.no_content): + # We had a problem + try: + self.logger.warning( + 'Failed to send Discord notification: ' + '%s (error=%s).' % ( + HTTP_ERROR_MAP[r.status_code], + r.status_code)) + + except KeyError: + self.logger.warning( + 'Failed to send Discord notification ' + '(error=%s).' % r.status_code) + + self.logger.debug('Response Details: %s' % r.raw.read()) + + # Return; we're done + return False + + else: + self.logger.info('Sent Discord notification.') + + except requests.RequestException as e: + self.logger.warning( + 'A Connection error occured sending Discord ' + 'notification.' + ) + self.logger.debug('Socket Exception: %s' % str(e)) + return False + + return True + + @staticmethod + def parse_url(url): + """ + Parses the URL and returns enough arguments that can allow + us to substantiate this object. + + Syntax: + discord://webhook_id/webhook_token + + """ + results = NotifyBase.parse_url(url) + + if not results: + # We're done early as we couldn't load the results + return results + + # Store our webhook ID + webhook_id = results['host'] + + # Now fetch our tokens + try: + webhook_token = [x for x in filter(bool, NotifyBase.split_path( + results['fullpath']))][0] + + except (ValueError, AttributeError, IndexError): + # Force some bad values that will get caught + # in parsing later + webhook_token = None + + results['webhook_id'] = webhook_id + results['webhook_token'] = webhook_token + + # Text To Speech + results['tts'] = parse_bool(results['qsd'].get('tts', False)) + + # Use Footer + results['footer'] = parse_bool(results['qsd'].get('footer', False)) + + # Update Avatar Icon + results['avatar'] = parse_bool(results['qsd'].get('avatar', True)) + + # Use Thumbnail + results['thumbnail'] = \ + parse_bool(results['qsd'].get('thumbnail', True)) + + return results diff --git a/apprise/plugins/NotifySlack.py b/apprise/plugins/NotifySlack.py index f8781232..802ea734 100644 --- a/apprise/plugins/NotifySlack.py +++ b/apprise/plugins/NotifySlack.py @@ -220,7 +220,7 @@ class NotifySlack(NotifyBase): 'attachments': [{ 'title': title, 'text': body, - 'color': self.asset.html_color(notify_type), + 'color': self.color(notify_type), # Time 'ts': time(), 'footer': self.app_id, diff --git a/apprise/plugins/__init__.py b/apprise/plugins/__init__.py index b0213a56..19699182 100644 --- a/apprise/plugins/__init__.py +++ b/apprise/plugins/__init__.py @@ -21,6 +21,7 @@ from . import NotifyEmail as NotifyEmailBase from .NotifyBoxcar import NotifyBoxcar +from .NotifyDiscord import NotifyDiscord from .NotifyEmail import NotifyEmail from .NotifyFaast import NotifyFaast from .NotifyGrowl.NotifyGrowl import NotifyGrowl @@ -55,7 +56,7 @@ __all__ = [ 'NotifyMyAndroid', 'NotifyProwl', 'NotifyPushalot', 'NotifyPushBullet', 'NotifyPushover', 'NotifyRocketChat', 'NotifyToasty', 'NotifyTwitter', 'NotifyXBMC', 'NotifyXML', 'NotifySlack', 'NotifyJoin', 'NotifyTelegram', - 'NotifyMatterMost', 'NotifyPushjet', + 'NotifyMatterMost', 'NotifyPushjet', 'NotifyDiscord', # Reference 'NotifyImageSize', 'NOTIFY_IMAGE_SIZES', 'NotifyType', 'NOTIFY_TYPES', diff --git a/test/test_api.py b/test/test_api.py index 0238b654..bbaa6a53 100644 --- a/test/test_api.py +++ b/test/test_api.py @@ -298,8 +298,30 @@ def test_apprise_asset(tmpdir): a.default_html_color = '#abcabc' a.html_notify_map[NotifyType.INFO] = '#aaaaaa' - assert(a.html_color('invalid') == '#abcabc') - assert(a.html_color(NotifyType.INFO) == '#aaaaaa') + assert(a.color('invalid', tuple) == (171, 202, 188)) + assert(a.color(NotifyType.INFO, tuple) == (170, 170, 170)) + + assert(a.color('invalid', int) == 11258556) + assert(a.color(NotifyType.INFO, int) == 11184810) + + assert(a.color('invalid', None) == '#abcabc') + assert(a.color(NotifyType.INFO, None) == '#aaaaaa') + # None is the default + assert(a.color(NotifyType.INFO) == '#aaaaaa') + + # Invalid Type + try: + a.color(NotifyType.INFO, dict) + # We should not get here (exception should be thrown) + assert(False) + + except ValueError: + # The exception we expect since dict is not supported + assert(True) + + except: + # Any other exception is not good + assert(False) assert(a.image_url(NotifyType.INFO, NotifyImageSize.XY_256) == 'http://localhost/dark/info-256x256.png') diff --git a/test/test_notify_base.py b/test/test_notify_base.py index f4af0a19..8d0bb709 100644 --- a/test/test_notify_base.py +++ b/test/test_notify_base.py @@ -20,6 +20,7 @@ from apprise.plugins.NotifyBase import NotifyBase from apprise import NotifyType from apprise import NotifyImageSize from timeit import default_timer +from apprise.utils import compat_is_basestring def test_notify_base(): @@ -75,6 +76,15 @@ def test_notify_base(): assert nb.image_path(notify_type=NotifyType.INFO) is None assert nb.image_raw(notify_type=NotifyType.INFO) is None + # Color handling + assert nb.color(notify_type='invalid') is None + assert compat_is_basestring( + nb.color(notify_type=NotifyType.INFO, color_type=None)) + assert isinstance( + nb.color(notify_type=NotifyType.INFO, color_type=int), int) + assert isinstance( + nb.color(notify_type=NotifyType.INFO, color_type=tuple), tuple) + # Create an object with an ImageSize loaded into it nb = NotifyBase(image_size=NotifyImageSize.XY_256) diff --git a/test/test_rest_plugins.py b/test/test_rest_plugins.py index d11ee2a5..2ed184ca 100644 --- a/test/test_rest_plugins.py +++ b/test/test_rest_plugins.py @@ -87,6 +87,65 @@ TEST_URLS = ( 'test_requests_exceptions': True, }), + ################################## + # NotifyDiscord + ################################## + ('discord://', { + 'instance': None, + }), + # No webhook_token specified + ('discord://%s' % ('i' * 24), { + 'instance': TypeError, + }), + # Provide both an webhook id and a webhook token + ('discord://%s/%s' % ('i' * 24, 't' * 64), { + 'instance': plugins.NotifyDiscord, + 'requests_response_code': requests.codes.no_content, + }), + # Provide a temporary username + ('discord://l2g@%s/%s' % ('i' * 24, 't' * 64), { + 'instance': plugins.NotifyDiscord, + 'requests_response_code': requests.codes.no_content, + }), + # Enable other options + ('discord://%s/%s?footer=Yes&thumbnail=Yes' % ('i' * 24, 't' * 64), { + 'instance': plugins.NotifyDiscord, + 'requests_response_code': requests.codes.no_content, + }), + ('discord://%s/%s?avatar=No&footer=No' % ('i' * 24, 't' * 64), { + 'instance': plugins.NotifyDiscord, + 'requests_response_code': requests.codes.no_content, + }), + # Test without image set + ('discord://%s/%s' % ('i' * 24, 't' * 64), { + 'instance': plugins.NotifyDiscord, + 'requests_response_code': requests.codes.no_content, + # don't include an image by default + 'include_image': False, + }), + # An invalid url + ('discord://:@/', { + 'instance': None, + }), + ('discord://%s/%s/' % ('a' * 24, 'b' * 64), { + 'instance': plugins.NotifyDiscord, + # force a failure + 'response': False, + 'requests_response_code': requests.codes.internal_server_error, + }), + ('discord://%s/%s/' % ('a' * 24, 'b' * 64), { + 'instance': plugins.NotifyDiscord, + # throw a bizzare code forcing us to fail to look it up + 'response': False, + 'requests_response_code': 999, + }), + ('discord://%s/%s/' % ('a' * 24, 'b' * 64), { + 'instance': plugins.NotifyDiscord, + # Throws a series of connection and transfer exceptions when this flag + # is set and tests that we gracfully handle them + 'test_requests_exceptions': True, + }), + ################################## # NotifyFaast ################################## @@ -1384,6 +1443,51 @@ def test_notify_boxcar_plugin(mock_post, mock_get): p.notify(body=None, title=None, notify_type=NotifyType.INFO) is True +@mock.patch('requests.get') +@mock.patch('requests.post') +def test_notify_discord_plugin(mock_post, mock_get): + """ + API: NotifyDiscord() Extra Checks + + """ + + # Initialize some generic (but valid) tokens + webhook_id = 'A' * 24 + webhook_token = 'B' * 64 + + # 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 + + # Empty Channel list + try: + plugins.NotifyDiscord(webhook_id=None, webhook_token=webhook_token) + assert(False) + + except TypeError: + # we'll thrown because no webhook_id was specified + assert(True) + + obj = plugins.NotifyDiscord( + webhook_id=webhook_id, + webhook_token=webhook_token, + footer=True, thumbnail=False) + + # Disable throttling to speed up unit tests + obj.throttle_attempt = 0 + + # This call includes an image with it's payload: + assert obj.notify(title='title', body='body', + notify_type=NotifyType.INFO) is True + + # Toggle our logo availability + obj.asset.image_url_logo = None + assert obj.notify(title='title', body='body', + notify_type=NotifyType.INFO) is True + + @mock.patch('requests.get') @mock.patch('requests.post') def test_notify_join_plugin(mock_post, mock_get):