Notifico Support (#163)

This commit is contained in:
Chris Caron 2019-10-13 13:59:02 -04:00 committed by GitHub
parent 6787d33e6b
commit aee893152c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 466 additions and 8 deletions

View File

@ -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

View File

@ -49,9 +49,9 @@ it easy to access:
Boxcar, ClickSend, Discord, E-Mail, Emby, Faast, Flock, Gitter, Gotify, Growl, Boxcar, ClickSend, Discord, E-Mail, Emby, Faast, Flock, Gitter, Gotify, Growl,
IFTTT, Join, KODI, Kumulos, Mailgun, MatterMost, Matrix, Microsoft Windows IFTTT, Join, KODI, Kumulos, Mailgun, MatterMost, Matrix, Microsoft Windows
Notifications, Microsoft Teams, MessageBird, MSG91, Nexmo, Notify MyAndroid, Notifications, Microsoft Teams, MessageBird, MSG91, Nexmo, Notifico,
Prowl, Pushalot, PushBullet, Pushjet, Pushover, Rocket.Chat, SendGrid, Notify MyAndroid, Prowl, Pushalot, PushBullet, Pushjet, Pushover, Rocket.Chat,
SimplePush, Slack, Super Toasty, Stride, Syslog, Techulus Push, Telegram, SendGrid, SimplePush, Slack, Super Toasty, Stride, Syslog, Techulus Push, Telegram,
Twilio, Twitter, Twist, XBMC, XMPP, Webex Teams} Twilio, Twitter, Twist, XBMC, XMPP, Webex Teams}
Name: python-%{pypi_name} Name: python-%{pypi_name}

View File

@ -8,7 +8,7 @@ license_file = LICENSE
[flake8] [flake8]
# We exclude packages we don't maintain # We exclude packages we don't maintain
exclude = .eggs,.tox,gntp exclude = .eggs,.tox,gntp
ignore = E722,W503,W504 ignore = E722,W503,W504,W605
statistics = true statistics = true
builtins = _ builtins = _

View File

@ -71,10 +71,10 @@ setup(
url='https://github.com/caronc/apprise', url='https://github.com/caronc/apprise',
keywords='Push Notifications Alerts Email AWS SNS Boxcar ClickSend ' keywords='Push Notifications Alerts Email AWS SNS Boxcar ClickSend '
'Discord Dbus Emby Faast Flock Gitter Gnome Gotify Growl IFTTT Join ' 'Discord Dbus Emby Faast Flock Gitter Gnome Gotify Growl IFTTT Join '
'KODI Kumulos Mailgun Matrix Mattermost MessageBird MSG91 Nexmo Prowl ' 'KODI Kumulos Mailgun Matrix Mattermost MessageBird MSG91 Nexmo '
'PushBullet Pushjet Pushed Pushover Rocket.Chat Ryver SendGrid ' 'Notifico Prowl PushBullet Pushjet Pushed Pushover Rocket.Chat Ryver '
'SimplePush Slack Stride Syslog Techulus Push Telegram Twilio Twist ' 'SendGrid SimplePush Slack Stride Syslog Techulus Push Telegram '
'Twitter XBMC Microsoft MSTeams Windows Webex CLI API', 'Twilio Twist Twitter XBMC Microsoft MSTeams Windows Webex CLI API',
author='Chris Caron', author='Chris Caron',
author_email='lead2gold@gmail.com', author_email='lead2gold@gmail.com',
packages=find_packages(), packages=find_packages(),

View File

@ -1401,6 +1401,86 @@ TEST_URLS = (
'test_requests_exceptions': True, 'test_requests_exceptions': True,
}), }),
##################################
# NotifyNotifico
##################################
('notifico://', {
'instance': None,
}),
('notifico://:@/', {
'instance': None,
}),
('notifico://1234', {
# Just a project id provided (no message token)
'instance': TypeError,
}),
('notifico://abcd/ckhrjW8w672m6HG', {
# an invalid project id provided
'instance': TypeError,
}),
('notifico://1234/ckhrjW8w672m6HG', {
# A project id and message hook provided
'instance': plugins.NotifyNotifico,
}),
('notifico://1234/ckhrjW8w672m6HG?prefix=no', {
# Disable our prefix
'instance': plugins.NotifyNotifico,
}),
('notifico://1234/ckhrjW8w672m6HG?color=yes', {
'instance': plugins.NotifyNotifico,
'notify_type': 'info',
}),
('notifico://1234/ckhrjW8w672m6HG?color=yes', {
'instance': plugins.NotifyNotifico,
'notify_type': 'success',
}),
('notifico://1234/ckhrjW8w672m6HG?color=yes', {
'instance': plugins.NotifyNotifico,
'notify_type': 'warning',
}),
('notifico://1234/ckhrjW8w672m6HG?color=yes', {
'instance': plugins.NotifyNotifico,
'notify_type': 'failure',
}),
('notifico://1234/ckhrjW8w672m6HG?color=yes', {
'instance': plugins.NotifyNotifico,
'notify_type': 'invalid',
}),
('notifico://1234/ckhrjW8w672m6HG?color=no', {
# Test our color flag by having it set to off
'instance': plugins.NotifyNotifico,
# Our expected url(privacy=True) startswith() response:
'privacy_url': 'notifico://1...4/c...G',
}),
# Support Native URLs
('https://n.tkte.ch/h/2144/uJmKaBW9WFk42miB146ci3Kj', {
'instance': plugins.NotifyNotifico,
}),
('notifico://1234/ckhrjW8w672m6HG', {
'instance': plugins.NotifyNotifico,
# don't include an image by default
'include_image': False,
}),
('notifico://1234/ckhrjW8w672m6HG', {
'instance': plugins.NotifyNotifico,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('notifico://1234/ckhrjW8w672m6HG', {
'instance': plugins.NotifyNotifico,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('notifico://1234/ckhrjW8w672m6HG', {
'instance': plugins.NotifyNotifico,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
################################## ##################################
# NotifyProwl # NotifyProwl
################################## ##################################