summaryrefslogtreecommitdiffhomepage
path: root/libs/apprise/plugins/NotifyBase.py
diff options
context:
space:
mode:
Diffstat (limited to 'libs/apprise/plugins/NotifyBase.py')
-rw-r--r--libs/apprise/plugins/NotifyBase.py789
1 files changed, 789 insertions, 0 deletions
diff --git a/libs/apprise/plugins/NotifyBase.py b/libs/apprise/plugins/NotifyBase.py
new file mode 100644
index 000000000..c29417c60
--- /dev/null
+++ b/libs/apprise/plugins/NotifyBase.py
@@ -0,0 +1,789 @@
+# -*- coding: utf-8 -*-
+# BSD 2-Clause License
+#
+# Apprise - Push Notification Library.
+# Copyright (c) 2024, Chris Caron <[email protected]>
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+import asyncio
+import re
+from functools import partial
+
+from ..URLBase import URLBase
+from ..common import NotifyType
+from ..utils import parse_bool
+from ..common import NOTIFY_TYPES
+from ..common import NotifyFormat
+from ..common import NOTIFY_FORMATS
+from ..common import OverflowMode
+from ..common import OVERFLOW_MODES
+from ..AppriseLocale import gettext_lazy as _
+from ..AppriseAttachment import AppriseAttachment
+
+
+class NotifyBase(URLBase):
+ """
+ This is the base class for all notification services
+ """
+
+ # An internal flag used to test the state of the plugin. If set to
+ # False, then the plugin is not used. Plugins can disable themselves
+ # due to enviroment issues (such as missing libraries, or platform
+ # dependencies that are not present). By default all plugins are
+ # enabled.
+ enabled = True
+
+ # The category allows for parent inheritance of this object to alter
+ # this when it's function/use is intended to behave differently. The
+ # following category types exist:
+ #
+ # native: Is a native plugin written/stored in `apprise/plugins/Notify*`
+ # custom: Is a custom plugin written/stored in a users plugin directory
+ # that they loaded at execution time.
+ category = 'native'
+
+ # Some plugins may require additional packages above what is provided
+ # already by Apprise.
+ #
+ # Use this section to relay this information to the users of the script to
+ # help guide them with what they need to know if they plan on using your
+ # plugin. The below configuration should otherwise accomodate all normal
+ # situations and will not requrie any updating:
+ requirements = {
+ # Use the description to provide a human interpretable description of
+ # what is required to make the plugin work. This is only nessisary
+ # if there are package dependencies. Setting this to default will
+ # cause a general response to be returned. Only set this if you plan
+ # on over-riding the default. Always consider language support here.
+ # So before providing a value do the following in your code base:
+ #
+ # from apprise.AppriseLocale import gettext_lazy as _
+ #
+ # 'details': _('My detailed requirements')
+ 'details': None,
+
+ # Define any required packages needed for the plugin to run. This is
+ # an array of strings that simply look like lines residing in a
+ # `requirements.txt` file...
+ #
+ # As an example, an entry may look like:
+ # 'packages_required': [
+ # 'cryptography < 3.4`,
+ # ]
+ 'packages_required': [],
+
+ # Recommended packages identify packages that are not required to make
+ # your plugin work, but would improve it's use or grant it access to
+ # full functionality (that might otherwise be limited).
+
+ # Similar to `packages_required`, you would identify each entry in
+ # the array as you would in a `requirements.txt` file.
+ #
+ # - Do not re-provide entries already in the `packages_required`
+ 'packages_recommended': [],
+ }
+
+ # The services URL
+ service_url = None
+
+ # A URL that takes you to the setup/help of the specific protocol
+ setup_url = None
+
+ # Most Servers do not like more then 1 request per 5 seconds, so 5.5 gives
+ # us a safe play range. Override the one defined already in the URLBase
+ request_rate_per_sec = 5.5
+
+ # Allows the user to specify the NotifyImageSize object
+ image_size = None
+
+ # The maximum allowable characters allowed in the body per message
+ body_maxlen = 32768
+
+ # Defines the maximum allowable characters in the title; set this to zero
+ # if a title can't be used. Titles that are not used but are defined are
+ # automatically placed into the body
+ title_maxlen = 250
+
+ # Set the maximum line count; if this is set to anything larger then zero
+ # the message (prior to it being sent) will be truncated to this number
+ # of lines. Setting this to zero disables this feature.
+ body_max_line_count = 0
+
+ # Default Notify Format
+ notify_format = NotifyFormat.TEXT
+
+ # Default Overflow Mode
+ overflow_mode = OverflowMode.UPSTREAM
+
+ # Default Emoji Interpretation
+ interpret_emojis = False
+
+ # Support Attachments; this defaults to being disabled.
+ # Since apprise allows you to send attachments without a body or title
+ # defined, by letting Apprise know the plugin won't support attachments
+ # up front, it can quickly pass over and ignore calls to these end points.
+
+ # You must set this to true if your application can handle attachments.
+ # You must also consider a flow change to your notification if this is set
+ # to True as well as now there will be cases where both the body and title
+ # may not be set. There will never be a case where a body, or attachment
+ # isn't set in the same call to your notify() function.
+ attachment_support = False
+
+ # Default Title HTML Tagging
+ # When a title is specified for a notification service that doesn't accept
+ # titles, by default apprise tries to give a plesant view and convert the
+ # title so that it can be placed into the body. The default is to just
+ # use a <b> tag. The below causes the <b>title</b> to get generated:
+ default_html_tag_id = 'b'
+
+ # Here is where we define all of the arguments we accept on the url
+ # such as: schema://whatever/?overflow=upstream&format=text
+ # These act the same way as tokens except they are optional and/or
+ # have default values set if mandatory. This rule must be followed
+ template_args = dict(URLBase.template_args, **{
+ 'overflow': {
+ 'name': _('Overflow Mode'),
+ 'type': 'choice:string',
+ 'values': OVERFLOW_MODES,
+ # Provide a default
+ 'default': overflow_mode,
+ # look up default using the following parent class value at
+ # runtime. The variable name identified here (in this case
+ # overflow_mode) is checked and it's result is placed over-top of
+ # the 'default'. This is done because once a parent class inherits
+ # this one, the overflow_mode already set as a default 'could' be
+ # potentially over-ridden and changed to a different value.
+ '_lookup_default': 'overflow_mode',
+ },
+ 'format': {
+ 'name': _('Notify Format'),
+ 'type': 'choice:string',
+ 'values': NOTIFY_FORMATS,
+ # Provide a default
+ 'default': notify_format,
+ # look up default using the following parent class value at
+ # runtime.
+ '_lookup_default': 'notify_format',
+ },
+ 'emojis': {
+ 'name': _('Interpret Emojis'),
+ # SSL Certificate Authority Verification
+ 'type': 'bool',
+ # Provide a default
+ 'default': interpret_emojis,
+ # look up default using the following parent class value at
+ # runtime.
+ '_lookup_default': 'interpret_emojis',
+ },
+ })
+
+ #
+ # Overflow Defaults / Configuration applicable to SPLIT mode only
+ #
+
+ # Display Count [X/X]
+ # ^^^^^^
+ # \\\\\\
+ # 6 characters (space + count)
+ # Display Count [XX/XX]
+ # ^^^^^^^^
+ # \\\\\\\\
+ # 8 characters (space + count)
+ # Display Count [XXX/XXX]
+ # ^^^^^^^^^^
+ # \\\\\\\\\\
+ # 10 characters (space + count)
+ # Display Count [XXXX/XXXX]
+ # ^^^^^^^^^^^^
+ # \\\\\\\\\\\\
+ # 12 characters (space + count)
+ #
+ # Given the above + some buffer we come up with the following:
+ # If this value is exceeded, display counts automatically shut off
+ overflow_max_display_count_width = 12
+
+ # The number of characters to reserver for whitespace buffering
+ # This is detected automatically, but you can enforce a value if
+ # you desire:
+ overflow_buffer = 0
+
+ # the min accepted length of a title to allow for a counter display
+ overflow_display_count_threshold = 130
+
+ # Whether or not when over-flow occurs, if the title should be repeated
+ # each time the message is split up
+ # - None: Detect
+ # - True: Always display title once
+ # - False: Display the title for each occurance
+ overflow_display_title_once = None
+
+ # If this is set to to True:
+ # The title_maxlen should be considered as a subset of the body_maxlen
+ # Hence: len(title) + len(body) should never be greater then body_maxlen
+ #
+ # If set to False, then there is no corrorlation between title_maxlen
+ # restrictions and that of body_maxlen
+ overflow_amalgamate_title = False
+
+ def __init__(self, **kwargs):
+ """
+ Initialize some general configuration that will keep things consistent
+ when working with the notifiers that will inherit this class.
+
+ """
+
+ super().__init__(**kwargs)
+
+ # Store our interpret_emoji's setting
+ # If asset emoji value is set to a default of True and the user
+ # specifies it to be false, this is accepted and False over-rides.
+ #
+ # If asset emoji value is set to a default of None, a user may
+ # optionally over-ride this and set it to True from the Apprise
+ # URL. ?emojis=yes
+ #
+ # If asset emoji value is set to a default of False, then all emoji's
+ # are turned off (no user over-rides allowed)
+ #
+
+ # Take a default
+ self.interpret_emojis = self.asset.interpret_emojis
+ if 'emojis' in kwargs:
+ # possibly over-ride default
+ self.interpret_emojis = True if self.interpret_emojis \
+ in (None, True) and \
+ parse_bool(
+ kwargs.get('emojis', False),
+ default=NotifyBase.template_args['emojis']['default']) \
+ else False
+
+ if 'format' in kwargs:
+ # Store the specified format if specified
+ notify_format = kwargs.get('format', '')
+ if notify_format.lower() not in NOTIFY_FORMATS:
+ msg = 'Invalid notification format {}'.format(notify_format)
+ self.logger.error(msg)
+ raise TypeError(msg)
+
+ # Provide override
+ self.notify_format = notify_format
+
+ if 'overflow' in kwargs:
+ # Store the specified format if specified
+ overflow = kwargs.get('overflow', '')
+ if overflow.lower() not in OVERFLOW_MODES:
+ msg = 'Invalid overflow method {}'.format(overflow)
+ self.logger.error(msg)
+ raise TypeError(msg)
+
+ # Provide override
+ self.overflow_mode = overflow
+
+ def image_url(self, notify_type, logo=False, extension=None,
+ image_size=None):
+ """
+ Returns Image URL if possible
+ """
+
+ if not self.image_size:
+ return None
+
+ if notify_type not in NOTIFY_TYPES:
+ return None
+
+ return self.asset.image_url(
+ notify_type=notify_type,
+ image_size=self.image_size if image_size is None else image_size,
+ logo=logo,
+ extension=extension,
+ )
+
+ def image_path(self, notify_type, extension=None):
+ """
+ Returns the path of the image if it can
+ """
+ if not self.image_size:
+ return None
+
+ if notify_type not in NOTIFY_TYPES:
+ return None
+
+ return self.asset.image_path(
+ notify_type=notify_type,
+ image_size=self.image_size,
+ extension=extension,
+ )
+
+ def image_raw(self, notify_type, extension=None):
+ """
+ Returns the raw image if it can
+ """
+ if not self.image_size:
+ return None
+
+ if notify_type not in NOTIFY_TYPES:
+ return None
+
+ return self.asset.image_raw(
+ notify_type=notify_type,
+ image_size=self.image_size,
+ extension=extension,
+ )
+
+ def color(self, notify_type, color_type=None):
+ """
+ Returns the html color (hex code) associated with the notify_type
+ """
+ if notify_type not in NOTIFY_TYPES:
+ return None
+
+ return self.asset.color(
+ notify_type=notify_type,
+ color_type=color_type,
+ )
+
+ def notify(self, *args, **kwargs):
+ """
+ Performs notification
+ """
+ try:
+ # Build a list of dictionaries that can be used to call send().
+ send_calls = list(self._build_send_calls(*args, **kwargs))
+
+ except TypeError:
+ # Internal error
+ return False
+
+ else:
+ # Loop through each call, one at a time. (Use a list rather than a
+ # generator to call all the partials, even in case of a failure.)
+ the_calls = [self.send(**kwargs2) for kwargs2 in send_calls]
+ return all(the_calls)
+
+ async def async_notify(self, *args, **kwargs):
+ """
+ Performs notification for asynchronous callers
+ """
+ try:
+ # Build a list of dictionaries that can be used to call send().
+ send_calls = list(self._build_send_calls(*args, **kwargs))
+
+ except TypeError:
+ # Internal error
+ return False
+
+ else:
+ loop = asyncio.get_event_loop()
+
+ # Wrap each call in a coroutine that uses the default executor.
+ # TODO: In the future, allow plugins to supply a native
+ # async_send() method.
+ async def do_send(**kwargs2):
+ send = partial(self.send, **kwargs2)
+ result = await loop.run_in_executor(None, send)
+ return result
+
+ # gather() all calls in parallel.
+ the_cors = (do_send(**kwargs2) for kwargs2 in send_calls)
+ return all(await asyncio.gather(*the_cors))
+
+ def _build_send_calls(self, body=None, title=None,
+ notify_type=NotifyType.INFO, overflow=None,
+ attach=None, body_format=None, **kwargs):
+ """
+ Get a list of dictionaries that can be used to call send() or
+ (in the future) async_send().
+ """
+
+ if not self.enabled:
+ # Deny notifications issued to services that are disabled
+ msg = f"{self.service_name} is currently disabled on this system."
+ self.logger.warning(msg)
+ raise TypeError(msg)
+
+ # Prepare attachments if required
+ if attach is not None and not isinstance(attach, AppriseAttachment):
+ try:
+ attach = AppriseAttachment(attach, asset=self.asset)
+
+ except TypeError:
+ # bad attachments
+ raise
+
+ # Handle situations where the body is None
+ body = '' if not body else body
+
+ elif not (body or attach):
+ # If there is not an attachment at the very least, a body must be
+ # present
+ msg = "No message body or attachment was specified."
+ self.logger.warning(msg)
+ raise TypeError(msg)
+
+ if not body and not self.attachment_support:
+ # If no body was specified, then we know that an attachment
+ # was. This is logic checked earlier in the code.
+ #
+ # Knowing this, if the plugin itself doesn't support sending
+ # attachments, there is nothing further to do here, just move
+ # along.
+ msg = f"{self.service_name} does not support attachments; " \
+ " service skipped"
+ self.logger.warning(msg)
+ raise TypeError(msg)
+
+ # Handle situations where the title is None
+ title = '' if not title else title
+
+ # Truncate flag set with attachments ensures that only 1
+ # attachment passes through. In the event there could be many
+ # services specified, we only want to do this logic once.
+ # The logic is only applicable if ther was more then 1 attachment
+ # specified
+ overflow = self.overflow_mode if overflow is None else overflow
+ if attach and len(attach) > 1 and overflow == OverflowMode.TRUNCATE:
+ # Save first attachment
+ _attach = AppriseAttachment(attach[0], asset=self.asset)
+ else:
+ # reference same attachment
+ _attach = attach
+
+ # Apply our overflow (if defined)
+ for chunk in self._apply_overflow(
+ body=body, title=title, overflow=overflow,
+ body_format=body_format):
+
+ # Send notification
+ yield dict(
+ body=chunk['body'], title=chunk['title'],
+ notify_type=notify_type, attach=_attach,
+ body_format=body_format
+ )
+
+ def _apply_overflow(self, body, title=None, overflow=None,
+ body_format=None):
+ """
+ Takes the message body and title as input. This function then
+ applies any defined overflow restrictions associated with the
+ notification service and may alter the message if/as required.
+
+ The function will always return a list object in the following
+ structure:
+ [
+ {
+ title: 'the title goes here',
+ body: 'the message body goes here',
+ },
+ {
+ title: 'the title goes here',
+ body: 'the continued message body goes here',
+ },
+
+ ]
+ """
+
+ response = list()
+
+ # tidy
+ title = '' if not title else title.strip()
+ body = '' if not body else body.rstrip()
+
+ if overflow is None:
+ # default
+ overflow = self.overflow_mode
+
+ if self.title_maxlen <= 0 and len(title) > 0:
+ if self.notify_format == NotifyFormat.HTML:
+ # Content is appended to body as html
+ body = '<{open_tag}>{title}</{close_tag}>' \
+ '<br />\r\n{body}'.format(
+ open_tag=self.default_html_tag_id,
+ title=title,
+ close_tag=self.default_html_tag_id,
+ body=body)
+
+ elif self.notify_format == NotifyFormat.MARKDOWN and \
+ body_format == NotifyFormat.TEXT:
+ # Content is appended to body as markdown
+ title = title.lstrip('\r\n \t\v\f#-')
+ if title:
+ # Content is appended to body as text
+ body = '# {}\r\n{}'.format(title, body)
+
+ else:
+ # Content is appended to body as text
+ body = '{}\r\n{}'.format(title, body)
+
+ title = ''
+
+ # Enforce the line count first always
+ if self.body_max_line_count > 0:
+ # Limit results to just the first 2 line otherwise
+ # there is just to much content to display
+ body = re.split(r'\r*\n', body)
+ body = '\r\n'.join(body[0:self.body_max_line_count])
+
+ if overflow == OverflowMode.UPSTREAM:
+ # Nothing more to do
+ response.append({'body': body, 'title': title})
+ return response
+
+ # a value of '2' allows for the \r\n that is applied when
+ # amalgamating the title
+ overflow_buffer = max(2, self.overflow_buffer) \
+ if (self.title_maxlen == 0 and len(title)) \
+ else self.overflow_buffer
+
+ #
+ # If we reach here in our code, then we're using TRUNCATE, or SPLIT
+ # actions which require some math to handle the data
+ #
+
+ # Handle situations where our body and title are amalamated into one
+ # calculation
+ title_maxlen = self.title_maxlen \
+ if not self.overflow_amalgamate_title \
+ else min(len(title) + self.overflow_max_display_count_width,
+ self.title_maxlen, self.body_maxlen)
+
+ if len(title) > title_maxlen:
+ # Truncate our Title
+ title = title[:title_maxlen].rstrip()
+
+ if self.overflow_amalgamate_title and (
+ self.body_maxlen - overflow_buffer) >= title_maxlen:
+ body_maxlen = (self.body_maxlen if not title else (
+ self.body_maxlen - title_maxlen)) - overflow_buffer
+ else:
+ # status quo
+ body_maxlen = self.body_maxlen \
+ if not self.overflow_amalgamate_title else \
+ (self.body_maxlen - overflow_buffer)
+
+ if body_maxlen > 0 and len(body) <= body_maxlen:
+ response.append({'body': body, 'title': title})
+ return response
+
+ if overflow == OverflowMode.TRUNCATE:
+ # Truncate our body and return
+ response.append({
+ 'body': body[:body_maxlen].lstrip('\r\n\x0b\x0c').rstrip(),
+ 'title': title,
+ })
+ # For truncate mode, we're done now
+ return response
+
+ if self.overflow_display_title_once is None:
+ # Detect if we only display our title once or not:
+ overflow_display_title_once = \
+ True if self.overflow_amalgamate_title and \
+ body_maxlen < self.overflow_display_count_threshold \
+ else False
+ else:
+ # Take on defined value
+
+ overflow_display_title_once = self.overflow_display_title_once
+
+ # If we reach here, then we are in SPLIT mode.
+ # For here, we want to split the message as many times as we have to
+ # in order to fit it within the designated limits.
+ if not overflow_display_title_once and not (
+ # edge case that can occur when overflow_display_title_once is
+ # forced off, but no body exists
+ self.overflow_amalgamate_title and body_maxlen <= 0):
+
+ show_counter = title and len(body) > body_maxlen and \
+ ((self.overflow_amalgamate_title and
+ body_maxlen >= self.overflow_display_count_threshold) or
+ (not self.overflow_amalgamate_title and
+ title_maxlen > self.overflow_display_count_threshold)) and (
+ title_maxlen > (self.overflow_max_display_count_width +
+ overflow_buffer) and
+ self.title_maxlen >= self.overflow_display_count_threshold)
+
+ count = 0
+ template = ''
+ if show_counter:
+ # introduce padding
+ body_maxlen -= overflow_buffer
+
+ count = int(len(body) / body_maxlen) \
+ + (1 if len(body) % body_maxlen else 0)
+
+ # Detect padding and prepare template
+ digits = len(str(count))
+ template = ' [{:0%d}/{:0%d}]' % (digits, digits)
+
+ # Update our counter
+ overflow_display_count_width = 4 + (digits * 2)
+ if overflow_display_count_width <= \
+ self.overflow_max_display_count_width:
+ if len(title) > \
+ title_maxlen - overflow_display_count_width:
+ # Truncate our title further
+ title = title[:title_maxlen -
+ overflow_display_count_width]
+
+ else: # Way to many messages to display
+ show_counter = False
+
+ response = [{
+ 'body': body[i: i + body_maxlen]
+ .lstrip('\r\n\x0b\x0c').rstrip(),
+ 'title': title + (
+ '' if not show_counter else
+ template.format(idx, count))} for idx, i in
+ enumerate(range(0, len(body), body_maxlen), start=1)]
+
+ else: # Display title once and move on
+ response = []
+ try:
+ i = range(0, len(body), body_maxlen)[0]
+
+ response.append({
+ 'body': body[i: i + body_maxlen]
+ .lstrip('\r\n\x0b\x0c').rstrip(),
+ 'title': title,
+ })
+
+ except (ValueError, IndexError):
+ # IndexError:
+ # - This happens if there simply was no body to display
+
+ # ValueError:
+ # - This happens when body_maxlen < 0 (due to title being
+ # so large)
+
+ # No worries; send title along
+ response.append({
+ 'body': '',
+ 'title': title,
+ })
+
+ # Ensure our start is set properly
+ body_maxlen = 0
+
+ # Now re-calculate based on the increased length
+ for i in range(body_maxlen, len(body), self.body_maxlen):
+ response.append({
+ 'body': body[i: i + self.body_maxlen]
+ .lstrip('\r\n\x0b\x0c').rstrip(),
+ 'title': '',
+ })
+
+ return response
+
+ def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
+ """
+ Should preform the actual notification itself.
+
+ """
+ raise NotImplementedError(
+ "send() is not implimented by the child class.")
+
+ def url_parameters(self, *args, **kwargs):
+ """
+ Provides a default set of parameters to work with. This can greatly
+ simplify URL construction in the acommpanied url() function in all
+ defined plugin services.
+ """
+
+ params = {
+ 'format': self.notify_format,
+ 'overflow': self.overflow_mode,
+ }
+
+ params.update(super().url_parameters(*args, **kwargs))
+
+ # return default parameters
+ return params
+
+ @staticmethod
+ def parse_url(url, verify_host=True, plus_to_space=False):
+ """Parses the URL and returns it broken apart into a dictionary.
+
+ This is very specific and customized for Apprise.
+
+
+ Args:
+ url (str): The URL you want to fully parse.
+ verify_host (:obj:`bool`, optional): a flag kept with the parsed
+ URL which some child classes will later use to verify SSL
+ keys (if SSL transactions take place). Unless under very
+ specific circumstances, it is strongly recomended that
+ you leave this default value set to True.
+
+ Returns:
+ A dictionary is returned containing the URL fully parsed if
+ successful, otherwise None is returned.
+ """
+ results = URLBase.parse_url(
+ url, verify_host=verify_host, plus_to_space=plus_to_space)
+
+ if not results:
+ # We're done; we failed to parse our url
+ return results
+
+ # Allow overriding the default format
+ if 'format' in results['qsd']:
+ results['format'] = results['qsd'].get('format')
+ if results['format'] not in NOTIFY_FORMATS:
+ URLBase.logger.warning(
+ 'Unsupported format specified {}'.format(
+ results['format']))
+ del results['format']
+
+ # Allow overriding the default overflow
+ if 'overflow' in results['qsd']:
+ results['overflow'] = results['qsd'].get('overflow')
+ if results['overflow'] not in OVERFLOW_MODES:
+ URLBase.logger.warning(
+ 'Unsupported overflow specified {}'.format(
+ results['overflow']))
+ del results['overflow']
+
+ # Allow emoji's override
+ if 'emojis' in results['qsd']:
+ results['emojis'] = parse_bool(results['qsd'].get('emojis'))
+
+ return results
+
+ @staticmethod
+ def parse_native_url(url):
+ """
+ This is a base class that can be optionally over-ridden by child
+ classes who can build their Apprise URL based on the one provided
+ by the notification service they choose to use.
+
+ The intent of this is to make Apprise a little more userfriendly
+ to people who aren't familiar with constructing URLs and wish to
+ use the ones that were just provied by their notification serivice
+ that they're using.
+
+ This function will return None if the passed in URL can't be matched
+ as belonging to the notification service. Otherwise this function
+ should return the same set of results that parse_url() does.
+ """
+ return None