diff --git a/KEYWORDS b/KEYWORDS index 66da641b..20db7817 100644 --- a/KEYWORDS +++ b/KEYWORDS @@ -86,7 +86,6 @@ SMS Manager SMTP2Go SNS SparkPost -Spontit Streamlabs Stride Synology Chat diff --git a/README.md b/README.md index 2fd26e76..94ddfe9e 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,6 @@ The table below identifies the services this tool supports and some example serv | [SMTP2Go](https://github.com/caronc/apprise/wiki/Notify_smtp2go) | smtp2go:// | (TCP) 443 | smtp2go://user@hostname/apikey
smtp2go://user@hostname/apikey/email
smtp2go://user@hostname/apikey/email1/email2/emailN
smtp2go://user@hostname/apikey/?name="From%20User" | [Streamlabs](https://github.com/caronc/apprise/wiki/Notify_streamlabs) | strmlabs:// | (TCP) 443 | strmlabs://AccessToken/
strmlabs://AccessToken/?name=name&identifier=identifier&amount=0¤cy=USD | [SparkPost](https://github.com/caronc/apprise/wiki/Notify_sparkpost) | sparkpost:// | (TCP) 443 | sparkpost://user@hostname/apikey
sparkpost://user@hostname/apikey/email
sparkpost://user@hostname/apikey/email1/email2/emailN
sparkpost://user@hostname/apikey/?name="From%20User" -| [Spontit](https://github.com/caronc/apprise/wiki/Notify_spontit) | spontit:// | (TCP) 443 | spontit://UserID@APIKey/
spontit://UserID@APIKey/Channel
spontit://UserID@APIKey/Channel1/Channel2/ChannelN | [Synology Chat](https://github.com/caronc/apprise/wiki/Notify_synology_chat) | synology:// or synologys:// | (TCP) 80 or 443 | synology://hostname/token
synology://hostname:port/token | [Syslog](https://github.com/caronc/apprise/wiki/Notify_syslog) | syslog:// | n/a | syslog://
syslog://Facility | [Telegram](https://github.com/caronc/apprise/wiki/Notify_telegram) | tgram:// | (TCP) 443 | tgram://bottoken/ChatID
tgram://bottoken/ChatID1/ChatID2/ChatIDN diff --git a/apprise/plugins/NotifySpontit.py b/apprise/plugins/NotifySpontit.py deleted file mode 100644 index 4705fc05..00000000 --- a/apprise/plugins/NotifySpontit.py +++ /dev/null @@ -1,386 +0,0 @@ -# -*- coding: utf-8 -*- -# BSD 2-Clause License -# -# Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# -# 2. Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -# To use this service you will need a Spontit account from their website -# at https://spontit.com/ -# -# After you have an account created: -# - Visit your profile at https://spontit.com/profile and take note of your -# {username}. It might look something like: user12345678901 -# - Next generate an API key at https://spontit.com/secret_keys. This will -# generate a very long alpha-numeric string we'll refer to as the -# {apikey} - -# The Spontit Syntax is as follows: -# spontit://{username}@{apikey} - -import re -import requests -from json import loads - -from .NotifyBase import NotifyBase -from ..common import NotifyType -from ..utils import parse_list -from ..utils import validate_regex -from ..AppriseLocale import gettext_lazy as _ - -# Syntax suggests you use a hashtag '#' to help distinguish we're dealing -# with a channel. -# Secondly we extract the user information only if it's -# specified. If not, we use the user of the person sending the notification -# Finally the channel identifier is detected -CHANNEL_REGEX = re.compile( - r'^\s*(\#|\%23)?((\@|\%40)?(?P[a-z0-9_]+)([/\\]|\%2F))?' - r'(?P[a-z0-9_-]+)\s*$', re.I) - - -class NotifySpontit(NotifyBase): - """ - A wrapper for Spontit Notifications - """ - - # The default descriptive name associated with the Notification - service_name = 'Spontit' - - # The services URL - service_url = 'https://spontit.com/' - - # All notification requests are secure - secure_protocol = 'spontit' - - # Allow 300 requests per minute. - # 60/300 = 0.2 - request_rate_per_sec = 0.20 - - # A URL that takes you to the setup/help of the specific protocol - setup_url = 'https://github.com/caronc/apprise/wiki/Notify_spontit' - - # Spontit single notification URL - notify_url = 'https://api.spontit.com/v3/push' - - # The maximum length of the body - body_maxlen = 5000 - - # The maximum length of the title - title_maxlen = 100 - - # If we don't have the specified min length, then we don't bother using - # the body directive - spontit_body_minlen = 100 - - # Subtitle support; this is the maximum allowed characters defined by - # the API page - spontit_subtitle_maxlen = 20 - - # Define object templates - templates = ( - '{schema}://{user}@{apikey}', - '{schema}://{user}@{apikey}/{targets}', - ) - - # Define our template tokens - template_tokens = dict(NotifyBase.template_tokens, **{ - 'user': { - 'name': _('User ID'), - 'type': 'string', - 'required': True, - 'regex': (r'^[a-z0-9_-]+$', 'i'), - }, - 'apikey': { - 'name': _('API Key'), - 'type': 'string', - 'required': True, - 'private': True, - 'regex': (r'^[a-z0-9]+$', 'i'), - }, - # Target Channel ID's - # If a slash is used; you must escape it - # If no slash is used; channel is presumed to be your own - 'target_channel': { - 'name': _('Target Channel ID'), - 'type': 'string', - 'prefix': '#', - 'regex': (r'^[0-9\s)(+-]+$', 'i'), - 'map_to': 'targets', - }, - 'targets': { - 'name': _('Targets'), - 'type': 'list:string', - }, - }) - - # Define our template arguments - template_args = dict(NotifyBase.template_args, **{ - 'to': { - 'alias_of': 'targets', - }, - 'subtitle': { - # Subtitle is available for MacOS users - 'name': _('Subtitle'), - 'type': 'string', - }, - }) - - def __init__(self, apikey, targets=None, subtitle=None, **kwargs): - """ - Initialize Spontit Object - """ - super().__init__(**kwargs) - - # User ID (associated with project) - user = validate_regex( - self.user, *self.template_tokens['user']['regex']) - if not user: - msg = 'An invalid Spontit User ID ' \ - '({}) was specified.'.format(self.user) - self.logger.warning(msg) - raise TypeError(msg) - # use cleaned up version - self.user = user - - # API Key (associated with project) - self.apikey = validate_regex( - apikey, *self.template_tokens['apikey']['regex']) - if not self.apikey: - msg = 'An invalid Spontit API Key ' \ - '({}) was specified.'.format(apikey) - self.logger.warning(msg) - raise TypeError(msg) - - # Save our subtitle information - self.subtitle = subtitle - - # Parse our targets - self.targets = list() - - for target in parse_list(targets): - # Validate targets and drop bad ones: - result = CHANNEL_REGEX.match(target) - if result: - # Just extract the channel - self.targets.append( - '{}'.format(result.group('channel'))) - continue - - self.logger.warning( - 'Dropped invalid channel/user ({}) specified.'.format(target)) - - return - - def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): - """ - Sends Message - """ - - # error tracking (used for function return) - has_error = False - - # Prepare our headers - headers = { - 'User-Agent': self.app_id, - 'Content-Type': 'application/json', - 'X-Authorization': self.apikey, - 'X-UserId': self.user, - } - - # use the list directly - targets = list(self.targets) - - if not len(targets): - # The user did not specify a channel and therefore wants to notify - # the main account only. We just set a substitute marker of - # None so that our while loop below can still process one iteration - targets = [None, ] - - while len(targets): - # Get our target(s) to notify - target = targets.pop(0) - - # Prepare our payload - payload = { - 'message': body, - } - - # Use our body directive if we exceed the minimum message - # limitation - if len(body) > self.spontit_body_minlen: - payload['message'] = '{}...'.format( - body[:self.spontit_body_minlen - 3]) - payload['body'] = body - - if self.subtitle: - # Set title if specified - payload['subtitle'] = \ - self.subtitle[:self.spontit_subtitle_maxlen] - - elif self.app_desc: - # fall back to app description - payload['subtitle'] = \ - self.app_desc[:self.spontit_subtitle_maxlen] - - elif self.app_id: - # fall back to app id - payload['subtitle'] = \ - self.app_id[:self.spontit_subtitle_maxlen] - - if title: - # Set title if specified - payload['pushTitle'] = title - - if target is not None: - payload['channelName'] = target - - # Some Debug Logging - self.logger.debug( - 'Spontit POST URL: {} (cert_verify={})'.format( - self.notify_url, self.verify_certificate)) - self.logger.debug('Spontit Payload: {}' .format(payload)) - - # Always call throttle before any remote server i/o is made - self.throttle() - try: - r = requests.post( - self.notify_url, - params=payload, - headers=headers, - verify=self.verify_certificate, - timeout=self.request_timeout, - ) - - if r.status_code not in ( - requests.codes.created, requests.codes.ok): - status_str = \ - NotifyBase.http_response_code_lookup( - r.status_code) - - try: - # Update our status response if we can - json_response = loads(r.content) - status_str = json_response.get('message', status_str) - - except (AttributeError, TypeError, ValueError): - # ValueError = r.content is Unparsable - # TypeError = r.content is None - # AttributeError = r is None - - # We could not parse JSON response. - # We will just use the status we already have. - pass - - self.logger.warning( - 'Failed to send Spontit notification to {}: ' - '{}{}error={}.'.format( - target, - status_str, - ', ' if status_str else '', - r.status_code)) - - self.logger.debug( - 'Response Details:\r\n{}'.format(r.content)) - - # Mark our failure - has_error = True - continue - - # If we reach here; the message was sent - self.logger.info( - 'Sent Spontit notification to {}.'.format(target)) - - self.logger.debug( - 'Response Details:\r\n{}'.format(r.content)) - - except requests.RequestException as e: - self.logger.warning( - 'A Connection error occurred sending Spontit:%s ' % ( - ', '.join(self.targets)) + 'notification.' - ) - self.logger.debug('Socket Exception: %s' % str(e)) - # Mark our failure - has_error = True - continue - - return not has_error - - def url(self, privacy=False, *args, **kwargs): - """ - Returns the URL built dynamically based on specified arguments. - """ - - # Our URL parameters - params = self.url_parameters(privacy=privacy, *args, **kwargs) - - if self.subtitle: - params['subtitle'] = self.subtitle - - return '{schema}://{userid}@{apikey}/{targets}?{params}'.format( - schema=self.secure_protocol, - userid=self.user, - apikey=self.pprint(self.apikey, privacy, safe=''), - targets='/'.join( - [NotifySpontit.quote(x, safe='') for x in self.targets]), - params=NotifySpontit.urlencode(params)) - - def __len__(self): - """ - Returns the number of targets associated with this notification - """ - targets = len(self.targets) - return targets if targets > 0 else 1 - - @staticmethod - def parse_url(url): - """ - Parses the URL and returns enough arguments that can allow - us to re-instantiate this object. - - """ - - results = NotifyBase.parse_url(url, verify_host=False) - if not results: - # We're done early as we couldn't load the results - return results - - # Get our entries; split_path() looks after unquoting content for us - # by default - results['targets'] = NotifySpontit.split_path(results['fullpath']) - - # The hostname is our authentication key - results['apikey'] = NotifySpontit.unquote(results['host']) - - # Support MacOS subtitle option - if 'subtitle' in results['qsd'] and len(results['qsd']['subtitle']): - results['subtitle'] = \ - NotifySpontit.unquote(results['qsd']['subtitle']) - - # Support the 'to' variable so that we can support targets this way too - # The 'to' makes it easier to use yaml configuration - if 'to' in results['qsd'] and len(results['qsd']['to']): - results['targets'] += \ - NotifySpontit.parse_list(results['qsd']['to']) - - return results diff --git a/packaging/redhat/python-apprise.spec b/packaging/redhat/python-apprise.spec index c73d26ac..404042c2 100644 --- a/packaging/redhat/python-apprise.spec +++ b/packaging/redhat/python-apprise.spec @@ -49,7 +49,7 @@ Notifico, ntfy, Office365, OneSignal, Opsgenie, PagerDuty, PagerTree, ParsePlatform, PopcornNotify, Prowl, Pushalot, PushBullet, Pushjet, PushMe, Pushover, PushSafer, Pushy, PushDeer, Reddit, Rocket.Chat, RSyslog, SendGrid, ServerChan, Signal, SimplePush, Sinch, Slack, SMSEagle, SMS Manager, SMTP2Go, -Spontit, SparkPost, Super Toasty, Streamlabs, Stride, Synology Chat, Syslog, +SparkPost, Super Toasty, Streamlabs, Stride, Synology Chat, Syslog, Techulus Push, Telegram, Threema Gateway, Twilio, Twitter, Twist, XBMC, Voipms, Vonage, WeCom Bot, WhatsApp, Webex Teams} diff --git a/test/test_plugin_spontit.py b/test/test_plugin_spontit.py deleted file mode 100644 index 8d1b7f49..00000000 --- a/test/test_plugin_spontit.py +++ /dev/null @@ -1,123 +0,0 @@ -# -*- coding: utf-8 -*- -# BSD 2-Clause License -# -# Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# -# 2. Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -import requests - -from apprise.plugins.NotifySpontit import NotifySpontit -from helpers import AppriseURLTester - -# Disable logging for a cleaner testing output -import logging -logging.disable(logging.CRITICAL) - -# Our Testing URLs -apprise_url_tests = ( - ('spontit://', { - # invalid url - 'instance': TypeError, - }), - # Another bad url - ('spontit://:@/', { - 'instance': TypeError, - }), - # No user specified - ('spontit://%s' % ('a' * 100), { - 'instance': TypeError, - }), - # Invalid API Key specified - ('spontit://user@%%20_', { - 'instance': TypeError, - }), - # Provide a valid user and API Key - ('spontit://%s@%s' % ('u' * 11, 'b' * 100), { - 'instance': NotifySpontit, - # Our expected url(privacy=True) startswith() response: - 'privacy_url': 'spontit://{}@b...b/'.format('u' * 11), - }), - # Provide a valid user and API Key, but provide an invalid channel - ('spontit://%s@%s/#!!' % ('u' * 11, 'b' * 100), { - # An instance is still created, but the channel won't be notified - 'instance': NotifySpontit, - }), - # Provide a valid user, API Key and a valid channel - ('spontit://%s@%s/#abcd' % ('u' * 11, 'b' * 100), { - 'instance': NotifySpontit, - }), - # Provide a valid user, API Key, and a subtitle - ('spontit://%s@%s/?subtitle=Test' % ('u' * 11, 'b' * 100), { - 'instance': NotifySpontit, - }), - # Provide a valid user, API Key, and a lengthy subtitle - ('spontit://%s@%s/?subtitle=%s' % ('u' * 11, 'b' * 100, 'c' * 300), { - 'instance': NotifySpontit, - }), - # Provide a valid user and API Key, but provide a valid channel (that is - # not ours). - # Spontit uses a slash (/) to delimite the user from the channel id when - # specifying channel entries. For Apprise we need to encode this - # so we convert the slash (/) into %2F - ('spontit://{}@{}/#1245%2Fabcd'.format('u' * 11, 'b' * 100), { - 'instance': NotifySpontit, - }), - # Provide multipe channels - ('spontit://{}@{}/#1245%2Fabcd/defg'.format('u' * 11, 'b' * 100), { - 'instance': NotifySpontit, - }), - # Provide multipe channels through the use of the to= variable - ('spontit://{}@{}/?to=#1245/abcd'.format('u' * 11, 'b' * 100), { - 'instance': NotifySpontit, - }), - ('spontit://%s@%s' % ('u' * 11, 'b' * 100), { - 'instance': NotifySpontit, - # force a failure - 'response': False, - 'requests_response_code': requests.codes.internal_server_error, - }), - ('spontit://%s@%s' % ('u' * 11, 'b' * 100), { - 'instance': NotifySpontit, - # throw a bizzare code forcing us to fail to look it up - 'response': False, - 'requests_response_code': 999, - }), - ('spontit://%s@%s' % ('u' * 11, 'b' * 100), { - 'instance': NotifySpontit, - # Throws a series of connection and transfer exceptions when this flag - # is set and tests that we gracfully handle them - 'test_requests_exceptions': True, - }), -) - - -def test_plugin_spontit_urls(): - """ - NotifySpontit() Apprise URLs - - """ - - # Run our general tests - AppriseURLTester(tests=apprise_url_tests).run_all()