parent
777913bd40
commit
1f8b5bd2e1
@ -0,0 +1,270 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com>
|
||||
# All rights reserved.
|
||||
#
|
||||
# This code is licensed under the MIT License.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files(the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions :
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
import six
|
||||
|
||||
from . import attachment
|
||||
from . import URLBase
|
||||
from .AppriseAsset import AppriseAsset
|
||||
from .logger import logger
|
||||
from .utils import GET_SCHEMA_RE
|
||||
|
||||
|
||||
class AppriseAttachment(object):
|
||||
"""
|
||||
Our Apprise Attachment File Manager
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, paths=None, asset=None, cache=True, **kwargs):
|
||||
"""
|
||||
Loads all of the paths/urls specified (if any).
|
||||
|
||||
The path can either be a single string identifying one explicit
|
||||
location, otherwise you can pass in a series of locations to scan
|
||||
via a list.
|
||||
|
||||
By default we cache our responses so that subsiquent calls does not
|
||||
cause the content to be retrieved again. For local file references
|
||||
this makes no difference at all. But for remote content, this does
|
||||
mean more then one call can be made to retrieve the (same) data. This
|
||||
method can be somewhat inefficient if disabled. Only disable caching
|
||||
if you understand the consequences.
|
||||
|
||||
You can alternatively set the cache value to an int identifying the
|
||||
number of seconds the previously retrieved can exist for before it
|
||||
should be considered expired.
|
||||
|
||||
It's also worth nothing that the cache value is only set to elements
|
||||
that are not already of subclass AttachBase()
|
||||
"""
|
||||
|
||||
# Initialize our attachment listings
|
||||
self.attachments = list()
|
||||
|
||||
# Set our cache flag
|
||||
self.cache = cache
|
||||
|
||||
# Prepare our Asset Object
|
||||
self.asset = \
|
||||
asset if isinstance(asset, AppriseAsset) else AppriseAsset()
|
||||
|
||||
# Now parse any paths specified
|
||||
if paths is not None:
|
||||
# Store our path(s)
|
||||
if not self.add(paths):
|
||||
# Parse Source domain based on from_addr
|
||||
raise TypeError("One or more attachments could not be added.")
|
||||
|
||||
def add(self, attachments, asset=None, cache=None):
|
||||
"""
|
||||
Adds one or more attachments into our list.
|
||||
|
||||
By default we cache our responses so that subsiquent calls does not
|
||||
cause the content to be retrieved again. For local file references
|
||||
this makes no difference at all. But for remote content, this does
|
||||
mean more then one call can be made to retrieve the (same) data. This
|
||||
method can be somewhat inefficient if disabled. Only disable caching
|
||||
if you understand the consequences.
|
||||
|
||||
You can alternatively set the cache value to an int identifying the
|
||||
number of seconds the previously retrieved can exist for before it
|
||||
should be considered expired.
|
||||
|
||||
It's also worth nothing that the cache value is only set to elements
|
||||
that are not already of subclass AttachBase()
|
||||
"""
|
||||
# Initialize our return status
|
||||
return_status = True
|
||||
|
||||
# Initialize our default cache value
|
||||
cache = cache if cache is not None else self.cache
|
||||
|
||||
if isinstance(asset, AppriseAsset):
|
||||
# prepare default asset
|
||||
asset = self.asset
|
||||
|
||||
if isinstance(attachments, attachment.AttachBase):
|
||||
# Go ahead and just add our attachments into our list
|
||||
self.attachments.append(attachments)
|
||||
return True
|
||||
|
||||
elif isinstance(attachments, six.string_types):
|
||||
# Save our path
|
||||
attachments = (attachments, )
|
||||
|
||||
elif not isinstance(attachments, (tuple, set, list)):
|
||||
logger.error(
|
||||
'An invalid attachment url (type={}) was '
|
||||
'specified.'.format(type(attachments)))
|
||||
return False
|
||||
|
||||
# Iterate over our attachments
|
||||
for _attachment in attachments:
|
||||
|
||||
if isinstance(_attachment, attachment.AttachBase):
|
||||
# Go ahead and just add our attachment into our list
|
||||
self.attachments.append(_attachment)
|
||||
continue
|
||||
|
||||
elif not isinstance(_attachment, six.string_types):
|
||||
logger.warning(
|
||||
"An invalid attachment (type={}) was specified.".format(
|
||||
type(_attachment)))
|
||||
return_status = False
|
||||
continue
|
||||
|
||||
logger.debug("Loading attachment: {}".format(_attachment))
|
||||
|
||||
# Instantiate ourselves an object, this function throws or
|
||||
# returns None if it fails
|
||||
instance = AppriseAttachment.instantiate(
|
||||
_attachment, asset=asset, cache=cache)
|
||||
if not isinstance(instance, attachment.AttachBase):
|
||||
return_status = False
|
||||
continue
|
||||
|
||||
# Add our initialized plugin to our server listings
|
||||
self.attachments.append(instance)
|
||||
|
||||
# Return our status
|
||||
return return_status
|
||||
|
||||
@staticmethod
|
||||
def instantiate(url, asset=None, cache=None, suppress_exceptions=True):
|
||||
"""
|
||||
Returns the instance of a instantiated attachment plugin based on
|
||||
the provided Attachment URL. If the url fails to be parsed, then None
|
||||
is returned.
|
||||
|
||||
A specified cache value will over-ride anything set
|
||||
|
||||
"""
|
||||
# Attempt to acquire the schema at the very least to allow our
|
||||
# attachment based urls.
|
||||
schema = GET_SCHEMA_RE.match(url)
|
||||
if schema is None:
|
||||
# Plan B is to assume we're dealing with a file
|
||||
schema = attachment.AttachFile.protocol
|
||||
url = '{}://{}'.format(schema, URLBase.quote(url))
|
||||
|
||||
else:
|
||||
# Ensure our schema is always in lower case
|
||||
schema = schema.group('schema').lower()
|
||||
|
||||
# Some basic validation
|
||||
if schema not in attachment.SCHEMA_MAP:
|
||||
logger.warning('Unsupported schema {}.'.format(schema))
|
||||
return None
|
||||
|
||||
# Parse our url details of the server object as dictionary containing
|
||||
# all of the information parsed from our URL
|
||||
results = attachment.SCHEMA_MAP[schema].parse_url(url)
|
||||
|
||||
if not results:
|
||||
# Failed to parse the server URL
|
||||
logger.warning('Unparseable URL {}.'.format(url))
|
||||
return None
|
||||
|
||||
# Prepare our Asset Object
|
||||
results['asset'] = \
|
||||
asset if isinstance(asset, AppriseAsset) else AppriseAsset()
|
||||
|
||||
if cache is not None:
|
||||
# Force an over-ride of the cache value to what we have specified
|
||||
results['cache'] = cache
|
||||
|
||||
if suppress_exceptions:
|
||||
try:
|
||||
# Attempt to create an instance of our plugin using the parsed
|
||||
# URL information
|
||||
attach_plugin = \
|
||||
attachment.SCHEMA_MAP[results['schema']](**results)
|
||||
|
||||
except Exception:
|
||||
# the arguments are invalid or can not be used.
|
||||
logger.warning('Could not load URL: %s' % url)
|
||||
return None
|
||||
|
||||
else:
|
||||
# Attempt to create an instance of our plugin using the parsed
|
||||
# URL information but don't wrap it in a try catch
|
||||
attach_plugin = attachment.SCHEMA_MAP[results['schema']](**results)
|
||||
|
||||
return attach_plugin
|
||||
|
||||
def clear(self):
|
||||
"""
|
||||
Empties our attachment list
|
||||
|
||||
"""
|
||||
self.attachments[:] = []
|
||||
|
||||
def size(self):
|
||||
"""
|
||||
Returns the total size of accumulated attachments
|
||||
"""
|
||||
return sum([len(a) for a in self.attachments if len(a) > 0])
|
||||
|
||||
def pop(self, index=-1):
|
||||
"""
|
||||
Removes an indexed Apprise Attachment from the stack and returns it.
|
||||
|
||||
by default the last element is poped from the list
|
||||
"""
|
||||
# Remove our entry
|
||||
return self.attachments.pop(index)
|
||||
|
||||
def __getitem__(self, index):
|
||||
"""
|
||||
Returns the indexed entry of a loaded apprise attachments
|
||||
"""
|
||||
return self.attachments[index]
|
||||
|
||||
def __bool__(self):
|
||||
"""
|
||||
Allows the Apprise object to be wrapped in an Python 3.x based 'if
|
||||
statement'. True is returned if at least one service has been loaded.
|
||||
"""
|
||||
return True if self.attachments else False
|
||||
|
||||
def __nonzero__(self):
|
||||
"""
|
||||
Allows the Apprise object to be wrapped in an Python 2.x based 'if
|
||||
statement'. True is returned if at least one service has been loaded.
|
||||
"""
|
||||
return True if self.attachments else False
|
||||
|
||||
def __iter__(self):
|
||||
"""
|
||||
Returns an iterator to our attachment list
|
||||
"""
|
||||
return iter(self.attachments)
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Returns the number of attachment entries loaded
|
||||
"""
|
||||
return len(self.attachments)
|
@ -0,0 +1,333 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com>
|
||||
# All rights reserved.
|
||||
#
|
||||
# This code is licensed under the MIT License.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files(the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions :
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
import os
|
||||
import time
|
||||
import mimetypes
|
||||
from ..URLBase import URLBase
|
||||
from ..utils import parse_bool
|
||||
|
||||
|
||||
class AttachBase(URLBase):
|
||||
"""
|
||||
This is the base class for all supported attachment types
|
||||
"""
|
||||
|
||||
# For attachment type detection; this amount of data is read into memory
|
||||
# 128KB (131072B)
|
||||
max_detect_buffer_size = 131072
|
||||
|
||||
# Unknown mimetype
|
||||
unknown_mimetype = 'application/octet-stream'
|
||||
|
||||
# Our filename when we can't otherwise determine one
|
||||
unknown_filename = 'apprise-attachment'
|
||||
|
||||
# Our filename extension when we can't otherwise determine one
|
||||
unknown_filename_extension = '.obj'
|
||||
|
||||
# The strict argument is a flag specifying whether the list of known MIME
|
||||
# types is limited to only the official types registered with IANA. When
|
||||
# strict is True, only the IANA types are supported; when strict is False
|
||||
# (the default), some additional non-standard but commonly used MIME types
|
||||
# are also recognized.
|
||||
strict = False
|
||||
|
||||
# The maximum file-size we will accept for an attachment size. If this is
|
||||
# set to zero (0), then no check is performed
|
||||
# 1 MB = 1048576 bytes
|
||||
# 5 MB = 5242880 bytes
|
||||
max_file_size = 5242880
|
||||
|
||||
def __init__(self, name=None, mimetype=None, cache=True, **kwargs):
|
||||
"""
|
||||
Initialize some general logging and common server arguments that will
|
||||
keep things consistent when working with the configurations that
|
||||
inherit this class.
|
||||
|
||||
Optionally provide a filename to over-ride name associated with the
|
||||
actual file retrieved (from where-ever).
|
||||
|
||||
The mime-type is automatically detected, but you can over-ride this by
|
||||
explicitly stating what it should be.
|
||||
|
||||
By default we cache our responses so that subsiquent calls does not
|
||||
cause the content to be retrieved again. For local file references
|
||||
this makes no difference at all. But for remote content, this does
|
||||
mean more then one call can be made to retrieve the (same) data. This
|
||||
method can be somewhat inefficient if disabled. Only disable caching
|
||||
if you understand the consequences.
|
||||
|
||||
You can alternatively set the cache value to an int identifying the
|
||||
number of seconds the previously retrieved can exist for before it
|
||||
should be considered expired.
|
||||
"""
|
||||
|
||||
super(AttachBase, self).__init__(**kwargs)
|
||||
|
||||
if not mimetypes.inited:
|
||||
# Ensure mimetypes has been initialized
|
||||
mimetypes.init()
|
||||
|
||||
# Attach Filename (does not have to be the same as path)
|
||||
self._name = name
|
||||
|
||||
# The mime type of the attached content. This is detected if not
|
||||
# otherwise specified.
|
||||
self._mimetype = mimetype
|
||||
|
||||
# The detected_mimetype, this is only used as a fallback if the
|
||||
# mimetype wasn't forced by the user
|
||||
self.detected_mimetype = None
|
||||
|
||||
# The detected filename by calling child class. A detected filename
|
||||
# is always used if no force naming was specified.
|
||||
self.detected_name = None
|
||||
|
||||
# Absolute path to attachment
|
||||
self.download_path = None
|
||||
|
||||
# Set our cache flag; it can be True or a (positive) integer
|
||||
try:
|
||||
self.cache = cache if isinstance(cache, bool) else int(cache)
|
||||
if self.cache < 0:
|
||||
err = 'A negative cache value ({}) was specified.'.format(
|
||||
cache)
|
||||
self.logger.warning(err)
|
||||
raise TypeError(err)
|
||||
|
||||
except (ValueError, TypeError):
|
||||
err = 'An invalid cache value ({}) was specified.'.format(cache)
|
||||
self.logger.warning(err)
|
||||
raise TypeError(err)
|
||||
|
||||
# Validate mimetype if specified
|
||||
if self._mimetype:
|
||||
if next((t for t in mimetypes.types_map.values()
|
||||
if self._mimetype == t), None) is None:
|
||||
err = 'An invalid mime-type ({}) was specified.'.format(
|
||||
mimetype)
|
||||
self.logger.warning(err)
|
||||
raise TypeError(err)
|
||||
|
||||
return
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
"""
|
||||
Returns the absolute path to the filename. If this is not known or
|
||||
is know but has been considered expired (due to cache setting), then
|
||||
content is re-retrieved prior to returning.
|
||||
"""
|
||||
|
||||
if not self.exists():
|
||||
# we could not obtain our path
|
||||
return None
|
||||
|
||||
return self.download_path
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""
|
||||
Returns the filename
|
||||
"""
|
||||
if self._name:
|
||||
# return our fixed content
|
||||
return self._name
|
||||
|
||||
if not self.exists():
|
||||
# we could not obtain our name
|
||||
return None
|
||||
|
||||
if not self.detected_name:
|
||||
# If we get here, our download was successful but we don't have a
|
||||
# filename based on our content.
|
||||
extension = mimetypes.guess_extension(self.mimetype)
|
||||
self.detected_name = '{}{}'.format(
|
||||
self.unknown_filename,
|
||||
extension if extension else self.unknown_filename_extension)
|
||||
|
||||
return self.detected_name
|
||||
|
||||
@property
|
||||
def mimetype(self):
|
||||
"""
|
||||
Returns mime type (if one is present).
|
||||
|
||||
Content is cached once determied to prevent overhead of future
|
||||
calls.
|
||||
"""
|
||||
|
||||
if self._mimetype:
|
||||
# return our pre-calculated cached content
|
||||
return self._mimetype
|
||||
|
||||
if not self.exists():
|
||||
# we could not obtain our attachment
|
||||
return None
|
||||
|
||||
if not self.detected_mimetype:
|
||||
# guess_type() returns: (type, encoding) and sets type to None
|
||||
# if it can't otherwise determine it.
|
||||
try:
|
||||
# Directly reference _name and detected_name to prevent
|
||||
# recursion loop (as self.name calls this function)
|
||||
self.detected_mimetype, _ = mimetypes.guess_type(
|
||||
self._name if self._name
|
||||
else self.detected_name, strict=self.strict)
|
||||
|
||||
except TypeError:
|
||||
# Thrown if None was specified in filename section
|
||||
pass
|
||||
|
||||
# Return our mime type
|
||||
return self.detected_mimetype \
|
||||
if self.detected_mimetype else self.unknown_mimetype
|
||||
|
||||
def exists(self):
|
||||
"""
|
||||
Simply returns true if the object has downloaded and stored the
|
||||
attachment AND the attachment has not expired.
|
||||
"""
|
||||
if self.download_path and os.path.isfile(self.download_path) \
|
||||
and self.cache:
|
||||
|
||||
# We have enough reason to look further into our cached content
|
||||
# and verify it has not expired.
|
||||
if self.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
|
||||
if age_in_sec <= self.cache:
|
||||
return True
|
||||
|
||||
except (OSError, IOError):
|
||||
# The file is not present
|
||||
pass
|
||||
|
||||
return self.download()
|
||||
|
||||
def invalidate(self):
|
||||
"""
|
||||
Release any temporary data that may be open by child classes.
|
||||
Externally fetched content should be automatically cleaned up when
|
||||
this function is called.
|
||||
|
||||
This function should also reset the following entries to None:
|
||||
- detected_name : Should identify a human readable filename
|
||||
- download_path: Must contain a absolute path to content
|
||||
- detected_mimetype: Should identify mimetype of content
|
||||
"""
|
||||
self.detected_name = None
|
||||
self.download_path = None
|
||||
self.detected_mimetype = None
|
||||
return
|
||||
|
||||
def download(self):
|
||||
"""
|
||||
This function must be over-ridden by inheriting classes.
|
||||
|
||||
Inherited classes MUST populate:
|
||||
- detected_name: Should identify a human readable filename
|
||||
- download_path: Must contain a absolute path to content
|
||||
- detected_mimetype: Should identify mimetype of content
|
||||
|
||||
If a download fails, you should ensure these values are set to None.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"download() is implimented by the child class.")
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url, verify_host=True, mimetype_db=None):
|
||||
"""Parses the URL and returns it broken apart into a dictionary.
|
||||
|
||||
This is very specific and customized for Apprise.
|
||||
|
||||
Args:
|
||||
url (str): The URL you want to fully parse.
|
||||
verify_host (:obj:`bool`, optional): a flag kept with the parsed
|
||||
URL which some child classes will later use to verify SSL
|
||||
keys (if SSL transactions take place). Unless under very
|
||||
specific circumstances, it is strongly recomended that
|
||||
you leave this default value set to True.
|
||||
|
||||
Returns:
|
||||
A dictionary is returned containing the URL fully parsed if
|
||||
successful, otherwise None is returned.
|
||||
"""
|
||||
|
||||
results = URLBase.parse_url(url, verify_host=verify_host)
|
||||
|
||||
if not results:
|
||||
# We're done; we failed to parse our url
|
||||
return results
|
||||
|
||||
# Allow overriding the default config mime type
|
||||
if 'mime' in results['qsd']:
|
||||
results['mimetype'] = results['qsd'].get('mime', '') \
|
||||
.strip().lower()
|
||||
|
||||
# Allow overriding the default file name
|
||||
if 'name' in results['qsd']:
|
||||
results['name'] = results['qsd'].get('name', '') \
|
||||
.strip().lower()
|
||||
|
||||
# Our cache value
|
||||
if 'cache' in results['qsd']:
|
||||
# First try to get it's integer value
|
||||
try:
|
||||
results['cache'] = int(results['qsd']['cache'])
|
||||
|
||||
except (ValueError, TypeError):
|
||||
# No problem, it just isn't an integer; now treat it as a bool
|
||||
# instead:
|
||||
results['cache'] = parse_bool(results['qsd']['cache'])
|
||||
|
||||
return results
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Returns the filesize of the attachment.
|
||||
|
||||
"""
|
||||
return os.path.getsize(self.path) if self.path else 0
|
||||
|
||||
def __bool__(self):
|
||||
"""
|
||||
Allows the Apprise object to be wrapped in an Python 3.x based 'if
|
||||
statement'. True is returned if our content was downloaded correctly.
|
||||
"""
|
||||
return True if self.path else False
|
||||
|
||||
def __nonzero__(self):
|
||||
"""
|
||||
Allows the Apprise object to be wrapped in an Python 2.x based 'if
|
||||
statement'. True is returned if our content was downloaded correctly.
|
||||
"""
|
||||
return True if self.path else False
|
@ -0,0 +1,129 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com>
|
||||
# All rights reserved.
|
||||
#
|
||||
# This code is licensed under the MIT License.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files(the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions :
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
import re
|
||||
import os
|
||||
from .AttachBase import AttachBase
|
||||
from ..AppriseLocale import gettext_lazy as _
|
||||
|
||||
|
||||
class AttachFile(AttachBase):
|
||||
"""
|
||||
A wrapper for File based attachment sources
|
||||
"""
|
||||
|
||||
# The default descriptive name associated with the service
|
||||
service_name = _('Local File')
|
||||
|
||||
# The default protocol
|
||||
protocol = 'file'
|
||||
|
||||
def __init__(self, path, **kwargs):
|
||||
"""
|
||||
Initialize Local File Attachment Object
|
||||
|
||||
"""
|
||||
super(AttachFile, self).__init__(**kwargs)
|
||||
|
||||
# Store path but mark it dirty since we have not performed any
|
||||
# verification at this point.
|
||||
self.dirty_path = os.path.expanduser(path)
|
||||
return
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {}
|
||||
|
||||
if self._mimetype:
|
||||
# A mime-type was enforced
|
||||
args['mime'] = self._mimetype
|
||||
|
||||
if self._name:
|
||||
# A name was enforced
|
||||
args['name'] = self._name
|
||||
|
||||
return 'file://{path}{args}'.format(
|
||||
path=self.quote(self.dirty_path),
|
||||
args='?{}'.format(self.urlencode(args)) if args else '',
|
||||
)
|
||||
|
||||
def download(self, **kwargs):
|
||||
"""
|
||||
Perform retrieval of our data.
|
||||
|
||||
For file base attachments, our data already exists, so we only need to
|
||||
validate it.
|
||||
"""
|
||||
|
||||
# Ensure any existing content set has been invalidated
|
||||
self.invalidate()
|
||||
|
||||
if not os.path.isfile(self.dirty_path):
|
||||
return False
|
||||
|
||||
if self.max_file_size > 0 and \
|
||||
os.path.getsize(self.dirty_path) > self.max_file_size:
|
||||
|
||||
# The content to attach is to large
|
||||
self.logger.error(
|
||||
'Content exceeds allowable maximum file length '
|
||||
'({}KB): {}'.format(
|
||||
int(self.max_file_size / 1024), self.url(privacy=True)))
|
||||
|
||||
# Return False (signifying a failure)
|
||||
return False
|
||||
|
||||
# We're good to go if we get here. Set our minimum requirements of
|
||||
# a call do download() before returning a success
|
||||
self.download_path = self.dirty_path
|
||||
self.detected_name = os.path.basename(self.download_path)
|
||||
|
||||
# We don't need to set our self.detected_mimetype as it can be
|
||||
# pulled at the time it's needed based on the detected_name
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL so that we can handle all different file paths
|
||||
and return it as our path object
|
||||
|
||||
"""
|
||||
|
||||
results = AttachBase.parse_url(url, verify_host=False)
|
||||
if not results:
|
||||
# We're done early; it's not a good URL
|
||||
return results
|
||||
|
||||
match = re.match(r'file://(?P<path>[^?]+)(\?.*)?', url, re.I)
|
||||
if not match:
|
||||
return None
|
||||
|
||||
results['path'] = AttachFile.unquote(match.group('path'))
|
||||
return results
|
@ -0,0 +1,321 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com>
|
||||
# All rights reserved.
|
||||
#
|
||||
# This code is licensed under the MIT License.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files(the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions :
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
import re
|
||||
import os
|
||||
import six
|
||||
import requests
|
||||
from tempfile import NamedTemporaryFile
|
||||
from .AttachBase import AttachBase
|
||||
from ..URLBase import PrivacyMode
|
||||
from ..AppriseLocale import gettext_lazy as _
|
||||
|
||||
|
||||
class AttachHTTP(AttachBase):
|
||||
"""
|
||||
A wrapper for HTTP based attachment sources
|
||||
"""
|
||||
|
||||
# The default descriptive name associated with the service
|
||||
service_name = _('Web Based')
|
||||
|
||||
# The default protocol
|
||||
protocol = 'http'
|
||||
|
||||
# The default secure protocol
|
||||
secure_protocol = 'https'
|
||||
|
||||
# The maximum number of seconds to wait for a connection to be established
|
||||
# before out-right just giving up
|
||||
connection_timeout_sec = 5.0
|
||||
|
||||
# The number of bytes in memory to read from the remote source at a time
|
||||
chunk_size = 8192
|
||||
|
||||
def __init__(self, headers=None, **kwargs):
|
||||
"""
|
||||
Initialize HTTP Object
|
||||
|
||||
headers can be a dictionary of key/value pairs that you want to
|
||||
additionally include as part of the server headers to post with
|
||||
|
||||
"""
|
||||
super(AttachHTTP, self).__init__(**kwargs)
|
||||
|
||||
self.schema = 'https' if self.secure else 'http'
|
||||
|
||||
self.fullpath = kwargs.get('fullpath')
|
||||
if not isinstance(self.fullpath, six.string_types):
|
||||
self.fullpath = '/'
|
||||
|
||||
self.headers = {}
|
||||
if headers:
|
||||
# Store our extra headers
|
||||
self.headers.update(headers)
|
||||
|
||||
# Where our content is written to upon a call to download.
|
||||
self._temp_file = None
|
||||
|
||||
return
|
||||
|
||||
def download(self, **kwargs):
|
||||
"""
|
||||
Perform retrieval of the configuration based on the specified request
|
||||
"""
|
||||
|
||||
# Ensure any existing content set has been invalidated
|
||||
self.invalidate()
|
||||
|
||||
# prepare header
|
||||
headers = {
|
||||
'User-Agent': self.app_id,
|
||||
}
|
||||
|
||||
# Apply any/all header over-rides defined
|
||||
headers.update(self.headers)
|
||||
|
||||
auth = None
|
||||
if self.user:
|
||||
auth = (self.user, self.password)
|
||||
|
||||
url = '%s://%s' % (self.schema, self.host)
|
||||
if isinstance(self.port, int):
|
||||
url += ':%d' % self.port
|
||||
|
||||
url += self.fullpath
|
||||
|
||||
self.logger.debug('HTTP POST URL: %s (cert_verify=%r)' % (
|
||||
url, self.verify_certificate,
|
||||
))
|
||||
|
||||
# Where our request object will temporarily live.
|
||||
r = None
|
||||
|
||||
# Always call throttle before any remote server i/o is made
|
||||
self.throttle()
|
||||
|
||||
try:
|
||||
# Make our request
|
||||
with requests.get(
|
||||
url,
|
||||
headers=headers,
|
||||
auth=auth,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.connection_timeout_sec,
|
||||
stream=True) as r:
|
||||
|
||||
# Handle Errors
|
||||
r.raise_for_status()
|
||||
|
||||
# Get our file-size (if known)
|
||||
try:
|
||||
file_size = int(r.headers.get('Content-Length', '0'))
|
||||
except (TypeError, ValueError):
|
||||
# Handle edge case where Content-Length is a bad value
|
||||
file_size = 0
|
||||
|
||||
# Perform a little Q/A on file limitations and restrictions
|
||||
if self.max_file_size > 0 and file_size > self.max_file_size:
|
||||
|
||||
# The content retrieved is to large
|
||||
self.logger.error(
|
||||
'HTTP response exceeds allowable maximum file length '
|
||||
'({}KB): {}'.format(
|
||||
int(self.max_file_size / 1024),
|
||||
self.url(privacy=True)))
|
||||
|
||||
# Return False (signifying a failure)
|
||||
return False
|
||||
|
||||
# Detect config format based on mime if the format isn't
|
||||
# already enforced
|
||||
self.detected_mimetype = r.headers.get('Content-Type')
|
||||
|
||||
d = r.headers.get('Content-Disposition', '')
|
||||
result = re.search(
|
||||
"filename=['\"]?(?P<name>[^'\"]+)['\"]?", d, re.I)
|
||||
if result:
|
||||
self.detected_name = result.group('name').strip()
|
||||
|
||||
# Create a temporary file to work with
|
||||
self._temp_file = NamedTemporaryFile()
|
||||
|
||||
# Get our chunk size
|
||||
chunk_size = self.chunk_size
|
||||
|
||||
# Track all bytes written to disk
|
||||
bytes_written = 0
|
||||
|
||||
# If we get here, we can now safely write our content to disk
|
||||
for chunk in r.iter_content(chunk_size=chunk_size):
|
||||
# filter out keep-alive chunks
|
||||
if chunk:
|
||||
self._temp_file.write(chunk)
|
||||
bytes_written = self._temp_file.tell()
|
||||
|
||||
# Prevent a case where Content-Length isn't provided
|
||||
# we don't want to fetch beyond our limits
|
||||
if self.max_file_size > 0:
|
||||
if bytes_written > self.max_file_size:
|
||||
# The content retrieved is to large
|
||||
self.logger.error(
|
||||
'HTTP response exceeds allowable maximum '
|
||||
'file length ({}KB): {}'.format(
|
||||
int(self.max_file_size / 1024),
|
||||
self.url(privacy=True)))
|
||||
|
||||
# Invalidate any variables previously set
|
||||
self.invalidate()
|
||||
|
||||
# Return False (signifying a failure)
|
||||
return False
|
||||
|
||||
elif bytes_written + chunk_size \
|
||||
> self.max_file_size:
|
||||
# Adjust out next read to accomodate up to our
|
||||
# limit +1. This will prevent us from readig
|
||||
# to much into our memory buffer
|
||||
self.max_file_size - bytes_written + 1
|
||||
|
||||
# Ensure our content is flushed to disk for post-processing
|
||||
self._temp_file.flush()
|
||||
|
||||
# Set our minimum requirements for a successful download() call
|
||||
self.download_path = self._temp_file.name
|
||||
if not self.detected_name:
|
||||
self.detected_name = os.path.basename(self.fullpath)
|
||||
|
||||
except requests.RequestException as e:
|
||||
self.logger.error(
|
||||
'A Connection error occured retrieving HTTP '
|
||||
'configuration from %s.' % self.host)
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
||||
# Invalidate any variables previously set
|
||||
self.invalidate()
|
||||
|
||||
# Return False (signifying a failure)
|
||||
return False
|
||||
|
||||
except (IOError, OSError):
|
||||
# IOError is present for backwards compatibility with Python
|
||||
# versions older then 3.3. >= 3.3 throw OSError now.
|
||||
|
||||
# Could not open and/or write the temporary file
|
||||
self.logger.error(
|
||||
'Could not write attachment to disk: {}'.format(
|
||||
self.url(privacy=True)))
|
||||
|
||||
# Invalidate any variables previously set
|
||||
self.invalidate()
|
||||
|
||||
# Return False (signifying a failure)
|
||||
return False
|
||||
|
||||
# Return our success
|
||||
return True
|
||||
|
||||
def invalidate(self):
|
||||
"""
|
||||
Close our temporary file
|
||||
"""
|
||||
if self._temp_file:
|
||||
self._temp_file.close()
|
||||
self._temp_file = None
|
||||
|
||||
super(AttachHTTP, self).invalidate()
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Prepare our cache value
|
||||
if isinstance(self.cache, bool) or not self.cache:
|
||||
cache = 'yes' if self.cache else 'no'
|
||||
else:
|
||||
cache = int(self.cache)
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
'cache': cache,
|
||||
}
|
||||
|
||||
if self._mimetype:
|
||||
# A format was enforced
|
||||
args['mime'] = self._mimetype
|
||||
|
||||
if self._name:
|
||||
# A name was enforced
|
||||
args['name'] = self._name
|
||||
|
||||
# Append our headers into our args
|
||||
args.update({'+{}'.format(k): v for k, v in self.headers.items()})
|
||||
|
||||
# Determine Authentication
|
||||
auth = ''
|
||||
if self.user and self.password:
|
||||
auth = '{user}:{password}@'.format(
|
||||
user=self.quote(self.user, safe=''),
|
||||
password=self.pprint(
|
||||
self.password, privacy, mode=PrivacyMode.Secret, safe=''),
|
||||
)
|
||||
elif self.user:
|
||||
auth = '{user}@'.format(
|
||||
user=self.quote(self.user, safe=''),
|
||||
)
|
||||
|
||||
default_port = 443 if self.secure else 80
|
||||
|
||||
return '{schema}://{auth}{hostname}{port}{fullpath}/?{args}'.format(
|
||||
schema=self.secure_protocol if self.secure else self.protocol,
|
||||
auth=auth,
|
||||
hostname=self.quote(self.host, safe=''),
|
||||
port='' if self.port is None or self.port == default_port
|
||||
else ':{}'.format(self.port),
|
||||
fullpath=self.quote(self.fullpath, safe='/'),
|
||||
args=self.urlencode(args),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
|
||||
"""
|
||||
results = AttachBase.parse_url(url)
|
||||
|
||||
if not results:
|
||||
# We're done early as we couldn't load the results
|
||||
return results
|
||||
|
||||
# Add our headers that the user can potentially over-ride if they wish
|
||||
# to to our returned result set
|
||||
results['headers'] = results['qsd-']
|
||||
results['headers'].update(results['qsd+'])
|
||||
|
||||
return results
|
@ -0,0 +1,119 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com>
|
||||
# All rights reserved.
|
||||
#
|
||||
# This code is licensed under the MIT License.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files(the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions :
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
import six
|
||||
import re
|
||||
|
||||
from os import listdir
|
||||
from os.path import dirname
|
||||
from os.path import abspath
|
||||
|
||||
# Maintains a mapping of all of the attachment services
|
||||
SCHEMA_MAP = {}
|
||||
|
||||
__all__ = []
|
||||
|
||||
|
||||
# Load our Lookup Matrix
|
||||
def __load_matrix(path=abspath(dirname(__file__)), name='apprise.attachment'):
|
||||
"""
|
||||
Dynamically load our schema map; this allows us to gracefully
|
||||
skip over modules we simply don't have the dependencies for.
|
||||
|
||||
"""
|
||||
# Used for the detection of additional Attachment Services objects
|
||||
# The .py extension is optional as we support loading directories too
|
||||
module_re = re.compile(r'^(?P<name>Attach[a-z0-9]+)(\.py)?$', re.I)
|
||||
|
||||
for f in listdir(path):
|
||||
match = module_re.match(f)
|
||||
if not match:
|
||||
# keep going
|
||||
continue
|
||||
|
||||
# Store our notification/plugin name:
|
||||
plugin_name = match.group('name')
|
||||
try:
|
||||
module = __import__(
|
||||
'{}.{}'.format(name, plugin_name),
|
||||
globals(), locals(),
|
||||
fromlist=[plugin_name])
|
||||
|
||||
except ImportError:
|
||||
# No problem, we can't use this object
|
||||
continue
|
||||
|
||||
if not hasattr(module, plugin_name):
|
||||
# Not a library we can load as it doesn't follow the simple rule
|
||||
# that the class must bear the same name as the notification
|
||||
# file itself.
|
||||
continue
|
||||
|
||||
# Get our plugin
|
||||
plugin = getattr(module, plugin_name)
|
||||
if not hasattr(plugin, 'app_id'):
|
||||
# Filter out non-notification modules
|
||||
continue
|
||||
|
||||
elif plugin_name in __all__:
|
||||
# we're already handling this object
|
||||
continue
|
||||
|
||||
# Add our module name to our __all__
|
||||
__all__.append(plugin_name)
|
||||
|
||||
# Ensure we provide the class as the reference to this directory and
|
||||
# not the module:
|
||||
globals()[plugin_name] = plugin
|
||||
|
||||
# Load protocol(s) if defined
|
||||
proto = getattr(plugin, 'protocol', None)
|
||||
if isinstance(proto, six.string_types):
|
||||
if proto not in SCHEMA_MAP:
|
||||
SCHEMA_MAP[proto] = plugin
|
||||
|
||||
elif isinstance(proto, (set, list, tuple)):
|
||||
# Support iterables list types
|
||||
for p in proto:
|
||||
if p not in SCHEMA_MAP:
|
||||
SCHEMA_MAP[p] = plugin
|
||||
|
||||
# Load secure protocol(s) if defined
|
||||
protos = getattr(plugin, 'secure_protocol', None)
|
||||
if isinstance(protos, six.string_types):
|
||||
if protos not in SCHEMA_MAP:
|
||||
SCHEMA_MAP[protos] = plugin
|
||||
|
||||
if isinstance(protos, (set, list, tuple)):
|
||||
# Support iterables list types
|
||||
for p in protos:
|
||||
if p not in SCHEMA_MAP:
|
||||
SCHEMA_MAP[p] = plugin
|
||||
|
||||
return SCHEMA_MAP
|
||||
|
||||
|
||||
# Dynamically build our schema base
|
||||
__load_matrix()
|
@ -0,0 +1,382 @@
|
||||
# Translations template for apprise.
|
||||
# Copyright (C) 2019 Chris Caron
|
||||
# This file is distributed under the same license as the apprise project.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2019.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: apprise 0.8.1\n"
|
||||
"Report-Msgid-Bugs-To: lead2gold@gmail.com\n"
|
||||
"POT-Creation-Date: 2019-10-13 21:39-0400\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.7.0\n"
|
||||
|
||||
msgid "API Key"
|
||||
msgstr ""
|
||||
|
||||
msgid "API Secret"
|
||||
msgstr ""
|
||||
|
||||
msgid "Access Key"
|
||||
msgstr ""
|
||||
|
||||
msgid "Access Key ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Access Secret"
|
||||
msgstr ""
|
||||
|
||||
msgid "Access Token"
|
||||
msgstr ""
|
||||
|
||||
msgid "Account SID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Add Tokens"
|
||||
msgstr ""
|
||||
|
||||
msgid "Application Key"
|
||||
msgstr ""
|
||||
|
||||
msgid "Application Secret"
|
||||
msgstr ""
|
||||
|
||||
msgid "Auth Token"
|
||||
msgstr ""
|
||||
|
||||
msgid "Authentication Key"
|
||||
msgstr ""
|
||||
|
||||
msgid "Authorization Token"
|
||||
msgstr ""
|
||||
|
||||
msgid "Avatar Image"
|
||||
msgstr ""
|
||||
|
||||
msgid "Batch Mode"
|
||||
msgstr ""
|
||||
|
||||
msgid "Blind Carbon Copy"
|
||||
msgstr ""
|
||||
|
||||
msgid "Bot Name"
|
||||
msgstr ""
|
||||
|
||||
msgid "Bot Token"
|
||||
msgstr ""
|
||||
|
||||
msgid "Cache Results"
|
||||
msgstr ""
|
||||
|
||||
msgid "Carbon Copy"
|
||||
msgstr ""
|
||||
|
||||
msgid "Channels"
|
||||
msgstr ""
|
||||
|
||||
msgid "Consumer Key"
|
||||
msgstr ""
|
||||
|
||||
msgid "Consumer Secret"
|
||||
msgstr ""
|
||||
|
||||
msgid "Country"
|
||||
msgstr ""
|
||||
|
||||
msgid "Detect Bot Owner"
|
||||
msgstr ""
|
||||
|
||||
msgid "Device ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Display Footer"
|
||||
msgstr ""
|
||||
|
||||
msgid "Domain"
|
||||
msgstr ""
|
||||
|
||||
msgid "Duration"
|
||||
msgstr ""
|
||||
|
||||
msgid "Email"
|
||||
msgstr ""
|
||||
|
||||
msgid "Encrypted Password"
|
||||
msgstr ""
|
||||
|
||||
msgid "Encrypted Salt"
|
||||
msgstr ""
|
||||
|
||||
msgid "Event"
|
||||
msgstr ""
|
||||
|
||||
msgid "Events"
|
||||
msgstr ""
|
||||
|
||||
msgid "Expire"
|
||||
msgstr ""
|
||||
|
||||
msgid "Facility"
|
||||
msgstr ""
|
||||
|
||||
msgid "Footer Logo"
|
||||
msgstr ""
|
||||
|
||||
msgid "From Email"
|
||||
msgstr ""
|
||||
|
||||
msgid "From Name"
|
||||
msgstr ""
|
||||
|
||||
msgid "From Phone No"
|
||||
msgstr ""
|
||||
|
||||
msgid "Group"
|
||||
msgstr ""
|
||||
|
||||
msgid "HTTP Header"
|
||||
msgstr ""
|
||||
|
||||
msgid "Hostname"
|
||||
msgstr ""
|
||||
|
||||
msgid "IRC Colors"
|
||||
msgstr ""
|
||||
|
||||
msgid "Include Image"
|
||||
msgstr ""
|
||||
|
||||
msgid "Log PID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Log to STDERR"
|
||||
msgstr ""
|
||||
|
||||
msgid "Message Hook"
|
||||
msgstr ""
|
||||
|
||||
msgid "Message Mode"
|
||||
msgstr ""
|
||||
|
||||
msgid "Modal"
|
||||
msgstr ""
|
||||
|
||||
msgid "Notify Format"
|
||||
msgstr ""
|
||||
|
||||
msgid "Organization"
|
||||
msgstr ""
|
||||
|
||||
msgid "Originating Address"
|
||||
msgstr ""
|
||||
|
||||
msgid "Overflow Mode"
|
||||
msgstr ""
|
||||
|
||||
msgid "Password"
|
||||
msgstr ""
|
||||
|
||||
msgid "Path"
|
||||
msgstr ""
|
||||
|
||||
msgid "Port"
|
||||
msgstr ""
|
||||
|
||||
msgid "Prefix"
|
||||
msgstr ""
|
||||
|
||||
msgid "Priority"
|
||||
msgstr ""
|
||||
|
||||
msgid "Project ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Provider Key"
|
||||
msgstr ""
|
||||
|
||||
msgid "Region"
|
||||
msgstr ""
|
||||
|
||||
msgid "Region Name"
|
||||
msgstr ""
|
||||
|
||||
msgid "Remove Tokens"
|
||||
msgstr ""
|
||||
|
||||
msgid "Retry"
|
||||
msgstr ""
|
||||
|
||||
msgid "Rooms"
|
||||
msgstr ""
|
||||
|
||||
msgid "Route"
|
||||
msgstr ""
|
||||
|
||||
msgid "SMTP Server"
|
||||
msgstr ""
|
||||
|
||||
msgid "Schema"
|
||||
msgstr ""
|
||||
|
||||
msgid "Secret Access Key"
|
||||
msgstr ""
|
||||
|
||||
msgid "Secret Key"
|
||||
msgstr ""
|
||||
|
||||
msgid "Secure Mode"
|
||||
msgstr ""
|
||||
|
||||
msgid "Sender ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Server Key"
|
||||
msgstr ""
|
||||
|
||||
msgid "Server Timeout"
|
||||
msgstr ""
|
||||
|
||||
msgid "Sound"
|
||||
msgstr ""
|
||||
|
||||
msgid "Source Email"
|
||||
msgstr ""
|
||||
|
||||
msgid "Source JID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Source Phone No"
|
||||
msgstr ""
|
||||
|
||||
msgid "Target Channel"
|
||||
msgstr ""
|
||||
|
||||
msgid "Target Channel ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Target Chat ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Target Device"
|
||||
msgstr ""
|
||||
|
||||
msgid "Target Device ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Target Email"
|
||||
msgstr ""
|
||||
|
||||
msgid "Target Emails"
|
||||
msgstr ""
|
||||
|
||||
msgid "Target Encoded ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Target JID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Target Phone No"
|
||||
msgstr ""
|
||||
|
||||
msgid "Target Room Alias"
|
||||
msgstr ""
|
||||
|
||||
msgid "Target Room ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Target Short Code"
|
||||
msgstr ""
|
||||
|
||||
msgid "Target Tag ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Target Topic"
|
||||
msgstr ""
|
||||
|
||||
msgid "Target User"
|
||||
msgstr ""
|
||||
|
||||
msgid "Targets"
|
||||
msgstr ""
|
||||
|
||||
msgid "Template"
|
||||
msgstr ""
|
||||
|
||||
msgid "Template Data"
|
||||
msgstr ""
|
||||
|
||||
msgid "Text To Speech"
|
||||
msgstr ""
|
||||
|
||||
msgid "To Channel ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "To Email"
|
||||
msgstr ""
|
||||
|
||||
msgid "To User ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Token"
|
||||
msgstr ""
|
||||
|
||||
msgid "Token A"
|
||||
msgstr ""
|
||||
|
||||
msgid "Token B"
|
||||
msgstr ""
|
||||
|
||||
msgid "Token C"
|
||||
msgstr ""
|
||||
|
||||
msgid "Urgency"
|
||||
msgstr ""
|
||||
|
||||
msgid "Use Avatar"
|
||||
msgstr ""
|
||||
|
||||
msgid "User Key"
|
||||
msgstr ""
|
||||
|
||||
msgid "User Name"
|
||||
msgstr ""
|
||||
|
||||
msgid "Username"
|
||||
msgstr ""
|
||||
|
||||
msgid "Verify SSL"
|
||||
msgstr ""
|
||||
|
||||
msgid "Version"
|
||||
msgstr ""
|
||||
|
||||
msgid "Webhook"
|
||||
msgstr ""
|
||||
|
||||
msgid "Webhook ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Webhook Mode"
|
||||
msgstr ""
|
||||
|
||||
msgid "Webhook Token"
|
||||
msgstr ""
|
||||
|
||||
msgid "X-Axis"
|
||||
msgstr ""
|
||||
|
||||
msgid "XEP"
|
||||
msgstr ""
|
||||
|
||||
msgid "Y-Axis"
|
||||
msgstr ""
|
||||
|
||||
msgid "ttl"
|
||||
msgstr ""
|
||||
|
@ -0,0 +1,293 @@
|
||||
# English translations for apprise.
|
||||
# Copyright (C) 2019 Chris Caron
|
||||
# This file is distributed under the same license as the apprise project.
|
||||
# Chris Caron <lead2gold@gmail.com>, 2019.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: apprise 0.7.6\n"
|
||||
"Report-Msgid-Bugs-To: lead2gold@gmail.com\n"
|
||||
"POT-Creation-Date: 2019-05-28 16:56-0400\n"
|
||||
"PO-Revision-Date: 2019-05-24 20:00-0400\n"
|
||||
"Last-Translator: Chris Caron <lead2gold@gmail.com>\n"
|
||||
"Language: en\n"
|
||||
"Language-Team: en <LL@li.org>\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.6.0\n"
|
||||
|
||||
msgid "API Key"
|
||||
msgstr ""
|
||||
|
||||
msgid "Access Key"
|
||||
msgstr ""
|
||||
|
||||
msgid "Access Key ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Access Secret"
|
||||
msgstr ""
|
||||
|
||||
msgid "Access Token"
|
||||
msgstr ""
|
||||
|
||||
msgid "Account SID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Add Tokens"
|
||||
msgstr ""
|
||||
|
||||
msgid "Application Key"
|
||||
msgstr ""
|
||||
|
||||
msgid "Application Secret"
|
||||
msgstr ""
|
||||
|
||||
msgid "Auth Token"
|
||||
msgstr ""
|
||||
|
||||
msgid "Authorization Token"
|
||||
msgstr ""
|
||||
|
||||
msgid "Avatar Image"
|
||||
msgstr ""
|
||||
|
||||
msgid "Bot Name"
|
||||
msgstr ""
|
||||
|
||||
msgid "Bot Token"
|
||||
msgstr ""
|
||||
|
||||
msgid "Channels"
|
||||
msgstr ""
|
||||
|
||||
msgid "Consumer Key"
|
||||
msgstr ""
|
||||
|
||||
msgid "Consumer Secret"
|
||||
msgstr ""
|
||||
|
||||
msgid "Detect Bot Owner"
|
||||
msgstr ""
|
||||
|
||||
msgid "Device ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Display Footer"
|
||||
msgstr ""
|
||||
|
||||
msgid "Domain"
|
||||
msgstr ""
|
||||
|
||||
msgid "Duration"
|
||||
msgstr ""
|
||||
|
||||
msgid "Events"
|
||||
msgstr ""
|
||||
|
||||
msgid "Footer Logo"
|
||||
msgstr ""
|
||||
|
||||
msgid "From Email"
|
||||
msgstr ""
|
||||
|
||||
msgid "From Name"
|
||||
msgstr ""
|
||||
|
||||
msgid "From Phone No"
|
||||
msgstr ""
|
||||
|
||||
msgid "Group"
|
||||
msgstr ""
|
||||
|
||||
msgid "HTTP Header"
|
||||
msgstr ""
|
||||
|
||||
msgid "Hostname"
|
||||
msgstr ""
|
||||
|
||||
msgid "Include Image"
|
||||
msgstr ""
|
||||
|
||||
msgid "Modal"
|
||||
msgstr ""
|
||||
|
||||
msgid "Notify Format"
|
||||
msgstr ""
|
||||
|
||||
msgid "Organization"
|
||||
msgstr ""
|
||||
|
||||
msgid "Overflow Mode"
|
||||
msgstr ""
|
||||
|
||||
msgid "Password"
|
||||
msgstr ""
|
||||
|
||||
msgid "Port"
|
||||
msgstr ""
|
||||
|
||||
msgid "Priority"
|
||||
msgstr ""
|
||||
|
||||
msgid "Provider Key"
|
||||
msgstr ""
|
||||
|
||||
msgid "Region"
|
||||
msgstr ""
|
||||
|
||||
msgid "Region Name"
|
||||
msgstr ""
|
||||
|
||||
msgid "Remove Tokens"
|
||||
msgstr ""
|
||||
|
||||
msgid "Rooms"
|
||||
msgstr ""
|
||||
|
||||
msgid "SMTP Server"
|
||||
msgstr ""
|
||||
|
||||
msgid "Schema"
|
||||
msgstr ""
|
||||
|
||||
msgid "Secret Access Key"
|
||||
msgstr ""
|
||||
|
||||
msgid "Secret Key"
|
||||
msgstr ""
|
||||
|
||||
msgid "Secure Mode"
|
||||
msgstr ""
|
||||
|
||||
msgid "Server Timeout"
|
||||
msgstr ""
|
||||
|
||||
msgid "Sound"
|
||||
msgstr ""
|
||||
|
||||
msgid "Source JID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Target Channel"
|
||||
msgstr ""
|
||||
|
||||
msgid "Target Chat ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Target Device"
|
||||
msgstr ""
|
||||
|
||||
msgid "Target Device ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Target Email"
|
||||
msgstr ""
|
||||
|
||||
msgid "Target Emails"
|
||||
msgstr ""
|
||||
|
||||
msgid "Target Encoded ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Target JID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Target Phone No"
|
||||
msgstr ""
|
||||
|
||||
msgid "Target Room Alias"
|
||||
msgstr ""
|
||||
|
||||
msgid "Target Room ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Target Short Code"
|
||||
msgstr ""
|
||||
|
||||
msgid "Target Tag ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Target Topic"
|
||||
msgstr ""
|
||||
|
||||
msgid "Target User"
|
||||
msgstr ""
|
||||
|
||||
msgid "Targets"
|
||||
msgstr ""
|
||||
|
||||
msgid "Text To Speech"
|
||||
msgstr ""
|
||||
|
||||
msgid "To Channel ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "To Email"
|
||||
msgstr ""
|
||||
|
||||
msgid "To User ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Token"
|
||||
msgstr ""
|
||||
|
||||
msgid "Token A"
|
||||
msgstr ""
|
||||
|
||||
msgid "Token B"
|
||||
msgstr ""
|
||||
|
||||
msgid "Token C"
|
||||
msgstr ""
|
||||
|
||||
msgid "Urgency"
|
||||
msgstr ""
|
||||
|
||||
msgid "Use Avatar"
|
||||
msgstr ""
|
||||
|
||||
msgid "User"
|
||||
msgstr ""
|
||||
|
||||
msgid "User Key"
|
||||
msgstr ""
|
||||
|
||||
msgid "User Name"
|
||||
msgstr ""
|
||||
|
||||
msgid "Username"
|
||||
msgstr ""
|
||||
|
||||
msgid "Verify SSL"
|
||||
msgstr ""
|
||||
|
||||
msgid "Version"
|
||||
msgstr ""
|
||||
|
||||
msgid "Webhook"
|
||||
msgstr ""
|
||||
|
||||
msgid "Webhook ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Webhook Mode"
|
||||
msgstr ""
|
||||
|
||||
msgid "Webhook Token"
|
||||
msgstr ""
|
||||
|
||||
msgid "X-Axis"
|
||||
msgstr ""
|
||||
|
||||
msgid "XEP"
|
||||
msgstr ""
|
||||
|
||||
msgid "Y-Axis"
|
||||
msgstr ""
|
||||
|
||||
#~ msgid "Access Key Secret"
|
||||
#~ msgstr ""
|
||||
|
@ -0,0 +1,327 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com>
|
||||
# All rights reserved.
|
||||
#
|
||||
# This code is licensed under the MIT License.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files(the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions :
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
# To use this plugin, simply signup with clicksend:
|
||||
# https://www.clicksend.com/
|
||||
#
|
||||
# You're done at this point, you only need to know your user/pass that
|
||||
# you signed up with.
|
||||
|
||||
# The following URLs would be accepted by Apprise:
|
||||
# - clicksend://{user}:{password}@{phoneno}
|
||||
# - clicksend://{user}:{password}@{phoneno1}/{phoneno2}
|
||||
|
||||
# The API reference used to build this plugin was documented here:
|
||||
# https://developers.clicksend.com/docs/rest/v3/
|
||||
#
|
||||
import re
|
||||
import requests
|
||||
from json import dumps
|
||||
from base64 import b64encode
|
||||
|
||||
from .NotifyBase import NotifyBase
|
||||
from ..URLBase import PrivacyMode
|
||||
from ..common import NotifyType
|
||||
from ..utils import parse_list
|
||||
from ..utils import parse_bool
|
||||
from ..AppriseLocale import gettext_lazy as _
|
||||
|
||||
# Extend HTTP Error Messages
|
||||
CLICKSEND_HTTP_ERROR_MAP = {
|
||||
401: 'Unauthorized - Invalid Token.',
|
||||
}
|
||||
|
||||
# Some Phone Number Detection
|
||||
IS_PHONE_NO = re.compile(r'^\+?(?P<phone>[0-9\s)(+-]+)\s*$')
|
||||
|
||||
# Used to break path apart into list of channels
|
||||
TARGET_LIST_DELIM = re.compile(r'[ \t\r\n,#\\/]+')
|
||||
|
||||
|
||||
class NotifyClickSend(NotifyBase):
|
||||
"""
|
||||
A wrapper for ClickSend Notifications
|
||||
"""
|
||||
|
||||
# The default descriptive name associated with the Notification
|
||||
service_name = 'ClickSend'
|
||||
|
||||
# The services URL
|
||||
service_url = 'https://clicksend.com/'
|
||||
|
||||
# The default secure protocol
|
||||
secure_protocol = 'clicksend'
|
||||
|
||||
# A URL that takes you to the setup/help of the specific protocol
|
||||
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_clicksend'
|
||||
|
||||
# ClickSend uses the http protocol with JSON requests
|
||||
notify_url = 'https://rest.clicksend.com/v3/sms/send'
|
||||
|
||||
# The maximum length of the body
|
||||
body_maxlen = 160
|
||||
|
||||
# A title can not be used for SMS Messages. Setting this to zero will
|
||||
# cause any title (if defined) to get placed into the message body.
|
||||
title_maxlen = 0
|
||||
|
||||
# The maximum SMS batch size accepted by the ClickSend API
|
||||
default_batch_size = 1000
|
||||
|
||||
# Define object templates
|
||||
templates = (
|
||||
'{schema}://{user}:{password}@{targets}',
|
||||
)
|
||||
|
||||
# Define our template tokens
|
||||
template_tokens = dict(NotifyBase.template_tokens, **{
|
||||
'user': {
|
||||
'name': _('User Name'),
|
||||
'type': 'string',
|
||||
'required': True,
|
||||
},
|
||||
'password': {
|
||||
'name': _('Password'),
|
||||
'type': 'string',
|
||||
'private': True,
|
||||
'required': True,
|
||||
},
|
||||
'target_phone': {
|
||||
'name': _('Target Phone No'),
|
||||
'type': 'string',
|
||||
'prefix': '+',
|
||||
'regex': (r'^[0-9\s)(+-]+$', 'i'),
|
||||
'map_to': 'targets',
|
||||
},
|
||||
'targets': {
|
||||
'name': _('Targets'),
|
||||
'type': 'list:string',
|
||||
'required': True,
|
||||
},
|
||||
})
|
||||
|
||||
# Define our template arguments
|
||||
template_args = dict(NotifyBase.template_args, **{
|
||||
'to': {
|
||||
'alias_of': 'targets',
|
||||
},
|
||||
'batch': {
|
||||
'name': _('Batch Mode'),
|
||||
'type': 'bool',
|
||||
'default': False,
|
||||
},
|
||||
})
|
||||
|
||||
def __init__(self, targets=None, batch=False, **kwargs):
|
||||
"""
|
||||
Initialize ClickSend Object
|
||||
"""
|
||||
super(NotifyClickSend, self).__init__(**kwargs)
|
||||
|
||||
# Prepare Batch Mode Flag
|
||||
self.batch = batch
|
||||
|
||||
# Parse our targets
|
||||
self.targets = list()
|
||||
|
||||
if not (self.user and self.password):
|
||||
msg = 'A ClickSend user/pass was not provided.'
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
for target in parse_list(targets):
|
||||
# Validate targets and drop bad ones:
|
||||
result = IS_PHONE_NO.match(target)
|
||||
if result:
|
||||
# Further check our phone # for it's digit count
|
||||
result = ''.join(re.findall(r'\d+', result.group('phone')))
|
||||
if len(result) < 11 or len(result) > 14:
|
||||
self.logger.warning(
|
||||
'Dropped invalid phone # '
|
||||
'({}) specified.'.format(target),
|
||||
)
|
||||
continue
|
||||
|
||||
# store valid phone number
|
||||
self.targets.append(result)
|
||||
continue
|
||||
|
||||
self.logger.warning(
|
||||
'Dropped invalid phone # '
|
||||
'({}) specified.'.format(target))
|
||||
|
||||
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||
"""
|
||||
Perform ClickSend Notification
|
||||
"""
|
||||
|
||||
if len(self.targets) == 0:
|
||||
# There were no services to notify
|
||||
self.logger.warning('There were no ClickSend targets to notify.')
|
||||
return False
|
||||
|
||||
headers = {
|
||||
'User-Agent': self.app_id,
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
'Authorization': 'Basic {}'.format(
|
||||
b64encode('{}:{}'.format(
|
||||
self.user, self.password).encode('utf-8'))),
|
||||
}
|
||||
|
||||
# error tracking (used for function return)
|
||||
has_error = False
|
||||
|
||||
# prepare JSON Object
|
||||
payload = {
|
||||
'messages': []
|
||||
}
|
||||
|
||||
# Send in batches if identified to do so
|
||||
default_batch_size = 1 if not self.batch else self.default_batch_size
|
||||
|
||||
for index in range(0, len(self.targets), default_batch_size):
|
||||
payload['messages'] = [{
|
||||
'source': 'php',
|
||||
'body': body,
|
||||
'to': '+{}'.format(to),
|
||||
} for to in self.targets[index:index + default_batch_size]]
|
||||
|
||||
self.logger.debug('ClickSend POST URL: %s (cert_verify=%r)' % (
|
||||
self.notify_url, self.verify_certificate,
|
||||
))
|
||||
self.logger.debug('ClickSend Payload: %s' % str(payload))
|
||||
|
||||
# Always call throttle before any remote server i/o is made
|
||||
self.throttle()
|
||||
try:
|
||||
r = requests.post(
|
||||
self.notify_url,
|
||||
data=dumps(payload),
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
)
|
||||
if r.status_code != requests.codes.ok:
|
||||
# We had a problem
|
||||
status_str = \
|
||||
NotifyClickSend.http_response_code_lookup(
|
||||
r.status_code, CLICKSEND_HTTP_ERROR_MAP)
|
||||
|
||||
self.logger.warning(
|
||||
'Failed to send {} ClickSend notification{}: '
|
||||
'{}{}error={}.'.format(
|
||||
len(payload['messages']),
|
||||
' to {}'.format(self.targets[index])
|
||||
if default_batch_size == 1 else '(s)',
|
||||
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 {} ClickSend notification{}.'
|
||||
.format(
|
||||
len(payload['messages']),
|
||||
' to {}'.format(self.targets[index])
|
||||
if default_batch_size == 1 else '(s)',
|
||||
))
|
||||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured sending {} ClickSend '
|
||||
'notification(s).'.format(len(payload['messages'])))
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
||||
# Mark our failure
|
||||
has_error = True
|
||||
continue
|
||||
|
||||
return not has_error
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
'batch': 'yes' if self.batch else 'no',
|
||||
}
|
||||
|
||||
# Setup Authentication
|
||||
auth = '{user}:{password}@'.format(
|
||||
user=NotifyClickSend.quote(self.user, safe=''),
|
||||
password=self.pprint(
|
||||
self.password, privacy, mode=PrivacyMode.Secret, safe=''),
|
||||
)
|
||||
|
||||
return '{schema}://{auth}{targets}?{args}'.format(
|
||||
schema=self.secure_protocol,
|
||||
auth=auth,
|
||||
targets='/'.join(
|
||||
[NotifyClickSend.quote(x, safe='') for x in self.targets]),
|
||||
args=NotifyClickSend.urlencode(args),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
|
||||
"""
|
||||
results = NotifyBase.parse_url(url, verify_host=False)
|
||||
if not results:
|
||||
# We're done early as we couldn't load the results
|
||||
return results
|
||||
|
||||
# All elements are targets
|
||||
results['targets'] = [NotifyClickSend.unquote(results['host'])]
|
||||
|
||||
# All entries after the hostname are additional targets
|
||||
results['targets'].extend(
|
||||
NotifyClickSend.split_path(results['fullpath']))
|
||||
|
||||
# Get Batch Mode Flag
|
||||
results['batch'] = \
|
||||
parse_bool(results['qsd'].get('batch', False))
|
||||
|
||||
# Support the 'to' variable so that we can support rooms this way too
|
||||
# The 'to' makes it easier to use yaml configuration
|
||||
if 'to' in results['qsd'] and len(results['qsd']['to']):
|
||||
results['targets'] += [x for x in filter(
|
||||
bool, TARGET_LIST_DELIM.split(
|
||||
NotifyClickSend.unquote(results['qsd']['to'])))]
|
||||
|
||||
return results
|
@ -0,0 +1,241 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com>
|
||||
# All rights reserved.
|
||||
#
|
||||
# This code is licensed under the MIT License.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files(the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions :
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
# To use this plugin, you must have a Kumulos account set up. Add a client
|
||||
# and link it with your phone using the phone app (using your Companion App
|
||||
# option in the profile menu area):
|
||||
# Android: https://play.google.com/store/apps/\
|
||||
# details?id=com.kumulos.companion
|
||||
# iOS: https://apps.apple.com/us/app/kumulos/id1463947782
|
||||
#
|
||||
# The API reference used to build this plugin was documented here:
|
||||
# https://docs.kumulos.com/messaging/api/#sending-in-app-messages
|
||||
#
|
||||
import requests
|
||||
from json import dumps
|
||||
|
||||
from .NotifyBase import NotifyBase
|
||||
from ..common import NotifyType
|
||||
from ..utils import validate_regex
|
||||
from ..AppriseLocale import gettext_lazy as _
|
||||
|
||||
# Extend HTTP Error Messages
|
||||
KUMULOS_HTTP_ERROR_MAP = {
|
||||
401: 'Unauthorized - Invalid API and/or Server Key.',
|
||||
422: 'Unprocessable Entity - The request was unparsable.',
|
||||
400: 'Bad Request - Targeted users do not exist or have unsubscribed.',
|
||||
}
|
||||
|
||||
|
||||
class NotifyKumulos(NotifyBase):
|
||||
"""
|
||||
A wrapper for Kumulos Notifications
|
||||
"""
|
||||
|
||||
# The default descriptive name associated with the Notification
|
||||
service_name = 'Kumulos'
|
||||
|
||||
# The services URL
|
||||
service_url = 'https://kumulos.com/'
|
||||
|
||||
# The default secure protocol
|
||||
secure_protocol = 'kumulos'
|
||||
|
||||
# A URL that takes you to the setup/help of the specific protocol
|
||||
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_kumulos'
|
||||
|
||||
# Kumulos uses the http protocol with JSON requests
|
||||
notify_url = 'https://messages.kumulos.com/v2/notifications'
|
||||
|
||||
# The maximum allowable characters allowed in the title per message
|
||||
title_maxlen = 64
|
||||
|
||||
# The maximum allowable characters allowed in the body per message
|
||||
body_maxlen = 240
|
||||
|
||||
# Define object templates
|
||||
templates = (
|
||||
'{schema}://{apikey}/{serverkey}',
|
||||
)
|
||||
|
||||
# Define our template tokens
|
||||
template_tokens = dict(NotifyBase.template_tokens, **{
|
||||
'apikey': {
|
||||
'name': _('API Key'),
|
||||
'type': 'string',
|
||||
'private': True,
|
||||
'required': True,
|
||||
# UUID4
|
||||
'regex': (r'^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-'
|
||||
r'[89ab][0-9a-f]{3}-[0-9a-f]{12}$', 'i')
|
||||
},
|
||||
'serverkey': {
|
||||
'name': _('Server Key'),
|
||||
'type': 'string',
|
||||
'private': True,
|
||||
'required': True,
|
||||
'regex': (r'^[A-Z0-9+]{36}$', 'i'),
|
||||
},
|
||||
})
|
||||
|
||||
def __init__(self, apikey, serverkey, **kwargs):
|
||||
"""
|
||||
Initialize Kumulos Object
|
||||
"""
|
||||
super(NotifyKumulos, self).__init__(**kwargs)
|
||||
|
||||
# API Key (associated with project)
|
||||
self.apikey = validate_regex(
|
||||
apikey, *self.template_tokens['apikey']['regex'])
|
||||
if not self.apikey:
|
||||
msg = 'An invalid Kumulos API Key ' \
|
||||
'({}) was specified.'.format(apikey)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
# Server Key (associated with project)
|
||||
self.serverkey = validate_regex(
|
||||
serverkey, *self.template_tokens['serverkey']['regex'])
|
||||
if not self.serverkey:
|
||||
msg = 'An invalid Kumulos Server Key ' \
|
||||
'({}) was specified.'.format(serverkey)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||
"""
|
||||
Perform Kumulos Notification
|
||||
"""
|
||||
|
||||
headers = {
|
||||
'User-Agent': self.app_id,
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
}
|
||||
|
||||
# prepare JSON Object
|
||||
payload = {
|
||||
'target': {
|
||||
'broadcast': True,
|
||||
},
|
||||
'content': {
|
||||
'title': title,
|
||||
'message': body,
|
||||
},
|
||||
}
|
||||
|
||||
# Determine Authentication
|
||||
auth = (self.apikey, self.serverkey)
|
||||
|
||||
self.logger.debug('Kumulos POST URL: %s (cert_verify=%r)' % (
|
||||
self.notify_url, self.verify_certificate,
|
||||
))
|
||||
self.logger.debug('Kumulos Payload: %s' % str(payload))
|
||||
|
||||
# Always call throttle before any remote server i/o is made
|
||||
self.throttle()
|
||||
try:
|
||||
r = requests.post(
|
||||
self.notify_url,
|
||||
data=dumps(payload),
|
||||
headers=headers,
|
||||
auth=auth,
|
||||
verify=self.verify_certificate,
|
||||
)
|
||||
if r.status_code != requests.codes.ok:
|
||||
# We had a problem
|
||||
status_str = \
|
||||
NotifyKumulos.http_response_code_lookup(
|
||||
r.status_code, KUMULOS_HTTP_ERROR_MAP)
|
||||
|
||||
self.logger.warning(
|
||||
'Failed to send Kumulos notification: '
|
||||
'{}{}error={}.'.format(
|
||||
status_str,
|
||||
', ' if status_str else '',
|
||||
r.status_code))
|
||||
|
||||
self.logger.debug(
|
||||
'Response Details:\r\n{}'.format(r.content))
|
||||
|
||||
return False
|
||||
|
||||
else:
|
||||
self.logger.info('Sent Kumulos notification.')
|
||||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured sending Kumulos '
|
||||
'notification.')
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
||||
return False
|
||||
return True
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
}
|
||||
|
||||
return '{schema}://{apikey}/{serverkey}/?{args}'.format(
|
||||
schema=self.secure_protocol,
|
||||
apikey=self.pprint(self.apikey, privacy, safe=''),
|
||||
serverkey=self.pprint(self.serverkey, privacy, safe=''),
|
||||
args=NotifyKumulos.urlencode(args),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
|
||||
"""
|
||||
results = NotifyBase.parse_url(url)
|
||||
|
||||
if not results:
|
||||
# We're done early as we couldn't load the results
|
||||
return results
|
||||
|
||||
# The first token is stored in the hostname
|
||||
results['apikey'] = NotifyKumulos.unquote(results['host'])
|
||||
|
||||
# Now fetch the remaining tokens
|
||||
try:
|
||||
results['serverkey'] = \
|
||||
NotifyKumulos.split_path(results['fullpath'])[0]
|
||||
|
||||
except IndexError:
|
||||
# no token
|
||||
results['serverkey'] = None
|
||||
|
||||
return results
|
@ -0,0 +1,370 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com>
|
||||
# All rights reserved.
|
||||
#
|
||||
# This code is licensed under the MIT License.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files(the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions :
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
# Create an account https://msg91.com/ if you don't already have one
|
||||
#
|
||||
# Get your (authkey) from the dashboard here:
|
||||
# - https://world.msg91.com/user/index.php#api
|
||||
#
|
||||
# Get details on the API used in this plugin here:
|
||||
# - https://world.msg91.com/apidoc/textsms/send-sms.php
|
||||
|
||||
import re
|
||||
import requests
|
||||
|
||||
from .NotifyBase import NotifyBase
|
||||
from ..common import NotifyType
|
||||
from ..utils import parse_list
|
||||
from ..utils import validate_regex
|
||||
from ..AppriseLocale import gettext_lazy as _
|
||||
|
||||
# Some Phone Number Detection
|
||||
IS_PHONE_NO = re.compile(r'^\+?(?P<phone>[0-9\s)(+-]+)\s*$')
|
||||
|
||||
|
||||
class MSG91Route(object):
|
||||
"""
|
||||
Transactional SMS Routes
|
||||
route=1 for promotional, route=4 for transactional SMS.
|
||||
"""
|
||||
PROMOTIONAL = 1
|
||||
TRANSACTIONAL = 4
|
||||
|
||||
|
||||
# Used for verification
|
||||
MSG91_ROUTES = (
|
||||
MSG91Route.PROMOTIONAL,
|
||||
MSG91Route.TRANSACTIONAL,
|
||||
)
|
||||
|
||||
|
||||
class MSG91Country(object):
|
||||
"""
|
||||
Optional value that can be specified on the MSG91 api
|
||||
"""
|
||||
INTERNATIONAL = 0
|
||||
USA = 1
|
||||
INDIA = 91
|
||||
|
||||
|
||||
# Used for verification
|
||||
MSG91_COUNTRIES = (
|
||||
MSG91Country.INTERNATIONAL,
|
||||
MSG91Country.USA,
|
||||
MSG91Country.INDIA,
|
||||
)
|
||||
|
||||
|
||||
class NotifyMSG91(NotifyBase):
|
||||
"""
|
||||
A wrapper for MSG91 Notifications
|
||||
"""
|
||||
|
||||
# The default descriptive name associated with the Notification
|
||||
service_name = 'MSG91'
|
||||
|
||||
# The services URL
|
||||
service_url = 'https://msg91.com'
|
||||
|
||||
# The default protocol
|
||||
secure_protocol = 'msg91'
|
||||
|
||||
# A URL that takes you to the setup/help of the specific protocol
|
||||
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_msg91'
|
||||
|
||||
# MSG91 uses the http protocol with JSON requests
|
||||
notify_url = 'https://world.msg91.com/api/sendhttp.php'
|
||||
|
||||
# The maximum length of the body
|
||||
body_maxlen = 140
|
||||
|
||||
# A title can not be used for SMS Messages. Setting this to zero will
|
||||
# cause any title (if defined) to get placed into the message body.
|
||||
title_maxlen = 0
|
||||
|
||||
# Define object templates
|
||||
templates = (
|
||||
'{schema}://{authkey}/{targets}',
|
||||
'{schema}://{sender}@{authkey}/{targets}',
|
||||
)
|
||||
|
||||
# Define our template tokens
|
||||
template_tokens = dict(NotifyBase.template_tokens, **{
|
||||
'authkey': {
|
||||
'name': _('Authentication Key'),
|
||||
'type': 'string',
|
||||
'required': True,
|
||||
'private': True,
|
||||
'regex': (r'^[a-z0-9]+$', 'i'),
|
||||
},
|
||||
'target_phone': {
|
||||
'name': _('Target Phone No'),
|
||||
'type': 'string',
|
||||
'prefix': '+',
|
||||
'regex': (r'^[0-9\s)(+-]+$', 'i'),
|
||||
'map_to': 'targets',
|
||||
},
|
||||
'targets': {
|
||||
'name': _('Targets'),
|
||||
'type': 'list:string',
|
||||
},
|
||||
'sender': {
|
||||
'name': _('Sender ID'),
|
||||
'type': 'string',
|
||||
},
|
||||
})
|
||||
|
||||
# Define our template arguments
|
||||
template_args = dict(NotifyBase.template_args, **{
|
||||
'to': {
|
||||
'alias_of': 'targets',
|
||||
},
|
||||
'route': {
|
||||
'name': _('Route'),
|
||||
'type': 'choice:int',
|
||||
'values': MSG91_ROUTES,
|
||||
'default': MSG91Route.TRANSACTIONAL,
|
||||
},
|
||||
'country': {
|
||||
'name': _('Country'),
|
||||
'type': 'choice:int',
|
||||
'values': MSG91_COUNTRIES,
|
||||
},
|
||||
})
|
||||
|
||||
def __init__(self, authkey, targets=None, sender=None, route=None,
|
||||
country=None, **kwargs):
|
||||
"""
|
||||
Initialize MSG91 Object
|
||||
"""
|
||||
super(NotifyMSG91, self).__init__(**kwargs)
|
||||
|
||||
# Authentication Key (associated with project)
|
||||
self.authkey = validate_regex(
|
||||
authkey, *self.template_tokens['authkey']['regex'])
|
||||
if not self.authkey:
|
||||
msg = 'An invalid MSG91 Authentication Key ' \
|
||||
'({}) was specified.'.format(authkey)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
if route is None:
|
||||
self.route = self.template_args['route']['default']
|
||||
|
||||
else:
|
||||
try:
|
||||
self.route = int(route)
|
||||
if self.route not in MSG91_ROUTES:
|
||||
# Let outer except catch thi
|
||||
raise ValueError()
|
||||
|
||||
except (ValueError, TypeError):
|
||||
msg = 'The MSG91 route specified ({}) is invalid.'\
|
||||
.format(route)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
if country:
|
||||
try:
|
||||
self.country = int(country)
|
||||
if self.country not in MSG91_COUNTRIES:
|
||||
# Let outer except catch thi
|
||||
raise ValueError()
|
||||
|
||||
except (ValueError, TypeError):
|
||||
msg = 'The MSG91 country specified ({}) is invalid.'\
|
||||
.format(country)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
else:
|
||||
self.country = country
|
||||
|
||||
# Store our sender
|
||||
self.sender = sender
|
||||
|
||||
# Parse our targets
|
||||
self.targets = list()
|
||||
|
||||
for target in parse_list(targets):
|
||||
# Validate targets and drop bad ones:
|
||||
result = IS_PHONE_NO.match(target)
|
||||
if result:
|
||||
# Further check our phone # for it's digit count
|
||||
result = ''.join(re.findall(r'\d+', result.group('phone')))
|
||||
if len(result) < 11 or len(result) > 14:
|
||||
self.logger.warning(
|
||||
'Dropped invalid phone # '
|
||||
'({}) specified.'.format(target),
|
||||
)
|
||||
continue
|
||||
|
||||
# store valid phone number
|
||||
self.targets.append(result)
|
||||
continue
|
||||
|
||||
self.logger.warning(
|
||||
'Dropped invalid phone # '
|
||||
'({}) specified.'.format(target),
|
||||
)
|
||||
|
||||
if not self.targets:
|
||||
# We have a bot token and no target(s) to message
|
||||
msg = 'No MSG91 targets to notify.'
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
return
|
||||
|
||||
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||
"""
|
||||
Perform MSG91 Notification
|
||||
"""
|
||||
|
||||
# Prepare our headers
|
||||
headers = {
|
||||
'User-Agent': self.app_id,
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
}
|
||||
|
||||
# Prepare our payload
|
||||
payload = {
|
||||
'sender': self.sender if self.sender else self.app_id,
|
||||
'authkey': self.authkey,
|
||||
'message': body,
|
||||
'response': 'json',
|
||||
# target phone numbers are sent with a comma delimiter
|
||||
'mobiles': ','.join(self.targets),
|
||||
'route': str(self.route),
|
||||
}
|
||||
|
||||
if self.country:
|
||||
payload['country'] = str(self.country)
|
||||
|
||||
# Some Debug Logging
|
||||
self.logger.debug('MSG91 POST URL: {} (cert_verify={})'.format(
|
||||
self.notify_url, self.verify_certificate))
|
||||
self.logger.debug('MSG91 Payload: {}' .format(payload))
|
||||
|
||||
# Always call throttle before any remote server i/o is made
|
||||
self.throttle()
|
||||
|
||||
try:
|
||||
r = requests.post(
|
||||
self.notify_url,
|
||||
data=payload,
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
)
|
||||
|
||||
if r.status_code != requests.codes.ok:
|
||||
# We had a problem
|
||||
status_str = \
|
||||
NotifyMSG91.http_response_code_lookup(
|
||||
r.status_code)
|
||||
|
||||
self.logger.warning(
|
||||
'Failed to send MSG91 notification to {}: '
|
||||
'{}{}error={}.'.format(
|
||||
','.join(self.targets),
|
||||
status_str,
|
||||
', ' if status_str else '',
|
||||
r.status_code))
|
||||
|
||||
self.logger.debug(
|
||||
'Response Details:\r\n{}'.format(r.content))
|
||||
return False
|
||||
|
||||
else:
|
||||
self.logger.info(
|
||||
'Sent MSG91 notification to %s.' % ','.join(self.targets))
|
||||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured sending MSG91:%s '
|
||||
'notification.' % ','.join(self.targets)
|
||||
)
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
'route': str(self.route),
|
||||
}
|
||||
|
||||
if self.country:
|
||||
args['country'] = str(self.country)
|
||||
|
||||
return '{schema}://{authkey}/{targets}/?{args}'.format(
|
||||
schema=self.secure_protocol,
|
||||
authkey=self.pprint(self.authkey, privacy, safe=''),
|
||||
targets='/'.join(
|
||||
[NotifyMSG91.quote(x, safe='') for x in self.targets]),
|
||||
args=NotifyMSG91.urlencode(args))
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
|
||||
"""
|
||||
|
||||
results = NotifyBase.parse_url(url)
|
||||
|
||||
if not results:
|
||||
# We're done early as we couldn't load the results
|
||||
return results
|
||||
|
||||
# Get our entries; split_path() looks after unquoting content for us
|
||||
# by default
|
||||
results['targets'] = NotifyMSG91.split_path(results['fullpath'])
|
||||
|
||||
# The hostname is our authentication key
|
||||
results['authkey'] = NotifyMSG91.unquote(results['host'])
|
||||
|
||||
if 'route' in results['qsd'] and len(results['qsd']['route']):
|
||||
results['route'] = results['qsd']['route']
|
||||
|
||||
if 'country' in results['qsd'] and len(results['qsd']['country']):
|
||||
results['country'] = results['qsd']['country']
|
||||
|
||||
# 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'] += \
|
||||
NotifyMSG91.parse_list(results['qsd']['to'])
|
||||
|
||||
return results
|
@ -0,0 +1,370 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com>
|
||||
# All rights reserved.
|
||||
#
|
||||
# This code is licensed under the MIT License.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files(the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions :
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
# Create an account https://messagebird.com if you don't already have one
|
||||
#
|
||||
# Get your (apikey) and api example from the dashboard here:
|
||||
# - https://dashboard.messagebird.com/en/user/index
|
||||
#
|
||||
|
||||
import re
|
||||
import requests
|
||||
|
||||
from .NotifyBase import NotifyBase
|
||||
from ..common import NotifyType
|
||||
from ..utils import parse_list
|
||||
from ..utils import validate_regex
|
||||
from ..AppriseLocale import gettext_lazy as _
|
||||
|
||||
# Some Phone Number Detection
|
||||
IS_PHONE_NO = re.compile(r'^\+?(?P<phone>[0-9\s)(+-]+)\s*$')
|
||||
|
||||
|
||||
class NotifyMessageBird(NotifyBase):
|
||||
"""
|
||||
A wrapper for MessageBird Notifications
|
||||
"""
|
||||
|
||||
# The default descriptive name associated with the Notification
|
||||
service_name = 'MessageBird'
|
||||
|
||||
# The services URL
|
||||
service_url = 'https://messagebird.com'
|
||||
|
||||
# The default protocol
|
||||
secure_protocol = 'msgbird'
|
||||
|
||||
# A URL that takes you to the setup/help of the specific protocol
|
||||
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_messagebird'
|
||||
|
||||
# MessageBird uses the http protocol with JSON requests
|
||||
notify_url = 'https://rest.messagebird.com/messages'
|
||||
|
||||
# The maximum length of the body
|
||||
body_maxlen = 140
|
||||
|
||||
# A title can not be used for SMS Messages. Setting this to zero will
|
||||
# cause any title (if defined) to get placed into the message body.
|
||||
title_maxlen = 0
|
||||
|
||||
# Define object templates
|
||||
templates = (
|
||||
'{schema}://{apikey}/{source}',
|
||||
'{schema}://{apikey}/{source}/{targets}',
|
||||
)
|
||||
|
||||
# Define our template tokens
|
||||
template_tokens = dict(NotifyBase.template_tokens, **{
|
||||
'apikey': {
|
||||
'name': _('API Key'),
|
||||
'type': 'string',
|
||||
'required': True,
|
||||
'private': True,
|
||||
'regex': (r'^[a-z0-9]{25}$', 'i'),
|
||||
},
|
||||
'source': {
|
||||
'name': _('Source Phone No'),
|
||||
'type': 'string',
|
||||
'prefix': '+',
|
||||
'required': True,
|
||||
'regex': (r'^[0-9\s)(+-]+$', 'i'),
|
||||
},
|
||||
'target_phone': {
|
||||
'name': _('Target Phone No'),
|
||||
'type': 'string',
|
||||
'prefix': '+',
|
||||
'regex': (r'^[0-9\s)(+-]+$', 'i'),
|
||||
'map_to': 'targets',
|
||||
},
|
||||
'targets': {
|
||||
'name': _('Targets'),
|
||||
'type': 'list:string',
|
||||
}
|
||||
})
|
||||
|
||||
# Define our template arguments
|
||||
template_args = dict(NotifyBase.template_args, **{
|
||||
'to': {
|
||||
'alias_of': 'targets',
|
||||
},
|
||||
'from': {
|
||||
'alias_of': 'source',
|
||||
},
|
||||
})
|
||||
|
||||
def __init__(self, apikey, source, targets=None, **kwargs):
|
||||
"""
|
||||
Initialize MessageBird Object
|
||||
"""
|
||||
super(NotifyMessageBird, self).__init__(**kwargs)
|
||||
|
||||
# API Key (associated with project)
|
||||
self.apikey = validate_regex(
|
||||
apikey, *self.template_tokens['apikey']['regex'])
|
||||
if not self.apikey:
|
||||
msg = 'An invalid MessageBird API Key ' \
|
||||
'({}) was specified.'.format(apikey)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
result = IS_PHONE_NO.match(source)
|
||||
if not result:
|
||||
msg = 'The MessageBird source specified ({}) is invalid.'\
|
||||
.format(source)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
# Further check our phone # for it's digit count
|
||||
result = ''.join(re.findall(r'\d+', result.group('phone')))
|
||||
if len(result) < 11 or len(result) > 14:
|
||||
msg = 'The MessageBird source # specified ({}) is invalid.'\
|
||||
.format(source)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
# Store our source
|
||||
self.source = result
|
||||
|
||||
# Parse our targets
|
||||
self.targets = list()
|
||||
|
||||
targets = parse_list(targets)
|
||||
if not targets:
|
||||
# No sources specified, use our own phone no
|
||||
self.targets.append(self.source)
|
||||
return
|
||||
|
||||
# otherwise, store all of our target numbers
|
||||
for target in targets:
|
||||
# Validate targets and drop bad ones:
|
||||
result = IS_PHONE_NO.match(target)
|
||||
if result:
|
||||
# Further check our phone # for it's digit count
|
||||
result = ''.join(re.findall(r'\d+', result.group('phone')))
|
||||
if len(result) < 11 or len(result) > 14:
|
||||
self.logger.warning(
|
||||
'Dropped invalid phone # '
|
||||
'({}) specified.'.format(target),
|
||||
)
|
||||
continue
|
||||
|
||||
# store valid phone number
|
||||
self.targets.append(result)
|
||||
continue
|
||||
|
||||
self.logger.warning(
|
||||
'Dropped invalid phone # '
|
||||
'({}) specified.'.format(target),
|
||||
)
|
||||
|
||||
if not self.targets:
|
||||
# We have a bot token and no target(s) to message
|
||||
msg = 'No MessageBird targets to notify.'
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
return
|
||||
|
||||
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||
"""
|
||||
Perform MessageBird Notification
|
||||
"""
|
||||
|
||||
# error tracking (used for function return)
|
||||
has_error = False
|
||||
|
||||
# Prepare our headers
|
||||
headers = {
|
||||
'User-Agent': self.app_id,
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Authorization': 'AccessKey {}'.format(self.apikey),
|
||||
}
|
||||
|
||||
# Prepare our payload
|
||||
payload = {
|
||||
'originator': '+{}'.format(self.source),
|
||||
'recipients': None,
|
||||
'body': body,
|
||||
|
||||
}
|
||||
|
||||
# Create a copy of the targets list
|
||||
targets = list(self.targets)
|
||||
|
||||
while len(targets):
|
||||
# Get our target to notify
|
||||
target = targets.pop(0)
|
||||
|
||||
# Prepare our user
|
||||
payload['recipients'] = '+{}'.format(target)
|
||||
|
||||
# Some Debug Logging
|
||||
self.logger.debug(
|
||||
'MessageBird POST URL: {} (cert_verify={})'.format(
|
||||
self.notify_url, self.verify_certificate))
|
||||
self.logger.debug('MessageBird Payload: {}' .format(payload))
|
||||
|
||||
# Always call throttle before any remote server i/o is made
|
||||
self.throttle()
|
||||
try:
|
||||
r = requests.post(
|
||||
self.notify_url,
|
||||
data=payload,
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
)
|
||||
|
||||
# Sample output of a successful transmission
|
||||
# {
|
||||
# "originator": "+15553338888",
|
||||
# "body": "test",
|
||||
# "direction": "mt",
|
||||
# "mclass": 1,
|
||||
# "reference": null,
|
||||
# "createdDatetime": "2019-08-22T01:32:18+00:00",
|
||||
# "recipients": {
|
||||
# "totalCount": 1,
|
||||
# "totalSentCount": 1,
|
||||
# "totalDeliveredCount": 0,
|
||||
# "totalDeliveryFailedCount": 0,
|
||||
# "items": [
|
||||
# {
|
||||
# "status": "sent",
|
||||
# "statusDatetime": "2019-08-22T01:32:18+00:00",
|
||||
# "recipient": 15553338888,
|
||||
# "messagePartCount": 1
|
||||
# }
|
||||
# ]
|
||||
# },
|
||||
# "validity": null,
|
||||
# "gateway": 10,
|
||||
# "typeDetails": {},
|
||||
# "href": "https://rest.messagebird.com/messages/\
|
||||
# b5d424244a5b4fd0b5b5728bccaafc23",
|
||||
# "datacoding": "plain",
|
||||
# "scheduledDatetime": null,
|
||||
# "type": "sms",
|
||||
# "id": "b5d424244a5b4fd0b5b5728bccaafc23"
|
||||
# }
|
||||
|
||||
if r.status_code not in (
|
||||
requests.codes.ok, requests.codes.created):
|
||||
# We had a problem
|
||||
status_str = \
|
||||
NotifyMessageBird.http_response_code_lookup(
|
||||
r.status_code)
|
||||
|
||||
self.logger.warning(
|
||||
'Failed to send MessageBird notification to {}: '
|
||||
'{}{}error={}.'.format(
|
||||
','.join(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 MessageBird notification to {}.'.format(target))
|
||||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured sending MessageBird:%s ' % (
|
||||
target) + 'notification.'
|
||||
)
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
||||
# Mark our failure
|
||||
has_error = True
|
||||
continue
|
||||
|
||||
return not has_error
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
}
|
||||
|
||||
return '{schema}://{apikey}/{source}/{targets}/?{args}'.format(
|
||||
schema=self.secure_protocol,
|
||||
apikey=self.pprint(self.apikey, privacy, safe=''),
|
||||
source=self.source,
|
||||
targets='/'.join(
|
||||
[NotifyMessageBird.quote(x, safe='') for x in self.targets]),
|
||||
args=NotifyMessageBird.urlencode(args))
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
|
||||
"""
|
||||
|
||||
results = NotifyBase.parse_url(url)
|
||||
|
||||
if not results:
|
||||
# We're done early as we couldn't load the results
|
||||
return results
|
||||
|
||||
# Get our entries; split_path() looks after unquoting content for us
|
||||
# by default
|
||||
results['targets'] = NotifyMessageBird.split_path(results['fullpath'])
|
||||
|
||||
try:
|
||||
# The first path entry is the source/originator
|
||||
results['source'] = results['targets'].pop(0)
|
||||
except IndexError:
|
||||
# No path specified... this URL is potentially un-parseable; we can
|
||||
# hope for a from= entry
|
||||
pass
|
||||
|
||||
# The hostname is our authentication key
|
||||
results['apikey'] = NotifyMessageBird.unquote(results['host'])
|
||||
|
||||
# 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'] += \
|
||||
NotifyMessageBird.parse_list(results['qsd']['to'])
|
||||
|
||||
if 'from' in results['qsd'] and len(results['qsd']['from']):
|
||||
results['source'] = \
|
||||
NotifyMessageBird.unquote(results['qsd']['from'])
|
||||
|
||||
return results
|
@ -0,0 +1,380 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com>
|
||||
# All rights reserved.
|
||||
#
|
||||
# This code is licensed under the MIT License.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files(the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions :
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CON
|
||||
|
||||
# 1. Simply visit https://notica.us
|
||||
# 2. You'll be provided a new variation of the website which will look
|
||||
# something like: https://notica.us/?abc123.
|
||||
# ^
|
||||
# |
|
||||
# token
|
||||
#
|
||||
# Your token is actually abc123 (do not include/grab the question mark)
|
||||
# You can use that URL as is directly in Apprise, or you can follow
|
||||
# the next step which shows you how to assemble the Apprise URL:
|
||||
#
|
||||
# 3. With respect to the above, your apprise URL would be:
|
||||
# notica://abc123
|
||||
#
|
||||
import re
|
||||
import six
|
||||
import requests
|
||||
|
||||
from .NotifyBase import NotifyBase
|
||||
from ..URLBase import PrivacyMode
|
||||
from ..common import NotifyType
|
||||
from ..utils import validate_regex
|
||||
from ..AppriseLocale import gettext_lazy as _
|
||||
|
||||
|
||||
class NoticaMode(object):
|
||||
"""
|
||||
Tracks if we're accessing the notica upstream server or a locally hosted
|
||||
one.
|
||||
"""
|
||||
# We're dealing with a self hosted service
|
||||
SELFHOSTED = 'selfhosted'
|
||||
|
||||
# We're dealing with the official hosted service at https://notica.us
|
||||
OFFICIAL = 'official'
|
||||
|
||||
|
||||
# Define our Notica Modes
|
||||
NOTICA_MODES = (
|
||||
NoticaMode.SELFHOSTED,
|
||||
NoticaMode.OFFICIAL,
|
||||
)
|
||||
|
||||
|
||||
class NotifyNotica(NotifyBase):
|
||||
"""
|
||||
A wrapper for Notica Notifications
|
||||
"""
|
||||
|
||||
# The default descriptive name associated with the Notification
|
||||
service_name = 'Notica'
|
||||
|
||||
# The services URL
|
||||
service_url = 'https://notica.us/'
|
||||
|
||||
# Insecure protocol (for those self hosted requests)
|
||||
protocol = 'notica'
|
||||
|
||||
# The default protocol (this is secure for notica)
|
||||
secure_protocol = 'noticas'
|
||||
|
||||
# A URL that takes you to the setup/help of the specific protocol
|
||||
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_notica'
|
||||
|
||||
# Notica URL
|
||||
notify_url = 'https://notica.us/?{token}'
|
||||
|
||||
# Notica does not support a title
|
||||
title_maxlen = 0
|
||||
|
||||
# Define object templates
|
||||
templates = (
|
||||
'{schema}://{token}',
|
||||
|
||||
# Self-hosted notica servers
|
||||
'{schema}://{host}/{token}',
|
||||
'{schema}://{host}:{port}/{token}',
|
||||
'{schema}://{user}@{host}/{token}',
|
||||
'{schema}://{user}@{host}:{port}/{token}',
|
||||
'{schema}://{user}:{password}@{host}/{token}',
|
||||
'{schema}://{user}:{password}@{host}:{port}/{token}',
|
||||
)
|
||||
|
||||
# Define our template tokens
|
||||
template_tokens = dict(NotifyBase.template_tokens, **{
|
||||
'token': {
|
||||
'name': _('Token'),
|
||||
'type': 'string',
|
||||
'private': True,
|
||||
'required': True,
|
||||
'regex': r'^\?*(?P<token>[^/]+)\s*$'
|
||||
},
|
||||
'host': {
|
||||
'name': _('Hostname'),
|
||||
'type': 'string',
|
||||
},
|
||||
'port': {
|
||||
'name': _('Port'),
|
||||
'type': 'int',
|
||||
'min': 1,
|
||||
'max': 65535,
|
||||
},
|
||||
'user': {
|
||||
'name': _('Username'),
|
||||
'type': 'string',
|
||||
},
|
||||
'password': {
|
||||
'name': _('Password'),
|
||||
'type': 'string',
|
||||
'private': True,
|
||||
},
|
||||
})
|
||||
|
||||
# Define any kwargs we're using
|
||||
template_kwargs = {
|
||||
'headers': {
|
||||
'name': _('HTTP Header'),
|
||||
'prefix': '+',
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, token, headers=None, **kwargs):
|
||||
"""
|
||||
Initialize Notica Object
|
||||
"""
|
||||
super(NotifyNotica, self).__init__(**kwargs)
|
||||
|
||||
# Token (associated with project)
|
||||
self.token = validate_regex(token)
|
||||
if not self.token:
|
||||
msg = 'An invalid Notica Token ' \
|
||||
'({}) was specified.'.format(token)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
# Setup our mode
|
||||
self.mode = NoticaMode.SELFHOSTED if self.host else NoticaMode.OFFICIAL
|
||||
|
||||
# prepare our fullpath
|
||||
self.fullpath = kwargs.get('fullpath')
|
||||
if not isinstance(self.fullpath, six.string_types):
|
||||
self.fullpath = '/'
|
||||
|
||||
self.headers = {}
|
||||
if headers:
|
||||
# Store our extra headers
|
||||
self.headers.update(headers)
|
||||
|
||||
return
|
||||
|
||||
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||
"""
|
||||
Perform Notica Notification
|
||||
"""
|
||||
|
||||
headers = {
|
||||
'User-Agent': self.app_id,
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
|
||||
# Prepare our payload
|
||||
payload = 'd:{}'.format(body)
|
||||
|
||||
# Auth is used for SELFHOSTED queries
|
||||
auth = None
|
||||
|
||||
if self.mode is NoticaMode.OFFICIAL:
|
||||
# prepare our notify url
|
||||
notify_url = self.notify_url.format(token=self.token)
|
||||
|
||||
else:
|
||||
# Prepare our self hosted URL
|
||||
|
||||
# Apply any/all header over-rides defined
|
||||
headers.update(self.headers)
|
||||
|
||||
if self.user:
|
||||
auth = (self.user, self.password)
|
||||
|
||||
# Set our schema
|
||||
schema = 'https' if self.secure else 'http'
|
||||
|
||||
# Prepare our notify_url
|
||||
notify_url = '%s://%s' % (schema, self.host)
|
||||
if isinstance(self.port, int):
|
||||
notify_url += ':%d' % self.port
|
||||
|
||||
notify_url += '{fullpath}?token={token}'.format(
|
||||
fullpath=self.fullpath,
|
||||
token=self.token)
|
||||
|
||||
self.logger.debug('Notica POST URL: %s (cert_verify=%r)' % (
|
||||
notify_url, self.verify_certificate,
|
||||
))
|
||||
self.logger.debug('Notica Payload: %s' % str(payload))
|
||||
|
||||
# Always call throttle before any remote server i/o is made
|
||||
self.throttle()
|
||||
|
||||
try:
|
||||
r = requests.post(
|
||||
notify_url.format(token=self.token),
|
||||
data=payload,
|
||||
headers=headers,
|
||||
auth=auth,
|
||||
verify=self.verify_certificate,
|
||||
)
|
||||
if r.status_code != requests.codes.ok:
|
||||
# We had a problem
|
||||
status_str = \
|
||||
NotifyNotica.http_response_code_lookup(r.status_code)
|
||||
|
||||
self.logger.warning(
|
||||
'Failed to send Notica 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 Notica notification.')
|
||||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured sending Notica notification.',
|
||||
)
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
||||
# Return; we're done
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
}
|
||||
|
||||
if self.mode == NoticaMode.OFFICIAL:
|
||||
# Official URLs are easy to assemble
|
||||
return '{schema}://{token}/?{args}'.format(
|
||||
schema=self.protocol,
|
||||
token=self.pprint(self.token, privacy, safe=''),
|
||||
args=NotifyNotica.urlencode(args),
|
||||
)
|
||||
|
||||
# If we reach here then we are assembling a self hosted URL
|
||||
|
||||
# Append our headers into our args
|
||||
args.update({'+{}'.format(k): v for k, v in self.headers.items()})
|
||||
|
||||
# Authorization can be used for self-hosted sollutions
|
||||
auth = ''
|
||||
|
||||
# Determine Authentication
|
||||
if self.user and self.password:
|
||||
auth = '{user}:{password}@'.format(
|
||||
user=NotifyNotica.quote(self.user, safe=''),
|
||||
password=self.pprint(
|
||||
self.password, privacy, mode=PrivacyMode.Secret, safe=''),
|
||||
)
|
||||
elif self.user:
|
||||
auth = '{user}@'.format(
|
||||
user=NotifyNotica.quote(self.user, safe=''),
|
||||
)
|
||||
|
||||
default_port = 443 if self.secure else 80
|
||||
|
||||
return '{schema}://{auth}{hostname}{port}{fullpath}{token}/?{args}' \
|
||||
.format(
|
||||
schema=self.secure_protocol
|
||||
if self.secure else self.protocol,
|
||||
auth=auth,
|
||||
hostname=NotifyNotica.quote(self.host, safe=''),
|
||||
port='' if self.port is None or self.port == default_port
|
||||
else ':{}'.format(self.port),
|
||||
fullpath=NotifyNotica.quote(
|
||||
self.fullpath, safe='/'),
|
||||
token=self.pprint(self.token, privacy, safe=''),
|
||||
args=NotifyNotica.urlencode(args),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
|
||||
"""
|
||||
results = NotifyBase.parse_url(url, verify_host=False)
|
||||
if not results:
|
||||
# We're done early as we couldn't load the results
|
||||
return results
|
||||
|
||||
# Get unquoted entries
|
||||
entries = NotifyNotica.split_path(results['fullpath'])
|
||||
if not entries:
|
||||
# If there are no path entries, then we're only dealing with the
|
||||
# official website
|
||||
results['mode'] = NoticaMode.OFFICIAL
|
||||
|
||||
# Store our token using the host
|
||||
results['token'] = NotifyNotica.unquote(results['host'])
|
||||
|
||||
# Unset our host
|
||||
results['host'] = None
|
||||
|
||||
else:
|
||||
# Otherwise we're running a self hosted instance
|
||||
results['mode'] = NoticaMode.SELFHOSTED
|
||||
|
||||
# The last element in the list is our token
|
||||
results['token'] = entries.pop()
|
||||
|
||||
# Re-assemble our full path
|
||||
results['fullpath'] = \
|
||||
'/' if not entries else '/{}/'.format('/'.join(entries))
|
||||
|
||||
# Add our headers that the user can potentially over-ride if they
|
||||
# wish to to our returned result set
|
||||
results['headers'] = results['qsd-']
|
||||
results['headers'].update(results['qsd+'])
|
||||
|
||||
return results
|
||||
|
||||
@staticmethod
|
||||
def parse_native_url(url):
|
||||
"""
|
||||
Support https://notica.us/?abc123
|
||||
"""
|
||||
|
||||
result = re.match(
|
||||
r'^https?://notica\.us/?'
|
||||
r'\??(?P<token>[^&]+)([&\s]*(?P<args>.+))?$', url, re.I)
|
||||
|
||||
if result:
|
||||
return NotifyNotica.parse_url(
|
||||
'{schema}://{token}/{args}'.format(
|
||||
schema=NotifyNotica.protocol,
|
||||
token=result.group('token'),
|
||||
args='' if not result.group('args')
|
||||
else '?{}'.format(result.group('args'))))
|
||||
|
||||
return None
|
@ -0,0 +1,378 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com>
|
||||
# All rights reserved.
|
||||
#
|
||||
# This code is licensed under the MIT License.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files(the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions :
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
# Notifico allows you to relay notifications into IRC channels.
|
||||
#
|
||||
# 1. visit https://n.tkte.ch and sign up for an account
|
||||
# 2. create a project; either manually or sync with github
|
||||
# 3. from within the project, you can create a message hook
|
||||
#
|
||||
# the URL will look something like this:
|
||||
# https://n.tkte.ch/h/2144/uJmKaBW9WFk42miB146ci3Kj
|
||||
# ^ ^
|
||||
# | |
|
||||
# project id message hook
|
||||
#
|
||||
# This plugin also supports taking the URL (as identified above) directly
|
||||
# as well.
|
||||
|
||||
import re
|
||||
import requests
|
||||
|
||||
from .NotifyBase import NotifyBase
|
||||
from ..common import NotifyType
|
||||
from ..utils import parse_bool
|
||||
from ..utils import validate_regex
|
||||
from ..AppriseLocale import gettext_lazy as _
|
||||
|
||||
|
||||
class NotificoFormat(object):
|
||||
# Resets all formating
|
||||
Reset = '\x0F'
|
||||
|
||||
# Formatting
|
||||
Bold = '\x02'
|
||||
Italic = '\x1D'
|
||||
Underline = '\x1F'
|
||||
BGSwap = '\x16'
|
||||
|
||||
|
||||
class NotificoColor(object):
|
||||
# Resets Color
|
||||
Reset = '\x03'
|
||||
|
||||
# Colors
|
||||
White = '\x0300'
|
||||
Black = '\x0301'
|
||||
Blue = '\x0302'
|
||||
Green = '\x0303'
|
||||
Red = '\x0304'
|
||||
Brown = '\x0305'
|
||||
Purple = '\x0306'
|
||||
Orange = '\x0307'
|
||||
Yellow = '\x0308',
|
||||
LightGreen = '\x0309'
|
||||
Teal = '\x0310'
|
||||
LightCyan = '\x0311'
|
||||
LightBlue = '\x0312'
|
||||
Violet = '\x0313'
|
||||
Grey = '\x0314'
|
||||
LightGrey = '\x0315'
|
||||
|
||||
|
||||
class NotifyNotifico(NotifyBase):
|
||||
"""
|
||||
A wrapper for Notifico Notifications
|
||||
"""
|
||||
|
||||
# The default descriptive name associated with the Notification
|
||||
service_name = 'Notifico'
|
||||
|
||||
# The services URL
|
||||
service_url = 'https://n.tkte.ch'
|
||||
|
||||
# The default protocol
|
||||
protocol = 'notifico'
|
||||
|
||||
# The default secure protocol
|
||||
secure_protocol = 'notifico'
|
||||
|
||||
# A URL that takes you to the setup/help of the specific protocol
|
||||
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_notifico'
|
||||
|
||||
# Plain Text Notification URL
|
||||
notify_url = 'https://n.tkte.ch/h/{proj}/{hook}'
|
||||
|
||||
# The title is not used
|
||||
title_maxlen = 0
|
||||
|
||||
# The maximum allowable characters allowed in the body per message
|
||||
body_maxlen = 512
|
||||
|
||||
# Define object templates
|
||||
templates = (
|
||||
'{schema}://{project_id}/{msghook}',
|
||||
)
|
||||
|
||||
# Define our template arguments
|
||||
template_tokens = dict(NotifyBase.template_tokens, **{
|
||||
# The Project ID is found as the first part of the URL
|
||||
# /1234/........................
|
||||
'project_id': {
|
||||
'name': _('Project ID'),
|
||||
'type': 'string',
|
||||
'required': True,
|
||||
'private': True,
|
||||
'regex': (r'^[0-9]+$', ''),
|
||||
},
|
||||
# The Message Hook follows the Project ID
|
||||
# /..../AbCdEfGhIjKlMnOpQrStUvWX
|
||||
'msghook': {
|
||||
'name': _('Message Hook'),
|
||||
'type': 'string',
|
||||
'required': True,
|
||||
'private': True,
|
||||
'regex': (r'^[a-z0-9]+$', 'i'),
|
||||
},
|
||||
})
|
||||
|
||||
# Define our template arguments
|
||||
template_args = dict(NotifyBase.template_args, **{
|
||||
# You can optionally pass IRC colors into
|
||||
'color': {
|
||||
'name': _('IRC Colors'),
|
||||
'type': 'bool',
|
||||
'default': True,
|
||||
},
|
||||
|
||||
# You can optionally pass IRC color into
|
||||
'prefix': {
|
||||
'name': _('Prefix'),
|
||||
'type': 'bool',
|
||||
'default': True,
|
||||
},
|
||||
})
|
||||
|
||||
def __init__(self, project_id, msghook, color=True, prefix=True,
|
||||
**kwargs):
|
||||
"""
|
||||
Initialize Notifico Object
|
||||
"""
|
||||
super(NotifyNotifico, self).__init__(**kwargs)
|
||||
|
||||
# Assign our message hook
|
||||
self.project_id = validate_regex(
|
||||
project_id, *self.template_tokens['project_id']['regex'])
|
||||
if not self.project_id:
|
||||
msg = 'An invalid Notifico Project ID ' \
|
||||
'({}) was specified.'.format(project_id)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
# Assign our message hook
|
||||
self.msghook = validate_regex(
|
||||
msghook, *self.template_tokens['msghook']['regex'])
|
||||
if not self.msghook:
|
||||
msg = 'An invalid Notifico Message Token ' \
|
||||
'({}) was specified.'.format(msghook)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
# Prefix messages with a [?] where ? identifies the message type
|
||||
# such as if it's an error, warning, info, or success
|
||||
self.prefix = prefix
|
||||
|
||||
# Send colors
|
||||
self.color = color
|
||||
|
||||
# Prepare our notification URL now:
|
||||
self.api_url = self.notify_url.format(
|
||||
proj=self.project_id,
|
||||
hook=self.msghook,
|
||||
)
|
||||
return
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
'color': 'yes' if self.color else 'no',
|
||||
'prefix': 'yes' if self.prefix else 'no',
|
||||
}
|
||||
|
||||
return '{schema}://{proj}/{hook}/?{args}'.format(
|
||||
schema=self.secure_protocol,
|
||||
proj=self.pprint(self.project_id, privacy, safe=''),
|
||||
hook=self.pprint(self.msghook, privacy, safe=''),
|
||||
args=NotifyNotifico.urlencode(args),
|
||||
)
|
||||
|
||||
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||
"""
|
||||
wrapper to _send since we can alert more then one channel
|
||||
"""
|
||||
|
||||
# prepare our headers
|
||||
headers = {
|
||||
'User-Agent': self.app_id,
|
||||
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
|
||||
}
|
||||
|
||||
# Prepare our IRC Prefix
|
||||
color = ''
|
||||
token = ''
|
||||
if notify_type == NotifyType.INFO:
|
||||
color = NotificoColor.Teal
|
||||
token = 'i'
|
||||
|
||||
elif notify_type == NotifyType.SUCCESS:
|
||||
color = NotificoColor.LightGreen
|
||||
token = '✔'
|
||||
|
||||
elif notify_type == NotifyType.WARNING:
|
||||
color = NotificoColor.Orange
|
||||
token = '!'
|
||||
|
||||
elif notify_type == NotifyType.FAILURE:
|
||||
color = NotificoColor.Red
|
||||
token = '✗'
|
||||
|
||||
if self.color:
|
||||
# Colors were specified, make sure we capture and correctly
|
||||
# allow them to exist inline in the message
|
||||
# \g<1> is less ambigious than \1
|
||||
body = re.sub(r'\\x03(\d{0,2})', '\x03\g<1>', body)
|
||||
|
||||
else:
|
||||
# no colors specified, make sure we strip out any colors found
|
||||
# to make the string read-able
|
||||
body = re.sub(r'\\x03(\d{1,2}(,[0-9]{1,2})?)?', '', body)
|
||||
|
||||
# Prepare our payload
|
||||
payload = {
|
||||
'payload': body if not self.prefix
|
||||
else '{}[{}]{} {}{}{}: {}{}'.format(
|
||||
# Token [?] at the head
|
||||
color if self.color else '',
|
||||
token,
|
||||
NotificoColor.Reset if self.color else '',
|
||||
# App ID
|
||||
NotificoFormat.Bold if self.color else '',
|
||||
self.app_id,
|
||||
NotificoFormat.Reset if self.color else '',
|
||||
# Message Body
|
||||
body,
|
||||
# Reset
|
||||
NotificoFormat.Reset if self.color else '',
|
||||
),
|
||||
}
|
||||
|
||||
self.logger.debug('Notifico GET URL: %s (cert_verify=%r)' % (
|
||||
self.api_url, self.verify_certificate))
|
||||
self.logger.debug('Notifico Payload: %s' % str(payload))
|
||||
|
||||
# Always call throttle before any remote server i/o is made
|
||||
self.throttle()
|
||||
|
||||
try:
|
||||
r = requests.get(
|
||||
self.api_url,
|
||||
params=payload,
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
)
|
||||
if r.status_code != requests.codes.ok:
|
||||
# We had a problem
|
||||
status_str = \
|
||||
NotifyNotifico.http_response_code_lookup(r.status_code)
|
||||
|
||||
self.logger.warning(
|
||||
'Failed to send Notifico 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 Notifico notification.')
|
||||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured sending Notifico '
|
||||
'notification.')
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
||||
# Return; we're done
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
|
||||
"""
|
||||
|
||||
results = NotifyBase.parse_url(url)
|
||||
if not results:
|
||||
# We're done early as we couldn't load the results
|
||||
return results
|
||||
|
||||
# The first token is stored in the hostname
|
||||
results['project_id'] = NotifyNotifico.unquote(results['host'])
|
||||
|
||||
# Get Message Hook
|
||||
try:
|
||||
results['msghook'] = NotifyNotifico.split_path(
|
||||
results['fullpath'])[0]
|
||||
|
||||
except IndexError:
|
||||
results['msghook'] = None
|
||||
|
||||
# Include Color
|
||||
results['color'] = \
|
||||
parse_bool(results['qsd'].get('color', True))
|
||||
|
||||
# Include Prefix
|
||||
results['prefix'] = \
|
||||
parse_bool(results['qsd'].get('prefix', True))
|
||||
|
||||
return results
|
||||
|
||||
@staticmethod
|
||||
def parse_native_url(url):
|
||||
"""
|
||||
Support https://n.tkte.ch/h/PROJ_ID/MESSAGE_HOOK/
|
||||
"""
|
||||
|
||||
result = re.match(
|
||||
r'^https?://n\.tkte\.ch/h/'
|
||||
r'(?P<proj>[0-9]+)/'
|
||||
r'(?P<hook>[A-Z0-9]+)/?'
|
||||
r'(?P<args>\?.+)?$', url, re.I)
|
||||
|
||||
if result:
|
||||
return NotifyNotifico.parse_url(
|
||||
'{schema}://{proj}/{hook}/{args}'.format(
|
||||
schema=NotifyNotifico.secure_protocol,
|
||||
proj=result.group('proj'),
|
||||
hook=result.group('hook'),
|
||||
args='' if not result.group('args')
|
||||
else result.group('args')))
|
||||
|
||||
return None
|
@ -0,0 +1,297 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com>
|
||||
# All rights reserved.
|
||||
#
|
||||
# This code is licensed under the MIT License.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files(the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions :
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
import requests
|
||||
from json import dumps
|
||||
|
||||
from .NotifyBase import NotifyBase
|
||||
from ..URLBase import PrivacyMode
|
||||
from ..common import NotifyType
|
||||
from ..utils import validate_regex
|
||||
from ..AppriseLocale import gettext_lazy as _
|
||||
|
||||
|
||||
class NotifyPushjet(NotifyBase):
|
||||
"""
|
||||
A wrapper for Pushjet Notifications
|
||||
"""
|
||||
|
||||
# The default descriptive name associated with the Notification
|
||||
service_name = 'Pushjet'
|
||||
|
||||
# The default protocol
|
||||
protocol = 'pjet'
|
||||
|
||||
# The default secure protocol
|
||||
secure_protocol = 'pjets'
|
||||
|
||||
# A URL that takes you to the setup/help of the specific protocol
|
||||
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_pushjet'
|
||||
|
||||
# Disable throttle rate for Pushjet requests since they are normally
|
||||
# local anyway (the remote/online service is no more)
|
||||
request_rate_per_sec = 0
|
||||
|
||||
# Define object templates
|
||||
templates = (
|
||||
'{schema}://{host}:{port}/{secret_key}',
|
||||
'{schema}://{host}/{secret_key}',
|
||||
'{schema}://{user}:{password}@{host}:{port}/{secret_key}',
|
||||
'{schema}://{user}:{password}@{host}/{secret_key}',
|
||||
|
||||
# Kept for backwards compatibility; will be depricated eventually
|
||||
'{schema}://{secret_key}@{host}',
|
||||
'{schema}://{secret_key}@{host}:{port}',
|
||||
)
|
||||
|
||||
# Define our tokens
|
||||
template_tokens = dict(NotifyBase.template_tokens, **{
|
||||
'host': {
|
||||
'name': _('Hostname'),
|
||||
'type': 'string',
|
||||
'required': True,
|
||||
},
|
||||
'port': {
|
||||
'name': _('Port'),
|
||||
'type': 'int',
|
||||
'min': 1,
|
||||
'max': 65535,
|
||||
},
|
||||
'secret_key': {
|
||||
'name': _('Secret Key'),
|
||||
'type': 'string',
|
||||
'required': True,
|
||||
'private': True,
|
||||
},
|
||||
'user': {
|
||||
'name': _('Username'),
|
||||
'type': 'string',
|
||||
},
|
||||
'password': {
|
||||
'name': _('Password'),
|
||||
'type': 'string',
|
||||
'private': True,
|
||||
},
|
||||
})
|
||||
|
||||
template_args = dict(NotifyBase.template_args, **{
|
||||
'secret': {
|
||||
'alias_of': 'secret_key',
|
||||
},
|
||||
})
|
||||
|
||||
def __init__(self, secret_key, **kwargs):
|
||||
"""
|
||||
Initialize Pushjet Object
|
||||
"""
|
||||
super(NotifyPushjet, self).__init__(**kwargs)
|
||||
|
||||
# Secret Key (associated with project)
|
||||
self.secret_key = validate_regex(secret_key)
|
||||
if not self.secret_key:
|
||||
msg = 'An invalid Pushjet Secret Key ' \
|
||||
'({}) was specified.'.format(secret_key)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
return
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
}
|
||||
|
||||
default_port = 443 if self.secure else 80
|
||||
|
||||
# Determine Authentication
|
||||
auth = ''
|
||||
if self.user and self.password:
|
||||
auth = '{user}:{password}@'.format(
|
||||
user=NotifyPushjet.quote(self.user, safe=''),
|
||||
password=self.pprint(
|
||||
self.password, privacy, mode=PrivacyMode.Secret, safe=''),
|
||||
)
|
||||
|
||||
return '{schema}://{auth}{hostname}{port}/{secret}/?{args}'.format(
|
||||
schema=self.secure_protocol if self.secure else self.protocol,
|
||||
auth=auth,
|
||||
hostname=NotifyPushjet.quote(self.host, safe=''),
|
||||
port='' if self.port is None or self.port == default_port
|
||||
else ':{}'.format(self.port),
|
||||
secret=self.pprint(
|
||||
self.secret_key, privacy, mode=PrivacyMode.Secret, safe=''),
|
||||
args=NotifyPushjet.urlencode(args),
|
||||
)
|
||||
|
||||
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||
"""
|
||||
Perform Pushjet Notification
|
||||
"""
|
||||
|
||||
params = {
|
||||
'secret': self.secret_key,
|
||||
}
|
||||
|
||||
# prepare Pushjet Object
|
||||
payload = {
|
||||
'message': body,
|
||||
'title': title,
|
||||
'link': None,
|
||||
'level': None,
|
||||
}
|
||||
|
||||
headers = {
|
||||
'User-Agent': self.app_id,
|
||||
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
|
||||
}
|
||||
|
||||
auth = None
|
||||
if self.user:
|
||||
auth = (self.user, self.password)
|
||||
|
||||
notify_url = '{schema}://{host}{port}/message/'.format(
|
||||
schema="https" if self.secure else "http",
|
||||
host=self.host,
|
||||
port=':{}'.format(self.port) if self.port else '')
|
||||
|
||||
self.logger.debug('Pushjet POST URL: %s (cert_verify=%r)' % (
|
||||
notify_url, self.verify_certificate,
|
||||
))
|
||||
self.logger.debug('Pushjet Payload: %s' % str(payload))
|
||||
|
||||
# Always call throttle before any remote server i/o is made
|
||||
self.throttle()
|
||||
|
||||
try:
|
||||
r = requests.post(
|
||||
notify_url,
|
||||
params=params,
|
||||
data=dumps(payload),
|
||||
headers=headers,
|
||||
auth=auth,
|
||||
verify=self.verify_certificate,
|
||||
)
|
||||
if r.status_code != requests.codes.ok:
|
||||
# We had a problem
|
||||
status_str = \
|
||||
NotifyPushjet.http_response_code_lookup(r.status_code)
|
||||
|
||||
self.logger.warning(
|
||||
'Failed to send Pushjet 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 Pushjet notification.')
|
||||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured sending Pushjet '
|
||||
'notification to %s.' % self.host)
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
||||
# Return; we're done
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
|
||||
Syntax:
|
||||
pjet://hostname/secret_key
|
||||
pjet://hostname:port/secret_key
|
||||
pjet://user:pass@hostname/secret_key
|
||||
pjet://user:pass@hostname:port/secret_key
|
||||
pjets://hostname/secret_key
|
||||
pjets://hostname:port/secret_key
|
||||
pjets://user:pass@hostname/secret_key
|
||||
pjets://user:pass@hostname:port/secret_key
|
||||
|
||||
Legacy (Depricated) Syntax:
|
||||
pjet://secret_key@hostname
|
||||
pjet://secret_key@hostname:port
|
||||
pjets://secret_key@hostname
|
||||
pjets://secret_key@hostname:port
|
||||
|
||||
"""
|
||||
results = NotifyBase.parse_url(url)
|
||||
|
||||
if not results:
|
||||
# We're done early as we couldn't load the results
|
||||
return results
|
||||
|
||||
try:
|
||||
# Retrieve our secret_key from the first entry in the url path
|
||||
results['secret_key'] = \
|
||||
NotifyPushjet.split_path(results['fullpath'])[0]
|
||||
|
||||
except IndexError:
|
||||
# no secret key specified
|
||||
results['secret_key'] = None
|
||||
|
||||
# Allow over-riding the secret by specifying it as an argument
|
||||
# this allows people who have http-auth infront to login
|
||||
# through it in addition to supporting the secret key
|
||||
if 'secret' in results['qsd'] and len(results['qsd']['secret']):
|
||||
results['secret_key'] = \
|
||||
NotifyPushjet.unquote(results['qsd']['secret'])
|
||||
|
||||
if results.get('secret_key') is None:
|
||||
# Deprication Notice issued for v0.7.9
|
||||
NotifyPushjet.logger.deprecate(
|
||||
'The Pushjet URL contains secret_key in the user field'
|
||||
' which will be deprecated in an upcoming '
|
||||
'release. Please place this in the path of the URL instead.'
|
||||
)
|
||||
|
||||
# Store it as it's value based on the user field
|
||||
results['secret_key'] = \
|
||||
NotifyPushjet.unquote(results.get('user'))
|
||||
|
||||
# there is no way http-auth is enabled, be sure to unset the
|
||||
# current defined user (if present). This is done due to some
|
||||
# logic that takes place in the send() since we support http-auth.
|
||||
results['user'] = None
|
||||
results['password'] = None
|
||||
|
||||
return results
|
@ -0,0 +1,336 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com>
|
||||
# All rights reserved.
|
||||
#
|
||||
# This code is licensed under the MIT License.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files(the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions :
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
from os import urandom
|
||||
from json import loads
|
||||
import requests
|
||||
|
||||
from .NotifyBase import NotifyBase
|
||||
from ..URLBase import PrivacyMode
|
||||
from ..common import NotifyType
|
||||
from ..utils import validate_regex
|
||||
from ..AppriseLocale import gettext_lazy as _
|
||||
|
||||
# Default our global support flag
|
||||
CRYPTOGRAPHY_AVAILABLE = False
|
||||
|
||||
try:
|
||||
from cryptography.hazmat.primitives import padding
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher
|
||||
from cryptography.hazmat.primitives.ciphers import algorithms
|
||||
from cryptography.hazmat.primitives.ciphers import modes
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from base64 import urlsafe_b64encode
|
||||
import hashlib
|
||||
|
||||
CRYPTOGRAPHY_AVAILABLE = True
|
||||
|
||||
except ImportError:
|
||||
# no problem; this just means the added encryption functionality isn't
|
||||
# available. You can still send a SimplePush message
|
||||
pass
|
||||
|
||||
|
||||
class NotifySimplePush(NotifyBase):
|
||||
"""
|
||||
A wrapper for SimplePush Notifications
|
||||
"""
|
||||
|
||||
# The default descriptive name associated with the Notification
|
||||
service_name = 'SimplePush'
|
||||
|
||||
# The services URL
|
||||
service_url = 'https://simplepush.io/'
|
||||
|
||||
# The default secure protocol
|
||||
secure_protocol = 'spush'
|
||||
|
||||
# A URL that takes you to the setup/help of the specific protocol
|
||||
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_simplepush'
|
||||
|
||||
# SimplePush uses the http protocol with SimplePush requests
|
||||
notify_url = 'https://api.simplepush.io/send'
|
||||
|
||||
# The maximum allowable characters allowed in the body per message
|
||||
body_maxlen = 10000
|
||||
|
||||
# Defines the maximum allowable characters in the title
|
||||
title_maxlen = 1024
|
||||
|
||||
# Define object templates
|
||||
templates = (
|
||||
'{schema}://{apikey}',
|
||||
'{schema}://{salt}:{password}@{apikey}',
|
||||
)
|
||||
|
||||
# Define our template tokens
|
||||
template_tokens = dict(NotifyBase.template_tokens, **{
|
||||
'apikey': {
|
||||
'name': _('API Key'),
|
||||
'type': 'string',
|
||||
'private': True,
|
||||
'required': True,
|
||||
},
|
||||
|
||||
# Used for encrypted logins
|
||||
'password': {
|
||||
'name': _('Encrypted Password'),
|
||||
'type': 'string',
|
||||
'private': True,
|
||||
},
|
||||
'salt': {
|
||||
'name': _('Encrypted Salt'),
|
||||
'type': 'string',
|
||||
'private': True,
|
||||
'map_to': 'user',
|
||||
},
|
||||
})
|
||||
|
||||
# Define our template arguments
|
||||
template_args = dict(NotifyBase.template_args, **{
|
||||
'event': {
|
||||
'name': _('Event'),
|
||||
'type': 'string',
|
||||
},
|
||||
})
|
||||
|
||||
def __init__(self, apikey, event=None, **kwargs):
|
||||
"""
|
||||
Initialize SimplePush Object
|
||||
"""
|
||||
super(NotifySimplePush, self).__init__(**kwargs)
|
||||
|
||||
# API Key (associated with project)
|
||||
self.apikey = validate_regex(apikey)
|
||||
if not self.apikey:
|
||||
msg = 'An invalid SimplePush API Key ' \
|
||||
'({}) was specified.'.format(apikey)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
if event:
|
||||
# Event Name (associated with project)
|
||||
self.event = validate_regex(event)
|
||||
if not self.event:
|
||||
msg = 'An invalid SimplePush Event Name ' \
|
||||
'({}) was specified.'.format(event)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
else:
|
||||
# Default Event Name
|
||||
self.event = None
|
||||
|
||||
# Encrypt Message (providing support is available)
|
||||
if self.password and self.user and not CRYPTOGRAPHY_AVAILABLE:
|
||||
# Provide the end user at least some notification that they're
|
||||
# not getting what they asked for
|
||||
self.logger.warning(
|
||||
'SimplePush extended encryption is not supported by this '
|
||||
'system.')
|
||||
|
||||
# Used/cached in _encrypt() function
|
||||
self._iv = None
|
||||
self._iv_hex = None
|
||||
self._key = None
|
||||
|
||||
def _encrypt(self, content):
|
||||
"""
|
||||
Encrypts message for use with SimplePush
|
||||
"""
|
||||
|
||||
if self._iv is None:
|
||||
# initialization vector and cache it
|
||||
self._iv = urandom(algorithms.AES.block_size // 8)
|
||||
|
||||
# convert vector into hex string (used in payload)
|
||||
self._iv_hex = ''.join(["{:02x}".format(ord(self._iv[idx:idx + 1]))
|
||||
for idx in range(len(self._iv))]).upper()
|
||||
|
||||
# encrypted key and cache it
|
||||
self._key = bytes(bytearray.fromhex(
|
||||
hashlib.sha1('{}{}'.format(self.password, self.user)
|
||||
.encode('utf-8')).hexdigest()[0:32]))
|
||||
|
||||
padder = padding.PKCS7(algorithms.AES.block_size).padder()
|
||||
content = padder.update(content.encode()) + padder.finalize()
|
||||
|
||||
encryptor = Cipher(
|
||||
algorithms.AES(self._key),
|
||||
modes.CBC(self._iv),
|
||||
default_backend()).encryptor()
|
||||
|
||||
return urlsafe_b64encode(
|
||||
encryptor.update(content) + encryptor.finalize())
|
||||
|
||||
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||
"""
|
||||
Perform SimplePush Notification
|
||||
"""
|
||||
|
||||
headers = {
|
||||
'User-Agent': self.app_id,
|
||||
'Content-type': "application/x-www-form-urlencoded",
|
||||
}
|
||||
|
||||
# Prepare our payload
|
||||
payload = {
|
||||
'key': self.apikey,
|
||||
}
|
||||
|
||||
if self.password and self.user and CRYPTOGRAPHY_AVAILABLE:
|
||||
body = self._encrypt(body)
|
||||
title = self._encrypt(title)
|
||||
payload.update({
|
||||
'encrypted': 'true',
|
||||
'iv': self._iv_hex,
|
||||
})
|
||||
|
||||
# prepare SimplePush Object
|
||||
payload.update({
|
||||
'msg': body,
|
||||
'title': title,
|
||||
})
|
||||
|
||||
if self.event:
|
||||
# Store Event
|
||||
payload['event'] = self.event
|
||||
|
||||
self.logger.debug('SimplePush POST URL: %s (cert_verify=%r)' % (
|
||||
self.notify_url, self.verify_certificate,
|
||||
))
|
||||
self.logger.debug('SimplePush Payload: %s' % str(payload))
|
||||
|
||||
# We need to rely on the status string returned in the SimplePush
|
||||
# response
|
||||
status_str = None
|
||||
status = None
|
||||
|
||||
# Always call throttle before any remote server i/o is made
|
||||
self.throttle()
|
||||
|
||||
try:
|
||||
r = requests.post(
|
||||
self.notify_url,
|
||||
data=payload,
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
)
|
||||
|
||||
# Get our SimplePush response (if it's possible)
|
||||
try:
|
||||
json_response = loads(r.content)
|
||||
status_str = json_response.get('message')
|
||||
status = json_response.get('status')
|
||||
|
||||
except (TypeError, ValueError, AttributeError):
|
||||
# TypeError = r.content is not a String
|
||||
# ValueError = r.content is Unparsable
|
||||
# AttributeError = r.content is None
|
||||
pass
|
||||
|
||||
if r.status_code != requests.codes.ok or status != 'OK':
|
||||
# We had a problem
|
||||
status_str = status_str if status_str else\
|
||||
NotifyBase.http_response_code_lookup(r.status_code)
|
||||
|
||||
self.logger.warning(
|
||||
'Failed to send SimplePush 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 SimplePush notification.')
|
||||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured sending SimplePush notification.')
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
||||
# Return; we're done
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
}
|
||||
|
||||
if self.event:
|
||||
args['event'] = self.event
|
||||
|
||||
# Determine Authentication
|
||||
auth = ''
|
||||
if self.user and self.password:
|
||||
auth = '{salt}:{password}@'.format(
|
||||
salt=self.pprint(
|
||||
self.user, privacy, mode=PrivacyMode.Secret, safe=''),
|
||||
password=self.pprint(
|
||||
self.password, privacy, mode=PrivacyMode.Secret, safe=''),
|
||||
)
|
||||
|
||||
return '{schema}://{auth}{apikey}/?{args}'.format(
|
||||
schema=self.secure_protocol,
|
||||
auth=auth,
|
||||
apikey=self.pprint(self.apikey, privacy, safe=''),
|
||||
args=NotifySimplePush.urlencode(args),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
|
||||
"""
|
||||
results = NotifyBase.parse_url(url)
|
||||
if not results:
|
||||
# We're done early as we couldn't load the results
|
||||
return results
|
||||
|
||||
# Set the API Key
|
||||
results['apikey'] = NotifySimplePush.unquote(results['host'])
|
||||
|
||||
# Event
|
||||
if 'event' in results['qsd'] and len(results['qsd']['event']):
|
||||
# Extract the account sid from an argument
|
||||
results['event'] = \
|
||||
NotifySimplePush.unquote(results['qsd']['event'])
|
||||
|
||||
return results
|
@ -0,0 +1,293 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com>
|
||||
# All rights reserved.
|
||||
#
|
||||
# This code is licensed under the MIT License.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files(the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions :
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
import syslog
|
||||
|
||||
from .NotifyBase import NotifyBase
|
||||
from ..common import NotifyType
|
||||
from ..utils import parse_bool
|
||||
from ..AppriseLocale import gettext_lazy as _
|
||||
|
||||
|
||||
class SyslogFacility:
|
||||
"""
|
||||
All of the supported facilities
|
||||
"""
|
||||
KERN = 'kern'
|
||||
USER = 'user'
|
||||
MAIL = 'mail'
|
||||
DAEMON = 'daemon'
|
||||
AUTH = 'auth'
|
||||
SYSLOG = 'syslog'
|
||||
LPR = 'lpr'
|
||||
NEWS = 'news'
|
||||
UUCP = 'uucp'
|
||||
CRON = 'cron'
|
||||
LOCAL0 = 'local0'
|
||||
LOCAL1 = 'local1'
|
||||
LOCAL2 = 'local2'
|
||||
LOCAL3 = 'local3'
|
||||
LOCAL4 = 'local4'
|
||||
LOCAL5 = 'local5'
|
||||
LOCAL6 = 'local6'
|
||||
LOCAL7 = 'local7'
|
||||
|
||||
|
||||
SYSLOG_FACILITY_MAP = {
|
||||
SyslogFacility.KERN: syslog.LOG_KERN,
|
||||
SyslogFacility.USER: syslog.LOG_USER,
|
||||
SyslogFacility.MAIL: syslog.LOG_MAIL,
|
||||
SyslogFacility.DAEMON: syslog.LOG_DAEMON,
|
||||
SyslogFacility.AUTH: syslog.LOG_AUTH,
|
||||
SyslogFacility.SYSLOG: syslog.LOG_SYSLOG,
|
||||
SyslogFacility.LPR: syslog.LOG_LPR,
|
||||
SyslogFacility.NEWS: syslog.LOG_NEWS,
|
||||
SyslogFacility.UUCP: syslog.LOG_UUCP,
|
||||
SyslogFacility.CRON: syslog.LOG_CRON,
|
||||
SyslogFacility.LOCAL0: syslog.LOG_LOCAL0,
|
||||
SyslogFacility.LOCAL1: syslog.LOG_LOCAL1,
|
||||
SyslogFacility.LOCAL2: syslog.LOG_LOCAL2,
|
||||
SyslogFacility.LOCAL3: syslog.LOG_LOCAL3,
|
||||
SyslogFacility.LOCAL4: syslog.LOG_LOCAL4,
|
||||
SyslogFacility.LOCAL5: syslog.LOG_LOCAL5,
|
||||
SyslogFacility.LOCAL6: syslog.LOG_LOCAL6,
|
||||
SyslogFacility.LOCAL7: syslog.LOG_LOCAL7,
|
||||
}
|
||||
|
||||
SYSLOG_FACILITY_RMAP = {
|
||||
syslog.LOG_KERN: SyslogFacility.KERN,
|
||||
syslog.LOG_USER: SyslogFacility.USER,
|
||||
syslog.LOG_MAIL: SyslogFacility.MAIL,
|
||||
syslog.LOG_DAEMON: SyslogFacility.DAEMON,
|
||||
syslog.LOG_AUTH: SyslogFacility.AUTH,
|
||||
syslog.LOG_SYSLOG: SyslogFacility.SYSLOG,
|
||||
syslog.LOG_LPR: SyslogFacility.LPR,
|
||||
syslog.LOG_NEWS: SyslogFacility.NEWS,
|
||||
syslog.LOG_UUCP: SyslogFacility.UUCP,
|
||||
syslog.LOG_CRON: SyslogFacility.CRON,
|
||||
syslog.LOG_LOCAL0: SyslogFacility.LOCAL0,
|
||||
syslog.LOG_LOCAL1: SyslogFacility.LOCAL1,
|
||||
syslog.LOG_LOCAL2: SyslogFacility.LOCAL2,
|
||||
syslog.LOG_LOCAL3: SyslogFacility.LOCAL3,
|
||||
syslog.LOG_LOCAL4: SyslogFacility.LOCAL4,
|
||||
syslog.LOG_LOCAL5: SyslogFacility.LOCAL5,
|
||||
syslog.LOG_LOCAL6: SyslogFacility.LOCAL6,
|
||||
syslog.LOG_LOCAL7: SyslogFacility.LOCAL7,
|
||||
}
|
||||
|
||||
|
||||
class NotifySyslog(NotifyBase):
|
||||
"""
|
||||
A wrapper for Syslog Notifications
|
||||
"""
|
||||
|
||||
# The default descriptive name associated with the Notification
|
||||
service_name = 'Syslog'
|
||||
|
||||
# The services URL
|
||||
service_url = 'https://tools.ietf.org/html/rfc5424'
|
||||
|
||||
# The default secure protocol
|
||||
secure_protocol = 'syslog'
|
||||
|
||||
# A URL that takes you to the setup/help of the specific protocol
|
||||
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_syslog'
|
||||
|
||||
# Disable throttle rate for Syslog requests since they are normally
|
||||
# local anyway
|
||||
request_rate_per_sec = 0
|
||||
|
||||
# Title to be added to body if present
|
||||
title_maxlen = 0
|
||||
|
||||
# Define object templates
|
||||
templates = (
|
||||
'{schema}://',
|
||||
'{schema}://{facility}',
|
||||
)
|
||||
|
||||
# Define our template tokens
|
||||
template_tokens = dict(NotifyBase.template_tokens, **{
|
||||
'facility': {
|
||||
'name': _('Facility'),
|
||||
'type': 'choice:string',
|
||||
'values': [k for k in SYSLOG_FACILITY_MAP.keys()],
|
||||
'default': SyslogFacility.USER,
|
||||
},
|
||||
})
|
||||
|
||||
# Define our template arguments
|
||||
template_args = dict(NotifyBase.template_args, **{
|
||||
'facility': {
|
||||
# We map back to the same element defined in template_tokens
|
||||
'alias_of': 'facility',
|
||||
},
|
||||
'logpid': {
|
||||
'name': _('Log PID'),
|
||||
'type': 'bool',
|
||||
'default': True,
|
||||
'map_to': 'log_pid',
|
||||
},
|
||||
'logperror': {
|
||||
'name': _('Log to STDERR'),
|
||||
'type': 'bool',
|
||||
'default': False,
|
||||
'map_to': 'log_perror',
|
||||
},
|
||||
})
|
||||
|
||||
def __init__(self, facility=None, log_pid=True, log_perror=False,
|
||||
**kwargs):
|
||||
"""
|
||||
Initialize Syslog Object
|
||||
"""
|
||||
super(NotifySyslog, self).__init__(**kwargs)
|
||||
|
||||
if facility:
|
||||
try:
|
||||
self.facility = SYSLOG_FACILITY_MAP[facility]
|
||||
|
||||
except KeyError:
|
||||
msg = 'An invalid syslog facility ' \
|
||||
'({}) was specified.'.format(facility)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
else:
|
||||
self.facility = \
|
||||
SYSLOG_FACILITY_MAP[
|
||||
self.template_tokens['facility']['default']]
|
||||
|
||||
# Logging Options
|
||||
self.logoptions = 0
|
||||
|
||||
# Include PID with each message.
|
||||
# This may not appear evident if using journalctl since the pid
|
||||
# will always display itself; however it will appear visible
|
||||
# for log_perror combinations
|
||||
self.log_pid = log_pid
|
||||
|
||||
# Print to stderr as well.
|
||||
self.log_perror = log_perror
|
||||
|
||||
if log_pid:
|
||||
self.logoptions |= syslog.LOG_PID
|
||||
|
||||
if log_perror:
|
||||
self.logoptions |= syslog.LOG_PERROR
|
||||
|
||||
# Initialize our loggig
|
||||
syslog.openlog(
|
||||
self.app_id, logoption=self.logoptions, facility=self.facility)
|
||||
return
|
||||
|
||||
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||
"""
|
||||
Perform Syslog Notification
|
||||
"""
|
||||
|
||||
_pmap = {
|
||||
NotifyType.INFO: syslog.LOG_INFO,
|
||||
NotifyType.SUCCESS: syslog.LOG_NOTICE,
|
||||
NotifyType.FAILURE: syslog.LOG_CRIT,
|
||||
NotifyType.WARNING: syslog.LOG_WARNING,
|
||||
}
|
||||
|
||||
# Always call throttle before any remote server i/o is made
|
||||
self.throttle()
|
||||
try:
|
||||
syslog.syslog(_pmap[notify_type], body)
|
||||
|
||||
except KeyError:
|
||||
# An invalid notification type was specified
|
||||
self.logger.warning(
|
||||
'An invalid notification type '
|
||||
'({}) was specified.'.format(notify_type))
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'logperror': 'yes' if self.log_perror else 'no',
|
||||
'logpid': 'yes' if self.log_pid else 'no',
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
}
|
||||
|
||||
return '{schema}://{facility}/?{args}'.format(
|
||||
facility=self.template_tokens['facility']['default']
|
||||
if self.facility not in SYSLOG_FACILITY_RMAP
|
||||
else SYSLOG_FACILITY_RMAP[self.facility],
|
||||
schema=self.secure_protocol,
|
||||
args=NotifySyslog.urlencode(args),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
|
||||
"""
|
||||
results = NotifyBase.parse_url(url, verify_host=False)
|
||||
if not results:
|
||||
return results
|
||||
|
||||
# if specified; save hostname into facility
|
||||
facility = None if not results['host'] \
|
||||
else NotifySyslog.unquote(results['host'])
|
||||
|
||||
# However if specified on the URL, that will over-ride what was
|
||||
# identified
|
||||
if 'facility' in results['qsd'] and len(results['qsd']['facility']):
|
||||
facility = results['qsd']['facility'].lower()
|
||||
|
||||
if facility and facility not in SYSLOG_FACILITY_MAP:
|
||||
# Find first match; if no match is found we set the result
|
||||
# to the matching key. This allows us to throw a TypeError
|
||||
# during the __init__() call. The benifit of doing this
|
||||
# check here is if we do have a valid match, we can support
|
||||
# short form matches like 'u' which will match against user
|
||||
facility = next((f for f in SYSLOG_FACILITY_MAP.keys()
|
||||
if f.startswith(facility)), facility)
|
||||
|
||||
# Save facility
|
||||
results['facility'] = facility
|
||||
|
||||
# Include PID as part of the message logged
|
||||
results['log_pid'] = \
|
||||
parse_bool(results['qsd'].get('logpid', True))
|
||||
|
||||
# Print to stderr as well.
|
||||
results['log_perror'] = \
|
||||
parse_bool(results['qsd'].get('logperror', False))
|
||||
|
||||
return results
|
Loading…
Reference in new issue