mirror of https://github.com/caronc/apprise
				
				
				
			Notifiarr Support (#953)
							parent
							
								
									be73b03a98
								
							
						
					
					
						commit
						ae0c412b41
					
				
							
								
								
									
										1
									
								
								KEYWORDS
								
								
								
								
							
							
						
						
									
										1
									
								
								KEYWORDS
								
								
								
								
							| 
						 | 
				
			
			@ -47,6 +47,7 @@ MSTeams
 | 
			
		|||
Nextcloud
 | 
			
		||||
NextcloudTalk
 | 
			
		||||
Notica
 | 
			
		||||
Notifiarr
 | 
			
		||||
Notifico
 | 
			
		||||
Ntfy
 | 
			
		||||
Office365
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			@ -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,]+'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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()
 | 
			
		||||
		Loading…
	
		Reference in New Issue