2023-01-12 01:04:47 +00:00
|
|
|
# -*- coding: utf-8 -*-
|
2023-10-21 11:47:10 +00:00
|
|
|
# BSD 2-Clause License
|
2023-01-12 01:04:47 +00:00
|
|
|
#
|
2023-04-13 08:41:12 +00:00
|
|
|
# Apprise - Push Notification Library.
|
2024-06-07 18:48:09 +00:00
|
|
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
2023-01-12 01:04:47 +00:00
|
|
|
#
|
2023-04-13 08:41:12 +00:00
|
|
|
# Redistribution and use in source and binary forms, with or without
|
|
|
|
# modification, are permitted provided that the following conditions are met:
|
2023-01-12 01:04:47 +00:00
|
|
|
#
|
2023-04-13 08:41:12 +00:00
|
|
|
# 1. Redistributions of source code must retain the above copyright notice,
|
|
|
|
# this list of conditions and the following disclaimer.
|
2023-01-12 01:04:47 +00:00
|
|
|
#
|
2023-04-13 08:41:12 +00:00
|
|
|
# 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.
|
2023-01-12 01:04:47 +00:00
|
|
|
#
|
2023-04-13 08:41:12 +00:00
|
|
|
# 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
|
2023-01-12 01:04:47 +00:00
|
|
|
import re
|
2023-04-13 08:41:12 +00:00
|
|
|
from functools import partial
|
2023-01-12 01:04:47 +00:00
|
|
|
|
2024-06-07 18:48:09 +00:00
|
|
|
from ..url import URLBase
|
2023-01-12 01:04:47 +00:00
|
|
|
from ..common import NotifyType
|
2024-06-07 18:48:09 +00:00
|
|
|
from ..utils import parse_bool
|
2023-01-12 01:04:47 +00:00
|
|
|
from ..common import NOTIFY_TYPES
|
|
|
|
from ..common import NotifyFormat
|
|
|
|
from ..common import NOTIFY_FORMATS
|
|
|
|
from ..common import OverflowMode
|
|
|
|
from ..common import OVERFLOW_MODES
|
2024-06-07 18:48:09 +00:00
|
|
|
from ..locale import gettext_lazy as _
|
|
|
|
from ..apprise_attachment import AppriseAttachment
|
2023-01-12 01:04:47 +00:00
|
|
|
|
|
|
|
|
2023-04-13 08:41:12 +00:00
|
|
|
class NotifyBase(URLBase):
|
2023-01-12 01:04:47 +00:00
|
|
|
"""
|
|
|
|
This is the base class for all notification services
|
|
|
|
"""
|
|
|
|
|
2023-01-14 20:40:05 +00:00
|
|
|
# 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': [],
|
|
|
|
}
|
|
|
|
|
2023-01-12 01:04:47 +00:00
|
|
|
# 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
|
|
|
|
|
2024-06-07 18:48:09 +00:00
|
|
|
# Default Emoji Interpretation
|
|
|
|
interpret_emojis = False
|
|
|
|
|
2023-10-21 11:47:10 +00:00
|
|
|
# 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
|
|
|
|
|
2023-01-12 01:04:47 +00:00
|
|
|
# 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
|
2023-01-14 20:40:05 +00:00
|
|
|
template_args = dict(URLBase.template_args, **{
|
2023-01-12 01:04:47 +00:00
|
|
|
'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',
|
|
|
|
},
|
2024-06-07 18:48:09 +00:00
|
|
|
'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',
|
|
|
|
},
|
2023-01-14 20:40:05 +00:00
|
|
|
})
|
2023-01-12 01:04:47 +00:00
|
|
|
|
2024-06-07 18:48:09 +00:00
|
|
|
#
|
|
|
|
# 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
|
|
|
|
|
2023-01-12 01:04:47 +00:00
|
|
|
def __init__(self, **kwargs):
|
|
|
|
"""
|
|
|
|
Initialize some general configuration that will keep things consistent
|
|
|
|
when working with the notifiers that will inherit this class.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
2023-01-14 20:40:05 +00:00
|
|
|
super().__init__(**kwargs)
|
2023-01-12 01:04:47 +00:00
|
|
|
|
2024-06-07 18:48:09 +00:00
|
|
|
# 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
|
|
|
|
|
2023-01-12 01:04:47 +00:00
|
|
|
if 'format' in kwargs:
|
|
|
|
# Store the specified format if specified
|
|
|
|
notify_format = kwargs.get('format', '')
|
|
|
|
if notify_format.lower() not in NOTIFY_FORMATS:
|
2023-01-14 20:40:05 +00:00
|
|
|
msg = 'Invalid notification format {}'.format(notify_format)
|
2023-01-12 01:04:47 +00:00
|
|
|
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
|
|
|
|
|
2023-01-14 20:40:05 +00:00
|
|
|
def image_url(self, notify_type, logo=False, extension=None,
|
|
|
|
image_size=None):
|
2023-01-12 01:04:47 +00:00
|
|
|
"""
|
|
|
|
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,
|
2023-01-14 20:40:05 +00:00
|
|
|
image_size=self.image_size if image_size is None else image_size,
|
2023-01-12 01:04:47 +00:00
|
|
|
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,
|
|
|
|
)
|
|
|
|
|
2024-06-07 18:48:09 +00:00
|
|
|
def ascii(self, notify_type):
|
|
|
|
"""
|
|
|
|
Returns the ascii characters associated with the notify_type
|
|
|
|
"""
|
|
|
|
if notify_type not in NOTIFY_TYPES:
|
|
|
|
return None
|
|
|
|
|
|
|
|
return self.asset.ascii(
|
|
|
|
notify_type=notify_type,
|
|
|
|
)
|
|
|
|
|
2023-04-13 08:41:12 +00:00
|
|
|
def notify(self, *args, **kwargs):
|
2023-01-12 01:04:47 +00:00
|
|
|
"""
|
|
|
|
Performs notification
|
2023-04-13 08:41:12 +00:00
|
|
|
"""
|
|
|
|
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
|
2023-01-12 01:04:47 +00:00
|
|
|
|
2023-04-13 08:41:12 +00:00
|
|
|
# gather() all calls in parallel.
|
|
|
|
the_cors = (do_send(**kwargs2) for kwargs2 in send_calls)
|
|
|
|
return all(await asyncio.gather(*the_cors))
|
|
|
|
|
2023-10-21 11:47:10 +00:00
|
|
|
def _build_send_calls(self, body=None, title=None,
|
2023-04-13 08:41:12 +00:00
|
|
|
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().
|
2023-01-12 01:04:47 +00:00
|
|
|
"""
|
|
|
|
|
2023-01-14 20:40:05 +00:00
|
|
|
if not self.enabled:
|
|
|
|
# Deny notifications issued to services that are disabled
|
2023-04-13 08:41:12 +00:00
|
|
|
msg = f"{self.service_name} is currently disabled on this system."
|
|
|
|
self.logger.warning(msg)
|
|
|
|
raise TypeError(msg)
|
2023-01-14 20:40:05 +00:00
|
|
|
|
2023-01-12 01:04:47 +00:00
|
|
|
# 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
|
2023-04-13 08:41:12 +00:00
|
|
|
raise
|
2023-01-12 01:04:47 +00:00
|
|
|
|
2023-10-21 11:47:10 +00:00
|
|
|
# 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)
|
|
|
|
|
2023-01-12 01:04:47 +00:00
|
|
|
# Handle situations where the title is None
|
|
|
|
title = '' if not title else title
|
|
|
|
|
2024-06-07 18:48:09 +00:00
|
|
|
# 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
|
|
|
|
|
2023-01-12 01:04:47 +00:00
|
|
|
# Apply our overflow (if defined)
|
2023-01-14 20:40:05 +00:00
|
|
|
for chunk in self._apply_overflow(
|
|
|
|
body=body, title=title, overflow=overflow,
|
|
|
|
body_format=body_format):
|
|
|
|
|
2023-01-12 01:04:47 +00:00
|
|
|
# Send notification
|
2023-04-13 08:41:12 +00:00
|
|
|
yield dict(
|
|
|
|
body=chunk['body'], title=chunk['title'],
|
2024-06-07 18:48:09 +00:00
|
|
|
notify_type=notify_type, attach=_attach,
|
2023-04-13 08:41:12 +00:00
|
|
|
body_format=body_format
|
|
|
|
)
|
2023-01-12 01:04:47 +00:00
|
|
|
|
2023-01-14 20:40:05 +00:00
|
|
|
def _apply_overflow(self, body, title=None, overflow=None,
|
|
|
|
body_format=None):
|
2023-01-12 01:04:47 +00:00
|
|
|
"""
|
|
|
|
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',
|
2024-06-07 18:48:09 +00:00
|
|
|
body: 'the continued message body goes here',
|
2023-01-12 01:04:47 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
]
|
|
|
|
"""
|
|
|
|
|
|
|
|
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:
|
2023-01-14 20:40:05 +00:00
|
|
|
if self.notify_format == NotifyFormat.HTML:
|
2023-01-12 01:04:47 +00:00
|
|
|
# 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,
|
2023-01-14 20:40:05 +00:00
|
|
|
title=title,
|
2023-01-12 01:04:47 +00:00
|
|
|
close_tag=self.default_html_tag_id,
|
|
|
|
body=body)
|
2023-01-14 20:40:05 +00:00
|
|
|
|
|
|
|
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)
|
|
|
|
|
2023-01-12 01:04:47 +00:00
|
|
|
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
|
|
|
|
|
2024-06-07 18:48:09 +00:00
|
|
|
# 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:
|
2023-01-12 01:04:47 +00:00
|
|
|
# Truncate our Title
|
2024-06-07 18:48:09 +00:00
|
|
|
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)
|
2023-01-12 01:04:47 +00:00
|
|
|
|
2024-06-07 18:48:09 +00:00
|
|
|
if body_maxlen > 0 and len(body) <= body_maxlen:
|
2023-01-12 01:04:47 +00:00
|
|
|
response.append({'body': body, 'title': title})
|
|
|
|
return response
|
|
|
|
|
|
|
|
if overflow == OverflowMode.TRUNCATE:
|
|
|
|
# Truncate our body and return
|
|
|
|
response.append({
|
2024-06-07 18:48:09 +00:00
|
|
|
'body': body[:body_maxlen].lstrip('\r\n\x0b\x0c').rstrip(),
|
2023-01-12 01:04:47 +00:00
|
|
|
'title': title,
|
|
|
|
})
|
|
|
|
# For truncate mode, we're done now
|
|
|
|
return response
|
|
|
|
|
2024-06-07 18:48:09 +00:00
|
|
|
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
|
|
|
|
|
2023-01-12 01:04:47 +00:00
|
|
|
# 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.
|
2024-06-07 18:48:09 +00:00
|
|
|
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': '',
|
|
|
|
})
|
2023-01-12 01:04:47 +00:00
|
|
|
|
|
|
|
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.")
|
|
|
|
|
2023-01-14 20:40:05 +00:00
|
|
|
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
|
|
|
|
|
2023-01-12 01:04:47 +00:00
|
|
|
@staticmethod
|
2023-01-14 20:40:05 +00:00
|
|
|
def parse_url(url, verify_host=True, plus_to_space=False):
|
2023-01-12 01:04:47 +00:00
|
|
|
"""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.
|
|
|
|
"""
|
2023-01-14 20:40:05 +00:00
|
|
|
results = URLBase.parse_url(
|
|
|
|
url, verify_host=verify_host, plus_to_space=plus_to_space)
|
2023-01-12 01:04:47 +00:00
|
|
|
|
|
|
|
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']
|
|
|
|
|
2024-06-07 18:48:09 +00:00
|
|
|
# Allow emoji's override
|
|
|
|
if 'emojis' in results['qsd']:
|
|
|
|
results['emojis'] = parse_bool(results['qsd'].get('emojis'))
|
|
|
|
|
2023-01-12 01:04:47 +00:00
|
|
|
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
|