From 54c3f6d9dfb15a0bbccb285758fa36a6c18eecc0 Mon Sep 17 00:00:00 2001 From: Chris Caron Date: Mon, 19 Oct 2020 20:01:06 -0400 Subject: [PATCH] Added Parse Platform Support (#212) --- README.md | 1 + apprise/plugins/NotifyParsePlatform.py | 320 +++++++++++++++++++++++++ packaging/redhat/python-apprise.spec | 9 +- setup.py | 8 +- test/test_rest_plugins.py | 60 +++++ 5 files changed, 390 insertions(+), 8 deletions(-) create mode 100644 apprise/plugins/NotifyParsePlatform.py diff --git a/README.md b/README.md index ac74c6b8..4d354b4e 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ The table below identifies the services this tool supports and some example serv | [Notica](https://github.com/caronc/apprise/wiki/Notify_notica) | notica:// | (TCP) 443 | notica://Token/ | [Notifico](https://github.com/caronc/apprise/wiki/Notify_notifico) | notifico:// | (TCP) 443 | notifico://ProjectID/MessageHook/ | [Office 365](https://github.com/caronc/apprise/wiki/Notify_office365) | o365:// | (TCP) 443 | o365://TenantID:AccountEmail/ClientID/ClientSecret
o365://TenantID:AccountEmail/ClientID/ClientSecret/TargetEmail
o365://TenantID:AccountEmail/ClientID/ClientSecret/TargetEmail1/TargetEmail2/TargetEmailN +| [ParsePlatform](https://github.com/caronc/apprise/wiki/Notify_parseplatform) | parsep:// or parseps:// | (TCP) 80 or 443 | parsep://AppID:MasterKey@Hostname
parseps://AppID:MasterKey@Hostnam | [PopcornNotify](https://github.com/caronc/apprise/wiki/Notify_popcornnotify) | popcorn:// | (TCP) 443 | popcorn://ApiKey/ToPhoneNo
popcorn://ApiKey/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/
popcorn://ApiKey/ToEmail
popcorn://ApiKey/ToEmail1/ToEmail2/ToEmailN/
popcorn://ApiKey/ToPhoneNo1/ToEmail1/ToPhoneNoN/ToEmailN | [Prowl](https://github.com/caronc/apprise/wiki/Notify_prowl) | prowl:// | (TCP) 443 | prowl://apikey
prowl://apikey/providerkey | [PushBullet](https://github.com/caronc/apprise/wiki/Notify_pushbullet) | pbul:// | (TCP) 443 | pbul://accesstoken
pbul://accesstoken/#channel
pbul://accesstoken/A_DEVICE_ID
pbul://accesstoken/email@address.com
pbul://accesstoken/#channel/#channel2/email@address.net/DEVICE diff --git a/apprise/plugins/NotifyParsePlatform.py b/apprise/plugins/NotifyParsePlatform.py new file mode 100644 index 00000000..07cff21d --- /dev/null +++ b/apprise/plugins/NotifyParsePlatform.py @@ -0,0 +1,320 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2020 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. + +# Official API reference: https://developer.gitter.im/docs/user-resource + +import re +import six +import requests +from json import dumps + +from .NotifyBase import NotifyBase +from ..common import NotifyType +from ..utils import validate_regex +from ..AppriseLocale import gettext_lazy as _ + +# Used to break path apart into list of targets +TARGET_LIST_DELIM = re.compile(r'[ \t\r\n,\\/]+') + + +# Priorities +class ParsePlatformDevice(object): + # All Devices + ALL = 'all' + + # Apple IOS (APNS) + IOS = 'ios' + + # Android/Firebase (FCM) + ANDROID = 'android' + + +PARSE_PLATFORM_DEVICES = ( + ParsePlatformDevice.ALL, + ParsePlatformDevice.IOS, + ParsePlatformDevice.ANDROID, +) + + +class NotifyParsePlatform(NotifyBase): + """ + A wrapper for Parse Platform Notifications + """ + + # The default descriptive name associated with the Notification + service_name = 'Parse Platform' + + # The services URL + service_url = ' https://parseplatform.org/' + + # insecure notifications (using http) + protocol = 'parsep' + + # Secure notifications (using https) + secure_protocol = 'parseps' + + # A URL that takes you to the setup/help of the specific protocol + setup_url = 'https://github.com/caronc/apprise/wiki/Notify_parseplatform' + + # Define object templates + templates = ( + '{schema}://{app_id}:{master_key}@{host}', + '{schema}://{app_id}:{master_key}@{host}:{port}', + ) + + # Define our template tokens + template_tokens = dict(NotifyBase.template_tokens, **{ + 'host': { + 'name': _('Hostname'), + 'type': 'string', + 'required': True, + }, + 'port': { + 'name': _('Port'), + 'type': 'int', + 'min': 1, + 'max': 65535, + }, + 'app_id': { + 'name': _('App ID'), + 'type': 'string', + 'private': True, + 'required': True, + }, + 'master_key': { + 'name': _('Master Key'), + 'type': 'string', + 'private': True, + 'required': True, + }, + }) + + # Define our template arguments + template_args = dict(NotifyBase.template_args, **{ + 'device': { + 'name': _('Device'), + 'type': 'choice:string', + 'values': PARSE_PLATFORM_DEVICES, + 'default': ParsePlatformDevice.ALL, + }, + 'app_id': { + 'alias_of': 'app_id', + }, + 'master_key': { + 'alias_of': 'master_key', + }, + }) + + def __init__(self, app_id, master_key, device=None, **kwargs): + """ + Initialize Parse Platform Object + """ + super(NotifyParsePlatform, self).__init__(**kwargs) + + self.fullpath = kwargs.get('fullpath') + if not isinstance(self.fullpath, six.string_types): + self.fullpath = '/' + + # Application ID + self.application_id = validate_regex(app_id) + if not self.application_id: + msg = 'An invalid Parse Platform Application ID ' \ + '({}) was specified.'.format(app_id) + self.logger.warning(msg) + raise TypeError(msg) + + # Master Key + self.master_key = validate_regex(master_key) + if not self.master_key: + msg = 'An invalid Parse Platform Master Key ' \ + '({}) was specified.'.format(master_key) + self.logger.warning(msg) + raise TypeError(msg) + + # Initialize Devices Array + self.devices = [] + + if device: + self.device = device.lower() + if device not in PARSE_PLATFORM_DEVICES: + msg = 'An invalid Parse Platform device ' \ + '({}) was specified.'.format(device) + self.logger.warning(msg) + raise TypeError(msg) + else: + self.device = self.template_args['device']['default'] + + if self.device == ParsePlatformDevice.ALL: + self.devices = [d for d in PARSE_PLATFORM_DEVICES + if d != ParsePlatformDevice.ALL] + else: + # Store our device + self.devices.append(device) + + return + + def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): + """ + Perform Parse Platform Notification + """ + + # Prepare our headers: + headers = { + 'User-Agent': self.app_id, + 'Content-Type': 'application/json', + 'X-Parse-Application-Id': self.application_id, + 'X-Parse-Master-Key': self.master_key, + } + + # prepare our payload + payload = { + 'where': { + 'deviceType': { + '$in': self.devices, + } + }, + 'data': { + 'title': title, + 'alert': body, + } + } + + # Set our schema + schema = 'https' if self.secure else 'http' + + # Our Notification URL + url = '%s://%s' % (schema, self.host) + if isinstance(self.port, int): + url += ':%d' % self.port + + url += self.fullpath.rstrip('/') + '/parse/push/' + + self.logger.debug('Parse Platform POST URL: %s (cert_verify=%r)' % ( + url, self.verify_certificate, + )) + self.logger.debug('Parse Platform Payload: %s' % str(payload)) + + # Always call throttle before any remote server i/o is made + self.throttle() + + try: + r = requests.post( + url, + data=dumps(payload), + headers=headers, + verify=self.verify_certificate, + ) + if r.status_code != requests.codes.ok: + # We had a problem + status_str = NotifyParsePlatform.\ + http_response_code_lookup(r.status_code) + + self.logger.warning( + 'Failed to send Parse Platform notification: ' + '{}{}error={}.'.format( + status_str, + ', ' if status_str else '', + r.status_code)) + + self.logger.debug('Response Details:\r\n{}'.format(r.content)) + + # Return; we're done + return False + + else: + self.logger.info('Sent Parse Platform notification.') + + except requests.RequestException as e: + self.logger.warning( + 'A Connection error occured sending Parse Platform ' + 'notification to %s.' % self.host) + self.logger.debug('Socket Exception: %s' % str(e)) + + # Return; we're done + return False + + return True + + def url(self, privacy=False, *args, **kwargs): + """ + Returns the URL built dynamically based on specified arguments. + """ + + # Define any arguments set + params = { + 'device': self.device, + } + + # Extend our parameters + params.update(self.url_parameters(privacy=privacy, *args, **kwargs)) + + default_port = 443 if self.secure else 80 + + return \ + '{schema}://{app_id}:{master_key}@' \ + '{hostname}{port}{fullpath}/?{params}'.format( + schema=self.secure_protocol if self.secure else self.protocol, + app_id=self.pprint(self.application_id, privacy, safe=''), + master_key=self.pprint(self.master_key, privacy, safe=''), + hostname=NotifyParsePlatform.quote(self.host, safe=''), + port='' if self.port is None or self.port == default_port + else ':{}'.format(self.port), + fullpath=NotifyParsePlatform.quote(self.fullpath, safe='/'), + params=NotifyParsePlatform.urlencode(params)) + + @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 + + # App ID is retrieved from the user + results['app_id'] = NotifyParsePlatform.unquote(results['user']) + + # Master Key is retrieved from the password + results['master_key'] = \ + NotifyParsePlatform.unquote(results['password']) + + # Device support override + if 'device' in results['qsd'] and len(results['qsd']['device']): + results['device'] = results['qsd']['device'] + + # Allow app_id attribute over-ride + if 'app_id' in results['qsd'] and len(results['qsd']['app_id']): + results['app_id'] = results['qsd']['app_id'] + + # Allow master_key attribute over-ride + if 'master_key' in results['qsd'] \ + and len(results['qsd']['master_key']): + results['master_key'] = results['qsd']['master_key'] + + return results diff --git a/packaging/redhat/python-apprise.spec b/packaging/redhat/python-apprise.spec index caa9ca84..4d34c2b9 100644 --- a/packaging/redhat/python-apprise.spec +++ b/packaging/redhat/python-apprise.spec @@ -50,10 +50,11 @@ it easy to access: Boxcar, ClickSend, Discord, E-Mail, Emby, Faast, Flock, Gitter, Gotify, Growl, IFTTT, Join, Kavenegar, KODI, Kumulos, LaMetric, MacOSX, Mailgun, MatterMost, Matrix, Microsoft Windows, Microsoft Teams, MessageBird, MSG91, MyAndroid, -Nexmo, Nextcloud, Notica, Notifico, Office365, PopcornNotify, Prowl, Pushalot, -PushBullet, Pushjet, Pushover, PushSafer, Rocket.Chat, SendGrid, SimplePush, -Sinch, Slack, Spontit, SparkPost, Super Toasty, Stride, Syslog, Techulus Push, -Telegram, Twilio, Twitter, Twist, XBMC, XMPP, Webex Teams} +Nexmo, Nextcloud, Notica, Notifico, Office365, ParsePlatform, PopcornNotify, +Prowl, Pushalot, PushBullet, Pushjet, Pushover, PushSafer, Rocket.Chat, +SendGrid, SimplePush, Sinch, Slack, Spontit, SparkPost, Super Toasty, Stride, +Syslog, Techulus Push, Telegram, Twilio, Twitter, Twist, XBMC, XMPP, +Webex Teams} Name: python-%{pypi_name} Version: 0.8.9 diff --git a/setup.py b/setup.py index 9c967d88..d46ef1ef 100755 --- a/setup.py +++ b/setup.py @@ -73,10 +73,10 @@ setup( 'Discord Dbus Emby Faast Flock Gitter Gnome Gotify Growl IFTTT Join ' 'Kavenegar KODI Kumulos LaMetric MacOS Mailgun Matrix Mattermost ' 'MessageBird MSG91 Nexmo Nextcloud Notica Notifico Office365 ' - 'PopcornNotify Prowl PushBullet Pushjet Pushed Pushover PushSafer ' - 'Rocket.Chat Ryver SendGrid SimplePush Sinch Slack SparkPost Spontit ' - 'Stride Syslog Techulus Push Telegram Twilio Twist Twitter XBMC ' - 'MSTeams Microsoft Windows Webex CLI API', + 'ParsePlatform PopcornNotify Prowl PushBullet Pushjet Pushed Pushover ' + 'PushSafer Rocket.Chat Ryver SendGrid SimplePush Sinch Slack ' + 'SparkPost Spontit Stride Syslog Techulus Push Telegram Twilio Twist ' + 'Twitter XBMC MSTeams Microsoft Windows Webex 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 2133d4ba..02d4ab93 100644 --- a/test/test_rest_plugins.py +++ b/test/test_rest_plugins.py @@ -2264,6 +2264,66 @@ TEST_URLS = ( 'test_requests_exceptions': True, }), + ################################## + # NotifyParsePlatform + ################################## + ('parsep://', { + 'instance': None, + }), + # API Key + bad url + ('parsep://:@/', { + 'instance': None, + }), + # APIkey; no app_id or master_key + ('parsep://%s' % ('a' * 32), { + 'instance': TypeError, + }), + # APIkey; no master_key + ('parsep://app_id@%s' % ('a' * 32), { + 'instance': TypeError, + }), + # APIkey; no app_id + ('parseps://:master_key@%s' % ('a' * 32), { + 'instance': TypeError, + }), + # app_id + master_key (using arguments=) + ('parseps://localhost?app_id=%s&master_key=%s' % ('a' * 32, 'd' * 32), { + 'instance': plugins.NotifyParsePlatform, + + # Our expected url(privacy=True) startswith() response: + 'privacy_url': 'parseps://a...a:d...d@localhost', + }), + # Set a device id + custom port + ('parsep://app_id:master_key@localhost:8080?device=ios', { + 'instance': plugins.NotifyParsePlatform, + }), + # invalid device id + ('parsep://app_id:master_key@localhost?device=invalid', { + 'instance': TypeError, + }), + # Normal Query + ('parseps://app_id:master_key@localhost', { + 'instance': plugins.NotifyParsePlatform, + }), + ('parseps://app_id:master_key@localhost', { + 'instance': plugins.NotifyParsePlatform, + # force a failure + 'response': False, + 'requests_response_code': requests.codes.internal_server_error, + }), + ('parseps://app_id:master_key@localhost', { + 'instance': plugins.NotifyParsePlatform, + # throw a bizzare code forcing us to fail to look it up + 'response': False, + 'requests_response_code': 999, + }), + ('parseps://app_id:master_key@localhost', { + 'instance': plugins.NotifyParsePlatform, + # Throws a series of connection and transfer exceptions when this flag + # is set and tests that we gracfully handle them + 'test_requests_exceptions': True, + }), + ################################## # NotifyProwl ##################################