diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 0f1b2ef2..e681434a 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -7,7 +7,7 @@ * [ ] apprise/plugins/Notify.py * [ ] setup.py - - add new service into the `keywords` section of the `setup()` declaration + - add new service into the `KEYWORDS` file located in the root directory * [ ] README.md - add entry for new service to table (as a quick reference) * [ ] packaging/redhat/python-apprise.spec @@ -19,3 +19,27 @@ * [ ] There is no commented out code in this PR. * [ ] No lint errors (use `flake8`) * [ ] 100% test coverage + +## Testing + +Anyone can help test this source code as follows: +```bash +# Create a virtual environment to work in as follows: +python3 -m venv apprise + +# Change into our new directory +cd apprise + +# Activate our virtual environment +source bin/activate + +# Install the branch +pip install git+https://github.com/caronc/apprise.git@ + +# Be sure you're running your Signal API Server and query it like so +apprise -t "Test Title" -b "Test Message" \ + + +``` + diff --git a/KEYWORDS b/KEYWORDS new file mode 100644 index 00000000..ed91fefd --- /dev/null +++ b/KEYWORDS @@ -0,0 +1,81 @@ +Alerts +API +AWS +Boxcar +Chat +CLI +ClickSend +DAPNET +Dbus +Dingtalk +Discord +Email +Emby +Faast +FCM +Flock +Gitter +Gnome +Google +Gotify +Growl +Guilded +Home Assistant +IFTTT +Join +Kavenegar +KODI +Kumulos +LaMetric +MacOS +Mailgun +Matrix +Mattermost +MessageBird +Microsoft +MQTT +MSG91 +MSTeams +Nexmo +Nextcloud +NextcloudTalk +Notica +Notifico +Ntfy +Office365 +OneSignal +Opsgenie +ParsePlatform +PopcornNotify +Prowl +PushBullet +Pushed +Pushjet +Push Notifications +Pushover +PushSafer +Reddit +Rocket.Chat +Ryver +SendGrid +ServerChan +SES +Signal +SimplePush +Sinch +Slack +SMTP2Go +SNS +SparkPost +Spontit +Streamlabs +Stride +Syslog +Techulus +Telegram +Twilio +Twist +Twitter +Webex +Windows +XBMC diff --git a/MANIFEST.in b/MANIFEST.in index 1a56934e..5afb2889 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,5 @@ include LICENSE +include KEYWORDS include README.md include requirements.txt include win-requirements.txt diff --git a/README.md b/README.md index 3dcba0c9..11389da8 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,8 @@ The table below identifies the services this tool supports and some example serv | -------------------- | ---------- | ------------ | -------------- | | [Apprise API](https://github.com/caronc/apprise/wiki/Notify_apprise_api) | apprise:// or apprises:// | (TCP) 80 or 443 | apprise://hostname/Token | [AWS SES](https://github.com/caronc/apprise/wiki/Notify_ses) | ses:// | (TCP) 443 | ses://user@domain/AccessKeyID/AccessSecretKey/RegionName
ses://user@domain/AccessKeyID/AccessSecretKey/RegionName/email1/email2/emailN -| [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 +| [Bark](https://github.com/caronc/apprise/wiki/Notify_bark) | bark:// | (TCP) 80 or 443 | bark://hostname
bark://hostname/device_key
bark://hostname/device_key1/device_key2/device_keyN +| [Boxcar](https://github.com/caronc/apprise/wiki/Notify_bark) | 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 | [Emby](https://github.com/caronc/apprise/wiki/Notify_emby) | emby:// or embys:// | (TCP) 8096 | emby://user@hostname/
emby://user:password@hostname | [Enigma2](https://github.com/caronc/apprise/wiki/Notify_enigma2) | enigma2:// or enigma2s:// | (TCP) 80 or 443 | enigma2://hostname diff --git a/apprise/plugins/NotifyBark.py b/apprise/plugins/NotifyBark.py new file mode 100644 index 00000000..ce6dd7a0 --- /dev/null +++ b/apprise/plugins/NotifyBark.py @@ -0,0 +1,508 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2022 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. +# +# API: https://github.com/Finb/bark-server/blob/master/docs/API_V2.md#python +# +import six +import requests +import json + +from .NotifyBase import NotifyBase +from ..URLBase import PrivacyMode +from ..common import NotifyImageSize +from ..common import NotifyType +from ..utils import parse_list +from ..utils import parse_bool +from ..AppriseLocale import gettext_lazy as _ + + +# Sounds generated off of: https://github.com/Finb/Bark/tree/master/Sounds +BARK_SOUNDS = ( + "alarm.caf", + "anticipate.caf", + "bell.caf", + "birdsong.caf", + "bloom.caf", + "calypso.caf", + "chime.caf", + "choo.caf", + "descent.caf", + "electronic.caf", + "fanfare.caf", + "glass.caf", + "gotosleep.caf", + "healthnotification.caf", + "horn.caf", + "ladder.caf", + "mailsent.caf", + "minuet.caf", + "multiwayinvitation.caf", + "newmail.caf", + "newsflash.caf", + "noir.caf", + "paymentsuccess.caf", + "shake.caf", + "sherwoodforest.caf", + "silence.caf", + "spell.caf", + "suspense.caf", + "telegraph.caf", + "tiptoes.caf", + "typewriters.caf", + "update.caf", +) + + +# Supported Level Entries +class NotifyBarkLevel(object): + """ + Defines the Bark Level options + """ + ACTIVE = 'active' + + TIME_SENSITIVE = 'timeSensitive' + + PASSIVE = 'passive' + + +BARK_LEVELS = ( + NotifyBarkLevel.ACTIVE, + NotifyBarkLevel.TIME_SENSITIVE, + NotifyBarkLevel.PASSIVE, +) + + +class NotifyBark(NotifyBase): + """ + A wrapper for Notify Bark Notifications + """ + + # The default descriptive name associated with the Notification + service_name = 'Bark' + + # The services URL + service_url = 'https://github.com/Finb/Bark' + + # The default protocol + protocol = 'bark' + + # The default secure protocol + secure_protocol = 'barks' + + # A URL that takes you to the setup/help of the specific protocol + setup_url = 'https://github.com/caronc/apprise/wiki/Notify_bark' + + # Allows the user to specify the NotifyImageSize object; this is supported + # through the webhook + image_size = NotifyImageSize.XY_128 + + # Define object templates + templates = ( + '{schema}://{host}:{port}/{targets}', + '{schema}://{user}:{password}@{host}/{targets}', + '{schema}://{user}:{password}@{host}:{port}/{targets}', + '{schema}://{user}:{password}@{host}/{targets}', + ) + + # Define our template arguments + template_tokens = dict(NotifyBase.template_tokens, **{ + 'host': { + 'name': _('Hostname'), + 'type': 'string', + 'required': True, + }, + 'port': { + 'name': _('Port'), + 'type': 'int', + 'min': 1, + 'max': 65535, + }, + 'user': { + 'name': _('Username'), + 'type': 'string', + }, + 'password': { + 'name': _('Password'), + 'type': 'string', + 'private': True, + }, + 'target_device': { + 'name': _('Target Device'), + 'type': 'string', + 'map_to': 'targets', + }, + 'targets': { + 'name': _('Targets'), + 'type': 'list:string', + }, + }) + + # Define our template arguments + template_args = dict(NotifyBase.template_args, **{ + 'to': { + 'alias_of': 'targets', + }, + 'sound': { + 'name': _('Sound'), + 'type': 'choice:string', + 'values': BARK_SOUNDS, + }, + 'level': { + 'name': _('Level'), + 'type': 'choice:string', + 'values': BARK_LEVELS, + }, + 'click': { + 'name': _('Click'), + 'type': 'string', + }, + 'badge': { + 'name': _('Badge'), + 'type': 'int', + 'min': 0, + }, + 'category': { + 'name': _('Category'), + 'type': 'string', + }, + 'group': { + 'name': _('Group'), + 'type': 'string', + }, + 'image': { + 'name': _('Include Image'), + 'type': 'bool', + 'default': True, + 'map_to': 'include_image', + }, + }) + + def __init__(self, targets=None, include_image=True, sound=None, + category=None, group=None, level=None, click=None, + badge=None, **kwargs): + """ + Initialize Notify Bark Object + """ + super(NotifyBark, self).__init__(**kwargs) + + # Prepare our URL + self.notify_url = '%s://%s/push' % ( + 'https' if self.secure else 'http', + self.host, + ) + + if isinstance(self.port, int): + self.notify_url += ':%d' % self.port + + # Assign our category + self.category = \ + category if isinstance(category, six.string_types) else None + + # Assign our group + self.group = group if isinstance(group, six.string_types) else None + + # Initialize device list + self.targets = parse_list(targets) + + # Place an image inline with the message body + self.include_image = include_image + + # A clickthrough option for notifications + self.click = click + + # Badge + try: + # Acquire our badge count if we can: + # - We accept both the integer form as well as a string + # representation + self.badge = int(badge) + if self.badge < 0: + raise ValueError() + + except TypeError: + # NoneType means use Default; this is an okay exception + self.badge = None + + except ValueError: + self.badge = None + self.logger.warning( + 'The specified Bark badge ({}) is not valid ', badge) + + # Sound (easy-lookup) + self.sound = None if not sound else next( + (f for f in BARK_SOUNDS if f.startswith(sound.lower())), None) + if sound and not self.sound: + self.logger.warning( + 'The specified Bark sound ({}) was not found ', sound) + + # Level + self.level = None if not level else next( + (f for f in BARK_LEVELS if f[0] == level[0]), None) + if level and not self.level: + self.logger.warning( + 'The specified Bark level ({}) is not valid ', level) + + return + + def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): + """ + Perform Bark Notification + """ + + # error tracking (used for function return) + has_error = False + + if not len(self.targets): + # We have nothing to notify; we're done + self.logger.warning('There are no Bark devices to notify') + return False + + # Prepare our headers + headers = { + 'User-Agent': self.app_id, + 'Content-Type': 'application/json; charset=utf-8', + } + + # Prepare our payload (sample below) + # { + # "body": "Test Bark Server", + # "device_key": "nysrshcqielvoxsa", + # "title": "bleem", + # "category": "category", + # "sound": "minuet.caf", + # "badge": 1, + # "icon": "https://day.app/assets/images/avatar.jpg", + # "group": "test", + # "url": "https://mritd.com" + # } + payload = { + 'title': title if title else self.app_desc, + 'body': body, + } + + # Acquire our image url if configured to do so + image_url = None if not self.include_image else \ + self.image_url(notify_type) + + if image_url: + payload['icon'] = image_url + + if self.sound: + payload['sound'] = self.sound + + if self.click: + payload['url'] = self.click + + if self.badge: + payload['badge'] = self.badge + + if self.level: + payload['level'] = self.level + + if self.category: + payload['category'] = self.category + + if self.group: + payload['group'] = self.group + + auth = None + if self.user: + auth = (self.user, self.password) + + # Create a copy of the targets + targets = list(self.targets) + + while len(targets) > 0: + # Retrieve our device key + target = targets.pop() + + payload['device_key'] = target + self.logger.debug('Bark POST URL: %s (cert_verify=%r)' % ( + self.notify_url, self.verify_certificate, + )) + self.logger.debug('Bark 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=json.dumps(payload), + headers=headers, + auth=auth, + verify=self.verify_certificate, + timeout=self.request_timeout, + ) + if r.status_code != requests.codes.ok: + # We had a problem + status_str = \ + NotifyBark.http_response_code_lookup( + r.status_code) + + self.logger.warning( + 'Failed to send Bark 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 + + else: + self.logger.info( + 'Sent Bark notification to {}.'.format(target)) + + except requests.RequestException as e: + self.logger.warning( + 'A Connection error occurred sending Bark ' + 'notification to {}.'.format(target)) + 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. + """ + + # Define any URL parameters + params = { + 'image': 'yes' if self.include_image else 'no', + } + + if self.sound: + params['sound'] = self.sound + + if self.click: + params['click'] = self.click + + if self.badge: + params['badge'] = str(self.badge) + + if self.level: + params['level'] = self.level + + if self.category: + params['category'] = self.category + + if self.group: + params['group'] = self.group + + # Extend our parameters + params.update(self.url_parameters(privacy=privacy, *args, **kwargs)) + + # Determine Authentication + auth = '' + if self.user and self.password: + auth = '{user}:{password}@'.format( + user=NotifyBark.quote(self.user, safe=''), + password=self.pprint( + self.password, privacy, mode=PrivacyMode.Secret, safe=''), + ) + elif self.user: + auth = '{user}@'.format( + user=NotifyBark.quote(self.user, safe=''), + ) + + default_port = 443 if self.secure else 80 + + return '{schema}://{auth}{hostname}{port}/{targets}?{params}'.format( + schema=self.secure_protocol if self.secure else self.protocol, + auth=auth, + # never encode hostname since we're expecting it to be a valid one + hostname=self.host, + port='' if self.port is None or self.port == default_port + else ':{}'.format(self.port), + targets='/'.join( + [NotifyBark.quote('{}'.format(x)) for x in self.targets]), + params=NotifyBark.urlencode(params), + ) + + @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) + if not results: + # We're done early as we couldn't load the results + return results + + # Apply our targets + results['targets'] = NotifyBark.split_path(results['fullpath']) + + # Category + if 'category' in results['qsd'] and results['qsd']['category']: + results['category'] = NotifyBark.unquote( + results['qsd']['category'].strip()) + + # Group + if 'group' in results['qsd'] and results['qsd']['group']: + results['group'] = NotifyBark.unquote( + results['qsd']['group'].strip()) + + # Badge + if 'badge' in results['qsd'] and results['qsd']['badge']: + results['badge'] = NotifyBark.unquote( + results['qsd']['badge'].strip()) + + # Level + if 'level' in results['qsd'] and results['qsd']['level']: + results['level'] = NotifyBark.unquote( + results['qsd']['level'].strip()) + + # Click (URL) + if 'click' in results['qsd'] and results['qsd']['click']: + results['click'] = NotifyBark.unquote( + results['qsd']['click'].strip()) + + # Sound + if 'sound' in results['qsd'] and results['qsd']['sound']: + results['sound'] = NotifyBark.unquote( + results['qsd']['sound'].strip()) + + # The 'to' makes it easier to use yaml configuration + if 'to' in results['qsd'] and len(results['qsd']['to']): + results['targets'] += \ + NotifyBark.parse_list(results['qsd']['to']) + + # use image= for consistency with the other plugins + results['include_image'] = \ + parse_bool(results['qsd'].get('image', True)) + + return results diff --git a/apprise/plugins/NotifyNtfy.py b/apprise/plugins/NotifyNtfy.py index b19cf7a9..c4698bb6 100644 --- a/apprise/plugins/NotifyNtfy.py +++ b/apprise/plugins/NotifyNtfy.py @@ -274,7 +274,7 @@ class NotifyNtfy(NotifyBase): self.logger.warning('There are no ntfy topics to notify') return False - # Create a copy of the subreddits list + # Create a copy of the topics topics = list(self.topics) while len(topics) > 0: # Retrieve our topic diff --git a/packaging/redhat/python-apprise.spec b/packaging/redhat/python-apprise.spec index 513b8e14..de0a5ae4 100644 --- a/packaging/redhat/python-apprise.spec +++ b/packaging/redhat/python-apprise.spec @@ -47,16 +47,16 @@ Apprise is a Python package for simplifying access to all of the different notification services that are out there. Apprise opens the door and makes it easy to access: -Apprise API, AWS SES, AWS SNS, Boxcar, ClickSend, DAPNET, DingTalk, Discord, -E-Mail, Emby, Faast, FCM, Flock, Gitter, Google Chat, Gotify, Growl, Guilded, Home -Assistant, IFTTT, Join, Kavenegar, KODI, Kumulos, LaMetric, MacOSX, Mailgun, -Mattermost, Matrix, Microsoft Windows, Microsoft Teams, MessageBird, MQTT, MSG91, -MyAndroid, Nexmo, Nextcloud, NextcloudTalk, Notica, Notifico, ntfy, Office365, -OneSignal, Opsgenie, ParsePlatform, PopcornNotify, Prowl, Pushalot, -PushBullet, Pushjet, Pushover, PushSafer, Reddit, Rocket.Chat, SendGrid, -ServerChan, Signal, SimplePush, Sinch, Slack, SMTP2Go, Spontit, SparkPost, -Super Toasty, Streamlabs, Stride, Syslog, Techulus Push, Telegram, Twilio, -Twitter, Twist, XBMC, XMPP, Webex Teams} +Apprise API, AWS SES, AWS SNS, Bark, Boxcar, ClickSend, DAPNET, DingTalk, +Discord, E-Mail, Emby, Faast, FCM, Flock, Gitter, Google Chat, Gotify, Growl, +Guilded, Home Assistant, IFTTT, Join, Kavenegar, KODI, Kumulos, LaMetric, +MacOSX, Mailgun, Mattermost, Matrix, Microsoft Windows, Microsoft Teams, +MessageBird, MQTT, MSG91, MyAndroid, Nexmo, Nextcloud, NextcloudTalk, Notica, +Notifico, ntfy, Office365, OneSignal, Opsgenie, ParsePlatform, PopcornNotify, +Prowl, Pushalot, PushBullet, Pushjet, Pushover, PushSafer, Reddit, +Rocket.Chat, SendGrid, ServerChan, Signal, SimplePush, Sinch, Slack, SMTP2Go, +Spontit, SparkPost, Super Toasty, Streamlabs, Stride, Syslog, Techulus Push, +Telegram, Twilio, Twitter, Twist, XBMC, XMPP, Webex Teams} Name: python-%{pypi_name} Version: 0.9.8.3 diff --git a/setup.py b/setup.py index 67c7d7c1..0773d530 100755 --- a/setup.py +++ b/setup.py @@ -24,6 +24,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +import re import os import platform try: @@ -69,16 +70,7 @@ setup( long_description_content_type='text/markdown', cmdclass=cmdclass, url='https://github.com/caronc/apprise', - keywords='Push Notifications Alerts Email AWS SES SNS Boxcar ClickSend ' - 'DAPNET Dingtalk Discord Dbus Emby Faast FCM Flock Gitter Gnome ' - 'Google Chat Gotify Growl Guilded Home Assistant IFTTT Join Kavenegar ' - 'KODI Kumulos LaMetric MacOS Mailgun Matrix Mattermost MessageBird ' - 'MQTT MSG91 Nexmo Nextcloud NextcloudTalk Notica Notifico Ntfy ' - 'Office365 OneSignal Opsgenie ParsePlatform PopcornNotify Prowl ' - 'PushBullet Pushjet Pushed Pushover PushSafer Reddit Rocket.Chat ' - 'Ryver SendGrid ServerChan Signal SimplePush Sinch Slack SMTP2Go ' - 'SparkPost Spontit Streamlabs Stride Syslog Techulus Telegram Twilio ' - 'Twist Twitter XBMC MSTeams Microsoft Windows Webex CLI API', + keywords=' '.join(re.split(r'\s+', open('KEYWORDS').read())), author='Chris Caron', author_email='lead2gold@gmail.com', packages=find_packages(), diff --git a/test/test_plugin_bark.py b/test/test_plugin_bark.py new file mode 100644 index 00000000..01086b3a --- /dev/null +++ b/test/test_plugin_bark.py @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2022 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. +from apprise import plugins +from helpers import AppriseURLTester + +# Disable logging for a cleaner testing output +import logging +logging.disable(logging.CRITICAL) + +# Our Testing URLs +apprise_url_tests = ( + ('bark://', { + # No no host + 'instance': None, + }), + ('bark://:@/', { + # just invalid all around + 'instance': None, + }), + ('bark://localhost', { + # No Device Key specified + 'instance': plugins.NotifyBark, + # Expected notify() response False (because we won't be able + # to actually notify anything if no device_key was specified + 'notify_response': False, + + }), + ('bark://192.168.0.6:8081/device_key', { + # Everything is okay + 'instance': plugins.NotifyBark, + + # Our expected url(privacy=True) startswith() response: + 'privacy_url': 'bark://192.168.0.6:8081/', + }), + ('bark://user@192.168.0.6:8081/device_key', { + # Everything is okay (test with user) + 'instance': plugins.NotifyBark, + + # Our expected url(privacy=True) startswith() response: + 'privacy_url': 'bark://user@192.168.0.6:8081/', + }), + ('bark://192.168.0.6:8081/device_key/?sound=invalid', { + # bad sound, but we go ahead anyway + 'instance': plugins.NotifyBark, + }), + ('bark://192.168.0.6:8081/device_key/?sound=alarm', { + # alarm.caf sound loaded + 'instance': plugins.NotifyBark, + }), + ('bark://192.168.0.6:8081/device_key/?sound=NOiR.cAf', { + # noir.caf sound loaded + 'instance': plugins.NotifyBark, + }), + ('bark://192.168.0.6:8081/device_key/?badge=100', { + # set badge + 'instance': plugins.NotifyBark, + }), + ('barks://192.168.0.6:8081/device_key/?badge=invalid', { + # set invalid badge + 'instance': plugins.NotifyBark, + }), + ('barks://192.168.0.6:8081/device_key/?badge=-12', { + # set invalid badge + 'instance': plugins.NotifyBark, + }), + ('bark://192.168.0.6:8081/device_key/?category=apprise', { + # set category + 'instance': plugins.NotifyBark, + }), + ('bark://192.168.0.6:8081/device_key/?image=no', { + # do not display image + 'instance': plugins.NotifyBark, + }), + ('bark://192.168.0.6:8081/device_key/?group=apprise', { + # set group + 'instance': plugins.NotifyBark, + }), + ('bark://192.168.0.6:8081/device_key/?level=invalid', { + # bad level, but we go ahead anyway + 'instance': plugins.NotifyBark, + }), + ('bark://192.168.0.6:8081/?to=device_key', { + # test use of to= argument + 'instance': plugins.NotifyBark, + }), + ('bark://192.168.0.6:8081/device_key/?click=http://localhost', { + # Our click link + 'instance': plugins.NotifyBark, + }), + ('bark://192.168.0.6:8081/device_key/?level=active', { + # active level + 'instance': plugins.NotifyBark, + }), + ('bark://user:pass@192.168.0.5:8086/device_key/device_key2/', { + # Everything is okay + 'instance': plugins.NotifyBark, + + # Our expected url(privacy=True) startswith() response: + 'privacy_url': 'bark://user:****@192.168.0.5:8086/', + }), + ('barks://192.168.0.7/device_key/', { + 'instance': plugins.NotifyBark, + # throw a bizzare code forcing us to fail to look it up + 'response': False, + 'requests_response_code': 999, + + # Our expected url(privacy=True) startswith() response: + 'privacy_url': 'barks://192.168.0.7/device_key', + }), + ('bark://192.168.0.7/device_key', { + 'instance': plugins.NotifyBark, + # 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_bark_urls(): + """ + NotifyBark() Apprise URLs + + """ + + # Run our general tests + AppriseURLTester(tests=apprise_url_tests).run_all()