mirror of
https://github.com/SickGear/SickGear.git
synced 2024-11-22 21:05:05 +00:00
335 lines
11 KiB
Python
335 lines
11 KiB
Python
# -*- coding: utf-8 -*-
|
|
# BSD 2-Clause License
|
|
#
|
|
# Apprise - Push Notification Library.
|
|
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
|
|
#
|
|
# Redistribution and use in source and binary forms, with or without
|
|
# modification, are permitted provided that the following conditions are met:
|
|
#
|
|
# 1. Redistributions of source code must retain the above copyright notice,
|
|
# this list of conditions and the following disclaimer.
|
|
#
|
|
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
# this list of conditions and the following disclaimer in the documentation
|
|
# and/or other materials provided with the distribution.
|
|
#
|
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
# POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
from os import urandom
|
|
from json import loads
|
|
import requests
|
|
|
|
from .base import NotifyBase
|
|
from ..url import PrivacyMode
|
|
from ..common import NotifyType
|
|
from ..utils import validate_regex
|
|
from ..locale import gettext_lazy as _
|
|
|
|
from base64 import urlsafe_b64encode
|
|
import hashlib
|
|
|
|
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
|
|
|
|
# We're good to go!
|
|
NOTIFY_SIMPLEPUSH_ENABLED = True
|
|
|
|
except ImportError:
|
|
# cryptography is required in order for this package to work
|
|
NOTIFY_SIMPLEPUSH_ENABLED = False
|
|
|
|
|
|
class NotifySimplePush(NotifyBase):
|
|
"""
|
|
A wrapper for SimplePush Notifications
|
|
"""
|
|
|
|
# Set our global enabled flag
|
|
enabled = NOTIFY_SIMPLEPUSH_ENABLED
|
|
|
|
requirements = {
|
|
# Define our required packaging in order to work
|
|
'packages_required': 'cryptography'
|
|
}
|
|
|
|
# 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': _('Password'),
|
|
'type': 'string',
|
|
'private': True,
|
|
},
|
|
'salt': {
|
|
'name': _('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().__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
|
|
|
|
# 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:
|
|
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,
|
|
timeout=self.request_timeout,
|
|
)
|
|
|
|
# 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 occurred 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.
|
|
"""
|
|
|
|
# Our URL parameters
|
|
params = self.url_parameters(privacy=privacy, *args, **kwargs)
|
|
|
|
if self.event:
|
|
params['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}/?{params}'.format(
|
|
schema=self.secure_protocol,
|
|
auth=auth,
|
|
apikey=self.pprint(self.apikey, privacy, safe=''),
|
|
params=NotifySimplePush.urlencode(params),
|
|
)
|
|
|
|
@staticmethod
|
|
def parse_url(url):
|
|
"""
|
|
Parses the URL and returns enough arguments that can allow
|
|
us to re-instantiate this object.
|
|
|
|
"""
|
|
results = NotifyBase.parse_url(url, verify_host=False)
|
|
if not results:
|
|
# We're done early as we couldn't load the results
|
|
return results
|
|
|
|
# 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
|