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: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL

View File

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

View File

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

115
README.md
View File

@ -534,9 +534,121 @@ aobj.add('foobar://')
# Send our notification out through our foobar:// # Send our notification out through our foobar://
aobj.notify("test") 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). 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? # 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: 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) * 🔧 [Troubleshooting](https://github.com/caronc/apprise/wiki/Troubleshooting)
* ⚙️ [Configuration File Help](https://github.com/caronc/apprise/wiki/config) * ⚙️ [Configuration File Help](https://github.com/caronc/apprise/wiki/config)
* ⚡ [Create Your Own Custom Notifications](https://github.com/caronc/apprise/wiki/decorator_notify) * ⚡ [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) * 🌎 [Apprise API/Web Interface](https://github.com/caronc/apprise-api)
* 🎉 [Showcase](https://github.com/caronc/apprise/wiki/showcase) * 🎉 [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 CONTENT_INCLUDE_MODES
from .common import ContentLocation from .common import ContentLocation
from .common import CONTENT_LOCATIONS from .common import CONTENT_LOCATIONS
from .common import PersistentStoreMode
from .common import PERSISTENT_STORE_MODES
from .url import URLBase from .url import URLBase
from .url import PrivacyMode from .url import PrivacyMode
from .plugins.base import NotifyBase from .plugins.base import NotifyBase
from .config.base import ConfigBase from .config.base import ConfigBase
from .attachment.base import AttachBase from .attachment.base import AttachBase
from . import exception
from .apprise import Apprise from .apprise import Apprise
from .locale import AppriseLocale from .locale import AppriseLocale
from .asset import AppriseAsset from .asset import AppriseAsset
from .persistent_store import PersistentStore
from .apprise_config import AppriseConfig from .apprise_config import AppriseConfig
from .apprise_attachment import AppriseAttachment from .apprise_attachment import AppriseAttachment
from .manager_attachment import AttachmentManager from .manager_attachment import AttachmentManager
@ -77,6 +81,10 @@ __all__ = [
# Core # Core
'Apprise', 'AppriseAsset', 'AppriseConfig', 'AppriseAttachment', 'URLBase', 'Apprise', 'AppriseAsset', 'AppriseConfig', 'AppriseAttachment', 'URLBase',
'NotifyBase', 'ConfigBase', 'AttachBase', 'AppriseLocale', 'NotifyBase', 'ConfigBase', 'AttachBase', 'AppriseLocale',
'PersistentStore',
# Exceptions
'exception',
# Reference # Reference
'NotifyType', 'NotifyImageSize', 'NotifyFormat', 'OverflowMode', 'NotifyType', 'NotifyImageSize', 'NotifyFormat', 'OverflowMode',
@ -84,6 +92,7 @@ __all__ = [
'ConfigFormat', 'CONFIG_FORMATS', 'ConfigFormat', 'CONFIG_FORMATS',
'ContentIncludeMode', 'CONTENT_INCLUDE_MODES', 'ContentIncludeMode', 'CONTENT_INCLUDE_MODES',
'ContentLocation', 'CONTENT_LOCATIONS', 'ContentLocation', 'CONTENT_LOCATIONS',
'PersistentStoreMode', 'PERSISTENT_STORE_MODES',
'PrivacyMode', 'PrivacyMode',
# Managers # Managers

View File

@ -33,6 +33,7 @@ from os.path import dirname
from os.path import isfile from os.path import isfile
from os.path import abspath from os.path import abspath
from .common import NotifyType from .common import NotifyType
from .common import PersistentStoreMode
from .manager_plugins import NotificationManager from .manager_plugins import NotificationManager
@ -157,6 +158,22 @@ class AppriseAsset:
# By default, no paths are scanned. # By default, no paths are scanned.
__plugin_paths = [] __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 (_) # All internal/system flags are prefixed with an underscore (_)
# These can only be initialized using Python libraries and are not picked # These can only be initialized using Python libraries and are not picked
# up from (yaml) configuration files (if set) # up from (yaml) configuration files (if set)
@ -171,7 +188,9 @@ class AppriseAsset:
# A unique identifer we can use to associate our calling source # A unique identifer we can use to associate our calling source
_uid = str(uuid4()) _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 Asset Initialization
@ -187,8 +206,49 @@ class AppriseAsset:
if plugin_paths: if plugin_paths:
# Load any decorated modules if defined # Load any decorated modules if defined
self.__plugin_paths = plugin_paths
N_MGR.module_detection(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): def color(self, notify_type, color_type=None):
""" """
Returns an HTML mapped color based on passed in notify type Returns an HTML mapped color based on passed in notify type
@ -356,3 +416,40 @@ class AppriseAsset:
""" """
return int(value.lstrip('#'), 16) 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 re
import os import os
from .base import AttachBase from .base import AttachBase
from ..utils import path_decode
from ..common import ContentLocation from ..common import ContentLocation
from ..locale import gettext_lazy as _ 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 # Store path but mark it dirty since we have not performed any
# verification at this point. # 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 return
def url(self, privacy=False, *args, **kwargs): def url(self, privacy=False, *args, **kwargs):
@ -77,7 +81,7 @@ class AttachFile(AttachBase):
params['name'] = self._name params['name'] = self._name
return 'file://{path}{params}'.format( return 'file://{path}{params}'.format(
path=self.quote(self.dirty_path), path=self.quote(self.__original_path),
params='?{}'.format(self.urlencode(params, safe='/')) params='?{}'.format(self.urlencode(params, safe='/'))
if params else '', if params else '',
) )

View File

@ -27,26 +27,27 @@
# POSSIBILITY OF SUCH DAMAGE. # POSSIBILITY OF SUCH DAMAGE.
import click import click
import textwrap
import logging import logging
import platform import platform
import sys import sys
import os import os
import shutil
import re import re
from os.path import isfile from os.path import isfile
from os.path import exists 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 Apprise
from . import AppriseAsset from . import AppriseAsset
from . import AppriseConfig 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_TYPES
from .common import NOTIFY_FORMATS from .common import NOTIFY_FORMATS
from .common import PERSISTENT_STORE_MODES
from .common import PersistentStoreState
from .common import ContentLocation from .common import ContentLocation
from .logger import logger from .logger import logger
@ -104,67 +105,94 @@ DEFAULT_PLUGIN_PATHS = (
'/var/lib/apprise/plugins', '/var/lib/apprise/plugins',
) )
#
# Persistent Storage
#
DEFAULT_STORAGE_PATH = '~/.local/share/apprise/cache'
# Detect Windows # Detect Windows
if platform.system() == 'Windows': if platform.system() == 'Windows':
# Default Config Search Path for Windows Users # Default Config Search Path for Windows Users
DEFAULT_CONFIG_PATHS = ( DEFAULT_CONFIG_PATHS = (
expandvars('%APPDATA%\\Apprise\\apprise'), '%APPDATA%\\Apprise\\apprise',
expandvars('%APPDATA%\\Apprise\\apprise.conf'), '%APPDATA%\\Apprise\\apprise.conf',
expandvars('%APPDATA%\\Apprise\\apprise.yml'), '%APPDATA%\\Apprise\\apprise.yml',
expandvars('%APPDATA%\\Apprise\\apprise.yaml'), '%APPDATA%\\Apprise\\apprise.yaml',
expandvars('%LOCALAPPDATA%\\Apprise\\apprise'), '%LOCALAPPDATA%\\Apprise\\apprise',
expandvars('%LOCALAPPDATA%\\Apprise\\apprise.conf'), '%LOCALAPPDATA%\\Apprise\\apprise.conf',
expandvars('%LOCALAPPDATA%\\Apprise\\apprise.yml'), '%LOCALAPPDATA%\\Apprise\\apprise.yml',
expandvars('%LOCALAPPDATA%\\Apprise\\apprise.yaml'), '%LOCALAPPDATA%\\Apprise\\apprise.yaml',
# #
# Global Support # Global Support
# #
# C:\ProgramData\Apprise # C:\ProgramData\Apprise
expandvars('%ALLUSERSPROFILE%\\Apprise\\apprise'), '%ALLUSERSPROFILE%\\Apprise\\apprise',
expandvars('%ALLUSERSPROFILE%\\Apprise\\apprise.conf'), '%ALLUSERSPROFILE%\\Apprise\\apprise.conf',
expandvars('%ALLUSERSPROFILE%\\Apprise\\apprise.yml'), '%ALLUSERSPROFILE%\\Apprise\\apprise.yml',
expandvars('%ALLUSERSPROFILE%\\Apprise\\apprise.yaml'), '%ALLUSERSPROFILE%\\Apprise\\apprise.yaml',
# C:\Program Files\Apprise # C:\Program Files\Apprise
expandvars('%PROGRAMFILES%\\Apprise\\apprise'), '%PROGRAMFILES%\\Apprise\\apprise',
expandvars('%PROGRAMFILES%\\Apprise\\apprise.conf'), '%PROGRAMFILES%\\Apprise\\apprise.conf',
expandvars('%PROGRAMFILES%\\Apprise\\apprise.yml'), '%PROGRAMFILES%\\Apprise\\apprise.yml',
expandvars('%PROGRAMFILES%\\Apprise\\apprise.yaml'), '%PROGRAMFILES%\\Apprise\\apprise.yaml',
# C:\Program Files\Common Files # C:\Program Files\Common Files
expandvars('%COMMONPROGRAMFILES%\\Apprise\\apprise'), '%COMMONPROGRAMFILES%\\Apprise\\apprise',
expandvars('%COMMONPROGRAMFILES%\\Apprise\\apprise.conf'), '%COMMONPROGRAMFILES%\\Apprise\\apprise.conf',
expandvars('%COMMONPROGRAMFILES%\\Apprise\\apprise.yml'), '%COMMONPROGRAMFILES%\\Apprise\\apprise.yml',
expandvars('%COMMONPROGRAMFILES%\\Apprise\\apprise.yaml'), '%COMMONPROGRAMFILES%\\Apprise\\apprise.yaml',
) )
# Default Plugin Search Path for Windows Users # Default Plugin Search Path for Windows Users
DEFAULT_PLUGIN_PATHS = ( DEFAULT_PLUGIN_PATHS = (
expandvars('%APPDATA%\\Apprise\\plugins'), '%APPDATA%\\Apprise\\plugins',
expandvars('%LOCALAPPDATA%\\Apprise\\plugins'), '%LOCALAPPDATA%\\Apprise\\plugins',
# #
# Global Support # Global Support
# #
# C:\ProgramData\Apprise\plugins # C:\ProgramData\Apprise\plugins
expandvars('%ALLUSERSPROFILE%\\Apprise\\plugins'), '%ALLUSERSPROFILE%\\Apprise\\plugins',
# C:\Program Files\Apprise\plugins # C:\Program Files\Apprise\plugins
expandvars('%PROGRAMFILES%\\Apprise\\plugins'), '%PROGRAMFILES%\\Apprise\\plugins',
# C:\Program Files\Common Files # 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: Persistent Storage Modes
click.echo(command.get_help(ctx)) """
# 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(): def print_version_msg():
@ -180,7 +208,106 @@ def print_version_msg():
click.echo('\n'.join(result)) 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, @click.option('--body', '-b', default=None, type=str,
help='Specify the message body. If no body is specified then ' help='Specify the message body. If no body is specified then '
'content is read from <stdin>.') 'content is read from <stdin>.')
@ -190,23 +317,43 @@ def print_version_msg():
@click.option('--plugin-path', '-P', default=None, type=str, multiple=True, @click.option('--plugin-path', '-P', default=None, type=str, multiple=True,
metavar='PLUGIN_PATH', metavar='PLUGIN_PATH',
help='Specify one or more plugin paths to scan.') 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, @click.option('--config', '-c', default=None, type=str, multiple=True,
metavar='CONFIG_URL', metavar='CONFIG_URL',
help='Specify one or more configuration locations.') help='Specify one or more configuration locations.')
@click.option('--attach', '-a', default=None, type=str, multiple=True, @click.option('--attach', '-a', default=None, type=str, multiple=True,
metavar='ATTACHMENT_URL', metavar='ATTACHMENT_URL',
help='Specify one or more attachment.') 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', metavar='TYPE',
help='Specify the message type (default={}). ' help='Specify the message type (default={}). '
'Possible values are "{}", and "{}".'.format( 'Possible values are "{}", and "{}".'.format(
NotifyType.INFO, '", "'.join(NOTIFY_TYPES[:-1]), NOTIFY_TYPES[0], '", "'.join(NOTIFY_TYPES[:-1]),
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', metavar='FORMAT',
help='Specify the message input format (default={}). ' help='Specify the message input format (default={}). '
'Possible values are "{}", and "{}".'.format( 'Possible values are "{}", and "{}".'.format(
NotifyFormat.TEXT, '", "'.join(NOTIFY_FORMATS[:-1]), NOTIFY_FORMATS[0], '", "'.join(NOTIFY_FORMATS[:-1]),
NOTIFY_FORMATS[-1])) NOTIFY_FORMATS[-1]))
@click.option('--theme', '-T', default='default', type=str, metavar='THEME', @click.option('--theme', '-T', default='default', type=str, metavar='THEME',
help='Specify the default theme.') help='Specify the default theme.')
@ -241,10 +388,12 @@ def print_version_msg():
help='Display the apprise version and exit.') help='Display the apprise version and exit.')
@click.argument('urls', nargs=-1, @click.argument('urls', nargs=-1,
metavar='SERVER_URL [SERVER_URL2 [SERVER_URL3]]',) 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, input_format, dry_run, recursion_depth, verbose, disable_async,
details, interpret_escapes, interpret_emojis, plugin_path, debug, details, interpret_escapes, interpret_emojis, plugin_path,
version): storage_path, storage_mode, storage_prune_days, storage_uid_length,
debug, version):
""" """
Send a notification to all of the specified servers identified by their Send a notification to all of the specified servers identified by their
URLs the content provided within the title, body and notification-type. 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 use them, check out at https://github.com/caronc/apprise
""" """
# Note: Click ignores the return values of functions it wraps, If you # 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. # as you will see below.
debug = True if debug else False debug = True if debug else False
@ -297,7 +446,7 @@ def main(body, title, config, attach, urls, notification_type, theme, tag,
if version: if version:
print_version_msg() print_version_msg()
sys.exit(0) ctx.exit(0)
# Simple Error Checking # Simple Error Checking
notification_type = notification_type.strip().lower() notification_type = notification_type.strip().lower()
@ -307,7 +456,7 @@ def main(body, title, config, attach, urls, notification_type, theme, tag,
.format(notification_type)) .format(notification_type))
# 2 is the same exit code returned by Click if there is a parameter # 2 is the same exit code returned by Click if there is a parameter
# issue. For consistency, we also return a 2 # issue. For consistency, we also return a 2
sys.exit(2) ctx.exit(2)
input_format = input_format.strip().lower() input_format = input_format.strip().lower()
if input_format not in NOTIFY_FORMATS: 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)) .format(input_format))
# 2 is the same exit code returned by Click if there is a parameter # 2 is the same exit code returned by Click if there is a parameter
# issue. For consistency, we also return a 2 # 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: if not plugin_path:
# Prepare a default set of plugin path # Prepare a default set of plugin path
plugin_path = \ plugin_path = \
next((path for path in DEFAULT_PLUGIN_PATHS [path for path in DEFAULT_PLUGIN_PATHS
if exists(expanduser(path))), None) 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 # Prepare our asset
asset = AppriseAsset( asset = AppriseAsset(
@ -346,6 +513,15 @@ def main(body, title, config, attach, urls, notification_type, theme, tag,
# Load our plugins # Load our plugins
plugin_paths=plugin_path, 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 # Create our Apprise object
@ -429,7 +605,7 @@ def main(body, title, config, attach, urls, notification_type, theme, tag,
# new line padding between entries # new line padding between entries
click.echo() click.echo()
sys.exit(0) ctx.exit(0)
# end if details() # end if details()
# The priorities of what is accepted are parsed in order below: # 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 # 4. Configuration by environment variable: APPRISE_CONFIG
# 5. Default Configuration File(s) (if found) # 5. Default Configuration File(s) (if found)
# #
if urls: elif urls and not 'storage'.startswith(urls[0]):
if tag: if tag:
# Ignore any tags specified # Ignore any tags specified
logger.warning( logger.warning(
@ -483,20 +659,145 @@ def main(body, title, config, attach, urls, notification_type, theme, tag,
else: else:
# Load default configuration # Load default configuration
a.add(AppriseConfig( 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)) asset=asset, recursion=recursion_depth))
if len(a) == 0 and not urls: if len(a) == 0 and not urls:
logger.error( logger.error(
'You must specify at least one server URL or populated ' 'You must specify at least one server URL or populated '
'configuration file.') 'configuration file.')
print_help_msg(main) click.echo(ctx.get_help())
sys.exit(1) ctx.exit(1)
# each --tag entry comprises of a comma separated 'and' list # each --tag entry comprises of a comma separated 'and' list
# we or each of of the --tag and sets specified. # we or each of of the --tag and sets specified.
tags = None if not tag else [parse_list(t) for t in tag] 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 not dry_run:
if body is None: if body is None:
logger.trace('No --body (-b) specified; reading from stdin') 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, body=body, title=title, notify_type=notification_type, tag=tags,
attach=attach) attach=attach)
else: else:
# Number of rows to assume in the terminal. In future, maybe this can # Number of columns to assume in the terminal. In future, maybe this
# be detected and made dynamic. The actual row count is 80, but 5 # can be detected and made dynamic. The actual column count is 80, but
# characters are already reserved for the counter on the left # 5 characters are already reserved for the counter on the left
rows = 75 (columns, _) = shutil.get_terminal_size(fallback=(80, 24))
# Initialize our URL response; This is populated within the for/loop # 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 # 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)): for idx, server in enumerate(a.find(tag=tags)):
url = server.url(privacy=True) url = server.url(privacy=True)
click.echo("{: 3d}. {}".format( click.echo("{: 4d}. {}".format(
idx + 1, 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: 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 # Initialize a default response of nothing matched, otherwise
# if we matched at least one entry, we can return True # 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 # Exit code 3 is used since Click uses exit code 2 if there is an
# error with the parameters specified # error with the parameters specified
sys.exit(3) ctx.exit(3)
elif result is False: elif result is False:
# At least 1 notification service failed to send # At least 1 notification service failed to send
sys.exit(1) ctx.exit(1)
# else: We're good! # else: We're good!
sys.exit(0) ctx.exit(0)

View File

@ -187,6 +187,42 @@ CONTENT_LOCATIONS = (
ContentLocation.INACCESSIBLE, 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 # This is a reserved tag that is automatically assigned to every
# Notification Plugin # Notification Plugin
MATCH_ALL_TAG = 'all' MATCH_ALL_TAG = 'all'

View File

@ -29,6 +29,7 @@
import re import re
import os import os
from .base import ConfigBase from .base import ConfigBase
from ..utils import path_decode
from ..common import ConfigFormat from ..common import ConfigFormat
from ..common import ContentIncludeMode from ..common import ContentIncludeMode
from ..locale import gettext_lazy as _ from ..locale import gettext_lazy as _
@ -59,7 +60,10 @@ class ConfigFile(ConfigBase):
super().__init__(**kwargs) super().__init__(**kwargs)
# Store our file path as it was set # 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 # Update the config path to be relative to our file we just loaded
self.config_path = os.path.dirname(self.path) self.config_path = os.path.dirname(self.path)
@ -89,7 +93,7 @@ class ConfigFile(ConfigBase):
params['format'] = self.config_format params['format'] = self.config_format
return 'file://{path}{params}'.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 '', 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 import_module
from .utils import Singleton from .utils import Singleton
from .utils import parse_list from .utils import parse_list
from .utils import path_decode
from os.path import dirname from os.path import dirname
from os.path import abspath from os.path import abspath
from os.path import join from os.path import join
@ -373,7 +374,7 @@ class PluginManager(metaclass=Singleton):
return return
for _path in paths: 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) \ if (cache and path in self._paths_previously_scanned) \
or not os.path.exists(path): or not os.path.exists(path):
# We're done as we've already scanned this # 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 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -729,6 +729,15 @@ class NotifyAprs(NotifyBase):
params=NotifyAprs.urlencode(params), 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): def __len__(self):
""" """
Returns the number of targets associated with this notification Returns the number of targets associated with this notification

View File

@ -395,6 +395,18 @@ class NotifyBark(NotifyBase):
return not has_error 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. 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 NOTIFY_FORMATS
from ..common import OverflowMode from ..common import OverflowMode
from ..common import OVERFLOW_MODES from ..common import OVERFLOW_MODES
from ..common import PersistentStoreMode
from ..locale import gettext_lazy as _ from ..locale import gettext_lazy as _
from ..persistent_store import PersistentStore
from ..apprise_attachment import AppriseAttachment from ..apprise_attachment import AppriseAttachment
@ -130,12 +132,19 @@ class NotifyBase(URLBase):
# of lines. Setting this to zero disables this feature. # of lines. Setting this to zero disables this feature.
body_max_line_count = 0 body_max_line_count = 0
# Persistent storage default html settings
persistent_storage = True
# Default Notify Format # Default Notify Format
notify_format = NotifyFormat.TEXT notify_format = NotifyFormat.TEXT
# Default Overflow Mode # Default Overflow Mode
overflow_mode = OverflowMode.UPSTREAM overflow_mode = OverflowMode.UPSTREAM
# Our default is to no not use persistent storage beyond in-memory
# reference
storage_mode = PersistentStoreMode.MEMORY
# Default Emoji Interpretation # Default Emoji Interpretation
interpret_emojis = False interpret_emojis = False
@ -197,6 +206,16 @@ class NotifyBase(URLBase):
# runtime. # runtime.
'_lookup_default': 'interpret_emojis', '_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) # are turned off (no user over-rides allowed)
# #
# Our Persistent Storage object is initialized on demand
self.__store = None
# Take a default # Take a default
self.interpret_emojis = self.asset.interpret_emojis self.interpret_emojis = self.asset.interpret_emojis
if 'emojis' in kwargs: if 'emojis' in kwargs:
@ -301,6 +323,14 @@ class NotifyBase(URLBase):
# Provide override # Provide override
self.overflow_mode = overflow 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, def image_url(self, notify_type, logo=False, extension=None,
image_size=None): image_size=None):
""" """
@ -726,6 +756,10 @@ class NotifyBase(URLBase):
'overflow': self.overflow_mode, '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)) params.update(super().url_parameters(*args, **kwargs))
# return default parameters # return default parameters
@ -778,6 +812,10 @@ class NotifyBase(URLBase):
# Allow emoji's override # Allow emoji's override
if 'emojis' in results['qsd']: if 'emojis' in results['qsd']:
results['emojis'] = parse_bool(results['qsd'].get('emojis')) 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 return results
@ -798,3 +836,29 @@ class NotifyBase(URLBase):
should return the same set of results that parse_url() does. should return the same set of results that parse_url() does.
""" """
return None 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), 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): def __len__(self):
""" """
Returns the number of targets associated with this notification Returns the number of targets associated with this notification

View File

@ -413,6 +413,19 @@ class NotifyBulkSMS(NotifyBase):
for x in self.groups])), for x in self.groups])),
params=NotifyBulkSMS.urlencode(params)) 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): def __len__(self):
""" """
Returns the number of targets associated with this notification Returns the number of targets associated with this notification

View File

@ -304,6 +304,15 @@ class NotifyBulkVS(NotifyBase):
return not has_error 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. 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]), [NotifyBurstSMS.quote(x, safe='') for x in self.targets]),
params=NotifyBurstSMS.urlencode(params)) 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): def __len__(self):
""" """
Returns the number of targets associated with this notification Returns the number of targets associated with this notification

View File

@ -181,6 +181,15 @@ class NotifyChantify(NotifyBase):
params=NotifyChantify.urlencode(params), 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 @staticmethod
def parse_url(url): def parse_url(url):
""" """

View File

@ -285,6 +285,15 @@ class NotifyClickSend(NotifyBase):
params=NotifyClickSend.urlencode(params), 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): def __len__(self):
""" """
Returns the number of targets associated with this notification Returns the number of targets associated with this notification

View File

@ -272,62 +272,6 @@ class NotifyForm(NotifyBase):
return 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, def send(self, body, title='', notify_type=NotifyType.INFO, attach=None,
**kwargs): **kwargs):
""" """
@ -486,6 +430,76 @@ class NotifyForm(NotifyBase):
return True 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 @staticmethod
def parse_url(url): def parse_url(url):
""" """

View File

@ -195,56 +195,6 @@ class NotifyJSON(NotifyBase):
return 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, def send(self, body, title='', notify_type=NotifyType.INFO, attach=None,
**kwargs): **kwargs):
""" """
@ -395,6 +345,70 @@ class NotifyJSON(NotifyBase):
return True 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 @staticmethod
def parse_url(url): def parse_url(url):
""" """

View File

@ -242,58 +242,6 @@ class NotifyXML(NotifyBase):
return 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, def send(self, body, title='', notify_type=NotifyType.INFO, attach=None,
**kwargs): **kwargs):
""" """
@ -467,6 +415,72 @@ class NotifyXML(NotifyBase):
return True 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 @staticmethod
def parse_url(url): def parse_url(url):
""" """

View File

@ -354,6 +354,15 @@ class NotifyD7Networks(NotifyBase):
[NotifyD7Networks.quote(x, safe='') for x in self.targets]), [NotifyD7Networks.quote(x, safe='') for x in self.targets]),
params=NotifyD7Networks.urlencode(params)) 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): def __len__(self):
""" """
Returns the number of targets associated with this notification Returns the number of targets associated with this notification

View File

@ -346,6 +346,15 @@ class NotifyDapnet(NotifyBase):
params=NotifyDapnet.urlencode(params), 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): def __len__(self):
""" """
Returns the number of targets associated with this notification 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 # 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 # Python v2. So converting the result set back into a list makes us
# compatible # compatible
# TODO: Review after dropping support for Python 2.
protocol = list(MAINLOOP_MAP.keys()) protocol = list(MAINLOOP_MAP.keys())
# A URL that takes you to the setup/help of the specific protocol # 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_interface = 'org.freedesktop.Notifications'
dbus_setting_location = '/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 # Define object templates
templates = ( templates = (
'{schema}://', '{schema}://',

View File

@ -310,6 +310,15 @@ class NotifyDingTalk(NotifyBase):
[NotifyDingTalk.quote(x, safe='') for x in self.targets]), [NotifyDingTalk.quote(x, safe='') for x in self.targets]),
args=NotifyDingTalk.urlencode(args)) 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): def __len__(self):
""" """
Returns the number of targets associated with this notification Returns the number of targets associated with this notification

View File

@ -607,6 +607,15 @@ class NotifyDiscord(NotifyBase):
params=NotifyDiscord.urlencode(params), 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 @staticmethod
def parse_url(url): def parse_url(url):
""" """

View File

@ -1031,6 +1031,20 @@ class NotifyEmail(NotifyBase):
params=NotifyEmail.urlencode(params), 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): def __len__(self):
""" """
Returns the number of targets associated with this notification Returns the number of targets associated with this notification

View File

@ -593,6 +593,18 @@ class NotifyEmby(NotifyBase):
return not has_error 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -181,6 +181,20 @@ class NotifyEnigma2(NotifyBase):
return 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -507,6 +507,15 @@ class NotifyFCM(NotifyBase):
return not has_error 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -192,6 +192,15 @@ class NotifyFeishu(NotifyBase):
return True 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -308,6 +308,15 @@ class NotifyFlock(NotifyBase):
return not has_error 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -103,6 +103,15 @@ class NotifyFreeMobile(NotifyBase):
return 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. 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. # cause any title (if defined) to get placed into the message body.
title_maxlen = 0 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 # Define object templates
templates = ( templates = (
'{schema}://', '{schema}://',

View File

@ -265,6 +265,18 @@ class NotifyGoogleChat(NotifyBase):
return True 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -265,6 +265,20 @@ class NotifyGotify(NotifyBase):
return True 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -338,6 +338,19 @@ class NotifyGrowl(NotifyBase):
return True 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -179,8 +179,8 @@ class NotifyHomeAssistant(NotifyBase):
if isinstance(self.port, int): if isinstance(self.port, int):
url += ':%d' % self.port url += ':%d' % self.port
url += '' if not self.fullpath else '/' + self.fullpath.strip('/') url += self.fullpath.rstrip('/') + \
url += '/api/services/persistent_notification/create' '/api/services/persistent_notification/create'
self.logger.debug('Home Assistant POST URL: %s (cert_verify=%r)' % ( self.logger.debug('Home Assistant POST URL: %s (cert_verify=%r)' % (
url, self.verify_certificate, url, self.verify_certificate,
@ -231,6 +231,22 @@ class NotifyHomeAssistant(NotifyBase):
return True 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.
@ -302,7 +318,7 @@ class NotifyHomeAssistant(NotifyBase):
results['accesstoken'] = fullpath.pop() if fullpath else None results['accesstoken'] = fullpath.pop() if fullpath else None
# Re-assemble our full path # 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 # Allow the specification of a unique notification_id so that
# it will always replace the last one sent. # it will always replace the last one sent.

View File

@ -253,6 +253,15 @@ class NotifyHttpSMS(NotifyBase):
return not has_error 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -287,6 +287,15 @@ class NotifyIFTTT(NotifyBase):
return not has_error 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -345,6 +345,15 @@ class NotifyJoin(NotifyBase):
return not has_error 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -304,6 +304,15 @@ class NotifyKavenegar(NotifyBase):
return not has_error 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -198,6 +198,15 @@ class NotifyKumulos(NotifyBase):
return False return False
return True 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -783,6 +783,29 @@ class NotifyLametric(NotifyBase):
return True 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.
@ -871,6 +894,9 @@ class NotifyLametric(NotifyBase):
results['password'] = results['user'] results['password'] = results['user']
results['user'] = None results['user'] = None
# Get unquoted entries
entries = NotifyLametric.split_path(results['fullpath'])
# Priority Handling # Priority Handling
if 'priority' in results['qsd'] and results['qsd']['priority']: if 'priority' in results['qsd'] and results['qsd']['priority']:
results['priority'] = NotifyLametric.unquote( results['priority'] = NotifyLametric.unquote(
@ -913,6 +939,10 @@ class NotifyLametric(NotifyBase):
results['app_ver'] = \ results['app_ver'] = \
NotifyLametric.unquote(results['qsd']['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']: if 'token' in results['qsd'] and results['qsd']['token']:
# Extract Application Access Token from an argument # Extract Application Access Token from an argument
results['app_token'] = \ results['app_token'] = \

View File

@ -241,6 +241,15 @@ class NotifyLine(NotifyBase):
return not has_error 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -324,6 +324,24 @@ class NotifyLunaSea(NotifyBase):
return not has_error 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -92,6 +92,10 @@ class NotifyMacOSX(NotifyBase):
# content to display # content to display
body_max_line_count = 10 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 # The possible paths to the terminal-notifier
notify_paths = ( notify_paths = (
'/opt/homebrew/bin/terminal-notifier', '/opt/homebrew/bin/terminal-notifier',

View File

@ -579,6 +579,17 @@ class NotifyMailgun(NotifyBase):
return not has_error 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -336,6 +336,18 @@ class NotifyMastodon(NotifyBase):
return 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. 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 NotifyType
from ..common import NotifyImageSize from ..common import NotifyImageSize
from ..common import NotifyFormat from ..common import NotifyFormat
from ..common import PersistentStoreMode
from ..utils import parse_bool from ..utils import parse_bool
from ..utils import parse_list from ..utils import parse_list
from ..utils import is_hostname from ..utils import is_hostname
@ -175,6 +176,13 @@ class NotifyMatrix(NotifyBase):
# the server doesn't remind us how long we shoul wait for # the server doesn't remind us how long we shoul wait for
default_wait_ms = 1000 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 # Define object templates
templates = ( templates = (
# Targets are ignored when using t2bot mode; only a token is required # 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 # Place an image inline with the message body
self.include_image = include_image 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 # Setup our mode
self.mode = self.template_args['mode']['default'] \ self.mode = self.template_args['mode']['default'] \
if not isinstance(mode, str) else mode.lower() if not isinstance(mode, str) else mode.lower()
@ -342,6 +346,7 @@ class NotifyMatrix(NotifyBase):
.format(self.host) .format(self.host)
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
else: else:
# Verify port if specified # Verify port if specified
if self.port is not None and not ( if self.port is not None and not (
@ -353,6 +358,23 @@ class NotifyMatrix(NotifyBase):
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(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): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
Perform Matrix Notification Perform Matrix Notification
@ -695,6 +717,9 @@ class NotifyMatrix(NotifyBase):
# recognized as retransmissions and ignored # recognized as retransmissions and ignored
if self.version == MatrixVersion.V3: if self.version == MatrixVersion.V3:
self.transaction_id += 1 self.transaction_id += 1
self.store.set(
'transaction_id', self.transaction_id,
expires=self.default_cache_expiry_sec)
if not postokay: if not postokay:
# Notify our user # Notify our user
@ -811,7 +836,18 @@ class NotifyMatrix(NotifyBase):
self.home_server = response.get('home_server') self.home_server = response.get('home_server')
self.user_id = response.get('user_id') 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: if self.access_token is not None:
# Store our token into our store
self.logger.debug( self.logger.debug(
'Registered successfully with Matrix server.') 'Registered successfully with Matrix server.')
return True return True
@ -870,6 +906,18 @@ class NotifyMatrix(NotifyBase):
self.logger.debug( self.logger.debug(
'Authenticated successfully with Matrix server.') '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 return True
def _logout(self): def _logout(self):
@ -907,8 +955,9 @@ class NotifyMatrix(NotifyBase):
self.home_server = None self.home_server = None
self.user_id = None self.user_id = None
# Clear our room cache # clear our tokens
self._room_cache = {} self.store.clear(
'access_token', 'home_server', 'user_id', 'transaction_id')
self.logger.debug( self.logger.debug(
'Unauthenticated successfully with Matrix server.') 'Unauthenticated successfully with Matrix server.')
@ -948,9 +997,13 @@ class NotifyMatrix(NotifyBase):
) )
# Check our cache for speed: # Check our cache for speed:
if room_id in self._room_cache: try:
# We're done as we've already joined the channel # 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 # Build our URL
path = '/join/{}'.format(NotifyMatrix.quote(room_id)) path = '/join/{}'.format(NotifyMatrix.quote(room_id))
@ -959,10 +1012,10 @@ class NotifyMatrix(NotifyBase):
postokay, _ = self._fetch(path, payload=payload) postokay, _ = self._fetch(path, payload=payload)
if postokay: if postokay:
# Cache our entry for fast access later # Cache our entry for fast access later
self._room_cache[room_id] = { self.store.set(room_id, {
'id': room_id, 'id': room_id,
'home_server': home_server, 'home_server': home_server,
} })
return room_id if postokay else None return room_id if postokay else None
@ -984,9 +1037,13 @@ class NotifyMatrix(NotifyBase):
room = '#{}:{}'.format(result.group('room'), home_server) room = '#{}:{}'.format(result.group('room'), home_server)
# Check our cache for speed: # Check our cache for speed:
if room in self._room_cache: try:
# We're done as we've already joined the channel # 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 # If we reach here, we need to join the channel
@ -997,11 +1054,12 @@ class NotifyMatrix(NotifyBase):
postokay, response = self._fetch(path, payload=payload) postokay, response = self._fetch(path, payload=payload)
if postokay: if postokay:
# Cache our entry for fast access later # Cache our entry for fast access later
self._room_cache[room] = { self.store.set(room, {
'id': response.get('room_id'), 'id': response.get('room_id'),
'home_server': home_server, 'home_server': home_server,
} })
return self._room_cache[room]['id']
return response.get('room_id')
# Try to create the channel # Try to create the channel
return self._room_create(room) return self._room_create(room)
@ -1056,10 +1114,10 @@ class NotifyMatrix(NotifyBase):
return None return None
# Cache our entry for fast access later # 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'), 'id': response.get('room_id'),
'home_server': home_server, 'home_server': home_server,
} })
return response.get('room_id') return response.get('room_id')
@ -1292,6 +1350,11 @@ class NotifyMatrix(NotifyBase):
# nothing to do # nothing to do
return 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: try:
self._logout() self._logout()
@ -1336,6 +1399,22 @@ class NotifyMatrix(NotifyBase):
# the end user if we don't have to. # the end user if we don't have to.
pass 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -223,8 +223,8 @@ class NotifyMattermost(NotifyBase):
payload['channel'] = channel payload['channel'] = channel
url = '{}://{}:{}{}/hooks/{}'.format( url = '{}://{}:{}{}/hooks/{}'.format(
self.schema, self.host, self.port, self.fullpath, self.schema, self.host, self.port,
self.token) self.fullpath.rstrip('/'), self.token)
self.logger.debug('Mattermost POST URL: %s (cert_verify=%r)' % ( self.logger.debug('Mattermost POST URL: %s (cert_verify=%r)' % (
url, self.verify_certificate, url, self.verify_certificate,
@ -286,6 +286,18 @@ class NotifyMattermost(NotifyBase):
# Return our overall status # Return our overall status
return not has_error 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -291,6 +291,15 @@ class NotifyMessageBird(NotifyBase):
return not has_error 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -191,6 +191,18 @@ class NotifyMisskey(NotifyBase):
return 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -429,6 +429,23 @@ class NotifyMQTT(NotifyBase):
return not has_error 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -310,6 +310,15 @@ class NotifyMSG91(NotifyBase):
return True 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -468,6 +468,19 @@ class NotifyMSTeams(NotifyBase):
return True 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -278,6 +278,18 @@ class NotifyNextcloud(NotifyBase):
return not has_error 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -253,6 +253,18 @@ class NotifyNextcloudTalk(NotifyBase):
return not has_error 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -278,6 +278,19 @@ class NotifyNotica(NotifyBase):
return True 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -199,6 +199,18 @@ class NotifyNotifiarr(NotifyBase):
return 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -197,6 +197,15 @@ class NotifyNotifico(NotifyBase):
) )
return 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -656,6 +656,34 @@ class NotifyNtfy(NotifyBase):
return False, response 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -558,6 +558,18 @@ class NotifyOffice365(NotifyBase):
return (True, content) 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -474,6 +474,17 @@ class NotifyOneSignal(NotifyBase):
return not has_error 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. 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 # API Integration Docs: https://docs.opsgenie.com/docs/api-integration
import requests import requests
from json import dumps from json import dumps, loads
import hashlib
from .base import NotifyBase 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 validate_regex
from ..utils import is_uuid from ..utils import is_uuid
from ..utils import parse_list 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 # Regions
class OpsgenieRegion: class OpsgenieRegion:
US = 'us' US = 'us'
@ -160,6 +203,10 @@ class NotifyOpsgenie(NotifyBase):
# The maximum length of the body # The maximum length of the body
body_maxlen = 15000 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 # If we don't have the specified min length, then we don't bother using
# the body directive # the body directive
opsgenie_body_minlen = 130 opsgenie_body_minlen = 130
@ -170,10 +217,24 @@ class NotifyOpsgenie(NotifyBase):
# The maximum allowable targets within a notification # The maximum allowable targets within a notification
default_batch_size = 50 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 # Define object templates
templates = ( templates = (
'{schema}://{apikey}', '{schema}://{apikey}',
'{schema}://{user}@{apikey}',
'{schema}://{apikey}/{targets}', '{schema}://{apikey}/{targets}',
'{schema}://{user}@{apikey}/{targets}',
) )
# Define our template tokens # Define our template tokens
@ -184,6 +245,10 @@ class NotifyOpsgenie(NotifyBase):
'private': True, 'private': True,
'required': True, 'required': True,
}, },
'user': {
'name': _('Username'),
'type': 'string',
},
'target_escalation': { 'target_escalation': {
'name': _('Target Escalation'), 'name': _('Target Escalation'),
'prefix': '^', 'prefix': '^',
@ -249,6 +314,12 @@ class NotifyOpsgenie(NotifyBase):
'to': { 'to': {
'alias_of': 'targets', '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. # Map of key-value pairs to use as custom properties of the alert.
@ -257,11 +328,15 @@ class NotifyOpsgenie(NotifyBase):
'name': _('Details'), 'name': _('Details'),
'prefix': '+', 'prefix': '+',
}, },
'mapping': {
'name': _('Action Mapping'),
'prefix': ':',
},
} }
def __init__(self, apikey, targets, region_name=None, details=None, def __init__(self, apikey, targets, region_name=None, details=None,
priority=None, alias=None, entity=None, batch=False, priority=None, alias=None, entity=None, batch=False,
tags=None, **kwargs): tags=None, action=None, mapping=None, **kwargs):
""" """
Initialize Opsgenie Object Initialize Opsgenie Object
""" """
@ -298,6 +373,41 @@ class NotifyOpsgenie(NotifyBase):
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(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 = {} self.details = {}
if details: if details:
# Store our extra details # Store our extra details
@ -367,115 +477,234 @@ class NotifyOpsgenie(NotifyBase):
if is_uuid(target) else if is_uuid(target) else
{'type': OpsgenieCategory.USER, 'username': target}) {'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 = { headers = {
'User-Agent': self.app_id, 'User-Agent': self.app_id,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': 'GenieKey {}'.format(self.apikey), '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 # Prepare our URL as it's based on our hostname
notify_url = OPSGENIE_API_LOOKUP[self.region_name] notify_url = OPSGENIE_API_LOOKUP[self.region_name]
# Initialize our has_error flag # Initialize our has_error flag
has_error = False has_error = False
# Use body if title not set # Default method is to post
title_body = body if not title else title method = requests.post
# Create a copy ouf our details object # For indexing in persistent store
details = self.details.copy() key = hashlib.sha1(
if 'type' not in details: (self.entity if self.entity else (
details['type'] = notify_type self.alias if self.alias else (
title if title else self.app_id)))
.encode('utf-8')).hexdigest()[0:10]
# Prepare our payload # Get our Opsgenie Request IDs
payload = { request_ids = self.store.get(key, [])
'source': self.app_desc, if not isinstance(request_ids, list):
'message': title_body, request_ids = []
'description': body,
'details': details,
'priority': 'P{}'.format(self.priority),
}
# Use our body directive if we exceed the minimum message if action == OpsgenieAlertAction.NEW:
# limitation # Create a copy ouf our details object
if len(payload['message']) > self.opsgenie_body_minlen: details = self.details.copy()
payload['message'] = '{}...'.format( if 'type' not in details:
title_body[:self.opsgenie_body_minlen - 3]) details['type'] = notify_type
if self.__tags: # Use body if title not set
payload['tags'] = self.__tags title_body = body if not title else title
if self.entity: # Prepare our payload
payload['entity'] = self.entity payload = {
'source': self.app_desc,
'message': title_body,
'description': body,
'details': details,
'priority': 'P{}'.format(self.priority),
}
if self.alias: # Use our body directive if we exceed the minimum message
payload['alias'] = self.alias # 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 if self.__tags:
for index in range(0, length, self.batch_size): payload['tags'] = self.__tags
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]
# Some Debug Logging if self.entity:
self.logger.debug( payload['entity'] = self.entity
'Opsgenie POST URL: {} (cert_verify={})'.format(
notify_url, self.verify_certificate))
self.logger.debug('Opsgenie Payload: {}' .format(payload))
# Always call throttle before any remote server i/o is made if self.alias:
self.throttle() payload['alias'] = self.alias
try:
r = requests.post(
notify_url,
data=dumps(payload),
headers=headers,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code not in ( if self.user:
requests.codes.accepted, requests.codes.ok): payload['user'] = self.user
status_str = \
NotifyBase.http_response_code_lookup(
r.status_code)
self.logger.warning( # reset our request IDs - we will re-populate them
'Failed to send Opsgenie notification:' request_ids = []
'{}{}error={}.'.format(
status_str,
', ' if status_str else '',
r.status_code))
self.logger.debug( length = len(self.targets) if self.targets else 1
'Response Details:\r\n{}'.format(r.content)) 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 has_error = True
continue
# If we reach here; the message was sent # Store our entries for a maximum of 60 days
self.logger.info('Sent Opsgenie notification') self.store.set(key, request_ids, expires=60 * 60 * 24 * 60)
self.logger.debug(
'Response Details:\r\n{}'.format(r.content))
except requests.RequestException as e: elif request_ids:
self.logger.warning( # Prepare our payload
'A Connection error occurred sending Opsgenie ' payload = {
'notification.') 'source': self.app_desc,
self.logger.debug('Socket Exception: %s' % str(e)) 'note': body,
# Mark our failure }
has_error = True
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 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.
@ -483,6 +712,7 @@ class NotifyOpsgenie(NotifyBase):
# Define any URL parameters # Define any URL parameters
params = { params = {
'action': self.action,
'region': self.region_name, 'region': self.region_name,
'priority': 'priority':
OPSGENIE_PRIORITIES[self.template_args['priority']['default']] OPSGENIE_PRIORITIES[self.template_args['priority']['default']]
@ -506,6 +736,10 @@ class NotifyOpsgenie(NotifyBase):
# Append our details into our parameters # Append our details into our parameters
params.update({'+{}'.format(k): v for k, v in self.details.items()}) 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 # Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs)) params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
@ -522,8 +756,9 @@ class NotifyOpsgenie(NotifyBase):
NotifyOpsgenie.template_tokens['target_team']['prefix'], NotifyOpsgenie.template_tokens['target_team']['prefix'],
} }
return '{schema}://{apikey}/{targets}/?{params}'.format( return '{schema}://{user}{apikey}/{targets}/?{params}'.format(
schema=self.secure_protocol, schema=self.secure_protocol,
user='{}@'.format(self.user) if self.user else '',
apikey=self.pprint(self.apikey, privacy, safe=''), apikey=self.pprint(self.apikey, privacy, safe=''),
targets='/'.join( targets='/'.join(
[NotifyOpsgenie.quote('{}{}'.format( [NotifyOpsgenie.quote('{}{}'.format(
@ -608,4 +843,14 @@ class NotifyOpsgenie(NotifyBase):
if 'to' in results['qsd'] and len(results['qsd']['to']): if 'to' in results['qsd'] and len(results['qsd']['to']):
results['targets'].append(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 return results

View File

@ -412,6 +412,18 @@ class NotifyPagerDuty(NotifyBase):
return True 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -299,6 +299,15 @@ class NotifyPagerTree(NotifyBase):
return True 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -257,6 +257,19 @@ class NotifyParsePlatform(NotifyBase):
return True 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -242,6 +242,15 @@ class NotifyPopcornNotify(NotifyBase):
return not has_error 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -250,6 +250,15 @@ class NotifyProwl(NotifyBase):
return True 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -386,6 +386,15 @@ class NotifyPushBullet(NotifyBase):
if files: if files:
files['file'][1].close() 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -180,6 +180,18 @@ class NotifyPushDeer(NotifyBase):
return True 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): def url(self, privacy=False):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -303,6 +303,15 @@ class NotifyPushed(NotifyBase):
return True 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -117,6 +117,18 @@ class NotifyPushjet(NotifyBase):
return 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -171,6 +171,15 @@ class NotifyPushMe(NotifyBase):
return True 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -549,6 +549,15 @@ class NotifyPushover(NotifyBase):
return True 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -756,6 +756,18 @@ class NotifyPushSafer(NotifyBase):
return False, response 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -310,6 +310,15 @@ class NotifyPushy(NotifyBase):
return not has_error 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -324,6 +324,18 @@ class NotifyReddit(NotifyBase):
return 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -354,6 +354,15 @@ class NotifyRevolt(NotifyBase):
return (True, content) 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -319,6 +319,23 @@ class NotifyRocketChat(NotifyBase):
return 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -300,6 +300,19 @@ class NotifyRSyslog(NotifyBase):
return True 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -272,6 +272,15 @@ class NotifyRyver(NotifyBase):
return True 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -243,6 +243,15 @@ class NotifySendGrid(NotifyBase):
return 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -149,6 +149,15 @@ class NotifyServerChan(NotifyBase):
return True 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): def url(self, privacy=False):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -770,6 +770,18 @@ class NotifySES(NotifyBase):
return response 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -356,6 +356,18 @@ class NotifySFR(NotifyBase):
return not has_error 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -372,6 +372,18 @@ class NotifySignalAPI(NotifyBase):
return not has_error 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -283,6 +283,15 @@ class NotifySimplePush(NotifyBase):
return True 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -381,6 +381,18 @@ class NotifySinch(NotifyBase):
return not has_error 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. 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 self.mode = SlackMode.BOT if access_token else SlackMode.WEBHOOK
if self.mode is SlackMode.WEBHOOK: if self.mode is SlackMode.WEBHOOK:
self.access_token = None
self.token_a = validate_regex( self.token_a = validate_regex(
token_a, *self.template_tokens['token_a']['regex']) token_a, *self.template_tokens['token_a']['regex'])
if not self.token_a: if not self.token_a:
@ -350,6 +351,9 @@ class NotifySlack(NotifyBase):
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
else: else:
self.token_a = None
self.token_b = None
self.token_c = None
self.access_token = validate_regex( self.access_token = validate_regex(
access_token, *self.template_tokens['access_token']['regex']) access_token, *self.template_tokens['access_token']['regex'])
if not self.access_token: if not self.access_token:
@ -1018,6 +1022,18 @@ class NotifySlack(NotifyBase):
# Return the response for processing # Return the response for processing
return response 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -564,6 +564,18 @@ class NotifySMSEagle(NotifyBase):
return not has_error 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -314,6 +314,15 @@ class NotifySMSManager(NotifyBase):
return not has_error 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -464,6 +464,15 @@ class NotifySMTP2Go(NotifyBase):
return not has_error 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -573,6 +573,18 @@ class NotifySNS(NotifyBase):
return response 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): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. 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