From e763f3b71eb209fc83c216a6086769944bd62836 Mon Sep 17 00:00:00 2001 From: Chris Caron Date: Mon, 1 Sep 2025 22:15:01 -0400 Subject: [PATCH] applied some feedback --- apprise/plugins/notificationapi.py | 431 +++++++++++++++++++-------- apprise/utils/parse.py | 22 +- tests/test_plugin_notificationapi.py | 82 ++--- 3 files changed, 359 insertions(+), 176 deletions(-) diff --git a/apprise/plugins/notificationapi.py b/apprise/plugins/notificationapi.py index 0cd0f1b0..aab08fca 100644 --- a/apprise/plugins/notificationapi.py +++ b/apprise/plugins/notificationapi.py @@ -32,13 +32,13 @@ from __future__ import annotations import base64 from email.utils import formataddr -from itertools import chain from json import dumps, loads import re import requests -from ..common import NotifyImageSize, NotifyType +from ..common import NotifyFormat, NotifyImageSize, NotifyType +from ..conversion import convert_between from ..locale import gettext_lazy as _ from ..utils.parse import ( is_email, @@ -49,12 +49,9 @@ from ..utils.parse import ( ) from .base import NotifyBase -# Valid targets: -# id:sms -# id:email -# id -IS_VALID_TARGET_RE = re.compile( - r"^\s*(((?P[\w_-]+)\s*:)?(?P.+)|(?P[\w_-]+))", re.I) +# Used to detect ID +IS_VALID_ID_RE = re.compile( + r"^\s*(@|%40)?(?P[\w_-]+)\s*", re.I) class NotificationAPIRegion: @@ -83,16 +80,36 @@ NOTIFICATIONAPI_REGIONS = ( class NotificationAPIChannel: """Channels""" - AUTO = "AUTO" - EMAIL = "EMAIL" - SMS = "SMS" + EMAIL = "email" + SMS = "sms" + INAPP = "inapp" + WEB_PUSH = "web_push" + MOBILE_PUSH = "mobile_push" + SLACK = "mobile_push" # A List of our channels we can use for verification NOTIFICATIONAPI_CHANNELS: frozenset[str] = frozenset([ - NotificationAPIChannel.AUTO, NotificationAPIChannel.EMAIL, NotificationAPIChannel.SMS, + NotificationAPIChannel.INAPP, + NotificationAPIChannel.WEB_PUSH, + NotificationAPIChannel.MOBILE_PUSH, + NotificationAPIChannel.SLACK, +]) + + +class NotificationAPIMode: + """Modes""" + + TEMPLATE = "template" + MESSAGE = "message" + + +# A List of our channels we can use for verification +NOTIFICATIONAPI_MODES: frozenset[str] = frozenset([ + NotificationAPIMode.TEMPLATE, + NotificationAPIMode.MESSAGE, ]) @@ -176,11 +193,10 @@ class NotifyNotificationAPI(NotifyBase): "type": { "alias_of": "type", }, - "channel": { - "name": _("Channel"), - "type": "choice:string", + "channels": { + "name": _("Channels"), + "type": "list:string", "values": NOTIFICATIONAPI_CHANNELS, - "default": NotificationAPIChannel.AUTO, }, "region": { "name": _("Region Name"), @@ -188,6 +204,11 @@ class NotifyNotificationAPI(NotifyBase): "values": NOTIFICATIONAPI_REGIONS, "default": NotificationAPIRegion.US, }, + "mode": { + "name": _("Mode"), + "type": "choice:string", + "values": NOTIFICATIONAPI_MODES, + }, "base_url": { "name": _("Base URL Override"), "type": "string", @@ -204,6 +225,11 @@ class NotifyNotificationAPI(NotifyBase): "name": _("Blind Carbon Copy"), "type": "list:string", }, + "reply": { + "name": _("Reply To"), + "type": "string", + "map_to": "reply_to", + }, "from": { "name": _("From Email"), "type": "string", @@ -226,8 +252,9 @@ class NotifyNotificationAPI(NotifyBase): } def __init__(self, client_id, client_secret, message_type=None, - targets=None, cc=None, bcc=None, channel=None, region=None, - from_addr=None, tokens=None, **kwargs): + targets=None, cc=None, bcc=None, reply_to=None, + channels=None, region=None, mode=None, from_addr=None, + tokens=None, **kwargs): """ Initialize Notify NotificationAPI Object """ @@ -252,7 +279,7 @@ class NotifyNotificationAPI(NotifyBase): # For tracking our email -> name lookups self.names = {} - # Temporary from_addr to work with for parsing + # Prepare our From Address _from_addr = [self.app_id, ""] self.from_addr = None if isinstance(from_addr, str): @@ -269,11 +296,32 @@ class NotifyNotificationAPI(NotifyBase): self.from_addr = _from_addr[1] self.names[_from_addr[1]] = _from_addr[0] - # Our Targets; must be formatted as - # id:email or id:phone_no - self.email_targets = [] - self.sms_targets = [] - self.id_targets = [] + # Prepare our Reply-To Address + self.reply_to = {} + if isinstance(reply_to, str): + result = is_email(reply_to) + if result and "full_email" in result: + self.reply_to = { + "senderName": result["name"] + if result["name"] else _from_addr[0], + "senderEmail": result["full_email"], + } + + # Our Targets are delimited by found ids + self.targets = [] + if mode and isinstance(mode, str): + self.mode = next( + (a for a in NOTIFICATIONAPI_MODES if a.startswith(mode)), None + ) + if self.mode not in NOTIFICATIONAPI_MODES: + msg = \ + f"The NotificationAPI mode specified ({mode}) is invalid." + self.logger.warning(msg) + raise TypeError(msg) + else: + # Detect mode based on whether or not a message_type was provided + self.mode = NotificationAPIMode.MESSAGE if not message_type else \ + NotificationAPIMode.TEMPLATE if not message_type: # Assign a default message type @@ -321,69 +369,111 @@ class NotifyNotificationAPI(NotifyBase): self.logger.warning(msg) raise TypeError(msg) from None - if targets: - # Validate recipients (to:) and drop bad ones: - for entry in parse_list(targets): - result = IS_VALID_TARGET_RE.match(entry) - if not result: - msg = ( - "The NotificationAPI target specified " - f"({entry}) is invalid.") - self.logger.warning(msg) - continue - - if result.group("id2"): - self.id_targets.append({ - "id": result.group("id2"), - }) - continue - - # Store our content - uid, recipient = result.group("id"), result.group("target") - - result = is_email(recipient) - if result: - self.email_targets.append({ - "id": uid, - "email": result["full_email"], - }) - continue - - result = is_phone_no(recipient) - if result: - self.sms_targets.append({ - "id": uid, - "number": result["full"], - }) - continue - - msg = ( - "The NotificationAPI target specified " - f"({entry}) is invalid.") - self.logger.warning(msg) - - if channel is None: - self.channel = NotificationAPIChannel.AUTO - - else: - # Store our channel - try: - self.channel = ( - self.template_args["channel"]["default"] - if channel is None else channel.upper() - ) - - if self.channel not in NOTIFICATIONAPI_CHANNELS: - # allow the outer except to handle this common response - raise IndexError() - - except (AttributeError, IndexError, TypeError): + # Initialize an empty set of channels + self.channels = set() + for _channel in parse_list(channels): + channel = _channel.lower() + if channel not in NOTIFICATIONAPI_CHANNELS: # Invalid channel specified msg = ( - "The NotificationAPI channel specified " + "The NotificationAPI forced channel specified " f"({channel}) is invalid.") self.logger.warning(msg) raise TypeError(msg) from None + self.channels.add(channel) + + if targets: + current_target = {} + for entry in parse_list(targets, sort=False): + result = is_email(entry) + if result: + if "email" not in current_target: + current_target["email"] = result["full_email"] + if not self.channels: + self.channels.add(NotificationAPIChannel.EMAIL) + self.logger.info( + "The NotificationAPI default channel of " + f"{NotificationAPIChannel.EMAIL} was set.") + continue + + elif "id" in current_target: + # Store and move on + self.targets.append(current_target) + current_target = { + "email": result["full_email"] + } + continue + + # if we got here, we have to many emails making it now + # ambiguous as to who the sender intended to notify + msg = ( + "The NotificationAPI received too many emails " + "creating an ambiguous situation; aborted at " + f"'{entry}'.") + self.logger.warning(msg) + raise TypeError(msg) from None + + result = is_phone_no(entry) + if result: + if "number" not in current_target: + current_target["number"] = \ + ('+' if entry[0] == '+' else '') + result["full"] + if not self.channels: + self.channels.add(NotificationAPIChannel.SMS) + self.logger.info( + "The NotificationAPI default channel of " + f"{NotificationAPIChannel.SMS} was set.") + continue + + elif "id" in current_target: + # Store and move on + self.targets.append(current_target) + current_target = { + "number": result["full"] + } + continue + + # if we got here, we have to many emails making it now + # ambiguous as to who the sender intended to notify + msg = ( + "The NotificationAPI received too many phone no's " + "creating an ambiguous situation; aborted at " + f"'{entry}'.") + self.logger.warning(msg) + raise TypeError(msg) from None + + result = IS_VALID_ID_RE.match(entry) + if result: + if "id" not in current_target: + current_target["id"] = result.group("id") + continue + + elif "id" in current_target: + # Store and move on + self.targets.append(current_target) + current_target = { + "id": result.group("id") + } + continue + + self.logger.warning( + "Ignoring invalid NotificationAPI target " + f"({entry}) specified") + continue + + if "id" in current_target: + # Store our final entry + self.targets.append(current_target) + current_target = {} + + if current_target: + # we have email or sms, but no id to go with it + msg = ( + "The NotificationAPI did not detect an id to " + "correlate the following with {}".format( + str(current_target))) + self.logger.warning(msg) + raise TypeError(msg) from None # Validate recipients (cc:) and drop bad ones: for recipient in parse_emails(cc): @@ -443,8 +533,13 @@ class NotifyNotificationAPI(NotifyBase): Returns the URL built dynamically based on specified arguments. """ - # Our URL parameters - params = self.url_parameters(privacy=privacy, *args, **kwargs) + # Define any URL parameters + params = { + "mode": self.mode, + } + + # Extend our parameters + params.update(self.url_parameters(privacy=privacy, *args, **kwargs)) if len(self.cc) > 0: # Handle our Carbon Copy Addresses @@ -466,9 +561,18 @@ class NotifyNotificationAPI(NotifyBase): charset="utf-8").replace(",", "%2C") for e in self.bcc]) - if self.channel != self.template_args["channel"]["default"]: + if self.reply_to: + # Handle our Reply-To Address + params["reply"] = formataddr( + (self.reply_to["senderName"], self.reply_to["senderEmail"]), + # Swap comma for its escaped url code (if detected) since + # we're using that as a delimiter + charset="utf-8", + ) + + if self.channels: # Prepare our default channel - params["channel"] = self.channel.lower() + params["channels"] = self.channels if self.region != self.template_args["region"]["default"]: # Prepare our default region @@ -483,27 +587,23 @@ class NotifyNotificationAPI(NotifyBase): # Store any template entries if specified params.update({f":{k}": v for k, v in self.tokens.items()}) - return "{schema}://{mtype}@{cid}/{secret}/{targets}?{params}".format( + targets = [] + for target in self.targets: + if "id" in target: + targets.append(f"@{target['id']}") + if "number" in target: + targets.append(f"{target['number']}") + if "email" in target: + targets.append(f"{target['email']}") + + mtype = f"{self.message_type}@" \ + if self.message_type != self.default_message_type else "" + return "{schema}://{mtype}{cid}/{secret}/{targets}?{params}".format( schema=self.secure_protocol[0], - mtype=self.message_type, + mtype=mtype, cid=self.pprint(self.client_id, privacy, safe=""), secret=self.pprint(self.client_secret, privacy, safe=""), - targets="" if not ( - self.sms_targets or self.email_targets) else "/".join( - chain( - [ - NotifyNotificationAPI.quote( - "{}:{}".format(x["id"], x["email"]), safe="") - for x in self.email_targets], - [ - NotifyNotificationAPI.quote( - "{}:{}".format( - x["id"], x["number"]), safe="") - for x in self.sms_targets], - [ - NotifyNotificationAPI.quote(x["id"], safe="") - for x in self.id_targets], - )), + targets=NotifyNotificationAPI.quote("/".join(targets), safe="/"), params=NotifyNotificationAPI.urlencode(params), ) @@ -512,10 +612,7 @@ class NotifyNotificationAPI(NotifyBase): Returns the number of targets associated with this notification """ - return max( - 1, - len(self.email_targets) + len(self.sms_targets) - + len(self.id_targets)) + return max(1, len(self.targets)) def gen_payload(self, body, title="", notify_type=NotifyType.INFO, **kwargs): @@ -523,32 +620,99 @@ class NotifyNotificationAPI(NotifyBase): generates our NotificationAPI payload """ - # Take a copy of our token dictionary - parameters = self.tokens.copy() - - # Apply some defaults template values - parameters["appBody"] = body - parameters["appTitle"] = title - parameters["appType"] = notify_type.value - parameters["appId"] = self.app_id - parameters["appDescription"] = self.app_desc - parameters["appColor"] = self.color(notify_type) - parameters["appImageUrl"] = self.image_url(notify_type) - parameters["appUrl"] = self.app_url - - # A Simple Email Payload Template _payload = { "type": self.message_type, - "parameters": {**parameters}, } + if self.mode == NotificationAPIMode.TEMPLATE: + # Take a copy of our token dictionary + parameters = self.tokens.copy() - if self.channel != NotificationAPIChannel.AUTO: - _payload["forceChannels"] = [self.channel] + # Apply some defaults template values + parameters["appBody"] = body + parameters["appTitle"] = title + parameters["appType"] = notify_type.value + parameters["appId"] = self.app_id + parameters["appDescription"] = self.app_desc + parameters["appColor"] = self.color(notify_type) + parameters["appImageUrl"] = self.image_url(notify_type) + parameters["appUrl"] = self.app_url + + # A Simple Email Payload Template + _payload.update({ + "parameters": {**parameters}, + }) + + else: + # Acquire text version of body if provided + text_body = convert_between( + NotifyFormat.HTML, NotifyFormat.TEXT, body) \ + if self.notify_format == NotifyFormat.HTML else body + + for channel in self.channels: + # Python v3.10 supports `match/case` but since Apprise aims to be + # compatible with Python v3.9+, we must use if/else for the time + # being + if channel == NotificationAPIChannel.SMS: + _payload.update({ + NotificationAPIChannel.SMS: { + "message": (title + "\n" + text_body) + if title else text_body, + }, + }) + + elif channel == NotificationAPIChannel.EMAIL: + html_body = convert_between( + NotifyFormat.TEXT, NotifyFormat.HTML, body) \ + if self.notify_format != NotifyFormat.HTML else body + + _payload.update({ + NotificationAPIChannel.EMAIL: { + "subject": title if title else self.app_id, + "html": html_body, + }, + }) + + if self.from_addr: + _payload[NotificationAPIChannel.EMAIL].update({ + "senderName": self.from_addr, + "senderEmail": self.names[self.from_addr], + }) + + elif channel == NotificationAPIChannel.INAPP: + _payload.update({ + NotificationAPIChannel.INAPP: { + "title": title if title else self.app_id, + "image": self.image_url(notify_type), + }, + }) + + elif channel == NotificationAPIChannel.WEB_PUSH: + _payload.update({ + NotificationAPIChannel.WEB_PUSH: { + "title": title if title else self.app_id, + "message": text_body, + "icon": self.image_url(notify_type), + }, + }) + + elif channel == NotificationAPIChannel.MOBILE_PUSH: + _payload.update({ + NotificationAPIChannel.MOBILE_PUSH: { + "title": title if title else self.app_id, + "message": text_body, + }, + }) + + elif channel == NotificationAPIChannel.SLACK: + _payload.update({ + NotificationAPIChannel.SLACK: { + "text": (title + "\n" + text_body) + if title else text_body, + }, + }) # Copy our list to work with - targets = list(self.email_targets) + \ - list(self.sms_targets) + list(self.id_targets) - + targets = list(self.targets) if self.from_addr: _payload.update({ "options": { @@ -600,7 +764,7 @@ class NotifyNotificationAPI(NotifyBase): # error tracking (used for function return) has_error = False - if not (self.email_targets or self.sms_targets or self.id_targets): + if not self.targets: # There is no one to email or send an sms message to; we're done self.logger.warning( "There are no NotificationAPI recipients to notify" @@ -689,7 +853,6 @@ class NotifyNotificationAPI(NotifyBase): "Sent NotifiationAPI notification to %s.", payload["to"]["id"]) - except requests.RequestException as e: self.logger.warning( "A Connection error occurred sending NotifiationAPI " @@ -764,6 +927,14 @@ class NotifyNotificationAPI(NotifyBase): results["message_type"] = \ NotifyNotificationAPI.unquote(results["qsd"]["type"]) + if "mode" in results["qsd"] and len(results["qsd"]["mode"]): + results["mode"] = \ + NotifyNotificationAPI.unquote(results["qsd"]["mode"]) + + if "reply" in results["qsd"] and len(results["qsd"]["reply"]): + results["reply_to"] = \ + NotifyNotificationAPI.unquote(results["qsd"]["reply"]) + elif results["user"]: # Pull from user results["message_type"] = \ diff --git a/apprise/utils/parse.py b/apprise/utils/parse.py index c751be1f..1400c3c9 100644 --- a/apprise/utils/parse.py +++ b/apprise/utils/parse.py @@ -1046,7 +1046,7 @@ def parse_urls(*args, store_unparseable=True, **kwargs): return result -def parse_list(*args, cast=None, allow_whitespace=True): +def parse_list(*args, cast=None, allow_whitespace=True, sort=True): """Take a string list and break it into a delimited list of arguments. This funciton also supports the processing of a list of delmited strings and will always return a unique set of arguments. Duplicates are always @@ -1081,7 +1081,8 @@ def parse_list(*args, cast=None, allow_whitespace=True): ) elif isinstance(arg, (set, list, tuple)): - result += parse_list(*arg, allow_whitespace=allow_whitespace) + result += parse_list( + *arg, allow_whitespace=allow_whitespace, sort=sort) # # filter() eliminates any empty entries @@ -1089,12 +1090,19 @@ def parse_list(*args, cast=None, allow_whitespace=True): # Since Python v3 returns a filter (iterator) whereas Python v2 returned # a list, we need to change it into a list object to remain compatible with # both distribution types. - return ( - sorted(filter(bool, list(set(result)))) - if allow_whitespace - else sorted( - [x.strip() for x in filter(bool, list(set(result))) if x.strip()] + if sort: + return ( + sorted(filter(bool, list(set(result)))) + if allow_whitespace + else sorted( + [x.strip() for x in filter( + bool, list(set(result))) if x.strip()] + ) ) + return ( + [x for x in filter(bool, list(result))] + if allow_whitespace + else [x.strip() for x in filter(bool, list(result)) if x.strip()] ) diff --git a/tests/test_plugin_notificationapi.py b/tests/test_plugin_notificationapi.py index 191f816c..0be4a342 100644 --- a/tests/test_plugin_notificationapi.py +++ b/tests/test_plugin_notificationapi.py @@ -63,32 +63,36 @@ apprise_url_tests = ( # Just an Email specified, no client_id or client_secret "instance": TypeError, }), - ("napi://type@client_id/client_secret/id:+15551235555/", { + ("napi://user@client_id/cs14/user@example.ca", { + # No id matched + "instance": TypeError, + }), + ("napi://type@client_id/client_secret/id/+15551235553/", { "instance": NotifyNotificationAPI, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, }), - ("napi://type@cid/secret/id:user@example.com/", { + ("napi://type@cid/secret/id/user1@example.com/", { "instance": NotifyNotificationAPI, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, }), - (("napi://type@cid/secret/id:user@example.com/" - "id:+15551235555/id:+15551235534"), { + (("napi://type@cid/secret/id10/user2@example.com/" + "id5/+15551235555/id8/+15551235534"), { "instance": NotifyNotificationAPI, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, }), - (("napi://type@cid/secret/id:user@example.com/" + (("napi://type@cid/secret/id/user3@example.com/" "?from=joe@example.ca"), { # Set from/source "instance": NotifyNotificationAPI, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, }), - (("napi://type@cid/secret/id:user@example.com/" + (("napi://type@cid/secret/id/user4@example.com/" "?from=joe@example.ca&bcc=user1@yahoo.ca&cc=user2@yahoo.ca"), { # Set from/source "instance": NotifyNotificationAPI, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, }), - ("napi://?id=ci&secret=cs&to=id:user@example.com&type=type", { + ("napi://?id=ci&secret=cs&to=id,user5@example.com&type=type", { # use just kwargs "instance": NotifyNotificationAPI, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, @@ -99,90 +103,90 @@ apprise_url_tests = ( "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, "notify_response": False, }), - ("napi://user@client_id/cs2/id:user@example.ca" + ("napi://user@client_id/cs2/id/user6@example.ca" "?bcc=invalid", { # A good email with a bad Blind Carbon Copy "instance": NotifyNotificationAPI, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, }), - ("napi://user@client_id/cs3/id:user@example.ca" + ("napi://user@client_id/cs3/id/user8@example.ca" "?cc=l2g@nuxref.com", { # A good email with Carbon Copy "instance": NotifyNotificationAPI, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, }), - ("napi://user@client_id/cs4/id:user@example.ca" + ("napi://user@client_id/cs4/id/user9@example.ca" "?cc=Chris", { # A good email with Carbon Copy "instance": NotifyNotificationAPI, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, }), - ("napi://user@client_id/cs5/id:user@example.ca" + ("napi://user@client_id/cs5/id/user10@example.ca" "?cc=invalid", { # A good email with Carbon Copy "instance": NotifyNotificationAPI, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, }), - ("napi://user@client_id/cs6/id:user@example.ca" + ("napi://user@client_id/cs6/id/user11@example.ca" "?to=invalid", { # an invalid to email "instance": NotifyNotificationAPI, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, }), - ("napi://user@client_id/cs7/id:chris@example.com", { + ("napi://user@client_id/cs7/id/chris1@example.com", { # An email with a designated to email "instance": NotifyNotificationAPI, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, }), - ("napi://user@client_id/cs8/id:user@example.ca" - "?to=id:Chris", { + ("napi://user@client_id/cs8/id1/user12@example.ca" + "?to=id,Chris", { # An email with a full name in in To field "instance": NotifyNotificationAPI, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, }), - ("napi://user@client_id/cs9/id:user@example.ca" - "id:chris@example.com/id:chris2@example.com/id:+15552341234", { + ("napi://user@client_id/cs9/id2/user13@example.ca/" + "id/kris@example.com/id/chris2@example.com/id/+15552341234", { # Several emails to notify "instance": NotifyNotificationAPI, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, }), - ("napi://user@client_id/cs10/id:user@example.ca" - "?cc=Chris", { + ("napi://user@client_id/cs10/id/user14@example.ca" + "?cc=Chris", { # An email with a full name in cc "instance": NotifyNotificationAPI, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, }), - ("napi://user@client_id/cs11/id:user@example.ca" - "?cc=chris@example.com", { + ("napi://user@client_id/cs11/id/user15@example.ca" + "?cc=chris12@example.com", { # An email with a full name in cc "instance": NotifyNotificationAPI, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, }), - ("napi://user@client_id/cs12/id:user@example.ca" - "?bcc=Chris", { + ("napi://user@client_id/cs12/id/user16@example.ca" + "?bcc=Chris", { # An email with a full name in bcc "instance": NotifyNotificationAPI, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, }), - ("napi://user@client_id/cs13/id:user@example.ca" - "?bcc=chris@example.com", { + ("napi://user@client_id/cs13/id/user@example.ca" + "?bcc=chris13@example.com", { # An email with a full name in bcc "instance": NotifyNotificationAPI, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, }), - ("napi://user@client_id/cs14/id:user@example.ca" - "?to=Chris", { + ("napi://user@client_id/cs14/id/user@example.ca" + "?to=Chris,id14", { # An email with a full name in bcc "instance": NotifyNotificationAPI, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, }), - ("napi://user@client_id/cs15/id:user@example.ca" - "?to=chris@example.com", { + ("napi://user@client_id/cs15/id" + "?to=user@example.com", { # An email with a full name in bcc "instance": NotifyNotificationAPI, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, }), - ("napi://user@client_id/cs16/id:user@example.ca" + ("napi://user@client_id/cs16/id/user@example.ca" "?template=1234&+sub=value&+sub2=value2", { # A good email with a template + substitutions "instance": NotifyNotificationAPI, @@ -191,21 +195,21 @@ apprise_url_tests = ( # Our expected url(privacy=True) startswith() response: "privacy_url": "napi://user@c...d/c...6/", }), - ("napi://user@client_id/cs17/id:user@example.ca", { + ("napi://user@client_id/cs17/id/user@example.ca", { "instance": NotifyNotificationAPI, # force a failure "response": False, "requests_response_code": requests.codes.internal_server_error, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, }), - ("napi://user@client_id/cs18/id:user@example.ca", { + ("napi://user@client_id/cs18/id/user@example.ca", { "instance": NotifyNotificationAPI, # throw a bizzare code forcing us to fail to look it up "response": False, "requests_response_code": 999, "requests_response_text": NOTIFICATIONAPI_GOOD_RESPONSE, }), - ("napi://user@client_id/cs19/id:user@example.ca", { + ("napi://user@client_id/cs19/id/user@example.ca", { "instance": NotifyNotificationAPI, # Throws a series of connection and transfer exceptions when this flag # is set and tests that we gracfully handle them @@ -240,11 +244,11 @@ def test_plugin_napi_sms_payloads(mock_post): client_id = "my_id" client_secret = "my_secret" message_type = "apprise-post" - phone_no = "userid:+1-555-123-4567" + targets = "userid/+1-555-123-4567" obj = Apprise.instantiate( f"napi://{message_type}@{client_id}/{client_secret}/" - f"{phone_no}") + f"{targets}?mode=template") assert isinstance(obj, NotifyNotificationAPI) assert isinstance(obj.url(), str) @@ -270,7 +274,7 @@ def test_plugin_napi_sms_payloads(mock_post): "type": "apprise-post", "to": { "id": "userid", - "number": "15551234567", + "number": "+15551234567", }, "parameters": { "appBody": "body", @@ -309,12 +313,12 @@ def test_plugin_napi_email_payloads(mock_post): client_id = "my_id_abc" client_secret = "my_secret" message_type = "apprise-post" - email = "userid:test@example.ca" + targets = "userid/test@example.ca" obj = Apprise.instantiate( f"napi://{message_type}@{client_id}/{client_secret}/" - f"{email}?from=Chris&bcc=joe@hidden.com&" - f"cc=jason@hidden.com&:customToken=customValue") + f"{targets}?from=Chris&bcc=joe@hidden.com&" + f"cc=jason@hidden.com&:customToken=customValue&mode=template") assert isinstance(obj, NotifyNotificationAPI) assert isinstance(obj.url(), str)