Persistent Storage (#1131)

This commit is contained in:
Chris Caron 2024-08-22 21:44:14 -04:00 committed by GitHub
parent 5cee11ac84
commit 827db528d0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
156 changed files with 7297 additions and 518 deletions

View File

@ -29,7 +29,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL

View File

@ -49,7 +49,7 @@ jobs:
steps:
- name: Acquire sources
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Build package
run: |

View File

@ -75,7 +75,7 @@ jobs:
steps:
- name: Acquire sources
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install prerequisites (Linux)
if: runner.os == 'Linux'
@ -137,7 +137,7 @@ jobs:
coverage report
- name: Upload coverage data
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v4
with:
files: ./coverage.xml
fail_ci_if_error: false

115
README.md
View File

@ -534,9 +534,121 @@ aobj.add('foobar://')
# Send our notification out through our foobar://
aobj.notify("test")
```
You can read more about creating your own custom notifications and/or hooks [here](https://github.com/caronc/apprise/wiki/decorator_notify).
# Persistent Storage
Persistent storage allows Apprise to cache re-occurring actions optionaly to disk. This can greatly reduce the overhead used to send a notification.
There are 3 options Apprise can operate using this:
1. `AUTO`: Flush any gathered data for persistent storage on demand. This option is incredibly light weight. This is the default behavior for all CLI usage. Content can be manually flushed to disk using this option as well should a developer choose to do so. The CLI uses this option by default and only writes anything accumulated to disk after all of it's notifications have completed.
1. `FLUSH`: Flushes any gathered data for persistent storage as often as it is acquired.
1. `MEMORY`: Only store information in memory, never write to disk. This is the option one would set if they simply wish to disable Persistent Storage entirely. By default this is the mode used by the API and is at the developers discretion to enable one of the other options.
## CLI Persistent Storage Commands
Persistent storage is set to `AUTO` mode by default.
Specifying the keyword `storage` will assume that all subseqent calls are related to the storage subsection of Apprise.
```bash
# List all of the occupied space used by Apprise's Persistent Storage:
apprise storage list
# list is the default option, so the following does the same thing:
apprise storage
# You can prune all of your storage older then 30 days
# and not accessed for this period like so:
apprise storage prune
# You can do a hard reset (and wipe all persistent storage) with:
apprise storage clean
```
You can also filter your results by adding tags and/or URL Identifiers. When you get a listing (`apprise storage list`), you may see:
```
# example output of 'apprise storage list':
1. f7077a65 0.00B unused
- matrixs://abcdef:****@synapse.example12.com/%23general?image=no&mode=off&version=3&msgtype...
tags: team
2. 0e873a46 81.10B active
- tgram://W...U//?image=False&detect=yes&silent=no&preview=no&content=before&mdv=v1&format=m...
tags: personal
3. abcd123 12.00B stale
```
The states are:
- `unused`: This plugin has not commited anything to disk for reuse/cache purposes
- `active`: This plugin has written content to disk. Or at the very least, it has prepared a persistent storage location it can write into.
- `stale`: The system detected a location where a URL may have possibly written to in the past, but there is nothing linking to it using the URLs provided. It is likely wasting space or is no longer of any use.
You can use this information to filter your results by specifying _URL ID_ values after your command. For example:
```bash
# The below commands continue with the example already identified above
# the following would match abcd123 (even though just ab was provided)
# The output would only list the 'stale' entry above
apprise storage list ab
# knowing our filter is safe, we could remove it
# the below command would not obstruct our other to URLs and would only
# remove our stale one:
apprise storage clean ab
# Entries can be filtered by tag as well:
apprise storage list --tag=team
# You can match on multiple URL ID's as well:
# The followin would actually match the URL ID's of 1. and .2 above
apprise storage list f 0
```
For more information on persistent storage, [visit here](https://github.com/caronc/apprise/wiki/persistent_storage).
## API Persistent Storage Commands
By default, no persistent storage is set to be in `MEMORY` mode for those building from within the Apprise API.
It's at the developers discretion to enable it. But should you choose to do so, it's as easy as including the information in the `AppriseAsset()` object prior to the initialization of your `Apprise()` instance.
For example:
```python
from apprise import Apprise
from apprise import AppriseAsset
from apprise import PersistentStoreMode
# Prepare a location the persistent storage can write to
# This immediately assumes you wish to write in AUTO mode
asset = AppriseAsset(storage_path="/path/to/save/data")
# If you want to be more explicit and set more options, then
# you may do the following
asset = AppriseAsset(
# Set our storage path directory (minimum requirement to enable it)
storage_path="/path/to/save/data",
# Set the mode... the options are:
# 1. PersistentStoreMode.MEMORY
# - disable persistent storage from writing to disk
# 2. PersistentStoreMode.AUTO
# - write to disk on demand
# 3. PersistentStoreMode.FLUSH
# - write to disk always and often
storage_mode=PersistentStoreMode.FLUSH
# the URL IDs are by default 8 characters in length, there is
# really no reason to change this. You can increase/decrease
# it's value here. Must be > 2; default is 8 if not specified
storage_idlen=6,
)
# Now that we've got our asset, we just work with our Apprise object as we
# normally do
aobj = Apprise(asset=asset)
```
For more information on persistent storage, [visit here](https://github.com/caronc/apprise/wiki/persistent_storage).
# Want To Learn More?
If you're interested in reading more about this and other methods on how to customize your own notifications, please check out the following links:
@ -545,6 +657,7 @@ If you're interested in reading more about this and other methods on how to cust
* 🔧 [Troubleshooting](https://github.com/caronc/apprise/wiki/Troubleshooting)
* ⚙️ [Configuration File Help](https://github.com/caronc/apprise/wiki/config)
* ⚡ [Create Your Own Custom Notifications](https://github.com/caronc/apprise/wiki/decorator_notify)
* 💾 [Persistent Storage](https://github.com/caronc/apprise/wiki/persistent_storage)
* 🌎 [Apprise API/Web Interface](https://github.com/caronc/apprise-api)
* 🎉 [Showcase](https://github.com/caronc/apprise/wiki/showcase)

View File

@ -48,16 +48,20 @@ from .common import ContentIncludeMode
from .common import CONTENT_INCLUDE_MODES
from .common import ContentLocation
from .common import CONTENT_LOCATIONS
from .common import PersistentStoreMode
from .common import PERSISTENT_STORE_MODES
from .url import URLBase
from .url import PrivacyMode
from .plugins.base import NotifyBase
from .config.base import ConfigBase
from .attachment.base import AttachBase
from . import exception
from .apprise import Apprise
from .locale import AppriseLocale
from .asset import AppriseAsset
from .persistent_store import PersistentStore
from .apprise_config import AppriseConfig
from .apprise_attachment import AppriseAttachment
from .manager_attachment import AttachmentManager
@ -77,6 +81,10 @@ __all__ = [
# Core
'Apprise', 'AppriseAsset', 'AppriseConfig', 'AppriseAttachment', 'URLBase',
'NotifyBase', 'ConfigBase', 'AttachBase', 'AppriseLocale',
'PersistentStore',
# Exceptions
'exception',
# Reference
'NotifyType', 'NotifyImageSize', 'NotifyFormat', 'OverflowMode',
@ -84,6 +92,7 @@ __all__ = [
'ConfigFormat', 'CONFIG_FORMATS',
'ContentIncludeMode', 'CONTENT_INCLUDE_MODES',
'ContentLocation', 'CONTENT_LOCATIONS',
'PersistentStoreMode', 'PERSISTENT_STORE_MODES',
'PrivacyMode',
# Managers

View File

@ -33,6 +33,7 @@ from os.path import dirname
from os.path import isfile
from os.path import abspath
from .common import NotifyType
from .common import PersistentStoreMode
from .manager_plugins import NotificationManager
@ -157,6 +158,22 @@ class AppriseAsset:
# By default, no paths are scanned.
__plugin_paths = []
# Optionally set the location of the persistent storage
# By default there is no path and thus persistent storage is not used
__storage_path = None
# Optionally define the default salt to apply to all persistent storage
# namespace generation (unless over-ridden)
__storage_salt = b''
# Optionally define the namespace length of the directories created by
# the storage. If this is set to zero, then the length is pre-determined
# by the generator (sha1, md5, sha256, etc)
__storage_idlen = 8
# Set storage to auto
__storage_mode = PersistentStoreMode.AUTO
# All internal/system flags are prefixed with an underscore (_)
# These can only be initialized using Python libraries and are not picked
# up from (yaml) configuration files (if set)
@ -171,7 +188,9 @@ class AppriseAsset:
# A unique identifer we can use to associate our calling source
_uid = str(uuid4())
def __init__(self, plugin_paths=None, **kwargs):
def __init__(self, plugin_paths=None, storage_path=None,
storage_mode=None, storage_salt=None,
storage_idlen=None, **kwargs):
"""
Asset Initialization
@ -187,8 +206,49 @@ class AppriseAsset:
if plugin_paths:
# Load any decorated modules if defined
self.__plugin_paths = plugin_paths
N_MGR.module_detection(plugin_paths)
if storage_path:
# Define our persistent storage path
self.__storage_path = storage_path
if storage_mode:
# Define how our persistent storage behaves
self.__storage_mode = storage_mode
if isinstance(storage_idlen, int):
# Define the number of characters utilized from our namespace lengh
if storage_idlen < 0:
# Unsupported type
raise ValueError(
'AppriseAsset storage_idlen(): Value must '
'be an integer and > 0')
# Store value
self.__storage_idlen = storage_idlen
if storage_salt is not None:
# Define the number of characters utilized from our namespace lengh
if isinstance(storage_salt, bytes):
self.__storage_salt = storage_salt
elif isinstance(storage_salt, str):
try:
self.__storage_salt = storage_salt.encode(self.encoding)
except UnicodeEncodeError:
# Bad data; don't pass it along
raise ValueError(
'AppriseAsset namespace_salt(): '
'Value provided could not be encoded')
else: # Unsupported
raise ValueError(
'AppriseAsset namespace_salt(): Value provided must be '
'string or bytes object')
def color(self, notify_type, color_type=None):
"""
Returns an HTML mapped color based on passed in notify type
@ -356,3 +416,40 @@ class AppriseAsset:
"""
return int(value.lstrip('#'), 16)
@property
def plugin_paths(self):
"""
Return the plugin paths defined
"""
return self.__plugin_paths
@property
def storage_path(self):
"""
Return the persistent storage path defined
"""
return self.__storage_path
@property
def storage_mode(self):
"""
Return the persistent storage mode defined
"""
return self.__storage_mode
@property
def storage_salt(self):
"""
Return the provided namespace salt; this is always of type bytes
"""
return self.__storage_salt
@property
def storage_idlen(self):
"""
Return the persistent storage id length
"""
return self.__storage_idlen

View File

@ -29,6 +29,7 @@
import re
import os
from .base import AttachBase
from ..utils import path_decode
from ..common import ContentLocation
from ..locale import gettext_lazy as _
@ -57,7 +58,10 @@ class AttachFile(AttachBase):
# Store path but mark it dirty since we have not performed any
# verification at this point.
self.dirty_path = os.path.expanduser(path)
self.dirty_path = path_decode(path)
# Track our file as it was saved
self.__original_path = os.path.normpath(path)
return
def url(self, privacy=False, *args, **kwargs):
@ -77,7 +81,7 @@ class AttachFile(AttachBase):
params['name'] = self._name
return 'file://{path}{params}'.format(
path=self.quote(self.dirty_path),
path=self.quote(self.__original_path),
params='?{}'.format(self.urlencode(params, safe='/'))
if params else '',
)

View File

@ -27,26 +27,27 @@
# POSSIBILITY OF SUCH DAMAGE.
import click
import textwrap
import logging
import platform
import sys
import os
import shutil
import re
from os.path import isfile
from os.path import exists
from os.path import expanduser
from os.path import expandvars
from . import NotifyType
from . import NotifyFormat
from . import Apprise
from . import AppriseAsset
from . import AppriseConfig
from . import PersistentStore
from .utils import parse_list
from .utils import dir_size, bytes_to_str, parse_list, path_decode
from .common import NOTIFY_TYPES
from .common import NOTIFY_FORMATS
from .common import PERSISTENT_STORE_MODES
from .common import PersistentStoreState
from .common import ContentLocation
from .logger import logger
@ -104,67 +105,94 @@ DEFAULT_PLUGIN_PATHS = (
'/var/lib/apprise/plugins',
)
#
# Persistent Storage
#
DEFAULT_STORAGE_PATH = '~/.local/share/apprise/cache'
# Detect Windows
if platform.system() == 'Windows':
# Default Config Search Path for Windows Users
DEFAULT_CONFIG_PATHS = (
expandvars('%APPDATA%\\Apprise\\apprise'),
expandvars('%APPDATA%\\Apprise\\apprise.conf'),
expandvars('%APPDATA%\\Apprise\\apprise.yml'),
expandvars('%APPDATA%\\Apprise\\apprise.yaml'),
expandvars('%LOCALAPPDATA%\\Apprise\\apprise'),
expandvars('%LOCALAPPDATA%\\Apprise\\apprise.conf'),
expandvars('%LOCALAPPDATA%\\Apprise\\apprise.yml'),
expandvars('%LOCALAPPDATA%\\Apprise\\apprise.yaml'),
'%APPDATA%\\Apprise\\apprise',
'%APPDATA%\\Apprise\\apprise.conf',
'%APPDATA%\\Apprise\\apprise.yml',
'%APPDATA%\\Apprise\\apprise.yaml',
'%LOCALAPPDATA%\\Apprise\\apprise',
'%LOCALAPPDATA%\\Apprise\\apprise.conf',
'%LOCALAPPDATA%\\Apprise\\apprise.yml',
'%LOCALAPPDATA%\\Apprise\\apprise.yaml',
#
# Global Support
#
# C:\ProgramData\Apprise
expandvars('%ALLUSERSPROFILE%\\Apprise\\apprise'),
expandvars('%ALLUSERSPROFILE%\\Apprise\\apprise.conf'),
expandvars('%ALLUSERSPROFILE%\\Apprise\\apprise.yml'),
expandvars('%ALLUSERSPROFILE%\\Apprise\\apprise.yaml'),
'%ALLUSERSPROFILE%\\Apprise\\apprise',
'%ALLUSERSPROFILE%\\Apprise\\apprise.conf',
'%ALLUSERSPROFILE%\\Apprise\\apprise.yml',
'%ALLUSERSPROFILE%\\Apprise\\apprise.yaml',
# C:\Program Files\Apprise
expandvars('%PROGRAMFILES%\\Apprise\\apprise'),
expandvars('%PROGRAMFILES%\\Apprise\\apprise.conf'),
expandvars('%PROGRAMFILES%\\Apprise\\apprise.yml'),
expandvars('%PROGRAMFILES%\\Apprise\\apprise.yaml'),
'%PROGRAMFILES%\\Apprise\\apprise',
'%PROGRAMFILES%\\Apprise\\apprise.conf',
'%PROGRAMFILES%\\Apprise\\apprise.yml',
'%PROGRAMFILES%\\Apprise\\apprise.yaml',
# C:\Program Files\Common Files
expandvars('%COMMONPROGRAMFILES%\\Apprise\\apprise'),
expandvars('%COMMONPROGRAMFILES%\\Apprise\\apprise.conf'),
expandvars('%COMMONPROGRAMFILES%\\Apprise\\apprise.yml'),
expandvars('%COMMONPROGRAMFILES%\\Apprise\\apprise.yaml'),
'%COMMONPROGRAMFILES%\\Apprise\\apprise',
'%COMMONPROGRAMFILES%\\Apprise\\apprise.conf',
'%COMMONPROGRAMFILES%\\Apprise\\apprise.yml',
'%COMMONPROGRAMFILES%\\Apprise\\apprise.yaml',
)
# Default Plugin Search Path for Windows Users
DEFAULT_PLUGIN_PATHS = (
expandvars('%APPDATA%\\Apprise\\plugins'),
expandvars('%LOCALAPPDATA%\\Apprise\\plugins'),
'%APPDATA%\\Apprise\\plugins',
'%LOCALAPPDATA%\\Apprise\\plugins',
#
# Global Support
#
# C:\ProgramData\Apprise\plugins
expandvars('%ALLUSERSPROFILE%\\Apprise\\plugins'),
'%ALLUSERSPROFILE%\\Apprise\\plugins',
# C:\Program Files\Apprise\plugins
expandvars('%PROGRAMFILES%\\Apprise\\plugins'),
'%PROGRAMFILES%\\Apprise\\plugins',
# C:\Program Files\Common Files
expandvars('%COMMONPROGRAMFILES%\\Apprise\\plugins'),
'%COMMONPROGRAMFILES%\\Apprise\\plugins',
)
#
# Persistent Storage
#
DEFAULT_STORAGE_PATH = '%APPDATA%/Apprise/cache'
def print_help_msg(command):
"""
Prints help message when -h or --help is specified.
class PersistentStorageMode:
"""
with click.Context(command) as ctx:
click.echo(command.get_help(ctx))
Persistent Storage Modes
"""
# List all detected configuration loaded
LIST = 'list'
# Prune persistent storage based on age
PRUNE = 'prune'
# Reset all (reguardless of age)
CLEAR = 'clear'
# Define the types in a list for validation purposes
PERSISTENT_STORAGE_MODES = (
PersistentStorageMode.LIST,
PersistentStorageMode.PRUNE,
PersistentStorageMode.CLEAR,
)
if os.environ.get('APPRISE_STORAGE', '').strip():
# Over-ride Default Storage Path
DEFAULT_STORAGE_PATH = os.environ.get('APPRISE_STORAGE')
def print_version_msg():
@ -180,7 +208,106 @@ def print_version_msg():
click.echo('\n'.join(result))
@click.command(context_settings=CONTEXT_SETTINGS)
class CustomHelpCommand(click.Command):
def format_help(self, ctx, formatter):
# Custom help message
content = (
'Send a notification to all of the specified servers '
'identified by their URLs',
'the content provided within the title, body and '
'notification-type.',
'',
'For a list of all of the supported services and information on '
'how to use ',
'them, check out at https://github.com/caronc/apprise')
for line in content:
formatter.write_text(line)
# Display options and arguments in the default format
self.format_options(ctx, formatter)
self.format_epilog(ctx, formatter)
# Custom 'Actions:' section after the 'Options:'
formatter.write_text('')
formatter.write_text('Actions:')
actions = [(
'storage', 'Access the persistent storage disk administration',
[(
'list',
'List all URL IDs associated with detected URL(s). '
'This is also the default action ran if nothing is provided',
), (
'prune',
'Eliminates stale entries found based on '
'--storage-prune-days (-SPD)',
), (
'clean',
'Removes any persistent data created by Apprise',
)],
)]
#
# Some variables
#
# actions are indented this many spaces
# sub actions double this value
action_indent = 2
# label padding (for alignment)
action_label_width = 10
space = ' '
space_re = re.compile(r'\r*\n')
cols = 80
indent = 10
# Format each action and its subactions
for action, description, sub_actions in actions:
# Our action indent
ai = ' ' * action_indent
# Format the main action description
formatted_description = space_re.split(textwrap.fill(
description, width=(cols - indent - action_indent),
initial_indent=space * indent,
subsequent_indent=space * indent))
for no, line in enumerate(formatted_description):
if not no:
formatter.write_text(
f'{ai}{action:<{action_label_width}}{line}')
else: # pragma: no cover
# Note: no branch is set intentionally since this is not
# tested since in 2024.08.13 when this was set up
# it never entered this area of the code. But we
# know it works because we repeat this process with
# our sub-options below
formatter.write_text(
f'{ai}{space:<{action_label_width}}{line}')
# Format each subaction
ai = ' ' * (action_indent * 2)
for action, description in sub_actions:
formatted_description = space_re.split(textwrap.fill(
description, width=(cols - indent - (action_indent * 3)),
initial_indent=space * (indent - action_indent),
subsequent_indent=space * (indent - action_indent)))
for no, line in enumerate(formatted_description):
if not no:
formatter.write_text(
f'{ai}{action:<{action_label_width}}{line}')
else:
formatter.write_text(
f'{ai}{space:<{action_label_width}}{line}')
# Include any epilog or additional text
self.format_epilog(ctx, formatter)
@click.command(context_settings=CONTEXT_SETTINGS, cls=CustomHelpCommand)
@click.option('--body', '-b', default=None, type=str,
help='Specify the message body. If no body is specified then '
'content is read from <stdin>.')
@ -190,23 +317,43 @@ def print_version_msg():
@click.option('--plugin-path', '-P', default=None, type=str, multiple=True,
metavar='PLUGIN_PATH',
help='Specify one or more plugin paths to scan.')
@click.option('--storage-path', '-S', default=DEFAULT_STORAGE_PATH, type=str,
metavar='STORAGE_PATH',
help='Specify the path to the persistent storage location '
'(default={}).'.format(DEFAULT_STORAGE_PATH))
@click.option('--storage-prune-days', '-SPD', default=30,
type=int,
help='Define the number of days the storage prune '
'should run using. Setting this to zero (0) will eliminate '
'all accumulated content. By default this value is 30 (days).')
@click.option('--storage-uid-length', '-SUL', default=8,
type=int,
help='Define the number of unique characters to store persistent'
'cache in. By default this value is 6 (characters).')
@click.option('--storage-mode', '-SM', default=PERSISTENT_STORE_MODES[0],
type=str, metavar='MODE',
help='Persistent disk storage write mode (default={}). '
'Possible values are "{}", and "{}".'.format(
PERSISTENT_STORE_MODES[0], '", "'.join(
PERSISTENT_STORE_MODES[:-1]),
PERSISTENT_STORE_MODES[-1]))
@click.option('--config', '-c', default=None, type=str, multiple=True,
metavar='CONFIG_URL',
help='Specify one or more configuration locations.')
@click.option('--attach', '-a', default=None, type=str, multiple=True,
metavar='ATTACHMENT_URL',
help='Specify one or more attachment.')
@click.option('--notification-type', '-n', default=NotifyType.INFO, type=str,
@click.option('--notification-type', '-n', default=NOTIFY_TYPES[0], type=str,
metavar='TYPE',
help='Specify the message type (default={}). '
'Possible values are "{}", and "{}".'.format(
NotifyType.INFO, '", "'.join(NOTIFY_TYPES[:-1]),
NOTIFY_TYPES[0], '", "'.join(NOTIFY_TYPES[:-1]),
NOTIFY_TYPES[-1]))
@click.option('--input-format', '-i', default=NotifyFormat.TEXT, type=str,
@click.option('--input-format', '-i', default=NOTIFY_FORMATS[0], type=str,
metavar='FORMAT',
help='Specify the message input format (default={}). '
'Possible values are "{}", and "{}".'.format(
NotifyFormat.TEXT, '", "'.join(NOTIFY_FORMATS[:-1]),
NOTIFY_FORMATS[0], '", "'.join(NOTIFY_FORMATS[:-1]),
NOTIFY_FORMATS[-1]))
@click.option('--theme', '-T', default='default', type=str, metavar='THEME',
help='Specify the default theme.')
@ -241,10 +388,12 @@ def print_version_msg():
help='Display the apprise version and exit.')
@click.argument('urls', nargs=-1,
metavar='SERVER_URL [SERVER_URL2 [SERVER_URL3]]',)
def main(body, title, config, attach, urls, notification_type, theme, tag,
@click.pass_context
def main(ctx, body, title, config, attach, urls, notification_type, theme, tag,
input_format, dry_run, recursion_depth, verbose, disable_async,
details, interpret_escapes, interpret_emojis, plugin_path, debug,
version):
details, interpret_escapes, interpret_emojis, plugin_path,
storage_path, storage_mode, storage_prune_days, storage_uid_length,
debug, version):
"""
Send a notification to all of the specified servers identified by their
URLs the content provided within the title, body and notification-type.
@ -253,7 +402,7 @@ def main(body, title, config, attach, urls, notification_type, theme, tag,
use them, check out at https://github.com/caronc/apprise
"""
# Note: Click ignores the return values of functions it wraps, If you
# want to return a specific error code, you must call sys.exit()
# want to return a specific error code, you must call ctx.exit()
# as you will see below.
debug = True if debug else False
@ -297,7 +446,7 @@ def main(body, title, config, attach, urls, notification_type, theme, tag,
if version:
print_version_msg()
sys.exit(0)
ctx.exit(0)
# Simple Error Checking
notification_type = notification_type.strip().lower()
@ -307,7 +456,7 @@ def main(body, title, config, attach, urls, notification_type, theme, tag,
.format(notification_type))
# 2 is the same exit code returned by Click if there is a parameter
# issue. For consistency, we also return a 2
sys.exit(2)
ctx.exit(2)
input_format = input_format.strip().lower()
if input_format not in NOTIFY_FORMATS:
@ -316,13 +465,31 @@ def main(body, title, config, attach, urls, notification_type, theme, tag,
.format(input_format))
# 2 is the same exit code returned by Click if there is a parameter
# issue. For consistency, we also return a 2
sys.exit(2)
ctx.exit(2)
storage_mode = storage_mode.strip().lower()
if storage_mode not in PERSISTENT_STORE_MODES:
logger.error(
'The --storage-mode (-SM) value of {} is not supported.'
.format(storage_mode))
# 2 is the same exit code returned by Click if there is a parameter
# issue. For consistency, we also return a 2
ctx.exit(2)
if not plugin_path:
# Prepare a default set of plugin path
plugin_path = \
next((path for path in DEFAULT_PLUGIN_PATHS
if exists(expanduser(path))), None)
[path for path in DEFAULT_PLUGIN_PATHS
if exists(path_decode(path))]
if storage_uid_length < 2:
logger.error(
'The --storage-uid-length (-SUL) value can not be lower '
'then two (2).')
# 2 is the same exit code returned by Click if there is a
# parameter issue. For consistency, we also return a 2
ctx.exit(2)
# Prepare our asset
asset = AppriseAsset(
@ -346,6 +513,15 @@ def main(body, title, config, attach, urls, notification_type, theme, tag,
# Load our plugins
plugin_paths=plugin_path,
# Load our persistent storage path
storage_path=path_decode(storage_path),
# Our storage URL ID Length
storage_idlen=storage_uid_length,
# Define if we flush to disk as soon as possible or not when required
storage_mode=storage_mode
)
# Create our Apprise object
@ -429,7 +605,7 @@ def main(body, title, config, attach, urls, notification_type, theme, tag,
# new line padding between entries
click.echo()
sys.exit(0)
ctx.exit(0)
# end if details()
# The priorities of what is accepted are parsed in order below:
@ -439,7 +615,7 @@ def main(body, title, config, attach, urls, notification_type, theme, tag,
# 4. Configuration by environment variable: APPRISE_CONFIG
# 5. Default Configuration File(s) (if found)
#
if urls:
elif urls and not 'storage'.startswith(urls[0]):
if tag:
# Ignore any tags specified
logger.warning(
@ -483,20 +659,145 @@ def main(body, title, config, attach, urls, notification_type, theme, tag,
else:
# Load default configuration
a.add(AppriseConfig(
paths=[f for f in DEFAULT_CONFIG_PATHS if isfile(expanduser(f))],
paths=[f for f in DEFAULT_CONFIG_PATHS if isfile(path_decode(f))],
asset=asset, recursion=recursion_depth))
if len(a) == 0 and not urls:
logger.error(
'You must specify at least one server URL or populated '
'configuration file.')
print_help_msg(main)
sys.exit(1)
click.echo(ctx.get_help())
ctx.exit(1)
# each --tag entry comprises of a comma separated 'and' list
# we or each of of the --tag and sets specified.
tags = None if not tag else [parse_list(t) for t in tag]
# Determine if we're dealing with URLs or url_ids based on the first
# entry provided.
if urls and 'storage'.startswith(urls[0]):
#
# Storage Mode
# - urls are now to be interpreted as best matching namespaces
#
if storage_prune_days < 0:
logger.error(
'The --storage-prune-days (-SPD) value can not be lower '
'then zero (0).')
# 2 is the same exit code returned by Click if there is a
# parameter issue. For consistency, we also return a 2
ctx.exit(2)
# Number of columns to assume in the terminal. In future, maybe this
# can be detected and made dynamic. The actual column count is 80, but
# 5 characters are already reserved for the counter on the left
(columns, _) = shutil.get_terminal_size(fallback=(80, 24))
filter_uids = urls[1:]
action = PERSISTENT_STORAGE_MODES[0]
if filter_uids:
_action = next( # pragma: no branch
(a for a in PERSISTENT_STORAGE_MODES
if a.startswith(filter_uids[0])), None)
if _action:
# pop top entry
filter_uids = filter_uids[1:]
action = _action
# Get our detected URL IDs
uids = {}
for plugin in (a if not tags else a.find(tag=tags)):
_id = plugin.url_id()
if not _id:
continue
if filter_uids and next(
(False for n in filter_uids if _id.startswith(n)), True):
continue
if _id not in uids:
uids[_id] = {
'plugins': [plugin],
'state': PersistentStoreState.UNUSED,
'size': 0,
}
else:
# It's possible to have more then one URL point to the same
# location (thus match against the same url id more then once
uids[_id]['plugins'].append(plugin)
if action == PersistentStorageMode.LIST:
detected_uid = PersistentStore.disk_scan(
# Use our asset path as it has already been properly parsed
path=asset.storage_path,
# Provide filter if specified
namespace=filter_uids,
)
for _id in detected_uid:
size, _ = dir_size(os.path.join(asset.storage_path, _id))
if _id in uids:
uids[_id]['state'] = PersistentStoreState.ACTIVE
uids[_id]['size'] = size
elif not tags:
uids[_id] = {
'plugins': [],
# No cross reference (wasted space?)
'state': PersistentStoreState.STALE,
# Acquire disk space
'size': size,
}
for idx, (uid, meta) in enumerate(uids.items()):
fg = "green" \
if meta['state'] == PersistentStoreState.ACTIVE else (
"red"
if meta['state'] == PersistentStoreState.STALE else
"white")
if idx > 0:
# New line
click.echo()
click.echo("{: 4d}. ".format(idx + 1), nl=False)
click.echo(click.style("{:<52} {:<8} {}".format(
uid, bytes_to_str(meta['size']), meta['state']),
fg=fg, bold=True))
for entry in meta['plugins']:
url = entry.url(privacy=True)
click.echo("{:>7} {}".format(
'-',
url if len(url) <= (columns - 8) else '{}...'.format(
url[:columns - 11])))
if entry.tags:
click.echo("{:>10}: {}".format(
'tags', ', '.join(entry.tags)))
else: # PersistentStorageMode.PRUNE or PersistentStorageMode.CLEAR
if action == PersistentStorageMode.CLEAR:
storage_prune_days = 0
# clean up storage
results = PersistentStore.disk_prune(
# Use our asset path as it has already been properly parsed
path=asset.storage_path,
# Provide our namespaces if they exist
namespace=None if not filter_uids else filter_uids,
# Convert expiry from days to seconds
expires=storage_prune_days * 60 * 60 * 24,
action=not dry_run)
ctx.exit(0)
# end if disk_prune()
ctx.exit(0)
# end if storage()
if not dry_run:
if body is None:
logger.trace('No --body (-b) specified; reading from stdin')
@ -508,10 +809,10 @@ def main(body, title, config, attach, urls, notification_type, theme, tag,
body=body, title=title, notify_type=notification_type, tag=tags,
attach=attach)
else:
# Number of rows to assume in the terminal. In future, maybe this can
# be detected and made dynamic. The actual row count is 80, but 5
# characters are already reserved for the counter on the left
rows = 75
# Number of columns to assume in the terminal. In future, maybe this
# can be detected and made dynamic. The actual column count is 80, but
# 5 characters are already reserved for the counter on the left
(columns, _) = shutil.get_terminal_size(fallback=(80, 24))
# Initialize our URL response; This is populated within the for/loop
# below; but plays a factor at the end when we need to determine if
@ -520,11 +821,18 @@ def main(body, title, config, attach, urls, notification_type, theme, tag,
for idx, server in enumerate(a.find(tag=tags)):
url = server.url(privacy=True)
click.echo("{: 3d}. {}".format(
click.echo("{: 4d}. {}".format(
idx + 1,
url if len(url) <= rows else '{}...'.format(url[:rows - 3])))
url if len(url) <= (columns - 8) else '{}...'.format(
url[:columns - 9])))
# Share our URL ID
click.echo("{:>10}: {}".format(
'uid', '- n/a -' if not server.url_id()
else server.url_id()))
if server.tags:
click.echo("{} - {}".format(' ' * 5, ', '.join(server.tags)))
click.echo("{:>10}: {}".format('tags', ', '.join(server.tags)))
# Initialize a default response of nothing matched, otherwise
# if we matched at least one entry, we can return True
@ -537,11 +845,11 @@ def main(body, title, config, attach, urls, notification_type, theme, tag,
# Exit code 3 is used since Click uses exit code 2 if there is an
# error with the parameters specified
sys.exit(3)
ctx.exit(3)
elif result is False:
# At least 1 notification service failed to send
sys.exit(1)
ctx.exit(1)
# else: We're good!
sys.exit(0)
ctx.exit(0)

View File

@ -187,6 +187,42 @@ CONTENT_LOCATIONS = (
ContentLocation.INACCESSIBLE,
)
class PersistentStoreMode:
# Allow persistent storage; write on demand
AUTO = 'auto'
# Always flush every change to disk after it's saved. This has higher i/o
# but enforces disk reflects what was set immediately
FLUSH = 'flush'
# memory based store only
MEMORY = 'memory'
PERSISTENT_STORE_MODES = (
PersistentStoreMode.AUTO,
PersistentStoreMode.FLUSH,
PersistentStoreMode.MEMORY,
)
class PersistentStoreState:
"""
Defines the persistent states describing what has been cached
"""
# Persistent Directory is actively cross-referenced against a matching URL
ACTIVE = 'active'
# Persistent Directory is no longer being used or has no cross-reference
STALE = 'stale'
# Persistent Directory is not utilizing any disk space at all, however
# it potentially could if the plugin it successfully cross-references
# is utilized
UNUSED = 'unused'
# This is a reserved tag that is automatically assigned to every
# Notification Plugin
MATCH_ALL_TAG = 'all'

View File

@ -29,6 +29,7 @@
import re
import os
from .base import ConfigBase
from ..utils import path_decode
from ..common import ConfigFormat
from ..common import ContentIncludeMode
from ..locale import gettext_lazy as _
@ -59,7 +60,10 @@ class ConfigFile(ConfigBase):
super().__init__(**kwargs)
# Store our file path as it was set
self.path = os.path.abspath(os.path.expanduser(path))
self.path = path_decode(path)
# Track the file as it was saved
self.__original_path = os.path.normpath(path)
# Update the config path to be relative to our file we just loaded
self.config_path = os.path.dirname(self.path)
@ -89,7 +93,7 @@ class ConfigFile(ConfigBase):
params['format'] = self.config_format
return 'file://{path}{params}'.format(
path=self.quote(self.path),
path=self.quote(self.__original_path),
params='?{}'.format(self.urlencode(params)) if params else '',
)

53
apprise/exception.py Normal file
View File

@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2024, 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.
#
# 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 errno
class AppriseException(Exception):
"""
Base Apprise Exception Class
"""
def __init__(self, message, error_code=0):
super().__init__(message)
self.error_code = error_code
class AppriseDiskIOError(AppriseException):
"""
Thrown when an disk i/o error occurs
"""
def __init__(self, message, error_code=errno.EIO):
super().__init__(message, error_code=error_code)
class AppriseFileNotFound(AppriseDiskIOError, FileNotFoundError):
"""
Thrown when a persistent write occured in MEMORY mode
"""
def __init__(self, message):
super().__init__(message, error_code=errno.ENOENT)

View File

@ -36,6 +36,7 @@ import threading
from .utils import import_module
from .utils import Singleton
from .utils import parse_list
from .utils import path_decode
from os.path import dirname
from os.path import abspath
from os.path import join
@ -373,7 +374,7 @@ class PluginManager(metaclass=Singleton):
return
for _path in paths:
path = os.path.abspath(os.path.expanduser(_path))
path = path_decode(_path)
if (cache and path in self._paths_previously_scanned) \
or not os.path.exists(path):
# We're done as we've already scanned this

1676
apprise/persistent_store.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -354,6 +354,15 @@ class NotifyAfricasTalking(NotifyBase):
return not has_error
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (self.secure_protocol, self.appuser, self.apikey)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -729,6 +729,15 @@ class NotifyAprs(NotifyBase):
params=NotifyAprs.urlencode(params),
)
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (self.user, self.password, self.locale)
def __len__(self):
"""
Returns the number of targets associated with this notification

View File

@ -395,6 +395,18 @@ class NotifyBark(NotifyBase):
return not has_error
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (
self.secure_protocol if self.secure else self.protocol,
self.user, self.password, self.host, self.port,
)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -38,7 +38,9 @@ from ..common import NotifyFormat
from ..common import NOTIFY_FORMATS
from ..common import OverflowMode
from ..common import OVERFLOW_MODES
from ..common import PersistentStoreMode
from ..locale import gettext_lazy as _
from ..persistent_store import PersistentStore
from ..apprise_attachment import AppriseAttachment
@ -130,12 +132,19 @@ class NotifyBase(URLBase):
# of lines. Setting this to zero disables this feature.
body_max_line_count = 0
# Persistent storage default html settings
persistent_storage = True
# Default Notify Format
notify_format = NotifyFormat.TEXT
# Default Overflow Mode
overflow_mode = OverflowMode.UPSTREAM
# Our default is to no not use persistent storage beyond in-memory
# reference
storage_mode = PersistentStoreMode.MEMORY
# Default Emoji Interpretation
interpret_emojis = False
@ -197,6 +206,16 @@ class NotifyBase(URLBase):
# runtime.
'_lookup_default': 'interpret_emojis',
},
'store': {
'name': _('Persistent Storage'),
# Use Persistent Storage
'type': 'bool',
# Provide a default
'default': persistent_storage,
# look up default using the following parent class value at
# runtime.
'_lookup_default': 'persistent_storage',
},
})
#
@ -268,6 +287,9 @@ class NotifyBase(URLBase):
# are turned off (no user over-rides allowed)
#
# Our Persistent Storage object is initialized on demand
self.__store = None
# Take a default
self.interpret_emojis = self.asset.interpret_emojis
if 'emojis' in kwargs:
@ -301,6 +323,14 @@ class NotifyBase(URLBase):
# Provide override
self.overflow_mode = overflow
# Prepare our Persistent Storage switch
self.persistent_storage = parse_bool(
kwargs.get('store', NotifyBase.persistent_storage))
if not self.persistent_storage:
# Enforce the disabling of cache (ortherwise defaults are use)
self.url_identifier = False
self.__cached_url_identifier = None
def image_url(self, notify_type, logo=False, extension=None,
image_size=None):
"""
@ -726,6 +756,10 @@ class NotifyBase(URLBase):
'overflow': self.overflow_mode,
}
# Persistent Storage Setting
if self.persistent_storage != NotifyBase.persistent_storage:
params['store'] = 'yes' if self.persistent_storage else 'no'
params.update(super().url_parameters(*args, **kwargs))
# return default parameters
@ -778,6 +812,10 @@ class NotifyBase(URLBase):
# Allow emoji's override
if 'emojis' in results['qsd']:
results['emojis'] = parse_bool(results['qsd'].get('emojis'))
# Store our persistent storage boolean
if 'store' in results['qsd']:
results['store'] = results['qsd']['store']
return results
@ -798,3 +836,29 @@ class NotifyBase(URLBase):
should return the same set of results that parse_url() does.
"""
return None
@property
def store(self):
"""
Returns a pointer to our persistent store for use.
The best use cases are:
self.store.get('key')
self.store.set('key', 'value')
self.store.delete('key1', 'key2', ...)
You can also access the keys this way:
self.store['key']
And clear them:
del self.store['key']
"""
if self.__store is None:
# Initialize our persistent store for use
self.__store = PersistentStore(
namespace=self.url_id(),
path=self.asset.storage_path,
mode=self.asset.storage_mode)
return self.__store

View File

@ -341,6 +341,15 @@ class NotifyBoxcar(NotifyBase):
params=NotifyBoxcar.urlencode(params),
)
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (self.secure_protocol, self.access, self.secret)
def __len__(self):
"""
Returns the number of targets associated with this notification

View File

@ -413,6 +413,19 @@ class NotifyBulkSMS(NotifyBase):
for x in self.groups])),
params=NotifyBulkSMS.urlencode(params))
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (
self.secure_protocol,
self.user if self.user else None,
self.password if self.password else None,
)
def __len__(self):
"""
Returns the number of targets associated with this notification

View File

@ -304,6 +304,15 @@ class NotifyBulkVS(NotifyBase):
return not has_error
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (self.secure_protocol, self.source, self.user, self.password)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -378,6 +378,15 @@ class NotifyBurstSMS(NotifyBase):
[NotifyBurstSMS.quote(x, safe='') for x in self.targets]),
params=NotifyBurstSMS.urlencode(params))
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (self.secure_protocol, self.apikey, self.secret, self.source)
def __len__(self):
"""
Returns the number of targets associated with this notification

View File

@ -181,6 +181,15 @@ class NotifyChantify(NotifyBase):
params=NotifyChantify.urlencode(params),
)
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (self.secure_protocol, self.token)
@staticmethod
def parse_url(url):
"""

View File

@ -285,6 +285,15 @@ class NotifyClickSend(NotifyBase):
params=NotifyClickSend.urlencode(params),
)
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (self.secure_protocol, self.user, self.password)
def __len__(self):
"""
Returns the number of targets associated with this notification

View File

@ -272,62 +272,6 @@ class NotifyForm(NotifyBase):
return
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.
"""
# Define any URL parameters
params = {
'method': self.method,
}
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
# Append our headers into our parameters
params.update({'+{}'.format(k): v for k, v in self.headers.items()})
# Append our GET params into our parameters
params.update({'-{}'.format(k): v for k, v in self.params.items()})
# Append our payload extra's into our parameters
params.update(
{':{}'.format(k): v for k, v in self.payload_extras.items()})
params.update(
{':{}'.format(k): v for k, v in self.payload_overrides.items()})
if self.attach_as != self.attach_as_default:
# Provide Attach-As extension details
params['attach-as'] = self.attach_as
# Determine Authentication
auth = ''
if self.user and self.password:
auth = '{user}:{password}@'.format(
user=NotifyForm.quote(self.user, safe=''),
password=self.pprint(
self.password, privacy, mode=PrivacyMode.Secret, safe=''),
)
elif self.user:
auth = '{user}@'.format(
user=NotifyForm.quote(self.user, safe=''),
)
default_port = 443 if self.secure else 80
return '{schema}://{auth}{hostname}{port}{fullpath}?{params}'.format(
schema=self.secure_protocol if self.secure else self.protocol,
auth=auth,
# never encode hostname since we're expecting it to be a valid one
hostname=self.host,
port='' if self.port is None or self.port == default_port
else ':{}'.format(self.port),
fullpath=NotifyForm.quote(self.fullpath, safe='/')
if self.fullpath else '/',
params=NotifyForm.urlencode(params),
)
def send(self, body, title='', notify_type=NotifyType.INFO, attach=None,
**kwargs):
"""
@ -486,6 +430,76 @@ class NotifyForm(NotifyBase):
return True
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (
self.secure_protocol if self.secure else self.protocol,
self.user, self.password, self.host,
self.port if self.port else (443 if self.secure else 80),
self.fullpath.rstrip('/'),
)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.
"""
# Define any URL parameters
params = {
'method': self.method,
}
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
# Append our headers into our parameters
params.update({'+{}'.format(k): v for k, v in self.headers.items()})
# Append our GET params into our parameters
params.update({'-{}'.format(k): v for k, v in self.params.items()})
# Append our payload extra's into our parameters
params.update(
{':{}'.format(k): v for k, v in self.payload_extras.items()})
params.update(
{':{}'.format(k): v for k, v in self.payload_overrides.items()})
if self.attach_as != self.attach_as_default:
# Provide Attach-As extension details
params['attach-as'] = self.attach_as
# Determine Authentication
auth = ''
if self.user and self.password:
auth = '{user}:{password}@'.format(
user=NotifyForm.quote(self.user, safe=''),
password=self.pprint(
self.password, privacy, mode=PrivacyMode.Secret, safe=''),
)
elif self.user:
auth = '{user}@'.format(
user=NotifyForm.quote(self.user, safe=''),
)
default_port = 443 if self.secure else 80
return '{schema}://{auth}{hostname}{port}{fullpath}?{params}'.format(
schema=self.secure_protocol if self.secure else self.protocol,
auth=auth,
# never encode hostname since we're expecting it to be a valid one
hostname=self.host,
port='' if self.port is None or self.port == default_port
else ':{}'.format(self.port),
fullpath=NotifyForm.quote(self.fullpath, safe='/')
if self.fullpath else '/',
params=NotifyForm.urlencode(params),
)
@staticmethod
def parse_url(url):
"""

View File

@ -195,56 +195,6 @@ class NotifyJSON(NotifyBase):
return
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.
"""
# Define any URL parameters
params = {
'method': self.method,
}
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
# Append our headers into our parameters
params.update({'+{}'.format(k): v for k, v in self.headers.items()})
# Append our GET params into our parameters
params.update({'-{}'.format(k): v for k, v in self.params.items()})
# Append our payload extra's into our parameters
params.update(
{':{}'.format(k): v for k, v in self.payload_extras.items()})
# Determine Authentication
auth = ''
if self.user and self.password:
auth = '{user}:{password}@'.format(
user=NotifyJSON.quote(self.user, safe=''),
password=self.pprint(
self.password, privacy, mode=PrivacyMode.Secret, safe=''),
)
elif self.user:
auth = '{user}@'.format(
user=NotifyJSON.quote(self.user, safe=''),
)
default_port = 443 if self.secure else 80
return '{schema}://{auth}{hostname}{port}{fullpath}?{params}'.format(
schema=self.secure_protocol if self.secure else self.protocol,
auth=auth,
# never encode hostname since we're expecting it to be a valid one
hostname=self.host,
port='' if self.port is None or self.port == default_port
else ':{}'.format(self.port),
fullpath=NotifyJSON.quote(self.fullpath, safe='/')
if self.fullpath else '/',
params=NotifyJSON.urlencode(params),
)
def send(self, body, title='', notify_type=NotifyType.INFO, attach=None,
**kwargs):
"""
@ -395,6 +345,70 @@ class NotifyJSON(NotifyBase):
return True
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (
self.secure_protocol if self.secure else self.protocol,
self.user, self.password, self.host,
self.port if self.port else (443 if self.secure else 80),
self.fullpath.rstrip('/'),
)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.
"""
# Define any URL parameters
params = {
'method': self.method,
}
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
# Append our headers into our parameters
params.update({'+{}'.format(k): v for k, v in self.headers.items()})
# Append our GET params into our parameters
params.update({'-{}'.format(k): v for k, v in self.params.items()})
# Append our payload extra's into our parameters
params.update(
{':{}'.format(k): v for k, v in self.payload_extras.items()})
# Determine Authentication
auth = ''
if self.user and self.password:
auth = '{user}:{password}@'.format(
user=NotifyJSON.quote(self.user, safe=''),
password=self.pprint(
self.password, privacy, mode=PrivacyMode.Secret, safe=''),
)
elif self.user:
auth = '{user}@'.format(
user=NotifyJSON.quote(self.user, safe=''),
)
default_port = 443 if self.secure else 80
return '{schema}://{auth}{hostname}{port}{fullpath}?{params}'.format(
schema=self.secure_protocol if self.secure else self.protocol,
auth=auth,
# never encode hostname since we're expecting it to be a valid one
hostname=self.host,
port='' if self.port is None or self.port == default_port
else ':{}'.format(self.port),
fullpath=NotifyJSON.quote(self.fullpath, safe='/')
if self.fullpath else '/',
params=NotifyJSON.urlencode(params),
)
@staticmethod
def parse_url(url):
"""

View File

@ -242,58 +242,6 @@ class NotifyXML(NotifyBase):
return
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.
"""
# Define any URL parameters
params = {
'method': self.method,
}
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
# Append our headers into our parameters
params.update({'+{}'.format(k): v for k, v in self.headers.items()})
# Append our GET params into our parameters
params.update({'-{}'.format(k): v for k, v in self.params.items()})
# Append our payload extra's into our parameters
params.update(
{':{}'.format(k): v for k, v in self.payload_extras.items()})
params.update(
{':{}'.format(k): v for k, v in self.payload_overrides.items()})
# Determine Authentication
auth = ''
if self.user and self.password:
auth = '{user}:{password}@'.format(
user=NotifyXML.quote(self.user, safe=''),
password=self.pprint(
self.password, privacy, mode=PrivacyMode.Secret, safe=''),
)
elif self.user:
auth = '{user}@'.format(
user=NotifyXML.quote(self.user, safe=''),
)
default_port = 443 if self.secure else 80
return '{schema}://{auth}{hostname}{port}{fullpath}?{params}'.format(
schema=self.secure_protocol if self.secure else self.protocol,
auth=auth,
# never encode hostname since we're expecting it to be a valid one
hostname=self.host,
port='' if self.port is None or self.port == default_port
else ':{}'.format(self.port),
fullpath=NotifyXML.quote(self.fullpath, safe='/')
if self.fullpath else '/',
params=NotifyXML.urlencode(params),
)
def send(self, body, title='', notify_type=NotifyType.INFO, attach=None,
**kwargs):
"""
@ -467,6 +415,72 @@ class NotifyXML(NotifyBase):
return True
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (
self.secure_protocol if self.secure else self.protocol,
self.user, self.password, self.host,
self.port if self.port else (443 if self.secure else 80),
self.fullpath.rstrip('/'),
)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.
"""
# Define any URL parameters
params = {
'method': self.method,
}
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
# Append our headers into our parameters
params.update({'+{}'.format(k): v for k, v in self.headers.items()})
# Append our GET params into our parameters
params.update({'-{}'.format(k): v for k, v in self.params.items()})
# Append our payload extra's into our parameters
params.update(
{':{}'.format(k): v for k, v in self.payload_extras.items()})
params.update(
{':{}'.format(k): v for k, v in self.payload_overrides.items()})
# Determine Authentication
auth = ''
if self.user and self.password:
auth = '{user}:{password}@'.format(
user=NotifyXML.quote(self.user, safe=''),
password=self.pprint(
self.password, privacy, mode=PrivacyMode.Secret, safe=''),
)
elif self.user:
auth = '{user}@'.format(
user=NotifyXML.quote(self.user, safe=''),
)
default_port = 443 if self.secure else 80
return '{schema}://{auth}{hostname}{port}{fullpath}?{params}'.format(
schema=self.secure_protocol if self.secure else self.protocol,
auth=auth,
# never encode hostname since we're expecting it to be a valid one
hostname=self.host,
port='' if self.port is None or self.port == default_port
else ':{}'.format(self.port),
fullpath=NotifyXML.quote(self.fullpath, safe='/')
if self.fullpath else '/',
params=NotifyXML.urlencode(params),
)
@staticmethod
def parse_url(url):
"""

View File

@ -354,6 +354,15 @@ class NotifyD7Networks(NotifyBase):
[NotifyD7Networks.quote(x, safe='') for x in self.targets]),
params=NotifyD7Networks.urlencode(params))
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (self.secure_protocol, self.token)
def __len__(self):
"""
Returns the number of targets associated with this notification

View File

@ -346,6 +346,15 @@ class NotifyDapnet(NotifyBase):
params=NotifyDapnet.urlencode(params),
)
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (self.secure_protocol, self.user, self.password)
def __len__(self):
"""
Returns the number of targets associated with this notification

View File

@ -173,7 +173,6 @@ class NotifyDBus(NotifyBase):
# object if we were to reference, we wouldn't be backwards compatible with
# Python v2. So converting the result set back into a list makes us
# compatible
# TODO: Review after dropping support for Python 2.
protocol = list(MAINLOOP_MAP.keys())
# A URL that takes you to the setup/help of the specific protocol
@ -196,6 +195,10 @@ class NotifyDBus(NotifyBase):
dbus_interface = 'org.freedesktop.Notifications'
dbus_setting_location = '/org/freedesktop/Notifications'
# No URL Identifier will be defined for this service as there simply isn't
# enough details to uniquely identify one dbus:// from another.
url_identifier = False
# Define object templates
templates = (
'{schema}://',

View File

@ -310,6 +310,15 @@ class NotifyDingTalk(NotifyBase):
[NotifyDingTalk.quote(x, safe='') for x in self.targets]),
args=NotifyDingTalk.urlencode(args))
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (self.secure_protocol, self.secret, self.token)
def __len__(self):
"""
Returns the number of targets associated with this notification

View File

@ -607,6 +607,15 @@ class NotifyDiscord(NotifyBase):
params=NotifyDiscord.urlencode(params),
)
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (self.secure_protocol, self.webhook_id, self.webhook_token)
@staticmethod
def parse_url(url):
"""

View File

@ -1031,6 +1031,20 @@ class NotifyEmail(NotifyBase):
params=NotifyEmail.urlencode(params),
)
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (
self.secure_protocol if self.secure else self.protocol,
self.user, self.password, self.host,
self.port if self.port
else SECURE_MODES[self.secure_mode]['default_port'],
)
def __len__(self):
"""
Returns the number of targets associated with this notification

View File

@ -593,6 +593,18 @@ class NotifyEmby(NotifyBase):
return not has_error
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (
self.secure_protocol, self.user, self.password, self.host,
self.port if self.port else (443 if self.secure else 80),
)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -181,6 +181,20 @@ class NotifyEnigma2(NotifyBase):
return
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (
self.secure_protocol,
self.user, self.password, self.host,
self.port if self.port else (443 if self.secure else 80),
self.fullpath.rstrip('/'),
)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -507,6 +507,15 @@ class NotifyFCM(NotifyBase):
return not has_error
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (self.secure_protocol, self.mode, self.apikey, self.project)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -192,6 +192,15 @@ class NotifyFeishu(NotifyBase):
return True
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (self.secure_protocol, self.token)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -308,6 +308,15 @@ class NotifyFlock(NotifyBase):
return not has_error
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (self.secure_protocol, self.token)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -103,6 +103,15 @@ class NotifyFreeMobile(NotifyBase):
return
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (self.secure_protocol, self.user, self.password)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -132,6 +132,10 @@ class NotifyGnome(NotifyBase):
# cause any title (if defined) to get placed into the message body.
title_maxlen = 0
# No URL Identifier will be defined for this service as there simply isn't
# enough details to uniquely identify one dbus:// from another.
url_identifier = False
# Define object templates
templates = (
'{schema}://',

View File

@ -265,6 +265,18 @@ class NotifyGoogleChat(NotifyBase):
return True
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (
self.secure_protocol, self.workspace, self.webhook_key,
self.webhook_token,
)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -265,6 +265,20 @@ class NotifyGotify(NotifyBase):
return True
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (
self.secure_protocol if self.secure else self.protocol,
self.user, self.password, self.host,
self.port if self.port else (443 if self.secure else 80),
self.fullpath.rstrip('/'),
)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -338,6 +338,19 @@ class NotifyGrowl(NotifyBase):
return True
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (
self.secure_protocol if self.secure else self.protocol,
self.user, self.password, self.host,
self.port if self.port else self.default_port,
)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -179,8 +179,8 @@ class NotifyHomeAssistant(NotifyBase):
if isinstance(self.port, int):
url += ':%d' % self.port
url += '' if not self.fullpath else '/' + self.fullpath.strip('/')
url += '/api/services/persistent_notification/create'
url += self.fullpath.rstrip('/') + \
'/api/services/persistent_notification/create'
self.logger.debug('Home Assistant POST URL: %s (cert_verify=%r)' % (
url, self.verify_certificate,
@ -231,6 +231,22 @@ class NotifyHomeAssistant(NotifyBase):
return True
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (
self.secure_protocol if self.secure else self.protocol,
self.user, self.password, self.host,
self.port if self.port else (
443 if self.secure else self.default_insecure_port),
self.fullpath.rstrip('/'),
self.accesstoken,
)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.
@ -302,7 +318,7 @@ class NotifyHomeAssistant(NotifyBase):
results['accesstoken'] = fullpath.pop() if fullpath else None
# Re-assemble our full path
results['fullpath'] = '/'.join(fullpath)
results['fullpath'] = '/' + '/'.join(fullpath) if fullpath else ''
# Allow the specification of a unique notification_id so that
# it will always replace the last one sent.

View File

@ -253,6 +253,15 @@ class NotifyHttpSMS(NotifyBase):
return not has_error
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (self.secure_protocol, self.source, self.apikey)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -287,6 +287,15 @@ class NotifyIFTTT(NotifyBase):
return not has_error
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (self.secure_protocol, self.webhook_id)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -345,6 +345,15 @@ class NotifyJoin(NotifyBase):
return not has_error
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (self.secure_protocol, self.apikey)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -304,6 +304,15 @@ class NotifyKavenegar(NotifyBase):
return not has_error
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (self.secure_protocol, self.source, self.apikey)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -198,6 +198,15 @@ class NotifyKumulos(NotifyBase):
return False
return True
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (self.secure_protocol, self.apikey, self.serverkey)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -783,6 +783,29 @@ class NotifyLametric(NotifyBase):
return True
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
if self.mode == LametricMode.DEVICE:
return (
self.secure_protocol if self.secure else self.protocol,
self.user, self.lametric_apikey, self.host,
self.port if self.port else (
443 if self.secure else
self.template_tokens['port']['default']),
)
return (
self.protocol,
self.lametric_app_access_token,
self.lametric_app_id,
self.lametric_app_ver,
)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.
@ -871,6 +894,9 @@ class NotifyLametric(NotifyBase):
results['password'] = results['user']
results['user'] = None
# Get unquoted entries
entries = NotifyLametric.split_path(results['fullpath'])
# Priority Handling
if 'priority' in results['qsd'] and results['qsd']['priority']:
results['priority'] = NotifyLametric.unquote(
@ -913,6 +939,10 @@ class NotifyLametric(NotifyBase):
results['app_ver'] = \
NotifyLametric.unquote(results['qsd']['app_ver'])
elif entries:
# Store our app id
results['app_ver'] = entries.pop(0)
if 'token' in results['qsd'] and results['qsd']['token']:
# Extract Application Access Token from an argument
results['app_token'] = \

View File

@ -241,6 +241,15 @@ class NotifyLine(NotifyBase):
return not has_error
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (self.secure_protocol, self.token)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -324,6 +324,24 @@ class NotifyLunaSea(NotifyBase):
return not has_error
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
secure = self.secure_protocol[0] \
if self.mode == LunaSeaMode.CLOUD else (
self.secure_protocol[0] if self.secure else self.protocol[0])
return (
secure,
self.host if self.mode == LunaSeaMode.PRIVATE else None,
self.port if self.port else (443 if self.secure else 80),
self.user if self.user else None,
self.password if self.password else None,
)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -92,6 +92,10 @@ class NotifyMacOSX(NotifyBase):
# content to display
body_max_line_count = 10
# No URL Identifier will be defined for this service as there simply isn't
# enough details to uniquely identify one dbus:// from another.
url_identifier = False
# The possible paths to the terminal-notifier
notify_paths = (
'/opt/homebrew/bin/terminal-notifier',

View File

@ -579,6 +579,17 @@ class NotifyMailgun(NotifyBase):
return not has_error
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (
self.secure_protocol, self.host, self.apikey, self.region_name,
)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -336,6 +336,18 @@ class NotifyMastodon(NotifyBase):
return
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (
self.secure_protocol[0], self.token, self.host,
self.port if self.port else (443 if self.secure else 80),
)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -42,6 +42,7 @@ from ..url import PrivacyMode
from ..common import NotifyType
from ..common import NotifyImageSize
from ..common import NotifyFormat
from ..common import PersistentStoreMode
from ..utils import parse_bool
from ..utils import parse_list
from ..utils import is_hostname
@ -175,6 +176,13 @@ class NotifyMatrix(NotifyBase):
# the server doesn't remind us how long we shoul wait for
default_wait_ms = 1000
# Our default is to no not use persistent storage beyond in-memory
# reference
storage_mode = PersistentStoreMode.AUTO
# Keep our cache for 20 days
default_cache_expiry_sec = 60 * 60 * 24 * 20
# Define object templates
templates = (
# Targets are ignored when using t2bot mode; only a token is required
@ -299,10 +307,6 @@ class NotifyMatrix(NotifyBase):
# Place an image inline with the message body
self.include_image = include_image
# maintain a lookup of room alias's we already paired with their id
# to speed up future requests
self._room_cache = {}
# Setup our mode
self.mode = self.template_args['mode']['default'] \
if not isinstance(mode, str) else mode.lower()
@ -342,6 +346,7 @@ class NotifyMatrix(NotifyBase):
.format(self.host)
self.logger.warning(msg)
raise TypeError(msg)
else:
# Verify port if specified
if self.port is not None and not (
@ -353,6 +358,23 @@ class NotifyMatrix(NotifyBase):
self.logger.warning(msg)
raise TypeError(msg)
#
# Initialize from cache if present
#
if self.mode != MatrixWebhookMode.T2BOT:
# our home server gets populated after a login/registration
self.home_server = self.store.get('home_server')
# our user_id gets populated after a login/registration
self.user_id = self.store.get('user_id')
# This gets initialized after a login/registration
self.access_token = self.store.get('access_token')
# This gets incremented for each request made against the v3 API
self.transaction_id = 0 if not self.access_token \
else self.store.get('transaction_id', 0)
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
"""
Perform Matrix Notification
@ -695,6 +717,9 @@ class NotifyMatrix(NotifyBase):
# recognized as retransmissions and ignored
if self.version == MatrixVersion.V3:
self.transaction_id += 1
self.store.set(
'transaction_id', self.transaction_id,
expires=self.default_cache_expiry_sec)
if not postokay:
# Notify our user
@ -811,7 +836,18 @@ class NotifyMatrix(NotifyBase):
self.home_server = response.get('home_server')
self.user_id = response.get('user_id')
self.store.set(
'access_token', self.access_token,
expires=self.default_cache_expiry_sec)
self.store.set(
'home_server', self.home_server,
expires=self.default_cache_expiry_sec)
self.store.set(
'user_id', self.user_id,
expires=self.default_cache_expiry_sec)
if self.access_token is not None:
# Store our token into our store
self.logger.debug(
'Registered successfully with Matrix server.')
return True
@ -870,6 +906,18 @@ class NotifyMatrix(NotifyBase):
self.logger.debug(
'Authenticated successfully with Matrix server.')
# Store our token into our store
self.store.set(
'access_token', self.access_token,
expires=self.default_cache_expiry_sec)
self.store.set(
'home_server', self.home_server,
expires=self.default_cache_expiry_sec)
self.store.set(
'user_id', self.user_id,
expires=self.default_cache_expiry_sec)
return True
def _logout(self):
@ -907,8 +955,9 @@ class NotifyMatrix(NotifyBase):
self.home_server = None
self.user_id = None
# Clear our room cache
self._room_cache = {}
# clear our tokens
self.store.clear(
'access_token', 'home_server', 'user_id', 'transaction_id')
self.logger.debug(
'Unauthenticated successfully with Matrix server.')
@ -948,9 +997,13 @@ class NotifyMatrix(NotifyBase):
)
# Check our cache for speed:
if room_id in self._room_cache:
try:
# We're done as we've already joined the channel
return self._room_cache[room_id]['id']
return self.store[room_id]['id']
except KeyError:
# No worries, we'll try to acquire the info
pass
# Build our URL
path = '/join/{}'.format(NotifyMatrix.quote(room_id))
@ -959,10 +1012,10 @@ class NotifyMatrix(NotifyBase):
postokay, _ = self._fetch(path, payload=payload)
if postokay:
# Cache our entry for fast access later
self._room_cache[room_id] = {
self.store.set(room_id, {
'id': room_id,
'home_server': home_server,
}
})
return room_id if postokay else None
@ -984,9 +1037,13 @@ class NotifyMatrix(NotifyBase):
room = '#{}:{}'.format(result.group('room'), home_server)
# Check our cache for speed:
if room in self._room_cache:
try:
# We're done as we've already joined the channel
return self._room_cache[room]['id']
return self.store[room]['id']
except KeyError:
# No worries, we'll try to acquire the info
pass
# If we reach here, we need to join the channel
@ -997,11 +1054,12 @@ class NotifyMatrix(NotifyBase):
postokay, response = self._fetch(path, payload=payload)
if postokay:
# Cache our entry for fast access later
self._room_cache[room] = {
self.store.set(room, {
'id': response.get('room_id'),
'home_server': home_server,
}
return self._room_cache[room]['id']
})
return response.get('room_id')
# Try to create the channel
return self._room_create(room)
@ -1056,10 +1114,10 @@ class NotifyMatrix(NotifyBase):
return None
# Cache our entry for fast access later
self._room_cache[response.get('room_alias')] = {
self.store.set(response.get('room_alias'), {
'id': response.get('room_id'),
'home_server': home_server,
}
})
return response.get('room_id')
@ -1292,6 +1350,11 @@ class NotifyMatrix(NotifyBase):
# nothing to do
return
if self.store.mode != PersistentStoreMode.MEMORY:
# We no longer have to log out as we have persistant storage to
# re-use our credentials with
return
try:
self._logout()
@ -1336,6 +1399,22 @@ class NotifyMatrix(NotifyBase):
# the end user if we don't have to.
pass
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (
self.secure_protocol if self.secure else self.protocol,
self.host if self.mode != MatrixWebhookMode.T2BOT
else self.access_token,
self.port if self.port else (443 if self.secure else 80),
self.user if self.mode != MatrixWebhookMode.T2BOT else None,
self.password if self.mode != MatrixWebhookMode.T2BOT else None,
)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -223,8 +223,8 @@ class NotifyMattermost(NotifyBase):
payload['channel'] = channel
url = '{}://{}:{}{}/hooks/{}'.format(
self.schema, self.host, self.port, self.fullpath,
self.token)
self.schema, self.host, self.port,
self.fullpath.rstrip('/'), self.token)
self.logger.debug('Mattermost POST URL: %s (cert_verify=%r)' % (
url, self.verify_certificate,
@ -286,6 +286,18 @@ class NotifyMattermost(NotifyBase):
# Return our overall status
return not has_error
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (
self.secure_protocol if self.secure else self.protocol,
self.token, self.host, self.port, self.fullpath,
)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -291,6 +291,15 @@ class NotifyMessageBird(NotifyBase):
return not has_error
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (self.secure_protocol, self.apikey, self.source)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -191,6 +191,18 @@ class NotifyMisskey(NotifyBase):
return
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (
self.secure_protocol if self.secure else self.protocol,
self.token, self.host, self.port,
)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -429,6 +429,23 @@ class NotifyMQTT(NotifyBase):
return not has_error
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (
self.secure_protocol if self.secure else self.protocol,
self.user, self.password, self.host,
self.port if self.port else (
self.mqtt_secure_port if self.secure
else self.mqtt_insecure_port),
self.fullpath.rstrip('/'),
self.client_id,
)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -310,6 +310,15 @@ class NotifyMSG91(NotifyBase):
return True
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (self.secure_protocol, self.template, self.authkey)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -468,6 +468,19 @@ class NotifyMSTeams(NotifyBase):
return True
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (
self.secure_protocol,
self.team if self.version > 1 else None,
self.token_a, self.token_b, self.token_c,
)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -278,6 +278,18 @@ class NotifyNextcloud(NotifyBase):
return not has_error
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (
self.secure_protocol if self.secure else self.protocol,
self.user, self.password, self.host, self.port,
)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -253,6 +253,18 @@ class NotifyNextcloudTalk(NotifyBase):
return not has_error
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (
self.secure_protocol if self.secure else self.protocol,
self.user, self.password, self.host, self.port,
)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -278,6 +278,19 @@ class NotifyNotica(NotifyBase):
return True
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (
self.secure_protocol if self.secure else self.protocol,
self.mode, self.token, self.user, self.password, self.host,
self.port,
)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -199,6 +199,18 @@ class NotifyNotifiarr(NotifyBase):
return
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (
self.secure_protocol if self.secure else self.protocol,
self.apikey,
)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -197,6 +197,15 @@ class NotifyNotifico(NotifyBase):
)
return
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (self.secure_protocol, self.project_id, self.msghook)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -656,6 +656,34 @@ class NotifyNtfy(NotifyBase):
return False, response
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
kwargs = [
self.secure_protocol if self.mode == NtfyMode.CLOUD else (
self.secure_protocol if self.secure else self.protocol),
self.host if self.mode == NtfyMode.PRIVATE else '',
443 if self.mode == NtfyMode.CLOUD else (
self.port if self.port else (443 if self.secure else 80)),
]
if self.mode == NtfyMode.PRIVATE:
if self.auth == NtfyAuth.BASIC:
kwargs.extend([
self.user if self.user else None,
self.password if self.password else None,
])
elif self.token: # NtfyAuth.TOKEN also
kwargs.append(self.token)
return kwargs
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -558,6 +558,18 @@ class NotifyOffice365(NotifyBase):
return (True, content)
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (
self.secure_protocol, self.email, self.tenant, self.client_id,
self.secret,
)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -474,6 +474,17 @@ class NotifyOneSignal(NotifyBase):
return not has_error
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (
self.secure_protocol, self.template_id, self.app, self.apikey,
)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -47,10 +47,12 @@
# API Integration Docs: https://docs.opsgenie.com/docs/api-integration
import requests
from json import dumps
from json import dumps, loads
import hashlib
from .base import NotifyBase
from ..common import NotifyType
from ..common import NotifyType, NOTIFY_TYPES
from ..common import PersistentStoreMode
from ..utils import validate_regex
from ..utils import is_uuid
from ..utils import parse_list
@ -76,6 +78,47 @@ OPSGENIE_CATEGORIES = (
)
class OpsgenieAlertAction:
"""
Defines the supported actions
"""
# Use mapping (specify :key=arg to over-ride)
MAP = 'map'
# Create new alert (default)
NEW = 'new'
# Close Alert
CLOSE = 'close'
# Delete Alert
DELETE = 'delete'
# Acknowledge Alert
ACKNOWLEDGE = 'acknowledge'
# Add note to alert
NOTE = 'note'
OPSGENIE_ACTIONS = (
OpsgenieAlertAction.MAP,
OpsgenieAlertAction.NEW,
OpsgenieAlertAction.CLOSE,
OpsgenieAlertAction.DELETE,
OpsgenieAlertAction.ACKNOWLEDGE,
OpsgenieAlertAction.NOTE,
)
# Map all support Apprise Categories to Opsgenie Categories
OPSGENIE_ALERT_MAP = {
NotifyType.INFO: OpsgenieAlertAction.CLOSE,
NotifyType.SUCCESS: OpsgenieAlertAction.CLOSE,
NotifyType.WARNING: OpsgenieAlertAction.NEW,
NotifyType.FAILURE: OpsgenieAlertAction.NEW,
}
# Regions
class OpsgenieRegion:
US = 'us'
@ -160,6 +203,10 @@ class NotifyOpsgenie(NotifyBase):
# The maximum length of the body
body_maxlen = 15000
# Our default is to no not use persistent storage beyond in-memory
# reference
storage_mode = PersistentStoreMode.AUTO
# If we don't have the specified min length, then we don't bother using
# the body directive
opsgenie_body_minlen = 130
@ -170,10 +217,24 @@ class NotifyOpsgenie(NotifyBase):
# The maximum allowable targets within a notification
default_batch_size = 50
# Defines our default message mapping
opsgenie_message_map = {
# Add a note to existing alert
NotifyType.INFO: OpsgenieAlertAction.NOTE,
# Close existing alert
NotifyType.SUCCESS: OpsgenieAlertAction.CLOSE,
# Create notice
NotifyType.WARNING: OpsgenieAlertAction.NEW,
# Create notice
NotifyType.FAILURE: OpsgenieAlertAction.NEW,
}
# Define object templates
templates = (
'{schema}://{apikey}',
'{schema}://{user}@{apikey}',
'{schema}://{apikey}/{targets}',
'{schema}://{user}@{apikey}/{targets}',
)
# Define our template tokens
@ -184,6 +245,10 @@ class NotifyOpsgenie(NotifyBase):
'private': True,
'required': True,
},
'user': {
'name': _('Username'),
'type': 'string',
},
'target_escalation': {
'name': _('Target Escalation'),
'prefix': '^',
@ -249,6 +314,12 @@ class NotifyOpsgenie(NotifyBase):
'to': {
'alias_of': 'targets',
},
'action': {
'name': _('Action'),
'type': 'choice:string',
'values': OPSGENIE_ACTIONS,
'default': OPSGENIE_ACTIONS[0],
}
})
# Map of key-value pairs to use as custom properties of the alert.
@ -257,11 +328,15 @@ class NotifyOpsgenie(NotifyBase):
'name': _('Details'),
'prefix': '+',
},
'mapping': {
'name': _('Action Mapping'),
'prefix': ':',
},
}
def __init__(self, apikey, targets, region_name=None, details=None,
priority=None, alias=None, entity=None, batch=False,
tags=None, **kwargs):
tags=None, action=None, mapping=None, **kwargs):
"""
Initialize Opsgenie Object
"""
@ -298,6 +373,41 @@ class NotifyOpsgenie(NotifyBase):
self.logger.warning(msg)
raise TypeError(msg)
if action and isinstance(action, str):
self.action = next(
(a for a in OPSGENIE_ACTIONS if a.startswith(action)), None)
if self.action not in OPSGENIE_ACTIONS:
msg = 'The Opsgenie action specified ({}) is invalid.'\
.format(action)
self.logger.warning(msg)
raise TypeError(msg)
else:
self.action = self.template_args['action']['default']
# Store our mappings
self.mapping = self.opsgenie_message_map.copy()
if mapping and isinstance(mapping, dict):
for _k, _v in mapping.items():
# Get our mapping
k = next((t for t in NOTIFY_TYPES if t.startswith(_k)), None)
if not k:
msg = 'The Opsgenie mapping key specified ({}) ' \
'is invalid.'.format(_k)
self.logger.warning(msg)
raise TypeError(msg)
_v_lower = _v.lower()
v = next((v for v in OPSGENIE_ACTIONS[1:]
if v.startswith(_v_lower)), None)
if not v:
msg = 'The Opsgenie mapping value (assigned to {}) ' \
'specified ({}) is invalid.'.format(k, _v)
self.logger.warning(msg)
raise TypeError(msg)
# Update our mapping
self.mapping[k] = v
self.details = {}
if details:
# Store our extra details
@ -367,115 +477,234 @@ class NotifyOpsgenie(NotifyBase):
if is_uuid(target) else
{'type': OpsgenieCategory.USER, 'username': target})
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
def _fetch(self, method, url, payload, params=None):
"""
Perform Opsgenie Notification
Performs server retrieval/update and returns JSON Response
"""
headers = {
'User-Agent': self.app_id,
'Content-Type': 'application/json',
'Authorization': 'GenieKey {}'.format(self.apikey),
}
# Some Debug Logging
self.logger.debug(
'Opsgenie POST URL: {} (cert_verify={})'.format(
url, self.verify_certificate))
self.logger.debug('Opsgenie Payload: {}' .format(payload))
# Initialize our response object
content = {}
# Always call throttle before any remote server i/o is made
self.throttle()
try:
r = method(
url,
data=dumps(payload),
params=params,
headers=headers,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
# A Response might look like:
# {
# "result": "Request will be processed",
# "took": 0.302,
# "requestId": "43a29c5c-3dbf-4fa4-9c26-f4f71023e120"
# }
try:
# Update our response object
content = loads(r.content)
except (AttributeError, TypeError, ValueError):
# ValueError = r.content is Unparsable
# TypeError = r.content is None
# AttributeError = r is None
content = {}
if r.status_code not in (
requests.codes.accepted, requests.codes.ok):
status_str = \
NotifyBase.http_response_code_lookup(
r.status_code)
self.logger.warning(
'Failed to send Opsgenie notification:'
'{}{}error={}.'.format(
status_str,
', ' if status_str else '',
r.status_code))
self.logger.debug(
'Response Details:\r\n{}'.format(r.content))
return (False, content.get('requestId'))
# If we reach here; the message was sent
self.logger.info('Sent Opsgenie notification')
self.logger.debug(
'Response Details:\r\n{}'.format(r.content))
return (True, content.get('requestId'))
except requests.RequestException as e:
self.logger.warning(
'A Connection error occurred sending Opsgenie '
'notification.')
self.logger.debug('Socket Exception: %s' % str(e))
return (False, content.get('requestId'))
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
"""
Perform Opsgenie Notification
"""
# Get our Opsgenie Action
action = OPSGENIE_ALERT_MAP[notify_type] \
if self.action == OpsgenieAlertAction.MAP else self.action
# Prepare our URL as it's based on our hostname
notify_url = OPSGENIE_API_LOOKUP[self.region_name]
# Initialize our has_error flag
has_error = False
# Use body if title not set
title_body = body if not title else title
# Default method is to post
method = requests.post
# Create a copy ouf our details object
details = self.details.copy()
if 'type' not in details:
details['type'] = notify_type
# For indexing in persistent store
key = hashlib.sha1(
(self.entity if self.entity else (
self.alias if self.alias else (
title if title else self.app_id)))
.encode('utf-8')).hexdigest()[0:10]
# Prepare our payload
payload = {
'source': self.app_desc,
'message': title_body,
'description': body,
'details': details,
'priority': 'P{}'.format(self.priority),
}
# Get our Opsgenie Request IDs
request_ids = self.store.get(key, [])
if not isinstance(request_ids, list):
request_ids = []
# Use our body directive if we exceed the minimum message
# limitation
if len(payload['message']) > self.opsgenie_body_minlen:
payload['message'] = '{}...'.format(
title_body[:self.opsgenie_body_minlen - 3])
if action == OpsgenieAlertAction.NEW:
# Create a copy ouf our details object
details = self.details.copy()
if 'type' not in details:
details['type'] = notify_type
if self.__tags:
payload['tags'] = self.__tags
# Use body if title not set
title_body = body if not title else title
if self.entity:
payload['entity'] = self.entity
# Prepare our payload
payload = {
'source': self.app_desc,
'message': title_body,
'description': body,
'details': details,
'priority': 'P{}'.format(self.priority),
}
if self.alias:
payload['alias'] = self.alias
# Use our body directive if we exceed the minimum message
# limitation
if len(payload['message']) > self.opsgenie_body_minlen:
payload['message'] = '{}...'.format(
title_body[:self.opsgenie_body_minlen - 3])
length = len(self.targets) if self.targets else 1
for index in range(0, length, self.batch_size):
if self.targets:
# If there were no targets identified, then we simply
# just iterate once without the responders set
payload['responders'] = \
self.targets[index:index + self.batch_size]
if self.__tags:
payload['tags'] = self.__tags
# Some Debug Logging
self.logger.debug(
'Opsgenie POST URL: {} (cert_verify={})'.format(
notify_url, self.verify_certificate))
self.logger.debug('Opsgenie Payload: {}' .format(payload))
if self.entity:
payload['entity'] = self.entity
# Always call throttle before any remote server i/o is made
self.throttle()
try:
r = requests.post(
notify_url,
data=dumps(payload),
headers=headers,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if self.alias:
payload['alias'] = self.alias
if r.status_code not in (
requests.codes.accepted, requests.codes.ok):
status_str = \
NotifyBase.http_response_code_lookup(
r.status_code)
if self.user:
payload['user'] = self.user
self.logger.warning(
'Failed to send Opsgenie notification:'
'{}{}error={}.'.format(
status_str,
', ' if status_str else '',
r.status_code))
# reset our request IDs - we will re-populate them
request_ids = []
self.logger.debug(
'Response Details:\r\n{}'.format(r.content))
length = len(self.targets) if self.targets else 1
for index in range(0, length, self.batch_size):
if self.targets:
# If there were no targets identified, then we simply
# just iterate once without the responders set
payload['responders'] = \
self.targets[index:index + self.batch_size]
# Mark our failure
# Perform our post
success, request_id = self._fetch(
method, notify_url, payload)
if success and request_id:
# Save our response
request_ids.append(request_id)
else:
has_error = True
continue
# If we reach here; the message was sent
self.logger.info('Sent Opsgenie notification')
self.logger.debug(
'Response Details:\r\n{}'.format(r.content))
# Store our entries for a maximum of 60 days
self.store.set(key, request_ids, expires=60 * 60 * 24 * 60)
except requests.RequestException as e:
self.logger.warning(
'A Connection error occurred sending Opsgenie '
'notification.')
self.logger.debug('Socket Exception: %s' % str(e))
# Mark our failure
has_error = True
elif request_ids:
# Prepare our payload
payload = {
'source': self.app_desc,
'note': body,
}
if self.user:
payload['user'] = self.user
# Prepare our Identifier type
params = {
'identifierType': 'id',
}
for request_id in request_ids:
if action == OpsgenieAlertAction.DELETE:
# Update our URL
url = f'{notify_url}/{request_id}'
method = requests.delete
elif action == OpsgenieAlertAction.ACKNOWLEDGE:
url = f'{notify_url}/{request_id}/acknowledge'
elif action == OpsgenieAlertAction.CLOSE:
url = f'{notify_url}/{request_id}/close'
else: # action == OpsgenieAlertAction.CLOSE:
url = f'{notify_url}/{request_id}/notes'
# Perform our post
success, _ = self._fetch(method, url, payload, params)
if not success:
has_error = True
if not has_error and action == OpsgenieAlertAction.DELETE:
# Remove cached entry
self.store.clear(key)
else:
self.logger.info(
'No Opsgenie notification sent due to (nothing to %s) '
'condition', self.action)
return not has_error
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (self.secure_protocol, self.region_name, self.apikey)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.
@ -483,6 +712,7 @@ class NotifyOpsgenie(NotifyBase):
# Define any URL parameters
params = {
'action': self.action,
'region': self.region_name,
'priority':
OPSGENIE_PRIORITIES[self.template_args['priority']['default']]
@ -506,6 +736,10 @@ class NotifyOpsgenie(NotifyBase):
# Append our details into our parameters
params.update({'+{}'.format(k): v for k, v in self.details.items()})
# Append our assignment extra's into our parameters
params.update(
{':{}'.format(k): v for k, v in self.mapping.items()})
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
@ -522,8 +756,9 @@ class NotifyOpsgenie(NotifyBase):
NotifyOpsgenie.template_tokens['target_team']['prefix'],
}
return '{schema}://{apikey}/{targets}/?{params}'.format(
return '{schema}://{user}{apikey}/{targets}/?{params}'.format(
schema=self.secure_protocol,
user='{}@'.format(self.user) if self.user else '',
apikey=self.pprint(self.apikey, privacy, safe=''),
targets='/'.join(
[NotifyOpsgenie.quote('{}{}'.format(
@ -608,4 +843,14 @@ class NotifyOpsgenie(NotifyBase):
if 'to' in results['qsd'] and len(results['qsd']['to']):
results['targets'].append(results['qsd']['to'])
# Store our action (if defined)
if 'action' in results['qsd'] and len(results['qsd']['action']):
results['action'] = \
NotifyOpsgenie.unquote(results['qsd']['action'])
# store any custom mapping defined
results['mapping'] = \
{NotifyOpsgenie.unquote(x): NotifyOpsgenie.unquote(y)
for x, y in results['qsd:'].items()}
return results

View File

@ -412,6 +412,18 @@ class NotifyPagerDuty(NotifyBase):
return True
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (
self.secure_protocol, self.integration_key, self.apikey,
self.source,
)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -299,6 +299,15 @@ class NotifyPagerTree(NotifyBase):
return True
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (self.secure_protocol, self.integration)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -257,6 +257,19 @@ class NotifyParsePlatform(NotifyBase):
return True
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (
self.secure_protocol if self.secure else self.protocol,
self.application_id, self.master_key, self.host, self.port,
self.fullpath,
)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -242,6 +242,15 @@ class NotifyPopcornNotify(NotifyBase):
return not has_error
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (self.secure_protocol, self.apikey)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -250,6 +250,15 @@ class NotifyProwl(NotifyBase):
return True
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (self.secure_protocol, self.apikey, self.providerkey)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -386,6 +386,15 @@ class NotifyPushBullet(NotifyBase):
if files:
files['file'][1].close()
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (self.secure_protocol, self.accesstoken)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -180,6 +180,18 @@ class NotifyPushDeer(NotifyBase):
return True
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (
self.secure_protocol if self.secure else self.protocol,
self.push_key, self.host, self.port,
)
def url(self, privacy=False):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -303,6 +303,15 @@ class NotifyPushed(NotifyBase):
return True
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (self.secure_protocol, self.app_key, self.app_secret)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -117,6 +117,18 @@ class NotifyPushjet(NotifyBase):
return
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (
self.secure_protocol if self.secure else self.protocol,
self.user, self.password, self.host, self.port, self.secret_key,
)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -171,6 +171,15 @@ class NotifyPushMe(NotifyBase):
return True
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (self.secure_protocol, self.token)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -549,6 +549,15 @@ class NotifyPushover(NotifyBase):
return True
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (self.secure_protocol, self.user_key, self.token)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -756,6 +756,18 @@ class NotifyPushSafer(NotifyBase):
return False, response
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (
self.secure_protocol if self.secure else self.protocol,
self.privatekey,
)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -310,6 +310,15 @@ class NotifyPushy(NotifyBase):
return not has_error
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (self.secure_protocol, self.apikey)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -324,6 +324,18 @@ class NotifyReddit(NotifyBase):
return
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (
self.secure_protocol, self.client_id, self.client_secret,
self.user, self.password,
)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -354,6 +354,15 @@ class NotifyRevolt(NotifyBase):
return (True, content)
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (self.secure_protocol, self.bot_token)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -319,6 +319,23 @@ class NotifyRocketChat(NotifyBase):
return
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (
self.secure_protocol if self.secure else self.protocol,
self.host,
self.port if self.port else (443 if self.secure else 80),
self.user,
self.password if self.mode in (
RocketChatAuthMode.BASIC, RocketChatAuthMode.TOKEN)
else self.webhook,
)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -300,6 +300,19 @@ class NotifyRSyslog(NotifyBase):
return True
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (
self.protocol, self.host,
self.port if self.port
else self.template_tokens['port']['default'],
)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -272,6 +272,15 @@ class NotifyRyver(NotifyBase):
return True
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (self.secure_protocol, self.organization, self.token)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -243,6 +243,15 @@ class NotifySendGrid(NotifyBase):
return
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (self.secure_protocol, self.apikey, self.from_email)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -149,6 +149,15 @@ class NotifyServerChan(NotifyBase):
return True
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (self.secure_protocol, self.token)
def url(self, privacy=False):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -770,6 +770,18 @@ class NotifySES(NotifyBase):
return response
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (
self.secure_protocol, self.from_addr, self.aws_access_key_id,
self.aws_secret_access_key, self.aws_region_name,
)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -356,6 +356,18 @@ class NotifySFR(NotifyBase):
return not has_error
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (
self.secure_protocol if self.secure else self.protocol,
self.user, self.password, self.space_id,
)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -372,6 +372,18 @@ class NotifySignalAPI(NotifyBase):
return not has_error
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (
self.secure_protocol if self.secure else self.protocol,
self.user, self.password, self.host, self.port, self.source,
)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -283,6 +283,15 @@ class NotifySimplePush(NotifyBase):
return True
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (self.secure_protocol, self.user, self.password, self.apikey)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -381,6 +381,18 @@ class NotifySinch(NotifyBase):
return not has_error
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (
self.secure_protocol if self.secure else self.protocol,
self.service_plan_id, self.api_token, self.source,
)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -326,6 +326,7 @@ class NotifySlack(NotifyBase):
self.mode = SlackMode.BOT if access_token else SlackMode.WEBHOOK
if self.mode is SlackMode.WEBHOOK:
self.access_token = None
self.token_a = validate_regex(
token_a, *self.template_tokens['token_a']['regex'])
if not self.token_a:
@ -350,6 +351,9 @@ class NotifySlack(NotifyBase):
self.logger.warning(msg)
raise TypeError(msg)
else:
self.token_a = None
self.token_b = None
self.token_c = None
self.access_token = validate_regex(
access_token, *self.template_tokens['access_token']['regex'])
if not self.access_token:
@ -1018,6 +1022,18 @@ class NotifySlack(NotifyBase):
# Return the response for processing
return response
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (
self.secure_protocol, self.token_a, self.token_b, self.token_c,
self.access_token,
)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -564,6 +564,18 @@ class NotifySMSEagle(NotifyBase):
return not has_error
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (
self.secure_protocol if self.secure else self.protocol,
self.token, self.host, self.port,
)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -314,6 +314,15 @@ class NotifySMSManager(NotifyBase):
return not has_error
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (self.secure_protocol[0], self.apikey)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -464,6 +464,15 @@ class NotifySMTP2Go(NotifyBase):
return not has_error
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (self.secure_protocol, self.user, self.host, self.apikey)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

View File

@ -573,6 +573,18 @@ class NotifySNS(NotifyBase):
return response
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (
self.secure_protocol, self.aws_access_key_id,
self.aws_secret_access_key, self.aws_region_name,
)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.

Some files were not shown because too many files have changed in this diff Show More