Notifiarr Support (#953)

This commit is contained in:
Chris Caron 2023-10-07 17:40:41 -04:00 committed by GitHub
parent be73b03a98
commit ae0c412b41
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 659 additions and 13 deletions

View File

@ -47,6 +47,7 @@ MSTeams
Nextcloud
NextcloudTalk
Notica
Notifiarr
Notifico
Ntfy
Office365

View File

@ -87,6 +87,7 @@ The table below identifies the services this tool supports and some example serv
| [Nextcloud](https://github.com/caronc/apprise/wiki/Notify_nextcloud) | ncloud:// or nclouds:// | (TCP) 80 or 443 | ncloud://adminuser:pass@host/User<br/>nclouds://adminuser:pass@host/User1/User2/UserN
| [NextcloudTalk](https://github.com/caronc/apprise/wiki/Notify_nextcloudtalk) | nctalk:// or nctalks:// | (TCP) 80 or 443 | nctalk://user:pass@host/RoomId<br/>nctalks://user:pass@host/RoomId1/RoomId2/RoomIdN
| [Notica](https://github.com/caronc/apprise/wiki/Notify_notica) | notica:// | (TCP) 443 | notica://Token/
| [Notifiarr](https://github.com/caronc/apprise/wiki/Notify_notifiarr) | notifiarr:// | (TCP) 443 | notifiarr://apikey/#channel<br />notifiarr://apikey/#channel1/#channel2/#channeln
| [Notifico](https://github.com/caronc/apprise/wiki/Notify_notifico) | notifico:// | (TCP) 443 | notifico://ProjectID/MessageHook/
| [ntfy](https://github.com/caronc/apprise/wiki/Notify_ntfy) | ntfy:// | (TCP) 80 or 443 | ntfy://topic/<br/>ntfys://topic/
| [Office 365](https://github.com/caronc/apprise/wiki/Notify_office365) | o365:// | (TCP) 443 | o365://TenantID:AccountEmail/ClientID/ClientSecret<br />o365://TenantID:AccountEmail/ClientID/ClientSecret/TargetEmail<br />o365://TenantID:AccountEmail/ClientID/ClientSecret/TargetEmail1/TargetEmail2/TargetEmailN

View File

@ -636,7 +636,7 @@ class ConfigBase(URLBase):
valid_line_re = re.compile(
r'^\s*(?P<line>([;#]+(?P<comment>.*))|'
r'(\s*(?P<tags>[a-z0-9, \t_-]+)\s*=|=)?\s*'
r'((?P<url>[a-z0-9]{2,9}://.*)|(?P<assign>[a-z0-9, \t_-]+))|'
r'((?P<url>[a-z0-9]{1,12}://.*)|(?P<assign>[a-z0-9, \t_-]+))|'
r'include\s+(?P<config>.+))?\s*$', re.I)
try:

View File

@ -0,0 +1,472 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
#
# 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.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# 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 re
import requests
from json import dumps
from itertools import chain
from .NotifyBase import NotifyBase
from ..common import NotifyType
from ..AppriseLocale import gettext_lazy as _
from ..common import NotifyImageSize
from ..utils import parse_list, parse_bool
from ..utils import validate_regex
# Used to break path apart into list of channels
CHANNEL_LIST_DELIM = re.compile(r'[ \t\r\n,#\\/]+')
CHANNEL_REGEX = re.compile(
r'^\s*(\#|\%35)?(?P<channel>[0-9]+)', re.I)
# For API Details see:
# https://notifiarr.wiki/Client/Installation
# Another good example:
# https://notifiarr.wiki/en/Website/ \
# Integrations/Passthrough#payload-example-1
class NotifyNotifiarr(NotifyBase):
"""
A wrapper for Notifiarr Notifications
"""
# The default descriptive name associated with the Notification
service_name = 'Notifiarr'
# The services URL
service_url = 'https://notifiarr.com/'
# The default secure protocol
secure_protocol = 'notifiarr'
# A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_notifiarr'
# The Notification URL
notify_url = 'https://notifiarr.com/api/v1/notification/apprise'
# Notifiarr Throttling (knowing in advance reduces 429 responses)
# define('NOTIFICATION_LIMIT_SECOND_USER', 5);
# define('NOTIFICATION_LIMIT_SECOND_PATRON', 15);
# Throttle requests ever so slightly
request_rate_per_sec = 0.04
# Allows the user to specify the NotifyImageSize object
image_size = NotifyImageSize.XY_256
# Define object templates
templates = (
'{schema}://{apikey}/{targets}',
)
# Define our apikeys; these are the minimum apikeys required required to
# be passed into this function (as arguments). The syntax appends any
# previously defined in the base package and builds onto them
template_tokens = dict(NotifyBase.template_tokens, **{
'apikey': {
'name': _('Token'),
'type': 'string',
'required': True,
'private': True,
},
'target_channel': {
'name': _('Target Channel'),
'type': 'string',
'prefix': '#',
'map_to': 'targets',
},
'targets': {
'name': _('Targets'),
'type': 'list:string',
'required': True,
},
})
# Define our template arguments
template_args = dict(NotifyBase.template_args, **{
'key': {
'alias_of': 'apikey',
},
'apikey': {
'alias_of': 'apikey',
},
'discord_user': {
'name': _('Ping Discord User'),
'type': 'int',
},
'discord_role': {
'name': _('Ping Discord Role'),
'type': 'int',
},
'event': {
'name': _('Discord Event ID'),
'type': 'int',
},
'image': {
'name': _('Include Image'),
'type': 'bool',
'default': False,
'map_to': 'include_image',
},
'source': {
'name': _('Source'),
'type': 'string',
},
'from': {
'alias_of': 'source'
},
'to': {
'alias_of': 'targets',
},
})
def __init__(self, apikey=None, include_image=None,
discord_user=None, discord_role=None, event=None,
targets=None, source=None, **kwargs):
"""
Initialize Notifiarr Object
headers can be a dictionary of key/value pairs that you want to
additionally include as part of the server headers to post with
"""
super().__init__(**kwargs)
self.apikey = apikey
if not self.apikey:
msg = 'An invalid Notifiarr APIKey ' \
'({}) was specified.'.format(apikey)
self.logger.warning(msg)
raise TypeError(msg)
# Place a thumbnail image inline with the message body
self.include_image = include_image \
if isinstance(include_image, bool) \
else self.template_args['image']['default']
# Set up our user if specified
self.discord_user = 0
if discord_user:
try:
self.discord_user = int(discord_user)
except (ValueError, TypeError):
msg = 'An invalid Notifiarr User ID ' \
'({}) was specified.'.format(discord_user)
self.logger.warning(msg)
raise TypeError(msg)
# Set up our role if specified
self.discord_role = 0
if discord_role:
try:
self.discord_role = int(discord_role)
except (ValueError, TypeError):
msg = 'An invalid Notifiarr Role ID ' \
'({}) was specified.'.format(discord_role)
self.logger.warning(msg)
raise TypeError(msg)
# Prepare our source (if set)
self.source = validate_regex(source)
self.event = 0
if event:
try:
self.event = int(event)
except (ValueError, TypeError):
msg = 'An invalid Notifiarr Discord Event ID ' \
'({}) was specified.'.format(event)
self.logger.warning(msg)
raise TypeError(msg)
# Prepare our targets
self.targets = {
'channels': [],
'invalid': [],
}
for target in parse_list(targets):
result = CHANNEL_REGEX.match(target)
if result:
# Store role information
self.targets['channels'].append(int(result.group('channel')))
continue
self.logger.warning(
'Dropped invalid channel '
'({}) specified.'.format(target),
)
self.targets['invalid'].append(target)
return
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.source:
params['source'] = self.source
if self.discord_user:
params['discord_user'] = self.discord_user
if self.discord_role:
params['discord_role'] = self.discord_role
if self.event:
params['event'] = self.event
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
return '{schema}://{apikey}' \
'/{targets}?{params}'.format(
schema=self.secure_protocol,
apikey=self.pprint(self.apikey, privacy, safe=''),
targets='/'.join(
[NotifyNotifiarr.quote(x, safe='+#@') for x in chain(
# Channels
['#{}'.format(x) for x in self.targets['channels']],
# Pass along the same invalid entries as were provided
self.targets['invalid'],
)]),
params=NotifyNotifiarr.urlencode(params),
)
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
"""
Perform Notifiarr Notification
"""
if not self.targets['channels']:
# There were no services to notify
self.logger.warning(
'There were no Notifiarr channels to notify.')
return False
# No error to start with
has_error = False
# Acquire image_url
image_url = self.image_url(notify_type)
for idx, channel in enumerate(self.targets['channels']):
# prepare Notifiarr Object
payload = {
'source': self.source if self.source else self.app_id,
'type': notify_type,
'notification': {
'update': True if self.event else False,
'name': self.app_id,
'event': str(self.event)
if self.event else "",
},
'discord': {
'color': self.color(notify_type),
'ping': {
'pingUser': self.discord_user
if not idx and self.discord_user else 0,
'pingRole': self.discord_role
if not idx and self.discord_role else 0,
},
'text': {
'title': title,
'content': '',
'description': body,
},
'ids': {
'channel': channel,
}
}
}
if self.include_image and image_url:
payload['discord']['text']['icon'] = image_url
if not self._send(payload):
has_error = True
return not has_error
def _send(self, payload):
"""
Send notification
"""
self.logger.debug('Notifiarr POST URL: %s (cert_verify=%r)' % (
self.notify_url, self.verify_certificate,
))
self.logger.debug('Notifiarr Payload: %s' % str(payload))
# Prepare HTTP Headers
headers = {
'User-Agent': self.app_id,
'Content-Type': 'application/json',
'Accept': 'text/plain',
'X-api-Key': self.apikey,
}
# 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,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code < 200 or r.status_code >= 300:
# We had a problem
status_str = \
NotifyNotifiarr.http_response_code_lookup(r.status_code)
self.logger.warning(
'Failed to send Notifiarr %s notification: '
'%serror=%s.',
status_str,
', ' if status_str else '',
str(r.status_code))
self.logger.debug('Response Details:\r\n{}'.format(r.content))
# Return; we're done
return False
else:
self.logger.info('Sent Notifiarr notification.')
except requests.RequestException as e:
self.logger.warning(
'A Connection error occurred sending Notifiarr '
'Chat notification to %s.' % self.host)
self.logger.debug('Socket Exception: %s' % str(e))
# Return; we're done
return False
return True
def __len__(self):
"""
Returns the number of targets associated with this notification
"""
targets = len(self.targets['channels']) + len(self.targets['invalid'])
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 channels
results['targets'] = NotifyNotifiarr.split_path(results['fullpath'])
if 'discord_user' in results['qsd'] and \
len(results['qsd']['discord_user']):
results['discord_user'] = \
NotifyNotifiarr.unquote(
results['qsd']['discord_user'])
if 'discord_role' in results['qsd'] and \
len(results['qsd']['discord_role']):
results['discord_role'] = \
NotifyNotifiarr.unquote(results['qsd']['discord_role'])
if 'event' in results['qsd'] and \
len(results['qsd']['event']):
results['event'] = \
NotifyNotifiarr.unquote(results['qsd']['event'])
# Include images with our message
results['include_image'] = \
parse_bool(results['qsd'].get('image', False))
# Track if we need to extract the hostname as a target
host_is_potential_target = False
if 'source' in results['qsd'] and len(results['qsd']['source']):
results['source'] = \
NotifyNotifiarr.unquote(results['qsd']['source'])
elif 'from' in results['qsd'] and len(results['qsd']['from']):
results['source'] = \
NotifyNotifiarr.unquote(results['qsd']['from'])
# Set our apikey if found as an argument
if 'apikey' in results['qsd'] and len(results['qsd']['apikey']):
results['apikey'] = \
NotifyNotifiarr.unquote(results['qsd']['apikey'])
host_is_potential_target = True
elif 'key' in results['qsd'] and len(results['qsd']['key']):
results['apikey'] = \
NotifyNotifiarr.unquote(results['qsd']['key'])
host_is_potential_target = True
else:
# Pop the first element (this is the api key)
results['apikey'] = \
NotifyNotifiarr.unquote(results['host'])
if host_is_potential_target is True and results['host']:
results['targets'].append(NotifyNotifiarr.unquote(results['host']))
# Support the 'to' variable so that we can support rooms this way too
# The 'to' makes it easier to use yaml configuration
if 'to' in results['qsd'] and len(results['qsd']['to']):
results['targets'] += [x for x in filter(
bool, CHANNEL_LIST_DELIM.split(
NotifyNotifiarr.unquote(results['qsd']['to'])))]
return results

View File

@ -143,14 +143,14 @@ NOTIFY_CUSTOM_DEL_TOKENS = re.compile(r'^-(?P<key>.*)\s*')
NOTIFY_CUSTOM_COLON_TOKENS = re.compile(r'^:(?P<key>.*)\s*')
# Used for attempting to acquire the schema if the URL can't be parsed.
GET_SCHEMA_RE = re.compile(r'\s*(?P<schema>[a-z0-9]{2,9})://.*$', re.I)
GET_SCHEMA_RE = re.compile(r'\s*(?P<schema>[a-z0-9]{1,12})://.*$', re.I)
# Used for validating that a provided entry is indeed a schema
# this is slightly different then the GET_SCHEMA_RE above which
# insists the schema is only valid with a :// entry. this one
# extrapolates the individual entries
URL_DETAILS_RE = re.compile(
r'\s*(?P<schema>[a-z0-9]{2,9})(://(?P<base>.*))?$', re.I)
r'\s*(?P<schema>[a-z0-9]{1,12})(://(?P<base>.*))?$', re.I)
# Regular expression based and expanded from:
# http://www.regular-expressions.info/email.html
@ -194,7 +194,7 @@ CALL_SIGN_DETECTION_RE = re.compile(
# Regular expression used to destinguish between multiple URLs
URL_DETECTION_RE = re.compile(
r'([a-z0-9]+?:\/\/.*?)(?=$|[\s,]+[a-z0-9]{2,9}?:\/\/)', re.I)
r'([a-z0-9]+?:\/\/.*?)(?=$|[\s,]+[a-z0-9]{1,12}?:\/\/)', re.I)
EMAIL_DETECTION_RE = re.compile(
r'[\s,]*([^@]+@.*?)(?=$|[\s,]+'

View File

@ -48,13 +48,13 @@ DAPNET, DingTalk, Discord, E-Mail, Emby, Faast, FCM, Flock, Google Chat,
Gotify, Growl, Guilded, Home Assistant, IFTTT, Join, Kavenegar, KODI, Kumulos,
LaMetric, Line, MacOSX, Mailgun, Mastodon, Mattermost, Matrix, MessageBird,
Microsoft Windows, Microsoft Teams, Misskey, MQTT, MSG91, MyAndroid, Nexmo,
Nextcloud, NextcloudTalk, Notica, 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, SMTP2Go, Spontit, SparkPost, Super Toasty, Streamlabs, Stride,
Syslog, Techulus Push, Telegram, Twilio, Twitter, Twist, XBMC, Voipms, Vonage,
WhatsApp, Webex Teams}
Nextcloud, NextcloudTalk, Notica, Notifiarr, 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, SMTP2Go, Spontit, SparkPost, Super Toasty,
Streamlabs, Stride, Syslog, Techulus Push, Telegram, Twilio, Twitter, Twist,
XBMC, Voipms, Vonage, WhatsApp, Webex Teams}
Name: python-%{pypi_name}
Version: 1.5.0

View File

@ -391,9 +391,13 @@ class AppriseURLTester:
targets = len(obj)
# check that we're as expected
assert obj.notify(
_resp = obj.notify(
body=self.body, title=self.title,
notify_type=notify_type) == notify_response
notify_type=notify_type)
if _resp != notify_response:
print('%s notify() returned %s (but expected %s)' % (
url, _resp, notify_response))
assert False
if notify_response:
# If we successfully got a response, there must have been

View File

@ -0,0 +1,168 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
#
# 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.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# 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.NotifyNotifiarr import NotifyNotifiarr
from helpers import AppriseURLTester
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
# Our Testing URLs
apprise_url_tests = (
('notifiarr://:@/', {
'instance': TypeError,
}),
('notifiarr://', {
'instance': TypeError,
}),
('notifiarr://apikey', {
'instance': NotifyNotifiarr,
# Response will fail due to no targets defined
'notify_response': False,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'notifiarr://a...y',
}),
('notifiarr://apikey/1234/?discord_user=invalid', {
'instance': TypeError,
}),
('notifiarr://apikey/1234/?discord_role=invalid', {
'instance': TypeError,
}),
('notifiarr://apikey/1234/?event=invalid', {
'instance': TypeError,
}),
('notifiarr://apikey/%%invalid%%', {
'instance': NotifyNotifiarr,
# Response will fail due to no targets defined
'notify_response': False,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'notifiarr://a...y',
}),
('notifiarr://apikey/#123', {
'instance': NotifyNotifiarr,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'notifiarr://a...y/#123'
}),
('notifiarr://apikey/123?image=No', {
'instance': NotifyNotifiarr,
}),
('notifiarr://apikey/123?image=yes', {
'instance': NotifyNotifiarr,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'notifiarr://a...y/#123',
}),
('notifiarr://apikey/?to=123,432', {
'instance': NotifyNotifiarr,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'notifiarr://a...y/#123/#432',
}),
('notifiarr://123/?apikey=myapikey', {
'instance': NotifyNotifiarr,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'notifiarr://m...y/#123',
}),
('notifiarr://123/?apikey=myapikey&source=My%20System', {
'instance': NotifyNotifiarr,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'notifiarr://m...y/#123',
}),
('notifiarr://123/?apikey=myapikey&from=My%20System', {
'instance': NotifyNotifiarr,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'notifiarr://m...y/#123',
}),
('notifiarr://?apikey=myapikey', {
# No Channel or host
'instance': NotifyNotifiarr,
# Response will fail due to no targets defined
'notify_response': False,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'notifiarr://m...y/',
}),
('notifiarr://invalid?apikey=myapikey', {
# No Channel or host
'instance': NotifyNotifiarr,
# invalid channel
'notify_response': False,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'notifiarr://m...y/',
}),
('notifiarr://123/325/?apikey=myapikey', {
'instance': NotifyNotifiarr,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'notifiarr://m...y/#123/#325',
}),
('notifiarr://12/?key=myapikey&discord_user=23'
'&discord_role=12&event=123', {
'instance': NotifyNotifiarr,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'notifiarr://m...y/#12'}),
('notifiarr://apikey/123/', {
'instance': NotifyNotifiarr,
}),
('notifiarr://apikey/123', {
'instance': NotifyNotifiarr,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('notifiarr://apikey/123', {
'instance': NotifyNotifiarr,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('notifiarr://apikey/123', {
'instance': NotifyNotifiarr,
# 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_custom_notifiarr_urls():
"""
NotifyNotifiarr() Apprise URLs
"""
# Run our general tests
AppriseURLTester(tests=apprise_url_tests).run_all()