mirror of
https://github.com/SickGear/SickGear.git
synced 2025-12-04 16:14:36 +00:00
Merge branch 'feature/UpdateApprise' into dev
This commit is contained in:
commit
9840765754
162 changed files with 10359 additions and 2624 deletions
|
|
@ -1,5 +1,6 @@
|
|||
### 3.34.x (2025-xx-xx xx:xx:00 UTC)
|
||||
|
||||
* Update apprise 1.8.0 (81caf92) to 1.9.2 (a2a2216)
|
||||
* Update Beautiful Soup 4.12.3 (7fb5175) to 4.13.0b3 (55e006b)
|
||||
* Update CacheControl 0.14.0 (e2be0c2) to 0.14.2 (928422d)
|
||||
* Update certifi 2024.08.30 to 2024.12.14
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -27,10 +27,10 @@
|
|||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
__title__ = 'Apprise'
|
||||
__version__ = '1.8.0'
|
||||
__version__ = '1.9.2'
|
||||
__author__ = 'Chris Caron'
|
||||
__license__ = 'BSD'
|
||||
__copywrite__ = 'Copyright (C) 2024 Chris Caron <lead2gold@gmail.com>'
|
||||
__license__ = 'BSD 2-Clause'
|
||||
__copywrite__ = 'Copyright (C) 2025 Chris Caron <lead2gold@gmail.com>'
|
||||
__email__ = 'lead2gold@gmail.com'
|
||||
__status__ = 'Production'
|
||||
|
||||
|
|
@ -48,16 +48,20 @@ from .common import ContentIncludeMode
|
|||
from .common import CONTENT_INCLUDE_MODES
|
||||
from .common import ContentLocation
|
||||
from .common import CONTENT_LOCATIONS
|
||||
from .common import PersistentStoreMode
|
||||
from .common import PERSISTENT_STORE_MODES
|
||||
|
||||
from .url import URLBase
|
||||
from .url import PrivacyMode
|
||||
from .plugins.base import NotifyBase
|
||||
from .config.base import ConfigBase
|
||||
from .attachment.base import AttachBase
|
||||
from . import exception
|
||||
|
||||
from .apprise import Apprise
|
||||
from .locale import AppriseLocale
|
||||
from .asset import AppriseAsset
|
||||
from .persistent_store import PersistentStore
|
||||
from .apprise_config import AppriseConfig
|
||||
from .apprise_attachment import AppriseAttachment
|
||||
from .manager_attachment import AttachmentManager
|
||||
|
|
@ -77,6 +81,10 @@ __all__ = [
|
|||
# Core
|
||||
'Apprise', 'AppriseAsset', 'AppriseConfig', 'AppriseAttachment', 'URLBase',
|
||||
'NotifyBase', 'ConfigBase', 'AttachBase', 'AppriseLocale',
|
||||
'PersistentStore',
|
||||
|
||||
# Exceptions
|
||||
'exception',
|
||||
|
||||
# Reference
|
||||
'NotifyType', 'NotifyImageSize', 'NotifyFormat', 'OverflowMode',
|
||||
|
|
@ -84,6 +92,7 @@ __all__ = [
|
|||
'ConfigFormat', 'CONFIG_FORMATS',
|
||||
'ContentIncludeMode', 'CONTENT_INCLUDE_MODES',
|
||||
'ContentLocation', 'CONTENT_LOCATIONS',
|
||||
'PersistentStoreMode', 'PERSISTENT_STORE_MODES',
|
||||
'PrivacyMode',
|
||||
|
||||
# Managers
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -32,11 +32,10 @@ import os
|
|||
from itertools import chain
|
||||
from . import common
|
||||
from .conversion import convert_between
|
||||
from .utils import is_exclusive_match
|
||||
from .utils.logic import is_exclusive_match
|
||||
from .utils.parse import parse_list, parse_urls
|
||||
from .utils.cwe312 import cwe312_url
|
||||
from .manager_plugins import NotificationManager
|
||||
from .utils import parse_list
|
||||
from .utils import parse_urls
|
||||
from .utils import cwe312_url
|
||||
from .emojis import apply_emojis
|
||||
from .logger import logger
|
||||
from .asset import AppriseAsset
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -33,7 +33,7 @@ from .manager_attachment import AttachmentManager
|
|||
from .logger import logger
|
||||
from .common import ContentLocation
|
||||
from .common import CONTENT_LOCATIONS
|
||||
from .utils import GET_SCHEMA_RE
|
||||
from .utils.parse import GET_SCHEMA_RE
|
||||
|
||||
# Grant access to our Notification Manager Singleton
|
||||
A_MGR = AttachmentManager()
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -32,9 +32,8 @@ from .manager_config import ConfigurationManager
|
|||
from . import URLBase
|
||||
from .asset import AppriseAsset
|
||||
from . import common
|
||||
from .utils import GET_SCHEMA_RE
|
||||
from .utils import parse_list
|
||||
from .utils import is_exclusive_match
|
||||
from .utils.parse import GET_SCHEMA_RE, parse_list
|
||||
from .utils.logic import is_exclusive_match
|
||||
from .logger import logger
|
||||
|
||||
# Grant access to our Configuration Manager Singleton
|
||||
|
|
@ -46,7 +45,6 @@ class AppriseConfig:
|
|||
Our Apprise Configuration File Manager
|
||||
|
||||
- Supports a list of URLs defined one after another (text format)
|
||||
- Supports a destinct YAML configuration format
|
||||
|
||||
"""
|
||||
|
||||
|
|
@ -222,7 +220,7 @@ class AppriseConfig:
|
|||
a memory based object and only exists for the life of this
|
||||
AppriseConfig object it was loaded into.
|
||||
|
||||
If you know the format ('yaml' or 'text') you can specify
|
||||
If you know the format ('text') you can specify
|
||||
it for slightly less overhead during this call. Otherwise the
|
||||
configuration is auto-detected.
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -33,6 +33,7 @@ from os.path import dirname
|
|||
from os.path import isfile
|
||||
from os.path import abspath
|
||||
from .common import NotifyType
|
||||
from .common import PersistentStoreMode
|
||||
from .manager_plugins import NotificationManager
|
||||
|
||||
|
||||
|
|
@ -142,6 +143,12 @@ class AppriseAsset:
|
|||
# Defines the encoding of the content passed into Apprise
|
||||
encoding = 'utf-8'
|
||||
|
||||
# Automatically generate our Pretty Good Privacy (PGP) keys if one isn't
|
||||
# present and our environment configuration allows for it.
|
||||
# For example, a case where the environment wouldn't allow for it would be
|
||||
# if Persistent Storage was set to `memory`
|
||||
pgp_autogen = True
|
||||
|
||||
# For more detail see CWE-312 @
|
||||
# https://cwe.mitre.org/data/definitions/312.html
|
||||
#
|
||||
|
|
@ -157,6 +164,22 @@ class AppriseAsset:
|
|||
# By default, no paths are scanned.
|
||||
__plugin_paths = []
|
||||
|
||||
# Optionally set the location of the persistent storage
|
||||
# By default there is no path and thus persistent storage is not used
|
||||
__storage_path = None
|
||||
|
||||
# Optionally define the default salt to apply to all persistent storage
|
||||
# namespace generation (unless over-ridden)
|
||||
__storage_salt = b''
|
||||
|
||||
# Optionally define the namespace length of the directories created by
|
||||
# the storage. If this is set to zero, then the length is pre-determined
|
||||
# by the generator (sha1, md5, sha256, etc)
|
||||
__storage_idlen = 8
|
||||
|
||||
# Set storage to auto
|
||||
__storage_mode = PersistentStoreMode.AUTO
|
||||
|
||||
# All internal/system flags are prefixed with an underscore (_)
|
||||
# These can only be initialized using Python libraries and are not picked
|
||||
# up from (yaml) configuration files (if set)
|
||||
|
|
@ -171,7 +194,9 @@ class AppriseAsset:
|
|||
# A unique identifer we can use to associate our calling source
|
||||
_uid = str(uuid4())
|
||||
|
||||
def __init__(self, plugin_paths=None, **kwargs):
|
||||
def __init__(self, plugin_paths=None, storage_path=None,
|
||||
storage_mode=None, storage_salt=None,
|
||||
storage_idlen=None, **kwargs):
|
||||
"""
|
||||
Asset Initialization
|
||||
|
||||
|
|
@ -187,8 +212,49 @@ class AppriseAsset:
|
|||
|
||||
if plugin_paths:
|
||||
# Load any decorated modules if defined
|
||||
self.__plugin_paths = plugin_paths
|
||||
N_MGR.module_detection(plugin_paths)
|
||||
|
||||
if storage_path:
|
||||
# Define our persistent storage path
|
||||
self.__storage_path = storage_path
|
||||
|
||||
if storage_mode:
|
||||
# Define how our persistent storage behaves
|
||||
self.__storage_mode = storage_mode
|
||||
|
||||
if isinstance(storage_idlen, int):
|
||||
# Define the number of characters utilized from our namespace lengh
|
||||
if storage_idlen < 0:
|
||||
# Unsupported type
|
||||
raise ValueError(
|
||||
'AppriseAsset storage_idlen(): Value must '
|
||||
'be an integer and > 0')
|
||||
|
||||
# Store value
|
||||
self.__storage_idlen = storage_idlen
|
||||
|
||||
if storage_salt is not None:
|
||||
# Define the number of characters utilized from our namespace lengh
|
||||
|
||||
if isinstance(storage_salt, bytes):
|
||||
self.__storage_salt = storage_salt
|
||||
|
||||
elif isinstance(storage_salt, str):
|
||||
try:
|
||||
self.__storage_salt = storage_salt.encode(self.encoding)
|
||||
|
||||
except UnicodeEncodeError:
|
||||
# Bad data; don't pass it along
|
||||
raise ValueError(
|
||||
'AppriseAsset namespace_salt(): '
|
||||
'Value provided could not be encoded')
|
||||
|
||||
else: # Unsupported
|
||||
raise ValueError(
|
||||
'AppriseAsset namespace_salt(): Value provided must be '
|
||||
'string or bytes object')
|
||||
|
||||
def color(self, notify_type, color_type=None):
|
||||
"""
|
||||
Returns an HTML mapped color based on passed in notify type
|
||||
|
|
@ -356,3 +422,40 @@ class AppriseAsset:
|
|||
|
||||
"""
|
||||
return int(value.lstrip('#'), 16)
|
||||
|
||||
@property
|
||||
def plugin_paths(self):
|
||||
"""
|
||||
Return the plugin paths defined
|
||||
"""
|
||||
return self.__plugin_paths
|
||||
|
||||
@property
|
||||
def storage_path(self):
|
||||
"""
|
||||
Return the persistent storage path defined
|
||||
"""
|
||||
return self.__storage_path
|
||||
|
||||
@property
|
||||
def storage_mode(self):
|
||||
"""
|
||||
Return the persistent storage mode defined
|
||||
"""
|
||||
|
||||
return self.__storage_mode
|
||||
|
||||
@property
|
||||
def storage_salt(self):
|
||||
"""
|
||||
Return the provided namespace salt; this is always of type bytes
|
||||
"""
|
||||
return self.__storage_salt
|
||||
|
||||
@property
|
||||
def storage_idlen(self):
|
||||
"""
|
||||
Return the persistent storage id length
|
||||
"""
|
||||
|
||||
return self.__storage_idlen
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -29,8 +29,10 @@
|
|||
import os
|
||||
import time
|
||||
import mimetypes
|
||||
import base64
|
||||
from .. import exception
|
||||
from ..url import URLBase
|
||||
from ..utils import parse_bool
|
||||
from ..utils.parse import parse_bool
|
||||
from ..common import ContentLocation
|
||||
from ..locale import gettext_lazy as _
|
||||
|
||||
|
|
@ -267,28 +269,60 @@ class AttachBase(URLBase):
|
|||
cache = self.template_args['cache']['default'] \
|
||||
if self.cache is None else self.cache
|
||||
|
||||
if self.download_path and os.path.isfile(self.download_path) \
|
||||
and cache:
|
||||
try:
|
||||
if self.download_path and os.path.isfile(self.download_path) \
|
||||
and cache:
|
||||
|
||||
# We have enough reason to look further into our cached content
|
||||
# and verify it has not expired.
|
||||
if cache is True:
|
||||
# return our fixed content as is; we will always cache it
|
||||
return True
|
||||
# We have enough reason to look further into our cached content
|
||||
# and verify it has not expired.
|
||||
if cache is True:
|
||||
# return our fixed content as is; we will always cache it
|
||||
return True
|
||||
|
||||
# Verify our cache time to determine whether we will get our
|
||||
# content again.
|
||||
try:
|
||||
age_in_sec = time.time() - os.stat(self.download_path).st_mtime
|
||||
# Verify our cache time to determine whether we will get our
|
||||
# content again.
|
||||
age_in_sec = \
|
||||
time.time() - os.stat(self.download_path).st_mtime
|
||||
if age_in_sec <= cache:
|
||||
return True
|
||||
|
||||
except (OSError, IOError):
|
||||
# The file is not present
|
||||
pass
|
||||
except (OSError, IOError):
|
||||
# The file is not present
|
||||
pass
|
||||
|
||||
return False if not retrieve_if_missing else self.download()
|
||||
|
||||
def base64(self, encoding='ascii'):
|
||||
"""
|
||||
Returns the attachment object as a base64 string otherwise
|
||||
None is returned if an error occurs.
|
||||
|
||||
If encoding is set to None, then it is not encoded when returned
|
||||
"""
|
||||
if not self:
|
||||
# We could not access the attachment
|
||||
self.logger.error(
|
||||
'Could not access attachment {}.'.format(
|
||||
self.url(privacy=True)))
|
||||
raise exception.AppriseFileNotFound("Attachment Missing")
|
||||
|
||||
try:
|
||||
with self.open() as f:
|
||||
# Prepare our Attachment in Base64
|
||||
return base64.b64encode(f.read()).decode(encoding) \
|
||||
if encoding else base64.b64encode(f.read())
|
||||
|
||||
except (TypeError, FileNotFoundError):
|
||||
# We no longer have a path to open
|
||||
raise exception.AppriseFileNotFound("Attachment Missing")
|
||||
|
||||
except (TypeError, OSError, IOError) as e:
|
||||
self.logger.warning(
|
||||
'An I/O error occurred while reading {}.'.format(
|
||||
self.name if self else 'attachment'))
|
||||
self.logger.debug('I/O Exception: %s' % str(e))
|
||||
raise exception.AppriseDiskIOError("Attachment Access Error")
|
||||
|
||||
def invalidate(self):
|
||||
"""
|
||||
Release any temporary data that may be open by child classes.
|
||||
|
|
@ -326,12 +360,27 @@ class AttachBase(URLBase):
|
|||
|
||||
def open(self, mode='rb'):
|
||||
"""
|
||||
return our file pointer and track it (we'll auto close later
|
||||
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 chunk(self, size=5242880):
|
||||
"""
|
||||
A Generator that yield chunks of a file with the specified size.
|
||||
|
||||
By default the chunk size is set to 5MB (5242880 bytes)
|
||||
"""
|
||||
|
||||
with self.open() as file:
|
||||
while True:
|
||||
chunk = file.read(size)
|
||||
if not chunk:
|
||||
break
|
||||
|
||||
yield chunk
|
||||
|
||||
def __enter__(self):
|
||||
"""
|
||||
support with keyword
|
||||
|
|
@ -398,7 +447,15 @@ class AttachBase(URLBase):
|
|||
Returns the filesize of the attachment.
|
||||
|
||||
"""
|
||||
return os.path.getsize(self.path) if self.path else 0
|
||||
if not self:
|
||||
return 0
|
||||
|
||||
try:
|
||||
return os.path.getsize(self.path) if self.path else 0
|
||||
|
||||
except OSError:
|
||||
# OSError can occur if the file is inaccessible
|
||||
return 0
|
||||
|
||||
def __bool__(self):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -29,6 +29,7 @@
|
|||
import re
|
||||
import os
|
||||
from .base import AttachBase
|
||||
from ..utils.disk import path_decode
|
||||
from ..common import ContentLocation
|
||||
from ..locale import gettext_lazy as _
|
||||
|
||||
|
|
@ -57,7 +58,10 @@ class AttachFile(AttachBase):
|
|||
|
||||
# Store path but mark it dirty since we have not performed any
|
||||
# verification at this point.
|
||||
self.dirty_path = os.path.expanduser(path)
|
||||
self.dirty_path = path_decode(path)
|
||||
|
||||
# Track our file as it was saved
|
||||
self.__original_path = os.path.normpath(path)
|
||||
return
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
|
|
@ -77,7 +81,7 @@ class AttachFile(AttachBase):
|
|||
params['name'] = self._name
|
||||
|
||||
return 'file://{path}{params}'.format(
|
||||
path=self.quote(self.dirty_path),
|
||||
path=self.quote(self.__original_path),
|
||||
params='?{}'.format(self.urlencode(params, safe='/'))
|
||||
if params else '',
|
||||
)
|
||||
|
|
@ -97,7 +101,11 @@ class AttachFile(AttachBase):
|
|||
# Ensure any existing content set has been invalidated
|
||||
self.invalidate()
|
||||
|
||||
if not os.path.isfile(self.dirty_path):
|
||||
try:
|
||||
if not os.path.isfile(self.dirty_path):
|
||||
return False
|
||||
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
if self.max_file_size > 0 and \
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -29,7 +29,9 @@
|
|||
import re
|
||||
import os
|
||||
import io
|
||||
import base64
|
||||
from .base import AttachBase
|
||||
from .. import exception
|
||||
from ..common import ContentLocation
|
||||
from ..locale import gettext_lazy as _
|
||||
import uuid
|
||||
|
|
@ -145,6 +147,23 @@ class AttachMemory(AttachBase):
|
|||
|
||||
return True
|
||||
|
||||
def base64(self, encoding='ascii'):
|
||||
"""
|
||||
We need to over-ride this since the base64 sub-library seems to close
|
||||
our file descriptor making it no longer referencable.
|
||||
"""
|
||||
|
||||
if not self:
|
||||
# We could not access the attachment
|
||||
self.logger.error(
|
||||
'Could not access attachment {}.'.format(
|
||||
self.url(privacy=True)))
|
||||
raise exception.AppriseFileNotFound("Attachment Missing")
|
||||
self._data.seek(0, 0)
|
||||
|
||||
return base64.b64encode(self._data.read()).decode(encoding) \
|
||||
if encoding else base64.b64encode(self._data.read())
|
||||
|
||||
def invalidate(self):
|
||||
"""
|
||||
Removes data
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -182,6 +182,42 @@ CONTENT_LOCATIONS = (
|
|||
ContentLocation.INACCESSIBLE,
|
||||
)
|
||||
|
||||
|
||||
class PersistentStoreMode:
|
||||
# Allow persistent storage; write on demand
|
||||
AUTO = 'auto'
|
||||
|
||||
# Always flush every change to disk after it's saved. This has higher i/o
|
||||
# but enforces disk reflects what was set immediately
|
||||
FLUSH = 'flush'
|
||||
|
||||
# memory based store only
|
||||
MEMORY = 'memory'
|
||||
|
||||
|
||||
PERSISTENT_STORE_MODES = (
|
||||
PersistentStoreMode.AUTO,
|
||||
PersistentStoreMode.FLUSH,
|
||||
PersistentStoreMode.MEMORY,
|
||||
)
|
||||
|
||||
|
||||
class PersistentStoreState:
|
||||
"""
|
||||
Defines the persistent states describing what has been cached
|
||||
"""
|
||||
# Persistent Directory is actively cross-referenced against a matching URL
|
||||
ACTIVE = 'active'
|
||||
|
||||
# Persistent Directory is no longer being used or has no cross-reference
|
||||
STALE = 'stale'
|
||||
|
||||
# Persistent Directory is not utilizing any disk space at all, however
|
||||
# it potentially could if the plugin it successfully cross-references
|
||||
# is utilized
|
||||
UNUSED = 'unused'
|
||||
|
||||
|
||||
# This is a reserved tag that is automatically assigned to every
|
||||
# Notification Plugin
|
||||
MATCH_ALL_TAG = 'all'
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -34,11 +34,8 @@ from .. import plugins
|
|||
from .. import common
|
||||
from ..asset import AppriseAsset
|
||||
from ..url import URLBase
|
||||
from ..utils import GET_SCHEMA_RE
|
||||
from ..utils import parse_list
|
||||
from ..utils import parse_bool
|
||||
from ..utils import parse_urls
|
||||
from ..utils import cwe312_url
|
||||
from ..utils.parse import GET_SCHEMA_RE, parse_list, parse_bool, parse_urls
|
||||
from ..utils.cwe312 import cwe312_url
|
||||
from ..manager_config import ConfigurationManager
|
||||
from ..manager_plugins import NotificationManager
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -29,6 +29,7 @@
|
|||
import re
|
||||
import os
|
||||
from .base import ConfigBase
|
||||
from ..utils.disk import path_decode
|
||||
from ..common import ConfigFormat
|
||||
from ..common import ContentIncludeMode
|
||||
from ..locale import gettext_lazy as _
|
||||
|
|
@ -59,7 +60,10 @@ class ConfigFile(ConfigBase):
|
|||
super().__init__(**kwargs)
|
||||
|
||||
# Store our file path as it was set
|
||||
self.path = os.path.abspath(os.path.expanduser(path))
|
||||
self.path = path_decode(path)
|
||||
|
||||
# Track the file as it was saved
|
||||
self.__original_path = os.path.normpath(path)
|
||||
|
||||
# Update the config path to be relative to our file we just loaded
|
||||
self.config_path = os.path.dirname(self.path)
|
||||
|
|
@ -89,7 +93,7 @@ class ConfigFile(ConfigBase):
|
|||
params['format'] = self.config_format
|
||||
|
||||
return 'file://{path}{params}'.format(
|
||||
path=self.quote(self.path),
|
||||
path=self.quote(self.__original_path),
|
||||
params='?{}'.format(self.urlencode(params)) if params else '',
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -180,8 +180,10 @@ class HTMLConverter(HTMLParser, object):
|
|||
self._result.append('\n')
|
||||
|
||||
elif tag == 'hr':
|
||||
if self._result:
|
||||
if self._result and isinstance(self._result[-1], str):
|
||||
self._result[-1] = self._result[-1].rstrip(' ')
|
||||
else:
|
||||
pass
|
||||
|
||||
self._result.append('\n---\n')
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -29,10 +29,8 @@
|
|||
|
||||
from ..plugins.base import NotifyBase
|
||||
from ..manager_plugins import NotificationManager
|
||||
from ..utils import URL_DETAILS_RE
|
||||
from ..utils import parse_url
|
||||
from ..utils import url_assembly
|
||||
from ..utils import dict_full_update
|
||||
from ..utils.parse import URL_DETAILS_RE, parse_url, url_assembly
|
||||
from ..utils.logic import dict_full_update
|
||||
from .. import common
|
||||
from ..logger import logger
|
||||
import inspect
|
||||
|
|
@ -58,6 +56,9 @@ class CustomNotifyPlugin(NotifyBase):
|
|||
# Support Attachments
|
||||
attachment_support = True
|
||||
|
||||
# Allow persistent storage support
|
||||
storage_mode = common.PersistentStoreMode.AUTO
|
||||
|
||||
# Define object templates
|
||||
templates = (
|
||||
'{schema}://',
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
|
|||
61
lib/apprise/exception.py
Normal file
61
lib/apprise/exception.py
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2025, Chris Caron <lead2gold@gmail.com>
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
#
|
||||
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
import errno
|
||||
|
||||
|
||||
class AppriseException(Exception):
|
||||
"""
|
||||
Base Apprise Exception Class
|
||||
"""
|
||||
def __init__(self, message, error_code=0):
|
||||
super().__init__(message)
|
||||
self.error_code = error_code
|
||||
|
||||
|
||||
class ApprisePluginException(AppriseException):
|
||||
"""
|
||||
Class object for handling exceptions raised from within a plugin
|
||||
"""
|
||||
def __init__(self, message, error_code=600):
|
||||
super().__init__(message, error_code=error_code)
|
||||
|
||||
|
||||
class AppriseDiskIOError(AppriseException):
|
||||
"""
|
||||
Thrown when an disk i/o error occurs
|
||||
"""
|
||||
def __init__(self, message, error_code=errno.EIO):
|
||||
super().__init__(message, error_code=error_code)
|
||||
|
||||
|
||||
class AppriseFileNotFound(AppriseDiskIOError, FileNotFoundError):
|
||||
"""
|
||||
Thrown when a persistent write occured in MEMORY mode
|
||||
"""
|
||||
def __init__(self, message):
|
||||
super().__init__(message, error_code=errno.ENOENT)
|
||||
|
|
@ -1,21 +1,21 @@
|
|||
# Translations template for apprise.
|
||||
# Copyright (C) 2024 Chris Caron
|
||||
# Copyright (C) 2025 Chris Caron
|
||||
# This file is distributed under the same license as the apprise project.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2024.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2025.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: apprise 1.8.0\n"
|
||||
"Project-Id-Version: apprise 1.9.2\n"
|
||||
"Report-Msgid-Bugs-To: lead2gold@gmail.com\n"
|
||||
"POT-Creation-Date: 2024-05-11 16:13-0400\n"
|
||||
"POT-Creation-Date: 2025-01-08 21:02-0500\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.13.1\n"
|
||||
"Generated-By: Babel 2.16.0\n"
|
||||
|
||||
msgid "A local Gnome environment is required."
|
||||
msgstr ""
|
||||
|
|
@ -35,6 +35,9 @@ msgstr ""
|
|||
msgid "API Token"
|
||||
msgstr ""
|
||||
|
||||
msgid "API Version"
|
||||
msgstr ""
|
||||
|
||||
msgid "Access Key"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -47,7 +50,7 @@ msgstr ""
|
|||
msgid "Access Token"
|
||||
msgstr ""
|
||||
|
||||
msgid "Account Email"
|
||||
msgid "Account Email or Object ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Account SID"
|
||||
|
|
@ -56,6 +59,9 @@ msgstr ""
|
|||
msgid "Action"
|
||||
msgstr ""
|
||||
|
||||
msgid "Action Mapping"
|
||||
msgstr ""
|
||||
|
||||
msgid "Add Tokens"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -74,6 +80,12 @@ msgstr ""
|
|||
msgid "App ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "App Token"
|
||||
msgstr ""
|
||||
|
||||
msgid "App User Name"
|
||||
msgstr ""
|
||||
|
||||
msgid "App Version"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -95,6 +107,9 @@ msgstr ""
|
|||
msgid "Attach Filename"
|
||||
msgstr ""
|
||||
|
||||
msgid "Auth ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Auth Token"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -143,15 +158,15 @@ msgstr ""
|
|||
msgid "Category"
|
||||
msgstr ""
|
||||
|
||||
msgid "Chanify"
|
||||
msgstr ""
|
||||
|
||||
msgid "Channel ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Channels"
|
||||
msgstr ""
|
||||
|
||||
msgid "Chantify"
|
||||
msgstr ""
|
||||
|
||||
msgid "Class"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -182,6 +197,9 @@ msgstr ""
|
|||
msgid "Currency"
|
||||
msgstr ""
|
||||
|
||||
msgid "Custom Data"
|
||||
msgstr ""
|
||||
|
||||
msgid "Custom Details"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -200,6 +218,9 @@ msgstr ""
|
|||
msgid "Data Entries"
|
||||
msgstr ""
|
||||
|
||||
msgid "Decode Template Args"
|
||||
msgstr ""
|
||||
|
||||
msgid "Delay"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -242,9 +263,15 @@ msgstr ""
|
|||
msgid "Embed URL"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable Contents"
|
||||
msgstr ""
|
||||
|
||||
msgid "Entity"
|
||||
msgstr ""
|
||||
|
||||
msgid "Entity ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Event"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -284,6 +311,9 @@ msgstr ""
|
|||
msgid "Free-Mobile"
|
||||
msgstr ""
|
||||
|
||||
msgid "From"
|
||||
msgstr ""
|
||||
|
||||
msgid "From Email"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -398,6 +428,9 @@ msgstr ""
|
|||
msgid "Matrix API Verion"
|
||||
msgstr ""
|
||||
|
||||
msgid "Media Type"
|
||||
msgstr ""
|
||||
|
||||
msgid "Memory"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -463,6 +496,12 @@ msgstr ""
|
|||
msgid "Overflow Mode"
|
||||
msgstr ""
|
||||
|
||||
msgid "PGP Encryption"
|
||||
msgstr ""
|
||||
|
||||
msgid "PGP Public Key Path"
|
||||
msgstr ""
|
||||
|
||||
msgid "Packages are recommended to improve functionality."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -478,15 +517,15 @@ msgstr ""
|
|||
msgid "Payload Extras"
|
||||
msgstr ""
|
||||
|
||||
msgid "Ping Discord Role"
|
||||
msgstr ""
|
||||
|
||||
msgid "Ping Discord User"
|
||||
msgid "Persistent Storage"
|
||||
msgstr ""
|
||||
|
||||
msgid "Port"
|
||||
msgstr ""
|
||||
|
||||
msgid "Postback Data"
|
||||
msgstr ""
|
||||
|
||||
msgid "Prefix"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -511,6 +550,9 @@ msgstr ""
|
|||
msgid "Query Method"
|
||||
msgstr ""
|
||||
|
||||
msgid "Recipient Phone Number"
|
||||
msgstr ""
|
||||
|
||||
msgid "Region"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -532,6 +574,9 @@ msgstr ""
|
|||
msgid "Resubmit Flag"
|
||||
msgstr ""
|
||||
|
||||
msgid "Retain Messages"
|
||||
msgstr ""
|
||||
|
||||
msgid "Retry"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -541,6 +586,9 @@ msgstr ""
|
|||
msgid "Route Group"
|
||||
msgstr ""
|
||||
|
||||
msgid "SMS Mode"
|
||||
msgstr ""
|
||||
|
||||
msgid "SMTP Server"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -571,15 +619,27 @@ msgstr ""
|
|||
msgid "Sender ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Sender Name"
|
||||
msgstr ""
|
||||
|
||||
msgid "Sensitive Attachments"
|
||||
msgstr ""
|
||||
|
||||
msgid "Server Discovery"
|
||||
msgstr ""
|
||||
|
||||
msgid "Server Key"
|
||||
msgstr ""
|
||||
|
||||
msgid "Server Timeout"
|
||||
msgstr ""
|
||||
|
||||
msgid "Service ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Service Password"
|
||||
msgstr ""
|
||||
|
||||
msgid "Severity"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -589,9 +649,15 @@ msgstr ""
|
|||
msgid "Show Status"
|
||||
msgstr ""
|
||||
|
||||
msgid "Signature"
|
||||
msgstr ""
|
||||
|
||||
msgid "Silent Notification"
|
||||
msgstr ""
|
||||
|
||||
msgid "Société Française du Radiotéléphone"
|
||||
msgstr ""
|
||||
|
||||
msgid "Socket Connect Timeout"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -613,9 +679,15 @@ msgstr ""
|
|||
msgid "Source Phone No"
|
||||
msgstr ""
|
||||
|
||||
msgid "Space ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Special Text Color"
|
||||
msgstr ""
|
||||
|
||||
msgid "Splunk On-Call"
|
||||
msgstr ""
|
||||
|
||||
msgid "Spoiler Text"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -625,6 +697,9 @@ msgstr ""
|
|||
msgid "Subtitle"
|
||||
msgstr ""
|
||||
|
||||
msgid "TTS Voice"
|
||||
msgstr ""
|
||||
|
||||
msgid "Tags"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -646,9 +721,6 @@ msgstr ""
|
|||
msgid "Target Device"
|
||||
msgstr ""
|
||||
|
||||
msgid "Target Device ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Target Email"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -667,6 +739,9 @@ msgstr ""
|
|||
msgid "Target Group ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Target Phone"
|
||||
msgstr ""
|
||||
|
||||
msgid "Target Phone No"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -682,6 +757,9 @@ msgstr ""
|
|||
msgid "Target Room ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Target Routing Key"
|
||||
msgstr ""
|
||||
|
||||
msgid "Target Schedule"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -694,9 +772,6 @@ msgstr ""
|
|||
msgid "Target Subreddit"
|
||||
msgstr ""
|
||||
|
||||
msgid "Target Tag ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Target Team"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -709,6 +784,9 @@ msgstr ""
|
|||
msgid "Target User"
|
||||
msgstr ""
|
||||
|
||||
msgid "Target User ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Targets"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -757,6 +835,9 @@ msgstr ""
|
|||
msgid "Thread Key"
|
||||
msgstr ""
|
||||
|
||||
msgid "Timeout"
|
||||
msgstr ""
|
||||
|
||||
msgid "To Channel ID"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -778,6 +859,9 @@ msgstr ""
|
|||
msgid "Token C"
|
||||
msgstr ""
|
||||
|
||||
msgid "Token D"
|
||||
msgstr ""
|
||||
|
||||
msgid "Topic"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -841,6 +925,9 @@ msgstr ""
|
|||
msgid "Visibility"
|
||||
msgstr ""
|
||||
|
||||
msgid "Volume"
|
||||
msgstr ""
|
||||
|
||||
msgid "Web Based"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -862,9 +949,15 @@ msgstr ""
|
|||
msgid "Webhook Token"
|
||||
msgstr ""
|
||||
|
||||
msgid "Workflow ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Workspace"
|
||||
msgstr ""
|
||||
|
||||
msgid "Wrap Text"
|
||||
msgstr ""
|
||||
|
||||
msgid "X-Axis"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -33,9 +33,10 @@ import time
|
|||
import hashlib
|
||||
import inspect
|
||||
import threading
|
||||
from .utils import import_module
|
||||
from .utils import Singleton
|
||||
from .utils import parse_list
|
||||
from .utils.module import import_module
|
||||
from .utils.singleton import Singleton
|
||||
from .utils.parse import parse_list
|
||||
from .utils.disk import path_decode
|
||||
from os.path import dirname
|
||||
from os.path import abspath
|
||||
from os.path import join
|
||||
|
|
@ -223,7 +224,7 @@ class PluginManager(metaclass=Singleton):
|
|||
if not hasattr(plugin, 'app_id'):
|
||||
# Filter out non-notification modules
|
||||
logger.trace(
|
||||
"(%s) import failed; no app_id defined in %s",
|
||||
"(%s.%s) import failed; no app_id defined in %s",
|
||||
self.name, m_class, os.path.join(module_path, f))
|
||||
continue
|
||||
|
||||
|
|
@ -243,8 +244,10 @@ class PluginManager(metaclass=Singleton):
|
|||
for schema in schemas:
|
||||
if schema in self._schema_map:
|
||||
logger.error(
|
||||
"{} schema ({}) mismatch detected - {} to {}"
|
||||
.format(self.name, schema, self._schema_map,
|
||||
"{} schema ({}) mismatch detected -"
|
||||
' {} already maps to {}'
|
||||
.format(self.name, schema,
|
||||
self._schema_map[schema],
|
||||
plugin))
|
||||
continue
|
||||
|
||||
|
|
@ -373,7 +376,7 @@ class PluginManager(metaclass=Singleton):
|
|||
return
|
||||
|
||||
for _path in paths:
|
||||
path = os.path.abspath(os.path.expanduser(_path))
|
||||
path = path_decode(_path)
|
||||
if (cache and path in self._paths_previously_scanned) \
|
||||
or not os.path.exists(path):
|
||||
# We're done as we've already scanned this
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
|
|||
1676
lib/apprise/persistent_store.py
Normal file
1676
lib/apprise/persistent_store.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -36,9 +36,8 @@ from ..common import NotifyImageSize
|
|||
from ..common import NOTIFY_IMAGE_SIZES
|
||||
from ..common import NotifyType
|
||||
from ..common import NOTIFY_TYPES
|
||||
from ..utils import parse_list
|
||||
from ..utils import cwe312_url
|
||||
from ..utils import GET_SCHEMA_RE
|
||||
from ..utils.cwe312 import cwe312_url
|
||||
from ..utils.parse import parse_list, GET_SCHEMA_RE
|
||||
from ..logger import logger
|
||||
from ..locale import gettext_lazy as _
|
||||
from ..locale import LazyTranslation
|
||||
|
|
|
|||
468
lib/apprise/plugins/africas_talking.py
Normal file
468
lib/apprise/plugins/africas_talking.py
Normal file
|
|
@ -0,0 +1,468 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2025, 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 must have a Africas Talking Account setup; See here:
|
||||
# https://account.africastalking.com/
|
||||
# From here... acquire your APIKey
|
||||
#
|
||||
# API Details: https://developers.africastalking.com/docs/sms/sending/bulk
|
||||
import requests
|
||||
|
||||
from .base import NotifyBase
|
||||
from ..common import NotifyType
|
||||
from ..utils.parse import (
|
||||
is_phone_no, parse_bool, parse_phone_no, validate_regex)
|
||||
from ..locale import gettext_lazy as _
|
||||
|
||||
|
||||
class AfricasTalkingSMSMode:
|
||||
"""
|
||||
Africas Talking SMS Mode
|
||||
"""
|
||||
# BulkSMS Mode
|
||||
BULKSMS = 'bulksms'
|
||||
|
||||
# Premium Mode
|
||||
PREMIUM = 'premium'
|
||||
|
||||
# Sandbox Mode
|
||||
SANDBOX = 'sandbox'
|
||||
|
||||
|
||||
# Define the types in a list for validation purposes
|
||||
AFRICAS_TALKING_SMS_MODES = (
|
||||
AfricasTalkingSMSMode.BULKSMS,
|
||||
AfricasTalkingSMSMode.PREMIUM,
|
||||
AfricasTalkingSMSMode.SANDBOX,
|
||||
)
|
||||
|
||||
|
||||
# Extend HTTP Error Messages
|
||||
AFRICAS_TALKING_HTTP_ERROR_MAP = {
|
||||
100: 'Processed',
|
||||
101: 'Sent',
|
||||
102: 'Queued',
|
||||
401: 'Risk Hold',
|
||||
402: 'Invalid Sender ID',
|
||||
403: 'Invalid Phone Number',
|
||||
404: 'Unsupported Number Type',
|
||||
405: 'Insufficient Balance',
|
||||
406: 'User In Blacklist',
|
||||
407: 'Could Not Route',
|
||||
409: 'Do Not Disturb Rejection',
|
||||
500: 'Internal Server Error',
|
||||
501: 'Gateway Error',
|
||||
502: 'Rejected By Gateway',
|
||||
}
|
||||
|
||||
|
||||
class NotifyAfricasTalking(NotifyBase):
|
||||
"""
|
||||
A wrapper for Africas Talking Notifications
|
||||
"""
|
||||
|
||||
# The default descriptive name associated with the Notification
|
||||
service_name = 'Africas Talking'
|
||||
|
||||
# The services URL
|
||||
service_url = 'https://africastalking.com/'
|
||||
|
||||
# The default secure protocol
|
||||
secure_protocol = 'atalk'
|
||||
|
||||
# A URL that takes you to the setup/help of the specific protocol
|
||||
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_africas_talking'
|
||||
|
||||
# Africas Talking API Request URLs
|
||||
notify_url = {
|
||||
AfricasTalkingSMSMode.BULKSMS:
|
||||
'https://api.africastalking.com/version1/messaging',
|
||||
AfricasTalkingSMSMode.PREMIUM:
|
||||
'https://content.africastalking.com/version1/messaging',
|
||||
AfricasTalkingSMSMode.SANDBOX:
|
||||
'https://api.sandbox.africastalking.com/version1/messaging',
|
||||
}
|
||||
|
||||
# The maximum allowable characters allowed in the title per message
|
||||
title_maxlen = 0
|
||||
|
||||
# The maximum allowable characters allowed in the body per message
|
||||
body_maxlen = 160
|
||||
|
||||
# The maximum amount of phone numbers that can reside within a single
|
||||
# batch transfer
|
||||
default_batch_size = 50
|
||||
|
||||
# Define object templates
|
||||
templates = (
|
||||
'{schema}://{appuser}@{apikey}/{targets}',
|
||||
)
|
||||
|
||||
# Define our template tokens
|
||||
template_tokens = dict(NotifyBase.template_tokens, **{
|
||||
'appuser': {
|
||||
'name': _('App User Name'),
|
||||
'type': 'string',
|
||||
'regex': (r'^[A-Z0-9_-]+$', 'i'),
|
||||
'required': True,
|
||||
},
|
||||
'apikey': {
|
||||
'name': _('API Key'),
|
||||
'type': 'string',
|
||||
'required': True,
|
||||
'private': True,
|
||||
'regex': (r'^[A-Z0-9_-]+$', 'i'),
|
||||
},
|
||||
'target_phone': {
|
||||
'name': _('Target Phone'),
|
||||
'type': 'string',
|
||||
'map_to': 'targets',
|
||||
},
|
||||
'targets': {
|
||||
'name': _('Targets'),
|
||||
'type': 'list:string',
|
||||
},
|
||||
})
|
||||
|
||||
# Define our template arguments
|
||||
template_args = dict(NotifyBase.template_args, **{
|
||||
'to': {
|
||||
'alias_of': 'targets',
|
||||
},
|
||||
'apikey': {
|
||||
'alias_of': 'apikey',
|
||||
},
|
||||
'from': {
|
||||
# Your registered short code or alphanumeric
|
||||
'name': _('From'),
|
||||
'type': 'string',
|
||||
'default': 'AFRICASTKNG',
|
||||
'map_to': 'sender',
|
||||
},
|
||||
'batch': {
|
||||
'name': _('Batch Mode'),
|
||||
'type': 'bool',
|
||||
'default': False,
|
||||
},
|
||||
'mode': {
|
||||
'name': _('SMS Mode'),
|
||||
'type': 'choice:string',
|
||||
'values': AFRICAS_TALKING_SMS_MODES,
|
||||
'default': AFRICAS_TALKING_SMS_MODES[0],
|
||||
},
|
||||
})
|
||||
|
||||
def __init__(self, appuser, apikey, targets=None, sender=None, batch=None,
|
||||
mode=None, **kwargs):
|
||||
"""
|
||||
Initialize Africas Talking Object
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.appuser = validate_regex(
|
||||
appuser, *self.template_tokens['appuser']['regex'])
|
||||
if not self.appuser:
|
||||
msg = 'The Africas Talking appuser specified ({}) is invalid.'\
|
||||
.format(appuser)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
self.apikey = validate_regex(
|
||||
apikey, *self.template_tokens['apikey']['regex'])
|
||||
if not self.apikey:
|
||||
msg = 'The Africas Talking apikey specified ({}) is invalid.'\
|
||||
.format(apikey)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
# Prepare Sender
|
||||
self.sender = self.template_args['from']['default'] \
|
||||
if sender is None else sender
|
||||
|
||||
# Prepare Batch Mode Flag
|
||||
self.batch = self.template_args['batch']['default'] \
|
||||
if batch is None else batch
|
||||
|
||||
self.mode = self.template_args['mode']['default'] \
|
||||
if not isinstance(mode, str) else mode.lower()
|
||||
|
||||
if isinstance(mode, str) and mode:
|
||||
self.mode = next(
|
||||
(a for a in AFRICAS_TALKING_SMS_MODES if a.startswith(
|
||||
mode.lower())), None)
|
||||
|
||||
if self.mode not in AFRICAS_TALKING_SMS_MODES:
|
||||
msg = 'The Africas Talking mode specified ({}) is invalid.'\
|
||||
.format(mode)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
else:
|
||||
self.mode = self.template_args['mode']['default']
|
||||
|
||||
# Parse our targets
|
||||
self.targets = list()
|
||||
|
||||
for target in parse_phone_no(targets):
|
||||
# Validate targets and drop bad ones:
|
||||
result = is_phone_no(target)
|
||||
if not result:
|
||||
self.logger.warning(
|
||||
'Dropped invalid phone # '
|
||||
'({}) specified.'.format(target),
|
||||
)
|
||||
continue
|
||||
|
||||
# store valid phone number
|
||||
# Carry forward '+' if defined, otherwise do not...
|
||||
self.targets.append(
|
||||
('+' + result['full'])
|
||||
if target.lstrip()[0] == '+' else result['full'])
|
||||
|
||||
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||
"""
|
||||
Perform Africas Talking Notification
|
||||
"""
|
||||
|
||||
if not self.targets:
|
||||
# There is no one to email; we're done
|
||||
self.logger.warning(
|
||||
'There are no Africas Talking recipients to notify')
|
||||
return False
|
||||
|
||||
headers = {
|
||||
'User-Agent': self.app_id,
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Accept': 'application/json',
|
||||
'apiKey': self.apikey,
|
||||
}
|
||||
|
||||
# error tracking (used for function return)
|
||||
has_error = False
|
||||
|
||||
# Send in batches if identified to do so
|
||||
batch_size = 1 if not self.batch else self.default_batch_size
|
||||
|
||||
# Create a copy of the target list
|
||||
for index in range(0, len(self.targets), batch_size):
|
||||
# Prepare our payload
|
||||
payload = {
|
||||
'username': self.appuser,
|
||||
'to': ','.join(self.targets[index:index + batch_size]),
|
||||
'from': self.sender,
|
||||
'message': body,
|
||||
}
|
||||
|
||||
# Acquire our URL
|
||||
notify_url = self.notify_url[self.mode]
|
||||
|
||||
self.logger.debug(
|
||||
'Africas Talking POST URL: %s (cert_verify=%r)' % (
|
||||
notify_url, self.verify_certificate))
|
||||
self.logger.debug('Africas Talking Payload: %s' % str(payload))
|
||||
|
||||
# Printable target detail
|
||||
p_target = self.targets[index] if batch_size == 1 \
|
||||
else '{} target(s)'.format(
|
||||
len(self.targets[index:index + batch_size]))
|
||||
|
||||
# Always call throttle before any remote server i/o is made
|
||||
self.throttle()
|
||||
try:
|
||||
r = requests.post(
|
||||
notify_url,
|
||||
data=payload,
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
# Sample response
|
||||
# {
|
||||
# "SMSMessageData": {
|
||||
# "Message": "Sent to 1/1 Total Cost: KES 0.8000",
|
||||
# "Recipients": [{
|
||||
# "statusCode": 101,
|
||||
# "number": "+254711XXXYYY",
|
||||
# "status": "Success",
|
||||
# "cost": "KES 0.8000",
|
||||
# "messageId": "ATPid_SampleTxnId123"
|
||||
# }]
|
||||
# }
|
||||
# }
|
||||
|
||||
if r.status_code not in (100, 101, 102, requests.codes.ok):
|
||||
# We had a problem
|
||||
status_str = \
|
||||
NotifyAfricasTalking.http_response_code_lookup(
|
||||
r.status_code, AFRICAS_TALKING_HTTP_ERROR_MAP)
|
||||
|
||||
self.logger.warning(
|
||||
'Failed to send Africas Talking notification to {}: '
|
||||
'{}{}error={}.'.format(
|
||||
p_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
|
||||
|
||||
else:
|
||||
self.logger.info(
|
||||
'Sent Africas Talking notification to {}.'
|
||||
.format(p_target))
|
||||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occurred sending Africas Talking '
|
||||
'notification to {}.'.format(p_target))
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
||||
# Mark our failure
|
||||
has_error = True
|
||||
continue
|
||||
|
||||
return not has_error
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
return (self.secure_protocol, self.appuser, self.apikey)
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any URL parameters
|
||||
params = {
|
||||
'batch': 'yes' if self.batch else 'no',
|
||||
}
|
||||
|
||||
if self.sender != self.template_args['from']['default']:
|
||||
# Set our sender if it was set
|
||||
params['from'] = self.sender
|
||||
|
||||
if self.mode != self.template_args['mode']['default']:
|
||||
# Set our mode
|
||||
params['mode'] = self.mode
|
||||
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
return '{schema}://{appuser}@{apikey}/{targets}?{params}'.format(
|
||||
schema=self.secure_protocol,
|
||||
appuser=NotifyAfricasTalking.quote(self.appuser, safe=''),
|
||||
apikey=self.pprint(self.apikey, privacy, safe=''),
|
||||
targets='/'.join(
|
||||
[NotifyAfricasTalking.quote(x, safe='+')
|
||||
for x in self.targets]),
|
||||
params=NotifyAfricasTalking.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 batch_size > 1:
|
||||
targets = int(targets / batch_size) + \
|
||||
(1 if targets % batch_size else 0)
|
||||
|
||||
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
|
||||
|
||||
# The Application User ID
|
||||
results['appuser'] = NotifyAfricasTalking.unquote(results['user'])
|
||||
|
||||
# Prepare our targets
|
||||
results['targets'] = []
|
||||
|
||||
# Our Application APIKey
|
||||
if 'apikey' in results['qsd'] and len(results['qsd']['apikey']):
|
||||
# Store our apikey if specified as keyword
|
||||
results['apikey'] = \
|
||||
NotifyAfricasTalking.unquote(results['qsd']['apikey'])
|
||||
|
||||
# This means our host is actually a phone number (target)
|
||||
results['targets'].append(
|
||||
NotifyAfricasTalking.unquote(results['host']))
|
||||
|
||||
else:
|
||||
# First item is our apikey
|
||||
results['apikey'] = NotifyAfricasTalking.unquote(results['host'])
|
||||
|
||||
# Store our remaining targets found on path
|
||||
results['targets'].extend(
|
||||
NotifyAfricasTalking.split_path(results['fullpath']))
|
||||
|
||||
# The 'from' makes it easier to use yaml configuration
|
||||
if 'from' in results['qsd'] and len(results['qsd']['from']):
|
||||
results['sender'] = \
|
||||
NotifyAfricasTalking.unquote(results['qsd']['from'])
|
||||
|
||||
# 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'] += \
|
||||
NotifyAfricasTalking.parse_phone_no(results['qsd']['to'])
|
||||
|
||||
# Get our Mode
|
||||
if 'mode' in results['qsd'] and len(results['qsd']['mode']):
|
||||
results['mode'] = \
|
||||
NotifyAfricasTalking.unquote(results['qsd']['mode'])
|
||||
|
||||
# Get Batch Mode Flag
|
||||
results['batch'] = \
|
||||
parse_bool(results['qsd'].get(
|
||||
'batch',
|
||||
NotifyAfricasTalking.template_args['batch']['default']))
|
||||
|
||||
return results
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -29,13 +29,12 @@
|
|||
import re
|
||||
import requests
|
||||
from json import dumps
|
||||
import base64
|
||||
|
||||
from .. import exception
|
||||
from .base import NotifyBase
|
||||
from ..url import PrivacyMode
|
||||
from ..common import NotifyType
|
||||
from ..utils import parse_list
|
||||
from ..utils import validate_regex
|
||||
from ..utils.parse import parse_list, validate_regex
|
||||
from ..locale import gettext_lazy as _
|
||||
|
||||
|
||||
|
|
@ -261,39 +260,45 @@ class NotifyAppriseAPI(NotifyBase):
|
|||
if not attachment:
|
||||
# We could not access the attachment
|
||||
self.logger.error(
|
||||
'Could not access attachment {}.'.format(
|
||||
'Could not access Apprise API attachment {}.'.format(
|
||||
attachment.url(privacy=True)))
|
||||
return False
|
||||
|
||||
try:
|
||||
# Our Attachment filename
|
||||
filename = attachment.name \
|
||||
if attachment.name else f'file{no:03}.dat'
|
||||
|
||||
if self.method == AppriseAPIMethod.JSON:
|
||||
with open(attachment.path, 'rb') as f:
|
||||
# Output must be in a DataURL format (that's what
|
||||
# PushSafer calls it):
|
||||
attachments.append({
|
||||
'filename': attachment.name,
|
||||
'base64': base64.b64encode(f.read())
|
||||
.decode('utf-8'),
|
||||
'mimetype': attachment.mimetype,
|
||||
})
|
||||
# Output must be in a DataURL format (that's what
|
||||
# PushSafer calls it):
|
||||
attachments.append({
|
||||
"filename": filename,
|
||||
'base64': attachment.base64(),
|
||||
'mimetype': attachment.mimetype,
|
||||
})
|
||||
|
||||
else: # AppriseAPIMethod.FORM
|
||||
files.append((
|
||||
'file{:02d}'.format(no),
|
||||
(
|
||||
attachment.name,
|
||||
filename,
|
||||
open(attachment.path, 'rb'),
|
||||
attachment.mimetype,
|
||||
)
|
||||
))
|
||||
|
||||
except (OSError, IOError) as e:
|
||||
self.logger.warning(
|
||||
'An I/O error occurred while reading {}.'.format(
|
||||
attachment.name if attachment else 'attachment'))
|
||||
self.logger.debug('I/O Exception: %s' % str(e))
|
||||
except (TypeError, OSError, exception.AppriseException):
|
||||
# We could not access the attachment
|
||||
self.logger.error(
|
||||
'Could not access AppriseAPI attachment {}.'.format(
|
||||
attachment.url(privacy=True)))
|
||||
return False
|
||||
|
||||
self.logger.debug(
|
||||
'Appending AppriseAPI attachment {}'.format(
|
||||
attachment.url(privacy=True)))
|
||||
|
||||
# prepare Apprise API Object
|
||||
payload = {
|
||||
# Apprise API Payload
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -74,8 +74,7 @@ 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 ..utils.parse import is_call_sign, parse_call_sign
|
||||
from .. import __version__
|
||||
import re
|
||||
|
||||
|
|
@ -729,6 +728,15 @@ class NotifyAprs(NotifyBase):
|
|||
params=NotifyAprs.urlencode(params),
|
||||
)
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
return (self.user, self.password, self.locale)
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Returns the number of targets associated with this notification
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -36,8 +36,7 @@ from .base import NotifyBase
|
|||
from ..url import PrivacyMode
|
||||
from ..common import NotifyImageSize
|
||||
from ..common import NotifyType
|
||||
from ..utils import parse_list
|
||||
from ..utils import parse_bool
|
||||
from ..utils.parse import parse_list, parse_bool
|
||||
from ..locale import gettext_lazy as _
|
||||
|
||||
|
||||
|
|
@ -89,11 +88,14 @@ class NotifyBarkLevel:
|
|||
|
||||
PASSIVE = 'passive'
|
||||
|
||||
CRITICAL = 'critical'
|
||||
|
||||
|
||||
BARK_LEVELS = (
|
||||
NotifyBarkLevel.ACTIVE,
|
||||
NotifyBarkLevel.TIME_SENSITIVE,
|
||||
NotifyBarkLevel.PASSIVE,
|
||||
NotifyBarkLevel.CRITICAL,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -178,6 +180,12 @@ class NotifyBark(NotifyBase):
|
|||
'type': 'choice:string',
|
||||
'values': BARK_LEVELS,
|
||||
},
|
||||
'volume': {
|
||||
'name': _('Volume'),
|
||||
'type': 'int',
|
||||
'min': 0,
|
||||
'max': 10,
|
||||
},
|
||||
'click': {
|
||||
'name': _('Click'),
|
||||
'type': 'string',
|
||||
|
|
@ -205,7 +213,7 @@ class NotifyBark(NotifyBase):
|
|||
|
||||
def __init__(self, targets=None, include_image=True, sound=None,
|
||||
category=None, group=None, level=None, click=None,
|
||||
badge=None, **kwargs):
|
||||
badge=None, volume=None, **kwargs):
|
||||
"""
|
||||
Initialize Notify Bark Object
|
||||
"""
|
||||
|
|
@ -260,6 +268,19 @@ class NotifyBark(NotifyBase):
|
|||
self.logger.warning(
|
||||
'The specified Bark sound ({}) was not found ', sound)
|
||||
|
||||
# Volume
|
||||
self.volume = None
|
||||
if volume is not None:
|
||||
try:
|
||||
self.volume = int(volume) if volume is not None else None
|
||||
if self.volume is not None and not (0 <= self.volume <= 10):
|
||||
raise ValueError()
|
||||
|
||||
except (TypeError, ValueError):
|
||||
self.logger.warning(
|
||||
'The specified Bark volume ({}) is not valid. '
|
||||
'Must be between 0 and 10', volume)
|
||||
|
||||
# Level
|
||||
self.level = None if not level else next(
|
||||
(f for f in BARK_LEVELS if f[0] == level[0]), None)
|
||||
|
|
@ -330,6 +351,9 @@ class NotifyBark(NotifyBase):
|
|||
if self.group:
|
||||
payload['group'] = self.group
|
||||
|
||||
if self.volume:
|
||||
payload['volume'] = self.volume
|
||||
|
||||
auth = None
|
||||
if self.user:
|
||||
auth = (self.user, self.password)
|
||||
|
|
@ -395,6 +419,18 @@ class NotifyBark(NotifyBase):
|
|||
|
||||
return not has_error
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
return (
|
||||
self.secure_protocol if self.secure else self.protocol,
|
||||
self.user, self.password, self.host, self.port,
|
||||
)
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
|
|
@ -417,6 +453,9 @@ class NotifyBark(NotifyBase):
|
|||
if self.level:
|
||||
params['level'] = self.level
|
||||
|
||||
if self.volume:
|
||||
params['volume'] = str(self.volume)
|
||||
|
||||
if self.category:
|
||||
params['category'] = self.category
|
||||
|
||||
|
|
@ -490,6 +529,11 @@ class NotifyBark(NotifyBase):
|
|||
results['badge'] = NotifyBark.unquote(
|
||||
results['qsd']['badge'].strip())
|
||||
|
||||
# Volume
|
||||
if 'volume' in results['qsd'] and results['qsd']['volume']:
|
||||
results['volume'] = NotifyBark.unquote(
|
||||
results['qsd']['volume'].strip())
|
||||
|
||||
# Level
|
||||
if 'level' in results['qsd'] and results['qsd']['level']:
|
||||
results['level'] = NotifyBark.unquote(
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -32,13 +32,15 @@ from functools import partial
|
|||
|
||||
from ..url import URLBase
|
||||
from ..common import NotifyType
|
||||
from ..utils import parse_bool
|
||||
from ..utils.parse import parse_bool
|
||||
from ..common import NOTIFY_TYPES
|
||||
from ..common import NotifyFormat
|
||||
from ..common import NOTIFY_FORMATS
|
||||
from ..common import OverflowMode
|
||||
from ..common import OVERFLOW_MODES
|
||||
from ..common import PersistentStoreMode
|
||||
from ..locale import gettext_lazy as _
|
||||
from ..persistent_store import PersistentStore
|
||||
from ..apprise_attachment import AppriseAttachment
|
||||
|
||||
|
||||
|
|
@ -130,12 +132,19 @@ class NotifyBase(URLBase):
|
|||
# of lines. Setting this to zero disables this feature.
|
||||
body_max_line_count = 0
|
||||
|
||||
# Persistent storage default settings
|
||||
persistent_storage = True
|
||||
|
||||
# Default Notify Format
|
||||
notify_format = NotifyFormat.TEXT
|
||||
|
||||
# Default Overflow Mode
|
||||
overflow_mode = OverflowMode.UPSTREAM
|
||||
|
||||
# Our default is to no not use persistent storage beyond in-memory
|
||||
# reference
|
||||
storage_mode = PersistentStoreMode.MEMORY
|
||||
|
||||
# Default Emoji Interpretation
|
||||
interpret_emojis = False
|
||||
|
||||
|
|
@ -197,6 +206,16 @@ class NotifyBase(URLBase):
|
|||
# runtime.
|
||||
'_lookup_default': 'interpret_emojis',
|
||||
},
|
||||
'store': {
|
||||
'name': _('Persistent Storage'),
|
||||
# Use Persistent Storage
|
||||
'type': 'bool',
|
||||
# Provide a default
|
||||
'default': persistent_storage,
|
||||
# look up default using the following parent class value at
|
||||
# runtime.
|
||||
'_lookup_default': 'persistent_storage',
|
||||
},
|
||||
})
|
||||
|
||||
#
|
||||
|
|
@ -268,6 +287,9 @@ class NotifyBase(URLBase):
|
|||
# are turned off (no user over-rides allowed)
|
||||
#
|
||||
|
||||
# Our Persistent Storage object is initialized on demand
|
||||
self.__store = None
|
||||
|
||||
# Take a default
|
||||
self.interpret_emojis = self.asset.interpret_emojis
|
||||
if 'emojis' in kwargs:
|
||||
|
|
@ -301,6 +323,14 @@ class NotifyBase(URLBase):
|
|||
# Provide override
|
||||
self.overflow_mode = overflow
|
||||
|
||||
# Prepare our Persistent Storage switch
|
||||
self.persistent_storage = parse_bool(
|
||||
kwargs.get('store', NotifyBase.persistent_storage))
|
||||
if not self.persistent_storage:
|
||||
# Enforce the disabling of cache (ortherwise defaults are use)
|
||||
self.url_identifier = False
|
||||
self.__cached_url_identifier = None
|
||||
|
||||
def image_url(self, notify_type, logo=False, extension=None,
|
||||
image_size=None):
|
||||
"""
|
||||
|
|
@ -726,6 +756,10 @@ class NotifyBase(URLBase):
|
|||
'overflow': self.overflow_mode,
|
||||
}
|
||||
|
||||
# Persistent Storage Setting
|
||||
if self.persistent_storage != NotifyBase.persistent_storage:
|
||||
params['store'] = 'yes' if self.persistent_storage else 'no'
|
||||
|
||||
params.update(super().url_parameters(*args, **kwargs))
|
||||
|
||||
# return default parameters
|
||||
|
|
@ -778,6 +812,10 @@ class NotifyBase(URLBase):
|
|||
# Allow emoji's override
|
||||
if 'emojis' in results['qsd']:
|
||||
results['emojis'] = parse_bool(results['qsd'].get('emojis'))
|
||||
# Store our persistent storage boolean
|
||||
|
||||
if 'store' in results['qsd']:
|
||||
results['store'] = results['qsd']['store']
|
||||
|
||||
return results
|
||||
|
||||
|
|
@ -798,3 +836,29 @@ class NotifyBase(URLBase):
|
|||
should return the same set of results that parse_url() does.
|
||||
"""
|
||||
return None
|
||||
|
||||
@property
|
||||
def store(self):
|
||||
"""
|
||||
Returns a pointer to our persistent store for use.
|
||||
|
||||
The best use cases are:
|
||||
self.store.get('key')
|
||||
self.store.set('key', 'value')
|
||||
self.store.delete('key1', 'key2', ...)
|
||||
|
||||
You can also access the keys this way:
|
||||
self.store['key']
|
||||
|
||||
And clear them:
|
||||
del self.store['key']
|
||||
|
||||
"""
|
||||
if self.__store is None:
|
||||
# Initialize our persistent store for use
|
||||
self.__store = PersistentStore(
|
||||
namespace=self.url_id(),
|
||||
path=self.asset.storage_path,
|
||||
mode=self.asset.storage_mode)
|
||||
|
||||
return self.__store
|
||||
|
|
|
|||
|
|
@ -1,395 +0,0 @@
|
|||
# -*- 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 requests
|
||||
import hmac
|
||||
from json import dumps
|
||||
from time import time
|
||||
from hashlib import sha1
|
||||
from itertools import chain
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from .base import NotifyBase
|
||||
from ..url import PrivacyMode
|
||||
from ..utils import parse_bool
|
||||
from ..utils import parse_list
|
||||
from ..utils import validate_regex
|
||||
from ..common import NotifyType
|
||||
from ..common import NotifyImageSize
|
||||
from ..locale import gettext_lazy as _
|
||||
|
||||
# Default to sending to all devices if nothing is specified
|
||||
DEFAULT_TAG = '@all'
|
||||
|
||||
# The tags value is an structure containing an array of strings defining the
|
||||
# list of tagged devices that the notification need to be send to, and a
|
||||
# boolean operator (‘and’ / ‘or’) that defines the criteria to match devices
|
||||
# against those tags.
|
||||
IS_TAG = re.compile(r'^[@]?(?P<name>[A-Z0-9]{1,63})$', re.I)
|
||||
|
||||
# Device tokens are only referenced when developing.
|
||||
# It's not likely you'll send a message directly to a device, but if you do;
|
||||
# this plugin supports it.
|
||||
IS_DEVICETOKEN = re.compile(r'^[A-Z0-9]{64}$', re.I)
|
||||
|
||||
# Used to break apart list of potential tags by their delimiter into a useable
|
||||
# list.
|
||||
TAGS_LIST_DELIM = re.compile(r'[ \t\r\n,\\/]+')
|
||||
|
||||
|
||||
class NotifyBoxcar(NotifyBase):
|
||||
"""
|
||||
A wrapper for Boxcar Notifications
|
||||
"""
|
||||
|
||||
# The default descriptive name associated with the Notification
|
||||
service_name = 'Boxcar'
|
||||
|
||||
# The services URL
|
||||
service_url = 'https://boxcar.io/'
|
||||
|
||||
# All boxcar notifications are secure
|
||||
secure_protocol = 'boxcar'
|
||||
|
||||
# A URL that takes you to the setup/help of the specific protocol
|
||||
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_boxcar'
|
||||
|
||||
# Boxcar URL
|
||||
notify_url = 'https://boxcar-api.io/api/push/'
|
||||
|
||||
# Allows the user to specify the NotifyImageSize object
|
||||
image_size = NotifyImageSize.XY_72
|
||||
|
||||
# The maximum allowable characters allowed in the body per message
|
||||
body_maxlen = 10000
|
||||
|
||||
# Define object templates
|
||||
templates = (
|
||||
'{schema}://{access_key}/{secret_key}/',
|
||||
'{schema}://{access_key}/{secret_key}/{targets}',
|
||||
)
|
||||
|
||||
# Define our template tokens
|
||||
template_tokens = dict(NotifyBase.template_tokens, **{
|
||||
'access_key': {
|
||||
'name': _('Access Key'),
|
||||
'type': 'string',
|
||||
'private': True,
|
||||
'required': True,
|
||||
'regex': (r'^[A-Z0-9_-]{64}$', 'i'),
|
||||
'map_to': 'access',
|
||||
},
|
||||
'secret_key': {
|
||||
'name': _('Secret Key'),
|
||||
'type': 'string',
|
||||
'private': True,
|
||||
'required': True,
|
||||
'regex': (r'^[A-Z0-9_-]{64}$', 'i'),
|
||||
'map_to': 'secret',
|
||||
},
|
||||
'target_tag': {
|
||||
'name': _('Target Tag ID'),
|
||||
'type': 'string',
|
||||
'prefix': '@',
|
||||
'regex': (r'^[A-Z0-9]{1,63}$', 'i'),
|
||||
'map_to': 'targets',
|
||||
},
|
||||
'target_device': {
|
||||
'name': _('Target Device ID'),
|
||||
'type': 'string',
|
||||
'regex': (r'^[A-Z0-9]{64}$', 'i'),
|
||||
'map_to': 'targets',
|
||||
},
|
||||
'targets': {
|
||||
'name': _('Targets'),
|
||||
'type': 'list:string',
|
||||
},
|
||||
})
|
||||
|
||||
# Define our template arguments
|
||||
template_args = dict(NotifyBase.template_args, **{
|
||||
'image': {
|
||||
'name': _('Include Image'),
|
||||
'type': 'bool',
|
||||
'default': True,
|
||||
'map_to': 'include_image',
|
||||
},
|
||||
'to': {
|
||||
'alias_of': 'targets',
|
||||
},
|
||||
'access': {
|
||||
'alias_of': 'access_key',
|
||||
},
|
||||
'secret': {
|
||||
'alias_of': 'secret_key',
|
||||
},
|
||||
})
|
||||
|
||||
def __init__(self, access, secret, targets=None, include_image=True,
|
||||
**kwargs):
|
||||
"""
|
||||
Initialize Boxcar Object
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
|
||||
# Initialize tag list
|
||||
self._tags = list()
|
||||
|
||||
# Initialize device_token list
|
||||
self.device_tokens = list()
|
||||
|
||||
# Access Key (associated with project)
|
||||
self.access = validate_regex(
|
||||
access, *self.template_tokens['access_key']['regex'])
|
||||
if not self.access:
|
||||
msg = 'An invalid Boxcar Access Key ' \
|
||||
'({}) was specified.'.format(access)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
# Secret Key (associated with project)
|
||||
self.secret = validate_regex(
|
||||
secret, *self.template_tokens['secret_key']['regex'])
|
||||
if not self.secret:
|
||||
msg = 'An invalid Boxcar Secret Key ' \
|
||||
'({}) was specified.'.format(secret)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
if not targets:
|
||||
self._tags.append(DEFAULT_TAG)
|
||||
targets = []
|
||||
|
||||
# Validate targets and drop bad ones:
|
||||
for target in parse_list(targets):
|
||||
result = IS_TAG.match(target)
|
||||
if result:
|
||||
# store valid tag/alias
|
||||
self._tags.append(result.group('name'))
|
||||
continue
|
||||
|
||||
result = IS_DEVICETOKEN.match(target)
|
||||
if result:
|
||||
# store valid device
|
||||
self.device_tokens.append(target)
|
||||
continue
|
||||
|
||||
self.logger.warning(
|
||||
'Dropped invalid tag/alias/device_token '
|
||||
'({}) specified.'.format(target),
|
||||
)
|
||||
|
||||
# Track whether or not we want to send an image with our notification
|
||||
# or not.
|
||||
self.include_image = include_image
|
||||
|
||||
return
|
||||
|
||||
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||
"""
|
||||
Perform Boxcar Notification
|
||||
"""
|
||||
headers = {
|
||||
'User-Agent': self.app_id,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
# prepare Boxcar Object
|
||||
payload = {
|
||||
'aps': {
|
||||
'badge': 'auto',
|
||||
'alert': '',
|
||||
},
|
||||
'expires': str(int(time() + 30)),
|
||||
}
|
||||
|
||||
if title:
|
||||
payload['aps']['@title'] = title
|
||||
|
||||
payload['aps']['alert'] = body
|
||||
|
||||
if self._tags:
|
||||
payload['tags'] = {'or': self._tags}
|
||||
|
||||
if self.device_tokens:
|
||||
payload['device_tokens'] = self.device_tokens
|
||||
|
||||
# Source picture should be <= 450 DP wide, ~2:1 aspect.
|
||||
image_url = None if not self.include_image \
|
||||
else self.image_url(notify_type)
|
||||
|
||||
if image_url:
|
||||
# Set our image
|
||||
payload['@img'] = image_url
|
||||
|
||||
# Acquire our hostname
|
||||
host = urlparse(self.notify_url).hostname
|
||||
|
||||
# Calculate signature.
|
||||
str_to_sign = "%s\n%s\n%s\n%s" % (
|
||||
"POST", host, "/api/push", dumps(payload))
|
||||
|
||||
h = hmac.new(
|
||||
bytearray(self.secret, 'utf-8'),
|
||||
bytearray(str_to_sign, 'utf-8'),
|
||||
sha1,
|
||||
)
|
||||
|
||||
params = NotifyBoxcar.urlencode({
|
||||
"publishkey": self.access,
|
||||
"signature": h.hexdigest(),
|
||||
})
|
||||
|
||||
notify_url = '%s?%s' % (self.notify_url, params)
|
||||
self.logger.debug('Boxcar POST URL: %s (cert_verify=%r)' % (
|
||||
notify_url, self.verify_certificate,
|
||||
))
|
||||
self.logger.debug('Boxcar Payload: %s' % str(payload))
|
||||
|
||||
# Always call throttle before any remote server i/o is made
|
||||
self.throttle()
|
||||
|
||||
try:
|
||||
r = requests.post(
|
||||
notify_url,
|
||||
data=dumps(payload),
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
|
||||
# Boxcar returns 201 (Created) when successful
|
||||
if r.status_code != requests.codes.created:
|
||||
# We had a problem
|
||||
status_str = \
|
||||
NotifyBoxcar.http_response_code_lookup(r.status_code)
|
||||
|
||||
self.logger.warning(
|
||||
'Failed to send Boxcar 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 Boxcar notification.')
|
||||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occurred sending Boxcar '
|
||||
'notification to %s.' % (host))
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
# Define any URL parameters
|
||||
params = {
|
||||
'image': 'yes' if self.include_image else 'no',
|
||||
}
|
||||
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
return '{schema}://{access}/{secret}/{targets}?{params}'.format(
|
||||
schema=self.secure_protocol,
|
||||
access=self.pprint(self.access, privacy, safe=''),
|
||||
secret=self.pprint(
|
||||
self.secret, privacy, mode=PrivacyMode.Secret, safe=''),
|
||||
targets='/'.join([
|
||||
NotifyBoxcar.quote(x, safe='') for x in chain(
|
||||
self._tags, self.device_tokens) if x != DEFAULT_TAG]),
|
||||
params=NotifyBoxcar.urlencode(params),
|
||||
)
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Returns the number of targets associated with this notification
|
||||
"""
|
||||
targets = len(self._tags) + len(self.device_tokens)
|
||||
# DEFAULT_TAG is set if no tokens/tags are otherwise set
|
||||
return targets if targets > 0 else 1
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns it broken apart into a dictionary.
|
||||
|
||||
"""
|
||||
results = NotifyBase.parse_url(url, verify_host=False)
|
||||
if not results:
|
||||
# We're done early
|
||||
return None
|
||||
|
||||
# The first token is stored in the hostname
|
||||
results['access'] = NotifyBoxcar.unquote(results['host'])
|
||||
|
||||
# Get our entries; split_path() looks after unquoting content for us
|
||||
# by default
|
||||
entries = NotifyBoxcar.split_path(results['fullpath'])
|
||||
|
||||
# Now fetch the remaining tokens
|
||||
results['secret'] = entries.pop(0) if entries else None
|
||||
|
||||
# Our recipients make up the remaining entries of our array
|
||||
results['targets'] = entries
|
||||
|
||||
# The 'to' makes it easier to use yaml configuration
|
||||
if 'to' in results['qsd'] and len(results['qsd']['to']):
|
||||
results['targets'] += \
|
||||
NotifyBoxcar.parse_list(results['qsd'].get('to'))
|
||||
|
||||
# Access
|
||||
if 'access' in results['qsd'] and results['qsd']['access']:
|
||||
results['access'] = NotifyBoxcar.unquote(
|
||||
results['qsd']['access'].strip())
|
||||
|
||||
# Secret
|
||||
if 'secret' in results['qsd'] and results['qsd']['secret']:
|
||||
results['secret'] = NotifyBoxcar.unquote(
|
||||
results['qsd']['secret'].strip())
|
||||
|
||||
# Include images with our message
|
||||
results['include_image'] = \
|
||||
parse_bool(results['qsd'].get('image', True))
|
||||
|
||||
return results
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -39,9 +39,7 @@ from itertools import chain
|
|||
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 ..utils.parse import is_phone_no, parse_phone_no, parse_bool
|
||||
from ..locale import gettext_lazy as _
|
||||
|
||||
|
||||
|
|
@ -269,7 +267,7 @@ class NotifyBulkSMS(NotifyBase):
|
|||
'to': None,
|
||||
'body': body,
|
||||
'routingGroup': self.route,
|
||||
'encoding': BulkSMSEncoding.UNICODE \
|
||||
'encoding': BulkSMSEncoding.UNICODE
|
||||
if self.unicode else BulkSMSEncoding.TEXT,
|
||||
# Options are NONE, ALL and ERRORS
|
||||
'deliveryReports': "ERRORS"
|
||||
|
|
@ -413,6 +411,19 @@ class NotifyBulkSMS(NotifyBase):
|
|||
for x in self.groups])),
|
||||
params=NotifyBulkSMS.urlencode(params))
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
return (
|
||||
self.secure_protocol,
|
||||
self.user if self.user else None,
|
||||
self.password if self.password else None,
|
||||
)
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Returns the number of targets associated with this notification
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -38,9 +38,7 @@ 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 ..utils.parse import is_phone_no, parse_phone_no, parse_bool
|
||||
from ..locale import gettext_lazy as _
|
||||
|
||||
|
||||
|
|
@ -304,6 +302,15 @@ class NotifyBulkVS(NotifyBase):
|
|||
|
||||
return not has_error
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
return (self.secure_protocol, self.source, self.user, self.password)
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -36,10 +36,8 @@ import requests
|
|||
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 ..utils import validate_regex
|
||||
from ..utils.parse import (
|
||||
is_phone_no, parse_phone_no, parse_bool, validate_regex)
|
||||
from ..locale import gettext_lazy as _
|
||||
|
||||
|
||||
|
|
@ -378,6 +376,15 @@ class NotifyBurstSMS(NotifyBase):
|
|||
[NotifyBurstSMS.quote(x, safe='') for x in self.targets]),
|
||||
params=NotifyBurstSMS.urlencode(params))
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
return (self.secure_protocol, self.apikey, self.secret, self.source)
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Returns the number of targets associated with this notification
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
# Chantify
|
||||
# Chanify
|
||||
# 1. Visit https://chanify.net/
|
||||
|
||||
# The API URL will look something like this:
|
||||
|
|
@ -37,26 +37,26 @@ import requests
|
|||
|
||||
from .base import NotifyBase
|
||||
from ..common import NotifyType
|
||||
from ..utils import validate_regex
|
||||
from ..utils.parse import validate_regex
|
||||
from ..locale import gettext_lazy as _
|
||||
|
||||
|
||||
class NotifyChantify(NotifyBase):
|
||||
class NotifyChanify(NotifyBase):
|
||||
"""
|
||||
A wrapper for Chantify Notifications
|
||||
A wrapper for Chanify Notifications
|
||||
"""
|
||||
|
||||
# The default descriptive name associated with the Notification
|
||||
service_name = _('Chantify')
|
||||
service_name = _('Chanify')
|
||||
|
||||
# The services URL
|
||||
service_url = 'https://chanify.net/'
|
||||
|
||||
# The default secure protocol
|
||||
secure_protocol = 'chantify'
|
||||
secure_protocol = 'chanify'
|
||||
|
||||
# A URL that takes you to the setup/help of the specific protocol
|
||||
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_chantify'
|
||||
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_chanify'
|
||||
|
||||
# Notification URL
|
||||
notify_url = 'https://api.chanify.net/v1/sender/{token}/'
|
||||
|
|
@ -91,14 +91,14 @@ class NotifyChantify(NotifyBase):
|
|||
|
||||
def __init__(self, token, **kwargs):
|
||||
"""
|
||||
Initialize Chantify Object
|
||||
Initialize Chanify Object
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.token = validate_regex(
|
||||
token, *self.template_tokens['token']['regex'])
|
||||
if not self.token:
|
||||
msg = 'The Chantify token specified ({}) is invalid.'\
|
||||
msg = 'The Chanify token specified ({}) is invalid.'\
|
||||
.format(token)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
|
@ -121,9 +121,9 @@ class NotifyChantify(NotifyBase):
|
|||
'text': body
|
||||
}
|
||||
|
||||
self.logger.debug('Chantify GET URL: %s (cert_verify=%r)' % (
|
||||
self.logger.debug('Chanify GET URL: %s (cert_verify=%r)' % (
|
||||
self.notify_url, self.verify_certificate))
|
||||
self.logger.debug('Chantify Payload: %s' % str(payload))
|
||||
self.logger.debug('Chanify Payload: %s' % str(payload))
|
||||
|
||||
# Always call throttle before any remote server i/o is made
|
||||
self.throttle()
|
||||
|
|
@ -139,10 +139,10 @@ class NotifyChantify(NotifyBase):
|
|||
if r.status_code != requests.codes.ok:
|
||||
# We had a problem
|
||||
status_str = \
|
||||
NotifyChantify.http_response_code_lookup(r.status_code)
|
||||
NotifyChanify.http_response_code_lookup(r.status_code)
|
||||
|
||||
self.logger.warning(
|
||||
'Failed to send Chantify notification: '
|
||||
'Failed to send Chanify notification: '
|
||||
'{}{}error={}.'.format(
|
||||
status_str,
|
||||
', ' if status_str else '',
|
||||
|
|
@ -154,11 +154,11 @@ class NotifyChantify(NotifyBase):
|
|||
return False
|
||||
|
||||
else:
|
||||
self.logger.info('Sent Chantify notification.')
|
||||
self.logger.info('Sent Chanify notification.')
|
||||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occurred sending Chantify '
|
||||
'A Connection error occurred sending Chanify '
|
||||
'notification.')
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
||||
|
|
@ -178,9 +178,18 @@ class NotifyChantify(NotifyBase):
|
|||
return '{schema}://{token}/?{params}'.format(
|
||||
schema=self.secure_protocol,
|
||||
token=self.pprint(self.token, privacy, safe=''),
|
||||
params=NotifyChantify.urlencode(params),
|
||||
params=NotifyChanify.urlencode(params),
|
||||
)
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
return (self.secure_protocol, self.token)
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
|
|
@ -198,9 +207,9 @@ class NotifyChantify(NotifyBase):
|
|||
|
||||
# Allow over-ride
|
||||
if 'token' in results['qsd'] and len(results['qsd']['token']):
|
||||
results['token'] = NotifyChantify.unquote(results['qsd']['token'])
|
||||
results['token'] = NotifyChanify.unquote(results['qsd']['token'])
|
||||
|
||||
else:
|
||||
results['token'] = NotifyChantify.unquote(results['host'])
|
||||
results['token'] = NotifyChanify.unquote(results['host'])
|
||||
|
||||
return results
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -45,9 +45,7 @@ from json import dumps
|
|||
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 ..utils.parse import is_phone_no, parse_phone_no, parse_bool
|
||||
from ..locale import gettext_lazy as _
|
||||
|
||||
# Extend HTTP Error Messages
|
||||
|
|
@ -285,6 +283,15 @@ class NotifyClickSend(NotifyBase):
|
|||
params=NotifyClickSend.urlencode(params),
|
||||
)
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
return (self.secure_protocol, self.user, self.password)
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Returns the number of targets associated with this notification
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -272,62 +272,6 @@ class NotifyForm(NotifyBase):
|
|||
|
||||
return
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any URL parameters
|
||||
params = {
|
||||
'method': self.method,
|
||||
}
|
||||
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
# Append our headers into our parameters
|
||||
params.update({'+{}'.format(k): v for k, v in self.headers.items()})
|
||||
|
||||
# Append our GET params into our parameters
|
||||
params.update({'-{}'.format(k): v for k, v in self.params.items()})
|
||||
|
||||
# Append our payload extra's into our parameters
|
||||
params.update(
|
||||
{':{}'.format(k): v for k, v in self.payload_extras.items()})
|
||||
params.update(
|
||||
{':{}'.format(k): v for k, v in self.payload_overrides.items()})
|
||||
|
||||
if self.attach_as != self.attach_as_default:
|
||||
# Provide Attach-As extension details
|
||||
params['attach-as'] = self.attach_as
|
||||
|
||||
# Determine Authentication
|
||||
auth = ''
|
||||
if self.user and self.password:
|
||||
auth = '{user}:{password}@'.format(
|
||||
user=NotifyForm.quote(self.user, safe=''),
|
||||
password=self.pprint(
|
||||
self.password, privacy, mode=PrivacyMode.Secret, safe=''),
|
||||
)
|
||||
elif self.user:
|
||||
auth = '{user}@'.format(
|
||||
user=NotifyForm.quote(self.user, safe=''),
|
||||
)
|
||||
|
||||
default_port = 443 if self.secure else 80
|
||||
|
||||
return '{schema}://{auth}{hostname}{port}{fullpath}?{params}'.format(
|
||||
schema=self.secure_protocol if self.secure else self.protocol,
|
||||
auth=auth,
|
||||
# never encode hostname since we're expecting it to be a valid one
|
||||
hostname=self.host,
|
||||
port='' if self.port is None or self.port == default_port
|
||||
else ':{}'.format(self.port),
|
||||
fullpath=NotifyForm.quote(self.fullpath, safe='/')
|
||||
if self.fullpath else '/',
|
||||
params=NotifyForm.urlencode(params),
|
||||
)
|
||||
|
||||
def send(self, body, title='', notify_type=NotifyType.INFO, attach=None,
|
||||
**kwargs):
|
||||
"""
|
||||
|
|
@ -358,7 +302,8 @@ class NotifyForm(NotifyBase):
|
|||
files.append((
|
||||
self.attach_as.format(no)
|
||||
if self.attach_multi_support else self.attach_as, (
|
||||
attachment.name,
|
||||
attachment.name
|
||||
if attachment.name else f'file{no:03}.dat',
|
||||
open(attachment.path, 'rb'),
|
||||
attachment.mimetype)
|
||||
))
|
||||
|
|
@ -486,6 +431,76 @@ class NotifyForm(NotifyBase):
|
|||
|
||||
return True
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
return (
|
||||
self.secure_protocol if self.secure else self.protocol,
|
||||
self.user, self.password, self.host,
|
||||
self.port if self.port else (443 if self.secure else 80),
|
||||
self.fullpath.rstrip('/'),
|
||||
)
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any URL parameters
|
||||
params = {
|
||||
'method': self.method,
|
||||
}
|
||||
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
# Append our headers into our parameters
|
||||
params.update({'+{}'.format(k): v for k, v in self.headers.items()})
|
||||
|
||||
# Append our GET params into our parameters
|
||||
params.update({'-{}'.format(k): v for k, v in self.params.items()})
|
||||
|
||||
# Append our payload extra's into our parameters
|
||||
params.update(
|
||||
{':{}'.format(k): v for k, v in self.payload_extras.items()})
|
||||
params.update(
|
||||
{':{}'.format(k): v for k, v in self.payload_overrides.items()})
|
||||
|
||||
if self.attach_as != self.attach_as_default:
|
||||
# Provide Attach-As extension details
|
||||
params['attach-as'] = self.attach_as
|
||||
|
||||
# Determine Authentication
|
||||
auth = ''
|
||||
if self.user and self.password:
|
||||
auth = '{user}:{password}@'.format(
|
||||
user=NotifyForm.quote(self.user, safe=''),
|
||||
password=self.pprint(
|
||||
self.password, privacy, mode=PrivacyMode.Secret, safe=''),
|
||||
)
|
||||
elif self.user:
|
||||
auth = '{user}@'.format(
|
||||
user=NotifyForm.quote(self.user, safe=''),
|
||||
)
|
||||
|
||||
default_port = 443 if self.secure else 80
|
||||
|
||||
return '{schema}://{auth}{hostname}{port}{fullpath}?{params}'.format(
|
||||
schema=self.secure_protocol if self.secure else self.protocol,
|
||||
auth=auth,
|
||||
# never encode hostname since we're expecting it to be a valid one
|
||||
hostname=self.host,
|
||||
port='' if self.port is None or self.port == default_port
|
||||
else ':{}'.format(self.port),
|
||||
fullpath=NotifyForm.quote(self.fullpath, safe='/')
|
||||
if self.fullpath else '/',
|
||||
params=NotifyForm.urlencode(params),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -27,9 +27,9 @@
|
|||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import requests
|
||||
import base64
|
||||
from json import dumps
|
||||
|
||||
from .. import exception
|
||||
from .base import NotifyBase
|
||||
from ..url import PrivacyMode
|
||||
from ..common import NotifyImageSize
|
||||
|
|
@ -195,56 +195,6 @@ class NotifyJSON(NotifyBase):
|
|||
|
||||
return
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any URL parameters
|
||||
params = {
|
||||
'method': self.method,
|
||||
}
|
||||
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
# Append our headers into our parameters
|
||||
params.update({'+{}'.format(k): v for k, v in self.headers.items()})
|
||||
|
||||
# Append our GET params into our parameters
|
||||
params.update({'-{}'.format(k): v for k, v in self.params.items()})
|
||||
|
||||
# Append our payload extra's into our parameters
|
||||
params.update(
|
||||
{':{}'.format(k): v for k, v in self.payload_extras.items()})
|
||||
|
||||
# Determine Authentication
|
||||
auth = ''
|
||||
if self.user and self.password:
|
||||
auth = '{user}:{password}@'.format(
|
||||
user=NotifyJSON.quote(self.user, safe=''),
|
||||
password=self.pprint(
|
||||
self.password, privacy, mode=PrivacyMode.Secret, safe=''),
|
||||
)
|
||||
elif self.user:
|
||||
auth = '{user}@'.format(
|
||||
user=NotifyJSON.quote(self.user, safe=''),
|
||||
)
|
||||
|
||||
default_port = 443 if self.secure else 80
|
||||
|
||||
return '{schema}://{auth}{hostname}{port}{fullpath}?{params}'.format(
|
||||
schema=self.secure_protocol if self.secure else self.protocol,
|
||||
auth=auth,
|
||||
# never encode hostname since we're expecting it to be a valid one
|
||||
hostname=self.host,
|
||||
port='' if self.port is None or self.port == default_port
|
||||
else ':{}'.format(self.port),
|
||||
fullpath=NotifyJSON.quote(self.fullpath, safe='/')
|
||||
if self.fullpath else '/',
|
||||
params=NotifyJSON.urlencode(params),
|
||||
)
|
||||
|
||||
def send(self, body, title='', notify_type=NotifyType.INFO, attach=None,
|
||||
**kwargs):
|
||||
"""
|
||||
|
|
@ -263,33 +213,34 @@ class NotifyJSON(NotifyBase):
|
|||
# Track our potential attachments
|
||||
attachments = []
|
||||
if attach and self.attachment_support:
|
||||
for attachment in attach:
|
||||
for no, attachment in enumerate(attach, start=1):
|
||||
# Perform some simple error checking
|
||||
if not attachment:
|
||||
# We could not access the attachment
|
||||
self.logger.error(
|
||||
'Could not access attachment {}.'.format(
|
||||
'Could not access Custom JSON attachment {}.'.format(
|
||||
attachment.url(privacy=True)))
|
||||
return False
|
||||
|
||||
try:
|
||||
with open(attachment.path, 'rb') as f:
|
||||
# Output must be in a DataURL format (that's what
|
||||
# PushSafer calls it):
|
||||
attachments.append({
|
||||
'filename': attachment.name,
|
||||
'base64': base64.b64encode(f.read())
|
||||
.decode('utf-8'),
|
||||
'mimetype': attachment.mimetype,
|
||||
})
|
||||
attachments.append({
|
||||
"filename": attachment.name
|
||||
if attachment.name else f'file{no:03}.dat',
|
||||
'base64': attachment.base64(),
|
||||
'mimetype': attachment.mimetype,
|
||||
})
|
||||
|
||||
except (OSError, IOError) as e:
|
||||
self.logger.warning(
|
||||
'An I/O error occurred while reading {}.'.format(
|
||||
attachment.name if attachment else 'attachment'))
|
||||
self.logger.debug('I/O Exception: %s' % str(e))
|
||||
except exception.AppriseException:
|
||||
# We could not access the attachment
|
||||
self.logger.error(
|
||||
'Could not access Custom JSON attachment {}.'.format(
|
||||
attachment.url(privacy=True)))
|
||||
return False
|
||||
|
||||
self.logger.debug(
|
||||
'Appending Custom JSON attachment {}'.format(
|
||||
attachment.url(privacy=True)))
|
||||
|
||||
# Prepare JSON Object
|
||||
payload = {
|
||||
JSONPayloadField.VERSION: self.json_version,
|
||||
|
|
@ -395,6 +346,70 @@ class NotifyJSON(NotifyBase):
|
|||
|
||||
return True
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
return (
|
||||
self.secure_protocol if self.secure else self.protocol,
|
||||
self.user, self.password, self.host,
|
||||
self.port if self.port else (443 if self.secure else 80),
|
||||
self.fullpath.rstrip('/'),
|
||||
)
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any URL parameters
|
||||
params = {
|
||||
'method': self.method,
|
||||
}
|
||||
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
# Append our headers into our parameters
|
||||
params.update({'+{}'.format(k): v for k, v in self.headers.items()})
|
||||
|
||||
# Append our GET params into our parameters
|
||||
params.update({'-{}'.format(k): v for k, v in self.params.items()})
|
||||
|
||||
# Append our payload extra's into our parameters
|
||||
params.update(
|
||||
{':{}'.format(k): v for k, v in self.payload_extras.items()})
|
||||
|
||||
# Determine Authentication
|
||||
auth = ''
|
||||
if self.user and self.password:
|
||||
auth = '{user}:{password}@'.format(
|
||||
user=NotifyJSON.quote(self.user, safe=''),
|
||||
password=self.pprint(
|
||||
self.password, privacy, mode=PrivacyMode.Secret, safe=''),
|
||||
)
|
||||
elif self.user:
|
||||
auth = '{user}@'.format(
|
||||
user=NotifyJSON.quote(self.user, safe=''),
|
||||
)
|
||||
|
||||
default_port = 443 if self.secure else 80
|
||||
|
||||
return '{schema}://{auth}{hostname}{port}{fullpath}?{params}'.format(
|
||||
schema=self.secure_protocol if self.secure else self.protocol,
|
||||
auth=auth,
|
||||
# never encode hostname since we're expecting it to be a valid one
|
||||
hostname=self.host,
|
||||
port='' if self.port is None or self.port == default_port
|
||||
else ':{}'.format(self.port),
|
||||
fullpath=NotifyJSON.quote(self.fullpath, safe='/')
|
||||
if self.fullpath else '/',
|
||||
params=NotifyJSON.urlencode(params),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -28,8 +28,8 @@
|
|||
|
||||
import re
|
||||
import requests
|
||||
import base64
|
||||
|
||||
from .. import exception
|
||||
from .base import NotifyBase
|
||||
from ..url import PrivacyMode
|
||||
from ..common import NotifyImageSize
|
||||
|
|
@ -242,58 +242,6 @@ class NotifyXML(NotifyBase):
|
|||
|
||||
return
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any URL parameters
|
||||
params = {
|
||||
'method': self.method,
|
||||
}
|
||||
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
# Append our headers into our parameters
|
||||
params.update({'+{}'.format(k): v for k, v in self.headers.items()})
|
||||
|
||||
# Append our GET params into our parameters
|
||||
params.update({'-{}'.format(k): v for k, v in self.params.items()})
|
||||
|
||||
# Append our payload extra's into our parameters
|
||||
params.update(
|
||||
{':{}'.format(k): v for k, v in self.payload_extras.items()})
|
||||
params.update(
|
||||
{':{}'.format(k): v for k, v in self.payload_overrides.items()})
|
||||
|
||||
# Determine Authentication
|
||||
auth = ''
|
||||
if self.user and self.password:
|
||||
auth = '{user}:{password}@'.format(
|
||||
user=NotifyXML.quote(self.user, safe=''),
|
||||
password=self.pprint(
|
||||
self.password, privacy, mode=PrivacyMode.Secret, safe=''),
|
||||
)
|
||||
elif self.user:
|
||||
auth = '{user}@'.format(
|
||||
user=NotifyXML.quote(self.user, safe=''),
|
||||
)
|
||||
|
||||
default_port = 443 if self.secure else 80
|
||||
|
||||
return '{schema}://{auth}{hostname}{port}{fullpath}?{params}'.format(
|
||||
schema=self.secure_protocol if self.secure else self.protocol,
|
||||
auth=auth,
|
||||
# never encode hostname since we're expecting it to be a valid one
|
||||
hostname=self.host,
|
||||
port='' if self.port is None or self.port == default_port
|
||||
else ':{}'.format(self.port),
|
||||
fullpath=NotifyXML.quote(self.fullpath, safe='/')
|
||||
if self.fullpath else '/',
|
||||
params=NotifyXML.urlencode(params),
|
||||
)
|
||||
|
||||
def send(self, body, title='', notify_type=NotifyType.INFO, attach=None,
|
||||
**kwargs):
|
||||
"""
|
||||
|
|
@ -339,35 +287,39 @@ class NotifyXML(NotifyBase):
|
|||
|
||||
attachments = []
|
||||
if attach and self.attachment_support:
|
||||
for attachment in attach:
|
||||
for no, attachment in enumerate(attach, start=1):
|
||||
# Perform some simple error checking
|
||||
if not attachment:
|
||||
# We could not access the attachment
|
||||
self.logger.error(
|
||||
'Could not access attachment {}.'.format(
|
||||
'Could not access Custom XML attachment {}.'.format(
|
||||
attachment.url(privacy=True)))
|
||||
return False
|
||||
|
||||
try:
|
||||
with open(attachment.path, 'rb') as f:
|
||||
# Prepare our Attachment in Base64
|
||||
entry = \
|
||||
'<Attachment filename="{}" mimetype="{}">'.format(
|
||||
NotifyXML.escape_html(
|
||||
attachment.name, whitespace=False),
|
||||
NotifyXML.escape_html(
|
||||
attachment.mimetype, whitespace=False))
|
||||
entry += base64.b64encode(f.read()).decode('utf-8')
|
||||
entry += '</Attachment>'
|
||||
attachments.append(entry)
|
||||
# Prepare our Attachment in Base64
|
||||
entry = \
|
||||
'<Attachment filename="{}" mimetype="{}">'.format(
|
||||
NotifyXML.escape_html(
|
||||
attachment.name if attachment.name
|
||||
else f'file{no:03}.dat', whitespace=False),
|
||||
NotifyXML.escape_html(
|
||||
attachment.mimetype, whitespace=False))
|
||||
entry += attachment.base64()
|
||||
entry += '</Attachment>'
|
||||
attachments.append(entry)
|
||||
|
||||
except (OSError, IOError) as e:
|
||||
self.logger.warning(
|
||||
'An I/O error occurred while reading {}.'.format(
|
||||
attachment.name if attachment else 'attachment'))
|
||||
self.logger.debug('I/O Exception: %s' % str(e))
|
||||
except exception.AppriseException:
|
||||
# We could not access the attachment
|
||||
self.logger.error(
|
||||
'Could not access Custom XML attachment {}.'.format(
|
||||
attachment.url(privacy=True)))
|
||||
return False
|
||||
|
||||
self.logger.debug(
|
||||
'Appending Custom XML attachment {}'.format(
|
||||
attachment.url(privacy=True)))
|
||||
|
||||
# Update our xml_attachments record:
|
||||
xml_attachments = \
|
||||
'<Attachments format="base64">' + \
|
||||
|
|
@ -467,6 +419,72 @@ class NotifyXML(NotifyBase):
|
|||
|
||||
return True
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
return (
|
||||
self.secure_protocol if self.secure else self.protocol,
|
||||
self.user, self.password, self.host,
|
||||
self.port if self.port else (443 if self.secure else 80),
|
||||
self.fullpath.rstrip('/'),
|
||||
)
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any URL parameters
|
||||
params = {
|
||||
'method': self.method,
|
||||
}
|
||||
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
# Append our headers into our parameters
|
||||
params.update({'+{}'.format(k): v for k, v in self.headers.items()})
|
||||
|
||||
# Append our GET params into our parameters
|
||||
params.update({'-{}'.format(k): v for k, v in self.params.items()})
|
||||
|
||||
# Append our payload extra's into our parameters
|
||||
params.update(
|
||||
{':{}'.format(k): v for k, v in self.payload_extras.items()})
|
||||
params.update(
|
||||
{':{}'.format(k): v for k, v in self.payload_overrides.items()})
|
||||
|
||||
# Determine Authentication
|
||||
auth = ''
|
||||
if self.user and self.password:
|
||||
auth = '{user}:{password}@'.format(
|
||||
user=NotifyXML.quote(self.user, safe=''),
|
||||
password=self.pprint(
|
||||
self.password, privacy, mode=PrivacyMode.Secret, safe=''),
|
||||
)
|
||||
elif self.user:
|
||||
auth = '{user}@'.format(
|
||||
user=NotifyXML.quote(self.user, safe=''),
|
||||
)
|
||||
|
||||
default_port = 443 if self.secure else 80
|
||||
|
||||
return '{schema}://{auth}{hostname}{port}{fullpath}?{params}'.format(
|
||||
schema=self.secure_protocol if self.secure else self.protocol,
|
||||
auth=auth,
|
||||
# never encode hostname since we're expecting it to be a valid one
|
||||
hostname=self.host,
|
||||
port='' if self.port is None or self.port == default_port
|
||||
else ':{}'.format(self.port),
|
||||
fullpath=NotifyXML.quote(self.fullpath, safe='/')
|
||||
if self.fullpath else '/',
|
||||
params=NotifyXML.urlencode(params),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -41,10 +41,8 @@ from json import loads
|
|||
|
||||
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 ..utils import parse_bool
|
||||
from ..utils.parse import (
|
||||
is_phone_no, parse_phone_no, validate_regex, parse_bool)
|
||||
from ..locale import gettext_lazy as _
|
||||
|
||||
# Extend HTTP Error Messages
|
||||
|
|
@ -354,6 +352,15 @@ class NotifyD7Networks(NotifyBase):
|
|||
[NotifyD7Networks.quote(x, safe='') for x in self.targets]),
|
||||
params=NotifyD7Networks.urlencode(params))
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
return (self.secure_protocol, self.token)
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Returns the number of targets associated with this notification
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -55,10 +55,8 @@ 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 ..utils import parse_list
|
||||
from ..utils import parse_bool
|
||||
from ..utils.parse import (
|
||||
is_call_sign, parse_call_sign, parse_list, parse_bool)
|
||||
|
||||
|
||||
class DapnetPriority:
|
||||
|
|
@ -346,6 +344,15 @@ class NotifyDapnet(NotifyBase):
|
|||
params=NotifyDapnet.urlencode(params),
|
||||
)
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
return (self.secure_protocol, self.user, self.password)
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Returns the number of targets associated with this notification
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -30,7 +30,7 @@ import sys
|
|||
from .base import NotifyBase
|
||||
from ..common import NotifyImageSize
|
||||
from ..common import NotifyType
|
||||
from ..utils import parse_bool
|
||||
from ..utils.parse import parse_bool
|
||||
from ..locale import gettext_lazy as _
|
||||
|
||||
# Default our global support flag
|
||||
|
|
@ -174,7 +174,6 @@ class NotifyDBus(NotifyBase):
|
|||
# object if we were to reference, we wouldn't be backwards compatible with
|
||||
# Python v2. So converting the result set back into a list makes us
|
||||
# compatible
|
||||
# TODO: Review after dropping support for Python 2.
|
||||
protocol = list(MAINLOOP_MAP.keys())
|
||||
|
||||
# A URL that takes you to the setup/help of the specific protocol
|
||||
|
|
@ -197,6 +196,10 @@ class NotifyDBus(NotifyBase):
|
|||
dbus_interface = 'org.freedesktop.Notifications'
|
||||
dbus_setting_location = '/org/freedesktop/Notifications'
|
||||
|
||||
# No URL Identifier will be defined for this service as there simply isn't
|
||||
# enough details to uniquely identify one dbus:// from another.
|
||||
url_identifier = False
|
||||
|
||||
# Define object templates
|
||||
templates = (
|
||||
'{schema}://',
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -38,8 +38,7 @@ from .base import NotifyBase
|
|||
from ..url import PrivacyMode
|
||||
from ..common import NotifyFormat
|
||||
from ..common import NotifyType
|
||||
from ..utils import parse_list
|
||||
from ..utils import validate_regex
|
||||
from ..utils.parse import parse_list, validate_regex
|
||||
from ..locale import gettext_lazy as _
|
||||
|
||||
# Register at https://dingtalk.com
|
||||
|
|
@ -310,6 +309,15 @@ class NotifyDingTalk(NotifyBase):
|
|||
[NotifyDingTalk.quote(x, safe='') for x in self.targets]),
|
||||
args=NotifyDingTalk.urlencode(args))
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
return (self.secure_protocol, self.secret, self.token)
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Returns the number of targets associated with this notification
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -54,8 +54,7 @@ from .base import NotifyBase
|
|||
from ..common import NotifyImageSize
|
||||
from ..common import NotifyFormat
|
||||
from ..common import NotifyType
|
||||
from ..utils import parse_bool
|
||||
from ..utils import validate_regex
|
||||
from ..utils.parse import parse_bool, validate_regex
|
||||
from ..locale import gettext_lazy as _
|
||||
from ..attachment.base import AttachBase
|
||||
|
||||
|
|
@ -597,15 +596,30 @@ class NotifyDiscord(NotifyBase):
|
|||
if self.thread_id:
|
||||
params['thread'] = self.thread_id
|
||||
|
||||
# Ensure our botname is set
|
||||
botname = f'{self.user}@' if self.user else ''
|
||||
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
return '{schema}://{webhook_id}/{webhook_token}/?{params}'.format(
|
||||
schema=self.secure_protocol,
|
||||
webhook_id=self.pprint(self.webhook_id, privacy, safe=''),
|
||||
webhook_token=self.pprint(self.webhook_token, privacy, safe=''),
|
||||
params=NotifyDiscord.urlencode(params),
|
||||
)
|
||||
return '{schema}://{botname}{webhook_id}/{webhook_token}/?{params}' \
|
||||
.format(
|
||||
schema=self.secure_protocol,
|
||||
botname=botname,
|
||||
webhook_id=self.pprint(self.webhook_id, privacy, safe=''),
|
||||
webhook_token=self.pprint(
|
||||
self.webhook_token, privacy, safe=''),
|
||||
params=NotifyDiscord.urlencode(params),
|
||||
)
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
return (self.secure_protocol, self.webhook_id, self.webhook_token)
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
|
|
@ -660,6 +674,11 @@ class NotifyDiscord(NotifyBase):
|
|||
results['include_image'] = parse_bool(results['qsd'].get(
|
||||
'image', NotifyDiscord.template_args['image']['default']))
|
||||
|
||||
if 'botname' in results['qsd']:
|
||||
# Alias to User
|
||||
results['user'] = \
|
||||
NotifyDiscord.unquote(results['qsd']['botname'])
|
||||
|
||||
# Extract avatar url if it was specified
|
||||
if 'avatar_url' in results['qsd']:
|
||||
results['avatar_url'] = \
|
||||
|
|
|
|||
53
lib/apprise/plugins/email/__init__.py
Normal file
53
lib/apprise/plugins/email/__init__.py
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2025, 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.
|
||||
|
||||
from email import charset
|
||||
|
||||
from .base import NotifyEmail
|
||||
from .common import (
|
||||
AppriseEmailException, EmailMessage, SecureMailMode, SECURE_MODES,
|
||||
WebBaseLogin)
|
||||
from .templates import EMAIL_TEMPLATES
|
||||
|
||||
# Globally Default encoding mode set to Quoted Printable.
|
||||
charset.add_charset('utf-8', charset.QP, charset.QP, 'utf-8')
|
||||
|
||||
__all__ = [
|
||||
# Reference
|
||||
'NotifyEmail',
|
||||
|
||||
# Pretty Good Privacy
|
||||
'ApprisePGPController', 'ApprisePGPException',
|
||||
|
||||
# Other
|
||||
'AppriseEmailException', 'EmailMessage', 'SecureMailMode', 'SECURE_MODES',
|
||||
'WebBaseLogin',
|
||||
|
||||
# Additional entries that may be useful to some developers
|
||||
'EMAIL_TEMPLATES', 'PGP_SUPPORT',
|
||||
]
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -26,312 +26,32 @@
|
|||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import dataclasses
|
||||
import re
|
||||
import smtplib
|
||||
import typing as t
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.application import MIMEApplication
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.base import MIMEBase
|
||||
from email.utils import formataddr, make_msgid
|
||||
from email.header import Header
|
||||
from email import charset
|
||||
|
||||
from socket import error as SocketError
|
||||
from datetime import datetime
|
||||
from datetime import timezone
|
||||
|
||||
from .base import NotifyBase
|
||||
from ..url import PrivacyMode
|
||||
from ..common import NotifyFormat, NotifyType
|
||||
from ..conversion import convert_between
|
||||
from ..utils import is_ipaddr, is_email, parse_emails, is_hostname
|
||||
from ..locale import gettext_lazy as _
|
||||
from ..logger import logger
|
||||
|
||||
# Globally Default encoding mode set to Quoted Printable.
|
||||
charset.add_charset('utf-8', charset.QP, charset.QP, 'utf-8')
|
||||
|
||||
|
||||
class WebBaseLogin:
|
||||
"""
|
||||
This class is just used in conjunction of the default emailers
|
||||
to best formulate a login to it using the data detected
|
||||
"""
|
||||
# User Login must be Email Based
|
||||
EMAIL = 'Email'
|
||||
|
||||
# User Login must UserID Based
|
||||
USERID = 'UserID'
|
||||
|
||||
|
||||
# Secure Email Modes
|
||||
class SecureMailMode:
|
||||
INSECURE = "insecure"
|
||||
SSL = "ssl"
|
||||
STARTTLS = "starttls"
|
||||
|
||||
|
||||
# Define all of the secure modes (used during validation)
|
||||
SECURE_MODES = {
|
||||
SecureMailMode.STARTTLS: {
|
||||
'default_port': 587,
|
||||
},
|
||||
SecureMailMode.SSL: {
|
||||
'default_port': 465,
|
||||
},
|
||||
SecureMailMode.INSECURE: {
|
||||
'default_port': 25,
|
||||
},
|
||||
}
|
||||
|
||||
# To attempt to make this script stupid proof, if we detect an email address
|
||||
# that is part of the this table, we can pre-use a lot more defaults if they
|
||||
# aren't otherwise specified on the users input.
|
||||
EMAIL_TEMPLATES = (
|
||||
# Google GMail
|
||||
(
|
||||
'Google Mail',
|
||||
re.compile(
|
||||
r'^((?P<label>[^+]+)\+)?(?P<id>[^@]+)@'
|
||||
r'(?P<domain>gmail\.com)$', re.I),
|
||||
{
|
||||
'port': 587,
|
||||
'smtp_host': 'smtp.gmail.com',
|
||||
'secure': True,
|
||||
'secure_mode': SecureMailMode.STARTTLS,
|
||||
'login_type': (WebBaseLogin.EMAIL, )
|
||||
},
|
||||
),
|
||||
|
||||
# Yandex
|
||||
(
|
||||
'Yandex',
|
||||
re.compile(
|
||||
r'^((?P<label>[^+]+)\+)?(?P<id>[^@]+)@'
|
||||
r'(?P<domain>yandex\.(com|ru|ua|by|kz|uz|tr|fr))$', re.I),
|
||||
{
|
||||
'port': 465,
|
||||
'smtp_host': 'smtp.yandex.ru',
|
||||
'secure': True,
|
||||
'secure_mode': SecureMailMode.SSL,
|
||||
'login_type': (WebBaseLogin.USERID, )
|
||||
},
|
||||
),
|
||||
|
||||
# Microsoft Hotmail
|
||||
(
|
||||
'Microsoft Hotmail',
|
||||
re.compile(
|
||||
r'^((?P<label>[^+]+)\+)?(?P<id>[^@]+)@'
|
||||
r'(?P<domain>(hotmail|live)\.com(\.au)?)$', re.I),
|
||||
{
|
||||
'port': 587,
|
||||
'smtp_host': 'smtp-mail.outlook.com',
|
||||
'secure': True,
|
||||
'secure_mode': SecureMailMode.STARTTLS,
|
||||
'login_type': (WebBaseLogin.EMAIL, )
|
||||
},
|
||||
),
|
||||
|
||||
# Microsoft Outlook
|
||||
(
|
||||
'Microsoft Outlook',
|
||||
re.compile(
|
||||
r'^((?P<label>[^+]+)\+)?(?P<id>[^@]+)@'
|
||||
r'(?P<domain>(smtp\.)?outlook\.com(\.au)?)$', re.I),
|
||||
{
|
||||
'port': 587,
|
||||
'smtp_host': 'smtp.outlook.com',
|
||||
'secure': True,
|
||||
'secure_mode': SecureMailMode.STARTTLS,
|
||||
'login_type': (WebBaseLogin.EMAIL, )
|
||||
},
|
||||
),
|
||||
|
||||
# Microsoft Office 365 (Email Server)
|
||||
# You must specify an authenticated sender address in the from= settings
|
||||
# and a valid email in the to= to deliver your emails to
|
||||
(
|
||||
'Microsoft Office 365',
|
||||
re.compile(
|
||||
r'^[^@]+@(?P<domain>(smtp\.)?office365\.com)$', re.I),
|
||||
{
|
||||
'port': 587,
|
||||
'smtp_host': 'smtp.office365.com',
|
||||
'secure': True,
|
||||
'secure_mode': SecureMailMode.STARTTLS,
|
||||
},
|
||||
),
|
||||
|
||||
# Yahoo Mail
|
||||
(
|
||||
'Yahoo Mail',
|
||||
re.compile(
|
||||
r'^((?P<label>[^+]+)\+)?(?P<id>[^@]+)@'
|
||||
r'(?P<domain>yahoo\.(ca|com))$', re.I),
|
||||
{
|
||||
'port': 465,
|
||||
'smtp_host': 'smtp.mail.yahoo.com',
|
||||
'secure': True,
|
||||
'secure_mode': SecureMailMode.STARTTLS,
|
||||
'login_type': (WebBaseLogin.EMAIL, )
|
||||
},
|
||||
),
|
||||
|
||||
# Fast Mail (Series 1)
|
||||
(
|
||||
'Fast Mail',
|
||||
re.compile(
|
||||
r'^((?P<label>[^+]+)\+)?(?P<id>[^@]+)@'
|
||||
r'(?P<domain>fastmail\.(com|cn|co\.uk|com\.au|de|es|fm|fr|im|'
|
||||
r'in|jp|mx|net|nl|org|se|to|tw|uk|us))$', re.I),
|
||||
{
|
||||
'port': 465,
|
||||
'smtp_host': 'smtp.fastmail.com',
|
||||
'secure': True,
|
||||
'secure_mode': SecureMailMode.SSL,
|
||||
'login_type': (WebBaseLogin.EMAIL, )
|
||||
},
|
||||
),
|
||||
|
||||
# Fast Mail (Series 2)
|
||||
(
|
||||
'Fast Mail Extended Addresses',
|
||||
re.compile(
|
||||
r'^((?P<label>[^+]+)\+)?(?P<id>[^@]+)@'
|
||||
r'(?P<domain>123mail\.org|airpost\.net|eml\.cc|fmail\.co\.uk|'
|
||||
r'fmgirl\.com|fmguy\.com|mailbolt\.com|mailcan\.com|'
|
||||
r'mailhaven\.com|mailmight\.com|ml1\.net|mm\.st|myfastmail\.com|'
|
||||
r'proinbox\.com|promessage\.com|rushpost\.com|sent\.(as|at|com)|'
|
||||
r'speedymail\.org|warpmail\.net|xsmail\.com|150mail\.com|'
|
||||
r'150ml\.com|16mail\.com|2-mail\.com|4email\.net|50mail\.com|'
|
||||
r'allmail\.net|bestmail\.us|cluemail\.com|elitemail\.org|'
|
||||
r'emailcorner\.net|emailengine\.(net|org)|emailgroups\.net|'
|
||||
r'emailplus\.org|emailuser\.net|f-m\.fm|fast-email\.com|'
|
||||
r'fast-mail\.org|fastem\.com|fastemail\.us|fastemailer\.com|'
|
||||
r'fastest\.cc|fastimap\.com|fastmailbox\.net|fastmessaging\.com|'
|
||||
r'fea\.st|fmailbox\.com|ftml\.net|h-mail\.us|hailmail\.net|'
|
||||
r'imap-mail\.com|imap\.cc|imapmail\.org|inoutbox\.com|'
|
||||
r'internet-e-mail\.com|internet-mail\.org|internetemails\.net|'
|
||||
r'internetmailing\.net|jetemail\.net|justemail\.net|'
|
||||
r'letterboxes\.org|mail-central\.com|mail-page\.com|'
|
||||
r'mailandftp\.com|mailas\.com|mailc\.net|mailforce\.net|'
|
||||
r'mailftp\.com|mailingaddress\.org|mailite\.com|mailnew\.com|'
|
||||
r'mailsent\.net|mailservice\.ms|mailup\.net|mailworks\.org|'
|
||||
r'mymacmail\.com|nospammail\.net|ownmail\.net|petml\.com|'
|
||||
r'postinbox\.com|postpro\.net|realemail\.net|reallyfast\.biz|'
|
||||
r'reallyfast\.info|speedpost\.net|ssl-mail\.com|swift-mail\.com|'
|
||||
r'the-fastest\.net|the-quickest\.com|theinternetemail\.com|'
|
||||
r'veryfast\.biz|veryspeedy\.net|yepmail\.net)$', re.I),
|
||||
{
|
||||
'port': 465,
|
||||
'smtp_host': 'smtp.fastmail.com',
|
||||
'secure': True,
|
||||
'secure_mode': SecureMailMode.SSL,
|
||||
'login_type': (WebBaseLogin.EMAIL, )
|
||||
},
|
||||
),
|
||||
|
||||
# Zoho Mail (Free)
|
||||
(
|
||||
'Zoho Mail',
|
||||
re.compile(
|
||||
r'^((?P<label>[^+]+)\+)?(?P<id>[^@]+)@'
|
||||
r'(?P<domain>zoho(mail)?\.com)$', re.I),
|
||||
{
|
||||
'port': 587,
|
||||
'smtp_host': 'smtp.zoho.com',
|
||||
'secure': True,
|
||||
'secure_mode': SecureMailMode.STARTTLS,
|
||||
'login_type': (WebBaseLogin.EMAIL, )
|
||||
},
|
||||
),
|
||||
|
||||
# SendGrid (Email Server)
|
||||
# You must specify an authenticated sender address in the from= settings
|
||||
# and a valid email in the to= to deliver your emails to
|
||||
(
|
||||
'SendGrid',
|
||||
re.compile(
|
||||
r'^((?P<label>[^+]+)\+)?(?P<id>[^@]+)@'
|
||||
r'(?P<domain>(\.smtp)?sendgrid\.(com|net))$', re.I),
|
||||
{
|
||||
'port': 465,
|
||||
'smtp_host': 'smtp.sendgrid.net',
|
||||
'secure': True,
|
||||
'secure_mode': SecureMailMode.SSL,
|
||||
'login_type': (WebBaseLogin.USERID, )
|
||||
},
|
||||
),
|
||||
|
||||
# 163.com
|
||||
(
|
||||
'163.com',
|
||||
re.compile(
|
||||
r'^((?P<label>[^+]+)\+)?(?P<id>[^@]+)@'
|
||||
r'(?P<domain>163\.com)$', re.I),
|
||||
{
|
||||
'port': 465,
|
||||
'smtp_host': 'smtp.163.com',
|
||||
'secure': True,
|
||||
'secure_mode': SecureMailMode.SSL,
|
||||
'login_type': (WebBaseLogin.EMAIL, )
|
||||
},
|
||||
),
|
||||
|
||||
# Foxmail.com
|
||||
(
|
||||
'Foxmail.com',
|
||||
re.compile(
|
||||
r'^((?P<label>[^+]+)\+)?(?P<id>[^@]+)@'
|
||||
r'(?P<domain>(foxmail|qq)\.com)$', re.I),
|
||||
{
|
||||
'port': 587,
|
||||
'smtp_host': 'smtp.qq.com',
|
||||
'secure': True,
|
||||
'secure_mode': SecureMailMode.STARTTLS,
|
||||
'login_type': (WebBaseLogin.EMAIL, )
|
||||
},
|
||||
),
|
||||
|
||||
# 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
|
||||
(
|
||||
'Custom',
|
||||
re.compile(
|
||||
r'^((?P<label>[^+]+)\+)?(?P<id>[^@]+)@'
|
||||
r'(?P<domain>.+)$', re.I),
|
||||
{
|
||||
# Setting smtp_host to None is a way of
|
||||
# auto-detecting it based on other parameters
|
||||
# specified. There is no reason to ever modify
|
||||
# this Catch All
|
||||
'smtp_host': None,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class EmailMessage:
|
||||
recipient: str
|
||||
to_addrs: t.List[str]
|
||||
body: str
|
||||
from ..base import NotifyBase
|
||||
from ...url import PrivacyMode
|
||||
from ...common import NotifyFormat, NotifyType
|
||||
from ...conversion import convert_between
|
||||
from ...utils import pgp as _pgp
|
||||
from ...utils.parse import (
|
||||
is_ipaddr, is_email, parse_emails, is_hostname, parse_bool)
|
||||
from ...locale import gettext_lazy as _
|
||||
from ...logger import logger
|
||||
from .common import (
|
||||
AppriseEmailException, EmailMessage, SecureMailMode, SECURE_MODES,
|
||||
WebBaseLogin)
|
||||
from . import templates
|
||||
|
||||
|
||||
class NotifyEmail(NotifyBase):
|
||||
|
|
@ -451,6 +171,20 @@ class NotifyEmail(NotifyBase):
|
|||
'type': 'list:string',
|
||||
'map_to': 'reply_to',
|
||||
},
|
||||
'pgp': {
|
||||
'name': _('PGP Encryption'),
|
||||
'type': 'bool',
|
||||
'map_to': 'use_pgp',
|
||||
'default': False,
|
||||
},
|
||||
'pgpkey': {
|
||||
'name': _('PGP Public Key Path'),
|
||||
'type': 'string',
|
||||
'private': True,
|
||||
# By default persistent storage is referenced
|
||||
'default': '',
|
||||
'map_to': 'pgp_key',
|
||||
},
|
||||
})
|
||||
|
||||
# Define any kwargs we're using
|
||||
|
|
@ -463,7 +197,7 @@ class NotifyEmail(NotifyBase):
|
|||
|
||||
def __init__(self, smtp_host=None, from_addr=None, secure_mode=None,
|
||||
targets=None, cc=None, bcc=None, reply_to=None, headers=None,
|
||||
**kwargs):
|
||||
use_pgp=None, pgp_key=None, **kwargs):
|
||||
"""
|
||||
Initialize Email Object
|
||||
|
||||
|
|
@ -564,7 +298,7 @@ class NotifyEmail(NotifyBase):
|
|||
)
|
||||
|
||||
# Apply any defaults based on certain known configurations
|
||||
self.NotifyEmailDefaults(secure_mode=secure_mode, **kwargs)
|
||||
self.apply_email_defaults(secure_mode=secure_mode, **kwargs)
|
||||
|
||||
if self.user:
|
||||
if self.host:
|
||||
|
|
@ -635,9 +369,25 @@ class NotifyEmail(NotifyBase):
|
|||
if not self.smtp_host:
|
||||
self.smtp_host = self.host
|
||||
|
||||
# Prepare our Pretty Good Privacy Object
|
||||
self.pgp = _pgp.ApprisePGPController(
|
||||
path=self.store.path, pub_keyfile=pgp_key,
|
||||
email=self.from_addr[1], asset=self.asset)
|
||||
|
||||
# We store so we can generate a URL later on
|
||||
self.pgp_key = pgp_key
|
||||
|
||||
self.use_pgp = use_pgp if not None \
|
||||
else self.template_args['pgp']['default']
|
||||
|
||||
if self.use_pgp and not _pgp.PGP_SUPPORT:
|
||||
self.logger.warning(
|
||||
'PGP Support is not available on this installation; '
|
||||
'ask admin to install PGPy')
|
||||
|
||||
return
|
||||
|
||||
def NotifyEmailDefaults(self, secure_mode=None, port=None, **kwargs):
|
||||
def apply_email_defaults(self, secure_mode=None, port=None, **kwargs):
|
||||
"""
|
||||
A function that prefills defaults based on the email
|
||||
it was provided.
|
||||
|
|
@ -656,39 +406,40 @@ class NotifyEmail(NotifyBase):
|
|||
self.host,
|
||||
)
|
||||
|
||||
for i in range(len(EMAIL_TEMPLATES)): # pragma: no branch
|
||||
for i in range(len(templates.EMAIL_TEMPLATES)): # pragma: no branch
|
||||
self.logger.trace('Scanning %s against %s' % (
|
||||
from_addr, EMAIL_TEMPLATES[i][0]
|
||||
from_addr, templates.EMAIL_TEMPLATES[i][0]
|
||||
))
|
||||
match = EMAIL_TEMPLATES[i][1].match(from_addr)
|
||||
match = templates.EMAIL_TEMPLATES[i][1].match(from_addr)
|
||||
if match:
|
||||
self.logger.info(
|
||||
'Applying %s Defaults' %
|
||||
EMAIL_TEMPLATES[i][0],
|
||||
templates.EMAIL_TEMPLATES[i][0],
|
||||
)
|
||||
# the secure flag can not be altered if defined in the template
|
||||
self.secure = EMAIL_TEMPLATES[i][2]\
|
||||
self.secure = templates.EMAIL_TEMPLATES[i][2]\
|
||||
.get('secure', self.secure)
|
||||
|
||||
# The SMTP Host check is already done above; if it was
|
||||
# specified we wouldn't even reach this part of the code.
|
||||
self.smtp_host = EMAIL_TEMPLATES[i][2]\
|
||||
self.smtp_host = templates.EMAIL_TEMPLATES[i][2]\
|
||||
.get('smtp_host', self.smtp_host)
|
||||
|
||||
# The following can be over-ridden if defined manually in the
|
||||
# Apprise URL. Otherwise they take on the template value
|
||||
if not port:
|
||||
self.port = EMAIL_TEMPLATES[i][2]\
|
||||
self.port = templates.EMAIL_TEMPLATES[i][2]\
|
||||
.get('port', self.port)
|
||||
if not secure_mode:
|
||||
self.secure_mode = EMAIL_TEMPLATES[i][2]\
|
||||
self.secure_mode = templates.EMAIL_TEMPLATES[i][2]\
|
||||
.get('secure_mode', self.secure_mode)
|
||||
|
||||
# Adjust email login based on the defined usertype. If no entry
|
||||
# was specified, then we default to having them all set (which
|
||||
# basically implies that there are no restrictions and use use
|
||||
# whatever was specified)
|
||||
login_type = EMAIL_TEMPLATES[i][2].get('login_type', [])
|
||||
login_type = \
|
||||
templates.EMAIL_TEMPLATES[i][2].get('login_type', [])
|
||||
if login_type:
|
||||
# only apply additional logic to our user if a login_type
|
||||
# was specified.
|
||||
|
|
@ -709,153 +460,14 @@ class NotifyEmail(NotifyBase):
|
|||
|
||||
break
|
||||
|
||||
def _get_charset(self, input_string):
|
||||
"""
|
||||
Get utf-8 charset if non ascii string only
|
||||
|
||||
Encode an ascii string to utf-8 is bad for email deliverability
|
||||
because some anti-spam gives a bad score for that
|
||||
like SUBJ_EXCESS_QP flag on Rspamd
|
||||
"""
|
||||
if not input_string:
|
||||
return None
|
||||
return 'utf-8' if not all(ord(c) < 128 for c in input_string) else None
|
||||
|
||||
def send(self, body, title='', notify_type=NotifyType.INFO, attach=None,
|
||||
**kwargs):
|
||||
"""
|
||||
Perform Email Notification
|
||||
"""
|
||||
|
||||
if not self.targets:
|
||||
# There is no one to email; we're done
|
||||
self.logger.warning(
|
||||
'There are no Email recipients to notify')
|
||||
logger.warning('There are no Email recipients to notify')
|
||||
return False
|
||||
|
||||
messages: t.List[EmailMessage] = []
|
||||
|
||||
# Create a copy of the targets list
|
||||
emails = list(self.targets)
|
||||
while len(emails):
|
||||
# Get our email to notify
|
||||
to_name, to_addr = emails.pop(0)
|
||||
|
||||
# Strip target out of cc list if in To or Bcc
|
||||
cc = (self.cc - self.bcc - set([to_addr]))
|
||||
|
||||
# Strip target out of bcc list if in To
|
||||
bcc = (self.bcc - set([to_addr]))
|
||||
|
||||
# Strip target out of reply_to list if in To
|
||||
reply_to = (self.reply_to - set([to_addr]))
|
||||
|
||||
# Format our cc addresses to support the Name field
|
||||
cc = [formataddr(
|
||||
(self.names.get(addr, False), addr), charset='utf-8')
|
||||
for addr in cc]
|
||||
|
||||
# Format our bcc addresses to support the Name field
|
||||
bcc = [formataddr(
|
||||
(self.names.get(addr, False), addr), charset='utf-8')
|
||||
for addr in bcc]
|
||||
|
||||
if reply_to:
|
||||
# Format our reply-to addresses to support the Name field
|
||||
reply_to = [formataddr(
|
||||
(self.names.get(addr, False), addr), charset='utf-8')
|
||||
for addr in reply_to]
|
||||
|
||||
self.logger.debug(
|
||||
'Email From: {}'.format(
|
||||
formataddr(self.from_addr, charset='utf-8')))
|
||||
|
||||
self.logger.debug('Email To: {}'.format(to_addr))
|
||||
if cc:
|
||||
self.logger.debug('Email Cc: {}'.format(', '.join(cc)))
|
||||
if bcc:
|
||||
self.logger.debug('Email Bcc: {}'.format(', '.join(bcc)))
|
||||
if reply_to:
|
||||
self.logger.debug(
|
||||
'Email Reply-To: {}'.format(', '.join(reply_to))
|
||||
)
|
||||
self.logger.debug('Login ID: {}'.format(self.user))
|
||||
self.logger.debug(
|
||||
'Delivery: {}:{}'.format(self.smtp_host, self.port))
|
||||
|
||||
# Prepare Email Message
|
||||
if self.notify_format == NotifyFormat.HTML:
|
||||
base = MIMEMultipart("alternative")
|
||||
base.attach(MIMEText(
|
||||
convert_between(
|
||||
NotifyFormat.HTML, NotifyFormat.TEXT, body),
|
||||
'plain', 'utf-8')
|
||||
)
|
||||
base.attach(MIMEText(body, 'html', 'utf-8'))
|
||||
else:
|
||||
base = MIMEText(body, 'plain', 'utf-8')
|
||||
|
||||
if attach and self.attachment_support:
|
||||
mixed = MIMEMultipart("mixed")
|
||||
mixed.attach(base)
|
||||
# Now store our attachments
|
||||
for attachment in attach:
|
||||
if not attachment:
|
||||
# We could not load the attachment; take an early
|
||||
# exit since this isn't what the end user wanted
|
||||
|
||||
# We could not access the attachment
|
||||
self.logger.error(
|
||||
'Could not access attachment {}.'.format(
|
||||
attachment.url(privacy=True)))
|
||||
|
||||
return False
|
||||
|
||||
self.logger.debug(
|
||||
'Preparing Email attachment {}'.format(
|
||||
attachment.url(privacy=True)))
|
||||
|
||||
with open(attachment.path, "rb") as abody:
|
||||
app = MIMEApplication(abody.read())
|
||||
app.set_type(attachment.mimetype)
|
||||
|
||||
app.add_header(
|
||||
'Content-Disposition',
|
||||
'attachment; filename="{}"'.format(
|
||||
Header(attachment.name, 'utf-8')),
|
||||
)
|
||||
mixed.attach(app)
|
||||
base = mixed
|
||||
|
||||
# Apply any provided custom headers
|
||||
for k, v in self.headers.items():
|
||||
base[k] = Header(v, self._get_charset(v))
|
||||
|
||||
base['Subject'] = Header(title, self._get_charset(title))
|
||||
base['From'] = formataddr(self.from_addr, charset='utf-8')
|
||||
base['To'] = formataddr((to_name, to_addr), charset='utf-8')
|
||||
base['Message-ID'] = make_msgid(domain=self.smtp_host)
|
||||
base['Date'] = \
|
||||
datetime.now(timezone.utc)\
|
||||
.strftime("%a, %d %b %Y %H:%M:%S +0000")
|
||||
base['X-Application'] = self.app_id
|
||||
|
||||
if cc:
|
||||
base['Cc'] = ','.join(cc)
|
||||
|
||||
if reply_to:
|
||||
base['Reply-To'] = ','.join(reply_to)
|
||||
|
||||
message = EmailMessage(
|
||||
recipient=to_addr,
|
||||
to_addrs=[to_addr] + list(cc) + list(bcc),
|
||||
body=base.as_string())
|
||||
messages.append(message)
|
||||
|
||||
return self.submit(messages)
|
||||
|
||||
def submit(self, messages: t.List[EmailMessage]):
|
||||
|
||||
# error tracking (used for function return)
|
||||
has_error = False
|
||||
|
||||
|
|
@ -884,42 +496,66 @@ class NotifyEmail(NotifyBase):
|
|||
self.logger.debug('Securing connection with STARTTLS...')
|
||||
socket.starttls()
|
||||
|
||||
self.logger.trace('Login ID: {}'.format(self.user))
|
||||
if self.user and self.password:
|
||||
# Apply Login credetials
|
||||
self.logger.debug('Applying user credentials...')
|
||||
socket.login(self.user, self.password)
|
||||
|
||||
# Send the emails
|
||||
for message in messages:
|
||||
# Prepare our headers
|
||||
headers = {
|
||||
'X-Application': self.app_id,
|
||||
}
|
||||
headers.update(self.headers)
|
||||
|
||||
# Iterate over our email messages we can generate and then
|
||||
# send them off.
|
||||
for message in NotifyEmail.prepare_emails(
|
||||
subject=title, body=body, notify_format=self.notify_format,
|
||||
from_addr=self.from_addr, to=self.targets,
|
||||
cc=self.cc, bcc=self.bcc, reply_to=self.reply_to,
|
||||
smtp_host=self.smtp_host,
|
||||
attach=attach, headers=headers, names=self.names,
|
||||
pgp=self.pgp if self.use_pgp else None):
|
||||
try:
|
||||
socket.sendmail(
|
||||
self.from_addr[1],
|
||||
message.to_addrs,
|
||||
message.body)
|
||||
|
||||
self.logger.info(
|
||||
f'Sent Email notification to "{message.recipient}".')
|
||||
self.logger.info('Sent Email to %s', message.recipient)
|
||||
|
||||
except (SocketError, smtplib.SMTPException, RuntimeError) as e:
|
||||
self.logger.warning(
|
||||
f'Sending email to "{message.recipient}" failed. '
|
||||
f'Reason: {e}')
|
||||
'Sending email to "%s" failed.', message.recipient)
|
||||
self.logger.debug(f'Socket Exception: {e}')
|
||||
|
||||
# Mark as failure
|
||||
has_error = True
|
||||
|
||||
except (SocketError, smtplib.SMTPException, RuntimeError) as e:
|
||||
self.logger.warning(
|
||||
f'Connection error while submitting email to {self.smtp_host}.'
|
||||
f' Reason: {e}')
|
||||
'Connection error while submitting email to "%s"',
|
||||
self.smtp_host)
|
||||
self.logger.debug(f'Socket Exception: {e}')
|
||||
|
||||
# Mark as failure
|
||||
has_error = True
|
||||
|
||||
except AppriseEmailException as e:
|
||||
self.logger.debug(f'Socket Exception: {e}')
|
||||
|
||||
# Mark as failure
|
||||
has_error = True
|
||||
|
||||
finally:
|
||||
# Gracefully terminate the connection with the server
|
||||
if socket is not None: # pragma: no branch
|
||||
if socket is not None:
|
||||
socket.quit()
|
||||
|
||||
# Reduce our dictionary (eliminate expired keys if any)
|
||||
self.pgp.prune()
|
||||
|
||||
return not has_error
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
|
|
@ -928,7 +564,13 @@ class NotifyEmail(NotifyBase):
|
|||
"""
|
||||
|
||||
# Define an URL parameters
|
||||
params = {}
|
||||
params = {
|
||||
'pgp': 'yes' if self.use_pgp else 'no',
|
||||
}
|
||||
|
||||
# Store our public key back into your URL
|
||||
if self.pgp_key is not None:
|
||||
params['pgp_key'] = NotifyEmail.quote(self.pgp_key, safe=':\\/')
|
||||
|
||||
# Append our headers into our parameters
|
||||
params.update({'+{}'.format(k): v for k, v in self.headers.items()})
|
||||
|
|
@ -961,7 +603,7 @@ class NotifyEmail(NotifyBase):
|
|||
params['from'] = \
|
||||
formataddr((False, self.from_addr[1]), charset='utf-8')
|
||||
|
||||
if len(self.cc) > 0:
|
||||
if self.cc:
|
||||
# Handle our Carbon Copy Addresses
|
||||
params['cc'] = ','.join([
|
||||
formataddr(
|
||||
|
|
@ -971,7 +613,7 @@ class NotifyEmail(NotifyBase):
|
|||
charset='utf-8').replace(',', '%2C')
|
||||
for e in self.cc])
|
||||
|
||||
if len(self.bcc) > 0:
|
||||
if self.bcc:
|
||||
# Handle our Blind Carbon Copy Addresses
|
||||
params['bcc'] = ','.join([
|
||||
formataddr(
|
||||
|
|
@ -1031,12 +673,25 @@ class NotifyEmail(NotifyBase):
|
|||
params=NotifyEmail.urlencode(params),
|
||||
)
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
return (
|
||||
self.secure_protocol if self.secure else self.protocol,
|
||||
self.user, self.password, self.host, self.smtp_host,
|
||||
self.port if self.port
|
||||
else SECURE_MODES[self.secure_mode]['default_port'],
|
||||
)
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Returns the number of targets associated with this notification
|
||||
"""
|
||||
targets = len(self.targets)
|
||||
return targets if targets > 0 else 1
|
||||
return len(self.targets) if self.targets else 1
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
|
|
@ -1068,6 +723,16 @@ class NotifyEmail(NotifyBase):
|
|||
# value if invalid; we'll attempt to figure this out later on
|
||||
results['host'] = ''
|
||||
|
||||
# Get PGP Flag
|
||||
results['use_pgp'] = \
|
||||
parse_bool(results['qsd'].get(
|
||||
'pgp', NotifyEmail.template_args['pgp']['default']))
|
||||
|
||||
# Get PGP Public Key Override
|
||||
if 'pgpkey' in results['qsd'] and results['qsd']['pgpkey']:
|
||||
results['pgp_key'] = \
|
||||
NotifyEmail.unquote(results['qsd']['pgpkey'])
|
||||
|
||||
# The From address is a must; either through the use of templates
|
||||
# from= entry and/or merging the user and hostname together, this
|
||||
# must be calculated or parse_url will fail.
|
||||
|
|
@ -1089,14 +754,9 @@ class NotifyEmail(NotifyBase):
|
|||
from_addr = NotifyEmail.unquote(results['qsd']['from'])
|
||||
|
||||
if 'name' in results['qsd'] and len(results['qsd']['name']):
|
||||
# Depricate use of both `from=` and `name=` in the same url as
|
||||
# they will be synomomus of one another in the future.
|
||||
from_addr = formataddr(
|
||||
(NotifyEmail.unquote(results['qsd']['name']), from_addr),
|
||||
charset='utf-8')
|
||||
logger.warning(
|
||||
'Email name= and from= are synonymous; '
|
||||
'use one or the other.')
|
||||
|
||||
elif 'name' in results['qsd'] and len(results['qsd']['name']):
|
||||
# Extract from name to associate with from address
|
||||
|
|
@ -1132,3 +792,233 @@ class NotifyEmail(NotifyBase):
|
|||
for x, y in results['qsd+'].items()}
|
||||
|
||||
return results
|
||||
|
||||
@staticmethod
|
||||
def _get_charset(input_string):
|
||||
"""
|
||||
Get utf-8 charset if non ascii string only
|
||||
|
||||
Encode an ascii string to utf-8 is bad for email deliverability
|
||||
because some anti-spam gives a bad score for that
|
||||
like SUBJ_EXCESS_QP flag on Rspamd
|
||||
"""
|
||||
if not input_string:
|
||||
return None
|
||||
return 'utf-8' if not all(ord(c) < 128 for c in input_string) else None
|
||||
|
||||
@staticmethod
|
||||
def prepare_emails(subject, body, from_addr, to,
|
||||
cc=set(), bcc=set(), reply_to=set(),
|
||||
# Providing an SMTP Host helps improve Email Message-ID
|
||||
# and avoids getting flagged as spam
|
||||
smtp_host=None,
|
||||
# Can be either 'html' or 'text'
|
||||
notify_format=NotifyFormat.HTML,
|
||||
attach=None, headers=dict(),
|
||||
# Names can be a dictionary
|
||||
names=None,
|
||||
# Pretty Good Privacy Support; Pass in an
|
||||
# ApprisePGPController if you wish to use it
|
||||
pgp=None):
|
||||
"""
|
||||
Generator for emails
|
||||
from_addr: must be in format: (from_name, from_addr)
|
||||
to: must be in the format:
|
||||
[(to_name, to_addr), (to_name, to_addr)), ...]
|
||||
cc: must be a set of email addresses
|
||||
bcc: must be a set of email addresses
|
||||
reply_to: must be either None, or an email address
|
||||
smtp_host: This is used to generate the email's Message-ID. Set
|
||||
this correctly to avoid getting flagged as Spam
|
||||
notify_format: can be either 'text' or 'html'
|
||||
attach: must be of class AppriseAttachment
|
||||
headers: Optionally provide a dictionary of additional headers you
|
||||
would like to include in the email payload
|
||||
names: This is a dictionary of email addresses as keys and the
|
||||
Names to associate with them when sending the email.
|
||||
This is cross referenced for the cc and bcc lists
|
||||
pgp: Encrypting the message using Pretty Good Privacy support
|
||||
This requires that the pgp_path provided exists and
|
||||
keys can be referenced here to perform the encryption
|
||||
with. If a key isn't found, one will be generated.
|
||||
|
||||
pgp support requires the 'PGPy' Python library to be
|
||||
available.
|
||||
|
||||
Pass in an ApprisePGPController() if you wish to use this
|
||||
"""
|
||||
|
||||
if not to:
|
||||
# There is no one to email; we're done
|
||||
msg = 'There are no Email recipients to notify'
|
||||
logger.warning(msg)
|
||||
raise AppriseEmailException(msg)
|
||||
|
||||
elif pgp and not _pgp.PGP_SUPPORT:
|
||||
msg = 'PGP Support unavailable; install PGPy library'
|
||||
logger.warning(msg)
|
||||
raise AppriseEmailException(msg)
|
||||
|
||||
if not names:
|
||||
# Prepare a empty dictionary to prevent errors/warnings
|
||||
names = {}
|
||||
|
||||
if not smtp_host:
|
||||
# Generate a host identifier (used for Message-ID Creation)
|
||||
smtp_host = from_addr[1].split('@')[1]
|
||||
|
||||
logger.debug('SMTP Host: {smtp_host}')
|
||||
|
||||
# Create a copy of the targets list
|
||||
emails = list(to)
|
||||
while len(emails):
|
||||
# Get our email to notify
|
||||
to_name, to_addr = emails.pop(0)
|
||||
|
||||
# Strip target out of cc list if in To or Bcc
|
||||
_cc = (cc - bcc - set([to_addr]))
|
||||
|
||||
# Strip target out of bcc list if in To
|
||||
_bcc = (bcc - set([to_addr]))
|
||||
|
||||
# Strip target out of reply_to list if in To
|
||||
_reply_to = (reply_to - set([to_addr]))
|
||||
|
||||
# Format our cc addresses to support the Name field
|
||||
_cc = [formataddr(
|
||||
(names.get(addr, False), addr), charset='utf-8')
|
||||
for addr in _cc]
|
||||
|
||||
# Format our bcc addresses to support the Name field
|
||||
_bcc = [formataddr(
|
||||
(names.get(addr, False), addr), charset='utf-8')
|
||||
for addr in _bcc]
|
||||
|
||||
if reply_to:
|
||||
# Format our reply-to addresses to support the Name field
|
||||
reply_to = [formataddr(
|
||||
(names.get(addr, False), addr), charset='utf-8')
|
||||
for addr in reply_to]
|
||||
|
||||
logger.debug(
|
||||
'Email From: {}'.format(
|
||||
formataddr(from_addr, charset='utf-8')))
|
||||
|
||||
logger.debug('Email To: {}'.format(to_addr))
|
||||
if _cc:
|
||||
logger.debug('Email Cc: {}'.format(', '.join(_cc)))
|
||||
if _bcc:
|
||||
logger.debug('Email Bcc: {}'.format(', '.join(_bcc)))
|
||||
if _reply_to:
|
||||
logger.debug(
|
||||
'Email Reply-To: {}'.format(', '.join(_reply_to))
|
||||
)
|
||||
|
||||
# Prepare Email Message
|
||||
if notify_format == NotifyFormat.HTML:
|
||||
base = MIMEMultipart("alternative")
|
||||
base.attach(MIMEText(
|
||||
convert_between(
|
||||
NotifyFormat.HTML, NotifyFormat.TEXT, body),
|
||||
'plain', 'utf-8')
|
||||
)
|
||||
base.attach(MIMEText(body, 'html', 'utf-8'))
|
||||
else:
|
||||
base = MIMEText(body, 'plain', 'utf-8')
|
||||
|
||||
if attach:
|
||||
mixed = MIMEMultipart("mixed")
|
||||
mixed.attach(base)
|
||||
# Now store our attachments
|
||||
for no, attachment in enumerate(attach, start=1):
|
||||
if not attachment:
|
||||
# We could not load the attachment; take an early
|
||||
# exit since this isn't what the end user wanted
|
||||
|
||||
# We could not access the attachment
|
||||
msg = 'Could not access attachment {}.'.format(
|
||||
attachment.url(privacy=True))
|
||||
logger.warning(msg)
|
||||
raise AppriseEmailException(msg)
|
||||
|
||||
logger.debug(
|
||||
'Preparing Email attachment {}'.format(
|
||||
attachment.url(privacy=True)))
|
||||
|
||||
with open(attachment.path, "rb") as abody:
|
||||
app = MIMEApplication(abody.read())
|
||||
app.set_type(attachment.mimetype)
|
||||
|
||||
# Prepare our attachment name
|
||||
filename = attachment.name \
|
||||
if attachment.name else f'file{no:03}.dat'
|
||||
|
||||
app.add_header(
|
||||
'Content-Disposition',
|
||||
'attachment; filename="{}"'.format(
|
||||
Header(filename, 'utf-8')),
|
||||
)
|
||||
mixed.attach(app)
|
||||
base = mixed
|
||||
|
||||
if pgp:
|
||||
logger.debug("Securing Email with PGP Encryption")
|
||||
# Set our header information to include in the encryption
|
||||
base['From'] = formataddr(
|
||||
(None, from_addr[1]), charset='utf-8')
|
||||
base['To'] = formataddr((None, to_addr), charset='utf-8')
|
||||
base['Subject'] = \
|
||||
Header(subject, NotifyEmail._get_charset(subject))
|
||||
|
||||
# Apply our encryption
|
||||
encrypted_content = \
|
||||
pgp.encrypt(base.as_string(), to_addr)
|
||||
|
||||
if not encrypted_content:
|
||||
# Unable to send notification
|
||||
msg = 'Unable to encrypt email via PGP'
|
||||
logger.warning(msg)
|
||||
raise AppriseEmailException(msg)
|
||||
|
||||
# prepare our messsage
|
||||
base = MIMEMultipart(
|
||||
"encrypted", protocol="application/pgp-encrypted")
|
||||
|
||||
# Store Autocrypt header (DeltaChat Support)
|
||||
base.add_header(
|
||||
"Autocrypt",
|
||||
"addr=%s; prefer-encrypt=mutual" % formataddr(
|
||||
(False, to_addr), charset='utf-8'))
|
||||
|
||||
# Set Encryption Info Part
|
||||
enc_payload = MIMEText("Version: 1", "plain")
|
||||
enc_payload.set_type("application/pgp-encrypted")
|
||||
base.attach(enc_payload)
|
||||
|
||||
enc_payload = MIMEBase("application", "octet-stream")
|
||||
enc_payload.set_payload(encrypted_content)
|
||||
base.attach(enc_payload)
|
||||
|
||||
# Apply any provided custom headers
|
||||
for k, v in headers.items():
|
||||
base[k] = Header(v, NotifyEmail._get_charset(v))
|
||||
|
||||
base['Subject'] = \
|
||||
Header(subject, NotifyEmail._get_charset(subject))
|
||||
base['From'] = formataddr(from_addr, charset='utf-8')
|
||||
base['To'] = formataddr((to_name, to_addr), charset='utf-8')
|
||||
base['Message-ID'] = make_msgid(domain=smtp_host)
|
||||
base['Date'] = \
|
||||
datetime.now(timezone.utc)\
|
||||
.strftime("%a, %d %b %Y %H:%M:%S +0000")
|
||||
|
||||
if cc:
|
||||
base['Cc'] = ','.join(_cc)
|
||||
|
||||
if reply_to:
|
||||
base['Reply-To'] = ','.join(_reply_to)
|
||||
|
||||
yield EmailMessage(
|
||||
recipient=to_addr,
|
||||
to_addrs=[to_addr] + list(_cc) + list(_bcc),
|
||||
body=base.as_string())
|
||||
84
lib/apprise/plugins/email/common.py
Normal file
84
lib/apprise/plugins/email/common.py
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2025, 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 dataclasses
|
||||
import typing as t
|
||||
|
||||
from ...exception import ApprisePluginException
|
||||
|
||||
|
||||
class AppriseEmailException(ApprisePluginException):
|
||||
"""
|
||||
Thrown when there is an error with the Email Attachment
|
||||
"""
|
||||
def __init__(self, message, error_code=601):
|
||||
super().__init__(message, error_code=error_code)
|
||||
|
||||
|
||||
class WebBaseLogin:
|
||||
"""
|
||||
This class is just used in conjunction of the default emailers
|
||||
to best formulate a login to it using the data detected
|
||||
"""
|
||||
# User Login must be Email Based
|
||||
EMAIL = 'Email'
|
||||
|
||||
# User Login must UserID Based
|
||||
USERID = 'UserID'
|
||||
|
||||
|
||||
# Secure Email Modes
|
||||
class SecureMailMode:
|
||||
INSECURE = "insecure"
|
||||
SSL = "ssl"
|
||||
STARTTLS = "starttls"
|
||||
|
||||
|
||||
# Define all of the secure modes (used during validation)
|
||||
SECURE_MODES = {
|
||||
SecureMailMode.STARTTLS: {
|
||||
'default_port': 587,
|
||||
},
|
||||
SecureMailMode.SSL: {
|
||||
'default_port': 465,
|
||||
},
|
||||
SecureMailMode.INSECURE: {
|
||||
'default_port': 25,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class EmailMessage:
|
||||
"""
|
||||
Our message structure
|
||||
"""
|
||||
recipient: str
|
||||
to_addrs: t.List[str]
|
||||
body: str
|
||||
272
lib/apprise/plugins/email/templates.py
Normal file
272
lib/apprise/plugins/email/templates.py
Normal file
|
|
@ -0,0 +1,272 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2025, 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 .common import (SecureMailMode, WebBaseLogin)
|
||||
|
||||
# To attempt to make this script stupid proof, if we detect an email address
|
||||
# that is part of the this table, we can pre-use a lot more defaults if they
|
||||
# aren't otherwise specified on the users input.
|
||||
EMAIL_TEMPLATES = (
|
||||
# Google GMail
|
||||
(
|
||||
'Google Mail',
|
||||
re.compile(
|
||||
r'^((?P<label>[^+]+)\+)?(?P<id>[^@]+)@'
|
||||
r'(?P<domain>gmail\.com)$', re.I),
|
||||
{
|
||||
'port': 587,
|
||||
'smtp_host': 'smtp.gmail.com',
|
||||
'secure': True,
|
||||
'secure_mode': SecureMailMode.STARTTLS,
|
||||
'login_type': (WebBaseLogin.EMAIL, )
|
||||
},
|
||||
),
|
||||
|
||||
# Yandex
|
||||
(
|
||||
'Yandex',
|
||||
re.compile(
|
||||
r'^((?P<label>[^+]+)\+)?(?P<id>[^@]+)@'
|
||||
r'(?P<domain>yandex\.(com|ru|ua|by|kz|uz|tr|fr))$', re.I),
|
||||
{
|
||||
'port': 465,
|
||||
'smtp_host': 'smtp.yandex.ru',
|
||||
'secure': True,
|
||||
'secure_mode': SecureMailMode.SSL,
|
||||
'login_type': (WebBaseLogin.USERID, )
|
||||
},
|
||||
),
|
||||
|
||||
# Microsoft Hotmail
|
||||
(
|
||||
'Microsoft Hotmail',
|
||||
re.compile(
|
||||
r'^((?P<label>[^+]+)\+)?(?P<id>[^@]+)@'
|
||||
r'(?P<domain>(hotmail|live)\.com(\.au)?)$', re.I),
|
||||
{
|
||||
'port': 587,
|
||||
'smtp_host': 'smtp-mail.outlook.com',
|
||||
'secure': True,
|
||||
'secure_mode': SecureMailMode.STARTTLS,
|
||||
'login_type': (WebBaseLogin.EMAIL, )
|
||||
},
|
||||
),
|
||||
|
||||
# Microsoft Outlook
|
||||
(
|
||||
'Microsoft Outlook',
|
||||
re.compile(
|
||||
r'^((?P<label>[^+]+)\+)?(?P<id>[^@]+)@'
|
||||
r'(?P<domain>(smtp\.)?outlook\.com(\.au)?)$', re.I),
|
||||
{
|
||||
'port': 587,
|
||||
'smtp_host': 'smtp.outlook.com',
|
||||
'secure': True,
|
||||
'secure_mode': SecureMailMode.STARTTLS,
|
||||
'login_type': (WebBaseLogin.EMAIL, )
|
||||
},
|
||||
),
|
||||
|
||||
# Microsoft Office 365 (Email Server)
|
||||
# You must specify an authenticated sender address in the from= settings
|
||||
# and a valid email in the to= to deliver your emails to
|
||||
(
|
||||
'Microsoft Office 365',
|
||||
re.compile(
|
||||
r'^[^@]+@(?P<domain>(smtp\.)?office365\.com)$', re.I),
|
||||
{
|
||||
'port': 587,
|
||||
'smtp_host': 'smtp.office365.com',
|
||||
'secure': True,
|
||||
'secure_mode': SecureMailMode.STARTTLS,
|
||||
},
|
||||
),
|
||||
|
||||
# Yahoo Mail
|
||||
(
|
||||
'Yahoo Mail',
|
||||
re.compile(
|
||||
r'^((?P<label>[^+]+)\+)?(?P<id>[^@]+)@'
|
||||
r'(?P<domain>yahoo\.(ca|com))$', re.I),
|
||||
{
|
||||
'port': 465,
|
||||
'smtp_host': 'smtp.mail.yahoo.com',
|
||||
'secure': True,
|
||||
'secure_mode': SecureMailMode.STARTTLS,
|
||||
'login_type': (WebBaseLogin.EMAIL, )
|
||||
},
|
||||
),
|
||||
|
||||
# Fast Mail (Series 1)
|
||||
(
|
||||
'Fast Mail',
|
||||
re.compile(
|
||||
r'^((?P<label>[^+]+)\+)?(?P<id>[^@]+)@'
|
||||
r'(?P<domain>fastmail\.(com|cn|co\.uk|com\.au|de|es|fm|fr|im|'
|
||||
r'in|jp|mx|net|nl|org|se|to|tw|uk|us))$', re.I),
|
||||
{
|
||||
'port': 465,
|
||||
'smtp_host': 'smtp.fastmail.com',
|
||||
'secure': True,
|
||||
'secure_mode': SecureMailMode.SSL,
|
||||
'login_type': (WebBaseLogin.EMAIL, )
|
||||
},
|
||||
),
|
||||
|
||||
# Fast Mail (Series 2)
|
||||
(
|
||||
'Fast Mail Extended Addresses',
|
||||
re.compile(
|
||||
r'^((?P<label>[^+]+)\+)?(?P<id>[^@]+)@'
|
||||
r'(?P<domain>123mail\.org|airpost\.net|eml\.cc|fmail\.co\.uk|'
|
||||
r'fmgirl\.com|fmguy\.com|mailbolt\.com|mailcan\.com|'
|
||||
r'mailhaven\.com|mailmight\.com|ml1\.net|mm\.st|myfastmail\.com|'
|
||||
r'proinbox\.com|promessage\.com|rushpost\.com|sent\.(as|at|com)|'
|
||||
r'speedymail\.org|warpmail\.net|xsmail\.com|150mail\.com|'
|
||||
r'150ml\.com|16mail\.com|2-mail\.com|4email\.net|50mail\.com|'
|
||||
r'allmail\.net|bestmail\.us|cluemail\.com|elitemail\.org|'
|
||||
r'emailcorner\.net|emailengine\.(net|org)|emailgroups\.net|'
|
||||
r'emailplus\.org|emailuser\.net|f-m\.fm|fast-email\.com|'
|
||||
r'fast-mail\.org|fastem\.com|fastemail\.us|fastemailer\.com|'
|
||||
r'fastest\.cc|fastimap\.com|fastmailbox\.net|fastmessaging\.com|'
|
||||
r'fea\.st|fmailbox\.com|ftml\.net|h-mail\.us|hailmail\.net|'
|
||||
r'imap-mail\.com|imap\.cc|imapmail\.org|inoutbox\.com|'
|
||||
r'internet-e-mail\.com|internet-mail\.org|internetemails\.net|'
|
||||
r'internetmailing\.net|jetemail\.net|justemail\.net|'
|
||||
r'letterboxes\.org|mail-central\.com|mail-page\.com|'
|
||||
r'mailandftp\.com|mailas\.com|mailc\.net|mailforce\.net|'
|
||||
r'mailftp\.com|mailingaddress\.org|mailite\.com|mailnew\.com|'
|
||||
r'mailsent\.net|mailservice\.ms|mailup\.net|mailworks\.org|'
|
||||
r'mymacmail\.com|nospammail\.net|ownmail\.net|petml\.com|'
|
||||
r'postinbox\.com|postpro\.net|realemail\.net|reallyfast\.biz|'
|
||||
r'reallyfast\.info|speedpost\.net|ssl-mail\.com|swift-mail\.com|'
|
||||
r'the-fastest\.net|the-quickest\.com|theinternetemail\.com|'
|
||||
r'veryfast\.biz|veryspeedy\.net|yepmail\.net)$', re.I),
|
||||
{
|
||||
'port': 465,
|
||||
'smtp_host': 'smtp.fastmail.com',
|
||||
'secure': True,
|
||||
'secure_mode': SecureMailMode.SSL,
|
||||
'login_type': (WebBaseLogin.EMAIL, )
|
||||
},
|
||||
),
|
||||
|
||||
# Zoho Mail (Free)
|
||||
(
|
||||
'Zoho Mail',
|
||||
re.compile(
|
||||
r'^((?P<label>[^+]+)\+)?(?P<id>[^@]+)@'
|
||||
r'(?P<domain>zoho(mail)?\.com)$', re.I),
|
||||
{
|
||||
'port': 587,
|
||||
'smtp_host': 'smtp.zoho.com',
|
||||
'secure': True,
|
||||
'secure_mode': SecureMailMode.STARTTLS,
|
||||
'login_type': (WebBaseLogin.EMAIL, )
|
||||
},
|
||||
),
|
||||
|
||||
# SendGrid (Email Server)
|
||||
# You must specify an authenticated sender address in the from= settings
|
||||
# and a valid email in the to= to deliver your emails to
|
||||
(
|
||||
'SendGrid',
|
||||
re.compile(
|
||||
r'^((?P<label>[^+]+)\+)?(?P<id>[^@]+)@'
|
||||
r'(?P<domain>(\.smtp)?sendgrid\.(com|net))$', re.I),
|
||||
{
|
||||
'port': 465,
|
||||
'smtp_host': 'smtp.sendgrid.net',
|
||||
'secure': True,
|
||||
'secure_mode': SecureMailMode.SSL,
|
||||
'login_type': (WebBaseLogin.USERID, )
|
||||
},
|
||||
),
|
||||
|
||||
# 163.com
|
||||
(
|
||||
'163.com',
|
||||
re.compile(
|
||||
r'^((?P<label>[^+]+)\+)?(?P<id>[^@]+)@'
|
||||
r'(?P<domain>163\.com)$', re.I),
|
||||
{
|
||||
'port': 465,
|
||||
'smtp_host': 'smtp.163.com',
|
||||
'secure': True,
|
||||
'secure_mode': SecureMailMode.SSL,
|
||||
'login_type': (WebBaseLogin.EMAIL, )
|
||||
},
|
||||
),
|
||||
|
||||
# Foxmail.com
|
||||
(
|
||||
'Foxmail.com',
|
||||
re.compile(
|
||||
r'^((?P<label>[^+]+)\+)?(?P<id>[^@]+)@'
|
||||
r'(?P<domain>(foxmail|qq)\.com)$', re.I),
|
||||
{
|
||||
'port': 587,
|
||||
'smtp_host': 'smtp.qq.com',
|
||||
'secure': True,
|
||||
'secure_mode': SecureMailMode.STARTTLS,
|
||||
'login_type': (WebBaseLogin.EMAIL, )
|
||||
},
|
||||
),
|
||||
|
||||
# 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
|
||||
(
|
||||
'Custom',
|
||||
re.compile(
|
||||
r'^((?P<label>[^+]+)\+)?(?P<id>[^@]+)@'
|
||||
r'(?P<domain>.+)$', re.I),
|
||||
{
|
||||
# Setting smtp_host to None is a way of
|
||||
# auto-detecting it based on other parameters
|
||||
# specified. There is no reason to ever modify
|
||||
# this Catch All
|
||||
'smtp_host': None,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -36,7 +36,7 @@ from json import loads
|
|||
|
||||
from .base import NotifyBase
|
||||
from ..url import PrivacyMode
|
||||
from ..utils import parse_bool
|
||||
from ..utils.parse import parse_bool
|
||||
from ..common import NotifyType
|
||||
from .. import __version__ as VERSION
|
||||
from ..locale import gettext_lazy as _
|
||||
|
|
@ -593,6 +593,18 @@ class NotifyEmby(NotifyBase):
|
|||
|
||||
return not has_error
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
return (
|
||||
self.secure_protocol, self.user, self.password, self.host,
|
||||
self.port if self.port else (443 if self.secure else 80),
|
||||
)
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -181,6 +181,20 @@ class NotifyEnigma2(NotifyBase):
|
|||
|
||||
return
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
return (
|
||||
self.secure_protocol,
|
||||
self.user, self.password, self.host,
|
||||
self.port if self.port else (443 if self.secure else 80),
|
||||
self.fullpath.rstrip('/'),
|
||||
)
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -52,10 +52,8 @@ import requests
|
|||
from json import dumps
|
||||
from ..base import NotifyBase
|
||||
from ...common import NotifyType
|
||||
from ...utils import validate_regex
|
||||
from ...utils import parse_list
|
||||
from ...utils import parse_bool
|
||||
from ...utils import dict_full_update
|
||||
from ...utils.parse import validate_regex, parse_list, parse_bool
|
||||
from ...utils.logic import dict_full_update
|
||||
from ...common import NotifyImageSize
|
||||
from ...apprise_attachment import AppriseAttachment
|
||||
from ...locale import gettext_lazy as _
|
||||
|
|
@ -507,6 +505,15 @@ class NotifyFCM(NotifyBase):
|
|||
|
||||
return not has_error
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
return (self.secure_protocol, self.mode, self.apikey, self.project)
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -34,7 +34,7 @@
|
|||
# https://firebase.google.com/docs/reference/fcm/rest/v1/\
|
||||
# projects.messages#androidnotification
|
||||
import re
|
||||
from ...utils import parse_bool
|
||||
from ...utils.parse import parse_bool
|
||||
from ...common import NotifyType
|
||||
from ...asset import AppriseAsset
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -38,7 +38,7 @@ from json import dumps
|
|||
|
||||
from .base import NotifyBase
|
||||
from ..common import NotifyType
|
||||
from ..utils import validate_regex
|
||||
from ..utils.parse import validate_regex
|
||||
from ..locale import gettext_lazy as _
|
||||
|
||||
|
||||
|
|
@ -192,6 +192,15 @@ class NotifyFeishu(NotifyBase):
|
|||
|
||||
return True
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
return (self.secure_protocol, self.token)
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -48,9 +48,7 @@ from .base import NotifyBase
|
|||
from ..common import NotifyType
|
||||
from ..common import NotifyFormat
|
||||
from ..common import NotifyImageSize
|
||||
from ..utils import parse_list
|
||||
from ..utils import parse_bool
|
||||
from ..utils import validate_regex
|
||||
from ..utils.parse import parse_list, parse_bool, validate_regex
|
||||
from ..locale import gettext_lazy as _
|
||||
|
||||
|
||||
|
|
@ -308,6 +306,15 @@ class NotifyFlock(NotifyBase):
|
|||
|
||||
return not has_error
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
return (self.secure_protocol, self.token)
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -103,6 +103,15 @@ class NotifyFreeMobile(NotifyBase):
|
|||
|
||||
return
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
return (self.secure_protocol, self.user, self.password)
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -29,7 +29,7 @@
|
|||
from .base import NotifyBase
|
||||
from ..common import NotifyImageSize
|
||||
from ..common import NotifyType
|
||||
from ..utils import parse_bool
|
||||
from ..utils.parse import parse_bool
|
||||
from ..locale import gettext_lazy as _
|
||||
|
||||
# Default our global support flag
|
||||
|
|
@ -134,6 +134,10 @@ class NotifyGnome(NotifyBase):
|
|||
# cause any title (if defined) to get placed into the message body.
|
||||
title_maxlen = 0
|
||||
|
||||
# No URL Identifier will be defined for this service as there simply isn't
|
||||
# enough details to uniquely identify one dbus:// from another.
|
||||
url_identifier = False
|
||||
|
||||
# Define object templates
|
||||
templates = (
|
||||
'{schema}://',
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -61,7 +61,7 @@ from json import dumps
|
|||
from .base import NotifyBase
|
||||
from ..common import NotifyFormat
|
||||
from ..common import NotifyType
|
||||
from ..utils import validate_regex
|
||||
from ..utils.parse import validate_regex
|
||||
from ..locale import gettext_lazy as _
|
||||
|
||||
|
||||
|
|
@ -265,6 +265,18 @@ class NotifyGoogleChat(NotifyBase):
|
|||
|
||||
return True
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
return (
|
||||
self.secure_protocol, self.workspace, self.webhook_key,
|
||||
self.webhook_token,
|
||||
)
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -36,7 +36,7 @@ from json import dumps
|
|||
|
||||
from .base import NotifyBase
|
||||
from ..common import NotifyType, NotifyFormat
|
||||
from ..utils import validate_regex
|
||||
from ..utils.parse import validate_regex
|
||||
from ..locale import gettext_lazy as _
|
||||
|
||||
|
||||
|
|
@ -265,6 +265,20 @@ class NotifyGotify(NotifyBase):
|
|||
|
||||
return True
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
return (
|
||||
self.secure_protocol if self.secure else self.protocol,
|
||||
self.user, self.password, self.host,
|
||||
self.port if self.port else (443 if self.secure else 80),
|
||||
self.fullpath.rstrip('/'),
|
||||
)
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -30,7 +30,7 @@ from .base import NotifyBase
|
|||
from ..url import PrivacyMode
|
||||
from ..common import NotifyImageSize
|
||||
from ..common import NotifyType
|
||||
from ..utils import parse_bool
|
||||
from ..utils.parse import parse_bool
|
||||
from ..locale import gettext_lazy as _
|
||||
|
||||
# Default our global support flag
|
||||
|
|
@ -338,6 +338,19 @@ class NotifyGrowl(NotifyBase):
|
|||
|
||||
return True
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
return (
|
||||
self.secure_protocol if self.secure else self.protocol,
|
||||
self.user, self.password, self.host,
|
||||
self.port if self.port else self.default_port,
|
||||
)
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -37,7 +37,7 @@ from uuid import uuid4
|
|||
from .base import NotifyBase
|
||||
from ..url import PrivacyMode
|
||||
from ..common import NotifyType
|
||||
from ..utils import validate_regex
|
||||
from ..utils.parse import validate_regex
|
||||
from ..locale import gettext_lazy as _
|
||||
|
||||
|
||||
|
|
@ -179,8 +179,8 @@ class NotifyHomeAssistant(NotifyBase):
|
|||
if isinstance(self.port, int):
|
||||
url += ':%d' % self.port
|
||||
|
||||
url += '' if not self.fullpath else '/' + self.fullpath.strip('/')
|
||||
url += '/api/services/persistent_notification/create'
|
||||
url += self.fullpath.rstrip('/') + \
|
||||
'/api/services/persistent_notification/create'
|
||||
|
||||
self.logger.debug('Home Assistant POST URL: %s (cert_verify=%r)' % (
|
||||
url, self.verify_certificate,
|
||||
|
|
@ -231,6 +231,22 @@ class NotifyHomeAssistant(NotifyBase):
|
|||
|
||||
return True
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
return (
|
||||
self.secure_protocol if self.secure else self.protocol,
|
||||
self.user, self.password, self.host,
|
||||
self.port if self.port else (
|
||||
443 if self.secure else self.default_insecure_port),
|
||||
self.fullpath.rstrip('/'),
|
||||
self.accesstoken,
|
||||
)
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
|
|
@ -302,7 +318,7 @@ class NotifyHomeAssistant(NotifyBase):
|
|||
results['accesstoken'] = fullpath.pop() if fullpath else None
|
||||
|
||||
# Re-assemble our full path
|
||||
results['fullpath'] = '/'.join(fullpath)
|
||||
results['fullpath'] = '/' + '/'.join(fullpath) if fullpath else ''
|
||||
|
||||
# Allow the specification of a unique notification_id so that
|
||||
# it will always replace the last one sent.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -33,9 +33,7 @@ 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 ..utils.parse import is_phone_no, parse_phone_no, validate_regex
|
||||
from ..locale import gettext_lazy as _
|
||||
|
||||
|
||||
|
|
@ -253,6 +251,15 @@ class NotifyHttpSMS(NotifyBase):
|
|||
|
||||
return not has_error
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
return (self.secure_protocol, self.source, self.apikey)
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -46,8 +46,7 @@ from json import dumps
|
|||
|
||||
from .base import NotifyBase
|
||||
from ..common import NotifyType
|
||||
from ..utils import parse_list
|
||||
from ..utils import validate_regex
|
||||
from ..utils.parse import parse_list, validate_regex
|
||||
from ..locale import gettext_lazy as _
|
||||
|
||||
|
||||
|
|
@ -287,6 +286,15 @@ class NotifyIFTTT(NotifyBase):
|
|||
|
||||
return not has_error
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
return (self.secure_protocol, self.webhook_id)
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -42,9 +42,7 @@ import requests
|
|||
from .base import NotifyBase
|
||||
from ..common import NotifyImageSize
|
||||
from ..common import NotifyType
|
||||
from ..utils import parse_list
|
||||
from ..utils import parse_bool
|
||||
from ..utils import validate_regex
|
||||
from ..utils.parse import parse_list, parse_bool, validate_regex
|
||||
from ..locale import gettext_lazy as _
|
||||
|
||||
# Extend HTTP Error Messages
|
||||
|
|
@ -345,6 +343,15 @@ class NotifyJoin(NotifyBase):
|
|||
|
||||
return not has_error
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
return (self.secure_protocol, self.apikey)
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -40,9 +40,7 @@ from json import loads
|
|||
|
||||
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 ..utils.parse import is_phone_no, parse_phone_no, validate_regex
|
||||
from ..locale import gettext_lazy as _
|
||||
|
||||
# Extend HTTP Error Messages
|
||||
|
|
@ -304,6 +302,15 @@ class NotifyKavenegar(NotifyBase):
|
|||
|
||||
return not has_error
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
return (self.secure_protocol, self.source, self.apikey)
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -41,7 +41,7 @@ from json import dumps
|
|||
|
||||
from .base import NotifyBase
|
||||
from ..common import NotifyType
|
||||
from ..utils import validate_regex
|
||||
from ..utils.parse import validate_regex
|
||||
from ..locale import gettext_lazy as _
|
||||
|
||||
# Extend HTTP Error Messages
|
||||
|
|
@ -198,6 +198,15 @@ class NotifyKumulos(NotifyBase):
|
|||
return False
|
||||
return True
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
return (self.secure_protocol, self.apikey, self.serverkey)
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -92,10 +92,8 @@ 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 _
|
||||
from ..utils import is_hostname
|
||||
from ..utils import is_ipaddr
|
||||
from ..utils.parse import validate_regex, is_hostname, is_ipaddr
|
||||
|
||||
# A URL Parser to detect App ID
|
||||
LAMETRIC_APP_ID_DETECTOR_RE = re.compile(
|
||||
|
|
@ -783,6 +781,29 @@ class NotifyLametric(NotifyBase):
|
|||
|
||||
return True
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
if self.mode == LametricMode.DEVICE:
|
||||
return (
|
||||
self.secure_protocol if self.secure else self.protocol,
|
||||
self.user, self.lametric_apikey, self.host,
|
||||
self.port if self.port else (
|
||||
443 if self.secure else
|
||||
self.template_tokens['port']['default']),
|
||||
)
|
||||
|
||||
return (
|
||||
self.protocol,
|
||||
self.lametric_app_access_token,
|
||||
self.lametric_app_id,
|
||||
self.lametric_app_ver,
|
||||
)
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
|
|
@ -871,6 +892,9 @@ class NotifyLametric(NotifyBase):
|
|||
results['password'] = results['user']
|
||||
results['user'] = None
|
||||
|
||||
# Get unquoted entries
|
||||
entries = NotifyLametric.split_path(results['fullpath'])
|
||||
|
||||
# Priority Handling
|
||||
if 'priority' in results['qsd'] and results['qsd']['priority']:
|
||||
results['priority'] = NotifyLametric.unquote(
|
||||
|
|
@ -913,6 +937,10 @@ class NotifyLametric(NotifyBase):
|
|||
results['app_ver'] = \
|
||||
NotifyLametric.unquote(results['qsd']['app_ver'])
|
||||
|
||||
elif entries:
|
||||
# Store our app id
|
||||
results['app_ver'] = entries.pop(0)
|
||||
|
||||
if 'token' in results['qsd'] and results['qsd']['token']:
|
||||
# Extract Application Access Token from an argument
|
||||
results['app_token'] = \
|
||||
|
|
@ -938,7 +966,7 @@ class NotifyLametric(NotifyBase):
|
|||
LAMETRIC_IS_APP_TOKEN.match(results['password'])) and
|
||||
|
||||
# Scan for app_ flags
|
||||
next((f for f in results.keys() \
|
||||
next((f for f in results.keys()
|
||||
if f.startswith('app_')), None) is None) \
|
||||
else LametricMode.CLOUD
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -37,9 +37,7 @@ from .base import NotifyBase
|
|||
from ..url import PrivacyMode
|
||||
from ..common import NotifyType
|
||||
from ..common import NotifyImageSize
|
||||
from ..utils import validate_regex
|
||||
from ..utils import parse_list
|
||||
from ..utils import parse_bool
|
||||
from ..utils.parse import validate_regex, parse_list, parse_bool
|
||||
from ..locale import gettext_lazy as _
|
||||
|
||||
|
||||
|
|
@ -241,6 +239,15 @@ class NotifyLine(NotifyBase):
|
|||
|
||||
return not has_error
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
return (self.secure_protocol, self.token)
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -36,10 +36,8 @@ 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 ..utils.parse import (
|
||||
parse_list, is_hostname, is_ipaddr, parse_bool)
|
||||
from ..locale import gettext_lazy as _
|
||||
from ..url import PrivacyMode
|
||||
|
||||
|
|
@ -324,6 +322,24 @@ class NotifyLunaSea(NotifyBase):
|
|||
|
||||
return not has_error
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
secure = self.secure_protocol[0] \
|
||||
if self.mode == LunaSeaMode.CLOUD else (
|
||||
self.secure_protocol[0] if self.secure else self.protocol[0])
|
||||
return (
|
||||
secure,
|
||||
self.host if self.mode == LunaSeaMode.PRIVATE else None,
|
||||
self.port if self.port else (443 if self.secure else 80),
|
||||
self.user if self.user else None,
|
||||
self.password if self.password else None,
|
||||
)
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -33,7 +33,7 @@ import os
|
|||
from .base import NotifyBase
|
||||
from ..common import NotifyImageSize
|
||||
from ..common import NotifyType
|
||||
from ..utils import parse_bool
|
||||
from ..utils.parse import parse_bool
|
||||
from ..locale import gettext_lazy as _
|
||||
|
||||
# Default our global support flag
|
||||
|
|
@ -92,6 +92,10 @@ class NotifyMacOSX(NotifyBase):
|
|||
# content to display
|
||||
body_max_line_count = 10
|
||||
|
||||
# No URL Identifier will be defined for this service as there simply isn't
|
||||
# enough details to uniquely identify one dbus:// from another.
|
||||
url_identifier = False
|
||||
|
||||
# The possible paths to the terminal-notifier
|
||||
notify_paths = (
|
||||
'/opt/homebrew/bin/terminal-notifier',
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -59,10 +59,8 @@ from email.utils import formataddr
|
|||
from .base import NotifyBase
|
||||
from ..common import NotifyType
|
||||
from ..common import NotifyFormat
|
||||
from ..utils import parse_emails
|
||||
from ..utils import parse_bool
|
||||
from ..utils import is_email
|
||||
from ..utils import validate_regex
|
||||
from ..utils.parse import (
|
||||
parse_emails, parse_bool, is_email, validate_regex)
|
||||
from ..logger import logger
|
||||
from ..locale import gettext_lazy as _
|
||||
|
||||
|
|
@ -383,9 +381,15 @@ class NotifyMailgun(NotifyBase):
|
|||
self.logger.debug(
|
||||
'Preparing Mailgun attachment {}'.format(
|
||||
attachment.url(privacy=True)))
|
||||
|
||||
# Prepare our filename
|
||||
filename = attachment.name \
|
||||
if attachment.name \
|
||||
else 'file{no:03}.dat'.format(no=idx + 1)
|
||||
|
||||
try:
|
||||
files['attachment[{}]'.format(idx)] = \
|
||||
(attachment.name, open(attachment.path, 'rb'))
|
||||
(filename, open(attachment.path, 'rb'))
|
||||
|
||||
except (OSError, IOError) as e:
|
||||
self.logger.warning(
|
||||
|
|
@ -579,6 +583,17 @@ class NotifyMailgun(NotifyBase):
|
|||
|
||||
return not has_error
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
return (
|
||||
self.secure_protocol, self.host, self.apikey, self.region_name,
|
||||
)
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -38,9 +38,7 @@ from ..url import PrivacyMode
|
|||
from ..common import NotifyImageSize
|
||||
from ..common import NotifyFormat
|
||||
from ..common import NotifyType
|
||||
from ..utils import parse_list
|
||||
from ..utils import parse_bool
|
||||
from ..utils import validate_regex
|
||||
from ..utils.parse import parse_list, parse_bool, validate_regex
|
||||
from ..locale import gettext_lazy as _
|
||||
from ..attachment.base import AttachBase
|
||||
|
||||
|
|
@ -336,6 +334,18 @@ class NotifyMastodon(NotifyBase):
|
|||
|
||||
return
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
return (
|
||||
self.secure_protocol[0], self.token, self.host,
|
||||
self.port if self.port else (443 if self.secure else 80),
|
||||
)
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -32,6 +32,7 @@
|
|||
#
|
||||
import re
|
||||
import requests
|
||||
import uuid
|
||||
from markdown import markdown
|
||||
from json import dumps
|
||||
from json import loads
|
||||
|
|
@ -39,13 +40,13 @@ from time import time
|
|||
|
||||
from .base import NotifyBase
|
||||
from ..url import PrivacyMode
|
||||
from ..exception import AppriseException
|
||||
from ..common import NotifyType
|
||||
from ..common import NotifyImageSize
|
||||
from ..common import NotifyFormat
|
||||
from ..utils import parse_bool
|
||||
from ..utils import parse_list
|
||||
from ..utils import is_hostname
|
||||
from ..utils import validate_regex
|
||||
from ..common import PersistentStoreMode
|
||||
from ..utils.parse import (
|
||||
parse_bool, parse_list, is_hostname, validate_regex)
|
||||
from ..locale import gettext_lazy as _
|
||||
|
||||
# Define default path
|
||||
|
|
@ -55,6 +56,13 @@ MATRIX_V3_API_PATH = '/_matrix/client/v3'
|
|||
MATRIX_V3_MEDIA_PATH = '/_matrix/media/v3'
|
||||
MATRIX_V2_MEDIA_PATH = '/_matrix/media/r0'
|
||||
|
||||
|
||||
class MatrixDiscoveryException(AppriseException):
|
||||
"""
|
||||
Apprise Matrix Exception Class
|
||||
"""
|
||||
|
||||
|
||||
# Extend HTTP Error Messages
|
||||
MATRIX_HTTP_ERROR_MAP = {
|
||||
403: 'Unauthorized - Invalid Token.',
|
||||
|
|
@ -164,9 +172,6 @@ class NotifyMatrix(NotifyBase):
|
|||
# Throttle a wee-bit to avoid thrashing
|
||||
request_rate_per_sec = 0.5
|
||||
|
||||
# Our Matrix API Version
|
||||
matrix_api_version = '3'
|
||||
|
||||
# How many retry attempts we'll make in the event the server asks us to
|
||||
# throttle back.
|
||||
default_retries = 2
|
||||
|
|
@ -175,15 +180,31 @@ class NotifyMatrix(NotifyBase):
|
|||
# the server doesn't remind us how long we shoul wait for
|
||||
default_wait_ms = 1000
|
||||
|
||||
# Our default is to no not use persistent storage beyond in-memory
|
||||
# reference
|
||||
storage_mode = PersistentStoreMode.AUTO
|
||||
|
||||
# Keep our cache for 20 days
|
||||
default_cache_expiry_sec = 60 * 60 * 24 * 20
|
||||
|
||||
# Used for server discovery
|
||||
discovery_base_key = '__discovery_base'
|
||||
discovery_identity_key = '__discovery_identity'
|
||||
|
||||
# Defines how long we cache our discovery for
|
||||
discovery_cache_length_sec = 86400
|
||||
|
||||
# Define object templates
|
||||
templates = (
|
||||
# Targets are ignored when using t2bot mode; only a token is required
|
||||
'{schema}://{token}',
|
||||
'{schema}://{user}@{token}',
|
||||
|
||||
# Disabled webhook
|
||||
# Matrix Server
|
||||
'{schema}://{user}:{password}@{host}/{targets}',
|
||||
'{schema}://{user}:{password}@{host}:{port}/{targets}',
|
||||
'{schema}://{token}@{host}/{targets}',
|
||||
'{schema}://{token}@{host}:{port}/{targets}',
|
||||
|
||||
# Webhook mode
|
||||
'{schema}://{user}:{token}@{host}/{targets}',
|
||||
|
|
@ -248,6 +269,11 @@ class NotifyMatrix(NotifyBase):
|
|||
'default': False,
|
||||
'map_to': 'include_image',
|
||||
},
|
||||
'discovery': {
|
||||
'name': _('Server Discovery'),
|
||||
'type': 'bool',
|
||||
'default': True,
|
||||
},
|
||||
'mode': {
|
||||
'name': _('Webhook Mode'),
|
||||
'type': 'choice:string',
|
||||
|
|
@ -275,7 +301,7 @@ class NotifyMatrix(NotifyBase):
|
|||
})
|
||||
|
||||
def __init__(self, targets=None, mode=None, msgtype=None, version=None,
|
||||
include_image=False, **kwargs):
|
||||
include_image=None, discovery=None, **kwargs):
|
||||
"""
|
||||
Initialize Matrix Object
|
||||
"""
|
||||
|
|
@ -297,11 +323,12 @@ class NotifyMatrix(NotifyBase):
|
|||
self.transaction_id = 0
|
||||
|
||||
# Place an image inline with the message body
|
||||
self.include_image = include_image
|
||||
self.include_image = self.template_args['image']['default'] \
|
||||
if include_image is None else include_image
|
||||
|
||||
# maintain a lookup of room alias's we already paired with their id
|
||||
# to speed up future requests
|
||||
self._room_cache = {}
|
||||
# Prepare Delegate Server Lookup Check
|
||||
self.discovery = self.template_args['discovery']['default'] \
|
||||
if discovery is None else discovery
|
||||
|
||||
# Setup our mode
|
||||
self.mode = self.template_args['mode']['default'] \
|
||||
|
|
@ -342,6 +369,7 @@ class NotifyMatrix(NotifyBase):
|
|||
.format(self.host)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
else:
|
||||
# Verify port if specified
|
||||
if self.port is not None and not (
|
||||
|
|
@ -353,6 +381,27 @@ class NotifyMatrix(NotifyBase):
|
|||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
if self.mode != MatrixWebhookMode.DISABLED:
|
||||
# Discovery only works when we're not using webhooks
|
||||
self.discovery = False
|
||||
|
||||
#
|
||||
# Initialize from cache if present
|
||||
#
|
||||
if self.mode != MatrixWebhookMode.T2BOT:
|
||||
# our home server gets populated after a login/registration
|
||||
self.home_server = self.store.get('home_server')
|
||||
|
||||
# our user_id gets populated after a login/registration
|
||||
self.user_id = self.store.get('user_id')
|
||||
|
||||
# This gets initialized after a login/registration
|
||||
self.access_token = self.store.get('access_token')
|
||||
|
||||
# This gets incremented for each request made against the v3 API
|
||||
self.transaction_id = 0 if not self.access_token \
|
||||
else self.store.get('transaction_id', 0)
|
||||
|
||||
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||
"""
|
||||
Perform Matrix Notification
|
||||
|
|
@ -564,6 +613,10 @@ class NotifyMatrix(NotifyBase):
|
|||
Perform Direct Matrix Server Notification (no webhook)
|
||||
"""
|
||||
|
||||
if self.access_token is None and self.password and not self.user:
|
||||
self.access_token = self.password
|
||||
self.transaction_id = uuid.uuid4()
|
||||
|
||||
if self.access_token is None:
|
||||
# We need to register
|
||||
if not self._login():
|
||||
|
|
@ -693,8 +746,12 @@ class NotifyMatrix(NotifyBase):
|
|||
|
||||
# Increment the transaction ID to avoid future messages being
|
||||
# recognized as retransmissions and ignored
|
||||
if self.version == MatrixVersion.V3:
|
||||
if self.version == MatrixVersion.V3 \
|
||||
and self.access_token != self.password:
|
||||
self.transaction_id += 1
|
||||
self.store.set(
|
||||
'transaction_id', self.transaction_id,
|
||||
expires=self.default_cache_expiry_sec)
|
||||
|
||||
if not postokay:
|
||||
# Notify our user
|
||||
|
|
@ -811,7 +868,18 @@ class NotifyMatrix(NotifyBase):
|
|||
self.home_server = response.get('home_server')
|
||||
self.user_id = response.get('user_id')
|
||||
|
||||
self.store.set(
|
||||
'access_token', self.access_token,
|
||||
expires=self.default_cache_expiry_sec)
|
||||
self.store.set(
|
||||
'home_server', self.home_server,
|
||||
expires=self.default_cache_expiry_sec)
|
||||
self.store.set(
|
||||
'user_id', self.user_id,
|
||||
expires=self.default_cache_expiry_sec)
|
||||
|
||||
if self.access_token is not None:
|
||||
# Store our token into our store
|
||||
self.logger.debug(
|
||||
'Registered successfully with Matrix server.')
|
||||
return True
|
||||
|
|
@ -828,32 +896,33 @@ class NotifyMatrix(NotifyBase):
|
|||
# Login not required; silently skip-over
|
||||
return True
|
||||
|
||||
if not (self.user and self.password):
|
||||
if (self.user and self.password):
|
||||
# Prepare our Authentication Payload
|
||||
if self.version == MatrixVersion.V3:
|
||||
payload = {
|
||||
'type': 'm.login.password',
|
||||
'identifier': {
|
||||
'type': 'm.id.user',
|
||||
'user': self.user,
|
||||
},
|
||||
'password': self.password,
|
||||
}
|
||||
|
||||
else:
|
||||
payload = {
|
||||
'type': 'm.login.password',
|
||||
'user': self.user,
|
||||
'password': self.password,
|
||||
}
|
||||
|
||||
else:
|
||||
# It's not possible to register since we need these 2 values to
|
||||
# make the action possible.
|
||||
self.logger.warning(
|
||||
'Failed to login to Matrix server: '
|
||||
'user/pass combo is missing.')
|
||||
'token or user/pass combo is missing.')
|
||||
return False
|
||||
|
||||
# Prepare our Authentication Payload
|
||||
if self.version == MatrixVersion.V3:
|
||||
payload = {
|
||||
'type': 'm.login.password',
|
||||
'identifier': {
|
||||
'type': 'm.id.user',
|
||||
'user': self.user,
|
||||
},
|
||||
'password': self.password,
|
||||
}
|
||||
|
||||
else:
|
||||
payload = {
|
||||
'type': 'm.login.password',
|
||||
'user': self.user,
|
||||
'password': self.password,
|
||||
}
|
||||
|
||||
# Build our URL
|
||||
postokay, response = self._fetch('/login', payload=payload)
|
||||
if not (postokay and isinstance(response, dict)):
|
||||
|
|
@ -870,6 +939,18 @@ class NotifyMatrix(NotifyBase):
|
|||
|
||||
self.logger.debug(
|
||||
'Authenticated successfully with Matrix server.')
|
||||
|
||||
# Store our token into our store
|
||||
self.store.set(
|
||||
'access_token', self.access_token,
|
||||
expires=self.default_cache_expiry_sec)
|
||||
self.store.set(
|
||||
'home_server', self.home_server,
|
||||
expires=self.default_cache_expiry_sec)
|
||||
self.store.set(
|
||||
'user_id', self.user_id,
|
||||
expires=self.default_cache_expiry_sec)
|
||||
|
||||
return True
|
||||
|
||||
def _logout(self):
|
||||
|
|
@ -907,8 +988,9 @@ class NotifyMatrix(NotifyBase):
|
|||
self.home_server = None
|
||||
self.user_id = None
|
||||
|
||||
# Clear our room cache
|
||||
self._room_cache = {}
|
||||
# clear our tokens
|
||||
self.store.clear(
|
||||
'access_token', 'home_server', 'user_id', 'transaction_id')
|
||||
|
||||
self.logger.debug(
|
||||
'Unauthenticated successfully with Matrix server.')
|
||||
|
|
@ -948,9 +1030,13 @@ class NotifyMatrix(NotifyBase):
|
|||
)
|
||||
|
||||
# Check our cache for speed:
|
||||
if room_id in self._room_cache:
|
||||
try:
|
||||
# We're done as we've already joined the channel
|
||||
return self._room_cache[room_id]['id']
|
||||
return self.store[room_id]['id']
|
||||
|
||||
except KeyError:
|
||||
# No worries, we'll try to acquire the info
|
||||
pass
|
||||
|
||||
# Build our URL
|
||||
path = '/join/{}'.format(NotifyMatrix.quote(room_id))
|
||||
|
|
@ -959,10 +1045,10 @@ class NotifyMatrix(NotifyBase):
|
|||
postokay, _ = self._fetch(path, payload=payload)
|
||||
if postokay:
|
||||
# Cache our entry for fast access later
|
||||
self._room_cache[room_id] = {
|
||||
self.store.set(room_id, {
|
||||
'id': room_id,
|
||||
'home_server': home_server,
|
||||
}
|
||||
})
|
||||
|
||||
return room_id if postokay else None
|
||||
|
||||
|
|
@ -984,9 +1070,13 @@ class NotifyMatrix(NotifyBase):
|
|||
room = '#{}:{}'.format(result.group('room'), home_server)
|
||||
|
||||
# Check our cache for speed:
|
||||
if room in self._room_cache:
|
||||
try:
|
||||
# We're done as we've already joined the channel
|
||||
return self._room_cache[room]['id']
|
||||
return self.store[room]['id']
|
||||
|
||||
except KeyError:
|
||||
# No worries, we'll try to acquire the info
|
||||
pass
|
||||
|
||||
# If we reach here, we need to join the channel
|
||||
|
||||
|
|
@ -997,11 +1087,12 @@ class NotifyMatrix(NotifyBase):
|
|||
postokay, response = self._fetch(path, payload=payload)
|
||||
if postokay:
|
||||
# Cache our entry for fast access later
|
||||
self._room_cache[room] = {
|
||||
self.store.set(room, {
|
||||
'id': response.get('room_id'),
|
||||
'home_server': home_server,
|
||||
}
|
||||
return self._room_cache[room]['id']
|
||||
})
|
||||
|
||||
return response.get('room_id')
|
||||
|
||||
# Try to create the channel
|
||||
return self._room_create(room)
|
||||
|
|
@ -1056,10 +1147,10 @@ class NotifyMatrix(NotifyBase):
|
|||
return None
|
||||
|
||||
# Cache our entry for fast access later
|
||||
self._room_cache[response.get('room_alias')] = {
|
||||
self.store.set(response.get('room_alias'), {
|
||||
'id': response.get('room_id'),
|
||||
'home_server': home_server,
|
||||
}
|
||||
})
|
||||
|
||||
return response.get('room_id')
|
||||
|
||||
|
|
@ -1122,14 +1213,16 @@ class NotifyMatrix(NotifyBase):
|
|||
|
||||
return None
|
||||
|
||||
def _fetch(self, path, payload=None, params=None, attachment=None,
|
||||
method='POST'):
|
||||
def _fetch(self, path, payload=None, params={}, attachment=None,
|
||||
method='POST', url_override=None):
|
||||
"""
|
||||
Wrapper to request.post() to manage it's response better and make
|
||||
the send() function cleaner and easier to maintain.
|
||||
|
||||
This function returns True if the _post was successful and False
|
||||
if it wasn't.
|
||||
|
||||
this function returns the status code if url_override is used
|
||||
"""
|
||||
|
||||
# Define our headers
|
||||
|
|
@ -1142,14 +1235,20 @@ class NotifyMatrix(NotifyBase):
|
|||
if self.access_token is not None:
|
||||
headers["Authorization"] = 'Bearer %s' % self.access_token
|
||||
|
||||
default_port = 443 if self.secure else 80
|
||||
# Server Discovery / Well-known URI
|
||||
if url_override:
|
||||
url = url_override
|
||||
|
||||
url = \
|
||||
'{schema}://{hostname}{port}'.format(
|
||||
schema='https' if self.secure else 'http',
|
||||
hostname=self.host,
|
||||
port='' if self.port is None
|
||||
or self.port == default_port else f':{self.port}')
|
||||
else:
|
||||
try:
|
||||
url = self.base_url
|
||||
|
||||
except MatrixDiscoveryException:
|
||||
# Discovery failed; we're done
|
||||
return (False, {})
|
||||
|
||||
# Default return status code
|
||||
status_code = requests.codes.internal_server_error
|
||||
|
||||
if path == '/upload':
|
||||
# FUTURE if self.version == MatrixVersion.V3:
|
||||
|
|
@ -1159,14 +1258,14 @@ class NotifyMatrix(NotifyBase):
|
|||
# FUTURE url += MATRIX_V2_MEDIA_PATH + path
|
||||
url += MATRIX_V2_MEDIA_PATH + path
|
||||
|
||||
params = {'filename': attachment.name}
|
||||
params.update({'filename': attachment.name})
|
||||
with open(attachment.path, 'rb') as fp:
|
||||
payload = fp.read()
|
||||
|
||||
# Update our content type
|
||||
headers['Content-Type'] = attachment.mimetype
|
||||
|
||||
else:
|
||||
elif not url_override:
|
||||
if self.version == MatrixVersion.V3:
|
||||
url += MATRIX_V3_API_PATH + path
|
||||
|
||||
|
|
@ -1188,7 +1287,9 @@ class NotifyMatrix(NotifyBase):
|
|||
# Decrement our throttle retry count
|
||||
retries -= 1
|
||||
|
||||
self.logger.debug('Matrix POST URL: %s (cert_verify=%r)' % (
|
||||
self.logger.debug('Matrix %s URL: %s (cert_verify=%r)' % (
|
||||
'POST' if method == 'POST' else (
|
||||
requests.put if method == 'PUT' else 'GET'),
|
||||
url, self.verify_certificate,
|
||||
))
|
||||
self.logger.debug('Matrix Payload: %s' % str(payload))
|
||||
|
|
@ -1200,18 +1301,21 @@ class NotifyMatrix(NotifyBase):
|
|||
r = fn(
|
||||
url,
|
||||
data=dumps(payload) if not attachment else payload,
|
||||
params=params,
|
||||
params=None if not params else params,
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
|
||||
# Store status code
|
||||
status_code = r.status_code
|
||||
|
||||
self.logger.debug(
|
||||
'Matrix Response: code=%d, %s' % (
|
||||
r.status_code, str(r.content)))
|
||||
response = loads(r.content)
|
||||
|
||||
if r.status_code == 429:
|
||||
if r.status_code == requests.codes.too_many_requests:
|
||||
wait = self.default_wait_ms / 1000
|
||||
try:
|
||||
wait = response['retry_after_ms'] / 1000
|
||||
|
|
@ -1252,7 +1356,8 @@ class NotifyMatrix(NotifyBase):
|
|||
'Response Details:\r\n{}'.format(r.content))
|
||||
|
||||
# Return; we're done
|
||||
return (False, response)
|
||||
return (
|
||||
False if not url_override else status_code, response)
|
||||
|
||||
except (AttributeError, TypeError, ValueError):
|
||||
# This gets thrown if we can't parse our JSON Response
|
||||
|
|
@ -1262,27 +1367,27 @@ class NotifyMatrix(NotifyBase):
|
|||
self.logger.warning('Invalid response from Matrix server.')
|
||||
self.logger.debug(
|
||||
'Response Details:\r\n{}'.format(r.content))
|
||||
return (False, {})
|
||||
return (False if not url_override else status_code, {})
|
||||
|
||||
except requests.RequestException as e:
|
||||
except (requests.TooManyRedirects, requests.RequestException) as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occurred while registering with Matrix'
|
||||
' server.')
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
self.logger.debug('Socket Exception: %s', str(e))
|
||||
# Return; we're done
|
||||
return (False, response)
|
||||
return (False if not url_override else status_code, response)
|
||||
|
||||
except (OSError, IOError) as e:
|
||||
self.logger.warning(
|
||||
'An I/O error occurred while reading {}.'.format(
|
||||
attachment.name if attachment else 'unknown file'))
|
||||
self.logger.debug('I/O Exception: %s' % str(e))
|
||||
return (False, {})
|
||||
self.logger.debug('I/O Exception: %s', str(e))
|
||||
return (False if not url_override else status_code, {})
|
||||
|
||||
return (True, response)
|
||||
return (True if not url_override else status_code, response)
|
||||
|
||||
# If we get here, we ran out of retries
|
||||
return (False, {})
|
||||
return (False if not url_override else status_code, {})
|
||||
|
||||
def __del__(self):
|
||||
"""
|
||||
|
|
@ -1292,6 +1397,15 @@ class NotifyMatrix(NotifyBase):
|
|||
# nothing to do
|
||||
return
|
||||
|
||||
if self.store.mode != PersistentStoreMode.MEMORY:
|
||||
# We no longer have to log out as we have persistant storage to
|
||||
# re-use our credentials with
|
||||
return
|
||||
|
||||
if self.access_token is not None \
|
||||
and self.access_token == self.password and not self.user:
|
||||
return
|
||||
|
||||
try:
|
||||
self._logout()
|
||||
|
||||
|
|
@ -1336,6 +1450,22 @@ class NotifyMatrix(NotifyBase):
|
|||
# the end user if we don't have to.
|
||||
pass
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
return (
|
||||
self.secure_protocol if self.secure else self.protocol,
|
||||
self.host if self.mode != MatrixWebhookMode.T2BOT
|
||||
else self.access_token,
|
||||
self.port if self.port else (443 if self.secure else 80),
|
||||
self.user if self.mode != MatrixWebhookMode.T2BOT else None,
|
||||
self.password if self.mode != MatrixWebhookMode.T2BOT else None,
|
||||
)
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
|
|
@ -1347,6 +1477,7 @@ class NotifyMatrix(NotifyBase):
|
|||
'mode': self.mode,
|
||||
'version': self.version,
|
||||
'msgtype': self.msgtype,
|
||||
'discovery': 'yes' if self.discovery else 'no',
|
||||
}
|
||||
|
||||
# Extend our parameters
|
||||
|
|
@ -1363,9 +1494,10 @@ class NotifyMatrix(NotifyBase):
|
|||
safe=''),
|
||||
)
|
||||
|
||||
elif self.user:
|
||||
auth = '{user}@'.format(
|
||||
user=NotifyMatrix.quote(self.user, safe=''),
|
||||
elif self.user or self.password:
|
||||
auth = '{value}@'.format(
|
||||
value=NotifyMatrix.quote(
|
||||
self.user if self.user else self.password, safe=''),
|
||||
)
|
||||
|
||||
default_port = 443 if self.secure else 80
|
||||
|
|
@ -1416,6 +1548,10 @@ class NotifyMatrix(NotifyBase):
|
|||
results['include_image'] = parse_bool(results['qsd'].get(
|
||||
'image', NotifyMatrix.template_args['image']['default']))
|
||||
|
||||
# Boolean to perform a server discovery
|
||||
results['discovery'] = parse_bool(results['qsd'].get(
|
||||
'discovery', NotifyMatrix.template_args['discovery']['default']))
|
||||
|
||||
# Get our mode
|
||||
results['mode'] = results['qsd'].get('mode')
|
||||
|
||||
|
|
@ -1443,6 +1579,11 @@ class NotifyMatrix(NotifyBase):
|
|||
if 'token' in results['qsd'] and len(results['qsd']['token']):
|
||||
results['password'] = NotifyMatrix.unquote(results['qsd']['token'])
|
||||
|
||||
elif not results['password'] and results['user']:
|
||||
# swap
|
||||
results['password'] = results['user']
|
||||
results['user'] = None
|
||||
|
||||
# Support the use of the version= or v= keyword
|
||||
if 'version' in results['qsd'] and len(results['qsd']['version']):
|
||||
results['version'] = \
|
||||
|
|
@ -1475,3 +1616,200 @@ class NotifyMatrix(NotifyBase):
|
|||
else '{}&{}'.format(result.group('params'), mode)))
|
||||
|
||||
return None
|
||||
|
||||
def server_discovery(self):
|
||||
"""
|
||||
Home Server Discovery as documented here:
|
||||
https://spec.matrix.org/v1.11/client-server-api/#well-known-uri
|
||||
"""
|
||||
|
||||
if not (self.discovery and self.secure):
|
||||
# Nothing further to do with insecure server setups
|
||||
return ''
|
||||
|
||||
# Get our content from cache
|
||||
base_url, identity_url = (
|
||||
self.store.get(self.discovery_base_key),
|
||||
self.store.get(self.discovery_identity_key),
|
||||
)
|
||||
|
||||
if not (base_url is None and identity_url is None):
|
||||
# We can use our cached value and return early
|
||||
return base_url
|
||||
|
||||
# 1. Extract the server name from the user’s Matrix ID by splitting
|
||||
# the Matrix ID at the first colon.
|
||||
verify_url = f'https://{self.host}/.well-known/matrix/client'
|
||||
code, wk_response = self._fetch(
|
||||
None, method='GET', url_override=verify_url)
|
||||
|
||||
# Output may look as follows:
|
||||
# {
|
||||
# "m.homeserver": {
|
||||
# "base_url": "https://matrix.example.com"
|
||||
# },
|
||||
# "m.identity_server": {
|
||||
# "base_url": "https://nuxref.com"
|
||||
# }
|
||||
# }
|
||||
|
||||
if code == requests.codes.not_found:
|
||||
# This is an acceptable response; we're done
|
||||
self.logger.debug(
|
||||
'Matrix Well-Known Base URI not found at %s', verify_url)
|
||||
|
||||
# Set our keys out for fast recall later on
|
||||
self.store.set(
|
||||
self.discovery_base_key, '',
|
||||
expires=self.discovery_cache_length_sec)
|
||||
self.store.set(
|
||||
self.discovery_identity_key, '',
|
||||
expires=self.discovery_cache_length_sec)
|
||||
return ''
|
||||
|
||||
elif code != requests.codes.ok:
|
||||
# We're done early as we couldn't load the results
|
||||
msg = 'Matrix Well-Known Base URI Discovery Failed'
|
||||
self.logger.warning(
|
||||
'%s - %s returned error code: %d', msg, verify_url, code)
|
||||
raise MatrixDiscoveryException(msg, error_code=code)
|
||||
|
||||
if not wk_response:
|
||||
# This is an acceptable response; we simply do nothing
|
||||
self.logger.debug(
|
||||
'Matrix Well-Known Base URI not defined %s', verify_url)
|
||||
|
||||
# Set our keys out for fast recall later on
|
||||
self.store.set(
|
||||
self.discovery_base_key, '',
|
||||
expires=self.discovery_cache_length_sec)
|
||||
self.store.set(
|
||||
self.discovery_identity_key, '',
|
||||
expires=self.discovery_cache_length_sec)
|
||||
return ''
|
||||
|
||||
#
|
||||
# Parse our m.homeserver information
|
||||
#
|
||||
try:
|
||||
base_url = wk_response['m.homeserver']['base_url'].rstrip('/')
|
||||
results = NotifyBase.parse_url(base_url, verify_host=True)
|
||||
|
||||
except (AttributeError, TypeError, KeyError):
|
||||
# AttributeError: result wasn't a string (rstrip failed)
|
||||
# TypeError : wk_response wasn't a dictionary
|
||||
# KeyError : wk_response not to standards
|
||||
results = None
|
||||
|
||||
if not results:
|
||||
msg = 'Matrix Well-Known Base URI Discovery Failed'
|
||||
self.logger.warning(
|
||||
'%s - m.homeserver payload is missing or invalid: %s',
|
||||
msg, str(wk_response))
|
||||
raise MatrixDiscoveryException(msg)
|
||||
|
||||
#
|
||||
# Our .well-known extraction was successful; now we need to verify
|
||||
# that the version information resolves.
|
||||
#
|
||||
verify_url = f'{base_url}/_matrix/client/versions'
|
||||
# Post our content
|
||||
code, response = self._fetch(
|
||||
None, method='GET', url_override=verify_url)
|
||||
if code != requests.codes.ok:
|
||||
# We're done early as we couldn't load the results
|
||||
msg = 'Matrix Well-Known Base URI Discovery Verification Failed'
|
||||
self.logger.warning(
|
||||
'%s - %s returned error code: %d', msg, verify_url, code)
|
||||
raise MatrixDiscoveryException(msg, error_code=code)
|
||||
|
||||
#
|
||||
# Phase 2: Handle m.identity_server IF defined
|
||||
#
|
||||
if 'm.identity_server' in wk_response:
|
||||
try:
|
||||
identity_url = \
|
||||
wk_response['m.identity_server']['base_url'].rstrip('/')
|
||||
results = NotifyBase.parse_url(identity_url, verify_host=True)
|
||||
|
||||
except (AttributeError, TypeError, KeyError):
|
||||
# AttributeError: result wasn't a string (rstrip failed)
|
||||
# TypeError : wk_response wasn't a dictionary
|
||||
# KeyError : wk_response not to standards
|
||||
results = None
|
||||
|
||||
if not results:
|
||||
msg = 'Matrix Well-Known Identity URI Discovery Failed'
|
||||
self.logger.warning(
|
||||
'%s - m.identity_server payload is missing or invalid: %s',
|
||||
msg, str(wk_response))
|
||||
raise MatrixDiscoveryException(msg)
|
||||
|
||||
#
|
||||
# Verify identity server found
|
||||
#
|
||||
verify_url = f'{identity_url}/_matrix/identity/v2'
|
||||
|
||||
# Post our content
|
||||
code, response = self._fetch(
|
||||
None, method='GET', url_override=verify_url)
|
||||
if code != requests.codes.ok:
|
||||
# We're done early as we couldn't load the results
|
||||
msg = 'Matrix Well-Known Identity URI Discovery Failed'
|
||||
self.logger.warning(
|
||||
'%s - %s returned error code: %d', msg, verify_url, code)
|
||||
raise MatrixDiscoveryException(msg, error_code=code)
|
||||
|
||||
# Update our cache
|
||||
self.store.set(
|
||||
self.discovery_identity_key, identity_url,
|
||||
# Add 2 seconds to prevent this key from expiring before base
|
||||
expires=self.discovery_cache_length_sec + 2)
|
||||
else:
|
||||
# No identity server
|
||||
self.store.set(
|
||||
self.discovery_identity_key, '',
|
||||
# Add 2 seconds to prevent this key from expiring before base
|
||||
expires=self.discovery_cache_length_sec + 2)
|
||||
|
||||
# Update our cache
|
||||
self.store.set(
|
||||
self.discovery_base_key, base_url,
|
||||
expires=self.discovery_cache_length_sec)
|
||||
|
||||
return base_url
|
||||
|
||||
@property
|
||||
def base_url(self):
|
||||
"""
|
||||
Returns the base_url if known
|
||||
"""
|
||||
try:
|
||||
base_url = self.server_discovery()
|
||||
if base_url:
|
||||
# We can use our cached value and return early
|
||||
return base_url
|
||||
|
||||
except MatrixDiscoveryException:
|
||||
self.store.clear(
|
||||
self.discovery_base_key, self.discovery_identity_key)
|
||||
raise
|
||||
|
||||
# If we get hear, we need to build our URL dynamically based on what
|
||||
# was provided to us during the plugins initialization
|
||||
default_port = 443 if self.secure else 80
|
||||
|
||||
return '{schema}://{hostname}{port}'.format(
|
||||
schema='https' if self.secure else 'http',
|
||||
hostname=self.host,
|
||||
port='' if self.port is None
|
||||
or self.port == default_port else f':{self.port}')
|
||||
|
||||
@property
|
||||
def identity_url(self):
|
||||
"""
|
||||
Returns the identity_url if known
|
||||
"""
|
||||
base_url = self.base_url
|
||||
identity_url = self.store.get(self.discovery_identity_key)
|
||||
return base_url if not identity_url else identity_url
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -36,15 +36,14 @@
|
|||
# - swap http with mmost
|
||||
# - drop /hooks/ reference
|
||||
|
||||
import re
|
||||
import requests
|
||||
from json import dumps
|
||||
|
||||
from .base import NotifyBase
|
||||
from ..common import NotifyImageSize
|
||||
from ..common import NotifyType
|
||||
from ..utils import parse_bool
|
||||
from ..utils import parse_list
|
||||
from ..utils import validate_regex
|
||||
from ..utils.parse import parse_bool, parse_list, validate_regex
|
||||
from ..locale import gettext_lazy as _
|
||||
|
||||
# Some Reference Locations:
|
||||
|
|
@ -72,9 +71,6 @@ class NotifyMattermost(NotifyBase):
|
|||
# A URL that takes you to the setup/help of the specific protocol
|
||||
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_mattermost'
|
||||
|
||||
# The default Mattermost port
|
||||
default_port = 8065
|
||||
|
||||
# Allows the user to specify the NotifyImageSize object
|
||||
image_size = NotifyImageSize.XY_72
|
||||
|
||||
|
|
@ -132,6 +128,9 @@ class NotifyMattermost(NotifyBase):
|
|||
'name': _('Channels'),
|
||||
'type': 'list:string',
|
||||
},
|
||||
'channel': {
|
||||
'alias_of': 'channels',
|
||||
},
|
||||
'image': {
|
||||
'name': _('Include Image'),
|
||||
'type': 'bool',
|
||||
|
|
@ -171,9 +170,6 @@ class NotifyMattermost(NotifyBase):
|
|||
# Optional Channels (strip off any channel prefix entries if present)
|
||||
self.channels = [x.lstrip('#') for x in parse_list(channels)]
|
||||
|
||||
if not self.port:
|
||||
self.port = self.default_port
|
||||
|
||||
# Place a thumbnail image inline with the message body
|
||||
self.include_image = include_image
|
||||
|
||||
|
|
@ -220,8 +216,8 @@ class NotifyMattermost(NotifyBase):
|
|||
payload['channel'] = channel
|
||||
|
||||
url = '{}://{}:{}{}/hooks/{}'.format(
|
||||
self.schema, self.host, self.port, self.fullpath,
|
||||
self.token)
|
||||
self.schema, self.host, self.port,
|
||||
self.fullpath.rstrip('/'), self.token)
|
||||
|
||||
self.logger.debug('Mattermost POST URL: %s (cert_verify=%r)' % (
|
||||
url, self.verify_certificate,
|
||||
|
|
@ -283,6 +279,18 @@ class NotifyMattermost(NotifyBase):
|
|||
# Return our overall status
|
||||
return not has_error
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
return (
|
||||
self.secure_protocol if self.secure else self.protocol,
|
||||
self.token, self.host, self.port, self.fullpath,
|
||||
)
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
|
|
@ -303,7 +311,7 @@ class NotifyMattermost(NotifyBase):
|
|||
params['channel'] = ','.join(
|
||||
[NotifyMattermost.quote(x, safe='') for x in self.channels])
|
||||
|
||||
default_port = 443 if self.secure else self.default_port
|
||||
default_port = 443 if self.secure else 80
|
||||
default_schema = self.secure_protocol if self.secure else self.protocol
|
||||
|
||||
# Determine if there is a botname present
|
||||
|
|
@ -321,8 +329,8 @@ class NotifyMattermost(NotifyBase):
|
|||
# never encode hostname since we're expecting it to be a valid
|
||||
# one
|
||||
hostname=self.host,
|
||||
port='' if not self.port or self.port == default_port
|
||||
else ':{}'.format(self.port),
|
||||
port='' if self.port is None or self.port == default_port
|
||||
else ':{}'.format(self.port),
|
||||
fullpath='/' if not self.fullpath else '{}/'.format(
|
||||
NotifyMattermost.quote(self.fullpath, safe='/')),
|
||||
token=self.pprint(self.token, privacy, safe=''),
|
||||
|
|
@ -357,16 +365,60 @@ class NotifyMattermost(NotifyBase):
|
|||
# Support both 'to' (for yaml configuration) and channel=
|
||||
if 'to' in results['qsd'] and len(results['qsd']['to']):
|
||||
# Allow the user to specify the channel to post to
|
||||
results['channels'].append(
|
||||
results['channels'].extend(
|
||||
NotifyMattermost.parse_list(results['qsd']['to']))
|
||||
|
||||
if 'channel' in results['qsd'] and len(results['qsd']['channel']):
|
||||
# Allow the user to specify the channel to post to
|
||||
results['channels'].append(
|
||||
results['channels'].extend(
|
||||
NotifyMattermost.parse_list(results['qsd']['channel']))
|
||||
|
||||
if 'channels' in results['qsd'] and len(results['qsd']['channels']):
|
||||
# Allow the user to specify the channel to post to
|
||||
results['channels'].extend(
|
||||
NotifyMattermost.parse_list(results['qsd']['channels']))
|
||||
|
||||
# Image manipulation
|
||||
results['include_image'] = \
|
||||
parse_bool(results['qsd'].get('image', False))
|
||||
results['include_image'] = parse_bool(results['qsd'].get(
|
||||
'image', NotifyMattermost.template_args['image']['default']))
|
||||
|
||||
return results
|
||||
|
||||
@staticmethod
|
||||
def parse_native_url(url):
|
||||
"""
|
||||
Support parsing the webhook straight from URL
|
||||
https://HOST:443/workflows/WORKFLOWID/triggers/manual/paths/invoke
|
||||
https://mattermost.HOST/hooks/TOKEN
|
||||
"""
|
||||
|
||||
# Match our workflows webhook URL and re-assemble
|
||||
result = re.match(
|
||||
r'^http(?P<secure>s?)://(?P<host>mattermost\.[A-Z0-9_.-]+)'
|
||||
r'(:(?P<port>[1-9][0-9]{0,5}))?'
|
||||
r'/hooks/'
|
||||
r'(?P<token>[A-Z0-9_-]+)/?'
|
||||
r'(?P<params>\?.+)?$', url, re.I)
|
||||
|
||||
if result:
|
||||
default_port = \
|
||||
int(result.group('port')) if result.group('port') else (
|
||||
443 if result.group('secure') else 80)
|
||||
|
||||
default_schema = \
|
||||
NotifyMattermost.secure_protocol \
|
||||
if result.group('secure') else NotifyMattermost.protocol
|
||||
|
||||
# Construct our URL
|
||||
return NotifyMattermost.parse_url(
|
||||
'{schema}://{host}{port}/{token}'
|
||||
'/{params}'.format(
|
||||
schema=default_schema,
|
||||
host=result.group('host'),
|
||||
port='' if not result.group('port')
|
||||
or int(result.group('port')) == default_port
|
||||
else f':{default_port}',
|
||||
token=result.group('token'),
|
||||
params='' if not result.group('params')
|
||||
else result.group('params')))
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -36,9 +36,7 @@ import requests
|
|||
|
||||
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 ..utils.parse import is_phone_no, parse_phone_no, validate_regex
|
||||
from ..locale import gettext_lazy as _
|
||||
|
||||
|
||||
|
|
@ -291,6 +289,15 @@ class NotifyMessageBird(NotifyBase):
|
|||
|
||||
return not has_error
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
return (self.secure_protocol, self.apikey, self.source)
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -49,7 +49,7 @@ from json import dumps
|
|||
|
||||
from .base import NotifyBase
|
||||
from ..common import NotifyType
|
||||
from ..utils import validate_regex
|
||||
from ..utils.parse import validate_regex
|
||||
from ..locale import gettext_lazy as _
|
||||
|
||||
|
||||
|
|
@ -64,8 +64,6 @@ class MisskeyVisibility:
|
|||
|
||||
FOLLOWERS = 'followers'
|
||||
|
||||
PRIVATE = 'private'
|
||||
|
||||
SPECIFIED = 'specified'
|
||||
|
||||
|
||||
|
|
@ -74,7 +72,6 @@ MISSKEY_VISIBILITIES = (
|
|||
MisskeyVisibility.PUBLIC,
|
||||
MisskeyVisibility.HOME,
|
||||
MisskeyVisibility.FOLLOWERS,
|
||||
MisskeyVisibility.PRIVATE,
|
||||
MisskeyVisibility.SPECIFIED,
|
||||
)
|
||||
|
||||
|
|
@ -191,6 +188,18 @@ class NotifyMisskey(NotifyBase):
|
|||
|
||||
return
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
return (
|
||||
self.secure_protocol if self.secure else self.protocol,
|
||||
self.token, self.host, self.port,
|
||||
)
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -41,8 +41,7 @@ from os.path import isfile
|
|||
from .base import NotifyBase
|
||||
from ..url import PrivacyMode
|
||||
from ..common import NotifyType
|
||||
from ..utils import parse_list
|
||||
from ..utils import parse_bool
|
||||
from ..utils.parse import parse_list, parse_bool
|
||||
from ..locale import gettext_lazy as _
|
||||
|
||||
# Default our global support flag
|
||||
|
|
@ -89,7 +88,7 @@ class NotifyMQTT(NotifyBase):
|
|||
|
||||
requirements = {
|
||||
# Define our required packaging in order to work
|
||||
'packages_required': 'paho-mqtt < 2.0.0'
|
||||
'packages_required': 'paho-mqtt != 2.0.*'
|
||||
}
|
||||
|
||||
# The default descriptive name associated with the Notification
|
||||
|
|
@ -204,10 +203,15 @@ class NotifyMQTT(NotifyBase):
|
|||
'type': 'bool',
|
||||
'default': False,
|
||||
},
|
||||
'retain': {
|
||||
'name': _('Retain Messages'),
|
||||
'type': 'bool',
|
||||
'default': False,
|
||||
},
|
||||
})
|
||||
|
||||
def __init__(self, targets=None, version=None, qos=None,
|
||||
client_id=None, session=None, **kwargs):
|
||||
client_id=None, session=None, retain=None, **kwargs):
|
||||
"""
|
||||
Initialize MQTT Object
|
||||
"""
|
||||
|
|
@ -230,6 +234,10 @@ class NotifyMQTT(NotifyBase):
|
|||
if session is None or not self.client_id \
|
||||
else parse_bool(session)
|
||||
|
||||
# Our Retain Message Flag
|
||||
self.retain = self.template_args['retain']['default'] \
|
||||
if retain is None else parse_bool(retain)
|
||||
|
||||
# Set up our Quality of Service (QoS)
|
||||
try:
|
||||
self.qos = self.template_args['qos']['default'] \
|
||||
|
|
@ -376,7 +384,7 @@ class NotifyMQTT(NotifyBase):
|
|||
self.logger.debug('MQTT Payload: %s' % str(body))
|
||||
|
||||
result = self.client.publish(
|
||||
topic, payload=body, qos=self.qos, retain=False)
|
||||
topic, payload=body, qos=self.qos, retain=self.retain)
|
||||
|
||||
if result.rc != mqtt.MQTT_ERR_SUCCESS:
|
||||
# Toggle our status
|
||||
|
|
@ -429,6 +437,23 @@ class NotifyMQTT(NotifyBase):
|
|||
|
||||
return not has_error
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
return (
|
||||
self.secure_protocol if self.secure else self.protocol,
|
||||
self.user, self.password, self.host,
|
||||
self.port if self.port else (
|
||||
self.mqtt_secure_port if self.secure
|
||||
else self.mqtt_insecure_port),
|
||||
self.fullpath.rstrip('/'),
|
||||
self.client_id,
|
||||
)
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
|
|
@ -439,6 +464,7 @@ class NotifyMQTT(NotifyBase):
|
|||
'version': self.version,
|
||||
'qos': str(self.qos),
|
||||
'session': 'yes' if self.session else 'no',
|
||||
'retain': 'yes' if self.retain else 'no',
|
||||
}
|
||||
|
||||
if self.client_id:
|
||||
|
|
@ -518,6 +544,10 @@ class NotifyMQTT(NotifyBase):
|
|||
if 'session' in results['qsd'] and len(results['qsd']['session']):
|
||||
results['session'] = parse_bool(results['qsd']['session'])
|
||||
|
||||
# Message Retain Flag
|
||||
if 'retain' in results['qsd'] and len(results['qsd']['retain']):
|
||||
results['retain'] = parse_bool(results['qsd']['retain'])
|
||||
|
||||
# The MQTT Quality of Service to use
|
||||
if 'qos' in results['qsd'] and len(results['qsd']['qos']):
|
||||
results['qos'] = \
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -40,9 +40,8 @@ import requests
|
|||
from json import dumps
|
||||
from .base import NotifyBase
|
||||
from ..common import NotifyType
|
||||
from ..utils import is_phone_no
|
||||
from ..utils import parse_phone_no, parse_bool
|
||||
from ..utils import validate_regex
|
||||
from ..utils.parse import (
|
||||
is_phone_no, parse_phone_no, parse_bool, validate_regex)
|
||||
from ..locale import gettext_lazy as _
|
||||
|
||||
|
||||
|
|
@ -310,6 +309,15 @@ class NotifyMSG91(NotifyBase):
|
|||
|
||||
return True
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
return (self.secure_protocol, self.template, self.authkey)
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -85,10 +85,8 @@ from .base import NotifyBase
|
|||
from ..common import NotifyImageSize
|
||||
from ..common import NotifyType
|
||||
from ..common import NotifyFormat
|
||||
from ..utils import parse_bool
|
||||
from ..utils import validate_regex
|
||||
from ..utils import apply_template
|
||||
from ..utils import TemplateType
|
||||
from ..utils.parse import parse_bool, validate_regex
|
||||
from ..utils.templates import apply_template, TemplateType
|
||||
from ..apprise_attachment import AppriseAttachment
|
||||
from ..locale import gettext_lazy as _
|
||||
|
||||
|
|
@ -118,6 +116,9 @@ class NotifyMSTeams(NotifyBase):
|
|||
notify_url_v2 = 'https://{team}.webhook.office.com/webhookb2/' \
|
||||
'{token_a}/IncomingWebhook/{token_b}/{token_c}'
|
||||
|
||||
notify_url_v3 = 'https://{team}.webhook.office.com/webhookb2/' \
|
||||
'{token_a}/IncomingWebhook/{token_b}/{token_c}/{token_d}'
|
||||
|
||||
# Allows the user to specify the NotifyImageSize object
|
||||
image_size = NotifyImageSize.XY_72
|
||||
|
||||
|
|
@ -176,6 +177,15 @@ class NotifyMSTeams(NotifyBase):
|
|||
'required': True,
|
||||
'regex': (r'^[a-z0-9-]+$', 'i'),
|
||||
},
|
||||
# Token required as part of the API request
|
||||
# /........./........./........./DDDDDDDDDDDDDDDDD
|
||||
'token_d': {
|
||||
'name': _('Token D'),
|
||||
'type': 'string',
|
||||
'private': True,
|
||||
'required': False,
|
||||
'regex': (r'^V2[a-zA-Z0-9-_]+$', 'i'),
|
||||
},
|
||||
})
|
||||
|
||||
# Define our template arguments
|
||||
|
|
@ -189,7 +199,7 @@ class NotifyMSTeams(NotifyBase):
|
|||
'version': {
|
||||
'name': _('Version'),
|
||||
'type': 'choice:int',
|
||||
'values': (1, 2),
|
||||
'values': (1, 2, 3),
|
||||
'default': 2,
|
||||
},
|
||||
'template': {
|
||||
|
|
@ -207,8 +217,9 @@ class NotifyMSTeams(NotifyBase):
|
|||
},
|
||||
}
|
||||
|
||||
def __init__(self, token_a, token_b, token_c, team=None, version=None,
|
||||
include_image=True, template=None, tokens=None, **kwargs):
|
||||
def __init__(self, token_a, token_b, token_c, token_d=None, team=None,
|
||||
version=None, include_image=True, template=None, tokens=None,
|
||||
**kwargs):
|
||||
"""
|
||||
Initialize Microsoft Teams Object
|
||||
|
||||
|
|
@ -271,6 +282,9 @@ class NotifyMSTeams(NotifyBase):
|
|||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
self.token_d = validate_regex(
|
||||
token_d, *self.template_tokens['token_d']['regex'])
|
||||
|
||||
# Place a thumbnail image inline with the message body
|
||||
self.include_image = include_image
|
||||
|
||||
|
|
@ -293,8 +307,12 @@ class NotifyMSTeams(NotifyBase):
|
|||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
# else: NoneType - this is okay
|
||||
return
|
||||
self.logger.deprecate(
|
||||
"Microsoft is deprecating their MSTeams webhooks on "
|
||||
"December 31, 2025. It is advised that you switch to "
|
||||
"Microsoft Power Automate (already supported by Apprise as "
|
||||
"workflows://. For more information visit: "
|
||||
"https://github.com/caronc/apprise/wiki/Notify_workflows")
|
||||
|
||||
def gen_payload(self, body, title='', notify_type=NotifyType.INFO,
|
||||
**kwargs):
|
||||
|
|
@ -398,17 +416,28 @@ class NotifyMSTeams(NotifyBase):
|
|||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
notify_url = self.notify_url_v2.format(
|
||||
team=self.team,
|
||||
token_a=self.token_a,
|
||||
token_b=self.token_b,
|
||||
token_c=self.token_c,
|
||||
) if self.version > 1 else \
|
||||
self.notify_url_v1.format(
|
||||
if self.version == 1:
|
||||
notify_url = self.notify_url_v1.format(
|
||||
token_a=self.token_a,
|
||||
token_b=self.token_b,
|
||||
token_c=self.token_c)
|
||||
|
||||
if self.version == 2:
|
||||
notify_url = self.notify_url_v2.format(
|
||||
team=self.team,
|
||||
token_a=self.token_a,
|
||||
token_b=self.token_b,
|
||||
token_c=self.token_c,
|
||||
)
|
||||
if self.version == 3:
|
||||
notify_url = self.notify_url_v3.format(
|
||||
team=self.team,
|
||||
token_a=self.token_a,
|
||||
token_b=self.token_b,
|
||||
token_c=self.token_c,
|
||||
token_d=self.token_d,
|
||||
)
|
||||
|
||||
# Generate our payload if it's possible
|
||||
payload = self.gen_payload(
|
||||
body=body, title=title, notify_type=notify_type, **kwargs)
|
||||
|
|
@ -463,6 +492,19 @@ class NotifyMSTeams(NotifyBase):
|
|||
|
||||
return True
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
return (
|
||||
self.secure_protocol,
|
||||
self.team if self.version > 1 else None,
|
||||
self.token_a, self.token_b, self.token_c,
|
||||
)
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
|
|
@ -485,8 +527,20 @@ class NotifyMSTeams(NotifyBase):
|
|||
# Store any template entries if specified
|
||||
params.update({':{}'.format(k): v for k, v in self.tokens.items()})
|
||||
|
||||
if self.version > 1:
|
||||
return '{schema}://{team}/{token_a}/{token_b}/{token_c}/'\
|
||||
result = None
|
||||
|
||||
if self.version == 1:
|
||||
result = '{schema}://{token_a}/{token_b}/{token_c}/'\
|
||||
'?{params}'.format(
|
||||
schema=self.secure_protocol,
|
||||
token_a=self.pprint(self.token_a, privacy, safe='@'),
|
||||
token_b=self.pprint(self.token_b, privacy, safe=''),
|
||||
token_c=self.pprint(self.token_c, privacy, safe=''),
|
||||
params=NotifyMSTeams.urlencode(params),
|
||||
)
|
||||
|
||||
if self.version == 2:
|
||||
result = '{schema}://{team}/{token_a}/{token_b}/{token_c}/'\
|
||||
'?{params}'.format(
|
||||
schema=self.secure_protocol,
|
||||
team=NotifyMSTeams.quote(self.team, safe=''),
|
||||
|
|
@ -496,15 +550,18 @@ class NotifyMSTeams(NotifyBase):
|
|||
params=NotifyMSTeams.urlencode(params),
|
||||
)
|
||||
|
||||
else: # Version 1
|
||||
return '{schema}://{token_a}/{token_b}/{token_c}/'\
|
||||
'?{params}'.format(
|
||||
if self.version == 3:
|
||||
result = '{schema}://{team}/{token_a}/{token_b}/{token_c}/'\
|
||||
'{token_d}/?{params}'.format(
|
||||
schema=self.secure_protocol,
|
||||
token_a=self.pprint(self.token_a, privacy, safe='@'),
|
||||
team=NotifyMSTeams.quote(self.team, safe=''),
|
||||
token_a=self.pprint(self.token_a, privacy, safe=''),
|
||||
token_b=self.pprint(self.token_b, privacy, safe=''),
|
||||
token_c=self.pprint(self.token_c, privacy, safe=''),
|
||||
token_d=self.pprint(self.token_d, privacy, safe=''),
|
||||
params=NotifyMSTeams.urlencode(params),
|
||||
)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
|
|
@ -543,6 +600,8 @@ class NotifyMSTeams(NotifyBase):
|
|||
else NotifyMSTeams.unquote(entries.pop(0))
|
||||
results['token_c'] = None if not entries \
|
||||
else NotifyMSTeams.unquote(entries.pop(0))
|
||||
results['token_d'] = None if not entries \
|
||||
else NotifyMSTeams.unquote(entries.pop(0))
|
||||
|
||||
# Get Image
|
||||
results['include_image'] = \
|
||||
|
|
@ -564,8 +623,13 @@ class NotifyMSTeams(NotifyBase):
|
|||
NotifyMSTeams.unquote(results['qsd']['version'])
|
||||
|
||||
else:
|
||||
version = 1
|
||||
if results.get('team'):
|
||||
version = 2
|
||||
if results.get('token_d'):
|
||||
version = 3
|
||||
# Set our version if not otherwise set
|
||||
results['version'] = 1 if not results.get('team') else 2
|
||||
results['version'] = version
|
||||
|
||||
# Store our tokens
|
||||
results['tokens'] = results['qsd:']
|
||||
|
|
@ -580,11 +644,38 @@ class NotifyMSTeams(NotifyBase):
|
|||
|
||||
New Hook Support:
|
||||
https://team-name.office.com/webhook/ABCD/IncomingWebhook/DEFG/HIJK
|
||||
|
||||
Newer Hook Support:
|
||||
https://team-name.office.com/webhook/ABCD/IncomingWebhook/DEFG/HIJK/V2LMNOP
|
||||
"""
|
||||
|
||||
# We don't need to do incredibly details token matching as the purpose
|
||||
# of this is just to detect that were dealing with an msteams url
|
||||
# token parsing will occur once we initialize the function
|
||||
result = re.match(
|
||||
r'^https?://(?P<team>[^.]+)(?P<v2a>\.webhook)?\.office\.com/'
|
||||
r'webhook(?P<v2b>b2)?/'
|
||||
r'(?P<token_a>[A-Z0-9-]+@[A-Z0-9-]+)/'
|
||||
r'IncomingWebhook/'
|
||||
r'(?P<token_b>[A-Z0-9]+)/'
|
||||
r'(?P<token_c>[A-Z0-9-]+)/'
|
||||
r'(?P<token_d>V2[A-Z0-9-_]+)/?'
|
||||
r'(?P<params>\?.+)?$', url, re.I)
|
||||
|
||||
if result:
|
||||
# Version 3 URL
|
||||
return NotifyMSTeams.parse_url(
|
||||
'{schema}://{team}/{token_a}/{token_b}/{token_c}/{token_d}'
|
||||
'/{params}'.format(
|
||||
schema=NotifyMSTeams.secure_protocol,
|
||||
team=result.group('team'),
|
||||
token_a=result.group('token_a'),
|
||||
token_b=result.group('token_b'),
|
||||
token_c=result.group('token_c'),
|
||||
token_d=result.group('token_d'),
|
||||
params='' if not result.group('params')
|
||||
else result.group('params')))
|
||||
|
||||
result = re.match(
|
||||
r'^https?://(?P<team>[^.]+)(?P<v2a>\.webhook)?\.office\.com/'
|
||||
r'webhook(?P<v2b>b2)?/'
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -31,7 +31,7 @@ import requests
|
|||
from .base import NotifyBase
|
||||
from ..url import PrivacyMode
|
||||
from ..common import NotifyType
|
||||
from ..utils import parse_list
|
||||
from ..utils.parse import parse_list
|
||||
from ..locale import gettext_lazy as _
|
||||
|
||||
|
||||
|
|
@ -278,6 +278,18 @@ class NotifyNextcloud(NotifyBase):
|
|||
|
||||
return not has_error
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
return (
|
||||
self.secure_protocol if self.secure else self.protocol,
|
||||
self.user, self.password, self.host, self.port,
|
||||
)
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -32,7 +32,7 @@ from json import dumps
|
|||
from .base import NotifyBase
|
||||
from ..url import PrivacyMode
|
||||
from ..common import NotifyType
|
||||
from ..utils import parse_list
|
||||
from ..utils.parse import parse_list
|
||||
from ..locale import gettext_lazy as _
|
||||
|
||||
|
||||
|
|
@ -253,6 +253,18 @@ class NotifyNextcloudTalk(NotifyBase):
|
|||
|
||||
return not has_error
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
return (
|
||||
self.secure_protocol if self.secure else self.protocol,
|
||||
self.user, self.password, self.host, self.port,
|
||||
)
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -46,7 +46,7 @@ import requests
|
|||
from .base import NotifyBase
|
||||
from ..url import PrivacyMode
|
||||
from ..common import NotifyType
|
||||
from ..utils import validate_regex
|
||||
from ..utils.parse import validate_regex
|
||||
from ..locale import gettext_lazy as _
|
||||
|
||||
|
||||
|
|
@ -278,6 +278,19 @@ class NotifyNotica(NotifyBase):
|
|||
|
||||
return True
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
return (
|
||||
self.secure_protocol if self.secure else self.protocol,
|
||||
self.mode, self.token, self.user, self.password, self.host,
|
||||
self.port,
|
||||
)
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -35,8 +35,8 @@ from .base import NotifyBase
|
|||
from ..common import NotifyType
|
||||
from ..locale import gettext_lazy as _
|
||||
from ..common import NotifyImageSize
|
||||
from ..utils import parse_list, parse_bool
|
||||
from ..utils import validate_regex
|
||||
from ..utils.parse import parse_list, parse_bool, validate_regex
|
||||
from .discord import USER_ROLE_DETECTION_RE
|
||||
|
||||
# Used to break path apart into list of channels
|
||||
CHANNEL_LIST_DELIM = re.compile(r'[ \t\r\n,#\\/]+')
|
||||
|
|
@ -118,14 +118,6 @@ class NotifyNotifiarr(NotifyBase):
|
|||
'apikey': {
|
||||
'alias_of': 'apikey',
|
||||
},
|
||||
'discord_user': {
|
||||
'name': _('Ping Discord User'),
|
||||
'type': 'int',
|
||||
},
|
||||
'discord_role': {
|
||||
'name': _('Ping Discord Role'),
|
||||
'type': 'int',
|
||||
},
|
||||
'event': {
|
||||
'name': _('Discord Event ID'),
|
||||
'type': 'int',
|
||||
|
|
@ -149,7 +141,6 @@ class NotifyNotifiarr(NotifyBase):
|
|||
})
|
||||
|
||||
def __init__(self, apikey=None, include_image=None,
|
||||
discord_user=None, discord_role=None,
|
||||
event=None, targets=None, source=None, **kwargs):
|
||||
"""
|
||||
Initialize Notifiarr Object
|
||||
|
|
@ -172,30 +163,6 @@ class NotifyNotifiarr(NotifyBase):
|
|||
if isinstance(include_image, bool) \
|
||||
else self.template_args['image']['default']
|
||||
|
||||
# Set up our user if specified
|
||||
self.discord_user = 0
|
||||
if discord_user:
|
||||
try:
|
||||
self.discord_user = int(discord_user)
|
||||
|
||||
except (ValueError, TypeError):
|
||||
msg = 'An invalid Notifiarr User ID ' \
|
||||
'({}) was specified.'.format(discord_user)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
# Set up our role if specified
|
||||
self.discord_role = 0
|
||||
if discord_role:
|
||||
try:
|
||||
self.discord_role = int(discord_role)
|
||||
|
||||
except (ValueError, TypeError):
|
||||
msg = 'An invalid Notifiarr Role ID ' \
|
||||
'({}) was specified.'.format(discord_role)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
# Prepare our source (if set)
|
||||
self.source = validate_regex(source)
|
||||
|
||||
|
|
@ -231,6 +198,18 @@ class NotifyNotifiarr(NotifyBase):
|
|||
|
||||
return
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
return (
|
||||
self.secure_protocol if self.secure else self.protocol,
|
||||
self.apikey,
|
||||
)
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
|
|
@ -244,12 +223,6 @@ class NotifyNotifiarr(NotifyBase):
|
|||
if self.source:
|
||||
params['source'] = self.source
|
||||
|
||||
if self.discord_user:
|
||||
params['discord_user'] = self.discord_user
|
||||
|
||||
if self.discord_role:
|
||||
params['discord_role'] = self.discord_role
|
||||
|
||||
if self.event:
|
||||
params['event'] = self.event
|
||||
|
||||
|
|
@ -287,6 +260,29 @@ class NotifyNotifiarr(NotifyBase):
|
|||
# Acquire image_url
|
||||
image_url = self.image_url(notify_type)
|
||||
|
||||
# Define our mentions
|
||||
mentions = {
|
||||
'pingUser': [],
|
||||
'pingRole': [],
|
||||
'content': [],
|
||||
}
|
||||
|
||||
# parse for user id's <@123> and role IDs <@&456>
|
||||
results = USER_ROLE_DETECTION_RE.findall(body)
|
||||
if results:
|
||||
for (is_role, no, value) in results:
|
||||
if value:
|
||||
# @everybody, @admin, etc - unsupported
|
||||
mentions['content'].append(f'@{value}')
|
||||
|
||||
elif is_role:
|
||||
mentions['pingRole'].append(no)
|
||||
mentions['content'].append(f'<@&{no}>')
|
||||
|
||||
else: # is_user
|
||||
mentions['pingUser'].append(no)
|
||||
mentions['content'].append(f'<@{no}>')
|
||||
|
||||
for idx, channel in enumerate(self.targets['channels']):
|
||||
# prepare Notifiarr Object
|
||||
payload = {
|
||||
|
|
@ -301,14 +297,17 @@ class NotifyNotifiarr(NotifyBase):
|
|||
'discord': {
|
||||
'color': self.color(notify_type),
|
||||
'ping': {
|
||||
'pingUser': self.discord_user
|
||||
if not idx and self.discord_user else 0,
|
||||
'pingRole': self.discord_role
|
||||
if not idx and self.discord_role else 0,
|
||||
# Only 1 user is supported, so truncate the rest
|
||||
'pingUser': 0 if not mentions['pingUser']
|
||||
else mentions['pingUser'][0],
|
||||
# Only 1 role is supported, so truncate the rest
|
||||
'pingRole': 0 if not mentions['pingRole']
|
||||
else mentions['pingRole'][0],
|
||||
},
|
||||
'text': {
|
||||
'title': title,
|
||||
'content': '',
|
||||
'content': '' if not mentions['content']
|
||||
else '👉 ' + ' '.join(mentions['content']),
|
||||
'description': body,
|
||||
'footer': self.app_desc,
|
||||
},
|
||||
|
|
@ -410,17 +409,6 @@ class NotifyNotifiarr(NotifyBase):
|
|||
# Get channels
|
||||
results['targets'] = NotifyNotifiarr.split_path(results['fullpath'])
|
||||
|
||||
if 'discord_user' in results['qsd'] and \
|
||||
len(results['qsd']['discord_user']):
|
||||
results['discord_user'] = \
|
||||
NotifyNotifiarr.unquote(
|
||||
results['qsd']['discord_user'])
|
||||
|
||||
if 'discord_role' in results['qsd'] and \
|
||||
len(results['qsd']['discord_role']):
|
||||
results['discord_role'] = \
|
||||
NotifyNotifiarr.unquote(results['qsd']['discord_role'])
|
||||
|
||||
if 'event' in results['qsd'] and \
|
||||
len(results['qsd']['event']):
|
||||
results['event'] = \
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -46,8 +46,7 @@ import requests
|
|||
|
||||
from .base import NotifyBase
|
||||
from ..common import NotifyType
|
||||
from ..utils import parse_bool
|
||||
from ..utils import validate_regex
|
||||
from ..utils.parse import parse_bool, validate_regex
|
||||
from ..locale import gettext_lazy as _
|
||||
|
||||
|
||||
|
|
@ -197,6 +196,15 @@ class NotifyNotifico(NotifyBase):
|
|||
)
|
||||
return
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
return (self.secure_protocol, self.project_id, self.msghook)
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -46,11 +46,8 @@ from ..common import NotifyFormat
|
|||
from ..common import NotifyType
|
||||
from ..common import NotifyImageSize
|
||||
from ..locale import gettext_lazy as _
|
||||
from ..utils import parse_list
|
||||
from ..utils import parse_bool
|
||||
from ..utils import is_hostname
|
||||
from ..utils import is_ipaddr
|
||||
from ..utils import validate_regex
|
||||
from ..utils.parse import (
|
||||
parse_list, parse_bool, is_hostname, is_ipaddr, validate_regex)
|
||||
from ..url import PrivacyMode
|
||||
from ..attachment.base import AttachBase
|
||||
from ..attachment.memory import AttachMemory
|
||||
|
|
@ -656,6 +653,34 @@ class NotifyNtfy(NotifyBase):
|
|||
|
||||
return False, response
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
|
||||
kwargs = [
|
||||
self.secure_protocol if self.mode == NtfyMode.CLOUD else (
|
||||
self.secure_protocol if self.secure else self.protocol),
|
||||
self.host if self.mode == NtfyMode.PRIVATE else '',
|
||||
443 if self.mode == NtfyMode.CLOUD else (
|
||||
self.port if self.port else (443 if self.secure else 80)),
|
||||
]
|
||||
|
||||
if self.mode == NtfyMode.PRIVATE:
|
||||
if self.auth == NtfyAuth.BASIC:
|
||||
kwargs.extend([
|
||||
self.user if self.user else None,
|
||||
self.password if self.password else None,
|
||||
])
|
||||
|
||||
elif self.token: # NtfyAuth.TOKEN also
|
||||
kwargs.append(self.token)
|
||||
|
||||
return kwargs
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -33,45 +33,25 @@
|
|||
# Information on sending an email:
|
||||
# https://docs.microsoft.com/en-us/graph/api/user-sendmail\
|
||||
# ?view=graph-rest-1.0&tabs=http
|
||||
|
||||
# Steps to get your Microsoft Client ID, Client Secret, and Tenant ID:
|
||||
# 1. You should have valid Microsoft personal account. Go to Azure Portal
|
||||
# 2. Go to -> Microsoft Active Directory --> App Registrations
|
||||
# 3. Click new -> give any name (your choice) in Name field -> select
|
||||
# personal Microsoft accounts only --> Register
|
||||
# 4. Now you have your client_id & Tenant id.
|
||||
# 5. To create client_secret , go to active directory ->
|
||||
# Certificate & Tokens -> New client secret
|
||||
# **This is auto-generated string which may have '@' and '?'
|
||||
# characters in it. You should encode these to prevent
|
||||
# from having any issues.**
|
||||
# 6. Now need to set permission Active directory -> API permissions ->
|
||||
# Add permission (search mail) , add relevant permission.
|
||||
# 7. Set the redirect uri (Web) to:
|
||||
# https://login.microsoftonline.com/common/oauth2/nativeclient
|
||||
#
|
||||
# ...and click register.
|
||||
# Note: One must set up Application Permissions (not Delegated Permissions)
|
||||
# - Scopes required: Mail.Send
|
||||
# - For Large Attachments: Mail.ReadWrite
|
||||
# - For Email Lookups: User.Read.All
|
||||
#
|
||||
# This needs to be inserted into the "Redirect URI" text box as simply
|
||||
# checking the check box next to this link seems to be insufficient.
|
||||
# This is the default redirect uri used by this library, but you can use
|
||||
# any other if you want.
|
||||
#
|
||||
# 8. Now you're good to go
|
||||
|
||||
import requests
|
||||
import json
|
||||
from uuid import uuid4
|
||||
from datetime import datetime
|
||||
from datetime import timedelta
|
||||
from json import loads
|
||||
from json import dumps
|
||||
from .base import NotifyBase
|
||||
from .. import exception
|
||||
from ..url import PrivacyMode
|
||||
from ..common import NotifyFormat
|
||||
from ..common import NotifyType
|
||||
from ..utils import is_email
|
||||
from ..utils import parse_emails
|
||||
from ..utils import validate_regex
|
||||
from ..utils.parse import is_email, parse_emails, validate_regex
|
||||
from ..locale import gettext_lazy as _
|
||||
from ..common import PersistentStoreMode
|
||||
|
||||
|
||||
class NotifyOffice365(NotifyBase):
|
||||
|
|
@ -86,7 +66,7 @@ class NotifyOffice365(NotifyBase):
|
|||
service_url = 'https://office.com/'
|
||||
|
||||
# The default protocol
|
||||
secure_protocol = 'o365'
|
||||
secure_protocol = ('azure', 'o365')
|
||||
|
||||
# Allow 300 requests per minute.
|
||||
# 60/300 = 0.2
|
||||
|
|
@ -101,6 +81,20 @@ class NotifyOffice365(NotifyBase):
|
|||
# Authentication URL
|
||||
auth_url = 'https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token'
|
||||
|
||||
# Support attachments
|
||||
attachment_support = True
|
||||
|
||||
# Our default is to no not use persistent storage beyond in-memory
|
||||
# reference
|
||||
storage_mode = PersistentStoreMode.AUTO
|
||||
|
||||
# the maximum size an attachment can be for it to be allowed to be
|
||||
# uploaded inline with the current email going out (one http post)
|
||||
# Anything larger than this and a second PUT request is required to
|
||||
# the outlook server to post the content through reference.
|
||||
# Currently (as of 2025.10.06) this was documented to be 3MB
|
||||
outlook_attachment_inline_max = 3145728
|
||||
|
||||
# Use all the direct application permissions you have configured for your
|
||||
# app. The endpoint should issue a token for the ones associated with the
|
||||
# resource you want to use.
|
||||
|
|
@ -113,8 +107,9 @@ class NotifyOffice365(NotifyBase):
|
|||
|
||||
# Define object templates
|
||||
templates = (
|
||||
'{schema}://{tenant}:{email}/{client_id}/{secret}',
|
||||
'{schema}://{tenant}:{email}/{client_id}/{secret}/{targets}',
|
||||
# Send as user (only supported method)
|
||||
'{schema}://{source}/{tenant}/{client_id}/{secret}',
|
||||
'{schema}://{source}/{tenant}/{client_id}/{secret}/{targets}',
|
||||
)
|
||||
|
||||
# Define our template tokens
|
||||
|
|
@ -126,8 +121,8 @@ class NotifyOffice365(NotifyBase):
|
|||
'private': True,
|
||||
'regex': (r'^[a-z0-9-]+$', 'i'),
|
||||
},
|
||||
'email': {
|
||||
'name': _('Account Email'),
|
||||
'source': {
|
||||
'name': _('Account Email or Object ID'),
|
||||
'type': 'string',
|
||||
'required': True,
|
||||
},
|
||||
|
|
@ -176,7 +171,7 @@ class NotifyOffice365(NotifyBase):
|
|||
},
|
||||
})
|
||||
|
||||
def __init__(self, tenant, email, client_id, secret,
|
||||
def __init__(self, tenant, client_id, secret, source=None,
|
||||
targets=None, cc=None, bcc=None, **kwargs):
|
||||
"""
|
||||
Initialize Office 365 Object
|
||||
|
|
@ -192,15 +187,8 @@ class NotifyOffice365(NotifyBase):
|
|||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
result = is_email(email)
|
||||
if not result:
|
||||
msg = 'An invalid Office 365 Email Account ID' \
|
||||
'({}) was specified.'.format(email)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
# Otherwise store our the email address
|
||||
self.email = result['full_email']
|
||||
# Store our email/ObjectID Source
|
||||
self.source = source
|
||||
|
||||
# Client Key (associated with generated OAuth2 Login)
|
||||
self.client_id = validate_regex(
|
||||
|
|
@ -247,8 +235,14 @@ class NotifyOffice365(NotifyBase):
|
|||
.format(recipient))
|
||||
|
||||
else:
|
||||
# If our target email list is empty we want to add ourselves to it
|
||||
self.targets.append((False, self.email))
|
||||
result = is_email(self.source)
|
||||
if not result:
|
||||
self.logger.warning('No Target Office 365 Email Detected')
|
||||
|
||||
else:
|
||||
# If our target email list is empty we want to add ourselves to
|
||||
# it
|
||||
self.targets.append((False, self.source))
|
||||
|
||||
# Validate recipients (cc:) and drop bad ones:
|
||||
for recipient in parse_emails(cc):
|
||||
|
|
@ -288,9 +282,23 @@ class NotifyOffice365(NotifyBase):
|
|||
# Presume that our token has expired 'now'
|
||||
self.token_expiry = datetime.now()
|
||||
|
||||
# Our email source; we detect this if the source is an ObjectID
|
||||
# If it is unknown we set this to None
|
||||
# User is the email associated with the account
|
||||
self.from_email = self.store.get('from')
|
||||
result = is_email(self.source)
|
||||
if result:
|
||||
self.from_email = result['full_email']
|
||||
self.from_name = \
|
||||
result['name'] or self.store.get('name')
|
||||
|
||||
else:
|
||||
self.from_name = self.store.get('name')
|
||||
|
||||
return
|
||||
|
||||
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||
def send(self, body, title='', notify_type=NotifyType.INFO, attach=None,
|
||||
**kwargs):
|
||||
"""
|
||||
Perform Office 365 Notification
|
||||
"""
|
||||
|
|
@ -304,31 +312,143 @@ class NotifyOffice365(NotifyBase):
|
|||
'There are no Email recipients to notify')
|
||||
return False
|
||||
|
||||
if self.from_email is None:
|
||||
if not self.authenticate():
|
||||
# We could not authenticate ourselves; we're done
|
||||
return False
|
||||
|
||||
# Acquire our from_email
|
||||
url = "https://graph.microsoft.com/v1.0/users/{}".format(
|
||||
self.source)
|
||||
postokay, response = self._fetch(url=url, method='GET')
|
||||
if not postokay:
|
||||
self.logger.warning(
|
||||
'Could not acquire From email address; ensure '
|
||||
'"User.Read.All" Application scope is set!')
|
||||
|
||||
else: # Acquire our from_email (if possible)
|
||||
from_email = \
|
||||
response.get("mail") or response.get("userPrincipalName")
|
||||
result = is_email(from_email)
|
||||
if not result:
|
||||
self.logger.warning(
|
||||
'Could not get From email from the Azure endpoint.')
|
||||
|
||||
# Prevent re-occuring upstream fetches for info that isn't
|
||||
# there
|
||||
self.from_email = False
|
||||
|
||||
else:
|
||||
# Store our email for future reference
|
||||
self.from_email = result['full_email']
|
||||
self.store.set('from', result['full_email'])
|
||||
|
||||
self.from_name = response.get("displayName")
|
||||
if self.from_name:
|
||||
self.store.set('name', self.from_name)
|
||||
|
||||
# Setup our Content Type
|
||||
content_type = \
|
||||
'HTML' if self.notify_format == NotifyFormat.HTML else 'Text'
|
||||
|
||||
# Prepare our payload
|
||||
payload = {
|
||||
'Message': {
|
||||
'Subject': title,
|
||||
'Body': {
|
||||
'ContentType': content_type,
|
||||
'Content': body,
|
||||
'message': {
|
||||
'subject': title,
|
||||
'body': {
|
||||
'contentType': content_type,
|
||||
'content': body,
|
||||
},
|
||||
},
|
||||
'SaveToSentItems': 'false'
|
||||
# Below takes a string (not bool) of either 'true' or 'false'
|
||||
'saveToSentItems': 'true'
|
||||
}
|
||||
|
||||
if self.from_email:
|
||||
# Apply from email if it is known
|
||||
payload.update({
|
||||
'message': {
|
||||
'from': {
|
||||
"emailAddress": {
|
||||
"address": self.from_email,
|
||||
"name": self.from_name or self.app_id,
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
# Create a copy of the email list
|
||||
emails = list(self.targets)
|
||||
|
||||
# Define our URL to post to
|
||||
url = '{graph_url}/v1.0/users/{email}/sendmail'.format(
|
||||
email=self.email,
|
||||
url = '{graph_url}/v1.0/users/{userid}/sendMail'.format(
|
||||
graph_url=self.graph_url,
|
||||
userid=self.source,
|
||||
)
|
||||
|
||||
# Prepare our Draft URL
|
||||
draft_url = \
|
||||
'{graph_url}/v1.0/users/{userid}/messages' \
|
||||
.format(
|
||||
graph_url=self.graph_url,
|
||||
userid=self.source,
|
||||
)
|
||||
|
||||
small_attachments = []
|
||||
large_attachments = []
|
||||
|
||||
# draft emails
|
||||
drafts = []
|
||||
|
||||
if attach and self.attachment_support:
|
||||
for no, attachment in enumerate(attach, start=1):
|
||||
# Perform some simple error checking
|
||||
if not attachment:
|
||||
# We could not access the attachment
|
||||
self.logger.error(
|
||||
'Could not access Office 365 attachment {}.'.format(
|
||||
attachment.url(privacy=True)))
|
||||
return False
|
||||
|
||||
if len(attachment) > self.outlook_attachment_inline_max:
|
||||
# Messages larger then xMB need to be uploaded after; a
|
||||
# draft email must be prepared; below is our session
|
||||
large_attachments.append({
|
||||
'obj': attachment,
|
||||
'name': attachment.name
|
||||
if attachment.name else f'file{no:03}.dat',
|
||||
})
|
||||
continue
|
||||
|
||||
try:
|
||||
# Prepare our Attachment in Base64
|
||||
small_attachments.append({
|
||||
"@odata.type": "#microsoft.graph.fileAttachment",
|
||||
# Name of the attachment (as it should appear in email)
|
||||
"name": attachment.name
|
||||
if attachment.name else f'file{no:03}.dat',
|
||||
# MIME type of the attachment
|
||||
"contentType": "attachment.mimetype",
|
||||
# Base64 Content
|
||||
"contentBytes": attachment.base64(),
|
||||
|
||||
})
|
||||
|
||||
except exception.AppriseException:
|
||||
# We could not access the attachment
|
||||
self.logger.error(
|
||||
'Could not access Office 365 attachment {}.'.format(
|
||||
attachment.url(privacy=True)))
|
||||
return False
|
||||
|
||||
self.logger.debug(
|
||||
'Appending Office 365 attachment {}'.format(
|
||||
attachment.url(privacy=True)))
|
||||
|
||||
if small_attachments:
|
||||
# Store Attachments
|
||||
payload['message']['attachments'] = small_attachments
|
||||
|
||||
while len(emails):
|
||||
# authenticate ourselves if we aren't already; but this function
|
||||
# also tracks if our token we have is still valid and will
|
||||
|
|
@ -347,63 +467,197 @@ class NotifyOffice365(NotifyBase):
|
|||
bcc = (self.bcc - set([to_addr]))
|
||||
|
||||
# Prepare our email
|
||||
payload['Message']['ToRecipients'] = [{
|
||||
'EmailAddress': {
|
||||
'Address': to_addr
|
||||
payload['message']['toRecipients'] = [{
|
||||
'emailAddress': {
|
||||
'address': to_addr
|
||||
}
|
||||
}]
|
||||
if to_name:
|
||||
# Apply our To Name
|
||||
payload['Message']['ToRecipients'][0]['EmailAddress']['Name'] \
|
||||
payload['message']['toRecipients'][0]['emailAddress']['name'] \
|
||||
= to_name
|
||||
|
||||
self.logger.debug('Email To: {}'.format(to_addr))
|
||||
self.logger.debug('{}Email To: {}'.format(
|
||||
'Draft' if large_attachments else '', to_addr))
|
||||
|
||||
if cc:
|
||||
# Prepare our CC list
|
||||
payload['Message']['CcRecipients'] = []
|
||||
payload['message']['ccRecipients'] = []
|
||||
for addr in cc:
|
||||
_payload = {'Address': addr}
|
||||
_payload = {'address': addr}
|
||||
if self.names.get(addr):
|
||||
_payload['Name'] = self.names[addr]
|
||||
_payload['name'] = self.names[addr]
|
||||
|
||||
# Store our address in our payload
|
||||
payload['Message']['CcRecipients']\
|
||||
.append({'EmailAddress': _payload})
|
||||
payload['message']['ccRecipients']\
|
||||
.append({'emailAddress': _payload})
|
||||
|
||||
self.logger.debug('Email Cc: {}'.format(', '.join(
|
||||
['{}{}'.format(
|
||||
'' if self.names.get(e)
|
||||
else '{}: '.format(self.names[e]), e) for e in cc])))
|
||||
self.logger.debug('{}Email Cc: {}'.format(
|
||||
'Draft' if large_attachments else '', ', '.join(
|
||||
['{}{}'.format(
|
||||
'' if self.names.get(e)
|
||||
else '{}: '.format(
|
||||
self.names[e]), e) for e in cc])))
|
||||
|
||||
if bcc:
|
||||
# Prepare our CC list
|
||||
payload['Message']['BccRecipients'] = []
|
||||
payload['message']['bccRecipients'] = []
|
||||
for addr in bcc:
|
||||
_payload = {'Address': addr}
|
||||
_payload = {'address': addr}
|
||||
if self.names.get(addr):
|
||||
_payload['Name'] = self.names[addr]
|
||||
_payload['name'] = self.names[addr]
|
||||
|
||||
# Store our address in our payload
|
||||
payload['Message']['BccRecipients']\
|
||||
.append({'EmailAddress': _payload})
|
||||
payload['message']['bccRecipients']\
|
||||
.append({'emailAddress': _payload})
|
||||
|
||||
self.logger.debug('Email Bcc: {}'.format(', '.join(
|
||||
['{}{}'.format(
|
||||
'' if self.names.get(e)
|
||||
else '{}: '.format(self.names[e]), e) for e in bcc])))
|
||||
self.logger.debug('{}Email Bcc: {}'.format(
|
||||
'Draft' if large_attachments else '', ', '.join(
|
||||
['{}{}'.format(
|
||||
'' if self.names.get(e)
|
||||
else '{}: '.format(
|
||||
self.names[e]), e) for e in bcc])))
|
||||
|
||||
# Perform upstream fetch
|
||||
# Perform upstream post
|
||||
postokay, response = self._fetch(
|
||||
url=url, payload=dumps(payload),
|
||||
content_type='application/json')
|
||||
url=url if not large_attachments
|
||||
else draft_url, payload=payload)
|
||||
|
||||
# Test if we were okay
|
||||
if not postokay:
|
||||
has_error = True
|
||||
|
||||
elif large_attachments:
|
||||
# We have large attachments now to upload and associate with
|
||||
# our message. We need to prepare a draft message; acquire
|
||||
# the message-id associated with it and then attach the file
|
||||
# via this means.
|
||||
|
||||
# Acquire our Draft ID to work with
|
||||
message_id = response.get("id")
|
||||
if not message_id:
|
||||
self.logger.warning(
|
||||
'Email Draft ID could not be retrieved')
|
||||
has_error = True
|
||||
continue
|
||||
|
||||
self.logger.debug('Email Draft ID: {}'.format(message_id))
|
||||
# In future, the below could probably be called via async
|
||||
has_attach_error = False
|
||||
for attachment in large_attachments:
|
||||
if not self.upload_attachment(
|
||||
attachment['obj'], message_id, attachment['name']):
|
||||
self.logger.warning(
|
||||
'Could not prepare attachment session for %s',
|
||||
attachment['name'])
|
||||
|
||||
has_error = True
|
||||
has_attach_error = True
|
||||
# Take early exit
|
||||
break
|
||||
|
||||
if has_attach_error:
|
||||
continue
|
||||
|
||||
# Send off our draft
|
||||
attach_url = \
|
||||
"https://graph.microsoft.com/v1.0/users/" \
|
||||
"{}/messages/{}/send"
|
||||
|
||||
attach_url = attach_url.format(
|
||||
self.source,
|
||||
message_id,
|
||||
)
|
||||
|
||||
# Trigger our send
|
||||
postokay, response = self._fetch(url=url)
|
||||
if not postokay:
|
||||
self.logger.warning(
|
||||
'Could not send drafted email id: {} ', message_id)
|
||||
has_error = True
|
||||
continue
|
||||
|
||||
# Memory management
|
||||
del small_attachments
|
||||
del large_attachments
|
||||
del drafts
|
||||
|
||||
return not has_error
|
||||
|
||||
def upload_attachment(self, attachment, message_id, name=None):
|
||||
"""
|
||||
Uploads an attachment to a session
|
||||
"""
|
||||
|
||||
# Perform some simple error checking
|
||||
if not attachment:
|
||||
# We could not access the attachment
|
||||
self.logger.error(
|
||||
'Could not access Office 365 attachment {}.'.format(
|
||||
attachment.url(privacy=True)))
|
||||
return False
|
||||
|
||||
# Our Session URL
|
||||
url = \
|
||||
'{graph_url}/v1.0/users/{userid}/message/{message_id}' \
|
||||
.format(
|
||||
graph_url=self.graph_url,
|
||||
userid=self.source,
|
||||
message_id=message_id,
|
||||
) + '/attachments/createUploadSession'
|
||||
|
||||
file_size = len(attachment)
|
||||
|
||||
payload = {
|
||||
"AttachmentItem": {
|
||||
"attachmentType": "file",
|
||||
"name": name if name else (
|
||||
attachment.name
|
||||
if attachment.name else '{}.dat'.format(str(uuid4()))),
|
||||
# MIME type of the attachment
|
||||
"contentType": attachment.mimetype,
|
||||
"size": file_size,
|
||||
}
|
||||
}
|
||||
|
||||
if not self.authenticate():
|
||||
# We could not authenticate ourselves; we're done
|
||||
return False
|
||||
|
||||
# Get our Upload URL
|
||||
postokay, response = self._fetch(url, payload)
|
||||
if not postokay:
|
||||
return False
|
||||
|
||||
upload_url = response.get('uploadUrl')
|
||||
if not upload_url:
|
||||
return False
|
||||
|
||||
start_byte = 0
|
||||
postokay = False
|
||||
response = None
|
||||
|
||||
for chunk in attachment.chunk():
|
||||
end_byte = start_byte + len(chunk) - 1
|
||||
|
||||
# Define headers for this chunk
|
||||
headers = {
|
||||
'User-Agent': self.app_id,
|
||||
'Content-Length': str(len(chunk)),
|
||||
'Content-Range':
|
||||
f'bytes {start_byte}-{end_byte}/{file_size}'
|
||||
}
|
||||
|
||||
# Upload the chunk
|
||||
postokay, response = self._fetch(
|
||||
upload_url, chunk, headers=headers, content_type=None,
|
||||
method='PUT')
|
||||
if not postokay:
|
||||
return False
|
||||
|
||||
# Return our Upload URL
|
||||
return postokay
|
||||
|
||||
def authenticate(self):
|
||||
"""
|
||||
Logs into and acquires us an authentication token to work with
|
||||
|
|
@ -420,12 +674,12 @@ class NotifyOffice365(NotifyBase):
|
|||
|
||||
# Prepare our payload
|
||||
payload = {
|
||||
'grant_type': 'client_credentials',
|
||||
'client_id': self.client_id,
|
||||
'client_secret': self.secret,
|
||||
'scope': '{graph_url}/{scope}'.format(
|
||||
graph_url=self.graph_url,
|
||||
scope=self.scope),
|
||||
'grant_type': 'client_credentials',
|
||||
}
|
||||
|
||||
# Prepare our URL
|
||||
|
|
@ -453,7 +707,9 @@ class NotifyOffice365(NotifyBase):
|
|||
# "correlation_id": "fb3d2015-bc17-4bb9-bb85-30c5cf1aaaa7"
|
||||
# }
|
||||
|
||||
postokay, response = self._fetch(url=url, payload=payload)
|
||||
postokay, response = self._fetch(
|
||||
url=url, payload=payload,
|
||||
content_type='application/x-www-form-urlencoded')
|
||||
if not postokay:
|
||||
return False
|
||||
|
||||
|
|
@ -480,18 +736,19 @@ class NotifyOffice365(NotifyBase):
|
|||
# We're authenticated
|
||||
return True if self.token else False
|
||||
|
||||
def _fetch(self, url, payload,
|
||||
content_type='application/x-www-form-urlencoded'):
|
||||
def _fetch(self, url, payload=None, headers=None,
|
||||
content_type='application/json', method='POST'):
|
||||
"""
|
||||
Wrapper to request object
|
||||
|
||||
"""
|
||||
|
||||
# Prepare our headers:
|
||||
headers = {
|
||||
'User-Agent': self.app_id,
|
||||
'Content-Type': content_type,
|
||||
}
|
||||
if not headers:
|
||||
headers = {
|
||||
'User-Agent': self.app_id,
|
||||
'Content-Type': content_type,
|
||||
}
|
||||
|
||||
if self.token:
|
||||
# Are we authenticated?
|
||||
|
|
@ -501,36 +758,84 @@ class NotifyOffice365(NotifyBase):
|
|||
content = {}
|
||||
|
||||
# Some Debug Logging
|
||||
self.logger.debug('Office 365 POST URL: {} (cert_verify={})'.format(
|
||||
url, self.verify_certificate))
|
||||
self.logger.debug('Office 365 %s URL: {} (cert_verify={})'.format(
|
||||
url, self.verify_certificate), method)
|
||||
self.logger.debug('Office 365 Payload: {}' .format(payload))
|
||||
|
||||
# Always call throttle before any remote server i/o is made
|
||||
self.throttle()
|
||||
|
||||
# fetch function
|
||||
req = requests.post if method == 'POST' else (
|
||||
requests.put if method == 'PUT' else requests.get)
|
||||
|
||||
try:
|
||||
r = requests.post(
|
||||
r = req(
|
||||
url,
|
||||
data=payload,
|
||||
data=json.dumps(payload)
|
||||
if content_type and content_type.endswith('/json')
|
||||
else payload,
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
|
||||
if r.status_code not in (
|
||||
requests.codes.ok, requests.codes.accepted):
|
||||
requests.codes.ok, requests.codes.created,
|
||||
requests.codes.accepted):
|
||||
|
||||
# We had a problem
|
||||
status_str = \
|
||||
NotifyOffice365.http_response_code_lookup(r.status_code)
|
||||
|
||||
self.logger.warning(
|
||||
'Failed to send Office 365 POST to {}: '
|
||||
'Failed to send Office 365 %s to {}: '
|
||||
'{}error={}.'.format(
|
||||
url,
|
||||
', ' if status_str else '',
|
||||
r.status_code))
|
||||
r.status_code), method)
|
||||
|
||||
# A Response could look like this if a Scope element was not
|
||||
# found:
|
||||
# {
|
||||
# "error": {
|
||||
# "code": "MissingClaimType",
|
||||
# "message":"The token is missing the claim type \'oid\'.",
|
||||
# "innerError": {
|
||||
# "oAuthEventOperationId":" 7abe20-339f-4659-9381-38f52",
|
||||
# "oAuthEventcV": "xsOSpAHSHVm3Tp4SNH5oIA.1.1",
|
||||
# "errorUrl": "https://url",
|
||||
# "requestId": "2328ea-ec9e-43a8-80f4-164c",
|
||||
# "date":"2024-12-01T02:03:13"
|
||||
# }}
|
||||
# }
|
||||
|
||||
# Error 403; the below is returned if he User.Read.All
|
||||
# Application scope is not set and a lookup is
|
||||
# attempted.
|
||||
# {
|
||||
# "error": {
|
||||
# "code": "Authorization_RequestDenied",
|
||||
# "message":
|
||||
# "Insufficient privileges to complete the operation.",
|
||||
# "innerError": {
|
||||
# "date": "2024-12-06T00:15:57",
|
||||
# "request-id":
|
||||
# "48fdb3e7-2f1a-4f45-a5a0-99b8b851278b",
|
||||
# "client-request-id": "48f-2f1a-4f45-a5a0-99b8"
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
|
||||
# Another response type (error 415):
|
||||
# {
|
||||
# "error": {
|
||||
# "code": "RequestBodyRead",
|
||||
# "message": "A missing or empty content type header was \
|
||||
# found when trying to read a message. The content \
|
||||
# type header is required.",
|
||||
# }
|
||||
# }
|
||||
|
||||
self.logger.debug(
|
||||
'Response Details:\r\n{}'.format(r.content))
|
||||
|
|
@ -539,7 +844,7 @@ class NotifyOffice365(NotifyBase):
|
|||
return (False, content)
|
||||
|
||||
try:
|
||||
content = loads(r.content)
|
||||
content = json.loads(r.content)
|
||||
|
||||
except (AttributeError, TypeError, ValueError):
|
||||
# ValueError = r.content is Unparsable
|
||||
|
|
@ -549,8 +854,8 @@ class NotifyOffice365(NotifyBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'Exception received when sending Office 365 POST to {}: '.
|
||||
format(url))
|
||||
'Exception received when sending Office 365 %s to {}: '.
|
||||
format(url), method)
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
||||
# Mark our failure
|
||||
|
|
@ -558,12 +863,24 @@ class NotifyOffice365(NotifyBase):
|
|||
|
||||
return (True, content)
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
return (
|
||||
self.secure_protocol[0], self.source, self.tenant, self.client_id,
|
||||
self.secret,
|
||||
)
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Our URL parameters
|
||||
# Extend our parameters
|
||||
params = self.url_parameters(privacy=privacy, *args, **kwargs)
|
||||
|
||||
if self.cc:
|
||||
|
|
@ -580,13 +897,13 @@ class NotifyOffice365(NotifyBase):
|
|||
'' if not self.names.get(e)
|
||||
else '{}:'.format(self.names[e]), e) for e in self.bcc])
|
||||
|
||||
return '{schema}://{tenant}:{email}/{client_id}/{secret}' \
|
||||
return '{schema}://{source}/{tenant}/{client_id}/{secret}' \
|
||||
'/{targets}/?{params}'.format(
|
||||
schema=self.secure_protocol,
|
||||
schema=self.secure_protocol[0],
|
||||
tenant=self.pprint(self.tenant, privacy, safe=''),
|
||||
# email does not need to be escaped because it should
|
||||
# already be a valid host and username at this point
|
||||
email=self.email,
|
||||
source=self.source,
|
||||
client_id=self.pprint(self.client_id, privacy, safe=''),
|
||||
secret=self.pprint(
|
||||
self.secret, privacy, mode=PrivacyMode.Secret,
|
||||
|
|
@ -594,7 +911,7 @@ class NotifyOffice365(NotifyBase):
|
|||
targets='/'.join(
|
||||
[NotifyOffice365.quote('{}{}'.format(
|
||||
'' if not e[0] else '{}:'.format(e[0]), e[1]),
|
||||
safe='') for e in self.targets]),
|
||||
safe='@') for e in self.targets]),
|
||||
params=NotifyOffice365.urlencode(params))
|
||||
|
||||
def __len__(self):
|
||||
|
|
@ -623,16 +940,52 @@ class NotifyOffice365(NotifyBase):
|
|||
# of the secret key (since it can contain slashes in it)
|
||||
entries = NotifyOffice365.split_path(results['fullpath'])
|
||||
|
||||
try:
|
||||
# Initialize our tenant
|
||||
results['tenant'] = None
|
||||
|
||||
# Initialize our email
|
||||
results['email'] = None
|
||||
|
||||
# From Email
|
||||
if 'from' in results['qsd'] and \
|
||||
len(results['qsd']['from']):
|
||||
# Extract the sending account's information
|
||||
results['source'] = \
|
||||
NotifyOffice365.unquote(results['qsd']['from'])
|
||||
|
||||
# If tenant is occupied, then the user defined makes up our source
|
||||
elif results['user']:
|
||||
results['source'] = '{}@{}'.format(
|
||||
NotifyOffice365.unquote(results['user']),
|
||||
NotifyOffice365.unquote(results['host']),
|
||||
)
|
||||
|
||||
else:
|
||||
# Object ID instead of email
|
||||
results['source'] = NotifyOffice365.unquote(results['host'])
|
||||
|
||||
# Tenant
|
||||
if 'tenant' in results['qsd'] and len(results['qsd']['tenant']):
|
||||
# Extract the Tenant from the argument
|
||||
results['tenant'] = \
|
||||
NotifyOffice365.unquote(results['qsd']['tenant'])
|
||||
|
||||
elif entries:
|
||||
results['tenant'] = NotifyOffice365.unquote(entries.pop(0))
|
||||
|
||||
# OAuth2 ID
|
||||
if 'oauth_id' in results['qsd'] and len(results['qsd']['oauth_id']):
|
||||
# Extract the API Key from an argument
|
||||
results['client_id'] = \
|
||||
NotifyOffice365.unquote(results['qsd']['oauth_id'])
|
||||
|
||||
elif entries:
|
||||
# Get our client_id is the first entry on the path
|
||||
results['client_id'] = NotifyOffice365.unquote(entries.pop(0))
|
||||
|
||||
except IndexError:
|
||||
# no problem, we may get the client_id another way through
|
||||
# arguments...
|
||||
pass
|
||||
|
||||
#
|
||||
# Prepare our target listing
|
||||
#
|
||||
results['targets'] = list()
|
||||
while entries:
|
||||
# Pop the last entry
|
||||
|
|
@ -650,36 +1003,6 @@ class NotifyOffice365(NotifyBase):
|
|||
# We're done
|
||||
break
|
||||
|
||||
# Initialize our tenant
|
||||
results['tenant'] = None
|
||||
|
||||
# Assemble our secret key which is a combination of the host followed
|
||||
# by all entries in the full path that follow up until the first email
|
||||
results['secret'] = '/'.join(
|
||||
[NotifyOffice365.unquote(x) for x in entries])
|
||||
|
||||
# Assemble our client id from the user@hostname
|
||||
if results['password']:
|
||||
results['email'] = '{}@{}'.format(
|
||||
NotifyOffice365.unquote(results['password']),
|
||||
NotifyOffice365.unquote(results['host']),
|
||||
)
|
||||
# Update our tenant
|
||||
results['tenant'] = NotifyOffice365.unquote(results['user'])
|
||||
|
||||
else:
|
||||
# No tenant specified..
|
||||
results['email'] = '{}@{}'.format(
|
||||
NotifyOffice365.unquote(results['user']),
|
||||
NotifyOffice365.unquote(results['host']),
|
||||
)
|
||||
|
||||
# OAuth2 ID
|
||||
if 'oauth_id' in results['qsd'] and len(results['qsd']['oauth_id']):
|
||||
# Extract the API Key from an argument
|
||||
results['client_id'] = \
|
||||
NotifyOffice365.unquote(results['qsd']['oauth_id'])
|
||||
|
||||
# OAuth2 Secret
|
||||
if 'oauth_secret' in results['qsd'] and \
|
||||
len(results['qsd']['oauth_secret']):
|
||||
|
|
@ -687,19 +1010,12 @@ class NotifyOffice365(NotifyBase):
|
|||
results['secret'] = \
|
||||
NotifyOffice365.unquote(results['qsd']['oauth_secret'])
|
||||
|
||||
# Tenant
|
||||
if 'from' in results['qsd'] and \
|
||||
len(results['qsd']['from']):
|
||||
# Extract the sending account's information
|
||||
results['email'] = \
|
||||
NotifyOffice365.unquote(results['qsd']['from'])
|
||||
|
||||
# Tenant
|
||||
if 'tenant' in results['qsd'] and \
|
||||
len(results['qsd']['tenant']):
|
||||
# Extract the Tenant from the argument
|
||||
results['tenant'] = \
|
||||
NotifyOffice365.unquote(results['qsd']['tenant'])
|
||||
else:
|
||||
# Assemble our secret key which is a combination of the host
|
||||
# followed by all entries in the full path that follow up until
|
||||
# the first email
|
||||
results['secret'] = '/'.join(
|
||||
[NotifyOffice365.unquote(x) for x in entries])
|
||||
|
||||
# Support the 'to' variable so that we can support targets this way too
|
||||
# The 'to' makes it easier to use yaml configuration
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -40,10 +40,8 @@ from itertools import chain
|
|||
from .base import NotifyBase
|
||||
from ..common import NotifyType
|
||||
from ..common import NotifyImageSize
|
||||
from ..utils import validate_regex
|
||||
from ..utils import parse_list
|
||||
from ..utils import parse_bool
|
||||
from ..utils import is_email
|
||||
from ..utils.base64 import decode_b64_dict, encode_b64_dict
|
||||
from ..utils.parse import validate_regex, parse_list, parse_bool, is_email
|
||||
from ..locale import gettext_lazy as _
|
||||
|
||||
|
||||
|
|
@ -82,7 +80,7 @@ class NotifyOneSignal(NotifyBase):
|
|||
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_onesignal'
|
||||
|
||||
# Notification
|
||||
notify_url = "https://onesignal.com/api/v1/notifications"
|
||||
notify_url = "https://api.onesignal.com/notifications"
|
||||
|
||||
# Allows the user to specify the NotifyImageSize object
|
||||
image_size = NotifyImageSize.XY_72
|
||||
|
|
@ -161,6 +159,18 @@ class NotifyOneSignal(NotifyBase):
|
|||
'type': 'bool',
|
||||
'default': False,
|
||||
},
|
||||
'contents': {
|
||||
'name': _('Enable Contents'),
|
||||
'type': 'bool',
|
||||
'default': True,
|
||||
'map_to': 'use_contents',
|
||||
},
|
||||
'decode': {
|
||||
'name': _('Decode Template Args'),
|
||||
'type': 'bool',
|
||||
'default': False,
|
||||
'map_to': 'decode_tpl_args',
|
||||
},
|
||||
'template': {
|
||||
'alias_of': 'template',
|
||||
},
|
||||
|
|
@ -175,9 +185,22 @@ class NotifyOneSignal(NotifyBase):
|
|||
},
|
||||
})
|
||||
|
||||
# Define our token control
|
||||
template_kwargs = {
|
||||
'custom': {
|
||||
'name': _('Custom Data'),
|
||||
'prefix': ':',
|
||||
},
|
||||
'postback': {
|
||||
'name': _('Postback Data'),
|
||||
'prefix': '+',
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, app, apikey, targets=None, include_image=True,
|
||||
template=None, subtitle=None, language=None, batch=False,
|
||||
**kwargs):
|
||||
template=None, subtitle=None, language=None, batch=None,
|
||||
use_contents=None, decode_tpl_args=None,
|
||||
custom=None, postback=None, **kwargs):
|
||||
"""
|
||||
Initialize OneSignal
|
||||
|
||||
|
|
@ -201,7 +224,19 @@ class NotifyOneSignal(NotifyBase):
|
|||
raise TypeError(msg)
|
||||
|
||||
# Prepare Batch Mode Flag
|
||||
self.batch_size = self.default_batch_size if batch else 1
|
||||
self.batch_size = self.default_batch_size if (
|
||||
batch if batch is not None else
|
||||
self.template_args['batch']['default']) else 1
|
||||
|
||||
# Prepare Use Contents Flag
|
||||
self.use_contents = True if (
|
||||
use_contents if use_contents is not None else
|
||||
self.template_args['contents']['default']) else False
|
||||
|
||||
# Prepare Decode Template Arguments Flag
|
||||
self.decode_tpl_args = True if (
|
||||
decode_tpl_args if decode_tpl_args is not None else
|
||||
self.template_args['decode']['default']) else False
|
||||
|
||||
# Place a thumbnail image inline with the message body
|
||||
self.include_image = include_image
|
||||
|
|
@ -273,6 +308,30 @@ class NotifyOneSignal(NotifyBase):
|
|||
'Detected OneSignal Player ID: %s' %
|
||||
self.targets[OneSignalCategory.PLAYER][-1])
|
||||
|
||||
# Custom Data
|
||||
self.custom_data = {}
|
||||
if custom and isinstance(custom, dict):
|
||||
if self.decode_tpl_args:
|
||||
custom = decode_b64_dict(custom)
|
||||
|
||||
self.custom_data.update(custom)
|
||||
|
||||
elif custom:
|
||||
msg = 'The specified OneSignal Custom Data ' \
|
||||
'({}) are not identified as a dictionary.'.format(custom)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
# Postback Data
|
||||
self.postback_data = {}
|
||||
if postback and isinstance(postback, dict):
|
||||
self.postback_data.update(postback)
|
||||
|
||||
elif postback:
|
||||
msg = 'The specified OneSignal Postback Data ' \
|
||||
'({}) are not identified as a dictionary.'.format(postback)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
return
|
||||
|
||||
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||
|
|
@ -291,14 +350,9 @@ class NotifyOneSignal(NotifyBase):
|
|||
|
||||
payload = {
|
||||
'app_id': self.app,
|
||||
|
||||
'headings': {
|
||||
self.language: title if title else self.app_desc,
|
||||
},
|
||||
'contents': {
|
||||
self.language: body,
|
||||
},
|
||||
|
||||
# Sending true wakes your app from background to run custom native
|
||||
# code (Apple interprets this as content-available=1).
|
||||
# Note: Not applicable if the app is in the "force-quit" state
|
||||
|
|
@ -307,6 +361,33 @@ class NotifyOneSignal(NotifyBase):
|
|||
'content_available': True,
|
||||
}
|
||||
|
||||
if self.template_id:
|
||||
# Store template information
|
||||
payload['template_id'] = self.template_id
|
||||
|
||||
if not self.use_contents:
|
||||
# Only if a template is defined can contents be removed
|
||||
del payload['contents']
|
||||
|
||||
# Set our data if defined
|
||||
if self.custom_data:
|
||||
payload.update({
|
||||
'custom_data': self.custom_data,
|
||||
})
|
||||
|
||||
# Set our postback data if defined
|
||||
if self.postback_data:
|
||||
payload.update({
|
||||
'data': self.postback_data,
|
||||
})
|
||||
|
||||
if title:
|
||||
# Display our title if defined
|
||||
payload.update({
|
||||
'headings': {
|
||||
self.language: title,
|
||||
}})
|
||||
|
||||
if self.subtitle:
|
||||
payload.update({
|
||||
'subtitle': {
|
||||
|
|
@ -314,9 +395,6 @@ class NotifyOneSignal(NotifyBase):
|
|||
},
|
||||
})
|
||||
|
||||
if self.template_id:
|
||||
payload['template_id'] = self.template_id
|
||||
|
||||
# Acquire our large_icon image URL (if set)
|
||||
image_url = None if not self.include_image \
|
||||
else self.image_url(notify_type)
|
||||
|
|
@ -392,6 +470,17 @@ class NotifyOneSignal(NotifyBase):
|
|||
|
||||
return not has_error
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
return (
|
||||
self.secure_protocol, self.template_id, self.app, self.apikey,
|
||||
)
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
|
|
@ -406,6 +495,25 @@ class NotifyOneSignal(NotifyBase):
|
|||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
custom_data, needs_decoding = encode_b64_dict(self.custom_data)
|
||||
# custom_data, needs_decoding = self.custom_data, False
|
||||
# Save our template data
|
||||
params.update(
|
||||
{':{}'.format(k): v for k, v in custom_data.items()}
|
||||
)
|
||||
|
||||
# Save our postback data
|
||||
params.update(
|
||||
{'+{}'.format(k): v for k, v in self.postback_data.items()})
|
||||
|
||||
if self.use_contents != self.template_args['contents']['default']:
|
||||
params['contents'] = 'yes' if self.use_contents else 'no'
|
||||
|
||||
if (self.decode_tpl_args != self.template_args['decode']['default']
|
||||
or needs_decoding):
|
||||
params['decode'] = 'yes' if (self.decode_tpl_args or
|
||||
needs_decoding) else 'no'
|
||||
|
||||
return '{schema}://{tp_id}{app}@{apikey}/{targets}?{params}'.format(
|
||||
schema=self.secure_protocol,
|
||||
tp_id='{}:'.format(
|
||||
|
|
@ -485,6 +593,20 @@ class NotifyOneSignal(NotifyBase):
|
|||
'batch',
|
||||
NotifyOneSignal.template_args['batch']['default']))
|
||||
|
||||
# Get Use Contents Boolean (if set)
|
||||
results['use_contents'] = \
|
||||
parse_bool(
|
||||
results['qsd'].get(
|
||||
'contents',
|
||||
NotifyOneSignal.template_args['contents']['default']))
|
||||
|
||||
# Get Use Contents Boolean (if set)
|
||||
results['decode_tpl_args'] = \
|
||||
parse_bool(
|
||||
results['qsd'].get(
|
||||
'decode',
|
||||
NotifyOneSignal.template_args['decode']['default']))
|
||||
|
||||
# The API Key is stored in the hostname
|
||||
results['apikey'] = NotifyOneSignal.unquote(results['host'])
|
||||
|
||||
|
|
@ -516,4 +638,10 @@ class NotifyOneSignal(NotifyBase):
|
|||
results['language'] = \
|
||||
NotifyOneSignal.unquote(results['qsd']['lang'])
|
||||
|
||||
# Store our custom data
|
||||
results['custom'] = results['qsd:']
|
||||
|
||||
# Store our postback data
|
||||
results['postback'] = results['qsd+']
|
||||
|
||||
return results
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -47,14 +47,13 @@
|
|||
# API Integration Docs: https://docs.opsgenie.com/docs/api-integration
|
||||
|
||||
import requests
|
||||
from json import dumps
|
||||
from json import dumps, loads
|
||||
import hashlib
|
||||
|
||||
from .base import NotifyBase
|
||||
from ..common import NotifyType
|
||||
from ..utils import validate_regex
|
||||
from ..utils import is_uuid
|
||||
from ..utils import parse_list
|
||||
from ..utils import parse_bool
|
||||
from ..common import NotifyType, NOTIFY_TYPES
|
||||
from ..common import PersistentStoreMode
|
||||
from ..utils.parse import validate_regex, is_uuid, parse_list, parse_bool
|
||||
from ..locale import gettext_lazy as _
|
||||
|
||||
|
||||
|
|
@ -76,6 +75,47 @@ OPSGENIE_CATEGORIES = (
|
|||
)
|
||||
|
||||
|
||||
class OpsgenieAlertAction:
|
||||
"""
|
||||
Defines the supported actions
|
||||
"""
|
||||
# Use mapping (specify :key=arg to over-ride)
|
||||
MAP = 'map'
|
||||
|
||||
# Create new alert (default)
|
||||
NEW = 'new'
|
||||
|
||||
# Close Alert
|
||||
CLOSE = 'close'
|
||||
|
||||
# Delete Alert
|
||||
DELETE = 'delete'
|
||||
|
||||
# Acknowledge Alert
|
||||
ACKNOWLEDGE = 'acknowledge'
|
||||
|
||||
# Add note to alert
|
||||
NOTE = 'note'
|
||||
|
||||
|
||||
OPSGENIE_ACTIONS = (
|
||||
OpsgenieAlertAction.MAP,
|
||||
OpsgenieAlertAction.NEW,
|
||||
OpsgenieAlertAction.CLOSE,
|
||||
OpsgenieAlertAction.DELETE,
|
||||
OpsgenieAlertAction.ACKNOWLEDGE,
|
||||
OpsgenieAlertAction.NOTE,
|
||||
)
|
||||
|
||||
# Map all support Apprise Categories to Opsgenie Categories
|
||||
OPSGENIE_ALERT_MAP = {
|
||||
NotifyType.INFO: OpsgenieAlertAction.CLOSE,
|
||||
NotifyType.SUCCESS: OpsgenieAlertAction.CLOSE,
|
||||
NotifyType.WARNING: OpsgenieAlertAction.NEW,
|
||||
NotifyType.FAILURE: OpsgenieAlertAction.NEW,
|
||||
}
|
||||
|
||||
|
||||
# Regions
|
||||
class OpsgenieRegion:
|
||||
US = 'us'
|
||||
|
|
@ -160,6 +200,10 @@ class NotifyOpsgenie(NotifyBase):
|
|||
# The maximum length of the body
|
||||
body_maxlen = 15000
|
||||
|
||||
# Our default is to no not use persistent storage beyond in-memory
|
||||
# reference
|
||||
storage_mode = PersistentStoreMode.AUTO
|
||||
|
||||
# If we don't have the specified min length, then we don't bother using
|
||||
# the body directive
|
||||
opsgenie_body_minlen = 130
|
||||
|
|
@ -170,10 +214,24 @@ class NotifyOpsgenie(NotifyBase):
|
|||
# The maximum allowable targets within a notification
|
||||
default_batch_size = 50
|
||||
|
||||
# Defines our default message mapping
|
||||
opsgenie_message_map = {
|
||||
# Add a note to existing alert
|
||||
NotifyType.INFO: OpsgenieAlertAction.NOTE,
|
||||
# Close existing alert
|
||||
NotifyType.SUCCESS: OpsgenieAlertAction.CLOSE,
|
||||
# Create notice
|
||||
NotifyType.WARNING: OpsgenieAlertAction.NEW,
|
||||
# Create notice
|
||||
NotifyType.FAILURE: OpsgenieAlertAction.NEW,
|
||||
}
|
||||
|
||||
# Define object templates
|
||||
templates = (
|
||||
'{schema}://{apikey}',
|
||||
'{schema}://{user}@{apikey}',
|
||||
'{schema}://{apikey}/{targets}',
|
||||
'{schema}://{user}@{apikey}/{targets}',
|
||||
)
|
||||
|
||||
# Define our template tokens
|
||||
|
|
@ -184,6 +242,10 @@ class NotifyOpsgenie(NotifyBase):
|
|||
'private': True,
|
||||
'required': True,
|
||||
},
|
||||
'user': {
|
||||
'name': _('Username'),
|
||||
'type': 'string',
|
||||
},
|
||||
'target_escalation': {
|
||||
'name': _('Target Escalation'),
|
||||
'prefix': '^',
|
||||
|
|
@ -249,6 +311,12 @@ class NotifyOpsgenie(NotifyBase):
|
|||
'to': {
|
||||
'alias_of': 'targets',
|
||||
},
|
||||
'action': {
|
||||
'name': _('Action'),
|
||||
'type': 'choice:string',
|
||||
'values': OPSGENIE_ACTIONS,
|
||||
'default': OPSGENIE_ACTIONS[0],
|
||||
}
|
||||
})
|
||||
|
||||
# Map of key-value pairs to use as custom properties of the alert.
|
||||
|
|
@ -257,11 +325,15 @@ class NotifyOpsgenie(NotifyBase):
|
|||
'name': _('Details'),
|
||||
'prefix': '+',
|
||||
},
|
||||
'mapping': {
|
||||
'name': _('Action Mapping'),
|
||||
'prefix': ':',
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, apikey, targets, region_name=None, details=None,
|
||||
priority=None, alias=None, entity=None, batch=False,
|
||||
tags=None, **kwargs):
|
||||
tags=None, action=None, mapping=None, **kwargs):
|
||||
"""
|
||||
Initialize Opsgenie Object
|
||||
"""
|
||||
|
|
@ -298,6 +370,41 @@ class NotifyOpsgenie(NotifyBase):
|
|||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
if action and isinstance(action, str):
|
||||
self.action = next(
|
||||
(a for a in OPSGENIE_ACTIONS if a.startswith(action)), None)
|
||||
if self.action not in OPSGENIE_ACTIONS:
|
||||
msg = 'The Opsgenie action specified ({}) is invalid.'\
|
||||
.format(action)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
else:
|
||||
self.action = self.template_args['action']['default']
|
||||
|
||||
# Store our mappings
|
||||
self.mapping = self.opsgenie_message_map.copy()
|
||||
if mapping and isinstance(mapping, dict):
|
||||
for _k, _v in mapping.items():
|
||||
# Get our mapping
|
||||
k = next((t for t in NOTIFY_TYPES if t.startswith(_k)), None)
|
||||
if not k:
|
||||
msg = 'The Opsgenie mapping key specified ({}) ' \
|
||||
'is invalid.'.format(_k)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
_v_lower = _v.lower()
|
||||
v = next((v for v in OPSGENIE_ACTIONS[1:]
|
||||
if v.startswith(_v_lower)), None)
|
||||
if not v:
|
||||
msg = 'The Opsgenie mapping value (assigned to {}) ' \
|
||||
'specified ({}) is invalid.'.format(k, _v)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
# Update our mapping
|
||||
self.mapping[k] = v
|
||||
|
||||
self.details = {}
|
||||
if details:
|
||||
# Store our extra details
|
||||
|
|
@ -367,115 +474,234 @@ class NotifyOpsgenie(NotifyBase):
|
|||
if is_uuid(target) else
|
||||
{'type': OpsgenieCategory.USER, 'username': target})
|
||||
|
||||
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||
def _fetch(self, method, url, payload, params=None):
|
||||
"""
|
||||
Perform Opsgenie Notification
|
||||
Performs server retrieval/update and returns JSON Response
|
||||
"""
|
||||
|
||||
headers = {
|
||||
'User-Agent': self.app_id,
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'GenieKey {}'.format(self.apikey),
|
||||
}
|
||||
|
||||
# Some Debug Logging
|
||||
self.logger.debug(
|
||||
'Opsgenie POST URL: {} (cert_verify={})'.format(
|
||||
url, self.verify_certificate))
|
||||
self.logger.debug('Opsgenie Payload: {}' .format(payload))
|
||||
|
||||
# Initialize our response object
|
||||
content = {}
|
||||
|
||||
# Always call throttle before any remote server i/o is made
|
||||
self.throttle()
|
||||
try:
|
||||
r = method(
|
||||
url,
|
||||
data=dumps(payload),
|
||||
params=params,
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
|
||||
# A Response might look like:
|
||||
# {
|
||||
# "result": "Request will be processed",
|
||||
# "took": 0.302,
|
||||
# "requestId": "43a29c5c-3dbf-4fa4-9c26-f4f71023e120"
|
||||
# }
|
||||
|
||||
try:
|
||||
# Update our response object
|
||||
content = loads(r.content)
|
||||
|
||||
except (AttributeError, TypeError, ValueError):
|
||||
# ValueError = r.content is Unparsable
|
||||
# TypeError = r.content is None
|
||||
# AttributeError = r is None
|
||||
content = {}
|
||||
|
||||
if r.status_code not in (
|
||||
requests.codes.accepted, requests.codes.ok):
|
||||
status_str = \
|
||||
NotifyBase.http_response_code_lookup(
|
||||
r.status_code)
|
||||
|
||||
self.logger.warning(
|
||||
'Failed to send Opsgenie notification:'
|
||||
'{}{}error={}.'.format(
|
||||
status_str,
|
||||
', ' if status_str else '',
|
||||
r.status_code))
|
||||
|
||||
self.logger.debug(
|
||||
'Response Details:\r\n{}'.format(r.content))
|
||||
|
||||
return (False, content.get('requestId'))
|
||||
|
||||
# If we reach here; the message was sent
|
||||
self.logger.info('Sent Opsgenie notification')
|
||||
self.logger.debug(
|
||||
'Response Details:\r\n{}'.format(r.content))
|
||||
|
||||
return (True, content.get('requestId'))
|
||||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occurred sending Opsgenie '
|
||||
'notification.')
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
||||
return (False, content.get('requestId'))
|
||||
|
||||
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||
"""
|
||||
Perform Opsgenie Notification
|
||||
"""
|
||||
|
||||
# Get our Opsgenie Action
|
||||
action = OPSGENIE_ALERT_MAP[notify_type] \
|
||||
if self.action == OpsgenieAlertAction.MAP else self.action
|
||||
|
||||
# Prepare our URL as it's based on our hostname
|
||||
notify_url = OPSGENIE_API_LOOKUP[self.region_name]
|
||||
|
||||
# Initialize our has_error flag
|
||||
has_error = False
|
||||
|
||||
# Use body if title not set
|
||||
title_body = body if not title else title
|
||||
# Default method is to post
|
||||
method = requests.post
|
||||
|
||||
# Create a copy ouf our details object
|
||||
details = self.details.copy()
|
||||
if 'type' not in details:
|
||||
details['type'] = notify_type
|
||||
# For indexing in persistent store
|
||||
key = hashlib.sha1(
|
||||
(self.entity if self.entity else (
|
||||
self.alias if self.alias else (
|
||||
title if title else self.app_id)))
|
||||
.encode('utf-8')).hexdigest()[0:10]
|
||||
|
||||
# Prepare our payload
|
||||
payload = {
|
||||
'source': self.app_desc,
|
||||
'message': title_body,
|
||||
'description': body,
|
||||
'details': details,
|
||||
'priority': 'P{}'.format(self.priority),
|
||||
}
|
||||
# Get our Opsgenie Request IDs
|
||||
request_ids = self.store.get(key, [])
|
||||
if not isinstance(request_ids, list):
|
||||
request_ids = []
|
||||
|
||||
# Use our body directive if we exceed the minimum message
|
||||
# limitation
|
||||
if len(payload['message']) > self.opsgenie_body_minlen:
|
||||
payload['message'] = '{}...'.format(
|
||||
title_body[:self.opsgenie_body_minlen - 3])
|
||||
if action == OpsgenieAlertAction.NEW:
|
||||
# Create a copy ouf our details object
|
||||
details = self.details.copy()
|
||||
if 'type' not in details:
|
||||
details['type'] = notify_type
|
||||
|
||||
if self.__tags:
|
||||
payload['tags'] = self.__tags
|
||||
# Use body if title not set
|
||||
title_body = body if not title else title
|
||||
|
||||
if self.entity:
|
||||
payload['entity'] = self.entity
|
||||
# Prepare our payload
|
||||
payload = {
|
||||
'source': self.app_desc,
|
||||
'message': title_body,
|
||||
'description': body,
|
||||
'details': details,
|
||||
'priority': 'P{}'.format(self.priority),
|
||||
}
|
||||
|
||||
if self.alias:
|
||||
payload['alias'] = self.alias
|
||||
# Use our body directive if we exceed the minimum message
|
||||
# limitation
|
||||
if len(payload['message']) > self.opsgenie_body_minlen:
|
||||
payload['message'] = '{}...'.format(
|
||||
title_body[:self.opsgenie_body_minlen - 3])
|
||||
|
||||
length = len(self.targets) if self.targets else 1
|
||||
for index in range(0, length, self.batch_size):
|
||||
if self.targets:
|
||||
# If there were no targets identified, then we simply
|
||||
# just iterate once without the responders set
|
||||
payload['responders'] = \
|
||||
self.targets[index:index + self.batch_size]
|
||||
if self.__tags:
|
||||
payload['tags'] = self.__tags
|
||||
|
||||
# Some Debug Logging
|
||||
self.logger.debug(
|
||||
'Opsgenie POST URL: {} (cert_verify={})'.format(
|
||||
notify_url, self.verify_certificate))
|
||||
self.logger.debug('Opsgenie Payload: {}' .format(payload))
|
||||
if self.entity:
|
||||
payload['entity'] = self.entity
|
||||
|
||||
# Always call throttle before any remote server i/o is made
|
||||
self.throttle()
|
||||
try:
|
||||
r = requests.post(
|
||||
notify_url,
|
||||
data=dumps(payload),
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
if self.alias:
|
||||
payload['alias'] = self.alias
|
||||
|
||||
if r.status_code not in (
|
||||
requests.codes.accepted, requests.codes.ok):
|
||||
status_str = \
|
||||
NotifyBase.http_response_code_lookup(
|
||||
r.status_code)
|
||||
if self.user:
|
||||
payload['user'] = self.user
|
||||
|
||||
self.logger.warning(
|
||||
'Failed to send Opsgenie notification:'
|
||||
'{}{}error={}.'.format(
|
||||
status_str,
|
||||
', ' if status_str else '',
|
||||
r.status_code))
|
||||
# reset our request IDs - we will re-populate them
|
||||
request_ids = []
|
||||
|
||||
self.logger.debug(
|
||||
'Response Details:\r\n{}'.format(r.content))
|
||||
length = len(self.targets) if self.targets else 1
|
||||
for index in range(0, length, self.batch_size):
|
||||
if self.targets:
|
||||
# If there were no targets identified, then we simply
|
||||
# just iterate once without the responders set
|
||||
payload['responders'] = \
|
||||
self.targets[index:index + self.batch_size]
|
||||
|
||||
# Mark our failure
|
||||
# Perform our post
|
||||
success, request_id = self._fetch(
|
||||
method, notify_url, payload)
|
||||
|
||||
if success and request_id:
|
||||
# Save our response
|
||||
request_ids.append(request_id)
|
||||
|
||||
else:
|
||||
has_error = True
|
||||
continue
|
||||
|
||||
# If we reach here; the message was sent
|
||||
self.logger.info('Sent Opsgenie notification')
|
||||
self.logger.debug(
|
||||
'Response Details:\r\n{}'.format(r.content))
|
||||
# Store our entries for a maximum of 60 days
|
||||
self.store.set(key, request_ids, expires=60 * 60 * 24 * 60)
|
||||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occurred sending Opsgenie '
|
||||
'notification.')
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
# Mark our failure
|
||||
has_error = True
|
||||
elif request_ids:
|
||||
# Prepare our payload
|
||||
payload = {
|
||||
'source': self.app_desc,
|
||||
'note': body,
|
||||
}
|
||||
|
||||
if self.user:
|
||||
payload['user'] = self.user
|
||||
|
||||
# Prepare our Identifier type
|
||||
params = {
|
||||
'identifierType': 'id',
|
||||
}
|
||||
|
||||
for request_id in request_ids:
|
||||
if action == OpsgenieAlertAction.DELETE:
|
||||
# Update our URL
|
||||
url = f'{notify_url}/{request_id}'
|
||||
method = requests.delete
|
||||
|
||||
elif action == OpsgenieAlertAction.ACKNOWLEDGE:
|
||||
url = f'{notify_url}/{request_id}/acknowledge'
|
||||
|
||||
elif action == OpsgenieAlertAction.CLOSE:
|
||||
url = f'{notify_url}/{request_id}/close'
|
||||
|
||||
else: # action == OpsgenieAlertAction.CLOSE:
|
||||
url = f'{notify_url}/{request_id}/notes'
|
||||
|
||||
# Perform our post
|
||||
success, _ = self._fetch(method, url, payload, params)
|
||||
|
||||
if not success:
|
||||
has_error = True
|
||||
|
||||
if not has_error and action == OpsgenieAlertAction.DELETE:
|
||||
# Remove cached entry
|
||||
self.store.clear(key)
|
||||
|
||||
else:
|
||||
self.logger.info(
|
||||
'No Opsgenie notification sent due to (nothing to %s) '
|
||||
'condition', self.action)
|
||||
|
||||
return not has_error
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
return (self.secure_protocol, self.region_name, self.apikey)
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
|
|
@ -483,6 +709,7 @@ class NotifyOpsgenie(NotifyBase):
|
|||
|
||||
# Define any URL parameters
|
||||
params = {
|
||||
'action': self.action,
|
||||
'region': self.region_name,
|
||||
'priority':
|
||||
OPSGENIE_PRIORITIES[self.template_args['priority']['default']]
|
||||
|
|
@ -506,6 +733,10 @@ class NotifyOpsgenie(NotifyBase):
|
|||
# Append our details into our parameters
|
||||
params.update({'+{}'.format(k): v for k, v in self.details.items()})
|
||||
|
||||
# Append our assignment extra's into our parameters
|
||||
params.update(
|
||||
{':{}'.format(k): v for k, v in self.mapping.items()})
|
||||
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
|
|
@ -522,8 +753,9 @@ class NotifyOpsgenie(NotifyBase):
|
|||
NotifyOpsgenie.template_tokens['target_team']['prefix'],
|
||||
}
|
||||
|
||||
return '{schema}://{apikey}/{targets}/?{params}'.format(
|
||||
return '{schema}://{user}{apikey}/{targets}/?{params}'.format(
|
||||
schema=self.secure_protocol,
|
||||
user='{}@'.format(self.user) if self.user else '',
|
||||
apikey=self.pprint(self.apikey, privacy, safe=''),
|
||||
targets='/'.join(
|
||||
[NotifyOpsgenie.quote('{}{}'.format(
|
||||
|
|
@ -608,4 +840,14 @@ class NotifyOpsgenie(NotifyBase):
|
|||
if 'to' in results['qsd'] and len(results['qsd']['to']):
|
||||
results['targets'].append(results['qsd']['to'])
|
||||
|
||||
# Store our action (if defined)
|
||||
if 'action' in results['qsd'] and len(results['qsd']['action']):
|
||||
results['action'] = \
|
||||
NotifyOpsgenie.unquote(results['qsd']['action'])
|
||||
|
||||
# store any custom mapping defined
|
||||
results['mapping'] = \
|
||||
{NotifyOpsgenie.unquote(x): NotifyOpsgenie.unquote(y)
|
||||
for x, y in results['qsd:'].items()}
|
||||
|
||||
return results
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -38,8 +38,7 @@ from .base import NotifyBase
|
|||
from ..url import PrivacyMode
|
||||
from ..common import NotifyType
|
||||
from ..common import NotifyImageSize
|
||||
from ..utils import validate_regex
|
||||
from ..utils import parse_bool
|
||||
from ..utils.parse import validate_regex, parse_bool
|
||||
from ..locale import gettext_lazy as _
|
||||
|
||||
|
||||
|
|
@ -412,6 +411,18 @@ class NotifyPagerDuty(NotifyBase):
|
|||
|
||||
return True
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
return (
|
||||
self.secure_protocol, self.integration_key, self.apikey,
|
||||
self.source,
|
||||
)
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (c) 2025, 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:
|
||||
|
|
@ -33,8 +33,7 @@ from uuid import uuid4
|
|||
|
||||
from .base import NotifyBase
|
||||
from ..common import NotifyType
|
||||
from ..utils import parse_list
|
||||
from ..utils import validate_regex
|
||||
from ..utils.parse import parse_list, validate_regex
|
||||
from ..locale import gettext_lazy as _
|
||||
|
||||
|
||||
|
|
@ -299,6 +298,15 @@ class NotifyPagerTree(NotifyBase):
|
|||
|
||||
return True
|
||||
|
||||
@property
|
||||
def url_identifier(self):
|
||||
"""
|
||||
Returns all of the identifiers that make this URL unique from
|
||||
another simliar one. Targets or end points should never be identified
|
||||
here.
|
||||
"""
|
||||
return (self.secure_protocol, self.integration)
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue