diff --git a/README.md b/README.md index 239a6757..02dcad00 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ The table below identifies the services this tool supports and some example serv | [Emby](https://github.com/caronc/apprise/wiki/Notify_emby) | emby:// or embys:// | (TCP) 8096 | emby://user@hostname/
emby://user:password@hostname | [Faast](https://github.com/caronc/apprise/wiki/Notify_faast) | faast:// | (TCP) 443 | faast://authorizationtoken | [Gnome](https://github.com/caronc/apprise/wiki/Notify_gnome) | gnome:// | n/a | gnome:// +| [Gotify](https://github.com/caronc/apprise/wiki/Notify_gotify) | gotify:// or gotifys:// | (TCP) 80 or 443 | gotify://hostname/token
gotifys://hostname/token?priority=high | [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 | [IFTTT](https://github.com/caronc/apprise/wiki/Notify_ifttt) | ifttt:// | (TCP) 443 | ifttt://webhooksID/Event
ifttt://webhooksID/Event1/Event2/EventN
ifttt://webhooksID/Event1/?+Key=Value
ifttt://webhooksID/Event1/?-Key=value1 | [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/plugins/NotifyGotify.py b/apprise/plugins/NotifyGotify.py new file mode 100644 index 00000000..56b85e77 --- /dev/null +++ b/apprise/plugins/NotifyGotify.py @@ -0,0 +1,250 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2019 Chris Caron +# All rights reserved. +# +# This code is licensed under the MIT License. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files(the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions : +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# For this plugin to work correct, the Gotify server must be set up to allow +# for remote connections. + +# Gotify Docker configuration: https://hub.docker.com/r/gotify/server +# Example: https://github.com/gotify/server/blob/\ +# f2c2688f0b5e6a816bbcec768ca1c0de5af76b88/ADD_MESSAGE_EXAMPLES.md#python +# API: https://gotify.net/docs/swagger-docs + +import six +import requests +from json import dumps + +from .NotifyBase import NotifyBase +from ..common import NotifyType + + +# Priorities +class GotifyPriority(object): + LOW = 0 + MODERATE = 3 + NORMAL = 5 + HIGH = 8 + EMERGENCY = 10 + + +GOTIFY_PRIORITIES = ( + GotifyPriority.LOW, + GotifyPriority.MODERATE, + GotifyPriority.NORMAL, + GotifyPriority.HIGH, + GotifyPriority.EMERGENCY, +) + + +class NotifyGotify(NotifyBase): + """ + A wrapper for Gotify Notifications + """ + # The default descriptive name associated with the Notification + service_name = 'Gotify' + + # The services URL + service_url = 'https://github.com/gotify/server' + + # The default protocol + protocol = 'gotify' + + # The default secure protocol + secure_protocol = 'gotifys' + + # A URL that takes you to the setup/help of the specific protocol + setup_url = 'https://github.com/caronc/apprise/wiki/Notify_gotify' + + def __init__(self, token, priority=None, **kwargs): + """ + Initialize Gotify Object + + """ + super(NotifyGotify, self).__init__(**kwargs) + + if not isinstance(token, six.string_types): + msg = 'An invalid Gotify token was specified.' + self.logger.warning('msg') + raise TypeError(msg) + + if priority not in GOTIFY_PRIORITIES: + self.priority = GotifyPriority.NORMAL + + else: + self.priority = priority + + if self.secure: + self.schema = 'https' + + else: + self.schema = 'http' + + # Our access token does not get created until we first + # authenticate with our Gotify server. The same goes for the + # user id below. + self.access_token = token + + return + + def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): + """ + Perform Gotify Notification + """ + + url = '%s://%s' % (self.schema, self.host) + if self.port: + url += ':%d' % self.port + + # Append our remaining path + url += '/message' + + # Define our parameteers + params = { + 'token': self.access_token, + } + + # Prepare Gotify Object + payload = { + 'priority': 2, + 'title': title, + 'message': body, + } + + # Our headers + headers = { + 'User-Agent': self.app_id, + 'Content-Type': 'application/json', + } + + self.logger.debug('Gotify POST URL: %s (cert_verify=%r)' % ( + url, self.verify_certificate, + )) + self.logger.debug('Gotify Payload: %s' % str(payload)) + + # Always call throttle before the requests are made + self.throttle() + + try: + r = requests.post( + url, + params=params, + data=dumps(payload), + headers=headers, + verify=self.verify_certificate, + ) + if r.status_code != requests.codes.ok: + # We had a problem + status_str = \ + NotifyBase.http_response_code_lookup(r.status_code) + + self.logger.warning( + 'Failed to send Gotify notification: ' + '{}{}error={}.'.format( + status_str, + ', ' if status_str else '', + r.status_code)) + + self.logger.debug( + 'Response Details:\r\n{}'.format(r.content)) + + # Mark our failure + return False + + else: + self.logger.info('Sent Gotify notification.') + + except requests.RequestException as e: + self.logger.warning( + 'A Connection error occured sending Gotify ' + 'notification to %s.' % self.host) + self.logger.debug('Socket Exception: %s' % str(e)) + + # Mark our failure + return False + + return True + + def url(self): + """ + Returns the URL built dynamically based on specified arguments. + """ + + # Define any arguments set + args = { + 'format': self.notify_format, + 'overflow': self.overflow_mode, + 'priority': self.priority, + } + + default_port = 443 if self.secure else 80 + + return '{schema}://{hostname}{port}/{token}/?{args}'.format( + schema=self.secure_protocol if self.secure else self.protocol, + hostname=self.host, + port='' if self.port is None or self.port == default_port + else ':{}'.format(self.port), + token=self.access_token, + args=self.urlencode(args), + ) + + @staticmethod + def parse_url(url): + """ + Parses the URL and returns enough arguments that can allow + us to substantiate this object. + + """ + results = NotifyBase.parse_url(url) + if not results: + # We're done early + return results + + # optionally find the provider key + try: + token = [x for x in filter( + bool, NotifyBase.split_path(results['fullpath']))][0] + + except (AttributeError, IndexError): + token = None + + if 'priority' in results['qsd'] and len(results['qsd']['priority']): + _map = { + 'l': GotifyPriority.LOW, + 'm': GotifyPriority.MODERATE, + 'n': GotifyPriority.NORMAL, + 'h': GotifyPriority.HIGH, + 'e': GotifyPriority.EMERGENCY, + } + try: + results['priority'] = \ + _map[results['qsd']['priority'][0].lower()] + + except KeyError: + # No priority was set + pass + + # Set our token + results['token'] = token + + return results diff --git a/apprise/plugins/__init__.py b/apprise/plugins/__init__.py index 33e59fbd..e30d2bed 100644 --- a/apprise/plugins/__init__.py +++ b/apprise/plugins/__init__.py @@ -34,6 +34,7 @@ from .NotifyDiscord import NotifyDiscord from .NotifyEmail import NotifyEmail from .NotifyEmby import NotifyEmby from .NotifyFaast import NotifyFaast +from .NotifyGotify import NotifyGotify from .NotifyGrowl.NotifyGrowl import NotifyGrowl from .NotifyGnome import NotifyGnome from .NotifyIFTTT import NotifyIFTTT @@ -72,9 +73,9 @@ SCHEMA_MAP = {} __all__ = [ # Notification Services 'NotifyBoxcar', 'NotifyDBus', 'NotifyEmail', 'NotifyEmby', 'NotifyDiscord', - 'NotifyFaast', 'NotifyGnome', 'NotifyGrowl', 'NotifyIFTTT', 'NotifyJoin', - 'NotifyJSON', 'NotifyMatrix', 'NotifyMatterMost', 'NotifyProwl', - 'NotifyPushed', 'NotifyPushBullet', 'NotifyPushjet', + 'NotifyFaast', 'NotifyGnome', 'NotifyGotify', 'NotifyGrowl', 'NotifyIFTTT', + 'NotifyJoin', 'NotifyJSON', 'NotifyMatrix', 'NotifyMatterMost', + 'NotifyProwl', 'NotifyPushed', 'NotifyPushBullet', 'NotifyPushjet', 'NotifyPushover', 'NotifyRocketChat', 'NotifyRyver', 'NotifySlack', 'NotifySNS', 'NotifyTwitter', 'NotifyTelegram', 'NotifyXBMC', 'NotifyXML', 'NotifyWindows', diff --git a/setup.py b/setup.py index b5863f5f..9515bd9a 100755 --- a/setup.py +++ b/setup.py @@ -57,9 +57,9 @@ setup( long_description_content_type='text/markdown', url='https://github.com/caronc/apprise', keywords='Push Notifications Email AWS SNS Boxcar Discord Dbus Emby Faast ' - 'Gnome Growl IFTTT Join KODI Matrix Mattermost Prowl PushBullet ' - 'Pushjet Pushed Pushover Rocket.Chat Ryver Slack Telegram Twiiter ' - 'XBMC Microsoft Windows CLI API', + 'Gnome Gotify Growl IFTTT Join KODI Matrix Mattermost Prowl' + 'PushBullet Pushjet Pushed Pushover Rocket.Chat Ryver Slack Telegram ' + 'Twitter XBMC Microsoft Windows CLI API', author='Chris Caron', author_email='lead2gold@gmail.com', packages=find_packages(), diff --git a/test/test_rest_plugins.py b/test/test_rest_plugins.py index 9ee35e25..1ce0a398 100644 --- a/test/test_rest_plugins.py +++ b/test/test_rest_plugins.py @@ -269,6 +269,51 @@ TEST_URLS = ( 'test_requests_exceptions': True, }), + ################################## + # NotifyGotify + ################################## + ('gotify://', { + 'instance': None, + }), + # No token specified + ('gotify://hostname', { + 'instance': TypeError, + }), + # Provide a hostname and token + ('gotify://hostname/%s' % ('t' * 16), { + 'instance': plugins.NotifyGotify, + }), + # Provide a priority + ('gotify://hostname/%s?priority=high' % ('i' * 16), { + 'instance': plugins.NotifyGotify, + }), + # Provide an invalid priority + ('gotify://hostname:8008/%s?priority=invalid' % ('i' * 16), { + 'instance': plugins.NotifyGotify, + }), + # An invalid url + ('gotify://:@/', { + 'instance': None, + }), + ('gotify://hostname/%s/' % ('t' * 16), { + 'instance': plugins.NotifyGotify, + # force a failure + 'response': False, + 'requests_response_code': requests.codes.internal_server_error, + }), + ('gotifys://localhost/%s/' % ('t' * 16), { + 'instance': plugins.NotifyGotify, + # throw a bizzare code forcing us to fail to look it up + 'response': False, + 'requests_response_code': 999, + }), + ('gotify://localhost/%s/' % ('t' * 16), { + 'instance': plugins.NotifyGotify, + # Throws a series of connection and transfer exceptions when this flag + # is set and tests that we gracfully handle them + 'test_requests_exceptions': True, + }), + ################################## # NotifyIFTTT - If This Than That ##################################