mirror of
https://github.com/SickGear/SickGear.git
synced 2024-11-24 13:55:16 +00:00
Merge branch 'feature/UpdateApprise' into dev
This commit is contained in:
commit
026fc83142
161 changed files with 10193 additions and 2171 deletions
|
@ -1,5 +1,6 @@
|
||||||
### 3.32.0 (2024-xx-xx xx:xx:00 UTC)
|
### 3.32.0 (2024-xx-xx xx:xx:00 UTC)
|
||||||
|
|
||||||
|
* Update apprise 1.6.0 (0c0d5da) to 1.8.0 (81caf92)
|
||||||
* Update attr 23.1.0 (67e4ff2) to 23.2.0 (b393d79)
|
* Update attr 23.1.0 (67e4ff2) to 23.2.0 (b393d79)
|
||||||
* Update Beautiful Soup 4.12.2 (30c58a1) to 4.12.3 (7fb5175)
|
* Update Beautiful Soup 4.12.2 (30c58a1) to 4.12.3 (7fb5175)
|
||||||
* Update CacheControl 0.13.1 (783a338) to 0.14.0 (e2be0c2)
|
* Update CacheControl 0.13.1 (783a338) to 0.14.0 (e2be0c2)
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -27,10 +27,10 @@
|
||||||
# POSSIBILITY OF SUCH DAMAGE.
|
# POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
__title__ = 'Apprise'
|
__title__ = 'Apprise'
|
||||||
__version__ = '1.6.0'
|
__version__ = '1.8.0'
|
||||||
__author__ = 'Chris Caron'
|
__author__ = 'Chris Caron'
|
||||||
__license__ = 'BSD'
|
__license__ = 'BSD'
|
||||||
__copywrite__ = 'Copyright (C) 2023 Chris Caron <lead2gold@gmail.com>'
|
__copywrite__ = 'Copyright (C) 2024 Chris Caron <lead2gold@gmail.com>'
|
||||||
__email__ = 'lead2gold@gmail.com'
|
__email__ = 'lead2gold@gmail.com'
|
||||||
__status__ = 'Production'
|
__status__ = 'Production'
|
||||||
|
|
||||||
|
@ -49,17 +49,20 @@ 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 .URLBase import URLBase
|
from .url import URLBase
|
||||||
from .URLBase import PrivacyMode
|
from .url import PrivacyMode
|
||||||
from .plugins.NotifyBase import NotifyBase
|
from .plugins.base import NotifyBase
|
||||||
from .config.ConfigBase import ConfigBase
|
from .config.base import ConfigBase
|
||||||
from .attachment.AttachBase import AttachBase
|
from .attachment.base import AttachBase
|
||||||
|
|
||||||
from .Apprise import Apprise
|
|
||||||
from .AppriseAsset import AppriseAsset
|
|
||||||
from .AppriseConfig import AppriseConfig
|
|
||||||
from .AppriseAttachment import AppriseAttachment
|
|
||||||
|
|
||||||
|
from .apprise import Apprise
|
||||||
|
from .locale import AppriseLocale
|
||||||
|
from .asset import AppriseAsset
|
||||||
|
from .apprise_config import AppriseConfig
|
||||||
|
from .apprise_attachment import AppriseAttachment
|
||||||
|
from .manager_attachment import AttachmentManager
|
||||||
|
from .manager_config import ConfigurationManager
|
||||||
|
from .manager_plugins import NotificationManager
|
||||||
from . import decorators
|
from . import decorators
|
||||||
|
|
||||||
# Inherit our logging with our additional entries added to it
|
# Inherit our logging with our additional entries added to it
|
||||||
|
@ -73,7 +76,7 @@ logging.getLogger(__name__).addHandler(logging.NullHandler())
|
||||||
__all__ = [
|
__all__ = [
|
||||||
# Core
|
# Core
|
||||||
'Apprise', 'AppriseAsset', 'AppriseConfig', 'AppriseAttachment', 'URLBase',
|
'Apprise', 'AppriseAsset', 'AppriseConfig', 'AppriseAttachment', 'URLBase',
|
||||||
'NotifyBase', 'ConfigBase', 'AttachBase',
|
'NotifyBase', 'ConfigBase', 'AttachBase', 'AppriseLocale',
|
||||||
|
|
||||||
# Reference
|
# Reference
|
||||||
'NotifyType', 'NotifyImageSize', 'NotifyFormat', 'OverflowMode',
|
'NotifyType', 'NotifyImageSize', 'NotifyFormat', 'OverflowMode',
|
||||||
|
@ -83,6 +86,9 @@ __all__ = [
|
||||||
'ContentLocation', 'CONTENT_LOCATIONS',
|
'ContentLocation', 'CONTENT_LOCATIONS',
|
||||||
'PrivacyMode',
|
'PrivacyMode',
|
||||||
|
|
||||||
|
# Managers
|
||||||
|
'NotificationManager', 'ConfigurationManager', 'AttachmentManager',
|
||||||
|
|
||||||
# Decorator
|
# Decorator
|
||||||
'decorators',
|
'decorators',
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -33,21 +33,25 @@ from itertools import chain
|
||||||
from . import common
|
from . import common
|
||||||
from .conversion import convert_between
|
from .conversion import convert_between
|
||||||
from .utils import is_exclusive_match
|
from .utils import is_exclusive_match
|
||||||
|
from .manager_plugins import NotificationManager
|
||||||
from .utils import parse_list
|
from .utils import parse_list
|
||||||
from .utils import parse_urls
|
from .utils import parse_urls
|
||||||
from .utils import cwe312_url
|
from .utils import cwe312_url
|
||||||
|
from .emojis import apply_emojis
|
||||||
from .logger import logger
|
from .logger import logger
|
||||||
from .AppriseAsset import AppriseAsset
|
from .asset import AppriseAsset
|
||||||
from .AppriseConfig import AppriseConfig
|
from .apprise_config import AppriseConfig
|
||||||
from .AppriseAttachment import AppriseAttachment
|
from .apprise_attachment import AppriseAttachment
|
||||||
from .AppriseLocale import AppriseLocale
|
from .locale import AppriseLocale
|
||||||
from .config.ConfigBase import ConfigBase
|
from .config.base import ConfigBase
|
||||||
from .plugins.NotifyBase import NotifyBase
|
from .plugins.base import NotifyBase
|
||||||
|
|
||||||
|
|
||||||
from . import plugins
|
from . import plugins
|
||||||
from . import __version__
|
from . import __version__
|
||||||
|
|
||||||
|
# Grant access to our Notification Manager Singleton
|
||||||
|
N_MGR = NotificationManager()
|
||||||
|
|
||||||
|
|
||||||
class Apprise:
|
class Apprise:
|
||||||
"""
|
"""
|
||||||
|
@ -137,7 +141,7 @@ class Apprise:
|
||||||
# We already have our result set
|
# We already have our result set
|
||||||
results = url
|
results = url
|
||||||
|
|
||||||
if results.get('schema') not in common.NOTIFY_SCHEMA_MAP:
|
if results.get('schema') not in N_MGR:
|
||||||
# schema is a mandatory dictionary item as it is the only way
|
# schema is a mandatory dictionary item as it is the only way
|
||||||
# we can index into our loaded plugins
|
# we can index into our loaded plugins
|
||||||
logger.error('Dictionary does not include a "schema" entry.')
|
logger.error('Dictionary does not include a "schema" entry.')
|
||||||
|
@ -160,7 +164,7 @@ class Apprise:
|
||||||
type(url))
|
type(url))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if not common.NOTIFY_SCHEMA_MAP[results['schema']].enabled:
|
if not N_MGR[results['schema']].enabled:
|
||||||
#
|
#
|
||||||
# First Plugin Enable Check (Pre Initialization)
|
# First Plugin Enable Check (Pre Initialization)
|
||||||
#
|
#
|
||||||
|
@ -180,13 +184,12 @@ class Apprise:
|
||||||
try:
|
try:
|
||||||
# Attempt to create an instance of our plugin using the parsed
|
# Attempt to create an instance of our plugin using the parsed
|
||||||
# URL information
|
# URL information
|
||||||
plugin = common.NOTIFY_SCHEMA_MAP[results['schema']](**results)
|
plugin = N_MGR[results['schema']](**results)
|
||||||
|
|
||||||
# Create log entry of loaded URL
|
# Create log entry of loaded URL
|
||||||
logger.debug(
|
logger.debug(
|
||||||
'Loaded {} URL: {}'.format(
|
'Loaded {} URL: {}'.format(
|
||||||
common.
|
N_MGR[results['schema']].service_name,
|
||||||
NOTIFY_SCHEMA_MAP[results['schema']].service_name,
|
|
||||||
plugin.url(privacy=asset.secure_logging)))
|
plugin.url(privacy=asset.secure_logging)))
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
|
@ -197,15 +200,14 @@ class Apprise:
|
||||||
# the arguments are invalid or can not be used.
|
# the arguments are invalid or can not be used.
|
||||||
logger.error(
|
logger.error(
|
||||||
'Could not load {} URL: {}'.format(
|
'Could not load {} URL: {}'.format(
|
||||||
common.
|
N_MGR[results['schema']].service_name,
|
||||||
NOTIFY_SCHEMA_MAP[results['schema']].service_name,
|
|
||||||
loggable_url))
|
loggable_url))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Attempt to create an instance of our plugin using the parsed
|
# Attempt to create an instance of our plugin using the parsed
|
||||||
# URL information but don't wrap it in a try catch
|
# URL information but don't wrap it in a try catch
|
||||||
plugin = common.NOTIFY_SCHEMA_MAP[results['schema']](**results)
|
plugin = N_MGR[results['schema']](**results)
|
||||||
|
|
||||||
if not plugin.enabled:
|
if not plugin.enabled:
|
||||||
#
|
#
|
||||||
|
@ -376,7 +378,7 @@ class Apprise:
|
||||||
body, title,
|
body, title,
|
||||||
notify_type=notify_type, body_format=body_format,
|
notify_type=notify_type, body_format=body_format,
|
||||||
tag=tag, match_always=match_always, attach=attach,
|
tag=tag, match_always=match_always, attach=attach,
|
||||||
interpret_escapes=interpret_escapes
|
interpret_escapes=interpret_escapes,
|
||||||
)
|
)
|
||||||
|
|
||||||
except TypeError:
|
except TypeError:
|
||||||
|
@ -501,6 +503,11 @@ class Apprise:
|
||||||
key = server.notify_format if server.title_maxlen > 0\
|
key = server.notify_format if server.title_maxlen > 0\
|
||||||
else f'_{server.notify_format}'
|
else f'_{server.notify_format}'
|
||||||
|
|
||||||
|
if server.interpret_emojis:
|
||||||
|
# alter our key slightly to handle emojis since their value is
|
||||||
|
# pulled out of the notification
|
||||||
|
key += "-emojis"
|
||||||
|
|
||||||
if key not in conversion_title_map:
|
if key not in conversion_title_map:
|
||||||
|
|
||||||
# Prepare our title
|
# Prepare our title
|
||||||
|
@ -542,6 +549,16 @@ class Apprise:
|
||||||
logger.error(msg)
|
logger.error(msg)
|
||||||
raise TypeError(msg)
|
raise TypeError(msg)
|
||||||
|
|
||||||
|
if server.interpret_emojis:
|
||||||
|
#
|
||||||
|
# Convert our :emoji: definitions
|
||||||
|
#
|
||||||
|
|
||||||
|
conversion_body_map[key] = \
|
||||||
|
apply_emojis(conversion_body_map[key])
|
||||||
|
conversion_title_map[key] = \
|
||||||
|
apply_emojis(conversion_title_map[key])
|
||||||
|
|
||||||
kwargs = dict(
|
kwargs = dict(
|
||||||
body=conversion_body_map[key],
|
body=conversion_body_map[key],
|
||||||
title=conversion_title_map[key],
|
title=conversion_title_map[key],
|
||||||
|
@ -674,7 +691,7 @@ class Apprise:
|
||||||
'asset': self.asset.details(),
|
'asset': self.asset.details(),
|
||||||
}
|
}
|
||||||
|
|
||||||
for plugin in set(common.NOTIFY_SCHEMA_MAP.values()):
|
for plugin in N_MGR.plugins():
|
||||||
# Iterate over our hashed plugins and dynamically build details on
|
# Iterate over our hashed plugins and dynamically build details on
|
||||||
# their status:
|
# their status:
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -26,15 +26,18 @@
|
||||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
# POSSIBILITY OF SUCH DAMAGE.
|
# POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
from . import attachment
|
|
||||||
from . import URLBase
|
from . import URLBase
|
||||||
from .AppriseAsset import AppriseAsset
|
from .attachment.base import AttachBase
|
||||||
|
from .asset import AppriseAsset
|
||||||
|
from .manager_attachment import AttachmentManager
|
||||||
from .logger import logger
|
from .logger import logger
|
||||||
from .common import ContentLocation
|
from .common import ContentLocation
|
||||||
from .common import CONTENT_LOCATIONS
|
from .common import CONTENT_LOCATIONS
|
||||||
from .common import ATTACHMENT_SCHEMA_MAP
|
|
||||||
from .utils import GET_SCHEMA_RE
|
from .utils import GET_SCHEMA_RE
|
||||||
|
|
||||||
|
# Grant access to our Notification Manager Singleton
|
||||||
|
A_MGR = AttachmentManager()
|
||||||
|
|
||||||
|
|
||||||
class AppriseAttachment:
|
class AppriseAttachment:
|
||||||
"""
|
"""
|
||||||
|
@ -139,13 +142,8 @@ class AppriseAttachment:
|
||||||
# prepare default asset
|
# prepare default asset
|
||||||
asset = self.asset
|
asset = self.asset
|
||||||
|
|
||||||
if isinstance(attachments, attachment.AttachBase):
|
if isinstance(attachments, (AttachBase, str)):
|
||||||
# Go ahead and just add our attachments into our list
|
# store our instance
|
||||||
self.attachments.append(attachments)
|
|
||||||
return True
|
|
||||||
|
|
||||||
elif isinstance(attachments, str):
|
|
||||||
# Save our path
|
|
||||||
attachments = (attachments, )
|
attachments = (attachments, )
|
||||||
|
|
||||||
elif not isinstance(attachments, (tuple, set, list)):
|
elif not isinstance(attachments, (tuple, set, list)):
|
||||||
|
@ -169,7 +167,7 @@ class AppriseAttachment:
|
||||||
# returns None if it fails
|
# returns None if it fails
|
||||||
instance = AppriseAttachment.instantiate(
|
instance = AppriseAttachment.instantiate(
|
||||||
_attachment, asset=asset, cache=cache)
|
_attachment, asset=asset, cache=cache)
|
||||||
if not isinstance(instance, attachment.AttachBase):
|
if not isinstance(instance, AttachBase):
|
||||||
return_status = False
|
return_status = False
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -178,7 +176,7 @@ class AppriseAttachment:
|
||||||
# append our content together
|
# append our content together
|
||||||
instance = _attachment.attachments
|
instance = _attachment.attachments
|
||||||
|
|
||||||
elif not isinstance(_attachment, attachment.AttachBase):
|
elif not isinstance(_attachment, AttachBase):
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"An invalid attachment (type={}) was specified.".format(
|
"An invalid attachment (type={}) was specified.".format(
|
||||||
type(_attachment)))
|
type(_attachment)))
|
||||||
|
@ -228,7 +226,7 @@ class AppriseAttachment:
|
||||||
schema = GET_SCHEMA_RE.match(url)
|
schema = GET_SCHEMA_RE.match(url)
|
||||||
if schema is None:
|
if schema is None:
|
||||||
# Plan B is to assume we're dealing with a file
|
# Plan B is to assume we're dealing with a file
|
||||||
schema = attachment.AttachFile.protocol
|
schema = 'file'
|
||||||
url = '{}://{}'.format(schema, URLBase.quote(url))
|
url = '{}://{}'.format(schema, URLBase.quote(url))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
@ -236,13 +234,13 @@ class AppriseAttachment:
|
||||||
schema = schema.group('schema').lower()
|
schema = schema.group('schema').lower()
|
||||||
|
|
||||||
# Some basic validation
|
# Some basic validation
|
||||||
if schema not in ATTACHMENT_SCHEMA_MAP:
|
if schema not in A_MGR:
|
||||||
logger.warning('Unsupported schema {}.'.format(schema))
|
logger.warning('Unsupported schema {}.'.format(schema))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Parse our url details of the server object as dictionary containing
|
# Parse our url details of the server object as dictionary containing
|
||||||
# all of the information parsed from our URL
|
# all of the information parsed from our URL
|
||||||
results = ATTACHMENT_SCHEMA_MAP[schema].parse_url(url)
|
results = A_MGR[schema].parse_url(url)
|
||||||
|
|
||||||
if not results:
|
if not results:
|
||||||
# Failed to parse the server URL
|
# Failed to parse the server URL
|
||||||
|
@ -261,8 +259,7 @@ class AppriseAttachment:
|
||||||
try:
|
try:
|
||||||
# Attempt to create an instance of our plugin using the parsed
|
# Attempt to create an instance of our plugin using the parsed
|
||||||
# URL information
|
# URL information
|
||||||
attach_plugin = \
|
attach_plugin = A_MGR[results['schema']](**results)
|
||||||
ATTACHMENT_SCHEMA_MAP[results['schema']](**results)
|
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
# the arguments are invalid or can not be used.
|
# the arguments are invalid or can not be used.
|
||||||
|
@ -272,7 +269,7 @@ class AppriseAttachment:
|
||||||
else:
|
else:
|
||||||
# Attempt to create an instance of our plugin using the parsed
|
# Attempt to create an instance of our plugin using the parsed
|
||||||
# URL information but don't wrap it in a try catch
|
# URL information but don't wrap it in a try catch
|
||||||
attach_plugin = ATTACHMENT_SCHEMA_MAP[results['schema']](**results)
|
attach_plugin = A_MGR[results['schema']](**results)
|
||||||
|
|
||||||
return attach_plugin
|
return attach_plugin
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -26,17 +26,20 @@
|
||||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
# POSSIBILITY OF SUCH DAMAGE.
|
# POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
from . import config
|
|
||||||
from . import ConfigBase
|
from . import ConfigBase
|
||||||
from . import CONFIG_FORMATS
|
from . import CONFIG_FORMATS
|
||||||
|
from .manager_config import ConfigurationManager
|
||||||
from . import URLBase
|
from . import URLBase
|
||||||
from .AppriseAsset import AppriseAsset
|
from .asset import AppriseAsset
|
||||||
from . import common
|
from . import common
|
||||||
from .utils import GET_SCHEMA_RE
|
from .utils import GET_SCHEMA_RE
|
||||||
from .utils import parse_list
|
from .utils import parse_list
|
||||||
from .utils import is_exclusive_match
|
from .utils import is_exclusive_match
|
||||||
from .logger import logger
|
from .logger import logger
|
||||||
|
|
||||||
|
# Grant access to our Configuration Manager Singleton
|
||||||
|
C_MGR = ConfigurationManager()
|
||||||
|
|
||||||
|
|
||||||
class AppriseConfig:
|
class AppriseConfig:
|
||||||
"""
|
"""
|
||||||
|
@ -251,7 +254,7 @@ class AppriseConfig:
|
||||||
logger.debug("Loading raw configuration: {}".format(content))
|
logger.debug("Loading raw configuration: {}".format(content))
|
||||||
|
|
||||||
# Create ourselves a ConfigMemory Object to store our configuration
|
# Create ourselves a ConfigMemory Object to store our configuration
|
||||||
instance = config.ConfigMemory(
|
instance = C_MGR['memory'](
|
||||||
content=content, format=format, asset=asset, tag=tag,
|
content=content, format=format, asset=asset, tag=tag,
|
||||||
recursion=recursion, insecure_includes=insecure_includes)
|
recursion=recursion, insecure_includes=insecure_includes)
|
||||||
|
|
||||||
|
@ -326,7 +329,7 @@ class AppriseConfig:
|
||||||
schema = GET_SCHEMA_RE.match(url)
|
schema = GET_SCHEMA_RE.match(url)
|
||||||
if schema is None:
|
if schema is None:
|
||||||
# Plan B is to assume we're dealing with a file
|
# Plan B is to assume we're dealing with a file
|
||||||
schema = config.ConfigFile.protocol
|
schema = 'file'
|
||||||
url = '{}://{}'.format(schema, URLBase.quote(url))
|
url = '{}://{}'.format(schema, URLBase.quote(url))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
@ -334,13 +337,13 @@ class AppriseConfig:
|
||||||
schema = schema.group('schema').lower()
|
schema = schema.group('schema').lower()
|
||||||
|
|
||||||
# Some basic validation
|
# Some basic validation
|
||||||
if schema not in common.CONFIG_SCHEMA_MAP:
|
if schema not in C_MGR:
|
||||||
logger.warning('Unsupported schema {}.'.format(schema))
|
logger.warning('Unsupported schema {}.'.format(schema))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Parse our url details of the server object as dictionary containing
|
# Parse our url details of the server object as dictionary containing
|
||||||
# all of the information parsed from our URL
|
# all of the information parsed from our URL
|
||||||
results = common.CONFIG_SCHEMA_MAP[schema].parse_url(url)
|
results = C_MGR[schema].parse_url(url)
|
||||||
|
|
||||||
if not results:
|
if not results:
|
||||||
# Failed to parse the server URL
|
# Failed to parse the server URL
|
||||||
|
@ -368,8 +371,7 @@ class AppriseConfig:
|
||||||
try:
|
try:
|
||||||
# Attempt to create an instance of our plugin using the parsed
|
# Attempt to create an instance of our plugin using the parsed
|
||||||
# URL information
|
# URL information
|
||||||
cfg_plugin = \
|
cfg_plugin = C_MGR[results['schema']](**results)
|
||||||
common.CONFIG_SCHEMA_MAP[results['schema']](**results)
|
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
# the arguments are invalid or can not be used.
|
# the arguments are invalid or can not be used.
|
||||||
|
@ -379,7 +381,7 @@ class AppriseConfig:
|
||||||
else:
|
else:
|
||||||
# Attempt to create an instance of our plugin using the parsed
|
# Attempt to create an instance of our plugin using the parsed
|
||||||
# URL information but don't wrap it in a try catch
|
# URL information but don't wrap it in a try catch
|
||||||
cfg_plugin = common.CONFIG_SCHEMA_MAP[results['schema']](**results)
|
cfg_plugin = C_MGR[results['schema']](**results)
|
||||||
|
|
||||||
return cfg_plugin
|
return cfg_plugin
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -33,7 +33,11 @@ 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 .utils import module_detection
|
from .manager_plugins import NotificationManager
|
||||||
|
|
||||||
|
|
||||||
|
# Grant access to our Notification Manager Singleton
|
||||||
|
N_MGR = NotificationManager()
|
||||||
|
|
||||||
|
|
||||||
class AppriseAsset:
|
class AppriseAsset:
|
||||||
|
@ -66,6 +70,9 @@ class AppriseAsset:
|
||||||
NotifyType.WARNING: '#CACF29',
|
NotifyType.WARNING: '#CACF29',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# The default color to return if a mapping isn't found in our table above
|
||||||
|
default_html_color = '#888888'
|
||||||
|
|
||||||
# Ascii Notification
|
# Ascii Notification
|
||||||
ascii_notify_map = {
|
ascii_notify_map = {
|
||||||
NotifyType.INFO: '[i]',
|
NotifyType.INFO: '[i]',
|
||||||
|
@ -74,8 +81,8 @@ class AppriseAsset:
|
||||||
NotifyType.WARNING: '[~]',
|
NotifyType.WARNING: '[~]',
|
||||||
}
|
}
|
||||||
|
|
||||||
# The default color to return if a mapping isn't found in our table above
|
# The default ascii to return if a mapping isn't found in our table above
|
||||||
default_html_color = '#888888'
|
default_ascii_chars = '[?]'
|
||||||
|
|
||||||
# The default image extension to use
|
# The default image extension to use
|
||||||
default_extension = '.png'
|
default_extension = '.png'
|
||||||
|
@ -121,6 +128,12 @@ class AppriseAsset:
|
||||||
# notifications are sent sequentially (one after another)
|
# notifications are sent sequentially (one after another)
|
||||||
async_mode = True
|
async_mode = True
|
||||||
|
|
||||||
|
# Support :smile:, and other alike keywords swapping them for their
|
||||||
|
# unicode value. A value of None leaves the interpretation up to the
|
||||||
|
# end user to control (allowing them to specify emojis=yes on the
|
||||||
|
# URL)
|
||||||
|
interpret_emojis = None
|
||||||
|
|
||||||
# Whether or not to interpret escapes found within the input text prior
|
# Whether or not to interpret escapes found within the input text prior
|
||||||
# to passing it upstream. Such as converting \t to an actual tab and \n
|
# to passing it upstream. Such as converting \t to an actual tab and \n
|
||||||
# to a new line.
|
# to a new line.
|
||||||
|
@ -174,7 +187,7 @@ class AppriseAsset:
|
||||||
|
|
||||||
if plugin_paths:
|
if plugin_paths:
|
||||||
# Load any decorated modules if defined
|
# Load any decorated modules if defined
|
||||||
module_detection(plugin_paths)
|
N_MGR.module_detection(plugin_paths)
|
||||||
|
|
||||||
def color(self, notify_type, color_type=None):
|
def color(self, notify_type, color_type=None):
|
||||||
"""
|
"""
|
||||||
|
@ -213,9 +226,8 @@ class AppriseAsset:
|
||||||
Returns an ascii representation based on passed in notify type
|
Returns an ascii representation based on passed in notify type
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# look our response up
|
# look our response up
|
||||||
return self.ascii_notify_map.get(notify_type, self.default_html_color)
|
return self.ascii_notify_map.get(notify_type, self.default_ascii_chars)
|
||||||
|
|
||||||
def image_url(self, notify_type, image_size, logo=False, extension=None):
|
def image_url(self, notify_type, image_size, logo=False, extension=None):
|
||||||
"""
|
"""
|
|
@ -1,337 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# BSD 2-Clause License
|
|
||||||
#
|
|
||||||
# Apprise - Push Notification Library.
|
|
||||||
# Copyright (c) 2023, 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 re
|
|
||||||
import os
|
|
||||||
import requests
|
|
||||||
from tempfile import NamedTemporaryFile
|
|
||||||
from .AttachBase import AttachBase
|
|
||||||
from ..common import ContentLocation
|
|
||||||
from ..URLBase import PrivacyMode
|
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
|
||||||
|
|
||||||
|
|
||||||
class AttachHTTP(AttachBase):
|
|
||||||
"""
|
|
||||||
A wrapper for HTTP based attachment sources
|
|
||||||
"""
|
|
||||||
|
|
||||||
# The default descriptive name associated with the service
|
|
||||||
service_name = _('Web Based')
|
|
||||||
|
|
||||||
# The default protocol
|
|
||||||
protocol = 'http'
|
|
||||||
|
|
||||||
# The default secure protocol
|
|
||||||
secure_protocol = 'https'
|
|
||||||
|
|
||||||
# The number of bytes in memory to read from the remote source at a time
|
|
||||||
chunk_size = 8192
|
|
||||||
|
|
||||||
# Web based requests are remote/external to our current location
|
|
||||||
location = ContentLocation.HOSTED
|
|
||||||
|
|
||||||
def __init__(self, headers=None, **kwargs):
|
|
||||||
"""
|
|
||||||
Initialize HTTP Object
|
|
||||||
|
|
||||||
headers can be a dictionary of key/value pairs that you want to
|
|
||||||
additionally include as part of the server headers to post with
|
|
||||||
|
|
||||||
"""
|
|
||||||
super().__init__(**kwargs)
|
|
||||||
|
|
||||||
self.schema = 'https' if self.secure else 'http'
|
|
||||||
|
|
||||||
self.fullpath = kwargs.get('fullpath')
|
|
||||||
if not isinstance(self.fullpath, str):
|
|
||||||
self.fullpath = '/'
|
|
||||||
|
|
||||||
self.headers = {}
|
|
||||||
if headers:
|
|
||||||
# Store our extra headers
|
|
||||||
self.headers.update(headers)
|
|
||||||
|
|
||||||
# Where our content is written to upon a call to download.
|
|
||||||
self._temp_file = None
|
|
||||||
|
|
||||||
# Our Query String Dictionary; we use this to track arguments
|
|
||||||
# specified that aren't otherwise part of this class
|
|
||||||
self.qsd = {k: v for k, v in kwargs.get('qsd', {}).items()
|
|
||||||
if k not in self.template_args}
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
def download(self, **kwargs):
|
|
||||||
"""
|
|
||||||
Perform retrieval of the configuration based on the specified request
|
|
||||||
"""
|
|
||||||
|
|
||||||
if self.location == ContentLocation.INACCESSIBLE:
|
|
||||||
# our content is inaccessible
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Ensure any existing content set has been invalidated
|
|
||||||
self.invalidate()
|
|
||||||
|
|
||||||
# prepare header
|
|
||||||
headers = {
|
|
||||||
'User-Agent': self.app_id,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Apply any/all header over-rides defined
|
|
||||||
headers.update(self.headers)
|
|
||||||
|
|
||||||
auth = None
|
|
||||||
if self.user:
|
|
||||||
auth = (self.user, self.password)
|
|
||||||
|
|
||||||
url = '%s://%s' % (self.schema, self.host)
|
|
||||||
if isinstance(self.port, int):
|
|
||||||
url += ':%d' % self.port
|
|
||||||
|
|
||||||
url += self.fullpath
|
|
||||||
|
|
||||||
self.logger.debug('HTTP POST URL: %s (cert_verify=%r)' % (
|
|
||||||
url, self.verify_certificate,
|
|
||||||
))
|
|
||||||
|
|
||||||
# Where our request object will temporarily live.
|
|
||||||
r = None
|
|
||||||
|
|
||||||
# Always call throttle before any remote server i/o is made
|
|
||||||
self.throttle()
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Make our request
|
|
||||||
with requests.get(
|
|
||||||
url,
|
|
||||||
headers=headers,
|
|
||||||
auth=auth,
|
|
||||||
params=self.qsd,
|
|
||||||
verify=self.verify_certificate,
|
|
||||||
timeout=self.request_timeout,
|
|
||||||
stream=True) as r:
|
|
||||||
|
|
||||||
# Handle Errors
|
|
||||||
r.raise_for_status()
|
|
||||||
|
|
||||||
# Get our file-size (if known)
|
|
||||||
try:
|
|
||||||
file_size = int(r.headers.get('Content-Length', '0'))
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
# Handle edge case where Content-Length is a bad value
|
|
||||||
file_size = 0
|
|
||||||
|
|
||||||
# Perform a little Q/A on file limitations and restrictions
|
|
||||||
if self.max_file_size > 0 and file_size > self.max_file_size:
|
|
||||||
|
|
||||||
# The content retrieved is to large
|
|
||||||
self.logger.error(
|
|
||||||
'HTTP response exceeds allowable maximum file length '
|
|
||||||
'({}KB): {}'.format(
|
|
||||||
int(self.max_file_size / 1024),
|
|
||||||
self.url(privacy=True)))
|
|
||||||
|
|
||||||
# Return False (signifying a failure)
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Detect config format based on mime if the format isn't
|
|
||||||
# already enforced
|
|
||||||
self.detected_mimetype = r.headers.get('Content-Type')
|
|
||||||
|
|
||||||
d = r.headers.get('Content-Disposition', '')
|
|
||||||
result = re.search(
|
|
||||||
"filename=['\"]?(?P<name>[^'\"]+)['\"]?", d, re.I)
|
|
||||||
if result:
|
|
||||||
self.detected_name = result.group('name').strip()
|
|
||||||
|
|
||||||
# Create a temporary file to work with
|
|
||||||
self._temp_file = NamedTemporaryFile()
|
|
||||||
|
|
||||||
# Get our chunk size
|
|
||||||
chunk_size = self.chunk_size
|
|
||||||
|
|
||||||
# Track all bytes written to disk
|
|
||||||
bytes_written = 0
|
|
||||||
|
|
||||||
# If we get here, we can now safely write our content to disk
|
|
||||||
for chunk in r.iter_content(chunk_size=chunk_size):
|
|
||||||
# filter out keep-alive chunks
|
|
||||||
if chunk:
|
|
||||||
self._temp_file.write(chunk)
|
|
||||||
bytes_written = self._temp_file.tell()
|
|
||||||
|
|
||||||
# Prevent a case where Content-Length isn't provided
|
|
||||||
# we don't want to fetch beyond our limits
|
|
||||||
if self.max_file_size > 0:
|
|
||||||
if bytes_written > self.max_file_size:
|
|
||||||
# The content retrieved is to large
|
|
||||||
self.logger.error(
|
|
||||||
'HTTP response exceeds allowable maximum '
|
|
||||||
'file length ({}KB): {}'.format(
|
|
||||||
int(self.max_file_size / 1024),
|
|
||||||
self.url(privacy=True)))
|
|
||||||
|
|
||||||
# Invalidate any variables previously set
|
|
||||||
self.invalidate()
|
|
||||||
|
|
||||||
# Return False (signifying a failure)
|
|
||||||
return False
|
|
||||||
|
|
||||||
elif bytes_written + chunk_size \
|
|
||||||
> self.max_file_size:
|
|
||||||
# Adjust out next read to accomodate up to our
|
|
||||||
# limit +1. This will prevent us from readig
|
|
||||||
# to much into our memory buffer
|
|
||||||
self.max_file_size - bytes_written + 1
|
|
||||||
|
|
||||||
# Ensure our content is flushed to disk for post-processing
|
|
||||||
self._temp_file.flush()
|
|
||||||
|
|
||||||
# Set our minimum requirements for a successful download() call
|
|
||||||
self.download_path = self._temp_file.name
|
|
||||||
if not self.detected_name:
|
|
||||||
self.detected_name = os.path.basename(self.fullpath)
|
|
||||||
|
|
||||||
except requests.RequestException as e:
|
|
||||||
self.logger.error(
|
|
||||||
'A Connection error occurred retrieving HTTP '
|
|
||||||
'configuration from %s.' % self.host)
|
|
||||||
self.logger.debug('Socket Exception: %s' % str(e))
|
|
||||||
|
|
||||||
# Invalidate any variables previously set
|
|
||||||
self.invalidate()
|
|
||||||
|
|
||||||
# Return False (signifying a failure)
|
|
||||||
return False
|
|
||||||
|
|
||||||
except (IOError, OSError):
|
|
||||||
# IOError is present for backwards compatibility with Python
|
|
||||||
# versions older then 3.3. >= 3.3 throw OSError now.
|
|
||||||
|
|
||||||
# Could not open and/or write the temporary file
|
|
||||||
self.logger.error(
|
|
||||||
'Could not write attachment to disk: {}'.format(
|
|
||||||
self.url(privacy=True)))
|
|
||||||
|
|
||||||
# Invalidate any variables previously set
|
|
||||||
self.invalidate()
|
|
||||||
|
|
||||||
# Return False (signifying a failure)
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Return our success
|
|
||||||
return True
|
|
||||||
|
|
||||||
def invalidate(self):
|
|
||||||
"""
|
|
||||||
Close our temporary file
|
|
||||||
"""
|
|
||||||
if self._temp_file:
|
|
||||||
self._temp_file.close()
|
|
||||||
self._temp_file = None
|
|
||||||
|
|
||||||
super().invalidate()
|
|
||||||
|
|
||||||
def url(self, privacy=False, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Returns the URL built dynamically based on specified arguments.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Our URL parameters
|
|
||||||
params = self.url_parameters(privacy=privacy, *args, **kwargs)
|
|
||||||
|
|
||||||
# Prepare our cache value
|
|
||||||
if self.cache is not None:
|
|
||||||
if isinstance(self.cache, bool) or not self.cache:
|
|
||||||
cache = 'yes' if self.cache else 'no'
|
|
||||||
else:
|
|
||||||
cache = int(self.cache)
|
|
||||||
|
|
||||||
# Set our cache value
|
|
||||||
params['cache'] = cache
|
|
||||||
|
|
||||||
if self._mimetype:
|
|
||||||
# A format was enforced
|
|
||||||
params['mime'] = self._mimetype
|
|
||||||
|
|
||||||
if self._name:
|
|
||||||
# A name was enforced
|
|
||||||
params['name'] = self._name
|
|
||||||
|
|
||||||
# Append our headers into our parameters
|
|
||||||
params.update({'+{}'.format(k): v for k, v in self.headers.items()})
|
|
||||||
|
|
||||||
# Apply any remaining entries to our URL
|
|
||||||
params.update(self.qsd)
|
|
||||||
|
|
||||||
# Determine Authentication
|
|
||||||
auth = ''
|
|
||||||
if self.user and self.password:
|
|
||||||
auth = '{user}:{password}@'.format(
|
|
||||||
user=self.quote(self.user, safe=''),
|
|
||||||
password=self.pprint(
|
|
||||||
self.password, privacy, mode=PrivacyMode.Secret, safe=''),
|
|
||||||
)
|
|
||||||
elif self.user:
|
|
||||||
auth = '{user}@'.format(
|
|
||||||
user=self.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,
|
|
||||||
hostname=self.quote(self.host, safe=''),
|
|
||||||
port='' if self.port is None or self.port == default_port
|
|
||||||
else ':{}'.format(self.port),
|
|
||||||
fullpath=self.quote(self.fullpath, safe='/'),
|
|
||||||
params=self.urlencode(params),
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def parse_url(url):
|
|
||||||
"""
|
|
||||||
Parses the URL and returns enough arguments that can allow
|
|
||||||
us to re-instantiate this object.
|
|
||||||
|
|
||||||
"""
|
|
||||||
results = AttachBase.parse_url(url)
|
|
||||||
|
|
||||||
if not results:
|
|
||||||
# We're done early as we couldn't load the results
|
|
||||||
return results
|
|
||||||
|
|
||||||
# Add our headers that the user can potentially over-ride if they wish
|
|
||||||
# to to our returned result set
|
|
||||||
results['headers'] = results['qsd-']
|
|
||||||
results['headers'].update(results['qsd+'])
|
|
||||||
|
|
||||||
return results
|
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -26,93 +26,15 @@
|
||||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
# POSSIBILITY OF SUCH DAMAGE.
|
# POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
import re
|
# Used for testing
|
||||||
from os import listdir
|
from .base import AttachBase
|
||||||
from os.path import dirname
|
from ..manager_attachment import AttachmentManager
|
||||||
from os.path import abspath
|
|
||||||
from ..common import ATTACHMENT_SCHEMA_MAP
|
|
||||||
|
|
||||||
__all__ = []
|
# Initalize our Attachment Manager Singleton
|
||||||
|
A_MGR = AttachmentManager()
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
# Load our Lookup Matrix
|
# Reference
|
||||||
def __load_matrix(path=abspath(dirname(__file__)), name='apprise.attachment'):
|
'AttachBase',
|
||||||
"""
|
'AttachmentManager',
|
||||||
Dynamically load our schema map; this allows us to gracefully
|
]
|
||||||
skip over modules we simply don't have the dependencies for.
|
|
||||||
|
|
||||||
"""
|
|
||||||
# Used for the detection of additional Attachment Services objects
|
|
||||||
# The .py extension is optional as we support loading directories too
|
|
||||||
module_re = re.compile(r'^(?P<name>Attach[a-z0-9]+)(\.py)?$', re.I)
|
|
||||||
|
|
||||||
for f in listdir(path):
|
|
||||||
match = module_re.match(f)
|
|
||||||
if not match:
|
|
||||||
# keep going
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Store our notification/plugin name:
|
|
||||||
plugin_name = match.group('name')
|
|
||||||
try:
|
|
||||||
module = __import__(
|
|
||||||
'{}.{}'.format(name, plugin_name),
|
|
||||||
globals(), locals(),
|
|
||||||
fromlist=[plugin_name])
|
|
||||||
|
|
||||||
except ImportError:
|
|
||||||
# No problem, we can't use this object
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not hasattr(module, plugin_name):
|
|
||||||
# Not a library we can load as it doesn't follow the simple rule
|
|
||||||
# that the class must bear the same name as the notification
|
|
||||||
# file itself.
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Get our plugin
|
|
||||||
plugin = getattr(module, plugin_name)
|
|
||||||
if not hasattr(plugin, 'app_id'):
|
|
||||||
# Filter out non-notification modules
|
|
||||||
continue
|
|
||||||
|
|
||||||
elif plugin_name in __all__:
|
|
||||||
# we're already handling this object
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Add our module name to our __all__
|
|
||||||
__all__.append(plugin_name)
|
|
||||||
|
|
||||||
# Ensure we provide the class as the reference to this directory and
|
|
||||||
# not the module:
|
|
||||||
globals()[plugin_name] = plugin
|
|
||||||
|
|
||||||
# Load protocol(s) if defined
|
|
||||||
proto = getattr(plugin, 'protocol', None)
|
|
||||||
if isinstance(proto, str):
|
|
||||||
if proto not in ATTACHMENT_SCHEMA_MAP:
|
|
||||||
ATTACHMENT_SCHEMA_MAP[proto] = plugin
|
|
||||||
|
|
||||||
elif isinstance(proto, (set, list, tuple)):
|
|
||||||
# Support iterables list types
|
|
||||||
for p in proto:
|
|
||||||
if p not in ATTACHMENT_SCHEMA_MAP:
|
|
||||||
ATTACHMENT_SCHEMA_MAP[p] = plugin
|
|
||||||
|
|
||||||
# Load secure protocol(s) if defined
|
|
||||||
protos = getattr(plugin, 'secure_protocol', None)
|
|
||||||
if isinstance(protos, str):
|
|
||||||
if protos not in ATTACHMENT_SCHEMA_MAP:
|
|
||||||
ATTACHMENT_SCHEMA_MAP[protos] = plugin
|
|
||||||
|
|
||||||
if isinstance(protos, (set, list, tuple)):
|
|
||||||
# Support iterables list types
|
|
||||||
for p in protos:
|
|
||||||
if p not in ATTACHMENT_SCHEMA_MAP:
|
|
||||||
ATTACHMENT_SCHEMA_MAP[p] = plugin
|
|
||||||
|
|
||||||
return ATTACHMENT_SCHEMA_MAP
|
|
||||||
|
|
||||||
|
|
||||||
# Dynamically build our schema base
|
|
||||||
__load_matrix()
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -29,10 +29,10 @@
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
import mimetypes
|
import mimetypes
|
||||||
from ..URLBase import URLBase
|
from ..url import URLBase
|
||||||
from ..utils import parse_bool
|
from ..utils import parse_bool
|
||||||
from ..common import ContentLocation
|
from ..common import ContentLocation
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..locale import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class AttachBase(URLBase):
|
class AttachBase(URLBase):
|
||||||
|
@ -148,6 +148,9 @@ class AttachBase(URLBase):
|
||||||
# Absolute path to attachment
|
# Absolute path to attachment
|
||||||
self.download_path = None
|
self.download_path = None
|
||||||
|
|
||||||
|
# Track open file pointers
|
||||||
|
self.__pointers = set()
|
||||||
|
|
||||||
# Set our cache flag; it can be True, False, None, or a (positive)
|
# Set our cache flag; it can be True, False, None, or a (positive)
|
||||||
# integer... nothing else
|
# integer... nothing else
|
||||||
if cache is not None:
|
if cache is not None:
|
||||||
|
@ -226,15 +229,14 @@ class AttachBase(URLBase):
|
||||||
Content is cached once determied to prevent overhead of future
|
Content is cached once determied to prevent overhead of future
|
||||||
calls.
|
calls.
|
||||||
"""
|
"""
|
||||||
|
if not self.exists():
|
||||||
|
# we could not obtain our attachment
|
||||||
|
return None
|
||||||
|
|
||||||
if self._mimetype:
|
if self._mimetype:
|
||||||
# return our pre-calculated cached content
|
# return our pre-calculated cached content
|
||||||
return self._mimetype
|
return self._mimetype
|
||||||
|
|
||||||
if not self.exists():
|
|
||||||
# we could not obtain our attachment
|
|
||||||
return None
|
|
||||||
|
|
||||||
if not self.detected_mimetype:
|
if not self.detected_mimetype:
|
||||||
# guess_type() returns: (type, encoding) and sets type to None
|
# guess_type() returns: (type, encoding) and sets type to None
|
||||||
# if it can't otherwise determine it.
|
# if it can't otherwise determine it.
|
||||||
|
@ -253,11 +255,14 @@ class AttachBase(URLBase):
|
||||||
return self.detected_mimetype \
|
return self.detected_mimetype \
|
||||||
if self.detected_mimetype else self.unknown_mimetype
|
if self.detected_mimetype else self.unknown_mimetype
|
||||||
|
|
||||||
def exists(self):
|
def exists(self, retrieve_if_missing=True):
|
||||||
"""
|
"""
|
||||||
Simply returns true if the object has downloaded and stored the
|
Simply returns true if the object has downloaded and stored the
|
||||||
attachment AND the attachment has not expired.
|
attachment AND the attachment has not expired.
|
||||||
"""
|
"""
|
||||||
|
if self.location == ContentLocation.INACCESSIBLE:
|
||||||
|
# our content is inaccessible
|
||||||
|
return False
|
||||||
|
|
||||||
cache = self.template_args['cache']['default'] \
|
cache = self.template_args['cache']['default'] \
|
||||||
if self.cache is None else self.cache
|
if self.cache is None else self.cache
|
||||||
|
@ -282,7 +287,7 @@ class AttachBase(URLBase):
|
||||||
# The file is not present
|
# The file is not present
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return self.download()
|
return False if not retrieve_if_missing else self.download()
|
||||||
|
|
||||||
def invalidate(self):
|
def invalidate(self):
|
||||||
"""
|
"""
|
||||||
|
@ -295,6 +300,11 @@ class AttachBase(URLBase):
|
||||||
- download_path: Must contain a absolute path to content
|
- download_path: Must contain a absolute path to content
|
||||||
- detected_mimetype: Should identify mimetype of content
|
- detected_mimetype: Should identify mimetype of content
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Remove all open pointers
|
||||||
|
while self.__pointers:
|
||||||
|
self.__pointers.pop().close()
|
||||||
|
|
||||||
self.detected_name = None
|
self.detected_name = None
|
||||||
self.download_path = None
|
self.download_path = None
|
||||||
self.detected_mimetype = None
|
self.detected_mimetype = None
|
||||||
|
@ -314,8 +324,28 @@ class AttachBase(URLBase):
|
||||||
raise NotImplementedError(
|
raise NotImplementedError(
|
||||||
"download() is implimented by the child class.")
|
"download() is implimented by the child class.")
|
||||||
|
|
||||||
|
def open(self, mode='rb'):
|
||||||
|
"""
|
||||||
|
return our file pointer and track it (we'll auto close later
|
||||||
|
"""
|
||||||
|
pointer = open(self.path, mode=mode)
|
||||||
|
self.__pointers.add(pointer)
|
||||||
|
return pointer
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
"""
|
||||||
|
support with keyword
|
||||||
|
"""
|
||||||
|
return self.open()
|
||||||
|
|
||||||
|
def __exit__(self, value_type, value, traceback):
|
||||||
|
"""
|
||||||
|
stub to do nothing; but support exit of with statement gracefully
|
||||||
|
"""
|
||||||
|
return
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_url(url, verify_host=True, mimetype_db=None):
|
def parse_url(url, verify_host=True, mimetype_db=None, sanitize=True):
|
||||||
"""Parses the URL and returns it broken apart into a dictionary.
|
"""Parses the URL and returns it broken apart into a dictionary.
|
||||||
|
|
||||||
This is very specific and customized for Apprise.
|
This is very specific and customized for Apprise.
|
||||||
|
@ -333,7 +363,8 @@ class AttachBase(URLBase):
|
||||||
successful, otherwise None is returned.
|
successful, otherwise None is returned.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
results = URLBase.parse_url(url, verify_host=verify_host)
|
results = URLBase.parse_url(
|
||||||
|
url, verify_host=verify_host, sanitize=sanitize)
|
||||||
|
|
||||||
if not results:
|
if not results:
|
||||||
# We're done; we failed to parse our url
|
# We're done; we failed to parse our url
|
||||||
|
@ -375,3 +406,9 @@ class AttachBase(URLBase):
|
||||||
True is returned if our content was downloaded correctly.
|
True is returned if our content was downloaded correctly.
|
||||||
"""
|
"""
|
||||||
return True if self.path else False
|
return True if self.path else False
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
"""
|
||||||
|
Perform any house cleaning
|
||||||
|
"""
|
||||||
|
self.invalidate()
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -28,9 +28,9 @@
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import os
|
import os
|
||||||
from .AttachBase import AttachBase
|
from .base import AttachBase
|
||||||
from ..common import ContentLocation
|
from ..common import ContentLocation
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..locale import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class AttachFile(AttachBase):
|
class AttachFile(AttachBase):
|
||||||
|
@ -78,7 +78,8 @@ class AttachFile(AttachBase):
|
||||||
|
|
||||||
return 'file://{path}{params}'.format(
|
return 'file://{path}{params}'.format(
|
||||||
path=self.quote(self.dirty_path),
|
path=self.quote(self.dirty_path),
|
||||||
params='?{}'.format(self.urlencode(params)) if params else '',
|
params='?{}'.format(self.urlencode(params, safe='/'))
|
||||||
|
if params else '',
|
||||||
)
|
)
|
||||||
|
|
||||||
def download(self, **kwargs):
|
def download(self, **kwargs):
|
375
lib/apprise/attachment/http.py
Normal file
375
lib/apprise/attachment/http.py
Normal file
|
@ -0,0 +1,375 @@
|
||||||
|
# -*- 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 re
|
||||||
|
import os
|
||||||
|
import requests
|
||||||
|
import threading
|
||||||
|
from tempfile import NamedTemporaryFile
|
||||||
|
from .base import AttachBase
|
||||||
|
from ..common import ContentLocation
|
||||||
|
from ..url import PrivacyMode
|
||||||
|
from ..locale import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class AttachHTTP(AttachBase):
|
||||||
|
"""
|
||||||
|
A wrapper for HTTP based attachment sources
|
||||||
|
"""
|
||||||
|
|
||||||
|
# The default descriptive name associated with the service
|
||||||
|
service_name = _('Web Based')
|
||||||
|
|
||||||
|
# The default protocol
|
||||||
|
protocol = 'http'
|
||||||
|
|
||||||
|
# The default secure protocol
|
||||||
|
secure_protocol = 'https'
|
||||||
|
|
||||||
|
# The number of bytes in memory to read from the remote source at a time
|
||||||
|
chunk_size = 8192
|
||||||
|
|
||||||
|
# Web based requests are remote/external to our current location
|
||||||
|
location = ContentLocation.HOSTED
|
||||||
|
|
||||||
|
# thread safe loading
|
||||||
|
_lock = threading.Lock()
|
||||||
|
|
||||||
|
def __init__(self, headers=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Initialize HTTP Object
|
||||||
|
|
||||||
|
headers can be a dictionary of key/value pairs that you want to
|
||||||
|
additionally include as part of the server headers to post with
|
||||||
|
|
||||||
|
"""
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
self.schema = 'https' if self.secure else 'http'
|
||||||
|
|
||||||
|
self.fullpath = kwargs.get('fullpath')
|
||||||
|
if not isinstance(self.fullpath, str):
|
||||||
|
self.fullpath = '/'
|
||||||
|
|
||||||
|
self.headers = {}
|
||||||
|
if headers:
|
||||||
|
# Store our extra headers
|
||||||
|
self.headers.update(headers)
|
||||||
|
|
||||||
|
# Where our content is written to upon a call to download.
|
||||||
|
self._temp_file = None
|
||||||
|
|
||||||
|
# Our Query String Dictionary; we use this to track arguments
|
||||||
|
# specified that aren't otherwise part of this class
|
||||||
|
self.qsd = {k: v for k, v in kwargs.get('qsd', {}).items()
|
||||||
|
if k not in self.template_args}
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
def download(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Perform retrieval of the configuration based on the specified request
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self.location == ContentLocation.INACCESSIBLE:
|
||||||
|
# our content is inaccessible
|
||||||
|
return False
|
||||||
|
|
||||||
|
# prepare header
|
||||||
|
headers = {
|
||||||
|
'User-Agent': self.app_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Apply any/all header over-rides defined
|
||||||
|
headers.update(self.headers)
|
||||||
|
|
||||||
|
auth = None
|
||||||
|
if self.user:
|
||||||
|
auth = (self.user, self.password)
|
||||||
|
|
||||||
|
url = '%s://%s' % (self.schema, self.host)
|
||||||
|
if isinstance(self.port, int):
|
||||||
|
url += ':%d' % self.port
|
||||||
|
|
||||||
|
url += self.fullpath
|
||||||
|
|
||||||
|
# Where our request object will temporarily live.
|
||||||
|
r = None
|
||||||
|
|
||||||
|
# Always call throttle before any remote server i/o is made
|
||||||
|
self.throttle()
|
||||||
|
|
||||||
|
with self._lock:
|
||||||
|
if self.exists(retrieve_if_missing=False):
|
||||||
|
# Due to locking; it's possible a concurrent thread already
|
||||||
|
# handled the retrieval in which case we can safely move on
|
||||||
|
self.logger.trace(
|
||||||
|
'HTTP Attachment %s already retrieved',
|
||||||
|
self._temp_file.name)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Ensure any existing content set has been invalidated
|
||||||
|
self.invalidate()
|
||||||
|
|
||||||
|
self.logger.debug(
|
||||||
|
'HTTP Attachment Fetch URL: %s (cert_verify=%r)' % (
|
||||||
|
url, self.verify_certificate))
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Make our request
|
||||||
|
with requests.get(
|
||||||
|
url,
|
||||||
|
headers=headers,
|
||||||
|
auth=auth,
|
||||||
|
params=self.qsd,
|
||||||
|
verify=self.verify_certificate,
|
||||||
|
timeout=self.request_timeout,
|
||||||
|
stream=True) as r:
|
||||||
|
|
||||||
|
# Handle Errors
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
# Get our file-size (if known)
|
||||||
|
try:
|
||||||
|
file_size = int(r.headers.get('Content-Length', '0'))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
# Handle edge case where Content-Length is a bad value
|
||||||
|
file_size = 0
|
||||||
|
|
||||||
|
# Perform a little Q/A on file limitations and restrictions
|
||||||
|
if self.max_file_size > 0 and \
|
||||||
|
file_size > self.max_file_size:
|
||||||
|
|
||||||
|
# The content retrieved is to large
|
||||||
|
self.logger.error(
|
||||||
|
'HTTP response exceeds allowable maximum file '
|
||||||
|
'length ({}KB): {}'.format(
|
||||||
|
int(self.max_file_size / 1024),
|
||||||
|
self.url(privacy=True)))
|
||||||
|
|
||||||
|
# Return False (signifying a failure)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Detect config format based on mime if the format isn't
|
||||||
|
# already enforced
|
||||||
|
self.detected_mimetype = r.headers.get('Content-Type')
|
||||||
|
|
||||||
|
d = r.headers.get('Content-Disposition', '')
|
||||||
|
result = re.search(
|
||||||
|
"filename=['\"]?(?P<name>[^'\"]+)['\"]?", d, re.I)
|
||||||
|
if result:
|
||||||
|
self.detected_name = result.group('name').strip()
|
||||||
|
|
||||||
|
# Create a temporary file to work with; delete must be set
|
||||||
|
# to False or it isn't compatible with Microsoft Windows
|
||||||
|
# instances. In lieu of this, __del__ will clean up the
|
||||||
|
# file for us.
|
||||||
|
self._temp_file = NamedTemporaryFile(delete=False)
|
||||||
|
|
||||||
|
# Get our chunk size
|
||||||
|
chunk_size = self.chunk_size
|
||||||
|
|
||||||
|
# Track all bytes written to disk
|
||||||
|
bytes_written = 0
|
||||||
|
|
||||||
|
# If we get here, we can now safely write our content to
|
||||||
|
# disk
|
||||||
|
for chunk in r.iter_content(chunk_size=chunk_size):
|
||||||
|
# filter out keep-alive chunks
|
||||||
|
if chunk:
|
||||||
|
self._temp_file.write(chunk)
|
||||||
|
bytes_written = self._temp_file.tell()
|
||||||
|
|
||||||
|
# Prevent a case where Content-Length isn't
|
||||||
|
# provided. In this case we don't want to fetch
|
||||||
|
# beyond our limits
|
||||||
|
if self.max_file_size > 0:
|
||||||
|
if bytes_written > self.max_file_size:
|
||||||
|
# The content retrieved is to large
|
||||||
|
self.logger.error(
|
||||||
|
'HTTP response exceeds allowable '
|
||||||
|
'maximum file length '
|
||||||
|
'({}KB): {}'.format(
|
||||||
|
int(self.max_file_size / 1024),
|
||||||
|
self.url(privacy=True)))
|
||||||
|
|
||||||
|
# Invalidate any variables previously set
|
||||||
|
self.invalidate()
|
||||||
|
|
||||||
|
# Return False (signifying a failure)
|
||||||
|
return False
|
||||||
|
|
||||||
|
elif bytes_written + chunk_size \
|
||||||
|
> self.max_file_size:
|
||||||
|
# Adjust out next read to accomodate up to
|
||||||
|
# our limit +1. This will prevent us from
|
||||||
|
# reading to much into our memory buffer
|
||||||
|
self.max_file_size - bytes_written + 1
|
||||||
|
|
||||||
|
# Ensure our content is flushed to disk for post-processing
|
||||||
|
self._temp_file.flush()
|
||||||
|
|
||||||
|
# Set our minimum requirements for a successful download()
|
||||||
|
# call
|
||||||
|
self.download_path = self._temp_file.name
|
||||||
|
if not self.detected_name:
|
||||||
|
self.detected_name = os.path.basename(self.fullpath)
|
||||||
|
|
||||||
|
except requests.RequestException as e:
|
||||||
|
self.logger.error(
|
||||||
|
'A Connection error occurred retrieving HTTP '
|
||||||
|
'configuration from %s.' % self.host)
|
||||||
|
self.logger.debug('Socket Exception: %s' % str(e))
|
||||||
|
|
||||||
|
# Invalidate any variables previously set
|
||||||
|
self.invalidate()
|
||||||
|
|
||||||
|
# Return False (signifying a failure)
|
||||||
|
return False
|
||||||
|
|
||||||
|
except (IOError, OSError):
|
||||||
|
# IOError is present for backwards compatibility with Python
|
||||||
|
# versions older then 3.3. >= 3.3 throw OSError now.
|
||||||
|
|
||||||
|
# Could not open and/or write the temporary file
|
||||||
|
self.logger.error(
|
||||||
|
'Could not write attachment to disk: {}'.format(
|
||||||
|
self.url(privacy=True)))
|
||||||
|
|
||||||
|
# Invalidate any variables previously set
|
||||||
|
self.invalidate()
|
||||||
|
|
||||||
|
# Return False (signifying a failure)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Return our success
|
||||||
|
return True
|
||||||
|
|
||||||
|
def invalidate(self):
|
||||||
|
"""
|
||||||
|
Close our temporary file
|
||||||
|
"""
|
||||||
|
if self._temp_file:
|
||||||
|
self.logger.trace(
|
||||||
|
'Attachment cleanup of %s', self._temp_file.name)
|
||||||
|
self._temp_file.close()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Ensure our file is removed (if it exists)
|
||||||
|
os.unlink(self._temp_file.name)
|
||||||
|
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Reset our temporary file to prevent from entering
|
||||||
|
# this block again
|
||||||
|
self._temp_file = None
|
||||||
|
|
||||||
|
super().invalidate()
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
"""
|
||||||
|
Tidy memory if open
|
||||||
|
"""
|
||||||
|
self.invalidate()
|
||||||
|
|
||||||
|
def url(self, privacy=False, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Returns the URL built dynamically based on specified arguments.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Our URL parameters
|
||||||
|
params = self.url_parameters(privacy=privacy, *args, **kwargs)
|
||||||
|
|
||||||
|
# Prepare our cache value
|
||||||
|
if self.cache is not None:
|
||||||
|
if isinstance(self.cache, bool) or not self.cache:
|
||||||
|
cache = 'yes' if self.cache else 'no'
|
||||||
|
else:
|
||||||
|
cache = int(self.cache)
|
||||||
|
|
||||||
|
# Set our cache value
|
||||||
|
params['cache'] = cache
|
||||||
|
|
||||||
|
if self._mimetype:
|
||||||
|
# A format was enforced
|
||||||
|
params['mime'] = self._mimetype
|
||||||
|
|
||||||
|
if self._name:
|
||||||
|
# A name was enforced
|
||||||
|
params['name'] = self._name
|
||||||
|
|
||||||
|
# Append our headers into our parameters
|
||||||
|
params.update({'+{}'.format(k): v for k, v in self.headers.items()})
|
||||||
|
|
||||||
|
# Apply any remaining entries to our URL
|
||||||
|
params.update(self.qsd)
|
||||||
|
|
||||||
|
# Determine Authentication
|
||||||
|
auth = ''
|
||||||
|
if self.user and self.password:
|
||||||
|
auth = '{user}:{password}@'.format(
|
||||||
|
user=self.quote(self.user, safe=''),
|
||||||
|
password=self.pprint(
|
||||||
|
self.password, privacy, mode=PrivacyMode.Secret, safe=''),
|
||||||
|
)
|
||||||
|
elif self.user:
|
||||||
|
auth = '{user}@'.format(
|
||||||
|
user=self.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,
|
||||||
|
hostname=self.quote(self.host, safe=''),
|
||||||
|
port='' if self.port is None or self.port == default_port
|
||||||
|
else ':{}'.format(self.port),
|
||||||
|
fullpath=self.quote(self.fullpath, safe='/'),
|
||||||
|
params=self.urlencode(params, safe='/'),
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_url(url):
|
||||||
|
"""
|
||||||
|
Parses the URL and returns enough arguments that can allow
|
||||||
|
us to re-instantiate this object.
|
||||||
|
|
||||||
|
"""
|
||||||
|
results = AttachBase.parse_url(url, sanitize=False)
|
||||||
|
if not results:
|
||||||
|
# We're done early as we couldn't load the results
|
||||||
|
return results
|
||||||
|
|
||||||
|
# Add our headers that the user can potentially over-ride if they wish
|
||||||
|
# to to our returned result set
|
||||||
|
results['headers'] = results['qsd-']
|
||||||
|
results['headers'].update(results['qsd+'])
|
||||||
|
|
||||||
|
return results
|
212
lib/apprise/attachment/memory.py
Normal file
212
lib/apprise/attachment/memory.py
Normal file
|
@ -0,0 +1,212 @@
|
||||||
|
# -*- 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 re
|
||||||
|
import os
|
||||||
|
import io
|
||||||
|
from .base import AttachBase
|
||||||
|
from ..common import ContentLocation
|
||||||
|
from ..locale import gettext_lazy as _
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
class AttachMemory(AttachBase):
|
||||||
|
"""
|
||||||
|
A wrapper for Memory based attachment sources
|
||||||
|
"""
|
||||||
|
|
||||||
|
# The default descriptive name associated with the service
|
||||||
|
service_name = _('Memory')
|
||||||
|
|
||||||
|
# The default protocol
|
||||||
|
protocol = 'memory'
|
||||||
|
|
||||||
|
# Content is local to the same location as the apprise instance
|
||||||
|
# being called (server-side)
|
||||||
|
location = ContentLocation.LOCAL
|
||||||
|
|
||||||
|
def __init__(self, content=None, name=None, mimetype=None,
|
||||||
|
encoding='utf-8', **kwargs):
|
||||||
|
"""
|
||||||
|
Initialize Memory Based Attachment Object
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Create our BytesIO object
|
||||||
|
self._data = io.BytesIO()
|
||||||
|
|
||||||
|
if content is None:
|
||||||
|
# Empty; do nothing
|
||||||
|
pass
|
||||||
|
|
||||||
|
elif isinstance(content, str):
|
||||||
|
content = content.encode(encoding)
|
||||||
|
if mimetype is None:
|
||||||
|
mimetype = 'text/plain'
|
||||||
|
|
||||||
|
if not name:
|
||||||
|
# Generate a unique filename
|
||||||
|
name = str(uuid.uuid4()) + '.txt'
|
||||||
|
|
||||||
|
elif not isinstance(content, bytes):
|
||||||
|
raise TypeError(
|
||||||
|
'Provided content for memory attachment is invalid')
|
||||||
|
|
||||||
|
# Store our content
|
||||||
|
if content:
|
||||||
|
self._data.write(content)
|
||||||
|
|
||||||
|
if mimetype is None:
|
||||||
|
# Default mimetype
|
||||||
|
mimetype = 'application/octet-stream'
|
||||||
|
|
||||||
|
if not name:
|
||||||
|
# Generate a unique filename
|
||||||
|
name = str(uuid.uuid4()) + '.dat'
|
||||||
|
|
||||||
|
# Initialize our base object
|
||||||
|
super().__init__(name=name, mimetype=mimetype, **kwargs)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
def url(self, privacy=False, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Returns the URL built dynamically based on specified arguments.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Define any URL parameters
|
||||||
|
params = {
|
||||||
|
'mime': self._mimetype,
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'memory://{name}?{params}'.format(
|
||||||
|
name=self.quote(self._name),
|
||||||
|
params=self.urlencode(params, safe='/')
|
||||||
|
)
|
||||||
|
|
||||||
|
def open(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
return our memory object
|
||||||
|
"""
|
||||||
|
# Return our object
|
||||||
|
self._data.seek(0, 0)
|
||||||
|
return self._data
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
"""
|
||||||
|
support with clause
|
||||||
|
"""
|
||||||
|
# Return our object
|
||||||
|
self._data.seek(0, 0)
|
||||||
|
return self._data
|
||||||
|
|
||||||
|
def download(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Handle memory download() call
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self.location == ContentLocation.INACCESSIBLE:
|
||||||
|
# our content is inaccessible
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.max_file_size > 0 and len(self) > self.max_file_size:
|
||||||
|
# The content to attach is to large
|
||||||
|
self.logger.error(
|
||||||
|
'Content exceeds allowable maximum memory size '
|
||||||
|
'({}KB): {}'.format(
|
||||||
|
int(self.max_file_size / 1024), self.url(privacy=True)))
|
||||||
|
|
||||||
|
# Return False (signifying a failure)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def invalidate(self):
|
||||||
|
"""
|
||||||
|
Removes data
|
||||||
|
"""
|
||||||
|
self._data.truncate(0)
|
||||||
|
return
|
||||||
|
|
||||||
|
def exists(self):
|
||||||
|
"""
|
||||||
|
over-ride exists() call
|
||||||
|
"""
|
||||||
|
size = len(self)
|
||||||
|
return True if self.location != ContentLocation.INACCESSIBLE \
|
||||||
|
and size > 0 and (
|
||||||
|
self.max_file_size <= 0 or
|
||||||
|
(self.max_file_size > 0 and size <= self.max_file_size)) \
|
||||||
|
else False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_url(url):
|
||||||
|
"""
|
||||||
|
Parses the URL so that we can handle all different file paths
|
||||||
|
and return it as our path object
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
results = AttachBase.parse_url(url, verify_host=False)
|
||||||
|
if not results:
|
||||||
|
# We're done early; it's not a good URL
|
||||||
|
return results
|
||||||
|
|
||||||
|
if 'name' not in results:
|
||||||
|
# Allow fall-back to be from URL
|
||||||
|
match = re.match(r'memory://(?P<path>[^?]+)(\?.*)?', url, re.I)
|
||||||
|
if match:
|
||||||
|
# Store our filename only (ignore any defined paths)
|
||||||
|
results['name'] = \
|
||||||
|
os.path.basename(AttachMemory.unquote(match.group('path')))
|
||||||
|
return results
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path(self):
|
||||||
|
"""
|
||||||
|
return the filename
|
||||||
|
"""
|
||||||
|
if not self.exists():
|
||||||
|
# we could not obtain our path
|
||||||
|
return None
|
||||||
|
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
"""
|
||||||
|
Returns the size of he memory attachment
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self._data.getbuffer().nbytes
|
||||||
|
|
||||||
|
def __bool__(self):
|
||||||
|
"""
|
||||||
|
Allows the Apprise object to be wrapped in an based 'if statement'.
|
||||||
|
True is returned if our content was downloaded correctly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.exists()
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -26,50 +26,6 @@
|
||||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
# POSSIBILITY OF SUCH DAMAGE.
|
# POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
# we mirror our base purely for the ability to reset everything; this
|
|
||||||
# is generally only used in testing and should not be used by developers
|
|
||||||
# It is also used as a means of preventing a module from being reloaded
|
|
||||||
# in the event it already exists
|
|
||||||
NOTIFY_MODULE_MAP = {}
|
|
||||||
|
|
||||||
# Maintains a mapping of all of the Notification services
|
|
||||||
NOTIFY_SCHEMA_MAP = {}
|
|
||||||
|
|
||||||
# This contains a mapping of all plugins dynamicaly loaded at runtime from
|
|
||||||
# external modules such as the @notify decorator
|
|
||||||
#
|
|
||||||
# The elements here will be additionally added to the NOTIFY_SCHEMA_MAP if
|
|
||||||
# there is no conflict otherwise.
|
|
||||||
# The structure looks like the following:
|
|
||||||
# Module path, e.g. /usr/share/apprise/plugins/my_notify_hook.py
|
|
||||||
# {
|
|
||||||
# 'path': path,
|
|
||||||
#
|
|
||||||
# 'notify': {
|
|
||||||
# 'schema': {
|
|
||||||
# 'name': 'Custom schema name',
|
|
||||||
# 'fn_name': 'name_of_function_decorator_was_found_on',
|
|
||||||
# 'url': 'schema://any/additional/info/found/on/url'
|
|
||||||
# 'plugin': <CustomNotifyWrapperPlugin>
|
|
||||||
# },
|
|
||||||
# 'schema2': {
|
|
||||||
# 'name': 'Custom schema name',
|
|
||||||
# 'fn_name': 'name_of_function_decorator_was_found_on',
|
|
||||||
# 'url': 'schema://any/additional/info/found/on/url'
|
|
||||||
# 'plugin': <CustomNotifyWrapperPlugin>
|
|
||||||
# }
|
|
||||||
# }
|
|
||||||
#
|
|
||||||
# Note: that the <CustomNotifyWrapperPlugin> inherits from
|
|
||||||
# NotifyBase
|
|
||||||
NOTIFY_CUSTOM_MODULE_MAP = {}
|
|
||||||
|
|
||||||
# Maintains a mapping of all configuration schema's supported
|
|
||||||
CONFIG_SCHEMA_MAP = {}
|
|
||||||
|
|
||||||
# Maintains a mapping of all attachment schema's supported
|
|
||||||
ATTACHMENT_SCHEMA_MAP = {}
|
|
||||||
|
|
||||||
|
|
||||||
class NotifyType:
|
class NotifyType:
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -26,84 +26,15 @@
|
||||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
# POSSIBILITY OF SUCH DAMAGE.
|
# POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
import re
|
# Used for testing
|
||||||
from os import listdir
|
from .base import ConfigBase
|
||||||
from os.path import dirname
|
from ..manager_config import ConfigurationManager
|
||||||
from os.path import abspath
|
|
||||||
from ..logger import logger
|
|
||||||
from ..common import CONFIG_SCHEMA_MAP
|
|
||||||
|
|
||||||
__all__ = []
|
# Initalize our Config Manager Singleton
|
||||||
|
C_MGR = ConfigurationManager()
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
# Load our Lookup Matrix
|
# Reference
|
||||||
def __load_matrix(path=abspath(dirname(__file__)), name='apprise.config'):
|
'ConfigBase',
|
||||||
"""
|
'ConfigurationManager',
|
||||||
Dynamically load our schema map; this allows us to gracefully
|
]
|
||||||
skip over modules we simply don't have the dependencies for.
|
|
||||||
|
|
||||||
"""
|
|
||||||
# Used for the detection of additional Configuration Services objects
|
|
||||||
# The .py extension is optional as we support loading directories too
|
|
||||||
module_re = re.compile(r'^(?P<name>Config[a-z0-9]+)(\.py)?$', re.I)
|
|
||||||
|
|
||||||
for f in listdir(path):
|
|
||||||
match = module_re.match(f)
|
|
||||||
if not match:
|
|
||||||
# keep going
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Store our notification/plugin name:
|
|
||||||
plugin_name = match.group('name')
|
|
||||||
try:
|
|
||||||
module = __import__(
|
|
||||||
'{}.{}'.format(name, plugin_name),
|
|
||||||
globals(), locals(),
|
|
||||||
fromlist=[plugin_name])
|
|
||||||
|
|
||||||
except ImportError:
|
|
||||||
# No problem, we can't use this object
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not hasattr(module, plugin_name):
|
|
||||||
# Not a library we can load as it doesn't follow the simple rule
|
|
||||||
# that the class must bear the same name as the notification
|
|
||||||
# file itself.
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Get our plugin
|
|
||||||
plugin = getattr(module, plugin_name)
|
|
||||||
if not hasattr(plugin, 'app_id'):
|
|
||||||
# Filter out non-notification modules
|
|
||||||
continue
|
|
||||||
|
|
||||||
elif plugin_name in __all__:
|
|
||||||
# we're already handling this object
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Add our module name to our __all__
|
|
||||||
__all__.append(plugin_name)
|
|
||||||
|
|
||||||
# Ensure we provide the class as the reference to this directory and
|
|
||||||
# not the module:
|
|
||||||
globals()[plugin_name] = plugin
|
|
||||||
|
|
||||||
fn = getattr(plugin, 'schemas', None)
|
|
||||||
schemas = set([]) if not callable(fn) else fn(plugin)
|
|
||||||
|
|
||||||
# map our schema to our plugin
|
|
||||||
for schema in schemas:
|
|
||||||
if schema in CONFIG_SCHEMA_MAP:
|
|
||||||
logger.error(
|
|
||||||
"Config schema ({}) mismatch detected - {} to {}"
|
|
||||||
.format(schema, CONFIG_SCHEMA_MAP[schema], plugin))
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Assign plugin
|
|
||||||
CONFIG_SCHEMA_MAP[schema] = plugin
|
|
||||||
|
|
||||||
return CONFIG_SCHEMA_MAP
|
|
||||||
|
|
||||||
|
|
||||||
# Dynamically build our schema base
|
|
||||||
__load_matrix()
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -32,18 +32,26 @@ import time
|
||||||
|
|
||||||
from .. import plugins
|
from .. import plugins
|
||||||
from .. import common
|
from .. import common
|
||||||
from ..AppriseAsset import AppriseAsset
|
from ..asset import AppriseAsset
|
||||||
from ..URLBase import URLBase
|
from ..url import URLBase
|
||||||
from ..utils import GET_SCHEMA_RE
|
from ..utils import GET_SCHEMA_RE
|
||||||
from ..utils import parse_list
|
from ..utils import parse_list
|
||||||
from ..utils import parse_bool
|
from ..utils import parse_bool
|
||||||
from ..utils import parse_urls
|
from ..utils import parse_urls
|
||||||
from ..utils import cwe312_url
|
from ..utils import cwe312_url
|
||||||
|
from ..manager_config import ConfigurationManager
|
||||||
|
from ..manager_plugins import NotificationManager
|
||||||
|
|
||||||
# Test whether token is valid or not
|
# Test whether token is valid or not
|
||||||
VALID_TOKEN = re.compile(
|
VALID_TOKEN = re.compile(
|
||||||
r'(?P<token>[a-z0-9][a-z0-9_]+)', re.I)
|
r'(?P<token>[a-z0-9][a-z0-9_]+)', re.I)
|
||||||
|
|
||||||
|
# Grant access to our Notification Manager Singleton
|
||||||
|
N_MGR = NotificationManager()
|
||||||
|
|
||||||
|
# Grant access to our Configuration Manager Singleton
|
||||||
|
C_MGR = ConfigurationManager()
|
||||||
|
|
||||||
|
|
||||||
class ConfigBase(URLBase):
|
class ConfigBase(URLBase):
|
||||||
"""
|
"""
|
||||||
|
@ -228,7 +236,7 @@ class ConfigBase(URLBase):
|
||||||
schema = schema.group('schema').lower()
|
schema = schema.group('schema').lower()
|
||||||
|
|
||||||
# Some basic validation
|
# Some basic validation
|
||||||
if schema not in common.CONFIG_SCHEMA_MAP:
|
if schema not in C_MGR:
|
||||||
ConfigBase.logger.warning(
|
ConfigBase.logger.warning(
|
||||||
'Unsupported include schema {}.'.format(schema))
|
'Unsupported include schema {}.'.format(schema))
|
||||||
continue
|
continue
|
||||||
|
@ -239,7 +247,7 @@ class ConfigBase(URLBase):
|
||||||
|
|
||||||
# Parse our url details of the server object as dictionary
|
# Parse our url details of the server object as dictionary
|
||||||
# containing all of the information parsed from our URL
|
# containing all of the information parsed from our URL
|
||||||
results = common.CONFIG_SCHEMA_MAP[schema].parse_url(url)
|
results = C_MGR[schema].parse_url(url)
|
||||||
if not results:
|
if not results:
|
||||||
# Failed to parse the server URL
|
# Failed to parse the server URL
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
|
@ -247,11 +255,10 @@ class ConfigBase(URLBase):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Handle cross inclusion based on allow_cross_includes rules
|
# Handle cross inclusion based on allow_cross_includes rules
|
||||||
if (common.CONFIG_SCHEMA_MAP[schema].allow_cross_includes ==
|
if (C_MGR[schema].allow_cross_includes ==
|
||||||
common.ContentIncludeMode.STRICT
|
common.ContentIncludeMode.STRICT
|
||||||
and schema not in self.schemas()
|
and schema not in self.schemas()
|
||||||
and not self.insecure_includes) or \
|
and not self.insecure_includes) or C_MGR[schema] \
|
||||||
common.CONFIG_SCHEMA_MAP[schema] \
|
|
||||||
.allow_cross_includes == \
|
.allow_cross_includes == \
|
||||||
common.ContentIncludeMode.NEVER:
|
common.ContentIncludeMode.NEVER:
|
||||||
|
|
||||||
|
@ -279,8 +286,7 @@ class ConfigBase(URLBase):
|
||||||
try:
|
try:
|
||||||
# Attempt to create an instance of our plugin using the
|
# Attempt to create an instance of our plugin using the
|
||||||
# parsed URL information
|
# parsed URL information
|
||||||
cfg_plugin = \
|
cfg_plugin = C_MGR[results['schema']](**results)
|
||||||
common.CONFIG_SCHEMA_MAP[results['schema']](**results)
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# the arguments are invalid or can not be used.
|
# the arguments are invalid or can not be used.
|
||||||
|
@ -392,7 +398,11 @@ class ConfigBase(URLBase):
|
||||||
# Track our groups
|
# Track our groups
|
||||||
groups.add(tag)
|
groups.add(tag)
|
||||||
|
|
||||||
# Store what we know is worth keping
|
# Store what we know is worth keeping
|
||||||
|
if tag not in group_tags: # pragma: no cover
|
||||||
|
# handle cases where the tag doesn't exist
|
||||||
|
group_tags[tag] = set()
|
||||||
|
|
||||||
results |= group_tags[tag] - tag_groups
|
results |= group_tags[tag] - tag_groups
|
||||||
|
|
||||||
# Get simple tag assignments
|
# Get simple tag assignments
|
||||||
|
@ -753,8 +763,7 @@ class ConfigBase(URLBase):
|
||||||
try:
|
try:
|
||||||
# Attempt to create an instance of our plugin using the
|
# Attempt to create an instance of our plugin using the
|
||||||
# parsed URL information
|
# parsed URL information
|
||||||
plugin = common.NOTIFY_SCHEMA_MAP[
|
plugin = N_MGR[results['schema']](**results)
|
||||||
results['schema']](**results)
|
|
||||||
|
|
||||||
# Create log entry of loaded URL
|
# Create log entry of loaded URL
|
||||||
ConfigBase.logger.debug(
|
ConfigBase.logger.debug(
|
||||||
|
@ -807,8 +816,7 @@ class ConfigBase(URLBase):
|
||||||
# Create a copy of our dictionary
|
# Create a copy of our dictionary
|
||||||
tokens = tokens.copy()
|
tokens = tokens.copy()
|
||||||
|
|
||||||
for kw, meta in common.NOTIFY_SCHEMA_MAP[schema]\
|
for kw, meta in N_MGR[schema].template_kwargs.items():
|
||||||
.template_kwargs.items():
|
|
||||||
|
|
||||||
# Determine our prefix:
|
# Determine our prefix:
|
||||||
prefix = meta.get('prefix', '+')
|
prefix = meta.get('prefix', '+')
|
||||||
|
@ -851,8 +859,7 @@ class ConfigBase(URLBase):
|
||||||
#
|
#
|
||||||
# This function here allows these mappings to take place within the
|
# This function here allows these mappings to take place within the
|
||||||
# YAML file as independant arguments.
|
# YAML file as independant arguments.
|
||||||
class_templates = \
|
class_templates = plugins.details(N_MGR[schema])
|
||||||
plugins.details(common.NOTIFY_SCHEMA_MAP[schema])
|
|
||||||
|
|
||||||
for key in list(tokens.keys()):
|
for key in list(tokens.keys()):
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -28,10 +28,10 @@
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import os
|
import os
|
||||||
from .ConfigBase import ConfigBase
|
from .base import ConfigBase
|
||||||
from ..common import ConfigFormat
|
from ..common import ConfigFormat
|
||||||
from ..common import ContentIncludeMode
|
from ..common import ContentIncludeMode
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..locale import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class ConfigFile(ConfigBase):
|
class ConfigFile(ConfigBase):
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -28,11 +28,11 @@
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import requests
|
import requests
|
||||||
from .ConfigBase import ConfigBase
|
from .base import ConfigBase
|
||||||
from ..common import ConfigFormat
|
from ..common import ConfigFormat
|
||||||
from ..common import ContentIncludeMode
|
from ..common import ContentIncludeMode
|
||||||
from ..URLBase import PrivacyMode
|
from ..url import PrivacyMode
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..locale import gettext_lazy as _
|
||||||
|
|
||||||
# Support TEXT formats
|
# Support TEXT formats
|
||||||
# text/plain
|
# text/plain
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -26,8 +26,8 @@
|
||||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
# POSSIBILITY OF SUCH DAMAGE.
|
# POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
from .ConfigBase import ConfigBase
|
from .base import ConfigBase
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..locale import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class ConfigMemory(ConfigBase):
|
class ConfigMemory(ConfigBase):
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -29,7 +29,7 @@
|
||||||
import re
|
import re
|
||||||
#from markdown import markdown
|
#from markdown import markdown
|
||||||
from .common import NotifyFormat
|
from .common import NotifyFormat
|
||||||
from .URLBase import URLBase
|
from .url import URLBase
|
||||||
|
|
||||||
from html.parser import HTMLParser
|
from html.parser import HTMLParser
|
||||||
|
|
||||||
|
@ -58,8 +58,8 @@ def convert_between(from_format, to_format, content):
|
||||||
# """
|
# """
|
||||||
# Converts specified content from markdown to HTML.
|
# Converts specified content from markdown to HTML.
|
||||||
# """
|
# """
|
||||||
|
# return markdown(content, extensions=[
|
||||||
# return markdown(content)
|
# 'markdown.extensions.nl2br', 'markdown.extensions.tables'])
|
||||||
|
|
||||||
|
|
||||||
def text_to_html(content):
|
def text_to_html(content):
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -27,7 +27,8 @@
|
||||||
# POSSIBILITY OF SUCH DAMAGE.USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
# POSSIBILITY OF SUCH DAMAGE.USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
# POSSIBILITY OF SUCH DAMAGE.
|
# POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
from ..plugins.NotifyBase import NotifyBase
|
from ..plugins.base import NotifyBase
|
||||||
|
from ..manager_plugins import NotificationManager
|
||||||
from ..utils import URL_DETAILS_RE
|
from ..utils import URL_DETAILS_RE
|
||||||
from ..utils import parse_url
|
from ..utils import parse_url
|
||||||
from ..utils import url_assembly
|
from ..utils import url_assembly
|
||||||
|
@ -36,6 +37,9 @@ from .. import common
|
||||||
from ..logger import logger
|
from ..logger import logger
|
||||||
import inspect
|
import inspect
|
||||||
|
|
||||||
|
# Grant access to our Notification Manager Singleton
|
||||||
|
N_MGR = NotificationManager()
|
||||||
|
|
||||||
|
|
||||||
class CustomNotifyPlugin(NotifyBase):
|
class CustomNotifyPlugin(NotifyBase):
|
||||||
"""
|
"""
|
||||||
|
@ -51,6 +55,9 @@ class CustomNotifyPlugin(NotifyBase):
|
||||||
# should be treated differently.
|
# should be treated differently.
|
||||||
category = 'custom'
|
category = 'custom'
|
||||||
|
|
||||||
|
# Support Attachments
|
||||||
|
attachment_support = True
|
||||||
|
|
||||||
# Define object templates
|
# Define object templates
|
||||||
templates = (
|
templates = (
|
||||||
'{schema}://',
|
'{schema}://',
|
||||||
|
@ -91,17 +98,17 @@ class CustomNotifyPlugin(NotifyBase):
|
||||||
logger.warning(msg)
|
logger.warning(msg)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Acquire our plugin name
|
# Acquire our schema
|
||||||
plugin_name = re_match.group('schema').lower()
|
schema = re_match.group('schema').lower()
|
||||||
|
|
||||||
if not re_match.group('base'):
|
if not re_match.group('base'):
|
||||||
url = '{}://'.format(plugin_name)
|
url = '{}://'.format(schema)
|
||||||
|
|
||||||
# Keep a default set of arguments to apply to all called references
|
# Keep a default set of arguments to apply to all called references
|
||||||
base_args = parse_url(
|
base_args = parse_url(
|
||||||
url, default_schema=plugin_name, verify_host=False, simple=True)
|
url, default_schema=schema, verify_host=False, simple=True)
|
||||||
|
|
||||||
if plugin_name in common.NOTIFY_SCHEMA_MAP:
|
if schema in N_MGR:
|
||||||
# we're already handling this object
|
# we're already handling this object
|
||||||
msg = 'The schema ({}) is already defined and could not be ' \
|
msg = 'The schema ({}) is already defined and could not be ' \
|
||||||
'loaded from custom notify function {}.' \
|
'loaded from custom notify function {}.' \
|
||||||
|
@ -117,10 +124,10 @@ class CustomNotifyPlugin(NotifyBase):
|
||||||
|
|
||||||
# Our Service Name
|
# Our Service Name
|
||||||
service_name = name if isinstance(name, str) \
|
service_name = name if isinstance(name, str) \
|
||||||
and name else 'Custom - {}'.format(plugin_name)
|
and name else 'Custom - {}'.format(schema)
|
||||||
|
|
||||||
# Store our matched schema
|
# Store our matched schema
|
||||||
secure_protocol = plugin_name
|
secure_protocol = schema
|
||||||
|
|
||||||
requirements = {
|
requirements = {
|
||||||
# Define our required packaging in order to work
|
# Define our required packaging in order to work
|
||||||
|
@ -143,6 +150,10 @@ class CustomNotifyPlugin(NotifyBase):
|
||||||
|
|
||||||
self._default_args = {}
|
self._default_args = {}
|
||||||
|
|
||||||
|
# Some variables do not need to be set
|
||||||
|
if 'secure' in kwargs:
|
||||||
|
del kwargs['secure']
|
||||||
|
|
||||||
# Apply our updates based on what was parsed
|
# Apply our updates based on what was parsed
|
||||||
dict_full_update(self._default_args, self._base_args)
|
dict_full_update(self._default_args, self._base_args)
|
||||||
dict_full_update(self._default_args, kwargs)
|
dict_full_update(self._default_args, kwargs)
|
||||||
|
@ -181,51 +192,26 @@ class CustomNotifyPlugin(NotifyBase):
|
||||||
# Unhandled Exception
|
# Unhandled Exception
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
'An exception occured sending a %s notification.',
|
'An exception occured sending a %s notification.',
|
||||||
common.
|
N_MGR[self.secure_protocol].service_name)
|
||||||
NOTIFY_SCHEMA_MAP[self.secure_protocol].service_name)
|
|
||||||
self.logger.debug(
|
self.logger.debug(
|
||||||
'%s Exception: %s',
|
'%s Exception: %s',
|
||||||
common.NOTIFY_SCHEMA_MAP[self.secure_protocol], str(e))
|
N_MGR[self.secure_protocol], str(e))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if response:
|
if response:
|
||||||
self.logger.info(
|
self.logger.info(
|
||||||
'Sent %s notification.',
|
'Sent %s notification.',
|
||||||
common.
|
N_MGR[self.secure_protocol].service_name)
|
||||||
NOTIFY_SCHEMA_MAP[self.secure_protocol].service_name)
|
|
||||||
else:
|
else:
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
'Failed to send %s notification.',
|
'Failed to send %s notification.',
|
||||||
common.
|
N_MGR[self.secure_protocol].service_name)
|
||||||
NOTIFY_SCHEMA_MAP[self.secure_protocol].service_name)
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
# Store our plugin into our core map file
|
# Store our plugin into our core map file
|
||||||
common.NOTIFY_SCHEMA_MAP[plugin_name] = CustomNotifyPluginWrapper
|
return N_MGR.add(
|
||||||
|
plugin=CustomNotifyPluginWrapper,
|
||||||
# Update our custom plugin map
|
schemas=schema,
|
||||||
module_pyname = str(send_func.__module__)
|
send_func=send_func,
|
||||||
if module_pyname not in common.NOTIFY_CUSTOM_MODULE_MAP:
|
url=url,
|
||||||
# Support non-dynamic includes as well...
|
)
|
||||||
common.NOTIFY_CUSTOM_MODULE_MAP[module_pyname] = {
|
|
||||||
'path': inspect.getfile(send_func),
|
|
||||||
|
|
||||||
# Initialize our template
|
|
||||||
'notify': {},
|
|
||||||
}
|
|
||||||
|
|
||||||
common.\
|
|
||||||
NOTIFY_CUSTOM_MODULE_MAP[module_pyname]['notify'][plugin_name] = {
|
|
||||||
# Our Serivice Description (for API and CLI --details view)
|
|
||||||
'name': CustomNotifyPluginWrapper.service_name,
|
|
||||||
# The name of the send function the @notify decorator wrapped
|
|
||||||
'fn_name': send_func.__name__,
|
|
||||||
# The URL that was provided in the @notify decorator call
|
|
||||||
# associated with the 'on='
|
|
||||||
'url': url,
|
|
||||||
# The Initialized Plugin that was generated based on the above
|
|
||||||
# parameters
|
|
||||||
'plugin': CustomNotifyPluginWrapper}
|
|
||||||
|
|
||||||
# return our plugin
|
|
||||||
return common.NOTIFY_SCHEMA_MAP[plugin_name]
|
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -26,7 +26,7 @@
|
||||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
# POSSIBILITY OF SUCH DAMAGE.
|
# POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
from .CustomNotifyPlugin import CustomNotifyPlugin
|
from .base import CustomNotifyPlugin
|
||||||
|
|
||||||
|
|
||||||
def notify(on, name=None):
|
def notify(on, name=None):
|
||||||
|
|
2273
lib/apprise/emojis.py
Normal file
2273
lib/apprise/emojis.py
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,21 +1,21 @@
|
||||||
# Translations template for apprise.
|
# Translations template for apprise.
|
||||||
# Copyright (C) 2023 Chris Caron
|
# Copyright (C) 2024 Chris Caron
|
||||||
# This file is distributed under the same license as the apprise project.
|
# This file is distributed under the same license as the apprise project.
|
||||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2023.
|
# FIRST AUTHOR <EMAIL@ADDRESS>, 2024.
|
||||||
#
|
#
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: apprise 1.6.0\n"
|
"Project-Id-Version: apprise 1.8.0\n"
|
||||||
"Report-Msgid-Bugs-To: lead2gold@gmail.com\n"
|
"Report-Msgid-Bugs-To: lead2gold@gmail.com\n"
|
||||||
"POT-Creation-Date: 2023-10-15 15:56-0400\n"
|
"POT-Creation-Date: 2024-05-11 16:13-0400\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=utf-8\n"
|
"Content-Type: text/plain; charset=utf-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Generated-By: Babel 2.11.0\n"
|
"Generated-By: Babel 2.13.1\n"
|
||||||
|
|
||||||
msgid "A local Gnome environment is required."
|
msgid "A local Gnome environment is required."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -32,6 +32,9 @@ msgstr ""
|
||||||
msgid "API Secret"
|
msgid "API Secret"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "API Token"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Access Key"
|
msgid "Access Key"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -101,9 +104,6 @@ msgstr ""
|
||||||
msgid "Authentication Type"
|
msgid "Authentication Type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Authorization Token"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Avatar Image"
|
msgid "Avatar Image"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -125,6 +125,9 @@ msgstr ""
|
||||||
msgid "Bot Token"
|
msgid "Bot Token"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Bot Webhook Key"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Cache Age"
|
msgid "Cache Age"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -140,9 +143,15 @@ msgstr ""
|
||||||
msgid "Category"
|
msgid "Category"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Channel ID"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Channels"
|
msgid "Channels"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Chantify"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Class"
|
msgid "Class"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -230,6 +239,9 @@ msgstr ""
|
||||||
msgid "Email Header"
|
msgid "Email Header"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Embed URL"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Entity"
|
msgid "Entity"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -245,6 +257,9 @@ msgstr ""
|
||||||
msgid "Facility"
|
msgid "Facility"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Feishu"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Fetch Method"
|
msgid "Fetch Method"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -266,6 +281,9 @@ msgstr ""
|
||||||
msgid "Forced Mime Type"
|
msgid "Forced Mime Type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Free-Mobile"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "From Email"
|
msgid "From Email"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -281,6 +299,12 @@ msgstr ""
|
||||||
msgid "GET Params"
|
msgid "GET Params"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Gateway"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Gateway ID"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Gnome Notification"
|
msgid "Gnome Notification"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -299,6 +323,9 @@ msgstr ""
|
||||||
msgid "Icon Type"
|
msgid "Icon Type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Icon URL"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Idempotency-Key"
|
msgid "Idempotency-Key"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -323,6 +350,9 @@ msgstr ""
|
||||||
msgid "Integration Key"
|
msgid "Integration Key"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Interpret Emojis"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Is Ad?"
|
msgid "Is Ad?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -344,6 +374,9 @@ msgstr ""
|
||||||
msgid "Local File"
|
msgid "Local File"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Locale"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Log PID"
|
msgid "Log PID"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -356,6 +389,9 @@ msgstr ""
|
||||||
msgid "MacOSX Notification"
|
msgid "MacOSX Notification"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Markdown Version"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Master Key"
|
msgid "Master Key"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -490,6 +526,9 @@ msgstr ""
|
||||||
msgid "Reply To Email"
|
msgid "Reply To Email"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Resend Delay"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Resubmit Flag"
|
msgid "Resubmit Flag"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -661,6 +700,9 @@ msgstr ""
|
||||||
msgid "Target Team"
|
msgid "Target Team"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Target Threema ID"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Target Topic"
|
msgid "Target Topic"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -757,6 +799,9 @@ msgstr ""
|
||||||
msgid "Unicode Characters"
|
msgid "Unicode Characters"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Upload"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Urgency"
|
msgid "Urgency"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -775,9 +820,6 @@ msgstr ""
|
||||||
msgid "User Email"
|
msgid "User Email"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "User ID"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "User Key"
|
msgid "User Key"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -219,6 +219,9 @@ class AppriseLocale:
|
||||||
try:
|
try:
|
||||||
# Acquire our locale
|
# Acquire our locale
|
||||||
lang = locale.getlocale()[0]
|
lang = locale.getlocale()[0]
|
||||||
|
# Compatibility for Python >= 3.12
|
||||||
|
if lang == 'C':
|
||||||
|
lang = AppriseLocale._default_language
|
||||||
|
|
||||||
except (ValueError, TypeError) as e:
|
except (ValueError, TypeError) as e:
|
||||||
# This occurs when an invalid locale was parsed from the
|
# This occurs when an invalid locale was parsed from the
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
|
756
lib/apprise/manager.py
Normal file
756
lib/apprise/manager.py
Normal file
|
@ -0,0 +1,756 @@
|
||||||
|
# -*- 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 os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import hashlib
|
||||||
|
import inspect
|
||||||
|
import threading
|
||||||
|
from .utils import import_module
|
||||||
|
from .utils import Singleton
|
||||||
|
from .utils import parse_list
|
||||||
|
from os.path import dirname
|
||||||
|
from os.path import abspath
|
||||||
|
from os.path import join
|
||||||
|
|
||||||
|
from .logger import logger
|
||||||
|
|
||||||
|
|
||||||
|
class PluginManager(metaclass=Singleton):
|
||||||
|
"""
|
||||||
|
Designed to be a singleton object to maintain all initialized loading
|
||||||
|
of modules in memory.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Description (used for logging)
|
||||||
|
name = 'Singleton Plugin'
|
||||||
|
|
||||||
|
# Memory Space
|
||||||
|
_id = 'undefined'
|
||||||
|
|
||||||
|
# Our Module Python path name
|
||||||
|
module_name_prefix = f'apprise.{_id}'
|
||||||
|
|
||||||
|
# The module path to scan
|
||||||
|
module_path = join(abspath(dirname(__file__)), _id)
|
||||||
|
|
||||||
|
# For filtering our result when scanning a module
|
||||||
|
module_filter_re = re.compile(r'^(?P<name>((?!_)[A-Za-z0-9]+))$')
|
||||||
|
|
||||||
|
# thread safe loading
|
||||||
|
_lock = threading.Lock()
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Over-ride our class instantiation to provide a singleton
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._module_map = None
|
||||||
|
self._schema_map = None
|
||||||
|
|
||||||
|
# This contains a mapping of all plugins dynamicaly loaded at runtime
|
||||||
|
# from external modules such as the @notify decorator
|
||||||
|
#
|
||||||
|
# The elements here will be additionally added to the _schema_map if
|
||||||
|
# there is no conflict otherwise.
|
||||||
|
# The structure looks like the following:
|
||||||
|
# Module path, e.g. /usr/share/apprise/plugins/my_notify_hook.py
|
||||||
|
# {
|
||||||
|
# 'path': path,
|
||||||
|
#
|
||||||
|
# 'notify': {
|
||||||
|
# 'schema': {
|
||||||
|
# 'name': 'Custom schema name',
|
||||||
|
# 'fn_name': 'name_of_function_decorator_was_found_on',
|
||||||
|
# 'url': 'schema://any/additional/info/found/on/url'
|
||||||
|
# 'plugin': <CustomNotifyWrapperPlugin>
|
||||||
|
# },
|
||||||
|
# 'schema2': {
|
||||||
|
# 'name': 'Custom schema name',
|
||||||
|
# 'fn_name': 'name_of_function_decorator_was_found_on',
|
||||||
|
# 'url': 'schema://any/additional/info/found/on/url'
|
||||||
|
# 'plugin': <CustomNotifyWrapperPlugin>
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
# Note: that the <CustomNotifyWrapperPlugin> inherits from
|
||||||
|
# NotifyBase
|
||||||
|
self._custom_module_map = {}
|
||||||
|
|
||||||
|
# Track manually disabled modules (by their schema)
|
||||||
|
self._disabled = set()
|
||||||
|
|
||||||
|
# Hash of all paths previously scanned so we don't waste
|
||||||
|
# effort/overhead doing it again
|
||||||
|
self._paths_previously_scanned = set()
|
||||||
|
|
||||||
|
# Track loaded module paths to prevent from loading them again
|
||||||
|
self._loaded = set()
|
||||||
|
|
||||||
|
def unload_modules(self, disable_native=False):
|
||||||
|
"""
|
||||||
|
Reset our object and unload all modules
|
||||||
|
"""
|
||||||
|
|
||||||
|
with self._lock:
|
||||||
|
if self._custom_module_map:
|
||||||
|
# Handle Custom Module Assignments
|
||||||
|
for meta in self._custom_module_map.values():
|
||||||
|
if meta['name'] not in self._module_map:
|
||||||
|
# Nothing to remove
|
||||||
|
continue
|
||||||
|
|
||||||
|
# For the purpose of tidying up un-used modules in memory
|
||||||
|
loaded = [m for m in sys.modules.keys()
|
||||||
|
if m.startswith(
|
||||||
|
self._module_map[meta['name']]['path'])]
|
||||||
|
|
||||||
|
for module_path in loaded:
|
||||||
|
del sys.modules[module_path]
|
||||||
|
|
||||||
|
# Reset disabled plugins (if any)
|
||||||
|
for schema in self._disabled:
|
||||||
|
self._schema_map[schema].enabled = True
|
||||||
|
self._disabled.clear()
|
||||||
|
|
||||||
|
# Reset our variables
|
||||||
|
self._schema_map = {}
|
||||||
|
self._custom_module_map = {}
|
||||||
|
if disable_native:
|
||||||
|
self._module_map = {}
|
||||||
|
|
||||||
|
else:
|
||||||
|
self._module_map = None
|
||||||
|
self._loaded = set()
|
||||||
|
|
||||||
|
# Reset our path cache
|
||||||
|
self._paths_previously_scanned = set()
|
||||||
|
|
||||||
|
def load_modules(self, path=None, name=None, force=False):
|
||||||
|
"""
|
||||||
|
Load our modules into memory
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Default value
|
||||||
|
module_name_prefix = self.module_name_prefix if name is None else name
|
||||||
|
module_path = self.module_path if path is None else path
|
||||||
|
|
||||||
|
with self._lock:
|
||||||
|
if not force and module_path in self._loaded:
|
||||||
|
# We're done
|
||||||
|
return
|
||||||
|
|
||||||
|
# Our base reference
|
||||||
|
module_count = len(self._module_map) if self._module_map else 0
|
||||||
|
schema_count = len(self._schema_map) if self._schema_map else 0
|
||||||
|
|
||||||
|
if not self:
|
||||||
|
# Initialize our maps
|
||||||
|
self._module_map = {}
|
||||||
|
self._schema_map = {}
|
||||||
|
self._custom_module_map = {}
|
||||||
|
|
||||||
|
# Used for the detection of additional Notify Services objects
|
||||||
|
# The .py extension is optional as we support loading directories
|
||||||
|
# too
|
||||||
|
module_re = re.compile(
|
||||||
|
r'^(?P<name>(?!base|_)[a-z0-9_]+)(\.py)?$',
|
||||||
|
re.I)
|
||||||
|
|
||||||
|
t_start = time.time()
|
||||||
|
for f in os.listdir(module_path):
|
||||||
|
tl_start = time.time()
|
||||||
|
match = module_re.match(f)
|
||||||
|
if not match:
|
||||||
|
# keep going
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Store our notification/plugin name:
|
||||||
|
module_name = match.group('name')
|
||||||
|
module_pyname = '{}.{}'.format(module_name_prefix, module_name)
|
||||||
|
|
||||||
|
if module_name in self._module_map:
|
||||||
|
logger.warning(
|
||||||
|
"%s(s) (%s) already loaded; ignoring %s",
|
||||||
|
self.name, module_name, os.path.join(module_path, f))
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
module = __import__(
|
||||||
|
module_pyname,
|
||||||
|
globals(), locals(),
|
||||||
|
fromlist=[module_name])
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
# No problem, we can try again another way...
|
||||||
|
module = import_module(
|
||||||
|
os.path.join(module_path, f), module_pyname)
|
||||||
|
if not module:
|
||||||
|
# logging found in import_module and not needed here
|
||||||
|
continue
|
||||||
|
|
||||||
|
module_class = None
|
||||||
|
for m_class in [obj for obj in dir(module)
|
||||||
|
if self.module_filter_re.match(obj)]:
|
||||||
|
# Get our plugin
|
||||||
|
plugin = getattr(module, m_class)
|
||||||
|
if not hasattr(plugin, 'app_id'):
|
||||||
|
# Filter out non-notification modules
|
||||||
|
logger.trace(
|
||||||
|
"(%s) import failed; no app_id defined in %s",
|
||||||
|
self.name, m_class, os.path.join(module_path, f))
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Add our plugin name to our module map
|
||||||
|
self._module_map[module_name] = {
|
||||||
|
'plugin': set([plugin]),
|
||||||
|
'module': module,
|
||||||
|
'path': '{}.{}'.format(
|
||||||
|
module_name_prefix, module_name),
|
||||||
|
'native': True,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn = getattr(plugin, 'schemas', None)
|
||||||
|
schemas = set([]) if not callable(fn) else fn(plugin)
|
||||||
|
|
||||||
|
# map our schema to our plugin
|
||||||
|
for schema in schemas:
|
||||||
|
if schema in self._schema_map:
|
||||||
|
logger.error(
|
||||||
|
"{} schema ({}) mismatch detected - {} to {}"
|
||||||
|
.format(self.name, schema, self._schema_map,
|
||||||
|
plugin))
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Assign plugin
|
||||||
|
self._schema_map[schema] = plugin
|
||||||
|
|
||||||
|
# Store our class
|
||||||
|
module_class = m_class
|
||||||
|
break
|
||||||
|
|
||||||
|
if not module_class:
|
||||||
|
# Not a library we can load as it doesn't follow the simple
|
||||||
|
# rule that the class must bear the same name as the
|
||||||
|
# notification file itself.
|
||||||
|
logger.trace(
|
||||||
|
"%s (%s) import failed; no filename/Class "
|
||||||
|
"match found in %s",
|
||||||
|
self.name, module_name, os.path.join(module_path, f))
|
||||||
|
continue
|
||||||
|
|
||||||
|
logger.trace(
|
||||||
|
'{} {} loaded in {:.6f}s'.format(
|
||||||
|
self.name, module_name, (time.time() - tl_start)))
|
||||||
|
|
||||||
|
# Track the directory loaded so we never load it again
|
||||||
|
self._loaded.add(module_path)
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
'{} {}(s) and {} Schema(s) loaded in {:.4f}s'
|
||||||
|
.format(
|
||||||
|
self.name,
|
||||||
|
len(self._module_map) - module_count,
|
||||||
|
len(self._schema_map) - schema_count,
|
||||||
|
(time.time() - t_start)))
|
||||||
|
|
||||||
|
def module_detection(self, paths, cache=True):
|
||||||
|
"""
|
||||||
|
Leverage the @notify decorator and load all objects found matching
|
||||||
|
this.
|
||||||
|
"""
|
||||||
|
# A simple restriction that we don't allow periods in the filename at
|
||||||
|
# all so it can't be hidden (Linux OS's) and it won't conflict with
|
||||||
|
# Python path naming. This also prevents us from loading any python
|
||||||
|
# file that starts with an underscore or dash
|
||||||
|
# We allow for __init__.py as well
|
||||||
|
module_re = re.compile(
|
||||||
|
r'^(?P<name>[_a-z0-9][a-z0-9._-]+)?(\.py)?$', re.I)
|
||||||
|
|
||||||
|
# Validate if we're a loadable Python file or not
|
||||||
|
valid_python_file_re = re.compile(r'.+\.py(o|c)?$', re.IGNORECASE)
|
||||||
|
|
||||||
|
if isinstance(paths, str):
|
||||||
|
paths = [paths, ]
|
||||||
|
|
||||||
|
if not paths or not isinstance(paths, (tuple, list)):
|
||||||
|
# We're done
|
||||||
|
return
|
||||||
|
|
||||||
|
def _import_module(path):
|
||||||
|
# Since our plugin name can conflict (as a module) with another
|
||||||
|
# we want to generate random strings to avoid steping on
|
||||||
|
# another's namespace
|
||||||
|
if not (path and valid_python_file_re.match(path)):
|
||||||
|
# Ignore file/module type
|
||||||
|
logger.trace('Plugin Scan: Skipping %s', path)
|
||||||
|
return
|
||||||
|
|
||||||
|
t_start = time.time()
|
||||||
|
module_name = hashlib.sha1(path.encode('utf-8')).hexdigest()
|
||||||
|
module_pyname = "{prefix}.{name}".format(
|
||||||
|
prefix='apprise.custom.module', name=module_name)
|
||||||
|
|
||||||
|
if module_pyname in self._custom_module_map:
|
||||||
|
# First clear out existing entries
|
||||||
|
for schema in \
|
||||||
|
self._custom_module_map[module_pyname]['notify']\
|
||||||
|
.keys():
|
||||||
|
|
||||||
|
# Remove any mapped modules to this file
|
||||||
|
del self._schema_map[schema]
|
||||||
|
|
||||||
|
# Reset
|
||||||
|
del self._custom_module_map[module_pyname]
|
||||||
|
|
||||||
|
# Load our module
|
||||||
|
module = import_module(path, module_pyname)
|
||||||
|
if not module:
|
||||||
|
# No problem, we can't use this object
|
||||||
|
logger.warning('Failed to load custom module: %s', _path)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Print our loaded modules if any
|
||||||
|
if module_pyname in self._custom_module_map:
|
||||||
|
logger.debug(
|
||||||
|
'Custom module %s - %d schema(s) (name=%s) '
|
||||||
|
'loaded in %.6fs', _path,
|
||||||
|
len(self._custom_module_map[module_pyname]['notify']),
|
||||||
|
module_name, (time.time() - t_start))
|
||||||
|
|
||||||
|
# Add our plugin name to our module map
|
||||||
|
self._module_map[module_name] = {
|
||||||
|
'plugin': set(),
|
||||||
|
'module': module,
|
||||||
|
'path': module_pyname,
|
||||||
|
'native': False,
|
||||||
|
}
|
||||||
|
|
||||||
|
for schema, meta in\
|
||||||
|
self._custom_module_map[module_pyname]['notify']\
|
||||||
|
.items():
|
||||||
|
|
||||||
|
# For mapping purposes; map our element in our main list
|
||||||
|
self._module_map[module_name]['plugin'].add(
|
||||||
|
self._schema_map[schema])
|
||||||
|
|
||||||
|
# Log our success
|
||||||
|
logger.info('Loaded custom notification: %s://', schema)
|
||||||
|
else:
|
||||||
|
# The code reaches here if we successfully loaded the Python
|
||||||
|
# module but no hooks/triggers were found. So we can safely
|
||||||
|
# just remove/ignore this entry
|
||||||
|
del sys.modules[module_pyname]
|
||||||
|
return
|
||||||
|
|
||||||
|
# end of _import_module()
|
||||||
|
return
|
||||||
|
|
||||||
|
for _path in paths:
|
||||||
|
path = os.path.abspath(os.path.expanduser(_path))
|
||||||
|
if (cache and path in self._paths_previously_scanned) \
|
||||||
|
or not os.path.exists(path):
|
||||||
|
# We're done as we've already scanned this
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Store our path as a way of hashing it has been handled
|
||||||
|
self._paths_previously_scanned.add(path)
|
||||||
|
|
||||||
|
if os.path.isdir(path) and not \
|
||||||
|
os.path.isfile(os.path.join(path, '__init__.py')):
|
||||||
|
|
||||||
|
logger.debug('Scanning for custom plugins in: %s', path)
|
||||||
|
for entry in os.listdir(path):
|
||||||
|
re_match = module_re.match(entry)
|
||||||
|
if not re_match:
|
||||||
|
# keep going
|
||||||
|
logger.trace('Plugin Scan: Ignoring %s', entry)
|
||||||
|
continue
|
||||||
|
|
||||||
|
new_path = os.path.join(path, entry)
|
||||||
|
if os.path.isdir(new_path):
|
||||||
|
# Update our path
|
||||||
|
new_path = os.path.join(path, entry, '__init__.py')
|
||||||
|
if not os.path.isfile(new_path):
|
||||||
|
logger.trace(
|
||||||
|
'Plugin Scan: Ignoring %s',
|
||||||
|
os.path.join(path, entry))
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not cache or \
|
||||||
|
(cache and new_path not in
|
||||||
|
self._paths_previously_scanned):
|
||||||
|
# Load our module
|
||||||
|
_import_module(new_path)
|
||||||
|
|
||||||
|
# Add our subdir path
|
||||||
|
self._paths_previously_scanned.add(new_path)
|
||||||
|
else:
|
||||||
|
if os.path.isdir(path):
|
||||||
|
# This logic is safe to apply because we already
|
||||||
|
# validated the directories state above; update our
|
||||||
|
# path
|
||||||
|
path = os.path.join(path, '__init__.py')
|
||||||
|
if cache and path in self._paths_previously_scanned:
|
||||||
|
continue
|
||||||
|
|
||||||
|
self._paths_previously_scanned.add(path)
|
||||||
|
|
||||||
|
# directly load as is
|
||||||
|
re_match = module_re.match(os.path.basename(path))
|
||||||
|
# must be a match and must have a .py extension
|
||||||
|
if not re_match or not re_match.group(1):
|
||||||
|
# keep going
|
||||||
|
logger.trace('Plugin Scan: Ignoring %s', path)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Load our module
|
||||||
|
_import_module(path)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def add(self, plugin, schemas=None, url=None, send_func=None):
|
||||||
|
"""
|
||||||
|
Ability to manually add Notification services to our stack
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not self:
|
||||||
|
# Lazy load
|
||||||
|
self.load_modules()
|
||||||
|
|
||||||
|
# Acquire a list of schemas
|
||||||
|
p_schemas = parse_list(plugin.secure_protocol, plugin.protocol)
|
||||||
|
if isinstance(schemas, str):
|
||||||
|
schemas = [schemas, ]
|
||||||
|
|
||||||
|
elif schemas is None:
|
||||||
|
# Default
|
||||||
|
schemas = p_schemas
|
||||||
|
|
||||||
|
if not schemas or not isinstance(schemas, (set, tuple, list)):
|
||||||
|
# We're done
|
||||||
|
logger.error(
|
||||||
|
'The schemas provided (type %s) is unsupported; '
|
||||||
|
'loaded from %s.',
|
||||||
|
type(schemas),
|
||||||
|
send_func.__name__ if send_func else plugin.__class__.__name__)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Convert our schemas into a set
|
||||||
|
schemas = set([s.lower() for s in schemas]) | set(p_schemas)
|
||||||
|
|
||||||
|
# Valdation
|
||||||
|
conflict = [s for s in schemas if s in self]
|
||||||
|
if conflict:
|
||||||
|
# we're already handling this schema
|
||||||
|
logger.warning(
|
||||||
|
'The schema(s) (%s) are already defined and could not be '
|
||||||
|
'loaded from %s%s.',
|
||||||
|
', '.join(conflict),
|
||||||
|
'custom notify function ' if send_func else '',
|
||||||
|
send_func.__name__ if send_func else plugin.__class__.__name__)
|
||||||
|
return False
|
||||||
|
|
||||||
|
if send_func:
|
||||||
|
# Acquire the function name
|
||||||
|
fn_name = send_func.__name__
|
||||||
|
|
||||||
|
# Acquire the python filename path
|
||||||
|
path = inspect.getfile(send_func)
|
||||||
|
|
||||||
|
# Acquire our path to our module
|
||||||
|
module_name = str(send_func.__module__)
|
||||||
|
|
||||||
|
if module_name not in self._custom_module_map:
|
||||||
|
# Support non-dynamic includes as well...
|
||||||
|
self._custom_module_map[module_name] = {
|
||||||
|
# Name can be useful for indexing back into the
|
||||||
|
# _module_map object; this is the key to do it with:
|
||||||
|
'name': module_name.split('.')[-1],
|
||||||
|
|
||||||
|
# The path to the module loaded
|
||||||
|
'path': path,
|
||||||
|
|
||||||
|
# Initialize our template
|
||||||
|
'notify': {},
|
||||||
|
}
|
||||||
|
|
||||||
|
for schema in schemas:
|
||||||
|
self._custom_module_map[module_name]['notify'][schema] = {
|
||||||
|
# The name of the send function the @notify decorator
|
||||||
|
# wrapped
|
||||||
|
'fn_name': fn_name,
|
||||||
|
# The URL that was provided in the @notify decorator call
|
||||||
|
# associated with the 'on='
|
||||||
|
'url': url,
|
||||||
|
}
|
||||||
|
|
||||||
|
else:
|
||||||
|
module_name = hashlib.sha1(
|
||||||
|
''.join(schemas).encode('utf-8')).hexdigest()
|
||||||
|
module_pyname = "{prefix}.{name}".format(
|
||||||
|
prefix='apprise.adhoc.module', name=module_name)
|
||||||
|
|
||||||
|
# Add our plugin name to our module map
|
||||||
|
self._module_map[module_name] = {
|
||||||
|
'plugin': set([plugin]),
|
||||||
|
'module': None,
|
||||||
|
'path': module_pyname,
|
||||||
|
'native': False,
|
||||||
|
}
|
||||||
|
|
||||||
|
for schema in schemas:
|
||||||
|
# Assign our mapping
|
||||||
|
self._schema_map[schema] = plugin
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def remove(self, *schemas):
|
||||||
|
"""
|
||||||
|
Removes a loaded element (if defined)
|
||||||
|
"""
|
||||||
|
if not self:
|
||||||
|
# Lazy load
|
||||||
|
self.load_modules()
|
||||||
|
|
||||||
|
for schema in schemas:
|
||||||
|
try:
|
||||||
|
del self[schema]
|
||||||
|
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def plugins(self, include_disabled=True):
|
||||||
|
"""
|
||||||
|
Return all of our loaded plugins
|
||||||
|
"""
|
||||||
|
if not self:
|
||||||
|
# Lazy load
|
||||||
|
self.load_modules()
|
||||||
|
|
||||||
|
for module in self._module_map.values():
|
||||||
|
for plugin in module['plugin']:
|
||||||
|
if not include_disabled and not plugin.enabled:
|
||||||
|
continue
|
||||||
|
yield plugin
|
||||||
|
|
||||||
|
def schemas(self, include_disabled=True):
|
||||||
|
"""
|
||||||
|
Return all of our loaded schemas
|
||||||
|
|
||||||
|
if include_disabled == True, then even disabled notifications are
|
||||||
|
returned
|
||||||
|
"""
|
||||||
|
if not self:
|
||||||
|
# Lazy load
|
||||||
|
self.load_modules()
|
||||||
|
|
||||||
|
# Return our list
|
||||||
|
return list(self._schema_map.keys()) if include_disabled else \
|
||||||
|
[s for s in self._schema_map.keys() if self._schema_map[s].enabled]
|
||||||
|
|
||||||
|
def disable(self, *schemas):
|
||||||
|
"""
|
||||||
|
Disables the modules associated with the specified schemas
|
||||||
|
"""
|
||||||
|
if not self:
|
||||||
|
# Lazy load
|
||||||
|
self.load_modules()
|
||||||
|
|
||||||
|
for schema in schemas:
|
||||||
|
if schema not in self._schema_map:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not self._schema_map[schema].enabled:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Disable
|
||||||
|
self._schema_map[schema].enabled = False
|
||||||
|
self._disabled.add(schema)
|
||||||
|
|
||||||
|
def enable_only(self, *schemas):
|
||||||
|
"""
|
||||||
|
Disables the modules associated with the specified schemas
|
||||||
|
"""
|
||||||
|
if not self:
|
||||||
|
# Lazy load
|
||||||
|
self.load_modules()
|
||||||
|
|
||||||
|
# convert to set for faster indexing
|
||||||
|
schemas = set(schemas)
|
||||||
|
|
||||||
|
for plugin in self.plugins():
|
||||||
|
# Get our plugin's schema list
|
||||||
|
p_schemas = set(
|
||||||
|
parse_list(plugin.secure_protocol, plugin.protocol))
|
||||||
|
|
||||||
|
if not schemas & p_schemas:
|
||||||
|
if plugin.enabled:
|
||||||
|
# Disable it (only if previously enabled); this prevents us
|
||||||
|
# from adjusting schemas that were disabled due to missing
|
||||||
|
# libraries or other environment reasons
|
||||||
|
plugin.enabled = False
|
||||||
|
self._disabled |= p_schemas
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If we reach here, our schema was flagged to be enabled
|
||||||
|
if p_schemas & self._disabled:
|
||||||
|
# Previously disabled; no worries, let's clear this up
|
||||||
|
self._disabled -= p_schemas
|
||||||
|
plugin.enabled = True
|
||||||
|
|
||||||
|
def __contains__(self, schema):
|
||||||
|
"""
|
||||||
|
Checks if a schema exists
|
||||||
|
"""
|
||||||
|
if not self:
|
||||||
|
# Lazy load
|
||||||
|
self.load_modules()
|
||||||
|
|
||||||
|
return schema in self._schema_map
|
||||||
|
|
||||||
|
def __delitem__(self, schema):
|
||||||
|
if not self:
|
||||||
|
# Lazy load
|
||||||
|
self.load_modules()
|
||||||
|
|
||||||
|
# Get our plugin (otherwise we throw a KeyError) which is
|
||||||
|
# intended on del action that doesn't align
|
||||||
|
plugin = self._schema_map[schema]
|
||||||
|
|
||||||
|
# Our list of all schema entries
|
||||||
|
p_schemas = set([schema])
|
||||||
|
|
||||||
|
for key in list(self._module_map.keys()):
|
||||||
|
if plugin in self._module_map[key]['plugin']:
|
||||||
|
# Remove our plugin
|
||||||
|
self._module_map[key]['plugin'].remove(plugin)
|
||||||
|
|
||||||
|
# Custom Plugin Entry; Clean up cross reference
|
||||||
|
module_pyname = self._module_map[key]['path']
|
||||||
|
if not self._module_map[key]['native'] and \
|
||||||
|
module_pyname in self._custom_module_map:
|
||||||
|
|
||||||
|
del self.\
|
||||||
|
_custom_module_map[module_pyname]['notify'][schema]
|
||||||
|
|
||||||
|
if not self._custom_module_map[module_pyname]['notify']:
|
||||||
|
#
|
||||||
|
# Last custom loaded element
|
||||||
|
#
|
||||||
|
|
||||||
|
# Free up custom object entry
|
||||||
|
del self._custom_module_map[module_pyname]
|
||||||
|
|
||||||
|
if not self._module_map[key]['plugin']:
|
||||||
|
#
|
||||||
|
# Last element
|
||||||
|
#
|
||||||
|
if self._module_map[key]['native']:
|
||||||
|
# Get our plugin's schema list
|
||||||
|
p_schemas = \
|
||||||
|
set([s for s in parse_list(
|
||||||
|
plugin.secure_protocol, plugin.protocol)
|
||||||
|
if s in self._schema_map])
|
||||||
|
|
||||||
|
# free system memory
|
||||||
|
if self._module_map[key]['module']:
|
||||||
|
del sys.modules[self._module_map[key]['path']]
|
||||||
|
|
||||||
|
# free last remaining pointer in module map
|
||||||
|
del self._module_map[key]
|
||||||
|
|
||||||
|
for schema in p_schemas:
|
||||||
|
# Final Tidy
|
||||||
|
del self._schema_map[schema]
|
||||||
|
|
||||||
|
def __setitem__(self, schema, plugin):
|
||||||
|
"""
|
||||||
|
Support fast assigning of Plugin/Notification Objects
|
||||||
|
"""
|
||||||
|
if not self:
|
||||||
|
# Lazy load
|
||||||
|
self.load_modules()
|
||||||
|
|
||||||
|
# Set default values if not otherwise set
|
||||||
|
if not plugin.service_name:
|
||||||
|
# Assign service name if one doesn't exist
|
||||||
|
plugin.service_name = f'{schema}://'
|
||||||
|
|
||||||
|
p_schemas = set(
|
||||||
|
parse_list(plugin.secure_protocol, plugin.protocol))
|
||||||
|
if not p_schemas:
|
||||||
|
# Assign our protocol
|
||||||
|
plugin.secure_protocol = schema
|
||||||
|
p_schemas.add(schema)
|
||||||
|
|
||||||
|
elif schema not in p_schemas:
|
||||||
|
# Add our others (if defined)
|
||||||
|
plugin.secure_protocol = \
|
||||||
|
set([schema] + parse_list(plugin.secure_protocol))
|
||||||
|
p_schemas.add(schema)
|
||||||
|
|
||||||
|
if not self.add(plugin, schemas=p_schemas):
|
||||||
|
raise KeyError('Conflicting Assignment')
|
||||||
|
|
||||||
|
def __getitem__(self, schema):
|
||||||
|
"""
|
||||||
|
Returns the indexed plugin identified by the schema specified
|
||||||
|
"""
|
||||||
|
if not self:
|
||||||
|
# Lazy load
|
||||||
|
self.load_modules()
|
||||||
|
|
||||||
|
return self._schema_map[schema]
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
"""
|
||||||
|
Returns an iterator so we can iterate over our loaded modules
|
||||||
|
"""
|
||||||
|
if not self:
|
||||||
|
# Lazy load
|
||||||
|
self.load_modules()
|
||||||
|
|
||||||
|
return iter(self._module_map.values())
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
"""
|
||||||
|
Returns the number of modules/plugins loaded
|
||||||
|
"""
|
||||||
|
if not self:
|
||||||
|
# Lazy load
|
||||||
|
self.load_modules()
|
||||||
|
|
||||||
|
return len(self._module_map)
|
||||||
|
|
||||||
|
def __bool__(self):
|
||||||
|
"""
|
||||||
|
Determines if object has loaded or not
|
||||||
|
"""
|
||||||
|
return True if self._loaded and self._module_map is not None else False
|
59
lib/apprise/manager_attachment.py
Normal file
59
lib/apprise/manager_attachment.py
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
# -*- 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 re
|
||||||
|
from os.path import dirname
|
||||||
|
from os.path import abspath
|
||||||
|
from os.path import join
|
||||||
|
from .manager import PluginManager
|
||||||
|
|
||||||
|
|
||||||
|
class AttachmentManager(PluginManager):
|
||||||
|
"""
|
||||||
|
Designed to be a singleton object to maintain all initialized
|
||||||
|
attachment plugins/modules in memory.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Description (used for logging)
|
||||||
|
name = 'Attachment Plugin'
|
||||||
|
|
||||||
|
# Filename Prefix to filter on
|
||||||
|
fname_prefix = 'Attach'
|
||||||
|
|
||||||
|
# Memory Space
|
||||||
|
_id = 'attachment'
|
||||||
|
|
||||||
|
# Our Module Python path name
|
||||||
|
module_name_prefix = f'apprise.{_id}'
|
||||||
|
|
||||||
|
# The module path to scan
|
||||||
|
module_path = join(abspath(dirname(__file__)), _id)
|
||||||
|
|
||||||
|
# For filtering our result set
|
||||||
|
module_filter_re = re.compile(
|
||||||
|
r'^(?P<name>' + fname_prefix + r'(?!Base)[A-Za-z0-9]+)$')
|
59
lib/apprise/manager_config.py
Normal file
59
lib/apprise/manager_config.py
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
# -*- 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 re
|
||||||
|
from os.path import dirname
|
||||||
|
from os.path import abspath
|
||||||
|
from os.path import join
|
||||||
|
from .manager import PluginManager
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigurationManager(PluginManager):
|
||||||
|
"""
|
||||||
|
Designed to be a singleton object to maintain all initialized
|
||||||
|
configuration plugins/modules in memory.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Description (used for logging)
|
||||||
|
name = 'Configuration Plugin'
|
||||||
|
|
||||||
|
# Filename Prefix to filter on
|
||||||
|
fname_prefix = 'Config'
|
||||||
|
|
||||||
|
# Memory Space
|
||||||
|
_id = 'config'
|
||||||
|
|
||||||
|
# Our Module Python path name
|
||||||
|
module_name_prefix = f'apprise.{_id}'
|
||||||
|
|
||||||
|
# The module path to scan
|
||||||
|
module_path = join(abspath(dirname(__file__)), _id)
|
||||||
|
|
||||||
|
# For filtering our result set
|
||||||
|
module_filter_re = re.compile(
|
||||||
|
r'^(?P<name>' + fname_prefix + r'(?!Base)[A-Za-z0-9]+)$')
|
60
lib/apprise/manager_plugins.py
Normal file
60
lib/apprise/manager_plugins.py
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
# -*- 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 re
|
||||||
|
from os.path import dirname
|
||||||
|
from os.path import abspath
|
||||||
|
from os.path import join
|
||||||
|
from .manager import PluginManager
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationManager(PluginManager):
|
||||||
|
"""
|
||||||
|
Designed to be a singleton object to maintain all initialized notifications
|
||||||
|
in memory.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Description (used for logging)
|
||||||
|
name = 'Notification Plugin'
|
||||||
|
|
||||||
|
# Filename Prefix to filter on
|
||||||
|
fname_prefix = 'Notify'
|
||||||
|
|
||||||
|
# Memory Space
|
||||||
|
_id = 'plugins'
|
||||||
|
|
||||||
|
# Our Module Python path name
|
||||||
|
module_name_prefix = f'apprise.{_id}'
|
||||||
|
|
||||||
|
# The module path to scan
|
||||||
|
module_path = join(abspath(dirname(__file__)), _id)
|
||||||
|
|
||||||
|
# For filtering our result set
|
||||||
|
module_filter_re = re.compile(
|
||||||
|
r'^(?P<name>' + fname_prefix +
|
||||||
|
r'(?!Base|ImageSize|Type)[A-Za-z0-9]+)$')
|
|
@ -1,386 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# BSD 2-Clause License
|
|
||||||
#
|
|
||||||
# Apprise - Push Notification Library.
|
|
||||||
# Copyright (c) 2023, 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.
|
|
||||||
|
|
||||||
# To use this service you will need a Spontit account from their website
|
|
||||||
# at https://spontit.com/
|
|
||||||
#
|
|
||||||
# After you have an account created:
|
|
||||||
# - Visit your profile at https://spontit.com/profile and take note of your
|
|
||||||
# {username}. It might look something like: user12345678901
|
|
||||||
# - Next generate an API key at https://spontit.com/secret_keys. This will
|
|
||||||
# generate a very long alpha-numeric string we'll refer to as the
|
|
||||||
# {apikey}
|
|
||||||
|
|
||||||
# The Spontit Syntax is as follows:
|
|
||||||
# spontit://{username}@{apikey}
|
|
||||||
|
|
||||||
import re
|
|
||||||
import requests
|
|
||||||
from json import loads
|
|
||||||
|
|
||||||
from .NotifyBase import NotifyBase
|
|
||||||
from ..common import NotifyType
|
|
||||||
from ..utils import parse_list
|
|
||||||
from ..utils import validate_regex
|
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
|
||||||
|
|
||||||
# Syntax suggests you use a hashtag '#' to help distinguish we're dealing
|
|
||||||
# with a channel.
|
|
||||||
# Secondly we extract the user information only if it's
|
|
||||||
# specified. If not, we use the user of the person sending the notification
|
|
||||||
# Finally the channel identifier is detected
|
|
||||||
CHANNEL_REGEX = re.compile(
|
|
||||||
r'^\s*(\#|\%23)?((\@|\%40)?(?P<user>[a-z0-9_]+)([/\\]|\%2F))?'
|
|
||||||
r'(?P<channel>[a-z0-9_-]+)\s*$', re.I)
|
|
||||||
|
|
||||||
|
|
||||||
class NotifySpontit(NotifyBase):
|
|
||||||
"""
|
|
||||||
A wrapper for Spontit Notifications
|
|
||||||
"""
|
|
||||||
|
|
||||||
# The default descriptive name associated with the Notification
|
|
||||||
service_name = 'Spontit'
|
|
||||||
|
|
||||||
# The services URL
|
|
||||||
service_url = 'https://spontit.com/'
|
|
||||||
|
|
||||||
# All notification requests are secure
|
|
||||||
secure_protocol = 'spontit'
|
|
||||||
|
|
||||||
# Allow 300 requests per minute.
|
|
||||||
# 60/300 = 0.2
|
|
||||||
request_rate_per_sec = 0.20
|
|
||||||
|
|
||||||
# A URL that takes you to the setup/help of the specific protocol
|
|
||||||
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_spontit'
|
|
||||||
|
|
||||||
# Spontit single notification URL
|
|
||||||
notify_url = 'https://api.spontit.com/v3/push'
|
|
||||||
|
|
||||||
# The maximum length of the body
|
|
||||||
body_maxlen = 5000
|
|
||||||
|
|
||||||
# The maximum length of the title
|
|
||||||
title_maxlen = 100
|
|
||||||
|
|
||||||
# If we don't have the specified min length, then we don't bother using
|
|
||||||
# the body directive
|
|
||||||
spontit_body_minlen = 100
|
|
||||||
|
|
||||||
# Subtitle support; this is the maximum allowed characters defined by
|
|
||||||
# the API page
|
|
||||||
spontit_subtitle_maxlen = 20
|
|
||||||
|
|
||||||
# Define object templates
|
|
||||||
templates = (
|
|
||||||
'{schema}://{user}@{apikey}',
|
|
||||||
'{schema}://{user}@{apikey}/{targets}',
|
|
||||||
)
|
|
||||||
|
|
||||||
# Define our template tokens
|
|
||||||
template_tokens = dict(NotifyBase.template_tokens, **{
|
|
||||||
'user': {
|
|
||||||
'name': _('User ID'),
|
|
||||||
'type': 'string',
|
|
||||||
'required': True,
|
|
||||||
'regex': (r'^[a-z0-9_-]+$', 'i'),
|
|
||||||
},
|
|
||||||
'apikey': {
|
|
||||||
'name': _('API Key'),
|
|
||||||
'type': 'string',
|
|
||||||
'required': True,
|
|
||||||
'private': True,
|
|
||||||
'regex': (r'^[a-z0-9]+$', 'i'),
|
|
||||||
},
|
|
||||||
# Target Channel ID's
|
|
||||||
# If a slash is used; you must escape it
|
|
||||||
# If no slash is used; channel is presumed to be your own
|
|
||||||
'target_channel': {
|
|
||||||
'name': _('Target Channel ID'),
|
|
||||||
'type': 'string',
|
|
||||||
'prefix': '#',
|
|
||||||
'regex': (r'^[0-9\s)(+-]+$', 'i'),
|
|
||||||
'map_to': 'targets',
|
|
||||||
},
|
|
||||||
'targets': {
|
|
||||||
'name': _('Targets'),
|
|
||||||
'type': 'list:string',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
# Define our template arguments
|
|
||||||
template_args = dict(NotifyBase.template_args, **{
|
|
||||||
'to': {
|
|
||||||
'alias_of': 'targets',
|
|
||||||
},
|
|
||||||
'subtitle': {
|
|
||||||
# Subtitle is available for MacOS users
|
|
||||||
'name': _('Subtitle'),
|
|
||||||
'type': 'string',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
def __init__(self, apikey, targets=None, subtitle=None, **kwargs):
|
|
||||||
"""
|
|
||||||
Initialize Spontit Object
|
|
||||||
"""
|
|
||||||
super().__init__(**kwargs)
|
|
||||||
|
|
||||||
# User ID (associated with project)
|
|
||||||
user = validate_regex(
|
|
||||||
self.user, *self.template_tokens['user']['regex'])
|
|
||||||
if not user:
|
|
||||||
msg = 'An invalid Spontit User ID ' \
|
|
||||||
'({}) was specified.'.format(self.user)
|
|
||||||
self.logger.warning(msg)
|
|
||||||
raise TypeError(msg)
|
|
||||||
# use cleaned up version
|
|
||||||
self.user = user
|
|
||||||
|
|
||||||
# API Key (associated with project)
|
|
||||||
self.apikey = validate_regex(
|
|
||||||
apikey, *self.template_tokens['apikey']['regex'])
|
|
||||||
if not self.apikey:
|
|
||||||
msg = 'An invalid Spontit API Key ' \
|
|
||||||
'({}) was specified.'.format(apikey)
|
|
||||||
self.logger.warning(msg)
|
|
||||||
raise TypeError(msg)
|
|
||||||
|
|
||||||
# Save our subtitle information
|
|
||||||
self.subtitle = subtitle
|
|
||||||
|
|
||||||
# Parse our targets
|
|
||||||
self.targets = list()
|
|
||||||
|
|
||||||
for target in parse_list(targets):
|
|
||||||
# Validate targets and drop bad ones:
|
|
||||||
result = CHANNEL_REGEX.match(target)
|
|
||||||
if result:
|
|
||||||
# Just extract the channel
|
|
||||||
self.targets.append(
|
|
||||||
'{}'.format(result.group('channel')))
|
|
||||||
continue
|
|
||||||
|
|
||||||
self.logger.warning(
|
|
||||||
'Dropped invalid channel/user ({}) specified.'.format(target))
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
|
||||||
"""
|
|
||||||
Sends Message
|
|
||||||
"""
|
|
||||||
|
|
||||||
# error tracking (used for function return)
|
|
||||||
has_error = False
|
|
||||||
|
|
||||||
# Prepare our headers
|
|
||||||
headers = {
|
|
||||||
'User-Agent': self.app_id,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-Authorization': self.apikey,
|
|
||||||
'X-UserId': self.user,
|
|
||||||
}
|
|
||||||
|
|
||||||
# use the list directly
|
|
||||||
targets = list(self.targets)
|
|
||||||
|
|
||||||
if not len(targets):
|
|
||||||
# The user did not specify a channel and therefore wants to notify
|
|
||||||
# the main account only. We just set a substitute marker of
|
|
||||||
# None so that our while loop below can still process one iteration
|
|
||||||
targets = [None, ]
|
|
||||||
|
|
||||||
while len(targets):
|
|
||||||
# Get our target(s) to notify
|
|
||||||
target = targets.pop(0)
|
|
||||||
|
|
||||||
# Prepare our payload
|
|
||||||
payload = {
|
|
||||||
'message': body,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Use our body directive if we exceed the minimum message
|
|
||||||
# limitation
|
|
||||||
if len(body) > self.spontit_body_minlen:
|
|
||||||
payload['message'] = '{}...'.format(
|
|
||||||
body[:self.spontit_body_minlen - 3])
|
|
||||||
payload['body'] = body
|
|
||||||
|
|
||||||
if self.subtitle:
|
|
||||||
# Set title if specified
|
|
||||||
payload['subtitle'] = \
|
|
||||||
self.subtitle[:self.spontit_subtitle_maxlen]
|
|
||||||
|
|
||||||
elif self.app_desc:
|
|
||||||
# fall back to app description
|
|
||||||
payload['subtitle'] = \
|
|
||||||
self.app_desc[:self.spontit_subtitle_maxlen]
|
|
||||||
|
|
||||||
elif self.app_id:
|
|
||||||
# fall back to app id
|
|
||||||
payload['subtitle'] = \
|
|
||||||
self.app_id[:self.spontit_subtitle_maxlen]
|
|
||||||
|
|
||||||
if title:
|
|
||||||
# Set title if specified
|
|
||||||
payload['pushTitle'] = title
|
|
||||||
|
|
||||||
if target is not None:
|
|
||||||
payload['channelName'] = target
|
|
||||||
|
|
||||||
# Some Debug Logging
|
|
||||||
self.logger.debug(
|
|
||||||
'Spontit POST URL: {} (cert_verify={})'.format(
|
|
||||||
self.notify_url, self.verify_certificate))
|
|
||||||
self.logger.debug('Spontit Payload: {}' .format(payload))
|
|
||||||
|
|
||||||
# Always call throttle before any remote server i/o is made
|
|
||||||
self.throttle()
|
|
||||||
try:
|
|
||||||
r = requests.post(
|
|
||||||
self.notify_url,
|
|
||||||
params=payload,
|
|
||||||
headers=headers,
|
|
||||||
verify=self.verify_certificate,
|
|
||||||
timeout=self.request_timeout,
|
|
||||||
)
|
|
||||||
|
|
||||||
if r.status_code not in (
|
|
||||||
requests.codes.created, requests.codes.ok):
|
|
||||||
status_str = \
|
|
||||||
NotifyBase.http_response_code_lookup(
|
|
||||||
r.status_code)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Update our status response if we can
|
|
||||||
json_response = loads(r.content)
|
|
||||||
status_str = json_response.get('message', status_str)
|
|
||||||
|
|
||||||
except (AttributeError, TypeError, ValueError):
|
|
||||||
# ValueError = r.content is Unparsable
|
|
||||||
# TypeError = r.content is None
|
|
||||||
# AttributeError = r is None
|
|
||||||
|
|
||||||
# We could not parse JSON response.
|
|
||||||
# We will just use the status we already have.
|
|
||||||
pass
|
|
||||||
|
|
||||||
self.logger.warning(
|
|
||||||
'Failed to send Spontit notification to {}: '
|
|
||||||
'{}{}error={}.'.format(
|
|
||||||
target,
|
|
||||||
status_str,
|
|
||||||
', ' if status_str else '',
|
|
||||||
r.status_code))
|
|
||||||
|
|
||||||
self.logger.debug(
|
|
||||||
'Response Details:\r\n{}'.format(r.content))
|
|
||||||
|
|
||||||
# Mark our failure
|
|
||||||
has_error = True
|
|
||||||
continue
|
|
||||||
|
|
||||||
# If we reach here; the message was sent
|
|
||||||
self.logger.info(
|
|
||||||
'Sent Spontit notification to {}.'.format(target))
|
|
||||||
|
|
||||||
self.logger.debug(
|
|
||||||
'Response Details:\r\n{}'.format(r.content))
|
|
||||||
|
|
||||||
except requests.RequestException as e:
|
|
||||||
self.logger.warning(
|
|
||||||
'A Connection error occurred sending Spontit:%s ' % (
|
|
||||||
', '.join(self.targets)) + 'notification.'
|
|
||||||
)
|
|
||||||
self.logger.debug('Socket Exception: %s' % str(e))
|
|
||||||
# Mark our failure
|
|
||||||
has_error = True
|
|
||||||
continue
|
|
||||||
|
|
||||||
return not has_error
|
|
||||||
|
|
||||||
def url(self, privacy=False, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Returns the URL built dynamically based on specified arguments.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Our URL parameters
|
|
||||||
params = self.url_parameters(privacy=privacy, *args, **kwargs)
|
|
||||||
|
|
||||||
if self.subtitle:
|
|
||||||
params['subtitle'] = self.subtitle
|
|
||||||
|
|
||||||
return '{schema}://{userid}@{apikey}/{targets}?{params}'.format(
|
|
||||||
schema=self.secure_protocol,
|
|
||||||
userid=self.user,
|
|
||||||
apikey=self.pprint(self.apikey, privacy, safe=''),
|
|
||||||
targets='/'.join(
|
|
||||||
[NotifySpontit.quote(x, safe='') for x in self.targets]),
|
|
||||||
params=NotifySpontit.urlencode(params))
|
|
||||||
|
|
||||||
def __len__(self):
|
|
||||||
"""
|
|
||||||
Returns the number of targets associated with this notification
|
|
||||||
"""
|
|
||||||
targets = len(self.targets)
|
|
||||||
return targets if targets > 0 else 1
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def parse_url(url):
|
|
||||||
"""
|
|
||||||
Parses the URL and returns enough arguments that can allow
|
|
||||||
us to re-instantiate this object.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
results = NotifyBase.parse_url(url, verify_host=False)
|
|
||||||
if not results:
|
|
||||||
# We're done early as we couldn't load the results
|
|
||||||
return results
|
|
||||||
|
|
||||||
# Get our entries; split_path() looks after unquoting content for us
|
|
||||||
# by default
|
|
||||||
results['targets'] = NotifySpontit.split_path(results['fullpath'])
|
|
||||||
|
|
||||||
# The hostname is our authentication key
|
|
||||||
results['apikey'] = NotifySpontit.unquote(results['host'])
|
|
||||||
|
|
||||||
# Support MacOS subtitle option
|
|
||||||
if 'subtitle' in results['qsd'] and len(results['qsd']['subtitle']):
|
|
||||||
results['subtitle'] = \
|
|
||||||
NotifySpontit.unquote(results['qsd']['subtitle'])
|
|
||||||
|
|
||||||
# Support the 'to' variable so that we can support targets this way too
|
|
||||||
# The 'to' makes it easier to use yaml configuration
|
|
||||||
if 'to' in results['qsd'] and len(results['qsd']['to']):
|
|
||||||
results['targets'] += \
|
|
||||||
NotifySpontit.parse_list(results['qsd']['to'])
|
|
||||||
|
|
||||||
return results
|
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -27,26 +27,26 @@
|
||||||
# POSSIBILITY OF SUCH DAMAGE.
|
# POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
import copy
|
import copy
|
||||||
|
|
||||||
from os.path import dirname
|
|
||||||
from os.path import abspath
|
|
||||||
|
|
||||||
# Used for testing
|
# Used for testing
|
||||||
from .NotifyBase import NotifyBase
|
from .base import NotifyBase
|
||||||
|
|
||||||
from ..common import NotifyImageSize
|
from ..common import NotifyImageSize
|
||||||
from ..common import NOTIFY_IMAGE_SIZES
|
from ..common import NOTIFY_IMAGE_SIZES
|
||||||
from ..common import NotifyType
|
from ..common import NotifyType
|
||||||
from ..common import NOTIFY_TYPES
|
from ..common import NOTIFY_TYPES
|
||||||
from .. import common
|
|
||||||
from ..utils import parse_list
|
from ..utils import parse_list
|
||||||
from ..utils import cwe312_url
|
from ..utils import cwe312_url
|
||||||
from ..utils import GET_SCHEMA_RE
|
from ..utils import GET_SCHEMA_RE
|
||||||
from ..logger import logger
|
from ..logger import logger
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..locale import gettext_lazy as _
|
||||||
from ..AppriseLocale import LazyTranslation
|
from ..locale import LazyTranslation
|
||||||
|
from ..manager_plugins import NotificationManager
|
||||||
|
|
||||||
|
|
||||||
|
# Grant access to our Notification Manager Singleton
|
||||||
|
N_MGR = NotificationManager()
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
# Reference
|
# Reference
|
||||||
|
@ -58,101 +58,6 @@ __all__ = [
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
# Load our Lookup Matrix
|
|
||||||
def __load_matrix(path=abspath(dirname(__file__)), name='apprise.plugins'):
|
|
||||||
"""
|
|
||||||
Dynamically load our schema map; this allows us to gracefully
|
|
||||||
skip over modules we simply don't have the dependencies for.
|
|
||||||
|
|
||||||
"""
|
|
||||||
# Used for the detection of additional Notify Services objects
|
|
||||||
# The .py extension is optional as we support loading directories too
|
|
||||||
module_re = re.compile(r'^(?P<name>Notify[a-z0-9]+)(\.py)?$', re.I)
|
|
||||||
|
|
||||||
for f in os.listdir(path):
|
|
||||||
match = module_re.match(f)
|
|
||||||
if not match:
|
|
||||||
# keep going
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Store our notification/plugin name:
|
|
||||||
plugin_name = match.group('name')
|
|
||||||
try:
|
|
||||||
module = __import__(
|
|
||||||
'{}.{}'.format(name, plugin_name),
|
|
||||||
globals(), locals(),
|
|
||||||
fromlist=[plugin_name])
|
|
||||||
|
|
||||||
except ImportError:
|
|
||||||
# No problem, we can't use this object
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not hasattr(module, plugin_name):
|
|
||||||
# Not a library we can load as it doesn't follow the simple rule
|
|
||||||
# that the class must bear the same name as the notification
|
|
||||||
# file itself.
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Get our plugin
|
|
||||||
plugin = getattr(module, plugin_name)
|
|
||||||
if not hasattr(plugin, 'app_id'):
|
|
||||||
# Filter out non-notification modules
|
|
||||||
continue
|
|
||||||
|
|
||||||
elif plugin_name in common.NOTIFY_MODULE_MAP:
|
|
||||||
# we're already handling this object
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Add our plugin name to our module map
|
|
||||||
common.NOTIFY_MODULE_MAP[plugin_name] = {
|
|
||||||
'plugin': plugin,
|
|
||||||
'module': module,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Add our module name to our __all__
|
|
||||||
__all__.append(plugin_name)
|
|
||||||
|
|
||||||
fn = getattr(plugin, 'schemas', None)
|
|
||||||
schemas = set([]) if not callable(fn) else fn(plugin)
|
|
||||||
|
|
||||||
# map our schema to our plugin
|
|
||||||
for schema in schemas:
|
|
||||||
if schema in common.NOTIFY_SCHEMA_MAP:
|
|
||||||
logger.error(
|
|
||||||
"Notification schema ({}) mismatch detected - {} to {}"
|
|
||||||
.format(schema, common.NOTIFY_SCHEMA_MAP[schema], plugin))
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Assign plugin
|
|
||||||
common.NOTIFY_SCHEMA_MAP[schema] = plugin
|
|
||||||
|
|
||||||
return common.NOTIFY_SCHEMA_MAP
|
|
||||||
|
|
||||||
|
|
||||||
# Reset our Lookup Matrix
|
|
||||||
def __reset_matrix():
|
|
||||||
"""
|
|
||||||
Restores the Lookup matrix to it's base setting. This is only used through
|
|
||||||
testing and should not be directly called.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Reset our schema map
|
|
||||||
common.NOTIFY_SCHEMA_MAP.clear()
|
|
||||||
|
|
||||||
# Iterate over our module map so we can clear out our __all__ and globals
|
|
||||||
for plugin_name in common.NOTIFY_MODULE_MAP.keys():
|
|
||||||
|
|
||||||
# Remove element from plugins
|
|
||||||
__all__.remove(plugin_name)
|
|
||||||
|
|
||||||
# Clear out our module map
|
|
||||||
common.NOTIFY_MODULE_MAP.clear()
|
|
||||||
|
|
||||||
|
|
||||||
# Dynamically build our schema base
|
|
||||||
__load_matrix()
|
|
||||||
|
|
||||||
|
|
||||||
def _sanitize_token(tokens, default_delimiter):
|
def _sanitize_token(tokens, default_delimiter):
|
||||||
"""
|
"""
|
||||||
This is called by the details() function and santizes the output by
|
This is called by the details() function and santizes the output by
|
||||||
|
@ -176,6 +81,10 @@ def _sanitize_token(tokens, default_delimiter):
|
||||||
# Do not touch this field
|
# Do not touch this field
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
elif 'name' not in tokens[key]:
|
||||||
|
# Default to key
|
||||||
|
tokens[key]['name'] = key
|
||||||
|
|
||||||
if 'map_to' not in tokens[key]:
|
if 'map_to' not in tokens[key]:
|
||||||
# Default type to key
|
# Default type to key
|
||||||
tokens[key]['map_to'] = key
|
tokens[key]['map_to'] = key
|
||||||
|
@ -538,16 +447,16 @@ def url_to_dict(url, secure_logging=True):
|
||||||
|
|
||||||
# Ensure our schema is always in lower case
|
# Ensure our schema is always in lower case
|
||||||
schema = schema.group('schema').lower()
|
schema = schema.group('schema').lower()
|
||||||
if schema not in common.NOTIFY_SCHEMA_MAP:
|
if schema not in N_MGR:
|
||||||
# Give the user the benefit of the doubt that the user may be using
|
# Give the user the benefit of the doubt that the user may be using
|
||||||
# one of the URLs provided to them by their notification service.
|
# one of the URLs provided to them by their notification service.
|
||||||
# Before we fail for good, just scan all the plugins that support the
|
# Before we fail for good, just scan all the plugins that support the
|
||||||
# native_url() parse function
|
# native_url() parse function
|
||||||
results = \
|
results = None
|
||||||
next((r['plugin'].parse_native_url(_url)
|
for plugin in N_MGR.plugins():
|
||||||
for r in common.NOTIFY_MODULE_MAP.values()
|
results = plugin.parse_native_url(_url)
|
||||||
if r['plugin'].parse_native_url(_url) is not None),
|
if results:
|
||||||
None)
|
break
|
||||||
|
|
||||||
if not results:
|
if not results:
|
||||||
logger.error('Unparseable URL {}'.format(loggable_url))
|
logger.error('Unparseable URL {}'.format(loggable_url))
|
||||||
|
@ -560,14 +469,14 @@ def url_to_dict(url, secure_logging=True):
|
||||||
else:
|
else:
|
||||||
# Parse our url details of the server object as dictionary
|
# Parse our url details of the server object as dictionary
|
||||||
# containing all of the information parsed from our URL
|
# containing all of the information parsed from our URL
|
||||||
results = common.NOTIFY_SCHEMA_MAP[schema].parse_url(_url)
|
results = N_MGR[schema].parse_url(_url)
|
||||||
if not results:
|
if not results:
|
||||||
logger.error('Unparseable {} URL {}'.format(
|
logger.error('Unparseable {} URL {}'.format(
|
||||||
common.NOTIFY_SCHEMA_MAP[schema].service_name, loggable_url))
|
N_MGR[schema].service_name, loggable_url))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
logger.trace('{} URL {} unpacked as:{}{}'.format(
|
logger.trace('{} URL {} unpacked as:{}{}'.format(
|
||||||
common.NOTIFY_SCHEMA_MAP[schema].service_name, url,
|
N_MGR[schema].service_name, url,
|
||||||
os.linesep, os.linesep.join(
|
os.linesep, os.linesep.join(
|
||||||
['{}="{}"'.format(k, v) for k, v in results.items()])))
|
['{}="{}"'.format(k, v) for k, v in results.items()])))
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -31,12 +31,12 @@ import requests
|
||||||
from json import dumps
|
from json import dumps
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
from .NotifyBase import NotifyBase
|
from .base import NotifyBase
|
||||||
from ..URLBase import PrivacyMode
|
from ..url import PrivacyMode
|
||||||
from ..common import NotifyType
|
from ..common import NotifyType
|
||||||
from ..utils import parse_list
|
from ..utils import parse_list
|
||||||
from ..utils import validate_regex
|
from ..utils import validate_regex
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..locale import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class AppriseAPIMethod:
|
class AppriseAPIMethod:
|
||||||
|
@ -123,7 +123,7 @@ class NotifyAppriseAPI(NotifyBase):
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
'required': True,
|
'required': True,
|
||||||
'private': True,
|
'private': True,
|
||||||
'regex': (r'^[A-Z0-9_-]{1,32}$', 'i'),
|
'regex': (r'^[A-Z0-9_-]{1,128}$', 'i'),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
778
lib/apprise/plugins/aprs.py
Normal file
778
lib/apprise/plugins/aprs.py
Normal file
|
@ -0,0 +1,778 @@
|
||||||
|
# -*- 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.
|
||||||
|
#
|
||||||
|
# To use this plugin, you need to be a licensed ham radio operator
|
||||||
|
#
|
||||||
|
# Plugin constraints:
|
||||||
|
#
|
||||||
|
# - message length = 67 chars max.
|
||||||
|
# - message content = ASCII 7 bit
|
||||||
|
# - APRS messages will be sent without msg ID, meaning that
|
||||||
|
# ham radio operators cannot acknowledge them
|
||||||
|
# - Bring your own APRS-IS passcode. If you don't know what
|
||||||
|
# this is or how to get it, then this plugin is not for you
|
||||||
|
# - Do NOT change the Device/ToCall ID setting UNLESS this
|
||||||
|
# module is used outside of Apprise. This identifier helps
|
||||||
|
# the ham radio community with determining the software behind
|
||||||
|
# a given APRS message.
|
||||||
|
# - With great (ham radio) power comes great responsibility; do
|
||||||
|
# not use this plugin for spamming other ham radio operators
|
||||||
|
|
||||||
|
#
|
||||||
|
# In order to digest text input which is not in plain English,
|
||||||
|
# users can install the optional 'unidecode' package as part
|
||||||
|
# of their venv environment. Details: see plugin description
|
||||||
|
#
|
||||||
|
|
||||||
|
#
|
||||||
|
# You're done at this point, you only need to know your user/pass that
|
||||||
|
# you signed up with.
|
||||||
|
|
||||||
|
# The following URLs would be accepted by Apprise:
|
||||||
|
# - aprs://{user}:{password}@{callsign}
|
||||||
|
# - aprs://{user}:{password}@{callsign1}/{callsign2}
|
||||||
|
|
||||||
|
# Optional parameters:
|
||||||
|
# - locale --> APRS-IS target server to connect with
|
||||||
|
# Default: EURO --> 'euro.aprs2.net'
|
||||||
|
# Details: https://www.aprs2.net/
|
||||||
|
|
||||||
|
#
|
||||||
|
# APRS message format specification:
|
||||||
|
# http://www.aprs.org/doc/APRS101.PDF
|
||||||
|
#
|
||||||
|
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
from itertools import chain
|
||||||
|
from .base import NotifyBase
|
||||||
|
from ..locale import gettext_lazy as _
|
||||||
|
from ..url import PrivacyMode
|
||||||
|
from ..common import NotifyType
|
||||||
|
from ..utils import is_call_sign
|
||||||
|
from ..utils import parse_call_sign
|
||||||
|
from .. import __version__
|
||||||
|
import re
|
||||||
|
|
||||||
|
# Fixed APRS-IS server locales
|
||||||
|
# Default is 'EURO'
|
||||||
|
# See https://www.aprs2.net/ for details
|
||||||
|
# Select the rotating server in case you
|
||||||
|
# don"t care about a specific locale
|
||||||
|
APRS_LOCALES = {
|
||||||
|
"NOAM": "noam.aprs2.net",
|
||||||
|
"SOAM": "soam.aprs2.net",
|
||||||
|
"EURO": "euro.aprs2.net",
|
||||||
|
"ASIA": "asia.aprs2.net",
|
||||||
|
"AUNZ": "aunz.aprs2.net",
|
||||||
|
"ROTA": "rotate.aprs2.net",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Identify all unsupported characters
|
||||||
|
APRS_BAD_CHARMAP = {
|
||||||
|
r"Ä": "Ae",
|
||||||
|
r"Ö": "Oe",
|
||||||
|
r"Ü": "Ue",
|
||||||
|
r"ä": "ae",
|
||||||
|
r"ö": "oe",
|
||||||
|
r"ü": "ue",
|
||||||
|
r"ß": "ss",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Our compiled mapping of bad characters
|
||||||
|
APRS_COMPILED_MAP = re.compile(
|
||||||
|
r'(' + '|'.join(APRS_BAD_CHARMAP.keys()) + r')')
|
||||||
|
|
||||||
|
|
||||||
|
class NotifyAprs(NotifyBase):
|
||||||
|
"""
|
||||||
|
A wrapper for APRS Notifications via APRS-IS
|
||||||
|
"""
|
||||||
|
|
||||||
|
# The default descriptive name associated with the Notification
|
||||||
|
service_name = "Aprs"
|
||||||
|
|
||||||
|
# The services URL
|
||||||
|
service_url = "https://www.aprs2.net/"
|
||||||
|
|
||||||
|
# The default secure protocol
|
||||||
|
secure_protocol = "aprs"
|
||||||
|
|
||||||
|
# A URL that takes you to the setup/help of the specific protocol
|
||||||
|
setup_url = "https://github.com/caronc/apprise/wiki/Notify_aprs"
|
||||||
|
|
||||||
|
# APRS default port, supported by all core servers
|
||||||
|
# Details: https://www.aprs-is.net/Connecting.aspx
|
||||||
|
notify_port = 10152
|
||||||
|
|
||||||
|
# The maximum length of the APRS message body
|
||||||
|
body_maxlen = 67
|
||||||
|
|
||||||
|
# Apprise APRS Device ID / TOCALL ID
|
||||||
|
# This is a FIXED value which is associated with this plugin.
|
||||||
|
# Its value MUST NOT be changed. If you use this APRS plugin
|
||||||
|
# code OUTSIDE of Apprise, please request your own TOCALL ID.
|
||||||
|
# Details: see https://github.com/aprsorg/aprs-deviceid
|
||||||
|
#
|
||||||
|
# Do NOT use the generic "APRS" TOCALL ID !!!!!
|
||||||
|
#
|
||||||
|
device_id = "APPRIS"
|
||||||
|
|
||||||
|
# A title can not be used for APRS Messages. Setting this to zero will
|
||||||
|
# cause any title (if defined) to get placed into the message body.
|
||||||
|
title_maxlen = 0
|
||||||
|
|
||||||
|
# Helps to reduce the number of login-related errors where the
|
||||||
|
# APRS-IS server "isn't ready yet". If we try to receive the rx buffer
|
||||||
|
# without this grace perid in place, we may receive "incomplete" responses
|
||||||
|
# where the login response lacks information. In case you receive too many
|
||||||
|
# "Rx: APRS-IS msg is too short - needs to have at least two lines" error
|
||||||
|
# messages, you might want to increase this value to a larger time span
|
||||||
|
# Per previous experience, do not use values lower than 0.5 (seconds)
|
||||||
|
request_rate_per_sec = 0.8
|
||||||
|
|
||||||
|
# Encoding of retrieved content
|
||||||
|
aprs_encoding = 'latin-1'
|
||||||
|
|
||||||
|
# Define object templates
|
||||||
|
templates = ("{schema}://{user}:{password}@{targets}",)
|
||||||
|
|
||||||
|
# Define our template tokens
|
||||||
|
template_tokens = dict(
|
||||||
|
NotifyBase.template_tokens,
|
||||||
|
**{
|
||||||
|
"user": {
|
||||||
|
"name": _("User Name"),
|
||||||
|
"type": "string",
|
||||||
|
"required": True,
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"name": _("Password"),
|
||||||
|
"type": "string",
|
||||||
|
"private": True,
|
||||||
|
"required": True,
|
||||||
|
},
|
||||||
|
"target_callsign": {
|
||||||
|
"name": _("Target Callsign"),
|
||||||
|
"type": "string",
|
||||||
|
"regex": (
|
||||||
|
r"^[a-z0-9]{2,5}(-[a-z0-9]{1,2})?$",
|
||||||
|
"i",
|
||||||
|
),
|
||||||
|
"map_to": "targets",
|
||||||
|
},
|
||||||
|
"targets": {
|
||||||
|
"name": _("Targets"),
|
||||||
|
"type": "list:string",
|
||||||
|
"required": True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Define our template arguments
|
||||||
|
template_args = dict(
|
||||||
|
NotifyBase.template_args,
|
||||||
|
**{
|
||||||
|
"to": {
|
||||||
|
"name": _("Target Callsign"),
|
||||||
|
"type": "string",
|
||||||
|
"map_to": "targets",
|
||||||
|
},
|
||||||
|
"delay": {
|
||||||
|
"name": _("Resend Delay"),
|
||||||
|
"type": "float",
|
||||||
|
"min": 0.0,
|
||||||
|
"max": 5.0,
|
||||||
|
"default": 0.0,
|
||||||
|
},
|
||||||
|
"locale": {
|
||||||
|
"name": _("Locale"),
|
||||||
|
"type": "choice:string",
|
||||||
|
"values": APRS_LOCALES,
|
||||||
|
"default": "EURO",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, targets=None, locale=None, delay=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Initialize APRS Object
|
||||||
|
"""
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
# Our (future) socket sobject
|
||||||
|
self.sock = None
|
||||||
|
|
||||||
|
# Parse our targets
|
||||||
|
self.targets = list()
|
||||||
|
|
||||||
|
"""
|
||||||
|
Check if the user has provided credentials
|
||||||
|
"""
|
||||||
|
if not (self.user and self.password):
|
||||||
|
msg = "An APRS user/pass was not provided."
|
||||||
|
self.logger.warning(msg)
|
||||||
|
raise TypeError(msg)
|
||||||
|
|
||||||
|
"""
|
||||||
|
Check if the user tries to use a read-only access
|
||||||
|
to APRS-IS. We need to send content, meaning that
|
||||||
|
read-only access will not work
|
||||||
|
"""
|
||||||
|
if self.password == "-1":
|
||||||
|
msg = "APRS read-only passwords are not supported."
|
||||||
|
self.logger.warning(msg)
|
||||||
|
raise TypeError(msg)
|
||||||
|
|
||||||
|
"""
|
||||||
|
Check if the password is numeric
|
||||||
|
"""
|
||||||
|
if not self.password.isnumeric():
|
||||||
|
msg = "Invalid APRS-IS password"
|
||||||
|
self.logger.warning(msg)
|
||||||
|
raise TypeError(msg)
|
||||||
|
|
||||||
|
"""
|
||||||
|
Convert given user name (FROM callsign) and
|
||||||
|
device ID to to uppercase
|
||||||
|
"""
|
||||||
|
self.user = self.user.upper()
|
||||||
|
self.device_id = self.device_id.upper()
|
||||||
|
|
||||||
|
"""
|
||||||
|
Check if the user has provided a locale for the
|
||||||
|
APRS-IS-server and validate it, if necessary
|
||||||
|
"""
|
||||||
|
if locale:
|
||||||
|
if locale.upper() not in APRS_LOCALES:
|
||||||
|
msg = (
|
||||||
|
"Unsupported APRS-IS server locale. "
|
||||||
|
"Received: {}. Valid: {}".format(
|
||||||
|
locale, ", ".join(str(x) for x in APRS_LOCALES.keys())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.logger.warning(msg)
|
||||||
|
raise TypeError(msg)
|
||||||
|
|
||||||
|
# Update our delay
|
||||||
|
if delay is None:
|
||||||
|
self.delay = NotifyAprs.template_args["delay"]["default"]
|
||||||
|
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
self.delay = float(delay)
|
||||||
|
if self.delay < NotifyAprs.template_args["delay"]["min"]:
|
||||||
|
raise ValueError()
|
||||||
|
|
||||||
|
elif self.delay >= NotifyAprs.template_args["delay"]["max"]:
|
||||||
|
raise ValueError()
|
||||||
|
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
msg = "Unsupported APRS-IS delay ({}) specified. ".format(
|
||||||
|
delay)
|
||||||
|
self.logger.warning(msg)
|
||||||
|
raise TypeError(msg)
|
||||||
|
|
||||||
|
# Bump up our request_rate
|
||||||
|
self.request_rate_per_sec += self.delay
|
||||||
|
|
||||||
|
# Set the transmitter group
|
||||||
|
self.locale = \
|
||||||
|
NotifyAprs.template_args["locale"]["default"] \
|
||||||
|
if not locale else locale.upper()
|
||||||
|
|
||||||
|
# Used for URL generation afterwards only
|
||||||
|
self.invalid_targets = list()
|
||||||
|
|
||||||
|
for target in parse_call_sign(targets):
|
||||||
|
# Validate targets and drop bad ones
|
||||||
|
# We just need to know if the call sign (including SSID, if
|
||||||
|
# provided) is valid and can then process the input as is
|
||||||
|
result = is_call_sign(target)
|
||||||
|
if not result:
|
||||||
|
self.logger.warning(
|
||||||
|
"Dropping invalid Amateur radio call sign ({}).".format(
|
||||||
|
target
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.invalid_targets.append(target.upper())
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Store entry
|
||||||
|
self.targets.append(target.upper())
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
def socket_close(self):
|
||||||
|
"""
|
||||||
|
Closes the socket connection whereas present
|
||||||
|
"""
|
||||||
|
if self.sock:
|
||||||
|
try:
|
||||||
|
self.sock.close()
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
# No worries if socket exception thrown on close()
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.sock = None
|
||||||
|
|
||||||
|
def socket_open(self):
|
||||||
|
"""
|
||||||
|
Establishes the connection to the APRS-IS
|
||||||
|
socket server
|
||||||
|
"""
|
||||||
|
self.logger.debug(
|
||||||
|
"Creating socket connection with APRS-IS {}:{}".format(
|
||||||
|
APRS_LOCALES[self.locale], self.notify_port
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.sock = socket.create_connection(
|
||||||
|
(APRS_LOCALES[self.locale], self.notify_port),
|
||||||
|
self.socket_connect_timeout,
|
||||||
|
)
|
||||||
|
|
||||||
|
except ConnectionError as e:
|
||||||
|
self.logger.debug("Socket Exception socket_open: %s", str(e))
|
||||||
|
self.sock = None
|
||||||
|
return False
|
||||||
|
|
||||||
|
except socket.gaierror as e:
|
||||||
|
self.logger.debug("Socket Exception socket_open: %s", str(e))
|
||||||
|
self.sock = None
|
||||||
|
return False
|
||||||
|
|
||||||
|
except socket.timeout as e:
|
||||||
|
self.logger.debug(
|
||||||
|
"Socket Timeout Exception socket_open: %s", str(e))
|
||||||
|
self.sock = None
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.debug("General Exception socket_open: %s", str(e))
|
||||||
|
self.sock = None
|
||||||
|
return False
|
||||||
|
|
||||||
|
# We are connected.
|
||||||
|
# getpeername() is not supported by every OS. Therefore,
|
||||||
|
# we MAY receive an exception even though we are
|
||||||
|
# connected successfully.
|
||||||
|
try:
|
||||||
|
# Get the physical host/port of the server
|
||||||
|
host, port = self.sock.getpeername()
|
||||||
|
# and create debug info
|
||||||
|
self.logger.debug("Connected to {}:{}".format(host, port))
|
||||||
|
|
||||||
|
except ValueError:
|
||||||
|
# Seens as if we are running on an operating
|
||||||
|
# system that does not support getpeername()
|
||||||
|
# Create a minimal log file entry
|
||||||
|
self.logger.debug("Connected to APRS-IS")
|
||||||
|
|
||||||
|
# Return success
|
||||||
|
return True
|
||||||
|
|
||||||
|
def aprsis_login(self):
|
||||||
|
"""
|
||||||
|
Generate the APRS-IS login string, send it to the server
|
||||||
|
and parse the response
|
||||||
|
|
||||||
|
Returns True/False wrt whether the login was successful
|
||||||
|
"""
|
||||||
|
self.logger.debug("socket_login: init")
|
||||||
|
|
||||||
|
# Check if we are connected
|
||||||
|
if not self.sock:
|
||||||
|
self.logger.warning("socket_login: Not connected to APRS-IS")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# APRS-IS login string, see https://www.aprs-is.net/Connecting.aspx
|
||||||
|
login_str = "user {0} pass {1} vers apprise {2}\r\n".format(
|
||||||
|
self.user, self.password, __version__
|
||||||
|
)
|
||||||
|
|
||||||
|
# Send the data & abort in case of error
|
||||||
|
if not self.socket_send(login_str):
|
||||||
|
self.logger.warning(
|
||||||
|
"socket_login: Login to APRS-IS unsuccessful,"
|
||||||
|
" exception occurred"
|
||||||
|
)
|
||||||
|
self.socket_close()
|
||||||
|
return False
|
||||||
|
|
||||||
|
rx_buf = self.socket_receive(len(login_str) + 100)
|
||||||
|
# Abort the remaining process in case an error has occurred
|
||||||
|
if not rx_buf:
|
||||||
|
self.logger.warning(
|
||||||
|
"socket_login: Login to APRS-IS "
|
||||||
|
"unsuccessful, exception occurred"
|
||||||
|
)
|
||||||
|
self.socket_close()
|
||||||
|
return False
|
||||||
|
|
||||||
|
# APRS-IS sends at least two lines of data
|
||||||
|
# The data that we need is in line #2 so
|
||||||
|
# let's split the content and see what we have
|
||||||
|
rx_lines = rx_buf.splitlines()
|
||||||
|
if len(rx_lines) < 2:
|
||||||
|
self.logger.warning(
|
||||||
|
"socket_login: APRS-IS msg is too short"
|
||||||
|
" - needs to have at least two lines"
|
||||||
|
)
|
||||||
|
self.socket_close()
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Now split the 2nd line's content and extract
|
||||||
|
# both call sign and login status
|
||||||
|
try:
|
||||||
|
_, _, callsign, status, _ = rx_lines[1].split(" ", 4)
|
||||||
|
|
||||||
|
except ValueError:
|
||||||
|
# ValueError is returned if there were not enough elements to
|
||||||
|
# populate the response
|
||||||
|
self.logger.warning(
|
||||||
|
"socket_login: " "received invalid response from APRS-IS"
|
||||||
|
)
|
||||||
|
self.socket_close()
|
||||||
|
return False
|
||||||
|
|
||||||
|
if callsign != self.user:
|
||||||
|
self.logger.warning(
|
||||||
|
"socket_login: " "call signs differ: %s" % callsign
|
||||||
|
)
|
||||||
|
self.socket_close()
|
||||||
|
return False
|
||||||
|
|
||||||
|
if status.startswith("unverified"):
|
||||||
|
self.logger.warning(
|
||||||
|
"socket_login: "
|
||||||
|
"invalid APRS-IS password for given call sign"
|
||||||
|
)
|
||||||
|
self.socket_close()
|
||||||
|
return False
|
||||||
|
|
||||||
|
# all validations are successful; we are connected
|
||||||
|
return True
|
||||||
|
|
||||||
|
def socket_send(self, tx_data):
|
||||||
|
"""
|
||||||
|
Generic "Send data to a socket"
|
||||||
|
"""
|
||||||
|
self.logger.debug("socket_send: init")
|
||||||
|
|
||||||
|
# Check if we are connected
|
||||||
|
if not self.sock:
|
||||||
|
self.logger.warning("socket_send: Not connected to APRS-IS")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Encode our data if we are on Python3 or later
|
||||||
|
payload = (
|
||||||
|
tx_data.encode("utf-8") if sys.version_info[0] >= 3 else tx_data
|
||||||
|
)
|
||||||
|
|
||||||
|
# Always call throttle before any remote server i/o is made
|
||||||
|
self.throttle()
|
||||||
|
|
||||||
|
# Try to open the socket
|
||||||
|
# Send the content to APRS-IS
|
||||||
|
try:
|
||||||
|
self.sock.setblocking(True)
|
||||||
|
self.sock.settimeout(self.socket_connect_timeout)
|
||||||
|
self.sock.sendall(payload)
|
||||||
|
|
||||||
|
except socket.gaierror as e:
|
||||||
|
self.logger.warning("Socket Exception socket_send: %s" % str(e))
|
||||||
|
self.sock = None
|
||||||
|
return False
|
||||||
|
|
||||||
|
except socket.timeout as e:
|
||||||
|
self.logger.warning(
|
||||||
|
"Socket Timeout Exception " "socket_send: %s" % str(e)
|
||||||
|
)
|
||||||
|
self.sock = None
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.warning(
|
||||||
|
"General Exception " "socket_send: %s" % str(e)
|
||||||
|
)
|
||||||
|
self.sock = None
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.logger.debug("socket_send: successful")
|
||||||
|
|
||||||
|
# mandatory on several APRS-IS servers
|
||||||
|
# helps to reduce the number of errors where
|
||||||
|
# the server only returns an abbreviated message
|
||||||
|
return True
|
||||||
|
|
||||||
|
def socket_reset(self):
|
||||||
|
"""
|
||||||
|
Resets the socket's buffer
|
||||||
|
"""
|
||||||
|
self.logger.debug("socket_reset: init")
|
||||||
|
_ = self.socket_receive(0)
|
||||||
|
self.logger.debug("socket_reset: successful")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def socket_receive(self, rx_len):
|
||||||
|
"""
|
||||||
|
Generic "Receive data from a socket"
|
||||||
|
"""
|
||||||
|
self.logger.debug("socket_receive: init")
|
||||||
|
|
||||||
|
# Check if we are connected
|
||||||
|
if not self.sock:
|
||||||
|
self.logger.warning("socket_receive: not connected to APRS-IS")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# len is zero in case we intend to
|
||||||
|
# reset the socket
|
||||||
|
if rx_len > 0:
|
||||||
|
self.logger.debug("socket_receive: Receiving data from APRS-IS")
|
||||||
|
|
||||||
|
# Receive content from the socket
|
||||||
|
try:
|
||||||
|
self.sock.setblocking(False)
|
||||||
|
self.sock.settimeout(self.socket_connect_timeout)
|
||||||
|
rx_buf = self.sock.recv(rx_len)
|
||||||
|
|
||||||
|
except socket.gaierror as e:
|
||||||
|
self.logger.warning(
|
||||||
|
"Socket Exception socket_receive: %s" % str(e)
|
||||||
|
)
|
||||||
|
self.sock = None
|
||||||
|
return False
|
||||||
|
|
||||||
|
except socket.timeout as e:
|
||||||
|
self.logger.warning(
|
||||||
|
"Socket Timeout Exception " "socket_receive: %s" % str(e)
|
||||||
|
)
|
||||||
|
self.sock = None
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.warning(
|
||||||
|
"General Exception " "socket_receive: %s" % str(e)
|
||||||
|
)
|
||||||
|
self.sock = None
|
||||||
|
return False
|
||||||
|
|
||||||
|
rx_buf = (
|
||||||
|
rx_buf.decode(self.aprs_encoding)
|
||||||
|
if sys.version_info[0] >= 3 else rx_buf
|
||||||
|
)
|
||||||
|
|
||||||
|
# There will be no data in case we reset the socket
|
||||||
|
if rx_len > 0:
|
||||||
|
self.logger.debug("Received content: {}".format(rx_buf))
|
||||||
|
|
||||||
|
self.logger.debug("socket_receive: successful")
|
||||||
|
|
||||||
|
return rx_buf.rstrip()
|
||||||
|
|
||||||
|
def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
|
||||||
|
"""
|
||||||
|
Perform APRS Notification
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not self.targets:
|
||||||
|
# There is no one to notify; we're done
|
||||||
|
self.logger.warning(
|
||||||
|
"There are no amateur radio call signs to notify"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# prepare payload
|
||||||
|
payload = body
|
||||||
|
|
||||||
|
# sock object is "None" if we were unable to establish a connection
|
||||||
|
# In case of errors, the error message has already been sent
|
||||||
|
# to the logger object
|
||||||
|
if not self.socket_open():
|
||||||
|
return False
|
||||||
|
|
||||||
|
# We have established a successful connection
|
||||||
|
# to the socket server. Now send the login information
|
||||||
|
if not self.aprsis_login():
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Login & authorization confirmed
|
||||||
|
# reset what is in our buffer
|
||||||
|
self.socket_reset()
|
||||||
|
|
||||||
|
# error tracking (used for function return)
|
||||||
|
has_error = False
|
||||||
|
|
||||||
|
# Create a copy of the targets list
|
||||||
|
targets = list(self.targets)
|
||||||
|
|
||||||
|
self.logger.debug("Starting Payload setup")
|
||||||
|
|
||||||
|
# Prepare the outgoing message
|
||||||
|
# Due to APRS's contraints, we need to do
|
||||||
|
# a lot of filtering before we can send
|
||||||
|
# the actual message
|
||||||
|
#
|
||||||
|
# First remove all characters from the
|
||||||
|
# payload that would break APRS
|
||||||
|
# see https://www.aprs.org/doc/APRS101.PDF pg. 71
|
||||||
|
payload = re.sub("[{}|~]+", "", payload)
|
||||||
|
|
||||||
|
payload = ( # pragma: no branch
|
||||||
|
APRS_COMPILED_MAP.sub(
|
||||||
|
lambda x: APRS_BAD_CHARMAP[x.group()], payload)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Finally, constrain output string to 67 characters as
|
||||||
|
# APRS messages are limited in length
|
||||||
|
payload = payload[:67]
|
||||||
|
|
||||||
|
# Our outgoing message MUST end with a CRLF so
|
||||||
|
# let's amend our payload respectively
|
||||||
|
payload = payload.rstrip("\r\n") + "\r\n"
|
||||||
|
|
||||||
|
self.logger.debug("Payload setup complete: {}".format(payload))
|
||||||
|
|
||||||
|
# send the message to our target call sign(s)
|
||||||
|
for index in range(0, len(targets)):
|
||||||
|
# prepare the output string
|
||||||
|
# Format:
|
||||||
|
# Device ID/TOCALL - our call sign - target call sign - body
|
||||||
|
buffer = "{}>{}::{:9}:{}".format(
|
||||||
|
self.user, self.device_id, targets[index], payload
|
||||||
|
)
|
||||||
|
|
||||||
|
# and send the content to the socket
|
||||||
|
# Note that there will be no response from APRS and
|
||||||
|
# that all exceptions are handled within the 'send' method
|
||||||
|
self.logger.debug("Sending APRS message: {}".format(buffer))
|
||||||
|
|
||||||
|
# send the content
|
||||||
|
if not self.socket_send(buffer):
|
||||||
|
has_error = True
|
||||||
|
break
|
||||||
|
|
||||||
|
# Finally, reset our socket buffer
|
||||||
|
# we DO NOT read from the socket as we
|
||||||
|
# would simply listen to the default APRS-IS stream
|
||||||
|
self.socket_reset()
|
||||||
|
|
||||||
|
self.logger.debug("Closing socket.")
|
||||||
|
self.socket_close()
|
||||||
|
self.logger.info(
|
||||||
|
"Sent %d/%d APRS-IS notification(s)", index + 1, len(targets))
|
||||||
|
return not has_error
|
||||||
|
|
||||||
|
def url(self, privacy=False, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Returns the URL built dynamically based on specified arguments.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Define any URL parameters
|
||||||
|
params = {}
|
||||||
|
|
||||||
|
if self.locale != NotifyAprs.template_args["locale"]["default"]:
|
||||||
|
# Store our locale if not default
|
||||||
|
params['locale'] = self.locale
|
||||||
|
|
||||||
|
if self.delay != NotifyAprs.template_args["delay"]["default"]:
|
||||||
|
# Store our locale if not default
|
||||||
|
params['delay'] = "{:.2f}".format(self.delay)
|
||||||
|
|
||||||
|
# Extend our parameters
|
||||||
|
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||||
|
|
||||||
|
# Setup Authentication
|
||||||
|
auth = "{user}:{password}@".format(
|
||||||
|
user=NotifyAprs.quote(self.user, safe=""),
|
||||||
|
password=self.pprint(
|
||||||
|
self.password, privacy, mode=PrivacyMode.Secret, safe=""
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return "{schema}://{auth}{targets}?{params}".format(
|
||||||
|
schema=self.secure_protocol,
|
||||||
|
auth=auth,
|
||||||
|
targets="/".join(chain(
|
||||||
|
[self.pprint(x, privacy, safe="") for x in self.targets],
|
||||||
|
[self.pprint(x, privacy, safe="")
|
||||||
|
for x in self.invalid_targets],
|
||||||
|
)),
|
||||||
|
params=NotifyAprs.urlencode(params),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
"""
|
||||||
|
Returns the number of targets associated with this notification
|
||||||
|
"""
|
||||||
|
targets = len(self.targets)
|
||||||
|
return targets if targets > 0 else 1
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
"""
|
||||||
|
Ensure we close any lingering connections
|
||||||
|
"""
|
||||||
|
self.socket_close()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_url(url):
|
||||||
|
"""
|
||||||
|
Parses the URL and returns enough arguments that can allow
|
||||||
|
us to re-instantiate this object.
|
||||||
|
|
||||||
|
"""
|
||||||
|
results = NotifyBase.parse_url(url, verify_host=False)
|
||||||
|
if not results:
|
||||||
|
# We're done early as we couldn't load the results
|
||||||
|
return results
|
||||||
|
|
||||||
|
# All elements are targets
|
||||||
|
results["targets"] = [NotifyAprs.unquote(results["host"])]
|
||||||
|
|
||||||
|
# All entries after the hostname are additional targets
|
||||||
|
results["targets"].extend(NotifyAprs.split_path(results["fullpath"]))
|
||||||
|
|
||||||
|
# Get Delay (if set)
|
||||||
|
if 'delay' in results['qsd'] and len(results['qsd']['delay']):
|
||||||
|
results['delay'] = NotifyAprs.unquote(results['qsd']['delay'])
|
||||||
|
|
||||||
|
# Support the 'to' variable so that we can support rooms this way too
|
||||||
|
# The 'to' makes it easier to use yaml configuration
|
||||||
|
if "to" in results["qsd"] and len(results["qsd"]["to"]):
|
||||||
|
results["targets"] += NotifyAprs.parse_list(results["qsd"]["to"])
|
||||||
|
|
||||||
|
# Set our APRS-IS server locale's key value and convert it to uppercase
|
||||||
|
if "locale" in results["qsd"] and len(results["qsd"]["locale"]):
|
||||||
|
results["locale"] = NotifyAprs.unquote(
|
||||||
|
results["qsd"]["locale"]
|
||||||
|
).upper()
|
||||||
|
|
||||||
|
return results
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -32,13 +32,13 @@
|
||||||
import requests
|
import requests
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from .NotifyBase import NotifyBase
|
from .base import NotifyBase
|
||||||
from ..URLBase import PrivacyMode
|
from ..url import PrivacyMode
|
||||||
from ..common import NotifyImageSize
|
from ..common import NotifyImageSize
|
||||||
from ..common import NotifyType
|
from ..common import NotifyType
|
||||||
from ..utils import parse_list
|
from ..utils import parse_list
|
||||||
from ..utils import parse_bool
|
from ..utils import parse_bool
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..locale import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
# Sounds generated off of: https://github.com/Finb/Bark/tree/master/Sounds
|
# Sounds generated off of: https://github.com/Finb/Bark/tree/master/Sounds
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -30,15 +30,16 @@ import asyncio
|
||||||
import re
|
import re
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from ..URLBase import URLBase
|
from ..url import URLBase
|
||||||
from ..common import NotifyType
|
from ..common import NotifyType
|
||||||
|
from ..utils import parse_bool
|
||||||
from ..common import NOTIFY_TYPES
|
from ..common import NOTIFY_TYPES
|
||||||
from ..common import NotifyFormat
|
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 ..AppriseLocale import gettext_lazy as _
|
from ..locale import gettext_lazy as _
|
||||||
from ..AppriseAttachment import AppriseAttachment
|
from ..apprise_attachment import AppriseAttachment
|
||||||
|
|
||||||
|
|
||||||
class NotifyBase(URLBase):
|
class NotifyBase(URLBase):
|
||||||
|
@ -135,6 +136,9 @@ class NotifyBase(URLBase):
|
||||||
# Default Overflow Mode
|
# Default Overflow Mode
|
||||||
overflow_mode = OverflowMode.UPSTREAM
|
overflow_mode = OverflowMode.UPSTREAM
|
||||||
|
|
||||||
|
# Default Emoji Interpretation
|
||||||
|
interpret_emojis = False
|
||||||
|
|
||||||
# Support Attachments; this defaults to being disabled.
|
# Support Attachments; this defaults to being disabled.
|
||||||
# Since apprise allows you to send attachments without a body or title
|
# Since apprise allows you to send attachments without a body or title
|
||||||
# defined, by letting Apprise know the plugin won't support attachments
|
# defined, by letting Apprise know the plugin won't support attachments
|
||||||
|
@ -183,8 +187,66 @@ class NotifyBase(URLBase):
|
||||||
# runtime.
|
# runtime.
|
||||||
'_lookup_default': 'notify_format',
|
'_lookup_default': 'notify_format',
|
||||||
},
|
},
|
||||||
|
'emojis': {
|
||||||
|
'name': _('Interpret Emojis'),
|
||||||
|
# SSL Certificate Authority Verification
|
||||||
|
'type': 'bool',
|
||||||
|
# Provide a default
|
||||||
|
'default': interpret_emojis,
|
||||||
|
# look up default using the following parent class value at
|
||||||
|
# runtime.
|
||||||
|
'_lookup_default': 'interpret_emojis',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
#
|
||||||
|
# Overflow Defaults / Configuration applicable to SPLIT mode only
|
||||||
|
#
|
||||||
|
|
||||||
|
# Display Count [X/X]
|
||||||
|
# ^^^^^^
|
||||||
|
# \\\\\\
|
||||||
|
# 6 characters (space + count)
|
||||||
|
# Display Count [XX/XX]
|
||||||
|
# ^^^^^^^^
|
||||||
|
# \\\\\\\\
|
||||||
|
# 8 characters (space + count)
|
||||||
|
# Display Count [XXX/XXX]
|
||||||
|
# ^^^^^^^^^^
|
||||||
|
# \\\\\\\\\\
|
||||||
|
# 10 characters (space + count)
|
||||||
|
# Display Count [XXXX/XXXX]
|
||||||
|
# ^^^^^^^^^^^^
|
||||||
|
# \\\\\\\\\\\\
|
||||||
|
# 12 characters (space + count)
|
||||||
|
#
|
||||||
|
# Given the above + some buffer we come up with the following:
|
||||||
|
# If this value is exceeded, display counts automatically shut off
|
||||||
|
overflow_max_display_count_width = 12
|
||||||
|
|
||||||
|
# The number of characters to reserver for whitespace buffering
|
||||||
|
# This is detected automatically, but you can enforce a value if
|
||||||
|
# you desire:
|
||||||
|
overflow_buffer = 0
|
||||||
|
|
||||||
|
# the min accepted length of a title to allow for a counter display
|
||||||
|
overflow_display_count_threshold = 130
|
||||||
|
|
||||||
|
# Whether or not when over-flow occurs, if the title should be repeated
|
||||||
|
# each time the message is split up
|
||||||
|
# - None: Detect
|
||||||
|
# - True: Always display title once
|
||||||
|
# - False: Display the title for each occurance
|
||||||
|
overflow_display_title_once = None
|
||||||
|
|
||||||
|
# If this is set to to True:
|
||||||
|
# The title_maxlen should be considered as a subset of the body_maxlen
|
||||||
|
# Hence: len(title) + len(body) should never be greater then body_maxlen
|
||||||
|
#
|
||||||
|
# If set to False, then there is no corrorlation between title_maxlen
|
||||||
|
# restrictions and that of body_maxlen
|
||||||
|
overflow_amalgamate_title = False
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
Initialize some general configuration that will keep things consistent
|
Initialize some general configuration that will keep things consistent
|
||||||
|
@ -194,6 +256,29 @@ class NotifyBase(URLBase):
|
||||||
|
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
# Store our interpret_emoji's setting
|
||||||
|
# If asset emoji value is set to a default of True and the user
|
||||||
|
# specifies it to be false, this is accepted and False over-rides.
|
||||||
|
#
|
||||||
|
# If asset emoji value is set to a default of None, a user may
|
||||||
|
# optionally over-ride this and set it to True from the Apprise
|
||||||
|
# URL. ?emojis=yes
|
||||||
|
#
|
||||||
|
# If asset emoji value is set to a default of False, then all emoji's
|
||||||
|
# are turned off (no user over-rides allowed)
|
||||||
|
#
|
||||||
|
|
||||||
|
# Take a default
|
||||||
|
self.interpret_emojis = self.asset.interpret_emojis
|
||||||
|
if 'emojis' in kwargs:
|
||||||
|
# possibly over-ride default
|
||||||
|
self.interpret_emojis = True if self.interpret_emojis \
|
||||||
|
in (None, True) and \
|
||||||
|
parse_bool(
|
||||||
|
kwargs.get('emojis', False),
|
||||||
|
default=NotifyBase.template_args['emojis']['default']) \
|
||||||
|
else False
|
||||||
|
|
||||||
if 'format' in kwargs:
|
if 'format' in kwargs:
|
||||||
# Store the specified format if specified
|
# Store the specified format if specified
|
||||||
notify_format = kwargs.get('format', '')
|
notify_format = kwargs.get('format', '')
|
||||||
|
@ -279,6 +364,17 @@ class NotifyBase(URLBase):
|
||||||
color_type=color_type,
|
color_type=color_type,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def ascii(self, notify_type):
|
||||||
|
"""
|
||||||
|
Returns the ascii characters associated with the notify_type
|
||||||
|
"""
|
||||||
|
if notify_type not in NOTIFY_TYPES:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return self.asset.ascii(
|
||||||
|
notify_type=notify_type,
|
||||||
|
)
|
||||||
|
|
||||||
def notify(self, *args, **kwargs):
|
def notify(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Performs notification
|
Performs notification
|
||||||
|
@ -372,6 +468,19 @@ class NotifyBase(URLBase):
|
||||||
# Handle situations where the title is None
|
# Handle situations where the title is None
|
||||||
title = '' if not title else title
|
title = '' if not title else title
|
||||||
|
|
||||||
|
# Truncate flag set with attachments ensures that only 1
|
||||||
|
# attachment passes through. In the event there could be many
|
||||||
|
# services specified, we only want to do this logic once.
|
||||||
|
# The logic is only applicable if ther was more then 1 attachment
|
||||||
|
# specified
|
||||||
|
overflow = self.overflow_mode if overflow is None else overflow
|
||||||
|
if attach and len(attach) > 1 and overflow == OverflowMode.TRUNCATE:
|
||||||
|
# Save first attachment
|
||||||
|
_attach = AppriseAttachment(attach[0], asset=self.asset)
|
||||||
|
else:
|
||||||
|
# reference same attachment
|
||||||
|
_attach = attach
|
||||||
|
|
||||||
# Apply our overflow (if defined)
|
# Apply our overflow (if defined)
|
||||||
for chunk in self._apply_overflow(
|
for chunk in self._apply_overflow(
|
||||||
body=body, title=title, overflow=overflow,
|
body=body, title=title, overflow=overflow,
|
||||||
|
@ -380,7 +489,7 @@ class NotifyBase(URLBase):
|
||||||
# Send notification
|
# Send notification
|
||||||
yield dict(
|
yield dict(
|
||||||
body=chunk['body'], title=chunk['title'],
|
body=chunk['body'], title=chunk['title'],
|
||||||
notify_type=notify_type, attach=attach,
|
notify_type=notify_type, attach=_attach,
|
||||||
body_format=body_format
|
body_format=body_format
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -400,7 +509,7 @@ class NotifyBase(URLBase):
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'the title goes here',
|
title: 'the title goes here',
|
||||||
body: 'the message body goes here',
|
body: 'the continued message body goes here',
|
||||||
},
|
},
|
||||||
|
|
||||||
]
|
]
|
||||||
|
@ -417,7 +526,6 @@ class NotifyBase(URLBase):
|
||||||
overflow = self.overflow_mode
|
overflow = self.overflow_mode
|
||||||
|
|
||||||
if self.title_maxlen <= 0 and len(title) > 0:
|
if self.title_maxlen <= 0 and len(title) > 0:
|
||||||
|
|
||||||
if self.notify_format == NotifyFormat.HTML:
|
if self.notify_format == NotifyFormat.HTML:
|
||||||
# Content is appended to body as html
|
# Content is appended to body as html
|
||||||
body = '<{open_tag}>{title}</{close_tag}>' \
|
body = '<{open_tag}>{title}</{close_tag}>' \
|
||||||
|
@ -453,29 +561,148 @@ class NotifyBase(URLBase):
|
||||||
response.append({'body': body, 'title': title})
|
response.append({'body': body, 'title': title})
|
||||||
return response
|
return response
|
||||||
|
|
||||||
elif len(title) > self.title_maxlen:
|
# a value of '2' allows for the \r\n that is applied when
|
||||||
# Truncate our Title
|
# amalgamating the title
|
||||||
title = title[:self.title_maxlen]
|
overflow_buffer = max(2, self.overflow_buffer) \
|
||||||
|
if (self.title_maxlen == 0 and len(title)) \
|
||||||
|
else self.overflow_buffer
|
||||||
|
|
||||||
if self.body_maxlen > 0 and len(body) <= self.body_maxlen:
|
#
|
||||||
|
# If we reach here in our code, then we're using TRUNCATE, or SPLIT
|
||||||
|
# actions which require some math to handle the data
|
||||||
|
#
|
||||||
|
|
||||||
|
# Handle situations where our body and title are amalamated into one
|
||||||
|
# calculation
|
||||||
|
title_maxlen = self.title_maxlen \
|
||||||
|
if not self.overflow_amalgamate_title \
|
||||||
|
else min(len(title) + self.overflow_max_display_count_width,
|
||||||
|
self.title_maxlen, self.body_maxlen)
|
||||||
|
|
||||||
|
if len(title) > title_maxlen:
|
||||||
|
# Truncate our Title
|
||||||
|
title = title[:title_maxlen].rstrip()
|
||||||
|
|
||||||
|
if self.overflow_amalgamate_title and (
|
||||||
|
self.body_maxlen - overflow_buffer) >= title_maxlen:
|
||||||
|
body_maxlen = (self.body_maxlen if not title else (
|
||||||
|
self.body_maxlen - title_maxlen)) - overflow_buffer
|
||||||
|
else:
|
||||||
|
# status quo
|
||||||
|
body_maxlen = self.body_maxlen \
|
||||||
|
if not self.overflow_amalgamate_title else \
|
||||||
|
(self.body_maxlen - overflow_buffer)
|
||||||
|
|
||||||
|
if body_maxlen > 0 and len(body) <= body_maxlen:
|
||||||
response.append({'body': body, 'title': title})
|
response.append({'body': body, 'title': title})
|
||||||
return response
|
return response
|
||||||
|
|
||||||
if overflow == OverflowMode.TRUNCATE:
|
if overflow == OverflowMode.TRUNCATE:
|
||||||
# Truncate our body and return
|
# Truncate our body and return
|
||||||
response.append({
|
response.append({
|
||||||
'body': body[:self.body_maxlen],
|
'body': body[:body_maxlen].lstrip('\r\n\x0b\x0c').rstrip(),
|
||||||
'title': title,
|
'title': title,
|
||||||
})
|
})
|
||||||
# For truncate mode, we're done now
|
# For truncate mode, we're done now
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
if self.overflow_display_title_once is None:
|
||||||
|
# Detect if we only display our title once or not:
|
||||||
|
overflow_display_title_once = \
|
||||||
|
True if self.overflow_amalgamate_title and \
|
||||||
|
body_maxlen < self.overflow_display_count_threshold \
|
||||||
|
else False
|
||||||
|
else:
|
||||||
|
# Take on defined value
|
||||||
|
|
||||||
|
overflow_display_title_once = self.overflow_display_title_once
|
||||||
|
|
||||||
# If we reach here, then we are in SPLIT mode.
|
# If we reach here, then we are in SPLIT mode.
|
||||||
# For here, we want to split the message as many times as we have to
|
# For here, we want to split the message as many times as we have to
|
||||||
# in order to fit it within the designated limits.
|
# in order to fit it within the designated limits.
|
||||||
|
if not overflow_display_title_once and not (
|
||||||
|
# edge case that can occur when overflow_display_title_once is
|
||||||
|
# forced off, but no body exists
|
||||||
|
self.overflow_amalgamate_title and body_maxlen <= 0):
|
||||||
|
|
||||||
|
show_counter = title and len(body) > body_maxlen and \
|
||||||
|
((self.overflow_amalgamate_title and
|
||||||
|
body_maxlen >= self.overflow_display_count_threshold) or
|
||||||
|
(not self.overflow_amalgamate_title and
|
||||||
|
title_maxlen > self.overflow_display_count_threshold)) and (
|
||||||
|
title_maxlen > (self.overflow_max_display_count_width +
|
||||||
|
overflow_buffer) and
|
||||||
|
self.title_maxlen >= self.overflow_display_count_threshold)
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
template = ''
|
||||||
|
if show_counter:
|
||||||
|
# introduce padding
|
||||||
|
body_maxlen -= overflow_buffer
|
||||||
|
|
||||||
|
count = int(len(body) / body_maxlen) \
|
||||||
|
+ (1 if len(body) % body_maxlen else 0)
|
||||||
|
|
||||||
|
# Detect padding and prepare template
|
||||||
|
digits = len(str(count))
|
||||||
|
template = ' [{:0%d}/{:0%d}]' % (digits, digits)
|
||||||
|
|
||||||
|
# Update our counter
|
||||||
|
overflow_display_count_width = 4 + (digits * 2)
|
||||||
|
if overflow_display_count_width <= \
|
||||||
|
self.overflow_max_display_count_width:
|
||||||
|
if len(title) > \
|
||||||
|
title_maxlen - overflow_display_count_width:
|
||||||
|
# Truncate our title further
|
||||||
|
title = title[:title_maxlen -
|
||||||
|
overflow_display_count_width]
|
||||||
|
|
||||||
|
else: # Way to many messages to display
|
||||||
|
show_counter = False
|
||||||
|
|
||||||
response = [{
|
response = [{
|
||||||
'body': body[i: i + self.body_maxlen],
|
'body': body[i: i + body_maxlen]
|
||||||
'title': title} for i in range(0, len(body), self.body_maxlen)]
|
.lstrip('\r\n\x0b\x0c').rstrip(),
|
||||||
|
'title': title + (
|
||||||
|
'' if not show_counter else
|
||||||
|
template.format(idx, count))} for idx, i in
|
||||||
|
enumerate(range(0, len(body), body_maxlen), start=1)]
|
||||||
|
|
||||||
|
else: # Display title once and move on
|
||||||
|
response = []
|
||||||
|
try:
|
||||||
|
i = range(0, len(body), body_maxlen)[0]
|
||||||
|
|
||||||
|
response.append({
|
||||||
|
'body': body[i: i + body_maxlen]
|
||||||
|
.lstrip('\r\n\x0b\x0c').rstrip(),
|
||||||
|
'title': title,
|
||||||
|
})
|
||||||
|
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
# IndexError:
|
||||||
|
# - This happens if there simply was no body to display
|
||||||
|
|
||||||
|
# ValueError:
|
||||||
|
# - This happens when body_maxlen < 0 (due to title being
|
||||||
|
# so large)
|
||||||
|
|
||||||
|
# No worries; send title along
|
||||||
|
response.append({
|
||||||
|
'body': '',
|
||||||
|
'title': title,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Ensure our start is set properly
|
||||||
|
body_maxlen = 0
|
||||||
|
|
||||||
|
# Now re-calculate based on the increased length
|
||||||
|
for i in range(body_maxlen, len(body), self.body_maxlen):
|
||||||
|
response.append({
|
||||||
|
'body': body[i: i + self.body_maxlen]
|
||||||
|
.lstrip('\r\n\x0b\x0c').rstrip(),
|
||||||
|
'title': '',
|
||||||
|
})
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@ -548,6 +775,10 @@ class NotifyBase(URLBase):
|
||||||
results['overflow']))
|
results['overflow']))
|
||||||
del results['overflow']
|
del results['overflow']
|
||||||
|
|
||||||
|
# Allow emoji's override
|
||||||
|
if 'emojis' in results['qsd']:
|
||||||
|
results['emojis'] = parse_bool(results['qsd'].get('emojis'))
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -33,20 +33,16 @@ from json import dumps
|
||||||
from time import time
|
from time import time
|
||||||
from hashlib import sha1
|
from hashlib import sha1
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
try:
|
|
||||||
from urlparse import urlparse
|
|
||||||
|
|
||||||
except ImportError:
|
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from .NotifyBase import NotifyBase
|
from .base import NotifyBase
|
||||||
from ..URLBase import PrivacyMode
|
from ..url import PrivacyMode
|
||||||
from ..utils import parse_bool
|
from ..utils import parse_bool
|
||||||
from ..utils import parse_list
|
from ..utils import parse_list
|
||||||
from ..utils import validate_regex
|
from ..utils import validate_regex
|
||||||
from ..common import NotifyType
|
from ..common import NotifyType
|
||||||
from ..common import NotifyImageSize
|
from ..common import NotifyImageSize
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..locale import gettext_lazy as _
|
||||||
|
|
||||||
# Default to sending to all devices if nothing is specified
|
# Default to sending to all devices if nothing is specified
|
||||||
DEFAULT_TAG = '@all'
|
DEFAULT_TAG = '@all'
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -36,13 +36,13 @@ import re
|
||||||
import requests
|
import requests
|
||||||
import json
|
import json
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
from .NotifyBase import NotifyBase
|
from .base import NotifyBase
|
||||||
from ..URLBase import PrivacyMode
|
from ..url import PrivacyMode
|
||||||
from ..common import NotifyType
|
from ..common import NotifyType
|
||||||
from ..utils import is_phone_no
|
from ..utils import is_phone_no
|
||||||
from ..utils import parse_phone_no
|
from ..utils import parse_phone_no
|
||||||
from ..utils import parse_bool
|
from ..utils import parse_bool
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..locale import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
IS_GROUP_RE = re.compile(
|
IS_GROUP_RE = re.compile(
|
||||||
|
@ -248,7 +248,7 @@ class NotifyBulkSMS(NotifyBase):
|
||||||
|
|
||||||
if not (self.targets or self.groups):
|
if not (self.targets or self.groups):
|
||||||
# We have nothing to notify
|
# We have nothing to notify
|
||||||
self.logger.warning('There are no Twist targets to notify')
|
self.logger.warning('There are no BulkSMS targets to notify')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Send in batches if identified to do so
|
# Send in batches if identified to do so
|
394
lib/apprise/plugins/bulkvs.py
Normal file
394
lib/apprise/plugins/bulkvs.py
Normal file
|
@ -0,0 +1,394 @@
|
||||||
|
# -*- 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.
|
||||||
|
|
||||||
|
# To use this service you will need a BulkVS account
|
||||||
|
# You will need credits (new accounts start with a few)
|
||||||
|
# https://www.bulkvs.com/
|
||||||
|
|
||||||
|
# API is documented here:
|
||||||
|
# - https://portal.bulkvs.com/api/v1.0/documentation#/\
|
||||||
|
# Messaging/post_messageSend
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
from .base import NotifyBase
|
||||||
|
from ..url import PrivacyMode
|
||||||
|
from ..common import NotifyType
|
||||||
|
from ..utils import is_phone_no
|
||||||
|
from ..utils import parse_phone_no
|
||||||
|
from ..utils import parse_bool
|
||||||
|
from ..locale import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class NotifyBulkVS(NotifyBase):
|
||||||
|
"""
|
||||||
|
A wrapper for BulkVS Notifications
|
||||||
|
"""
|
||||||
|
|
||||||
|
# The default descriptive name associated with the Notification
|
||||||
|
service_name = 'BulkVS'
|
||||||
|
|
||||||
|
# The services URL
|
||||||
|
service_url = 'https://www.bulkvs.com/'
|
||||||
|
|
||||||
|
# All notification requests are secure
|
||||||
|
secure_protocol = 'bulkvs'
|
||||||
|
|
||||||
|
# A URL that takes you to the setup/help of the specific protocol
|
||||||
|
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_bulkvs'
|
||||||
|
|
||||||
|
# BulkVS uses the http protocol with JSON requests
|
||||||
|
notify_url = 'https://portal.bulkvs.com/api/v1.0/messageSend'
|
||||||
|
|
||||||
|
# The maximum length of the body
|
||||||
|
body_maxlen = 160
|
||||||
|
|
||||||
|
# The maximum amount of texts that can go out in one batch
|
||||||
|
default_batch_size = 4000
|
||||||
|
|
||||||
|
# A title can not be used for SMS Messages. Setting this to zero will
|
||||||
|
# cause any title (if defined) to get placed into the message body.
|
||||||
|
title_maxlen = 0
|
||||||
|
|
||||||
|
# Define object templates
|
||||||
|
templates = (
|
||||||
|
'{schema}://{user}:{password}@{from_phone}/{targets}',
|
||||||
|
'{schema}://{user}:{password}@{from_phone}',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Define our template tokens
|
||||||
|
template_tokens = dict(NotifyBase.template_tokens, **{
|
||||||
|
'user': {
|
||||||
|
'name': _('User Name'),
|
||||||
|
'type': 'string',
|
||||||
|
'required': True,
|
||||||
|
},
|
||||||
|
'password': {
|
||||||
|
'name': _('Password'),
|
||||||
|
'type': 'string',
|
||||||
|
'private': True,
|
||||||
|
'required': True,
|
||||||
|
},
|
||||||
|
'from_phone': {
|
||||||
|
'name': _('From Phone No'),
|
||||||
|
'type': 'string',
|
||||||
|
'regex': (r'^\+?[0-9\s)(+-]+$', 'i'),
|
||||||
|
'map_to': 'source',
|
||||||
|
'required': True,
|
||||||
|
},
|
||||||
|
'target_phone': {
|
||||||
|
'name': _('Target Phone No'),
|
||||||
|
'type': 'string',
|
||||||
|
'prefix': '+',
|
||||||
|
'regex': (r'^[0-9\s)(+-]+$', 'i'),
|
||||||
|
'map_to': 'targets',
|
||||||
|
},
|
||||||
|
'targets': {
|
||||||
|
'name': _('Targets'),
|
||||||
|
'type': 'list:string',
|
||||||
|
'required': True,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
# Define our template arguments
|
||||||
|
template_args = dict(NotifyBase.template_args, **{
|
||||||
|
'to': {
|
||||||
|
'alias_of': 'targets',
|
||||||
|
},
|
||||||
|
'from': {
|
||||||
|
'name': _('From Phone No'),
|
||||||
|
'type': 'string',
|
||||||
|
'regex': (r'^\+?[0-9\s)(+-]+$', 'i'),
|
||||||
|
'map_to': 'source',
|
||||||
|
},
|
||||||
|
'batch': {
|
||||||
|
'name': _('Batch Mode'),
|
||||||
|
'type': 'bool',
|
||||||
|
'default': False,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
def __init__(self, source=None, targets=None, batch=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Initialize BulkVS Object
|
||||||
|
"""
|
||||||
|
super(NotifyBulkVS, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
if not (self.user and self.password):
|
||||||
|
msg = 'A BulkVS user/pass was not provided.'
|
||||||
|
self.logger.warning(msg)
|
||||||
|
raise TypeError(msg)
|
||||||
|
|
||||||
|
result = is_phone_no(source)
|
||||||
|
if not result:
|
||||||
|
msg = 'The Account (From) Phone # specified ' \
|
||||||
|
'({}) is invalid.'.format(source)
|
||||||
|
self.logger.warning(msg)
|
||||||
|
raise TypeError(msg)
|
||||||
|
|
||||||
|
# Tidy source
|
||||||
|
self.source = result['full']
|
||||||
|
|
||||||
|
# Define whether or not we should operate in a batch mode
|
||||||
|
self.batch = self.template_args['batch']['default'] \
|
||||||
|
if batch is None else bool(batch)
|
||||||
|
|
||||||
|
# Parse our targets
|
||||||
|
self.targets = list()
|
||||||
|
|
||||||
|
has_error = False
|
||||||
|
for target in parse_phone_no(targets):
|
||||||
|
# Parse each phone number we found
|
||||||
|
result = is_phone_no(target)
|
||||||
|
if result:
|
||||||
|
self.targets.append(result['full'])
|
||||||
|
continue
|
||||||
|
|
||||||
|
has_error = True
|
||||||
|
self.logger.warning(
|
||||||
|
'Dropped invalid phone # ({}) specified.'.format(target),
|
||||||
|
)
|
||||||
|
|
||||||
|
if not targets and not has_error:
|
||||||
|
# Default the SMS Message to ourselves
|
||||||
|
self.targets.append(self.source)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||||
|
"""
|
||||||
|
Perform BulkVS Notification
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not self.targets:
|
||||||
|
# We have nothing to notify
|
||||||
|
self.logger.warning('There are no BulkVS targets to notify')
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Send in batches if identified to do so
|
||||||
|
batch_size = 1 if not self.batch else self.default_batch_size
|
||||||
|
|
||||||
|
# error tracking (used for function return)
|
||||||
|
has_error = False
|
||||||
|
|
||||||
|
# Prepare our headers
|
||||||
|
headers = {
|
||||||
|
'User-Agent': self.app_id,
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Prepare our payload
|
||||||
|
payload = {
|
||||||
|
# The To gets populated in the loop below
|
||||||
|
'From': self.source,
|
||||||
|
'To': None,
|
||||||
|
'Message': body,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Authentication
|
||||||
|
auth = (self.user, self.password)
|
||||||
|
|
||||||
|
# Prepare our targets
|
||||||
|
targets = list(self.targets) if batch_size == 1 else \
|
||||||
|
[self.targets[index:index + batch_size]
|
||||||
|
for index in range(0, len(self.targets), batch_size)]
|
||||||
|
|
||||||
|
while len(targets):
|
||||||
|
# Get our target to notify
|
||||||
|
target = targets.pop(0)
|
||||||
|
|
||||||
|
# Prepare our user
|
||||||
|
payload['To'] = target
|
||||||
|
|
||||||
|
# Printable reference
|
||||||
|
if isinstance(target, list):
|
||||||
|
p_target = '{} targets'.format(len(target))
|
||||||
|
|
||||||
|
else:
|
||||||
|
p_target = target
|
||||||
|
|
||||||
|
# Some Debug Logging
|
||||||
|
self.logger.debug('BulkVS POST URL: {} (cert_verify={})'.format(
|
||||||
|
self.notify_url, self.verify_certificate))
|
||||||
|
self.logger.debug('BulkVS Payload: {}' .format(payload))
|
||||||
|
|
||||||
|
# Always call throttle before any remote server i/o is made
|
||||||
|
self.throttle()
|
||||||
|
try:
|
||||||
|
r = requests.post(
|
||||||
|
self.notify_url,
|
||||||
|
data=json.dumps(payload),
|
||||||
|
headers=headers,
|
||||||
|
auth=auth,
|
||||||
|
verify=self.verify_certificate,
|
||||||
|
timeout=self.request_timeout,
|
||||||
|
)
|
||||||
|
|
||||||
|
# A Response may look like:
|
||||||
|
# {
|
||||||
|
# "RefId": "5a66dee6-ff7a-40ee-8218-5805c074dc01",
|
||||||
|
# "From": "13109060901",
|
||||||
|
# "MessageType": "SMS|MMS",
|
||||||
|
# "Results": [
|
||||||
|
# {
|
||||||
|
# "To": "13105551212",
|
||||||
|
# "Status": "SUCCESS"
|
||||||
|
# },
|
||||||
|
# {
|
||||||
|
# "To": "13105551213",
|
||||||
|
# "Status": "SUCCESS"
|
||||||
|
# }
|
||||||
|
# ]
|
||||||
|
# }
|
||||||
|
if r.status_code != requests.codes.ok:
|
||||||
|
# We had a problem
|
||||||
|
status_str = \
|
||||||
|
NotifyBase.http_response_code_lookup(r.status_code)
|
||||||
|
|
||||||
|
# set up our status code to use
|
||||||
|
status_code = r.status_code
|
||||||
|
|
||||||
|
self.logger.warning(
|
||||||
|
'Failed to send BulkVS notification to {}: '
|
||||||
|
'{}{}error={}.'.format(
|
||||||
|
p_target,
|
||||||
|
status_str,
|
||||||
|
', ' if status_str else '',
|
||||||
|
status_code))
|
||||||
|
|
||||||
|
self.logger.debug(
|
||||||
|
'Response Details:\r\n{}'.format(r.content))
|
||||||
|
|
||||||
|
# Mark our failure
|
||||||
|
has_error = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.logger.info(
|
||||||
|
'Sent BulkVS notification to {}.'.format(p_target))
|
||||||
|
|
||||||
|
except requests.RequestException as e:
|
||||||
|
self.logger.warning(
|
||||||
|
'A Connection error occurred sending BulkVS: to %s ',
|
||||||
|
p_target)
|
||||||
|
self.logger.debug('Socket Exception: %s' % str(e))
|
||||||
|
|
||||||
|
# Mark our failure
|
||||||
|
has_error = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
return not has_error
|
||||||
|
|
||||||
|
def url(self, privacy=False, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Returns the URL built dynamically based on specified arguments.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Define any URL parameters
|
||||||
|
params = {
|
||||||
|
'batch': 'yes' if self.batch else 'no',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extend our parameters
|
||||||
|
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||||
|
|
||||||
|
# A nice way of cleaning up the URL length a bit
|
||||||
|
targets = [] if len(self.targets) == 1 \
|
||||||
|
and self.targets[0] == self.source else self.targets
|
||||||
|
|
||||||
|
return '{schema}://{user}:{password}@{source}/{targets}' \
|
||||||
|
'?{params}'.format(
|
||||||
|
schema=self.secure_protocol,
|
||||||
|
source=self.source,
|
||||||
|
user=self.pprint(self.user, privacy, safe=''),
|
||||||
|
password=self.pprint(
|
||||||
|
self.password, privacy, mode=PrivacyMode.Secret, safe=''),
|
||||||
|
targets='/'.join([
|
||||||
|
NotifyBulkVS.quote('{}'.format(x), safe='+')
|
||||||
|
for x in targets]),
|
||||||
|
params=NotifyBulkVS.urlencode(params))
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
"""
|
||||||
|
Returns the number of targets associated with this notification
|
||||||
|
"""
|
||||||
|
|
||||||
|
#
|
||||||
|
# Factor batch into calculation
|
||||||
|
#
|
||||||
|
batch_size = 1 if not self.batch else self.default_batch_size
|
||||||
|
targets = len(self.targets) if self.targets else 1
|
||||||
|
if batch_size > 1:
|
||||||
|
targets = int(targets / batch_size) + \
|
||||||
|
(1 if targets % batch_size else 0)
|
||||||
|
|
||||||
|
return targets
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_url(url):
|
||||||
|
"""
|
||||||
|
Parses the URL and returns enough arguments that can allow
|
||||||
|
us to re-instantiate this object.
|
||||||
|
|
||||||
|
"""
|
||||||
|
results = NotifyBase.parse_url(url, verify_host=False)
|
||||||
|
if not results:
|
||||||
|
# We're done early as we couldn't load the results
|
||||||
|
return results
|
||||||
|
|
||||||
|
# Support the 'from' and 'source' variable so that we can support
|
||||||
|
# targets this way too.
|
||||||
|
# The 'from' makes it easier to use yaml configuration
|
||||||
|
if 'from' in results['qsd'] and len(results['qsd']['from']):
|
||||||
|
results['source'] = \
|
||||||
|
NotifyBulkVS.unquote(results['qsd']['from'])
|
||||||
|
|
||||||
|
# hostname will also be a target in this case
|
||||||
|
results['targets'] = [
|
||||||
|
*NotifyBulkVS.parse_phone_no(results['host']),
|
||||||
|
*NotifyBulkVS.split_path(results['fullpath'])]
|
||||||
|
|
||||||
|
else:
|
||||||
|
# store our source
|
||||||
|
results['source'] = NotifyBulkVS.unquote(results['host'])
|
||||||
|
|
||||||
|
# store targets
|
||||||
|
results['targets'] = NotifyBulkVS.split_path(results['fullpath'])
|
||||||
|
|
||||||
|
# Support the 'to' variable so that we can support targets this way too
|
||||||
|
# The 'to' makes it easier to use yaml configuration
|
||||||
|
if 'to' in results['qsd'] and len(results['qsd']['to']):
|
||||||
|
results['targets'] += \
|
||||||
|
NotifyBulkVS.parse_phone_no(results['qsd']['to'])
|
||||||
|
|
||||||
|
# Get Batch Mode Flag
|
||||||
|
results['batch'] = \
|
||||||
|
parse_bool(results['qsd'].get(
|
||||||
|
'batch', NotifyBulkVS.template_args['batch']['default']))
|
||||||
|
|
||||||
|
return results
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -33,14 +33,14 @@
|
||||||
#
|
#
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from .NotifyBase import NotifyBase
|
from .base import NotifyBase
|
||||||
from ..URLBase import PrivacyMode
|
from ..url import PrivacyMode
|
||||||
from ..common import NotifyType
|
from ..common import NotifyType
|
||||||
from ..utils import is_phone_no
|
from ..utils import is_phone_no
|
||||||
from ..utils import parse_phone_no
|
from ..utils import parse_phone_no
|
||||||
from ..utils import parse_bool
|
from ..utils import parse_bool
|
||||||
from ..utils import validate_regex
|
from ..utils import validate_regex
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..locale import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class BurstSMSCountryCode:
|
class BurstSMSCountryCode:
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -26,118 +26,111 @@
|
||||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
# POSSIBILITY OF SUCH DAMAGE.
|
# POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
# Chantify
|
||||||
|
# 1. Visit https://chanify.net/
|
||||||
|
|
||||||
|
# The API URL will look something like this:
|
||||||
|
# https://api.chanify.net/v1/sender/token
|
||||||
|
#
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from .NotifyBase import NotifyBase
|
from .base import NotifyBase
|
||||||
from ..common import NotifyImageSize
|
|
||||||
from ..common import NotifyType
|
from ..common import NotifyType
|
||||||
from ..utils import parse_bool
|
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
|
||||||
from ..utils import validate_regex
|
from ..utils import validate_regex
|
||||||
|
from ..locale import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class NotifyFaast(NotifyBase):
|
class NotifyChantify(NotifyBase):
|
||||||
"""
|
"""
|
||||||
A wrapper for Faast Notifications
|
A wrapper for Chantify Notifications
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# The default descriptive name associated with the Notification
|
# The default descriptive name associated with the Notification
|
||||||
service_name = 'Faast'
|
service_name = _('Chantify')
|
||||||
|
|
||||||
# The services URL
|
# The services URL
|
||||||
service_url = 'http://www.faast.io/'
|
service_url = 'https://chanify.net/'
|
||||||
|
|
||||||
# The default protocol (this is secure for faast)
|
# The default secure protocol
|
||||||
protocol = 'faast'
|
secure_protocol = 'chantify'
|
||||||
|
|
||||||
# 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
|
||||||
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_faast'
|
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_chantify'
|
||||||
|
|
||||||
# Faast uses the http protocol with JSON requests
|
# Notification URL
|
||||||
notify_url = 'https://www.appnotifications.com/account/notifications.json'
|
notify_url = 'https://api.chanify.net/v1/sender/{token}/'
|
||||||
|
|
||||||
# Allows the user to specify the NotifyImageSize object
|
|
||||||
image_size = NotifyImageSize.XY_72
|
|
||||||
|
|
||||||
# Define object templates
|
# Define object templates
|
||||||
templates = (
|
templates = (
|
||||||
'{schema}://{authtoken}',
|
'{schema}://{token}',
|
||||||
)
|
)
|
||||||
|
|
||||||
# Define our template tokens
|
# The title is not used
|
||||||
|
title_maxlen = 0
|
||||||
|
|
||||||
|
# Define our tokens; these are the minimum tokens required required to
|
||||||
|
# be passed into this function (as arguments). The syntax appends any
|
||||||
|
# previously defined in the base package and builds onto them
|
||||||
template_tokens = dict(NotifyBase.template_tokens, **{
|
template_tokens = dict(NotifyBase.template_tokens, **{
|
||||||
'authtoken': {
|
'token': {
|
||||||
'name': _('Authorization Token'),
|
'name': _('Token'),
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
'private': True,
|
'private': True,
|
||||||
'required': True,
|
'required': True,
|
||||||
|
'regex': (r'^[A-Z0-9_-]+$', 'i'),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
# Define our template arguments
|
# Define our template arguments
|
||||||
template_args = dict(NotifyBase.template_args, **{
|
template_args = dict(NotifyBase.template_args, **{
|
||||||
'image': {
|
'token': {
|
||||||
'name': _('Include Image'),
|
'alias_of': 'token',
|
||||||
'type': 'bool',
|
|
||||||
'default': True,
|
|
||||||
'map_to': 'include_image',
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
def __init__(self, authtoken, include_image=True, **kwargs):
|
def __init__(self, token, **kwargs):
|
||||||
"""
|
"""
|
||||||
Initialize Faast Object
|
Initialize Chantify Object
|
||||||
"""
|
"""
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
# Store the Authentication Token
|
self.token = validate_regex(
|
||||||
self.authtoken = validate_regex(authtoken)
|
token, *self.template_tokens['token']['regex'])
|
||||||
if not self.authtoken:
|
if not self.token:
|
||||||
msg = 'An invalid Faast Authentication Token ' \
|
msg = 'The Chantify token specified ({}) is invalid.'\
|
||||||
'({}) was specified.'.format(authtoken)
|
.format(token)
|
||||||
self.logger.warning(msg)
|
self.logger.warning(msg)
|
||||||
raise TypeError(msg)
|
raise TypeError(msg)
|
||||||
|
|
||||||
# Associate an image with our post
|
|
||||||
self.include_image = include_image
|
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||||
"""
|
"""
|
||||||
Perform Faast Notification
|
Send our notification
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# prepare our headers
|
||||||
headers = {
|
headers = {
|
||||||
'User-Agent': self.app_id,
|
'User-Agent': self.app_id,
|
||||||
'Content-Type': 'multipart/form-data'
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
}
|
}
|
||||||
|
|
||||||
# prepare JSON Object
|
# Our Message
|
||||||
payload = {
|
payload = {
|
||||||
'user_credentials': self.authtoken,
|
'text': body
|
||||||
'title': title,
|
|
||||||
'message': body,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Acquire our image if we're configured to do so
|
self.logger.debug('Chantify GET URL: %s (cert_verify=%r)' % (
|
||||||
image_url = None if not self.include_image \
|
self.notify_url, self.verify_certificate))
|
||||||
else self.image_url(notify_type)
|
self.logger.debug('Chantify Payload: %s' % str(payload))
|
||||||
|
|
||||||
if image_url:
|
|
||||||
payload['icon_url'] = image_url
|
|
||||||
|
|
||||||
self.logger.debug('Faast POST URL: %s (cert_verify=%r)' % (
|
|
||||||
self.notify_url, self.verify_certificate,
|
|
||||||
))
|
|
||||||
self.logger.debug('Faast Payload: %s' % str(payload))
|
|
||||||
|
|
||||||
# Always call throttle before any remote server i/o is made
|
# Always call throttle before any remote server i/o is made
|
||||||
self.throttle()
|
self.throttle()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
r = requests.post(
|
r = requests.post(
|
||||||
self.notify_url,
|
self.notify_url.format(token=self.token),
|
||||||
data=payload,
|
data=payload,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
verify=self.verify_certificate,
|
verify=self.verify_certificate,
|
||||||
|
@ -146,10 +139,10 @@ class NotifyFaast(NotifyBase):
|
||||||
if r.status_code != requests.codes.ok:
|
if r.status_code != requests.codes.ok:
|
||||||
# We had a problem
|
# We had a problem
|
||||||
status_str = \
|
status_str = \
|
||||||
NotifyFaast.http_response_code_lookup(r.status_code)
|
NotifyChantify.http_response_code_lookup(r.status_code)
|
||||||
|
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
'Failed to send Faast notification:'
|
'Failed to send Chantify notification: '
|
||||||
'{}{}error={}.'.format(
|
'{}{}error={}.'.format(
|
||||||
status_str,
|
status_str,
|
||||||
', ' if status_str else '',
|
', ' if status_str else '',
|
||||||
|
@ -161,12 +154,12 @@ class NotifyFaast(NotifyBase):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.logger.info('Sent Faast notification.')
|
self.logger.info('Sent Chantify notification.')
|
||||||
|
|
||||||
except requests.RequestException as e:
|
except requests.RequestException as e:
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
'A Connection error occurred sending Faast notification.',
|
'A Connection error occurred sending Chantify '
|
||||||
)
|
'notification.')
|
||||||
self.logger.debug('Socket Exception: %s' % str(e))
|
self.logger.debug('Socket Exception: %s' % str(e))
|
||||||
|
|
||||||
# Return; we're done
|
# Return; we're done
|
||||||
|
@ -179,18 +172,13 @@ class NotifyFaast(NotifyBase):
|
||||||
Returns the URL built dynamically based on specified arguments.
|
Returns the URL built dynamically based on specified arguments.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Define any URL parameters
|
# Prepare our parameters
|
||||||
params = {
|
params = self.url_parameters(privacy=privacy, *args, **kwargs)
|
||||||
'image': 'yes' if self.include_image else 'no',
|
|
||||||
}
|
|
||||||
|
|
||||||
# Extend our parameters
|
return '{schema}://{token}/?{params}'.format(
|
||||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
schema=self.secure_protocol,
|
||||||
|
token=self.pprint(self.token, privacy, safe=''),
|
||||||
return '{schema}://{authtoken}/?{params}'.format(
|
params=NotifyChantify.urlencode(params),
|
||||||
schema=self.protocol,
|
|
||||||
authtoken=self.pprint(self.authtoken, privacy, safe=''),
|
|
||||||
params=NotifyFaast.urlencode(params),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -200,16 +188,19 @@ class NotifyFaast(NotifyBase):
|
||||||
us to re-instantiate this object.
|
us to re-instantiate this object.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# parse_url already handles getting the `user` and `password` fields
|
||||||
|
# populated.
|
||||||
results = NotifyBase.parse_url(url, verify_host=False)
|
results = NotifyBase.parse_url(url, verify_host=False)
|
||||||
if not results:
|
if not results:
|
||||||
# We're done early as we couldn't load the results
|
# We're done early as we couldn't load the results
|
||||||
return results
|
return results
|
||||||
|
|
||||||
# Store our authtoken using the host
|
# Allow over-ride
|
||||||
results['authtoken'] = NotifyFaast.unquote(results['host'])
|
if 'token' in results['qsd'] and len(results['qsd']['token']):
|
||||||
|
results['token'] = NotifyChantify.unquote(results['qsd']['token'])
|
||||||
|
|
||||||
# Include image with our post
|
else:
|
||||||
results['include_image'] = \
|
results['token'] = NotifyChantify.unquote(results['host'])
|
||||||
parse_bool(results['qsd'].get('image', True))
|
|
||||||
|
|
||||||
return results
|
return results
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -41,15 +41,14 @@
|
||||||
#
|
#
|
||||||
import requests
|
import requests
|
||||||
from json import dumps
|
from json import dumps
|
||||||
from base64 import b64encode
|
|
||||||
|
|
||||||
from .NotifyBase import NotifyBase
|
from .base import NotifyBase
|
||||||
from ..URLBase import PrivacyMode
|
from ..url import PrivacyMode
|
||||||
from ..common import NotifyType
|
from ..common import NotifyType
|
||||||
from ..utils import is_phone_no
|
from ..utils import is_phone_no
|
||||||
from ..utils import parse_phone_no
|
from ..utils import parse_phone_no
|
||||||
from ..utils import parse_bool
|
from ..utils import parse_bool
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..locale import gettext_lazy as _
|
||||||
|
|
||||||
# Extend HTTP Error Messages
|
# Extend HTTP Error Messages
|
||||||
CLICKSEND_HTTP_ERROR_MAP = {
|
CLICKSEND_HTTP_ERROR_MAP = {
|
||||||
|
@ -89,7 +88,7 @@ class NotifyClickSend(NotifyBase):
|
||||||
|
|
||||||
# Define object templates
|
# Define object templates
|
||||||
templates = (
|
templates = (
|
||||||
'{schema}://{user}:{password}@{targets}',
|
'{schema}://{user}:{apikey}@{targets}',
|
||||||
)
|
)
|
||||||
|
|
||||||
# Define our template tokens
|
# Define our template tokens
|
||||||
|
@ -99,11 +98,12 @@ class NotifyClickSend(NotifyBase):
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
'required': True,
|
'required': True,
|
||||||
},
|
},
|
||||||
'password': {
|
'apikey': {
|
||||||
'name': _('Password'),
|
'name': _('API Key'),
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
'private': True,
|
'private': True,
|
||||||
'required': True,
|
'required': True,
|
||||||
|
'map_to': 'password',
|
||||||
},
|
},
|
||||||
'target_phone': {
|
'target_phone': {
|
||||||
'name': _('Target Phone No'),
|
'name': _('Target Phone No'),
|
||||||
|
@ -124,6 +124,9 @@ class NotifyClickSend(NotifyBase):
|
||||||
'to': {
|
'to': {
|
||||||
'alias_of': 'targets',
|
'alias_of': 'targets',
|
||||||
},
|
},
|
||||||
|
'key': {
|
||||||
|
'alias_of': 'apikey',
|
||||||
|
},
|
||||||
'batch': {
|
'batch': {
|
||||||
'name': _('Batch Mode'),
|
'name': _('Batch Mode'),
|
||||||
'type': 'bool',
|
'type': 'bool',
|
||||||
|
@ -174,9 +177,6 @@ class NotifyClickSend(NotifyBase):
|
||||||
headers = {
|
headers = {
|
||||||
'User-Agent': self.app_id,
|
'User-Agent': self.app_id,
|
||||||
'Content-Type': 'application/json; charset=utf-8',
|
'Content-Type': 'application/json; charset=utf-8',
|
||||||
'Authorization': 'Basic {}'.format(
|
|
||||||
b64encode('{}:{}'.format(
|
|
||||||
self.user, self.password).encode('utf-8'))),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# error tracking (used for function return)
|
# error tracking (used for function return)
|
||||||
|
@ -208,6 +208,7 @@ class NotifyClickSend(NotifyBase):
|
||||||
r = requests.post(
|
r = requests.post(
|
||||||
self.notify_url,
|
self.notify_url,
|
||||||
data=dumps(payload),
|
data=dumps(payload),
|
||||||
|
auth=(self.user, self.password),
|
||||||
headers=headers,
|
headers=headers,
|
||||||
verify=self.verify_certificate,
|
verify=self.verify_certificate,
|
||||||
timeout=self.request_timeout,
|
timeout=self.request_timeout,
|
||||||
|
@ -322,6 +323,12 @@ class NotifyClickSend(NotifyBase):
|
||||||
results['batch'] = \
|
results['batch'] = \
|
||||||
parse_bool(results['qsd'].get('batch', False))
|
parse_bool(results['qsd'].get('batch', False))
|
||||||
|
|
||||||
|
# API Key
|
||||||
|
if 'key' in results['qsd'] and len(results['qsd']['key']):
|
||||||
|
# Extract the API Key from an argument
|
||||||
|
results['password'] = \
|
||||||
|
NotifyClickSend.unquote(results['qsd']['key'])
|
||||||
|
|
||||||
# Support the 'to' variable so that we can support rooms this way too
|
# Support the 'to' variable so that we can support rooms this way too
|
||||||
# The 'to' makes it easier to use yaml configuration
|
# The 'to' makes it easier to use yaml configuration
|
||||||
if 'to' in results['qsd'] and len(results['qsd']['to']):
|
if 'to' in results['qsd'] and len(results['qsd']['to']):
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -29,11 +29,11 @@
|
||||||
import re
|
import re
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from .NotifyBase import NotifyBase
|
from .base import NotifyBase
|
||||||
from ..URLBase import PrivacyMode
|
from ..url import PrivacyMode
|
||||||
from ..common import NotifyImageSize
|
from ..common import NotifyImageSize
|
||||||
from ..common import NotifyType
|
from ..common import NotifyType
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..locale import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class FORMPayloadField:
|
class FORMPayloadField:
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -30,11 +30,11 @@ import requests
|
||||||
import base64
|
import base64
|
||||||
from json import dumps
|
from json import dumps
|
||||||
|
|
||||||
from .NotifyBase import NotifyBase
|
from .base import NotifyBase
|
||||||
from ..URLBase import PrivacyMode
|
from ..url import PrivacyMode
|
||||||
from ..common import NotifyImageSize
|
from ..common import NotifyImageSize
|
||||||
from ..common import NotifyType
|
from ..common import NotifyType
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..locale import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class JSONPayloadField:
|
class JSONPayloadField:
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -30,11 +30,11 @@ import re
|
||||||
import requests
|
import requests
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
from .NotifyBase import NotifyBase
|
from .base import NotifyBase
|
||||||
from ..URLBase import PrivacyMode
|
from ..url import PrivacyMode
|
||||||
from ..common import NotifyImageSize
|
from ..common import NotifyImageSize
|
||||||
from ..common import NotifyType
|
from ..common import NotifyType
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..locale import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class XMLPayloadField:
|
class XMLPayloadField:
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -39,13 +39,13 @@ import requests
|
||||||
from json import dumps
|
from json import dumps
|
||||||
from json import loads
|
from json import loads
|
||||||
|
|
||||||
from .NotifyBase import NotifyBase
|
from .base import NotifyBase
|
||||||
from ..common import NotifyType
|
from ..common import NotifyType
|
||||||
from ..utils import is_phone_no
|
from ..utils import is_phone_no
|
||||||
from ..utils import parse_phone_no
|
from ..utils import parse_phone_no
|
||||||
from ..utils import validate_regex
|
from ..utils import validate_regex
|
||||||
from ..utils import parse_bool
|
from ..utils import parse_bool
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..locale import gettext_lazy as _
|
||||||
|
|
||||||
# Extend HTTP Error Messages
|
# Extend HTTP Error Messages
|
||||||
D7NETWORKS_HTTP_ERROR_MAP = {
|
D7NETWORKS_HTTP_ERROR_MAP = {
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -51,9 +51,9 @@ from json import dumps
|
||||||
import requests
|
import requests
|
||||||
from requests.auth import HTTPBasicAuth
|
from requests.auth import HTTPBasicAuth
|
||||||
|
|
||||||
from .NotifyBase import NotifyBase
|
from .base import NotifyBase
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..locale import gettext_lazy as _
|
||||||
from ..URLBase import PrivacyMode
|
from ..url import PrivacyMode
|
||||||
from ..common import NotifyType
|
from ..common import NotifyType
|
||||||
from ..utils import is_call_sign
|
from ..utils import is_call_sign
|
||||||
from ..utils import parse_call_sign
|
from ..utils import parse_call_sign
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -26,15 +26,12 @@
|
||||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
# POSSIBILITY OF SUCH DAMAGE.
|
# POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
from __future__ import absolute_import
|
|
||||||
from __future__ import print_function
|
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
from .NotifyBase import NotifyBase
|
from .base import NotifyBase
|
||||||
from ..common import NotifyImageSize
|
from ..common import NotifyImageSize
|
||||||
from ..common import NotifyType
|
from ..common import NotifyType
|
||||||
from ..utils import parse_bool
|
from ..utils import parse_bool
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..locale import gettext_lazy as _
|
||||||
|
|
||||||
# Default our global support flag
|
# Default our global support flag
|
||||||
NOTIFY_DBUS_SUPPORT_ENABLED = False
|
NOTIFY_DBUS_SUPPORT_ENABLED = False
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -34,13 +34,13 @@ import base64
|
||||||
import requests
|
import requests
|
||||||
from json import dumps
|
from json import dumps
|
||||||
|
|
||||||
from .NotifyBase import NotifyBase
|
from .base import NotifyBase
|
||||||
from ..URLBase import PrivacyMode
|
from ..url import PrivacyMode
|
||||||
from ..common import NotifyFormat
|
from ..common import NotifyFormat
|
||||||
from ..common import NotifyType
|
from ..common import NotifyType
|
||||||
from ..utils import parse_list
|
from ..utils import parse_list
|
||||||
from ..utils import validate_regex
|
from ..utils import validate_regex
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..locale import gettext_lazy as _
|
||||||
|
|
||||||
# Register at https://dingtalk.com
|
# Register at https://dingtalk.com
|
||||||
# - Download their PC based software as it is the only way you can create
|
# - Download their PC based software as it is the only way you can create
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -50,14 +50,19 @@ from datetime import timedelta
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from datetime import timezone
|
from datetime import timezone
|
||||||
|
|
||||||
from .NotifyBase import NotifyBase
|
from .base import NotifyBase
|
||||||
from ..common import NotifyImageSize
|
from ..common import NotifyImageSize
|
||||||
from ..common import NotifyFormat
|
from ..common import NotifyFormat
|
||||||
from ..common import NotifyType
|
from ..common import NotifyType
|
||||||
from ..utils import parse_bool
|
from ..utils import parse_bool
|
||||||
from ..utils import validate_regex
|
from ..utils import validate_regex
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..locale import gettext_lazy as _
|
||||||
from ..attachment.AttachBase import AttachBase
|
from ..attachment.base import AttachBase
|
||||||
|
|
||||||
|
|
||||||
|
# Used to detect user/role IDs
|
||||||
|
USER_ROLE_DETECTION_RE = re.compile(
|
||||||
|
r'\s*(?:<@(?P<role>&?)(?P<id>[0-9]+)>|@(?P<value>[a-z0-9]+))', re.I)
|
||||||
|
|
||||||
|
|
||||||
class NotifyDiscord(NotifyBase):
|
class NotifyDiscord(NotifyBase):
|
||||||
|
@ -100,6 +105,10 @@ class NotifyDiscord(NotifyBase):
|
||||||
# The maximum allowable characters allowed in the body per message
|
# The maximum allowable characters allowed in the body per message
|
||||||
body_maxlen = 2000
|
body_maxlen = 2000
|
||||||
|
|
||||||
|
# The 2000 characters above defined by the body_maxlen include that of the
|
||||||
|
# title. Setting this to True ensures overflow options behave properly
|
||||||
|
overflow_amalgamate_title = True
|
||||||
|
|
||||||
# Discord has a limit of the number of fields you can include in an
|
# Discord has a limit of the number of fields you can include in an
|
||||||
# embeds message. This value allows the discord message to safely
|
# embeds message. This value allows the discord message to safely
|
||||||
# break into multiple messages to handle these cases.
|
# break into multiple messages to handle these cases.
|
||||||
|
@ -336,6 +345,33 @@ class NotifyDiscord(NotifyBase):
|
||||||
payload['content'] = \
|
payload['content'] = \
|
||||||
body if not title else "{}\r\n{}".format(title, body)
|
body if not title else "{}\r\n{}".format(title, body)
|
||||||
|
|
||||||
|
# parse for user id's <@123> and role IDs <@&456>
|
||||||
|
results = USER_ROLE_DETECTION_RE.findall(body)
|
||||||
|
if results:
|
||||||
|
payload['allow_mentions'] = {
|
||||||
|
'parse': [],
|
||||||
|
'users': [],
|
||||||
|
'roles': [],
|
||||||
|
}
|
||||||
|
|
||||||
|
_content = []
|
||||||
|
for (is_role, no, value) in results:
|
||||||
|
if value:
|
||||||
|
payload['allow_mentions']['parse'].append(value)
|
||||||
|
_content.append(f'@{value}')
|
||||||
|
|
||||||
|
elif is_role:
|
||||||
|
payload['allow_mentions']['roles'].append(no)
|
||||||
|
_content.append(f'<@&{no}>')
|
||||||
|
|
||||||
|
else: # is_user
|
||||||
|
payload['allow_mentions']['users'].append(no)
|
||||||
|
_content.append(f'<@{no}>')
|
||||||
|
|
||||||
|
if self.notify_format == NotifyFormat.MARKDOWN:
|
||||||
|
# Add pingable elements to content field
|
||||||
|
payload['content'] = '👉 ' + ' '.join(_content)
|
||||||
|
|
||||||
if not self._send(payload, params=params):
|
if not self._send(payload, params=params):
|
||||||
# We failed to post our message
|
# We failed to post our message
|
||||||
return False
|
return False
|
||||||
|
@ -360,16 +396,21 @@ class NotifyDiscord(NotifyBase):
|
||||||
'wait': True,
|
'wait': True,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
#
|
||||||
# Remove our text/title based content for attachment use
|
# Remove our text/title based content for attachment use
|
||||||
|
#
|
||||||
if 'embeds' in payload:
|
if 'embeds' in payload:
|
||||||
# Markdown
|
|
||||||
del payload['embeds']
|
del payload['embeds']
|
||||||
|
|
||||||
if 'content' in payload:
|
if 'content' in payload:
|
||||||
# Markdown
|
|
||||||
del payload['content']
|
del payload['content']
|
||||||
|
|
||||||
|
if 'allow_mentions' in payload:
|
||||||
|
del payload['allow_mentions']
|
||||||
|
|
||||||
|
#
|
||||||
# Send our attachments
|
# Send our attachments
|
||||||
|
#
|
||||||
for attachment in attach:
|
for attachment in attach:
|
||||||
self.logger.info(
|
self.logger.info(
|
||||||
'Posting Discord Attachment {}'.format(attachment.name))
|
'Posting Discord Attachment {}'.format(attachment.name))
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -41,12 +41,12 @@ from socket import error as SocketError
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from datetime import timezone
|
from datetime import timezone
|
||||||
|
|
||||||
from .NotifyBase import NotifyBase
|
from .base import NotifyBase
|
||||||
from ..URLBase import PrivacyMode
|
from ..url import PrivacyMode
|
||||||
from ..common import NotifyFormat, NotifyType
|
from ..common import NotifyFormat, NotifyType
|
||||||
from ..conversion import convert_between
|
from ..conversion import convert_between
|
||||||
from ..utils import is_email, parse_emails
|
from ..utils import is_ipaddr, is_email, parse_emails, is_hostname
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..locale import gettext_lazy as _
|
||||||
from ..logger import logger
|
from ..logger import logger
|
||||||
|
|
||||||
# Globally Default encoding mode set to Quoted Printable.
|
# Globally Default encoding mode set to Quoted Printable.
|
||||||
|
@ -295,6 +295,21 @@ EMAIL_TEMPLATES = (
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
|
# Comcast.net
|
||||||
|
(
|
||||||
|
'Comcast.net',
|
||||||
|
re.compile(
|
||||||
|
r'^((?P<label>[^+]+)\+)?(?P<id>[^@]+)@'
|
||||||
|
r'(?P<domain>(comcast)\.net)$', re.I),
|
||||||
|
{
|
||||||
|
'port': 465,
|
||||||
|
'smtp_host': 'smtp.comcast.net',
|
||||||
|
'secure': True,
|
||||||
|
'secure_mode': SecureMailMode.SSL,
|
||||||
|
'login_type': (WebBaseLogin.EMAIL, )
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
# Catch All
|
# Catch All
|
||||||
(
|
(
|
||||||
'Custom',
|
'Custom',
|
||||||
|
@ -481,34 +496,6 @@ class NotifyEmail(NotifyBase):
|
||||||
# addresses from the URL provided
|
# addresses from the URL provided
|
||||||
self.from_addr = [False, '']
|
self.from_addr = [False, '']
|
||||||
|
|
||||||
if self.user and self.host:
|
|
||||||
# Prepare the bases of our email
|
|
||||||
self.from_addr = [self.app_id, '{}@{}'.format(
|
|
||||||
re.split(r'[\s@]+', self.user)[0],
|
|
||||||
self.host,
|
|
||||||
)]
|
|
||||||
|
|
||||||
if from_addr:
|
|
||||||
result = is_email(from_addr)
|
|
||||||
if result:
|
|
||||||
self.from_addr = (
|
|
||||||
result['name'] if result['name'] else False,
|
|
||||||
result['full_email'])
|
|
||||||
else:
|
|
||||||
self.from_addr[0] = from_addr
|
|
||||||
|
|
||||||
result = is_email(self.from_addr[1])
|
|
||||||
if not result:
|
|
||||||
# Parse Source domain based on from_addr
|
|
||||||
msg = 'Invalid ~From~ email specified: {}'.format(
|
|
||||||
'{} <{}>'.format(self.from_addr[0], self.from_addr[1])
|
|
||||||
if self.from_addr[0] else '{}'.format(self.from_addr[1]))
|
|
||||||
self.logger.warning(msg)
|
|
||||||
raise TypeError(msg)
|
|
||||||
|
|
||||||
# Store our lookup
|
|
||||||
self.names[self.from_addr[1]] = self.from_addr[0]
|
|
||||||
|
|
||||||
# Now detect the SMTP Server
|
# Now detect the SMTP Server
|
||||||
self.smtp_host = \
|
self.smtp_host = \
|
||||||
smtp_host if isinstance(smtp_host, str) else ''
|
smtp_host if isinstance(smtp_host, str) else ''
|
||||||
|
@ -528,25 +515,6 @@ class NotifyEmail(NotifyBase):
|
||||||
self.logger.warning(msg)
|
self.logger.warning(msg)
|
||||||
raise TypeError(msg)
|
raise TypeError(msg)
|
||||||
|
|
||||||
if targets:
|
|
||||||
# Validate recipients (to:) and drop bad ones:
|
|
||||||
for recipient in parse_emails(targets):
|
|
||||||
result = is_email(recipient)
|
|
||||||
if result:
|
|
||||||
self.targets.append(
|
|
||||||
(result['name'] if result['name'] else False,
|
|
||||||
result['full_email']))
|
|
||||||
continue
|
|
||||||
|
|
||||||
self.logger.warning(
|
|
||||||
'Dropped invalid To email '
|
|
||||||
'({}) specified.'.format(recipient),
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
|
||||||
# If our target email list is empty we want to add ourselves to it
|
|
||||||
self.targets.append((False, self.from_addr[1]))
|
|
||||||
|
|
||||||
# Validate recipients (cc:) and drop bad ones:
|
# Validate recipients (cc:) and drop bad ones:
|
||||||
for recipient in parse_emails(cc):
|
for recipient in parse_emails(cc):
|
||||||
email = is_email(recipient)
|
email = is_email(recipient)
|
||||||
|
@ -598,6 +566,62 @@ class NotifyEmail(NotifyBase):
|
||||||
# Apply any defaults based on certain known configurations
|
# Apply any defaults based on certain known configurations
|
||||||
self.NotifyEmailDefaults(secure_mode=secure_mode, **kwargs)
|
self.NotifyEmailDefaults(secure_mode=secure_mode, **kwargs)
|
||||||
|
|
||||||
|
if self.user:
|
||||||
|
if self.host:
|
||||||
|
# Prepare the bases of our email
|
||||||
|
self.from_addr = [self.app_id, '{}@{}'.format(
|
||||||
|
re.split(r'[\s@]+', self.user)[0],
|
||||||
|
self.host,
|
||||||
|
)]
|
||||||
|
|
||||||
|
else:
|
||||||
|
result = is_email(self.user)
|
||||||
|
if result:
|
||||||
|
# Prepare the bases of our email and include domain
|
||||||
|
self.host = result['domain']
|
||||||
|
self.from_addr = [self.app_id, self.user]
|
||||||
|
|
||||||
|
if from_addr:
|
||||||
|
result = is_email(from_addr)
|
||||||
|
if result:
|
||||||
|
self.from_addr = (
|
||||||
|
result['name'] if result['name'] else False,
|
||||||
|
result['full_email'])
|
||||||
|
else:
|
||||||
|
# Only update the string but use the already detected info
|
||||||
|
self.from_addr[0] = from_addr
|
||||||
|
|
||||||
|
result = is_email(self.from_addr[1])
|
||||||
|
if not result:
|
||||||
|
# Parse Source domain based on from_addr
|
||||||
|
msg = 'Invalid ~From~ email specified: {}'.format(
|
||||||
|
'{} <{}>'.format(self.from_addr[0], self.from_addr[1])
|
||||||
|
if self.from_addr[0] else '{}'.format(self.from_addr[1]))
|
||||||
|
self.logger.warning(msg)
|
||||||
|
raise TypeError(msg)
|
||||||
|
|
||||||
|
# Store our lookup
|
||||||
|
self.names[self.from_addr[1]] = self.from_addr[0]
|
||||||
|
|
||||||
|
if targets:
|
||||||
|
# Validate recipients (to:) and drop bad ones:
|
||||||
|
for recipient in parse_emails(targets):
|
||||||
|
result = is_email(recipient)
|
||||||
|
if result:
|
||||||
|
self.targets.append(
|
||||||
|
(result['name'] if result['name'] else False,
|
||||||
|
result['full_email']))
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.logger.warning(
|
||||||
|
'Dropped invalid To email '
|
||||||
|
'({}) specified.'.format(recipient),
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# If our target email list is empty we want to add ourselves to it
|
||||||
|
self.targets.append((False, self.from_addr[1]))
|
||||||
|
|
||||||
if not self.secure and self.secure_mode != SecureMailMode.INSECURE:
|
if not self.secure and self.secure_mode != SecureMailMode.INSECURE:
|
||||||
# Enable Secure mode if not otherwise set
|
# Enable Secure mode if not otherwise set
|
||||||
self.secure = True
|
self.secure = True
|
||||||
|
@ -664,9 +688,7 @@ class NotifyEmail(NotifyBase):
|
||||||
# was specified, then we default to having them all set (which
|
# was specified, then we default to having them all set (which
|
||||||
# basically implies that there are no restrictions and use use
|
# basically implies that there are no restrictions and use use
|
||||||
# whatever was specified)
|
# whatever was specified)
|
||||||
login_type = EMAIL_TEMPLATES[i][2]\
|
login_type = EMAIL_TEMPLATES[i][2].get('login_type', [])
|
||||||
.get('login_type', [])
|
|
||||||
|
|
||||||
if login_type:
|
if login_type:
|
||||||
# only apply additional logic to our user if a login_type
|
# only apply additional logic to our user if a login_type
|
||||||
# was specified.
|
# was specified.
|
||||||
|
@ -676,6 +698,10 @@ class NotifyEmail(NotifyBase):
|
||||||
# not supported; switch it to user id
|
# not supported; switch it to user id
|
||||||
self.user = match.group('id')
|
self.user = match.group('id')
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Enforce our host information
|
||||||
|
self.host = self.user.split('@')[1]
|
||||||
|
|
||||||
elif WebBaseLogin.USERID not in login_type:
|
elif WebBaseLogin.USERID not in login_type:
|
||||||
# user specified but login type
|
# user specified but login type
|
||||||
# not supported; switch it to email
|
# not supported; switch it to email
|
||||||
|
@ -1019,11 +1045,29 @@ class NotifyEmail(NotifyBase):
|
||||||
us to re-instantiate this object.
|
us to re-instantiate this object.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
results = NotifyBase.parse_url(url)
|
results = NotifyBase.parse_url(url, verify_host=False)
|
||||||
if not results:
|
if not results:
|
||||||
# We're done early as we couldn't load the results
|
# We're done early as we couldn't load the results
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
# Prepare our target lists
|
||||||
|
results['targets'] = []
|
||||||
|
|
||||||
|
if is_ipaddr(results['host']):
|
||||||
|
# Silently move on and do not disrupt any configuration
|
||||||
|
pass
|
||||||
|
|
||||||
|
elif not is_hostname(results['host'], ipv4=False, ipv6=False,
|
||||||
|
underscore=False):
|
||||||
|
|
||||||
|
if is_email(NotifyEmail.unquote(results['host'])):
|
||||||
|
# Don't lose defined email addresses
|
||||||
|
results['targets'].append(NotifyEmail.unquote(results['host']))
|
||||||
|
|
||||||
|
# Detect if we have a valid hostname or not; be sure to reset it's
|
||||||
|
# value if invalid; we'll attempt to figure this out later on
|
||||||
|
results['host'] = ''
|
||||||
|
|
||||||
# The From address is a must; either through the use of templates
|
# The From address is a must; either through the use of templates
|
||||||
# from= entry and/or merging the user and hostname together, this
|
# from= entry and/or merging the user and hostname together, this
|
||||||
# must be calculated or parse_url will fail.
|
# must be calculated or parse_url will fail.
|
||||||
|
@ -1034,7 +1078,7 @@ class NotifyEmail(NotifyBase):
|
||||||
|
|
||||||
# Get our potential email targets; if none our found we'll just
|
# Get our potential email targets; if none our found we'll just
|
||||||
# add one to ourselves
|
# add one to ourselves
|
||||||
results['targets'] = NotifyEmail.split_path(results['fullpath'])
|
results['targets'] += NotifyEmail.split_path(results['fullpath'])
|
||||||
|
|
||||||
# Attempt to detect 'to' email address
|
# Attempt to detect 'to' email address
|
||||||
if 'to' in results['qsd'] and len(results['qsd']['to']):
|
if 'to' in results['qsd'] and len(results['qsd']['to']):
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -34,12 +34,12 @@ import hashlib
|
||||||
from json import dumps
|
from json import dumps
|
||||||
from json import loads
|
from json import loads
|
||||||
|
|
||||||
from .NotifyBase import NotifyBase
|
from .base import NotifyBase
|
||||||
from ..URLBase import PrivacyMode
|
from ..url import PrivacyMode
|
||||||
from ..utils import parse_bool
|
from ..utils import parse_bool
|
||||||
from ..common import NotifyType
|
from ..common import NotifyType
|
||||||
from .. import __version__ as VERSION
|
from .. import __version__ as VERSION
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..locale import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class NotifyEmby(NotifyBase):
|
class NotifyEmby(NotifyBase):
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -37,10 +37,10 @@
|
||||||
import requests
|
import requests
|
||||||
from json import loads
|
from json import loads
|
||||||
|
|
||||||
from .NotifyBase import NotifyBase
|
from .base import NotifyBase
|
||||||
from ..URLBase import PrivacyMode
|
from ..url import PrivacyMode
|
||||||
from ..common import NotifyType
|
from ..common import NotifyType
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..locale import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class Enigma2MessageType:
|
class Enigma2MessageType:
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -50,15 +50,15 @@
|
||||||
# You will need this in order to send an apprise messag
|
# You will need this in order to send an apprise messag
|
||||||
import requests
|
import requests
|
||||||
from json import dumps
|
from json import dumps
|
||||||
from ..NotifyBase import NotifyBase
|
from ..base import NotifyBase
|
||||||
from ...common import NotifyType
|
from ...common import NotifyType
|
||||||
from ...utils import validate_regex
|
from ...utils import validate_regex
|
||||||
from ...utils import parse_list
|
from ...utils import parse_list
|
||||||
from ...utils import parse_bool
|
from ...utils import parse_bool
|
||||||
from ...utils import dict_full_update
|
from ...utils import dict_full_update
|
||||||
from ...common import NotifyImageSize
|
from ...common import NotifyImageSize
|
||||||
from ...AppriseAttachment import AppriseAttachment
|
from ...apprise_attachment import AppriseAttachment
|
||||||
from ...AppriseLocale import gettext_lazy as _
|
from ...locale import gettext_lazy as _
|
||||||
from .common import (FCMMode, FCM_MODES)
|
from .common import (FCMMode, FCM_MODES)
|
||||||
from .priority import (FCM_PRIORITIES, FCMPriorityManager)
|
from .priority import (FCM_PRIORITIES, FCMPriorityManager)
|
||||||
from .color import FCMColorManager
|
from .color import FCMColorManager
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -36,7 +36,7 @@
|
||||||
import re
|
import re
|
||||||
from ...utils import parse_bool
|
from ...utils import parse_bool
|
||||||
from ...common import NotifyType
|
from ...common import NotifyType
|
||||||
from ...AppriseAsset import AppriseAsset
|
from ...asset import AppriseAsset
|
||||||
|
|
||||||
|
|
||||||
class FCMColorManager:
|
class FCMColorManager:
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
231
lib/apprise/plugins/feishu.py
Normal file
231
lib/apprise/plugins/feishu.py
Normal file
|
@ -0,0 +1,231 @@
|
||||||
|
# -*- 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.
|
||||||
|
|
||||||
|
# Feishu
|
||||||
|
# 1. Visit https://open.feishu.cn
|
||||||
|
|
||||||
|
# Custom Bot Setup
|
||||||
|
# https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot
|
||||||
|
#
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from json import dumps
|
||||||
|
|
||||||
|
from .base import NotifyBase
|
||||||
|
from ..common import NotifyType
|
||||||
|
from ..utils import validate_regex
|
||||||
|
from ..locale import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class NotifyFeishu(NotifyBase):
|
||||||
|
"""
|
||||||
|
A wrapper for Feishu Notifications
|
||||||
|
"""
|
||||||
|
|
||||||
|
# The default descriptive name associated with the Notification
|
||||||
|
service_name = _('Feishu')
|
||||||
|
|
||||||
|
# The services URL
|
||||||
|
service_url = 'https://open.feishu.cn/'
|
||||||
|
|
||||||
|
# The default secure protocol
|
||||||
|
secure_protocol = 'feishu'
|
||||||
|
|
||||||
|
# A URL that takes you to the setup/help of the specific protocol
|
||||||
|
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_feishu'
|
||||||
|
|
||||||
|
# Notification URL
|
||||||
|
notify_url = 'https://open.feishu.cn/open-apis/bot/v2/hook/{token}/'
|
||||||
|
|
||||||
|
# Define object templates
|
||||||
|
templates = (
|
||||||
|
'{schema}://{token}',
|
||||||
|
)
|
||||||
|
|
||||||
|
# The title is not used
|
||||||
|
title_maxlen = 0
|
||||||
|
|
||||||
|
# Limit is documented to be 20K message sizes. This number safely
|
||||||
|
# allows padding around that size.
|
||||||
|
body_maxlen = 19985
|
||||||
|
|
||||||
|
# Define our tokens; these are the minimum tokens required required to
|
||||||
|
# be passed into this function (as arguments). The syntax appends any
|
||||||
|
# previously defined in the base package and builds onto them
|
||||||
|
template_tokens = dict(NotifyBase.template_tokens, **{
|
||||||
|
'token': {
|
||||||
|
'name': _('Token'),
|
||||||
|
'type': 'string',
|
||||||
|
'private': True,
|
||||||
|
'required': True,
|
||||||
|
'regex': (r'^[A-Z0-9_-]+$', 'i'),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
# Define our template arguments
|
||||||
|
template_args = dict(NotifyBase.template_args, **{
|
||||||
|
'token': {
|
||||||
|
'alias_of': 'token',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
def __init__(self, token, **kwargs):
|
||||||
|
"""
|
||||||
|
Initialize Feishu Object
|
||||||
|
"""
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
self.token = validate_regex(
|
||||||
|
token, *self.template_tokens['token']['regex'])
|
||||||
|
if not self.token:
|
||||||
|
msg = 'The Feishu token specified ({}) is invalid.'\
|
||||||
|
.format(token)
|
||||||
|
self.logger.warning(msg)
|
||||||
|
raise TypeError(msg)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||||
|
"""
|
||||||
|
Send our notification
|
||||||
|
"""
|
||||||
|
|
||||||
|
# prepare our headers
|
||||||
|
headers = {
|
||||||
|
'User-Agent': self.app_id,
|
||||||
|
'Content-Type': "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Our Message
|
||||||
|
payload = {
|
||||||
|
'msg_type': 'text',
|
||||||
|
"content": {
|
||||||
|
"text": body,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.logger.debug('Feishu GET URL: %s (cert_verify=%r)' % (
|
||||||
|
self.notify_url, self.verify_certificate))
|
||||||
|
self.logger.debug('Feishu Payload: %s' % str(payload))
|
||||||
|
|
||||||
|
# Always call throttle before any remote server i/o is made
|
||||||
|
self.throttle()
|
||||||
|
|
||||||
|
try:
|
||||||
|
r = requests.post(
|
||||||
|
self.notify_url.format(token=self.token),
|
||||||
|
data=dumps(payload).encode('utf-8'),
|
||||||
|
headers=headers,
|
||||||
|
verify=self.verify_certificate,
|
||||||
|
timeout=self.request_timeout,
|
||||||
|
)
|
||||||
|
|
||||||
|
#
|
||||||
|
# Sample Responses
|
||||||
|
#
|
||||||
|
# Valid:
|
||||||
|
# {
|
||||||
|
# "code": 0,
|
||||||
|
# "data": {},
|
||||||
|
# "msg": "success"
|
||||||
|
# }
|
||||||
|
|
||||||
|
# Invalid (non 200 response):
|
||||||
|
# {
|
||||||
|
# "code": 9499,
|
||||||
|
# "msg": "Bad Request",
|
||||||
|
# "data": {}
|
||||||
|
# }
|
||||||
|
if r.status_code != requests.codes.ok:
|
||||||
|
# We had a problem
|
||||||
|
status_str = \
|
||||||
|
NotifyFeishu.http_response_code_lookup(r.status_code)
|
||||||
|
|
||||||
|
self.logger.warning(
|
||||||
|
'Failed to send Feishu notification: '
|
||||||
|
'{}{}error={}.'.format(
|
||||||
|
status_str,
|
||||||
|
', ' if status_str else '',
|
||||||
|
r.status_code))
|
||||||
|
|
||||||
|
self.logger.debug('Response Details:\r\n{}'.format(r.content))
|
||||||
|
|
||||||
|
# Return; we're done
|
||||||
|
return False
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.logger.info('Sent Feishu notification.')
|
||||||
|
|
||||||
|
except requests.RequestException as e:
|
||||||
|
self.logger.warning(
|
||||||
|
'A Connection error occurred sending Feishu '
|
||||||
|
'notification.')
|
||||||
|
self.logger.debug('Socket Exception: %s' % str(e))
|
||||||
|
|
||||||
|
# Return; we're done
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def url(self, privacy=False, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Returns the URL built dynamically based on specified arguments.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Prepare our parameters
|
||||||
|
params = self.url_parameters(privacy=privacy, *args, **kwargs)
|
||||||
|
|
||||||
|
return '{schema}://{token}/?{params}'.format(
|
||||||
|
schema=self.secure_protocol,
|
||||||
|
token=self.pprint(self.token, privacy, safe=''),
|
||||||
|
params=NotifyFeishu.urlencode(params),
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_url(url):
|
||||||
|
"""
|
||||||
|
Parses the URL and returns enough arguments that can allow
|
||||||
|
us to re-instantiate this object.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# parse_url already handles getting the `user` and `password` fields
|
||||||
|
# populated.
|
||||||
|
results = NotifyBase.parse_url(url, verify_host=False)
|
||||||
|
if not results:
|
||||||
|
# We're done early as we couldn't load the results
|
||||||
|
return results
|
||||||
|
|
||||||
|
# Allow over-ride
|
||||||
|
if 'token' in results['qsd'] and len(results['qsd']['token']):
|
||||||
|
results['token'] = NotifyFeishu.unquote(results['qsd']['token'])
|
||||||
|
|
||||||
|
else:
|
||||||
|
results['token'] = NotifyFeishu.unquote(results['host'])
|
||||||
|
|
||||||
|
return results
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -44,14 +44,14 @@ import re
|
||||||
import requests
|
import requests
|
||||||
from json import dumps
|
from json import dumps
|
||||||
|
|
||||||
from .NotifyBase import NotifyBase
|
from .base import NotifyBase
|
||||||
from ..common import NotifyType
|
from ..common import NotifyType
|
||||||
from ..common import NotifyFormat
|
from ..common import NotifyFormat
|
||||||
from ..common import NotifyImageSize
|
from ..common import NotifyImageSize
|
||||||
from ..utils import parse_list
|
from ..utils import parse_list
|
||||||
from ..utils import parse_bool
|
from ..utils import parse_bool
|
||||||
from ..utils import validate_regex
|
from ..utils import validate_regex
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..locale import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
# Extend HTTP Error Messages
|
# Extend HTTP Error Messages
|
205
lib/apprise/plugins/freemobile.py
Normal file
205
lib/apprise/plugins/freemobile.py
Normal file
|
@ -0,0 +1,205 @@
|
||||||
|
# -*- 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.
|
||||||
|
|
||||||
|
# Free Mobile
|
||||||
|
# 1. Visit https://mobile.free.fr/
|
||||||
|
|
||||||
|
# the URL will look something like this:
|
||||||
|
# https://smsapi.free-mobile.fr/sendmsg
|
||||||
|
#
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from json import dumps
|
||||||
|
|
||||||
|
from .base import NotifyBase
|
||||||
|
from ..common import NotifyType
|
||||||
|
from ..locale import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class NotifyFreeMobile(NotifyBase):
|
||||||
|
"""
|
||||||
|
A wrapper for Free-Mobile Notifications
|
||||||
|
"""
|
||||||
|
|
||||||
|
# The default descriptive name associated with the Notification
|
||||||
|
service_name = _('Free-Mobile')
|
||||||
|
|
||||||
|
# The services URL
|
||||||
|
service_url = 'https://mobile.free.fr/'
|
||||||
|
|
||||||
|
# The default secure protocol
|
||||||
|
secure_protocol = 'freemobile'
|
||||||
|
|
||||||
|
# A URL that takes you to the setup/help of the specific protocol
|
||||||
|
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_freemobile'
|
||||||
|
|
||||||
|
# Plain Text Notification URL
|
||||||
|
notify_url = 'https://smsapi.free-mobile.fr/sendmsg'
|
||||||
|
|
||||||
|
# Define object templates
|
||||||
|
templates = (
|
||||||
|
'{schema}://{user}@{password}',
|
||||||
|
)
|
||||||
|
|
||||||
|
# The title is not used
|
||||||
|
title_maxlen = 0
|
||||||
|
|
||||||
|
# SMS Messages are restricted in size
|
||||||
|
body_maxlen = 160
|
||||||
|
|
||||||
|
# Define our tokens; these are the minimum tokens required required to
|
||||||
|
# be passed into this function (as arguments). The syntax appends any
|
||||||
|
# previously defined in the base package and builds onto them
|
||||||
|
template_tokens = dict(NotifyBase.template_tokens, **{
|
||||||
|
'user': {
|
||||||
|
'name': _('Username'),
|
||||||
|
'type': 'string',
|
||||||
|
'required': True,
|
||||||
|
},
|
||||||
|
'password': {
|
||||||
|
'name': _('Password'),
|
||||||
|
'type': 'string',
|
||||||
|
'private': True,
|
||||||
|
'required': True,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Initialize Free Mobile Object
|
||||||
|
"""
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
if not (self.user and self.password):
|
||||||
|
msg = 'A FreeMobile user and password ' \
|
||||||
|
'combination was not provided.'
|
||||||
|
self.logger.warning(msg)
|
||||||
|
raise TypeError(msg)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
def url(self, privacy=False, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Returns the URL built dynamically based on specified arguments.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Prepare our parameters
|
||||||
|
params = self.url_parameters(privacy=privacy, *args, **kwargs)
|
||||||
|
|
||||||
|
return '{schema}://{user}@{password}/?{params}'.format(
|
||||||
|
schema=self.secure_protocol,
|
||||||
|
user=self.user,
|
||||||
|
password=self.pprint(self.password, privacy, safe=''),
|
||||||
|
params=NotifyFreeMobile.urlencode(params),
|
||||||
|
)
|
||||||
|
|
||||||
|
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||||
|
"""
|
||||||
|
Send our notification
|
||||||
|
"""
|
||||||
|
|
||||||
|
# prepare our headers
|
||||||
|
headers = {
|
||||||
|
'User-Agent': self.app_id,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Prepare our payload
|
||||||
|
payload = {
|
||||||
|
'user': self.user,
|
||||||
|
'pass': self.password,
|
||||||
|
'msg': body
|
||||||
|
}
|
||||||
|
|
||||||
|
self.logger.debug('Free Mobile GET URL: %s (cert_verify=%r)' % (
|
||||||
|
self.notify_url, self.verify_certificate))
|
||||||
|
self.logger.debug('Free Mobile Payload: %s' % str(payload))
|
||||||
|
|
||||||
|
# Always call throttle before any remote server i/o is made
|
||||||
|
self.throttle()
|
||||||
|
|
||||||
|
try:
|
||||||
|
r = requests.post(
|
||||||
|
self.notify_url,
|
||||||
|
data=dumps(payload).encode('utf-8'),
|
||||||
|
headers=headers,
|
||||||
|
verify=self.verify_certificate,
|
||||||
|
timeout=self.request_timeout,
|
||||||
|
)
|
||||||
|
if r.status_code != requests.codes.ok:
|
||||||
|
# We had a problem
|
||||||
|
status_str = \
|
||||||
|
NotifyFreeMobile.http_response_code_lookup(r.status_code)
|
||||||
|
|
||||||
|
self.logger.warning(
|
||||||
|
'Failed to send Free Mobile notification: '
|
||||||
|
'{}{}error={}.'.format(
|
||||||
|
status_str,
|
||||||
|
', ' if status_str else '',
|
||||||
|
r.status_code))
|
||||||
|
|
||||||
|
self.logger.debug('Response Details:\r\n{}'.format(r.content))
|
||||||
|
|
||||||
|
# Return; we're done
|
||||||
|
return False
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.logger.info('Sent Free Mobile notification.')
|
||||||
|
|
||||||
|
except requests.RequestException as e:
|
||||||
|
self.logger.warning(
|
||||||
|
'A Connection error occurred sending Free Mobile '
|
||||||
|
'notification.')
|
||||||
|
self.logger.debug('Socket Exception: %s' % str(e))
|
||||||
|
|
||||||
|
# Return; we're done
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_url(url):
|
||||||
|
"""
|
||||||
|
Parses the URL and returns enough arguments that can allow
|
||||||
|
us to re-instantiate this object.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# parse_url already handles getting the `user` and `password` fields
|
||||||
|
# populated.
|
||||||
|
results = NotifyBase.parse_url(url, verify_host=False)
|
||||||
|
if not results:
|
||||||
|
# We're done early as we couldn't load the results
|
||||||
|
return results
|
||||||
|
|
||||||
|
# The hostname can act as the password if specified and a password
|
||||||
|
# was otherwise not (specified):
|
||||||
|
if not results.get('password'):
|
||||||
|
results['password'] = NotifyFreeMobile.unquote(results['host'])
|
||||||
|
|
||||||
|
return results
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -26,14 +26,11 @@
|
||||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
# POSSIBILITY OF SUCH DAMAGE.
|
# POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
from __future__ import absolute_import
|
from .base import NotifyBase
|
||||||
from __future__ import print_function
|
|
||||||
|
|
||||||
from .NotifyBase import NotifyBase
|
|
||||||
from ..common import NotifyImageSize
|
from ..common import NotifyImageSize
|
||||||
from ..common import NotifyType
|
from ..common import NotifyType
|
||||||
from ..utils import parse_bool
|
from ..utils import parse_bool
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..locale import gettext_lazy as _
|
||||||
|
|
||||||
# Default our global support flag
|
# Default our global support flag
|
||||||
NOTIFY_GNOME_SUPPORT_ENABLED = False
|
NOTIFY_GNOME_SUPPORT_ENABLED = False
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -58,11 +58,11 @@ import re
|
||||||
import requests
|
import requests
|
||||||
from json import dumps
|
from json import dumps
|
||||||
|
|
||||||
from .NotifyBase import NotifyBase
|
from .base import NotifyBase
|
||||||
from ..common import NotifyFormat
|
from ..common import NotifyFormat
|
||||||
from ..common import NotifyType
|
from ..common import NotifyType
|
||||||
from ..utils import validate_regex
|
from ..utils import validate_regex
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..locale import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class NotifyGoogleChat(NotifyBase):
|
class NotifyGoogleChat(NotifyBase):
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -34,10 +34,10 @@
|
||||||
import requests
|
import requests
|
||||||
from json import dumps
|
from json import dumps
|
||||||
|
|
||||||
from .NotifyBase import NotifyBase
|
from .base import NotifyBase
|
||||||
from ..common import NotifyType, NotifyFormat
|
from ..common import NotifyType, NotifyFormat
|
||||||
from ..utils import validate_regex
|
from ..utils import validate_regex
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..locale import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
# Priorities
|
# Priorities
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -26,12 +26,12 @@
|
||||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
# POSSIBILITY OF SUCH DAMAGE.
|
# POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
from .NotifyBase import NotifyBase
|
from .base import NotifyBase
|
||||||
from ..URLBase import PrivacyMode
|
from ..url import PrivacyMode
|
||||||
from ..common import NotifyImageSize
|
from ..common import NotifyImageSize
|
||||||
from ..common import NotifyType
|
from ..common import NotifyType
|
||||||
from ..utils import parse_bool
|
from ..utils import parse_bool
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..locale import gettext_lazy as _
|
||||||
|
|
||||||
# Default our global support flag
|
# Default our global support flag
|
||||||
NOTIFY_GROWL_SUPPORT_ENABLED = False
|
NOTIFY_GROWL_SUPPORT_ENABLED = False
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -45,10 +45,11 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from .NotifyDiscord import NotifyDiscord
|
# Import namespace so the class won't conflict with the actual Notify object
|
||||||
|
from . import discord
|
||||||
|
|
||||||
|
|
||||||
class NotifyGuilded(NotifyDiscord):
|
class NotifyGuilded(discord.NotifyDiscord):
|
||||||
"""
|
"""
|
||||||
A wrapper to Guilded Notifications
|
A wrapper to Guilded Notifications
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -34,11 +34,11 @@ from json import dumps
|
||||||
|
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from .NotifyBase import NotifyBase
|
from .base import NotifyBase
|
||||||
from ..URLBase import PrivacyMode
|
from ..url import PrivacyMode
|
||||||
from ..common import NotifyType
|
from ..common import NotifyType
|
||||||
from ..utils import validate_regex
|
from ..utils import validate_regex
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..locale import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class NotifyHomeAssistant(NotifyBase):
|
class NotifyHomeAssistant(NotifyBase):
|
330
lib/apprise/plugins/httpsms.py
Normal file
330
lib/apprise/plugins/httpsms.py
Normal file
|
@ -0,0 +1,330 @@
|
||||||
|
# -*- 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.
|
||||||
|
|
||||||
|
# To use this service you will need a httpSMS account
|
||||||
|
# You will need credits (new accounts start with a few)
|
||||||
|
# https://httpsms.com
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
from .base import NotifyBase
|
||||||
|
from ..common import NotifyType
|
||||||
|
from ..utils import is_phone_no
|
||||||
|
from ..utils import parse_phone_no
|
||||||
|
from ..utils import validate_regex
|
||||||
|
from ..locale import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class NotifyHttpSMS(NotifyBase):
|
||||||
|
"""
|
||||||
|
A wrapper for HttpSMS Notifications
|
||||||
|
"""
|
||||||
|
|
||||||
|
# The default descriptive name associated with the Notification
|
||||||
|
service_name = 'httpSMS'
|
||||||
|
|
||||||
|
# The services URL
|
||||||
|
service_url = 'https://httpsms.com'
|
||||||
|
|
||||||
|
# All notification requests are secure
|
||||||
|
secure_protocol = 'httpsms'
|
||||||
|
|
||||||
|
# A URL that takes you to the setup/help of the specific protocol
|
||||||
|
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_httpsms'
|
||||||
|
|
||||||
|
# HttpSMS uses the http protocol with JSON requests
|
||||||
|
notify_url = 'https://api.httpsms.com/v1/messages/send'
|
||||||
|
|
||||||
|
# The maximum length of the body
|
||||||
|
body_maxlen = 160
|
||||||
|
|
||||||
|
# A title can not be used for SMS Messages. Setting this to zero will
|
||||||
|
# cause any title (if defined) to get placed into the message body.
|
||||||
|
title_maxlen = 0
|
||||||
|
|
||||||
|
# Define object templates
|
||||||
|
templates = (
|
||||||
|
'{schema}://{apikey}@{from_phone}',
|
||||||
|
'{schema}://{apikey}@{from_phone}/{targets}',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Define our template tokens
|
||||||
|
template_tokens = dict(NotifyBase.template_tokens, **{
|
||||||
|
'apikey': {
|
||||||
|
'name': _('API Key'),
|
||||||
|
'type': 'string',
|
||||||
|
'private': True,
|
||||||
|
'required': True,
|
||||||
|
},
|
||||||
|
'from_phone': {
|
||||||
|
'name': _('From Phone No'),
|
||||||
|
'type': 'string',
|
||||||
|
'regex': (r'^\+?[0-9\s)(+-]+$', 'i'),
|
||||||
|
'map_to': 'source',
|
||||||
|
'required': True,
|
||||||
|
},
|
||||||
|
'target_phone': {
|
||||||
|
'name': _('Target Phone No'),
|
||||||
|
'type': 'string',
|
||||||
|
'prefix': '+',
|
||||||
|
'regex': (r'^[0-9\s)(+-]+$', 'i'),
|
||||||
|
'map_to': 'targets',
|
||||||
|
},
|
||||||
|
'targets': {
|
||||||
|
'name': _('Targets'),
|
||||||
|
'type': 'list:string',
|
||||||
|
'required': True,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
# Define our template arguments
|
||||||
|
template_args = dict(NotifyBase.template_args, **{
|
||||||
|
'key': {
|
||||||
|
'alias_of': 'apikey',
|
||||||
|
},
|
||||||
|
'to': {
|
||||||
|
'alias_of': 'targets',
|
||||||
|
},
|
||||||
|
'from': {
|
||||||
|
'name': _('From Phone No'),
|
||||||
|
'type': 'string',
|
||||||
|
'regex': (r'^\+?[0-9\s)(+-]+$', 'i'),
|
||||||
|
'map_to': 'source',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
def __init__(self, apikey=None, source=None, targets=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Initialize HttpSMS Object
|
||||||
|
"""
|
||||||
|
super(NotifyHttpSMS, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
self.apikey = validate_regex(apikey)
|
||||||
|
if not self.apikey:
|
||||||
|
msg = 'An invalid API Key ({}) was specified.'.format(apikey)
|
||||||
|
self.logger.warning(msg)
|
||||||
|
raise TypeError(msg)
|
||||||
|
|
||||||
|
result = is_phone_no(source)
|
||||||
|
if not result:
|
||||||
|
msg = 'The Account (From) Phone # specified ' \
|
||||||
|
'({}) is invalid.'.format(source)
|
||||||
|
self.logger.warning(msg)
|
||||||
|
raise TypeError(msg)
|
||||||
|
|
||||||
|
# Tidy source
|
||||||
|
self.source = result['full']
|
||||||
|
|
||||||
|
# Parse our targets
|
||||||
|
self.targets = list()
|
||||||
|
|
||||||
|
has_error = False
|
||||||
|
for target in parse_phone_no(targets):
|
||||||
|
# Parse each phone number we found
|
||||||
|
result = is_phone_no(target)
|
||||||
|
if result:
|
||||||
|
self.targets.append(result['full'])
|
||||||
|
continue
|
||||||
|
|
||||||
|
has_error = True
|
||||||
|
self.logger.warning(
|
||||||
|
'Dropped invalid phone # ({}) specified.'.format(target),
|
||||||
|
)
|
||||||
|
|
||||||
|
if not targets and not has_error:
|
||||||
|
# Default the SMS Message to ourselves
|
||||||
|
self.targets.append(self.source)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||||
|
"""
|
||||||
|
Perform HttpSMS Notification
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not self.targets:
|
||||||
|
# We have nothing to notify
|
||||||
|
self.logger.warning('There are no HttpSMS targets to notify')
|
||||||
|
return False
|
||||||
|
|
||||||
|
# error tracking (used for function return)
|
||||||
|
has_error = False
|
||||||
|
|
||||||
|
# Prepare our headers
|
||||||
|
headers = {
|
||||||
|
'User-Agent': self.app_id,
|
||||||
|
'x-api-key': self.apikey,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Prepare our payload
|
||||||
|
payload = {
|
||||||
|
# The To gets populated in the loop below
|
||||||
|
'from': '+' + self.source,
|
||||||
|
'to': None,
|
||||||
|
'content': body,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Prepare our targets
|
||||||
|
targets = list(self.targets)
|
||||||
|
while len(targets):
|
||||||
|
# Get our target to notify
|
||||||
|
target = targets.pop(0)
|
||||||
|
|
||||||
|
# Prepare our user
|
||||||
|
payload['to'] = '+' + target
|
||||||
|
|
||||||
|
# Some Debug Logging
|
||||||
|
self.logger.debug('HttpSMS POST URL: {} (cert_verify={})'.format(
|
||||||
|
self.notify_url, self.verify_certificate))
|
||||||
|
self.logger.debug('HttpSMS Payload: {}' .format(payload))
|
||||||
|
|
||||||
|
# Always call throttle before any remote server i/o is made
|
||||||
|
self.throttle()
|
||||||
|
try:
|
||||||
|
r = requests.post(
|
||||||
|
self.notify_url,
|
||||||
|
data=json.dumps(payload),
|
||||||
|
headers=headers,
|
||||||
|
verify=self.verify_certificate,
|
||||||
|
timeout=self.request_timeout,
|
||||||
|
)
|
||||||
|
|
||||||
|
if r.status_code != requests.codes.ok:
|
||||||
|
# We had a problem
|
||||||
|
status_str = \
|
||||||
|
NotifyBase.http_response_code_lookup(r.status_code)
|
||||||
|
|
||||||
|
# set up our status code to use
|
||||||
|
status_code = r.status_code
|
||||||
|
|
||||||
|
self.logger.warning(
|
||||||
|
'Failed to send HttpSMS notification to {}: '
|
||||||
|
'{}{}error={}.'.format(
|
||||||
|
target,
|
||||||
|
status_str,
|
||||||
|
', ' if status_str else '',
|
||||||
|
status_code))
|
||||||
|
|
||||||
|
self.logger.debug(
|
||||||
|
'Response Details:\r\n{}'.format(r.content))
|
||||||
|
|
||||||
|
# Mark our failure
|
||||||
|
has_error = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.logger.info(
|
||||||
|
'Sent HttpSMS notification to {}.'.format(target))
|
||||||
|
|
||||||
|
except requests.RequestException as e:
|
||||||
|
self.logger.warning(
|
||||||
|
'A Connection error occurred sending HttpSMS: to %s ',
|
||||||
|
target)
|
||||||
|
self.logger.debug('Socket Exception: %s' % str(e))
|
||||||
|
|
||||||
|
# Mark our failure
|
||||||
|
has_error = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
return not has_error
|
||||||
|
|
||||||
|
def url(self, privacy=False, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Returns the URL built dynamically based on specified arguments.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Prepare our parameters
|
||||||
|
params = self.url_parameters(privacy=privacy, *args, **kwargs)
|
||||||
|
|
||||||
|
# A nice way of cleaning up the URL length a bit
|
||||||
|
targets = [] if len(self.targets) == 1 \
|
||||||
|
and self.targets[0] == self.source else self.targets
|
||||||
|
|
||||||
|
return '{schema}://{apikey}@{source}/{targets}' \
|
||||||
|
'?{params}'.format(
|
||||||
|
schema=self.secure_protocol,
|
||||||
|
source=self.source,
|
||||||
|
apikey=self.pprint(self.apikey, privacy, safe=''),
|
||||||
|
targets='/'.join([
|
||||||
|
NotifyHttpSMS.quote('{}'.format(x), safe='+')
|
||||||
|
for x in targets]),
|
||||||
|
params=NotifyHttpSMS.urlencode(params))
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
"""
|
||||||
|
Returns the number of targets associated with this notification
|
||||||
|
"""
|
||||||
|
|
||||||
|
return len(self.targets) if self.targets else 1
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_url(url):
|
||||||
|
"""
|
||||||
|
Parses the URL and returns enough arguments that can allow
|
||||||
|
us to re-instantiate this object.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
results = NotifyBase.parse_url(url, verify_host=False)
|
||||||
|
if not results:
|
||||||
|
# We're done early as we couldn't load the results
|
||||||
|
return results
|
||||||
|
|
||||||
|
# Get our API Key
|
||||||
|
results['apikey'] = NotifyHttpSMS.unquote(results['user'])
|
||||||
|
|
||||||
|
# Support the 'from' and 'source' variable so that we can support
|
||||||
|
# targets this way too.
|
||||||
|
# The 'from' makes it easier to use yaml configuration
|
||||||
|
if 'from' in results['qsd'] and len(results['qsd']['from']):
|
||||||
|
results['source'] = \
|
||||||
|
NotifyHttpSMS.unquote(results['qsd']['from'])
|
||||||
|
|
||||||
|
# hostname will also be a target in this case
|
||||||
|
results['targets'] = [
|
||||||
|
*NotifyHttpSMS.parse_phone_no(results['host']),
|
||||||
|
*NotifyHttpSMS.split_path(results['fullpath'])]
|
||||||
|
|
||||||
|
else:
|
||||||
|
# store our source
|
||||||
|
results['source'] = NotifyHttpSMS.unquote(results['host'])
|
||||||
|
|
||||||
|
# store targets
|
||||||
|
results['targets'] = NotifyHttpSMS.split_path(results['fullpath'])
|
||||||
|
|
||||||
|
# Support the 'to' variable so that we can support targets this way too
|
||||||
|
# The 'to' makes it easier to use yaml configuration
|
||||||
|
if 'to' in results['qsd'] and len(results['qsd']['to']):
|
||||||
|
results['targets'] += \
|
||||||
|
NotifyHttpSMS.parse_phone_no(results['qsd']['to'])
|
||||||
|
|
||||||
|
if 'key' in results['qsd'] and len(results['qsd']['key']):
|
||||||
|
results['apikey'] = \
|
||||||
|
NotifyHttpSMS.unquote(results['qsd']['key'])
|
||||||
|
|
||||||
|
return results
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -44,11 +44,11 @@ import re
|
||||||
import requests
|
import requests
|
||||||
from json import dumps
|
from json import dumps
|
||||||
|
|
||||||
from .NotifyBase import NotifyBase
|
from .base import NotifyBase
|
||||||
from ..common import NotifyType
|
from ..common import NotifyType
|
||||||
from ..utils import parse_list
|
from ..utils import parse_list
|
||||||
from ..utils import validate_regex
|
from ..utils import validate_regex
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..locale import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class NotifyIFTTT(NotifyBase):
|
class NotifyIFTTT(NotifyBase):
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -39,13 +39,13 @@
|
||||||
import re
|
import re
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from .NotifyBase import NotifyBase
|
from .base import NotifyBase
|
||||||
from ..common import NotifyImageSize
|
from ..common import NotifyImageSize
|
||||||
from ..common import NotifyType
|
from ..common import NotifyType
|
||||||
from ..utils import parse_list
|
from ..utils import parse_list
|
||||||
from ..utils import parse_bool
|
from ..utils import parse_bool
|
||||||
from ..utils import validate_regex
|
from ..utils import validate_regex
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..locale import gettext_lazy as _
|
||||||
|
|
||||||
# Extend HTTP Error Messages
|
# Extend HTTP Error Messages
|
||||||
JOIN_HTTP_ERROR_MAP = {
|
JOIN_HTTP_ERROR_MAP = {
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -38,12 +38,12 @@
|
||||||
import requests
|
import requests
|
||||||
from json import loads
|
from json import loads
|
||||||
|
|
||||||
from .NotifyBase import NotifyBase
|
from .base import NotifyBase
|
||||||
from ..common import NotifyType
|
from ..common import NotifyType
|
||||||
from ..utils import is_phone_no
|
from ..utils import is_phone_no
|
||||||
from ..utils import parse_phone_no
|
from ..utils import parse_phone_no
|
||||||
from ..utils import validate_regex
|
from ..utils import validate_regex
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..locale import gettext_lazy as _
|
||||||
|
|
||||||
# Extend HTTP Error Messages
|
# Extend HTTP Error Messages
|
||||||
# Based on https://kavenegar.com/rest.html
|
# Based on https://kavenegar.com/rest.html
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -39,10 +39,10 @@
|
||||||
import requests
|
import requests
|
||||||
from json import dumps
|
from json import dumps
|
||||||
|
|
||||||
from .NotifyBase import NotifyBase
|
from .base import NotifyBase
|
||||||
from ..common import NotifyType
|
from ..common import NotifyType
|
||||||
from ..utils import validate_regex
|
from ..utils import validate_regex
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..locale import gettext_lazy as _
|
||||||
|
|
||||||
# Extend HTTP Error Messages
|
# Extend HTTP Error Messages
|
||||||
KUMULOS_HTTP_ERROR_MAP = {
|
KUMULOS_HTTP_ERROR_MAP = {
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -90,10 +90,10 @@
|
||||||
import re
|
import re
|
||||||
import requests
|
import requests
|
||||||
from json import dumps
|
from json import dumps
|
||||||
from .NotifyBase import NotifyBase
|
from .base import NotifyBase
|
||||||
from ..common import NotifyType
|
from ..common import NotifyType
|
||||||
from ..utils import validate_regex
|
from ..utils import validate_regex
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..locale import gettext_lazy as _
|
||||||
from ..utils import is_hostname
|
from ..utils import is_hostname
|
||||||
from ..utils import is_ipaddr
|
from ..utils import is_ipaddr
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -33,14 +33,14 @@ import requests
|
||||||
import re
|
import re
|
||||||
from json import dumps
|
from json import dumps
|
||||||
|
|
||||||
from .NotifyBase import NotifyBase
|
from .base import NotifyBase
|
||||||
from ..URLBase import PrivacyMode
|
from ..url import PrivacyMode
|
||||||
from ..common import NotifyType
|
from ..common import NotifyType
|
||||||
from ..common import NotifyImageSize
|
from ..common import NotifyImageSize
|
||||||
from ..utils import validate_regex
|
from ..utils import validate_regex
|
||||||
from ..utils import parse_list
|
from ..utils import parse_list
|
||||||
from ..utils import parse_bool
|
from ..utils import parse_bool
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..locale import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
# Used to break path apart into list of streams
|
# Used to break path apart into list of streams
|
440
lib/apprise/plugins/lunasea.py
Normal file
440
lib/apprise/plugins/lunasea.py
Normal file
|
@ -0,0 +1,440 @@
|
||||||
|
# -*- 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.
|
||||||
|
#
|
||||||
|
# API:
|
||||||
|
# https://docs.lunasea.app/lunasea/notifications/custom-notifications
|
||||||
|
#
|
||||||
|
import re
|
||||||
|
import requests
|
||||||
|
from json import dumps
|
||||||
|
|
||||||
|
from .base import NotifyBase
|
||||||
|
from ..common import NotifyType
|
||||||
|
from ..common import NotifyImageSize
|
||||||
|
from ..utils import parse_list
|
||||||
|
from ..utils import is_hostname
|
||||||
|
from ..utils import is_ipaddr
|
||||||
|
from ..utils import parse_bool
|
||||||
|
from ..locale import gettext_lazy as _
|
||||||
|
from ..url import PrivacyMode
|
||||||
|
|
||||||
|
|
||||||
|
class LunaSeaMode:
|
||||||
|
"""
|
||||||
|
Define LunaSea Notification Modes
|
||||||
|
"""
|
||||||
|
# App posts upstream to the developer API on LunaSea's website
|
||||||
|
CLOUD = "cloud"
|
||||||
|
|
||||||
|
# Running a dedicated private ntfy Server
|
||||||
|
PRIVATE = "private"
|
||||||
|
|
||||||
|
|
||||||
|
LUNASEA_MODES = (
|
||||||
|
LunaSeaMode.CLOUD,
|
||||||
|
LunaSeaMode.PRIVATE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class NotifyLunaSea(NotifyBase):
|
||||||
|
"""
|
||||||
|
A wrapper for LunaSea Notifications
|
||||||
|
"""
|
||||||
|
|
||||||
|
# The default descriptive name associated with the Notification
|
||||||
|
service_name = 'LunaSea'
|
||||||
|
|
||||||
|
# The services URL
|
||||||
|
service_url = 'https://luasea.app'
|
||||||
|
|
||||||
|
# The default insecure protocol
|
||||||
|
protocol = ('lunasea', 'lsea')
|
||||||
|
|
||||||
|
# The default secure protocol
|
||||||
|
secure_protocol = ('lunaseas', 'lseas')
|
||||||
|
|
||||||
|
# A URL that takes you to the setup/help of the specific protocol
|
||||||
|
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_lunasea'
|
||||||
|
|
||||||
|
# Allows the user to specify the NotifyImageSize object
|
||||||
|
image_size = NotifyImageSize.XY_256
|
||||||
|
|
||||||
|
# LunaSea Notification Details
|
||||||
|
cloud_notify_url = 'https://notify.lunasea.app'
|
||||||
|
notify_user_path = '/v1/custom/user/{}'
|
||||||
|
notify_device_path = '/v1/custom/device/{}'
|
||||||
|
|
||||||
|
# if our hostname matches the following we automatically enforce
|
||||||
|
# cloud mode
|
||||||
|
__auto_cloud_host = re.compile(r'(notify\.)?lunasea\.app', re.IGNORECASE)
|
||||||
|
|
||||||
|
# Define object templates
|
||||||
|
templates = (
|
||||||
|
'{schema}://{targets}',
|
||||||
|
'{schema}://{host}/{targets}',
|
||||||
|
'{schema}://{host}:{port}/{targets}',
|
||||||
|
'{schema}://{user}@{host}/{targets}',
|
||||||
|
'{schema}://{user}@{host}:{port}/{targets}',
|
||||||
|
'{schema}://{user}:{password}@{host}/{targets}',
|
||||||
|
'{schema}://{user}:{password}@{host}:{port}/{targets}',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Define our template tokens
|
||||||
|
template_tokens = dict(NotifyBase.template_tokens, **{
|
||||||
|
'host': {
|
||||||
|
'name': _('Hostname'),
|
||||||
|
'type': 'string',
|
||||||
|
},
|
||||||
|
'port': {
|
||||||
|
'name': _('Port'),
|
||||||
|
'type': 'int',
|
||||||
|
'min': 1,
|
||||||
|
'max': 65535,
|
||||||
|
},
|
||||||
|
'user': {
|
||||||
|
'name': _('Username'),
|
||||||
|
'type': 'string',
|
||||||
|
},
|
||||||
|
'password': {
|
||||||
|
'name': _('Password'),
|
||||||
|
'type': 'string',
|
||||||
|
'private': True,
|
||||||
|
},
|
||||||
|
'token': {
|
||||||
|
'name': _('Token'),
|
||||||
|
'type': 'string',
|
||||||
|
'private': True,
|
||||||
|
},
|
||||||
|
'target_user': {
|
||||||
|
'name': _('Target User'),
|
||||||
|
'type': 'string',
|
||||||
|
'prefix': '@',
|
||||||
|
'map_to': 'targets',
|
||||||
|
},
|
||||||
|
'target_device': {
|
||||||
|
'name': _('Target Device'),
|
||||||
|
'type': 'string',
|
||||||
|
'prefix': '+',
|
||||||
|
'map_to': 'targets',
|
||||||
|
},
|
||||||
|
'targets': {
|
||||||
|
'name': _('Targets'),
|
||||||
|
'type': 'list:string',
|
||||||
|
'required': True,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
# Define our template arguments
|
||||||
|
template_args = dict(NotifyBase.template_args, **{
|
||||||
|
'to': {
|
||||||
|
'alias_of': 'targets',
|
||||||
|
},
|
||||||
|
'image': {
|
||||||
|
'name': _('Include Image'),
|
||||||
|
'type': 'bool',
|
||||||
|
'default': False,
|
||||||
|
'map_to': 'include_image',
|
||||||
|
},
|
||||||
|
'mode': {
|
||||||
|
'name': _('Mode'),
|
||||||
|
'type': 'choice:string',
|
||||||
|
'values': LUNASEA_MODES,
|
||||||
|
'default': LunaSeaMode.PRIVATE,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
def __init__(self, targets=None, mode=None, token=None,
|
||||||
|
include_image=False, **kwargs):
|
||||||
|
"""
|
||||||
|
Initialize LunaSea Object
|
||||||
|
"""
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
# Show image associated with notification
|
||||||
|
self.include_image = \
|
||||||
|
self.template_args['image']['default'] \
|
||||||
|
if include_image is None else include_image
|
||||||
|
|
||||||
|
# Prepare our mode
|
||||||
|
self.mode = mode.strip().lower() \
|
||||||
|
if isinstance(mode, str) \
|
||||||
|
else self.template_args['mode']['default']
|
||||||
|
|
||||||
|
if self.mode not in LUNASEA_MODES:
|
||||||
|
msg = 'An invalid LunaSea mode ({}) was specified.'.format(mode)
|
||||||
|
self.logger.warning(msg)
|
||||||
|
raise TypeError(msg)
|
||||||
|
|
||||||
|
self.targets = []
|
||||||
|
for target in parse_list(targets):
|
||||||
|
if len(target) < 4:
|
||||||
|
self.logger.warning(
|
||||||
|
'A specified target ({}) is invalid and will be '
|
||||||
|
'ignored'.format(target))
|
||||||
|
continue
|
||||||
|
|
||||||
|
if target[0] == '+':
|
||||||
|
# Device
|
||||||
|
self.targets.append(('+', target[1:]))
|
||||||
|
|
||||||
|
elif target[0] == '@':
|
||||||
|
# User
|
||||||
|
self.targets.append(('@', target[1:]))
|
||||||
|
|
||||||
|
else:
|
||||||
|
# User
|
||||||
|
self.targets.append(('@', target))
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||||
|
"""
|
||||||
|
Perform LunaSea Notification
|
||||||
|
"""
|
||||||
|
|
||||||
|
# error tracking (used for function return)
|
||||||
|
has_error = False
|
||||||
|
|
||||||
|
if not len(self.targets):
|
||||||
|
# We have nothing to notify; we're done
|
||||||
|
self.logger.warning('There are no LunaSea targets to notify')
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Prepare our headers
|
||||||
|
headers = {
|
||||||
|
'User-Agent': self.app_id,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
}
|
||||||
|
|
||||||
|
# prepare payload
|
||||||
|
payload = {
|
||||||
|
'title': title if title else self.app_desc,
|
||||||
|
'body': body,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Acquire image_url
|
||||||
|
image_url = None if not self.include_image \
|
||||||
|
else self.image_url(notify_type)
|
||||||
|
|
||||||
|
if image_url:
|
||||||
|
payload['image'] = image_url
|
||||||
|
|
||||||
|
# Prepare our Authentication (if defined)
|
||||||
|
if self.user and self.password:
|
||||||
|
auth = (self.user, self.password)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# No Auth
|
||||||
|
auth = None
|
||||||
|
|
||||||
|
if self.mode == LunaSeaMode.CLOUD:
|
||||||
|
# Cloud Service
|
||||||
|
notify_url = self.cloud_notify_url
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Local Hosting
|
||||||
|
schema = 'https' if self.secure else 'http'
|
||||||
|
|
||||||
|
notify_url = '%s://%s' % (schema, self.host)
|
||||||
|
if isinstance(self.port, int):
|
||||||
|
notify_url += ':%d' % self.port
|
||||||
|
|
||||||
|
# Create a copy of the targets list
|
||||||
|
targets = list(self.targets)
|
||||||
|
while len(targets):
|
||||||
|
target = targets.pop(0)
|
||||||
|
|
||||||
|
if target[0] == '+':
|
||||||
|
url = notify_url + self.notify_device_path.format(target[1])
|
||||||
|
|
||||||
|
else:
|
||||||
|
url = notify_url + self.notify_user_path.format(target[1])
|
||||||
|
|
||||||
|
self.logger.debug('LunaSea POST URL: %s (cert_verify=%r)' % (
|
||||||
|
url, self.verify_certificate,
|
||||||
|
))
|
||||||
|
self.logger.debug('LunaSea Payload: %s' % str(payload))
|
||||||
|
|
||||||
|
# Always call throttle before any remote server i/o is made
|
||||||
|
self.throttle()
|
||||||
|
|
||||||
|
try:
|
||||||
|
r = requests.post(
|
||||||
|
url,
|
||||||
|
data=dumps(payload),
|
||||||
|
headers=headers,
|
||||||
|
auth=auth,
|
||||||
|
verify=self.verify_certificate,
|
||||||
|
timeout=self.request_timeout,
|
||||||
|
)
|
||||||
|
|
||||||
|
if r.status_code not in (
|
||||||
|
requests.codes.ok, requests.codes.no_content):
|
||||||
|
# We had a problem
|
||||||
|
status_str = \
|
||||||
|
NotifyLunaSea.http_response_code_lookup(r.status_code)
|
||||||
|
|
||||||
|
self.logger.warning(
|
||||||
|
'Failed to deliver payload to LunaSea:'
|
||||||
|
'{}{}error={}.'.format(
|
||||||
|
status_str,
|
||||||
|
', ' if status_str else '',
|
||||||
|
r.status_code))
|
||||||
|
|
||||||
|
self.logger.debug(
|
||||||
|
'Response Details:\r\n{}'.format(r.content))
|
||||||
|
|
||||||
|
has_error = True
|
||||||
|
|
||||||
|
# otherwise we were successful
|
||||||
|
continue
|
||||||
|
|
||||||
|
except requests.RequestException as e:
|
||||||
|
self.logger.warning(
|
||||||
|
'A Connection error occurred communicating with LunaSea.')
|
||||||
|
self.logger.debug('Socket Exception: %s' % str(e))
|
||||||
|
|
||||||
|
has_error = True
|
||||||
|
|
||||||
|
return not has_error
|
||||||
|
|
||||||
|
def url(self, privacy=False, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Returns the URL built dynamically based on specified arguments.
|
||||||
|
"""
|
||||||
|
|
||||||
|
params = {
|
||||||
|
'mode': self.mode,
|
||||||
|
'image': 'yes' if self.include_image else 'no',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Our URL parameters
|
||||||
|
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||||
|
|
||||||
|
auth = ''
|
||||||
|
if self.user and self.password:
|
||||||
|
auth = '{user}:{password}@'.format(
|
||||||
|
user=NotifyLunaSea.quote(self.user, safe=''),
|
||||||
|
password=self.pprint(
|
||||||
|
self.password, privacy, mode=PrivacyMode.Secret,
|
||||||
|
safe=''),
|
||||||
|
)
|
||||||
|
elif self.user:
|
||||||
|
auth = '{user}@'.format(
|
||||||
|
user=NotifyLunaSea.quote(self.user, safe=''),
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.mode == LunaSeaMode.PRIVATE:
|
||||||
|
default_port = 443 if self.secure else 80
|
||||||
|
return '{schema}://{auth}{host}{port}/{targets}?{params}'.format(
|
||||||
|
schema=self.secure_protocol[0]
|
||||||
|
if self.secure else self.protocol[0],
|
||||||
|
auth=auth,
|
||||||
|
host=self.host,
|
||||||
|
port='' if self.port is None or self.port == default_port
|
||||||
|
else ':{}'.format(self.port),
|
||||||
|
targets='/'.join(
|
||||||
|
[NotifyLunaSea.quote(x[0] + x[1], safe='@+')
|
||||||
|
for x in self.targets]),
|
||||||
|
params=NotifyLunaSea.urlencode(params)
|
||||||
|
)
|
||||||
|
|
||||||
|
else: # Cloud mode
|
||||||
|
return '{schema}://{auth}{targets}?{params}'.format(
|
||||||
|
schema=self.protocol[0],
|
||||||
|
auth=auth,
|
||||||
|
targets='/'.join(
|
||||||
|
[NotifyLunaSea.quote(x[0] + x[1], safe='@+')
|
||||||
|
for x in self.targets]),
|
||||||
|
params=NotifyLunaSea.urlencode(params)
|
||||||
|
)
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
"""
|
||||||
|
Returns the number of targets associated with this notification
|
||||||
|
"""
|
||||||
|
# always return 1
|
||||||
|
return 1 if not self.targets else len(self.targets)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_url(url):
|
||||||
|
"""
|
||||||
|
Parses the URL and returns enough arguments that can allow
|
||||||
|
us to re-instantiate this object.
|
||||||
|
|
||||||
|
"""
|
||||||
|
results = NotifyBase.parse_url(url, verify_host=False)
|
||||||
|
if not results:
|
||||||
|
# We're done early as we couldn't load the results
|
||||||
|
return results
|
||||||
|
|
||||||
|
# Fetch our targets
|
||||||
|
results['targets'] = NotifyLunaSea.split_path(results['fullpath'])
|
||||||
|
|
||||||
|
# Boolean to include an image or not
|
||||||
|
results['include_image'] = parse_bool(results['qsd'].get(
|
||||||
|
'image', NotifyLunaSea.template_args['image']['default']))
|
||||||
|
|
||||||
|
# The 'to' makes it easier to use yaml configuration
|
||||||
|
if 'to' in results['qsd'] and len(results['qsd']['to']):
|
||||||
|
results['targets'] += \
|
||||||
|
NotifyLunaSea.parse_list(results['qsd']['to'])
|
||||||
|
|
||||||
|
# Mode override
|
||||||
|
if 'mode' in results['qsd'] and results['qsd']['mode']:
|
||||||
|
results['mode'] = NotifyLunaSea.unquote(
|
||||||
|
results['qsd']['mode'].strip().lower())
|
||||||
|
|
||||||
|
else:
|
||||||
|
# We can try to detect the mode based on the validity of the
|
||||||
|
# hostname.
|
||||||
|
#
|
||||||
|
# This isn't a surfire way to do things though; it's best to
|
||||||
|
# specify the mode= flag
|
||||||
|
results['mode'] = LunaSeaMode.PRIVATE \
|
||||||
|
if ((is_hostname(results['host'])
|
||||||
|
or is_ipaddr(results['host'])) and results['targets']) \
|
||||||
|
else LunaSeaMode.CLOUD
|
||||||
|
|
||||||
|
if results['mode'] == LunaSeaMode.CLOUD:
|
||||||
|
# Store first entry as it can be a topic too in this case
|
||||||
|
# But only if we also rule it out not being the words
|
||||||
|
# lunasea.app itself, something that starts wiht an non-alpha
|
||||||
|
# numeric character:
|
||||||
|
if not NotifyLunaSea.__auto_cloud_host.search(results['host']):
|
||||||
|
# Add it to the front of the list for consistency
|
||||||
|
results['targets'].insert(0, results['host'])
|
||||||
|
|
||||||
|
elif results['mode'] == LunaSeaMode.PRIVATE and \
|
||||||
|
not (is_hostname(results['host'] or
|
||||||
|
is_ipaddr(results['host']))):
|
||||||
|
# Invalid Host for LunaSeaMode.PRIVATE
|
||||||
|
return None
|
||||||
|
|
||||||
|
return results
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -26,24 +26,20 @@
|
||||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
# POSSIBILITY OF SUCH DAMAGE.
|
# POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
from __future__ import absolute_import
|
|
||||||
from __future__ import print_function
|
|
||||||
|
|
||||||
import platform
|
import platform
|
||||||
import subprocess
|
import subprocess
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from .NotifyBase import NotifyBase
|
from .base import NotifyBase
|
||||||
from ..common import NotifyImageSize
|
from ..common import NotifyImageSize
|
||||||
from ..common import NotifyType
|
from ..common import NotifyType
|
||||||
from ..utils import parse_bool
|
from ..utils import parse_bool
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..locale import gettext_lazy as _
|
||||||
|
|
||||||
# Default our global support flag
|
# Default our global support flag
|
||||||
NOTIFY_MACOSX_SUPPORT_ENABLED = False
|
NOTIFY_MACOSX_SUPPORT_ENABLED = False
|
||||||
|
|
||||||
|
|
||||||
# TODO: The module will be easier to test without module-level code.
|
|
||||||
if platform.system() == 'Darwin':
|
if platform.system() == 'Darwin':
|
||||||
# Check this is Mac OS X 10.8, or higher
|
# Check this is Mac OS X 10.8, or higher
|
||||||
major, minor = platform.mac_ver()[0].split('.')[:2]
|
major, minor = platform.mac_ver()[0].split('.')[:2]
|
||||||
|
@ -102,6 +98,7 @@ class NotifyMacOSX(NotifyBase):
|
||||||
'/usr/local/bin/terminal-notifier',
|
'/usr/local/bin/terminal-notifier',
|
||||||
'/usr/bin/terminal-notifier',
|
'/usr/bin/terminal-notifier',
|
||||||
'/bin/terminal-notifier',
|
'/bin/terminal-notifier',
|
||||||
|
'/opt/local/bin/terminal-notifier',
|
||||||
)
|
)
|
||||||
|
|
||||||
# Define object templates
|
# Define object templates
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -56,7 +56,7 @@
|
||||||
#
|
#
|
||||||
import requests
|
import requests
|
||||||
from email.utils import formataddr
|
from email.utils import formataddr
|
||||||
from .NotifyBase import NotifyBase
|
from .base import NotifyBase
|
||||||
from ..common import NotifyType
|
from ..common import NotifyType
|
||||||
from ..common import NotifyFormat
|
from ..common import NotifyFormat
|
||||||
from ..utils import parse_emails
|
from ..utils import parse_emails
|
||||||
|
@ -64,7 +64,7 @@ from ..utils import parse_bool
|
||||||
from ..utils import is_email
|
from ..utils import is_email
|
||||||
from ..utils import validate_regex
|
from ..utils import validate_regex
|
||||||
from ..logger import logger
|
from ..logger import logger
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..locale import gettext_lazy as _
|
||||||
|
|
||||||
# Provide some known codes Mailgun uses and what they translate to:
|
# Provide some known codes Mailgun uses and what they translate to:
|
||||||
# Based on https://documentation.mailgun.com/en/latest/api-intro.html#errors
|
# Based on https://documentation.mailgun.com/en/latest/api-intro.html#errors
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -33,16 +33,16 @@ from json import dumps, loads
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from datetime import timezone
|
from datetime import timezone
|
||||||
|
|
||||||
from .NotifyBase import NotifyBase
|
from .base import NotifyBase
|
||||||
from ..URLBase import PrivacyMode
|
from ..url import PrivacyMode
|
||||||
from ..common import NotifyImageSize
|
from ..common import NotifyImageSize
|
||||||
from ..common import NotifyFormat
|
from ..common import NotifyFormat
|
||||||
from ..common import NotifyType
|
from ..common import NotifyType
|
||||||
from ..utils import parse_list
|
from ..utils import parse_list
|
||||||
from ..utils import parse_bool
|
from ..utils import parse_bool
|
||||||
from ..utils import validate_regex
|
from ..utils import validate_regex
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..locale import gettext_lazy as _
|
||||||
from ..attachment.AttachBase import AttachBase
|
from ..attachment.base import AttachBase
|
||||||
|
|
||||||
# Accept:
|
# Accept:
|
||||||
# - @username
|
# - @username
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -37,8 +37,8 @@ from json import dumps
|
||||||
from json import loads
|
from json import loads
|
||||||
from time import time
|
from time import time
|
||||||
|
|
||||||
from .NotifyBase import NotifyBase
|
from .base import NotifyBase
|
||||||
from ..URLBase import PrivacyMode
|
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
|
||||||
|
@ -46,7 +46,7 @@ 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
|
||||||
from ..utils import validate_regex
|
from ..utils import validate_regex
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..locale import gettext_lazy as _
|
||||||
|
|
||||||
# Define default path
|
# Define default path
|
||||||
MATRIX_V1_WEBHOOK_PATH = '/api/v1/matrix/hook'
|
MATRIX_V1_WEBHOOK_PATH = '/api/v1/matrix/hook'
|
||||||
|
@ -293,6 +293,9 @@ class NotifyMatrix(NotifyBase):
|
||||||
# This gets initialized after a login/registration
|
# This gets initialized after a login/registration
|
||||||
self.access_token = None
|
self.access_token = None
|
||||||
|
|
||||||
|
# This gets incremented for each request made against the v3 API
|
||||||
|
self.transaction_id = 0
|
||||||
|
|
||||||
# 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
|
||||||
|
|
||||||
|
@ -612,8 +615,10 @@ class NotifyMatrix(NotifyBase):
|
||||||
|
|
||||||
# Build our path
|
# Build our path
|
||||||
if self.version == MatrixVersion.V3:
|
if self.version == MatrixVersion.V3:
|
||||||
path = '/rooms/{}/send/m.room.message/0'.format(
|
path = '/rooms/{}/send/m.room.message/{}'.format(
|
||||||
NotifyMatrix.quote(room_id))
|
NotifyMatrix.quote(room_id),
|
||||||
|
self.transaction_id,
|
||||||
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
path = '/rooms/{}/send/m.room.message'.format(
|
path = '/rooms/{}/send/m.room.message'.format(
|
||||||
|
@ -685,6 +690,12 @@ class NotifyMatrix(NotifyBase):
|
||||||
method = 'PUT' if self.version == MatrixVersion.V3 else 'POST'
|
method = 'PUT' if self.version == MatrixVersion.V3 else 'POST'
|
||||||
postokay, response = self._fetch(
|
postokay, response = self._fetch(
|
||||||
path, payload=payload, method=method)
|
path, payload=payload, method=method)
|
||||||
|
|
||||||
|
# Increment the transaction ID to avoid future messages being
|
||||||
|
# recognized as retransmissions and ignored
|
||||||
|
if self.version == MatrixVersion.V3:
|
||||||
|
self.transaction_id += 1
|
||||||
|
|
||||||
if not postokay:
|
if not postokay:
|
||||||
# Notify our user
|
# Notify our user
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
|
@ -727,19 +738,29 @@ class NotifyMatrix(NotifyBase):
|
||||||
# "content_uri": "mxc://example.com/a-unique-key"
|
# "content_uri": "mxc://example.com/a-unique-key"
|
||||||
# }
|
# }
|
||||||
|
|
||||||
if self.version == MatrixVersion.V3:
|
# FUTURE if self.version == MatrixVersion.V3:
|
||||||
# Prepare our payload
|
# FUTURE # Prepare our payload
|
||||||
payloads.append({
|
# FUTURE payloads.append({
|
||||||
"body": attachment.name,
|
# FUTURE "body": attachment.name,
|
||||||
"info": {
|
# FUTURE "info": {
|
||||||
"mimetype": attachment.mimetype,
|
# FUTURE "mimetype": attachment.mimetype,
|
||||||
"size": len(attachment),
|
# FUTURE "size": len(attachment),
|
||||||
},
|
# FUTURE },
|
||||||
"msgtype": "m.image",
|
# FUTURE "msgtype": "m.image",
|
||||||
"url": response.get('content_uri'),
|
# FUTURE "url": response.get('content_uri'),
|
||||||
})
|
# FUTURE })
|
||||||
|
|
||||||
|
# FUTURE else:
|
||||||
|
# FUTURE # Prepare our payload
|
||||||
|
# FUTURE payloads.append({
|
||||||
|
# FUTURE "info": {
|
||||||
|
# FUTURE "mimetype": attachment.mimetype,
|
||||||
|
# FUTURE },
|
||||||
|
# FUTURE "msgtype": "m.image",
|
||||||
|
# FUTURE "body": "tta.webp",
|
||||||
|
# FUTURE "url": response.get('content_uri'),
|
||||||
|
# FUTURE })
|
||||||
|
|
||||||
else:
|
|
||||||
# Prepare our payload
|
# Prepare our payload
|
||||||
payloads.append({
|
payloads.append({
|
||||||
"info": {
|
"info": {
|
||||||
|
@ -1131,10 +1152,11 @@ class NotifyMatrix(NotifyBase):
|
||||||
or self.port == default_port else f':{self.port}')
|
or self.port == default_port else f':{self.port}')
|
||||||
|
|
||||||
if path == '/upload':
|
if path == '/upload':
|
||||||
if self.version == MatrixVersion.V3:
|
# FUTURE if self.version == MatrixVersion.V3:
|
||||||
url += MATRIX_V3_MEDIA_PATH + path
|
# FUTURE url += MATRIX_V3_MEDIA_PATH + path
|
||||||
|
|
||||||
else:
|
# FUTURE else:
|
||||||
|
# FUTURE url += MATRIX_V2_MEDIA_PATH + path
|
||||||
url += MATRIX_V2_MEDIA_PATH + path
|
url += MATRIX_V2_MEDIA_PATH + path
|
||||||
|
|
||||||
params = {'filename': attachment.name}
|
params = {'filename': attachment.name}
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -39,13 +39,13 @@
|
||||||
import requests
|
import requests
|
||||||
from json import dumps
|
from json import dumps
|
||||||
|
|
||||||
from .NotifyBase import NotifyBase
|
from .base import NotifyBase
|
||||||
from ..common import NotifyImageSize
|
from ..common import NotifyImageSize
|
||||||
from ..common import NotifyType
|
from ..common import NotifyType
|
||||||
from ..utils import parse_bool
|
from ..utils import parse_bool
|
||||||
from ..utils import parse_list
|
from ..utils import parse_list
|
||||||
from ..utils import validate_regex
|
from ..utils import validate_regex
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..locale import gettext_lazy as _
|
||||||
|
|
||||||
# Some Reference Locations:
|
# Some Reference Locations:
|
||||||
# - https://docs.mattermost.com/developer/webhooks-incoming.html
|
# - https://docs.mattermost.com/developer/webhooks-incoming.html
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -34,12 +34,12 @@
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from .NotifyBase import NotifyBase
|
from .base import NotifyBase
|
||||||
from ..common import NotifyType
|
from ..common import NotifyType
|
||||||
from ..utils import is_phone_no
|
from ..utils import is_phone_no
|
||||||
from ..utils import parse_phone_no
|
from ..utils import parse_phone_no
|
||||||
from ..utils import validate_regex
|
from ..utils import validate_regex
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..locale import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class NotifyMessageBird(NotifyBase):
|
class NotifyMessageBird(NotifyBase):
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -47,10 +47,10 @@
|
||||||
import requests
|
import requests
|
||||||
from json import dumps
|
from json import dumps
|
||||||
|
|
||||||
from .NotifyBase import NotifyBase
|
from .base import NotifyBase
|
||||||
from ..common import NotifyType
|
from ..common import NotifyType
|
||||||
from ..utils import validate_regex
|
from ..utils import validate_regex
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..locale import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class MisskeyVisibility:
|
class MisskeyVisibility:
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -38,12 +38,12 @@ import re
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from os.path import isfile
|
from os.path import isfile
|
||||||
from .NotifyBase import NotifyBase
|
from .base import NotifyBase
|
||||||
from ..URLBase import PrivacyMode
|
from ..url import PrivacyMode
|
||||||
from ..common import NotifyType
|
from ..common import NotifyType
|
||||||
from ..utils import parse_list
|
from ..utils import parse_list
|
||||||
from ..utils import parse_bool
|
from ..utils import parse_bool
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..locale import gettext_lazy as _
|
||||||
|
|
||||||
# Default our global support flag
|
# Default our global support flag
|
||||||
NOTIFY_MQTT_SUPPORT_ENABLED = False
|
NOTIFY_MQTT_SUPPORT_ENABLED = False
|
||||||
|
@ -89,7 +89,7 @@ class NotifyMQTT(NotifyBase):
|
||||||
|
|
||||||
requirements = {
|
requirements = {
|
||||||
# Define our required packaging in order to work
|
# Define our required packaging in order to work
|
||||||
'packages_required': 'paho-mqtt'
|
'packages_required': 'paho-mqtt < 2.0.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
# The default descriptive name associated with the Notification
|
# The default descriptive name associated with the Notification
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD 2-Clause License
|
# BSD 2-Clause License
|
||||||
#
|
#
|
||||||
# Apprise - Push Notification Library.
|
# Apprise - Push Notification Library.
|
||||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
# modification, are permitted provided that the following conditions are met:
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
@ -38,12 +38,12 @@
|
||||||
import re
|
import re
|
||||||
import requests
|
import requests
|
||||||
from json import dumps
|
from json import dumps
|
||||||
from .NotifyBase import NotifyBase
|
from .base import NotifyBase
|
||||||
from ..common import NotifyType
|
from ..common import NotifyType
|
||||||
from ..utils import is_phone_no
|
from ..utils import is_phone_no
|
||||||
from ..utils import parse_phone_no, parse_bool
|
from ..utils import parse_phone_no, parse_bool
|
||||||
from ..utils import validate_regex
|
from ..utils import validate_regex
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..locale import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class MSG91PayloadField:
|
class MSG91PayloadField:
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue