Kumulos Support (#151)

This commit is contained in:
Chris Caron 2019-09-11 22:10:32 -04:00 committed by GitHub
parent a459aaffef
commit b2635b8a65
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 335 additions and 6 deletions

View File

@ -45,6 +45,7 @@ The table below identifies the services this tool supports and some example serv
| [IFTTT](https://github.com/caronc/apprise/wiki/Notify_ifttt) | ifttt:// | (TCP) 443 | ifttt://webhooksID/Event<br />ifttt://webhooksID/Event1/Event2/EventN<br/>ifttt://webhooksID/Event1/?+Key=Value<br/>ifttt://webhooksID/Event1/?-Key=value1
| [Join](https://github.com/caronc/apprise/wiki/Notify_join) | join:// | (TCP) 443 | join://apikey/device<br />join://apikey/device1/device2/deviceN/<br />join://apikey/group<br />join://apikey/groupA/groupB/groupN<br />join://apikey/DeviceA/groupA/groupN/DeviceN/
| [KODI](https://github.com/caronc/apprise/wiki/Notify_kodi) | kodi:// or kodis:// | (TCP) 8080 or 443 | kodi://hostname<br />kodi://user@hostname<br />kodi://user:password@hostname:port
| [Kumulos](https://github.com/caronc/apprise/wiki/Notify_kumulos) | kumulos:// | (TCP) 443 | kumulos://apikey/serverkey
| [Mailgun](https://github.com/caronc/apprise/wiki/Notify_mailgun) | mailgun:// | (TCP) 443 | mailgun://user@hostname/apikey<br />mailgun://user@hostname/apikey/email<br />mailgun://user@hostname/apikey/email1/email2/emailN<br />mailgun://user@hostname/apikey/?name="From%20User"
| [Matrix](https://github.com/caronc/apprise/wiki/Notify_matrix) | matrix:// or matrixs:// | (TCP) 80 or 443 | matrix://hostname<br />matrix://user@hostname<br />matrixs://user:pass@hostname:port/#room_alias<br />matrixs://user:pass@hostname:port/!room_id<br />matrixs://user:pass@hostname:port/#room_alias/!room_id/#room2<br />matrixs://token@hostname:port/?webhook=matrix<br />matrix://user:token@hostname/?webhook=slack&format=markdown
| [Mattermost](https://github.com/caronc/apprise/wiki/Notify_mattermost) | mmost:// | (TCP) 8065 | mmost://hostname/authkey<br />mmost://hostname:80/authkey<br />mmost://user@hostname:80/authkey<br />mmost://hostname/authkey?channel=channel<br />mmosts://hostname/authkey<br />mmosts://user@hostname/authkey<br />

View File

@ -0,0 +1,261 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com>
# 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.
# To use this plugin, you must have a Kumulos account set up. Add a client
# and link it with your phone using the phone app (using your Companion App
# option in the profile menu area):
# Android: https://play.google.com/store/apps/\
# details?id=com.kumulos.companion
# iOS: https://apps.apple.com/us/app/kumulos/id1463947782
#
# The API reference used to build this plugin was documented here:
# https://docs.kumulos.com/messaging/api/#sending-in-app-messages
#
import re
import requests
from json import dumps
from .NotifyBase import NotifyBase
from ..common import NotifyType
from ..AppriseLocale import gettext_lazy as _
#
# API Key is a UUID; below is the regex matching
UUID4_RE = \
r'[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}'
# Secret Key Regex Mapping
SERVER_KEY_RE = r'[A-Z0-9+]{36}'
# API Key
VALIDATE_APIKEY = re.compile(UUID4_RE, re.I)
VALIDATE_SERVER_KEY = re.compile(SERVER_KEY_RE, re.I)
# Extend HTTP Error Messages
KUMULOS_HTTP_ERROR_MAP = {
401: 'Unauthorized - Invalid API and/or Server Key.',
422: 'Unprocessable Entity - The request was unparsable.',
400: 'Bad Request - Targeted users do not exist or have unsubscribed.',
}
# Used to break path apart into list of channels
TARGET_LIST_DELIM = re.compile(r'[ \t\r\n,#\\/]+')
class NotifyKumulos(NotifyBase):
"""
A wrapper for Kumulos Notifications
"""
# The default descriptive name associated with the Notification
service_name = 'Kumulos'
# The services URL
service_url = 'https://kumulos.com/'
# The default secure protocol
secure_protocol = 'kumulos'
# A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_kumulos'
# Kumulos uses the http protocol with JSON requests
notify_url = 'https://messages.kumulos.com/v2/notifications'
# The maximum allowable characters allowed in the title per message
title_maxlen = 64
# The maximum allowable characters allowed in the body per message
body_maxlen = 240
# Define object templates
templates = (
'{schema}://{apikey}/{serverkey}',
)
# Define our template tokens
template_tokens = dict(NotifyBase.template_tokens, **{
'apikey': {
'name': _('API Key'),
'type': 'string',
'private': True,
'required': True,
'regex': (UUID4_RE, 'i'),
},
'serverkey': {
'name': _('Server Key'),
'type': 'string',
'private': True,
'required': True,
'regex': (SERVER_KEY_RE, 'i'),
},
})
def __init__(self, apikey, serverkey, **kwargs):
"""
Initialize Kumulos Object
"""
super(NotifyKumulos, self).__init__(**kwargs)
if not apikey:
msg = 'The Kumulos API Key is not specified.'
self.logger.warning(msg)
raise TypeError(msg)
self.apikey = apikey.strip()
if not VALIDATE_APIKEY.match(self.apikey):
msg = 'The Kumulos API Key specified ({}) is invalid.'\
.format(apikey)
self.logger.warning(msg)
raise TypeError(msg)
if not serverkey:
msg = 'The Kumulos Server Key is not specified.'
self.logger.warning(msg)
raise TypeError(msg)
self.serverkey = serverkey.strip()
if not VALIDATE_SERVER_KEY.match(self.serverkey):
msg = 'The Kumulos Server Key specified ({}) is invalid.'\
.format(serverkey)
self.logger.warning(msg)
raise TypeError(msg)
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
"""
Perform Kumulos Notification
"""
headers = {
'User-Agent': self.app_id,
'Content-Type': 'application/json',
'Accept': 'application/json',
}
# prepare JSON Object
payload = {
'target': {
'broadcast': True,
},
'content': {
'title': title,
'message': body,
},
}
# Determine Authentication
auth = (self.apikey, self.serverkey)
self.logger.debug('Kumulos POST URL: %s (cert_verify=%r)' % (
self.notify_url, self.verify_certificate,
))
self.logger.debug('Kumulos Payload: %s' % str(payload))
# Always call throttle before any remote server i/o is made
self.throttle()
try:
r = requests.post(
self.notify_url,
data=dumps(payload),
headers=headers,
auth=auth,
verify=self.verify_certificate,
)
if r.status_code != requests.codes.ok:
# We had a problem
status_str = \
NotifyKumulos.http_response_code_lookup(
r.status_code, KUMULOS_HTTP_ERROR_MAP)
self.logger.warning(
'Failed to send Kumulos notification: '
'{}{}error={}.'.format(
status_str,
', ' if status_str else '',
r.status_code))
self.logger.debug(
'Response Details:\r\n{}'.format(r.content))
return False
else:
self.logger.info('Sent Kumulos notification.')
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured sending Kumulos '
'notification.')
self.logger.debug('Socket Exception: %s' % str(e))
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,
'verify': 'yes' if self.verify_certificate else 'no',
}
return '{schema}://{apikey}/{serverkey}/?{args}'.format(
schema=self.secure_protocol,
apikey=NotifyKumulos.quote(self.apikey, safe=''),
serverkey=NotifyKumulos.quote(self.serverkey, safe=''),
args=NotifyKumulos.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 as we couldn't load the results
return results
# The first token is stored in the hostname
results['apikey'] = NotifyKumulos.unquote(results['host'])
# Now fetch the remaining tokens
try:
results['serverkey'] = \
NotifyKumulos.split_path(results['fullpath'])[0]
except IndexError:
# no token
results['serverkey'] = None
return results

View File

@ -48,11 +48,11 @@ notification services that are out there. Apprise opens the door and makes
it easy to access:
Boxcar, ClickSend, Discord, E-Mail, Emby, Faast, Flock, Gitter, Gotify, Growl,
IFTTT, Join, KODI, Mailgun, MatterMost, Matrix, Microsoft Windows Notifications,
Microsoft Teams, MessageBird, MSG91, Nexmo, Notify MyAndroid, Prowl, Pushalot,
PushBullet, Pushjet, Pushover, Rocket.Chat, SendGrid, SimplePush, Slack,
Super Toasty, Stride, Techulus Push, Telegram, Twilio, Twitter, Twist, XBMC,
XMPP, Webex Teams}
IFTTT, Join, KODI, Kumulos, Mailgun, MatterMost, Matrix, Microsoft Windows
Notifications, Microsoft Teams, MessageBird, MSG91, Nexmo, Notify MyAndroid,
Prowl, Pushalot, PushBullet, Pushjet, Pushover, Rocket.Chat, SendGrid,
SimplePush, Slack, Super Toasty, Stride, Techulus Push, Telegram, Twilio,
Twitter, Twist, XBMC, XMPP, Webex Teams}
Name: python-%{pypi_name}
Version: 0.7.9

View File

@ -71,7 +71,7 @@ setup(
url='https://github.com/caronc/apprise',
keywords='Push Notifications Alerts Email AWS SNS Boxcar ClickSend '
'Discord Dbus Emby Faast Flock Gitter Gnome Gotify Growl IFTTT Join '
'KODI Mailgun Matrix Mattermost MessageBird MSG91 Nexmo Prowl '
'KODI Kumulos Mailgun Matrix Mattermost MessageBird MSG91 Nexmo Prowl '
'PushBullet Pushjet Pushed Pushover Rocket.Chat Ryver SendGrid '
'SimplePush Slack Stride Techulus Push Telegram Twilio Twist Twitter '
'XBMC Microsoft MSTeams Windows Webex CLI API',

View File

@ -888,6 +888,55 @@ TEST_URLS = (
'test_requests_exceptions': True,
}),
##################################
# NotifyKumulos
##################################
('kumulos://', {
# No API or Server Key specified
'instance': None,
}),
('kumulos://:@/', {
# No API or Server Key specified
# We don't have strict host checking on for kumulos, so this URL
# actually becomes parseable and :@ becomes a hostname.
# The below errors because a second token wasn't found
'instance': None,
}),
('kumulos://invalid-api-key', {
# Invalid API Key
'instance': TypeError,
}),
('kumulos://{}'.format(UUID4), {
# Server Key not specified
'instance': TypeError,
}),
('kumulos://{}/{}/'.format(UUID4, 'a' * 26), {
# Invalid Server Key
'instance': TypeError,
}),
('kumulos://{}/{}/'.format(UUID4, 'w' * 36), {
# Everything is okay
'instance': plugins.NotifyKumulos,
}),
('kumulos://{}/{}/'.format(UUID4, 'x' * 36), {
'instance': plugins.NotifyKumulos,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('kumulos://{}/{}/'.format(UUID4, 'y' * 36), {
'instance': plugins.NotifyKumulos,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('kumulos://{}/{}/'.format(UUID4, 'z' * 36), {
'instance': plugins.NotifyKumulos,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
##################################
# NotifyMailgun
##################################
@ -4203,6 +4252,24 @@ def test_notify_ifttt_plugin(mock_post, mock_get):
assert isinstance(obj, plugins.NotifyIFTTT) is True
def test_notify_kumulos_plugin():
"""
API: NotifyKumulos() Extra Checks
"""
# Disable Throttling to speed testing
plugins.NotifyBase.request_rate_per_sec = 0
# Invalid API Key
try:
plugins.NotifyKumulos(None, None)
assert False
except TypeError:
# we'll thrown because an empty list of channels was provided
assert True
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_notify_join_plugin(mock_post, mock_get):