1
0
Fork 0
mirror of https://github.com/morpheus65535/bazarr synced 2025-02-24 23:02:54 +00:00
bazarr/libs/apprise/plugins/wxpusher.py
2025-01-18 10:02:09 -05:00

373 lines
12 KiB
Python

# -*- coding: utf-8 -*-
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2025, Chris Caron <lead2gold@gmail.com>
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# Sign-up at https://wxpusher.zjiecode.com/
#
# Login and acquire your App Token
# - Open the backend of the application:
# https://wxpusher.zjiecode.com/admin/
# - Find the appToken menu from the left menu bar, here you can reset the
# appToken, please note that after resetting, the old appToken will be
# invalid immediately and the call interface will fail.
import re
import json
import requests
from itertools import chain
from .base import NotifyBase
from ..url import PrivacyMode
from ..common import NotifyType
from ..common import NotifyFormat
from ..utils.parse import parse_list, validate_regex
from ..locale import gettext_lazy as _
# Topics are always numerical
IS_TOPIC = re.compile(r'^\s*(?P<topic>[1-9][0-9]{0,20})\s*$')
# users always start with UID_
IS_USER = re.compile(
r'^\s*(?P<full>(?P<prefix>UID_)(?P<user>[^\s]+))\s*$', re.I)
WXPUSHER_RESPONSE_CODES = {
1000: "The request was processed successfully.",
1001: "The token provided in the request is missing.",
1002: "The token provided in the request is incorrect or expired.",
1003: "The body of the message was not provided.",
1004: "The user or topic you're trying to send the message to does not "
"exist",
1005: "The app or topic binding process failed.",
1006: "There was an error in sending the message.",
1007: "The message content exceeds the allowed length.",
1008: "The API call frequency is too high and the server rejected the "
"request.",
1009: "There might be other issues that are not explicitly covered by "
"the above codes",
1010: "The IP address making the request is not whitelisted.",
}
class WxPusherContentType:
"""
Defines the different supported content types
"""
TEXT = 1
HTML = 2
MARKDOWN = 3
class SubscriptionType:
# Verify Subscription Time
UNVERIFIED = 0
PAID_USERS = 1
UNSUBSCRIBED = 2
class NotifyWxPusher(NotifyBase):
"""
A wrapper for WxPusher Notifications
"""
# The default descriptive name associated with the Notification
service_name = 'WxPusher'
# The services URL
service_url = 'https://wxpusher.zjiecode.com/'
# The default protocol
secure_protocol = 'wxpusher'
# A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_wxpusher'
# WxPusher notification endpoint
notify_url = 'https://wxpusher.zjiecode.com/api/send/message'
# Define object templates
templates = (
'{schema}://{token}/{targets}',
)
# Define our template tokens
template_tokens = dict(NotifyBase.template_tokens, **{
'token': {
'name': _('App Token'),
'type': 'string',
'required': True,
'regex': (r'^AT_[^\s]+$', 'i'),
'private': True,
},
'target_topic': {
'name': _('Target Topic'),
'type': 'int',
'map_to': 'targets',
},
'target_user': {
'name': _('Target User ID'),
'type': 'string',
'regex': (r'^UID_[^\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',
},
'token': {
'alias_of': 'token',
},
})
# Used for mapping the content type to our output since Apprise supports
# The same formats that WxPusher does.
__content_type_map = {
NotifyFormat.MARKDOWN: WxPusherContentType.MARKDOWN,
NotifyFormat.TEXT: WxPusherContentType.TEXT,
NotifyFormat.HTML: WxPusherContentType.HTML,
}
def __init__(self, token, targets=None, **kwargs):
"""
Initialize WxPusher Object
"""
super().__init__(**kwargs)
# App Token (associated with WxPusher account)
self.token = validate_regex(
token, *self.template_tokens['token']['regex'])
if not self.token:
msg = 'An invalid WxPusher App Token ' \
'({}) was specified.'.format(token)
self.logger.warning(msg)
raise TypeError(msg)
# Used for URL generation afterwards only
self._invalid_targets = list()
# For storing what is detected
self._users = list()
self._topics = list()
# Parse our targets
for target in parse_list(targets):
# Validate targets and drop bad ones:
result = IS_USER.match(target)
if result:
# store valid user
self._users.append(result['full'])
continue
result = IS_TOPIC.match(target)
if result:
# store valid topic
self._topics.append(int(result['topic']))
continue
self.logger.warning(
'Dropped invalid WxPusher user/topic '
'(%s) specified.' % target,
)
self._invalid_targets.append(target)
return
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
"""
Perform WxPusher Notification
"""
if not self._users and not self._topics:
# There were no services to notify
self.logger.warning(
'There were no WxPusher targets to notify')
return False
# Prepare our headers
headers = {
'User-Agent': self.app_id,
'Content-Type': 'application/json',
'Accept': 'application/json',
}
# Prepare our payload
payload = {
'appToken': self.token,
'content': body,
'summary': title,
'contentType': self.__content_type_map[self.notify_format],
'topicIds': self._topics,
'uids': self._users,
# unsupported at this time
# 'verifyPay': False,
# 'verifyPayType': 0,
'url': None,
}
# Some Debug Logging
self.logger.debug('WxPusher POST URL: {} (cert_verify={})'.format(
self.notify_url, self.verify_certificate))
self.logger.debug('WxPusher Payload: {}' .format(payload))
# Always call throttle before any remote server i/o is made
self.throttle()
try:
r = requests.post(
self.notify_url,
data=json.dumps(payload).encode('utf-8'),
headers=headers,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
try:
content = json.loads(r.content)
except (AttributeError, TypeError, ValueError):
# ValueError = r.content is Unparsable
# TypeError = r.content is None
# AttributeError = r is None
content = {}
# 1000 is the expected return code for a successful query
if r.status_code == requests.codes.ok and \
content and content.get("code") == 1000:
# We're good!
self.logger.info(
'Sent WxPusher notification to %d targets.' % (
len(self._users) + len(self._topics)))
else:
error_str = content.get('msg') if content else (
WXPUSHER_RESPONSE_CODES.get(
content.get("code") if content else None,
"An unknown error occured."))
# We had a problem
status_str = \
NotifyWxPusher.http_response_code_lookup(
r.status_code) if not error_str else error_str
self.logger.warning(
'Failed to send WxPusher notification, '
'code={}/{}: {}'.format(
r.status_code,
'unk' if not content else content.get("code"),
status_str))
self.logger.debug(
'Response Details:\r\n{}'.format(
content if content else r.content))
# Mark our failure
return False
except requests.RequestException as e:
self.logger.warning(
'A Connection error occurred sending WxPusher '
'notification.'
)
self.logger.debug('Socket Exception: %s' % str(e))
return False
return True
@property
def url_identifier(self):
"""
Returns all of the identifiers that make this URL unique from
another simliar one. Targets or end points should never be identified
here.
"""
return (self.secure_protocol, self.token)
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.
"""
# Define any URL parameters
params = self.url_parameters(privacy=privacy, *args, **kwargs)
return '{schema}://{token}/{targets}/?{params}'.format(
schema=self.secure_protocol,
token=self.pprint(
self.token, privacy, mode=PrivacyMode.Secret, safe=''),
targets='/'.join(chain(
[str(t) for t in self._topics], self._users,
[NotifyWxPusher.quote(x, safe='')
for x in self._invalid_targets])),
params=NotifyWxPusher.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
# Get our entries; split_path() looks after unquoting content for us
# by default
results['targets'] = NotifyWxPusher.split_path(results['fullpath'])
# App Token
if 'token' in results['qsd'] and len(results['qsd']['token']):
# Extract the App token from an argument
results['token'] = \
NotifyWxPusher.unquote(results['qsd']['token'])
# Any host entry defined is actually part of the path
# store it's element (if defined)
if results['host']:
results['targets'].append(
NotifyWxPusher.split_path(results['host']))
else:
# The hostname is our source number
results['token'] = NotifyWxPusher.unquote(results['host'])
# 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'] += \
NotifyWxPusher.parse_list(results['qsd']['to'])
return results