mirror of https://github.com/caronc/apprise
Merged Matrix Webhook functionality with server config; refs #80
parent
426092cf3f
commit
28e490ab67
|
@ -43,7 +43,7 @@ The table below identifies the services this tool supports and some example serv
|
||||||
| [IFTTT](https://github.com/caronc/apprise/wiki/Notify_ifttt) | ifttt:// | (TCP) 443 | ifttt://webhooksID/Event<br />ifttt://webhooksID/Event1/Event2/EventN<br/>ifttt://webhooksID/Event1/?+Key=Value<br/>ifttt://webhooksID/Event1/?-Key=value1
|
| [IFTTT](https://github.com/caronc/apprise/wiki/Notify_ifttt) | ifttt:// | (TCP) 443 | ifttt://webhooksID/Event<br />ifttt://webhooksID/Event1/Event2/EventN<br/>ifttt://webhooksID/Event1/?+Key=Value<br/>ifttt://webhooksID/Event1/?-Key=value1
|
||||||
| [Join](https://github.com/caronc/apprise/wiki/Notify_join) | join:// | (TCP) 443 | join://apikey/device<br />join://apikey/device1/device2/deviceN/<br />join://apikey/group<br />join://apikey/groupA/groupB/groupN<br />join://apikey/DeviceA/groupA/groupN/DeviceN/
|
| [Join](https://github.com/caronc/apprise/wiki/Notify_join) | join:// | (TCP) 443 | join://apikey/device<br />join://apikey/device1/device2/deviceN/<br />join://apikey/group<br />join://apikey/groupA/groupB/groupN<br />join://apikey/DeviceA/groupA/groupN/DeviceN/
|
||||||
| [KODI](https://github.com/caronc/apprise/wiki/Notify_kodi) | kodi:// or kodis:// | (TCP) 8080 or 443 | kodi://hostname<br />kodi://user@hostname<br />kodi://user:password@hostname:port
|
| [KODI](https://github.com/caronc/apprise/wiki/Notify_kodi) | kodi:// or kodis:// | (TCP) 8080 or 443 | kodi://hostname<br />kodi://user@hostname<br />kodi://user:password@hostname:port
|
||||||
| [Matrix](https://github.com/caronc/apprise/wiki/Notify_matrix) | matrix:// or matrixs:// | (TCP) 8008 or 8448 | matrix://hostname<br />matrix://user@hostname<br />matrixs://user:pass@hostname:port/#room_alias<br />matrixs://user:pass@hostname:port/!room_id<br />matrixs://user:pass@hostname:port/#room_alias/!room_id/#room2
|
| [Matrix](https://github.com/caronc/apprise/wiki/Notify_matrix) | matrix:// or matrixs:// | (TCP) 80 or 443 | matrix://hostname<br />matrix://user@hostname<br />matrixs://user:pass@hostname:port/#room_alias<br />matrixs://user:pass@hostname:port/!room_id<br />matrixs://user:pass@hostname:port/#room_alias/!room_id/#room2<br />matrixs://token@hostname:port/?webhook=matrix<br />matrix://user:token@hostname/?webhook=slack&format=markdown
|
||||||
| [Mattermost](https://github.com/caronc/apprise/wiki/Notify_mattermost) | mmost:// | (TCP) 8065 | mmost://hostname/authkey<br />mmost://hostname:80/authkey<br />mmost://user@hostname:80/authkey<br />mmost://hostname/authkey?channel=channel<br />mmosts://hostname/authkey<br />mmosts://user@hostname/authkey<br />
|
| [Mattermost](https://github.com/caronc/apprise/wiki/Notify_mattermost) | mmost:// | (TCP) 8065 | mmost://hostname/authkey<br />mmost://hostname:80/authkey<br />mmost://user@hostname:80/authkey<br />mmost://hostname/authkey?channel=channel<br />mmosts://hostname/authkey<br />mmosts://user@hostname/authkey<br />
|
||||||
| [Prowl](https://github.com/caronc/apprise/wiki/Notify_prowl) | prowl:// | (TCP) 443 | prowl://apikey<br />prowl://apikey/providerkey
|
| [Prowl](https://github.com/caronc/apprise/wiki/Notify_prowl) | prowl:// | (TCP) 443 | prowl://apikey<br />prowl://apikey/providerkey
|
||||||
| [PushBullet](https://github.com/caronc/apprise/wiki/Notify_pushbullet) | pbul:// | (TCP) 443 | pbul://accesstoken<br />pbul://accesstoken/#channel<br/>pbul://accesstoken/A_DEVICE_ID<br />pbul://accesstoken/email@address.com<br />pbul://accesstoken/#channel/#channel2/email@address.net/DEVICE
|
| [PushBullet](https://github.com/caronc/apprise/wiki/Notify_pushbullet) | pbul:// | (TCP) 443 | pbul://accesstoken<br />pbul://accesstoken/#channel<br/>pbul://accesstoken/A_DEVICE_ID<br />pbul://accesstoken/email@address.com<br />pbul://accesstoken/#channel/#channel2/email@address.net/DEVICE
|
||||||
|
|
|
@ -32,14 +32,17 @@ import six
|
||||||
import requests
|
import requests
|
||||||
from json import dumps
|
from json import dumps
|
||||||
from json import loads
|
from json import loads
|
||||||
|
from time import time
|
||||||
|
|
||||||
from .NotifyBase import NotifyBase
|
from .NotifyBase import NotifyBase
|
||||||
from ..common import NotifyType
|
from ..common import NotifyType
|
||||||
from ..common import NotifyImageSize
|
from ..common import NotifyImageSize
|
||||||
|
from ..common import NotifyFormat
|
||||||
from ..utils import parse_bool
|
from ..utils import parse_bool
|
||||||
|
|
||||||
# Define default path
|
# Define default path
|
||||||
MATRIX_V2_API_PATH = '/_matrix/client/r0'
|
MATRIX_V2_API_PATH = '/_matrix/client/r0'
|
||||||
|
MATRIX_V1_WEBHOOK_PATH = '/api/v1/matrix/hook'
|
||||||
|
|
||||||
# Extend HTTP Error Messages
|
# Extend HTTP Error Messages
|
||||||
MATRIX_HTTP_ERROR_MAP = {
|
MATRIX_HTTP_ERROR_MAP = {
|
||||||
|
@ -61,6 +64,24 @@ IS_ROOM_ID = re.compile(
|
||||||
r'^\s*(!|!|%21)(?P<room>[a-z0-9-]+)((:|%3A)'
|
r'^\s*(!|!|%21)(?P<room>[a-z0-9-]+)((:|%3A)'
|
||||||
r'(?P<home_server>[a-z0-9.-]+))?\s*$', re.I)
|
r'(?P<home_server>[a-z0-9.-]+))?\s*$', re.I)
|
||||||
|
|
||||||
|
# Default User
|
||||||
|
SLACK_DEFAULT_USER = 'apprise'
|
||||||
|
|
||||||
|
|
||||||
|
class MatrixWebhookMode(object):
|
||||||
|
# The default webhook mode is to just be set to Matrix
|
||||||
|
MATRIX = "matrix"
|
||||||
|
|
||||||
|
# Support the slack webhook plugin
|
||||||
|
SLACK = "slack"
|
||||||
|
|
||||||
|
|
||||||
|
# webhook modes are placed ito this list for validation purposes
|
||||||
|
MATRIX_WEBHOOK_MODES = (
|
||||||
|
MatrixWebhookMode.MATRIX,
|
||||||
|
MatrixWebhookMode.SLACK,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class NotifyMatrix(NotifyBase):
|
class NotifyMatrix(NotifyBase):
|
||||||
"""
|
"""
|
||||||
|
@ -79,12 +100,6 @@ class NotifyMatrix(NotifyBase):
|
||||||
# The default secure protocol
|
# The default secure protocol
|
||||||
secure_protocol = 'matrixs'
|
secure_protocol = 'matrixs'
|
||||||
|
|
||||||
# Define the default secure port to use
|
|
||||||
default_secure_port = 8448
|
|
||||||
|
|
||||||
# Define the default insecure port to use
|
|
||||||
default_insecure_port = 8008
|
|
||||||
|
|
||||||
# A URL that takes you to the setup/help of the specific protocol
|
# A URL that takes you to the setup/help of the specific protocol
|
||||||
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_matrix'
|
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_matrix'
|
||||||
|
|
||||||
|
@ -105,7 +120,7 @@ class NotifyMatrix(NotifyBase):
|
||||||
# the server doesn't remind us how long we shoul wait for
|
# the server doesn't remind us how long we shoul wait for
|
||||||
default_wait_ms = 1000
|
default_wait_ms = 1000
|
||||||
|
|
||||||
def __init__(self, rooms=None, thumbnail=True, **kwargs):
|
def __init__(self, rooms=None, webhook=None, thumbnail=True, **kwargs):
|
||||||
"""
|
"""
|
||||||
Initialize Matrix Object
|
Initialize Matrix Object
|
||||||
"""
|
"""
|
||||||
|
@ -123,6 +138,13 @@ class NotifyMatrix(NotifyBase):
|
||||||
else:
|
else:
|
||||||
self.rooms = []
|
self.rooms = []
|
||||||
|
|
||||||
|
self.webhook = None \
|
||||||
|
if not isinstance(webhook, six.string_types) else webhook.lower()
|
||||||
|
if self.webhook and self.webhook not in MATRIX_WEBHOOK_MODES:
|
||||||
|
msg = 'The webhook specified ({}) is invalid.'.format(webhook)
|
||||||
|
self.logger.warning(msg)
|
||||||
|
raise TypeError(msg)
|
||||||
|
|
||||||
# our home server gets populated after a login/registration
|
# our home server gets populated after a login/registration
|
||||||
self.home_server = None
|
self.home_server = None
|
||||||
|
|
||||||
|
@ -144,6 +166,172 @@ class NotifyMatrix(NotifyBase):
|
||||||
Perform Matrix Notification
|
Perform Matrix Notification
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Call the _send_ function applicable to whatever mode we're in
|
||||||
|
# - calls _send_webhook_notification if the webhook variable is set
|
||||||
|
# - calls _send_server_notification if the webhook variable is not set
|
||||||
|
return getattr(self, '_send_{}_notification'.format(
|
||||||
|
'webhook' if self.webhook else 'server'))(
|
||||||
|
body=body, title=title, notify_type=notify_type, **kwargs)
|
||||||
|
|
||||||
|
def _send_webhook_notification(self, body, title='',
|
||||||
|
notify_type=NotifyType.INFO, **kwargs):
|
||||||
|
"""
|
||||||
|
Perform Matrix Notification as a webhook
|
||||||
|
"""
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'User-Agent': self.app_id,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Acquire our access token from our URL
|
||||||
|
access_token = self.password if self.password else self.user
|
||||||
|
|
||||||
|
default_port = 443 if self.secure else 80
|
||||||
|
|
||||||
|
# Prepare our URL
|
||||||
|
url = '{schema}://{hostname}:{port}/{token}{webhook_path}'.format(
|
||||||
|
schema='https' if self.secure else 'http',
|
||||||
|
hostname=self.host,
|
||||||
|
port='' if self.port is None
|
||||||
|
or self.port == default_port else self.port,
|
||||||
|
token=access_token,
|
||||||
|
webhook_path=MATRIX_V1_WEBHOOK_PATH,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Retrieve our payload
|
||||||
|
payload = getattr(self, '_{}_webhook_payload'.format(self.webhook))(
|
||||||
|
body=body, title=title, notify_type=notify_type, **kwargs)
|
||||||
|
|
||||||
|
self.logger.debug('Matrix POST URL: %s (cert_verify=%r)' % (
|
||||||
|
url, self.verify_certificate,
|
||||||
|
))
|
||||||
|
self.logger.debug('Matrix Payload: %s' % str(payload))
|
||||||
|
|
||||||
|
# Always call throttle before any remote server i/o is made
|
||||||
|
self.throttle()
|
||||||
|
|
||||||
|
try:
|
||||||
|
r = requests.post(
|
||||||
|
url,
|
||||||
|
data=dumps(payload),
|
||||||
|
headers=headers,
|
||||||
|
verify=self.verify_certificate,
|
||||||
|
)
|
||||||
|
if r.status_code != requests.codes.ok:
|
||||||
|
# We had a problem
|
||||||
|
status_str = \
|
||||||
|
NotifyBase.http_response_code_lookup(
|
||||||
|
r.status_code, MATRIX_HTTP_ERROR_MAP)
|
||||||
|
|
||||||
|
self.logger.warning(
|
||||||
|
'Failed to send Matrix 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 Matrix notification.')
|
||||||
|
|
||||||
|
except requests.RequestException as e:
|
||||||
|
self.logger.warning(
|
||||||
|
'A Connection error occured sending Matrix notification.'
|
||||||
|
)
|
||||||
|
self.logger.debug('Socket Exception: %s' % str(e))
|
||||||
|
# Return; we're done
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _slack_webhook_payload(self, body, title='',
|
||||||
|
notify_type=NotifyType.INFO, **kwargs):
|
||||||
|
"""
|
||||||
|
Format the payload for a Slack based message
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not hasattr(self, '_re_slack_formatting_rules'):
|
||||||
|
# Prepare some one-time slack formating variables
|
||||||
|
|
||||||
|
self._re_slack_formatting_map = {
|
||||||
|
# New lines must become the string version
|
||||||
|
r'\r\*\n': '\\n',
|
||||||
|
# Escape other special characters
|
||||||
|
r'&': '&',
|
||||||
|
r'<': '<',
|
||||||
|
r'>': '>',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Iterate over above list and store content accordingly
|
||||||
|
self._re_slack_formatting_rules = re.compile(
|
||||||
|
r'(' + '|'.join(self._re_slack_formatting_map.keys()) + r')',
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Perform Formatting
|
||||||
|
title = self._re_slack_formatting_rules.sub( # pragma: no branch
|
||||||
|
lambda x: self._re_slack_formatting_map[x.group()], title,
|
||||||
|
)
|
||||||
|
|
||||||
|
body = self._re_slack_formatting_rules.sub( # pragma: no branch
|
||||||
|
lambda x: self._re_slack_formatting_map[x.group()], body,
|
||||||
|
)
|
||||||
|
|
||||||
|
# prepare JSON Object
|
||||||
|
payload = {
|
||||||
|
'username': self.user if self.user else SLACK_DEFAULT_USER,
|
||||||
|
# Use Markdown language
|
||||||
|
'mrkdwn': (self.notify_format == NotifyFormat.MARKDOWN),
|
||||||
|
'attachments': [{
|
||||||
|
'title': title,
|
||||||
|
'text': body,
|
||||||
|
'color': self.color(notify_type),
|
||||||
|
'ts': time(),
|
||||||
|
'footer': self.app_id,
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload
|
||||||
|
|
||||||
|
def _matrix_webhook_payload(self, body, title='',
|
||||||
|
notify_type=NotifyType.INFO, **kwargs):
|
||||||
|
"""
|
||||||
|
Format the payload for a Matrix based message
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
'displayName':
|
||||||
|
self.user if self.user else self.matrix_default_user,
|
||||||
|
'format': 'html',
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.notify_format == NotifyFormat.HTML:
|
||||||
|
payload['text'] = '{}{}'.format('' if not title else title, body)
|
||||||
|
|
||||||
|
else: # TEXT or MARKDOWN
|
||||||
|
|
||||||
|
# Ensure our content is escaped
|
||||||
|
title = NotifyBase.escape_html(title)
|
||||||
|
body = NotifyBase.escape_html(body)
|
||||||
|
|
||||||
|
payload['text'] = '{}{}'.format(
|
||||||
|
'' if not title else '<h4>{}</h4>'.format(title), body)
|
||||||
|
|
||||||
|
return payload
|
||||||
|
|
||||||
|
def _send_server_notification(self, body, title='',
|
||||||
|
notify_type=NotifyType.INFO, **kwargs):
|
||||||
|
"""
|
||||||
|
Perform Direct Matrix Server Notification (no webhook)
|
||||||
|
"""
|
||||||
|
|
||||||
if self.access_token is None:
|
if self.access_token is None:
|
||||||
# We need to register
|
# We need to register
|
||||||
if not self._login():
|
if not self._login():
|
||||||
|
@ -573,14 +761,14 @@ class NotifyMatrix(NotifyBase):
|
||||||
if self.access_token is not None:
|
if self.access_token is not None:
|
||||||
headers["Authorization"] = 'Bearer %s' % self.access_token
|
headers["Authorization"] = 'Bearer %s' % self.access_token
|
||||||
|
|
||||||
default_port = self.default_secure_port \
|
default_port = 443 if self.secure else 80
|
||||||
if self.secure else self.default_insecure_port
|
|
||||||
|
|
||||||
url = \
|
url = \
|
||||||
'{schema}://{hostname}:{port}{matrix_api}{path}'.format(
|
'{schema}://{hostname}:{port}{matrix_api}{path}'.format(
|
||||||
schema='https' if self.secure else 'http',
|
schema='https' if self.secure else 'http',
|
||||||
hostname=self.host,
|
hostname=self.host,
|
||||||
port=default_port if self.port is None else self.port,
|
port='' if self.port is None
|
||||||
|
or self.port == default_port else self.port,
|
||||||
matrix_api=MATRIX_V2_API_PATH,
|
matrix_api=MATRIX_V2_API_PATH,
|
||||||
path=path)
|
path=path)
|
||||||
|
|
||||||
|
@ -691,6 +879,9 @@ class NotifyMatrix(NotifyBase):
|
||||||
'overflow': self.overflow_mode,
|
'overflow': self.overflow_mode,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.webhook:
|
||||||
|
args['webhook'] = self.webhook
|
||||||
|
|
||||||
# Determine Authentication method
|
# Determine Authentication method
|
||||||
auth = ''
|
auth = ''
|
||||||
if self.user and self.password:
|
if self.user and self.password:
|
||||||
|
@ -704,15 +895,14 @@ class NotifyMatrix(NotifyBase):
|
||||||
user=self.quote(self.user, safe=''),
|
user=self.quote(self.user, safe=''),
|
||||||
)
|
)
|
||||||
|
|
||||||
default_port = self.default_secure_port \
|
default_port = 443 if self.secure else 80
|
||||||
if self.secure else self.default_insecure_port
|
|
||||||
|
|
||||||
return '{schema}://{auth}{hostname}{port}/{rooms}?{args}'.format(
|
return '{schema}://{auth}{hostname}{port}/{rooms}?{args}'.format(
|
||||||
schema=self.secure_protocol if self.secure else self.protocol,
|
schema=self.secure_protocol if self.secure else self.protocol,
|
||||||
auth=auth,
|
auth=auth,
|
||||||
hostname=self.host,
|
hostname=self.host,
|
||||||
port='' if self.port is None or self.port == default_port
|
port='' if self.port is None
|
||||||
else ':{}'.format(self.port),
|
or self.port == default_port else ':{}'.format(self.port),
|
||||||
rooms=self.quote('/'.join(self.rooms)),
|
rooms=self.quote('/'.join(self.rooms)),
|
||||||
args=self.urlencode(args),
|
args=self.urlencode(args),
|
||||||
)
|
)
|
||||||
|
@ -739,4 +929,7 @@ class NotifyMatrix(NotifyBase):
|
||||||
results['thumbnail'] = \
|
results['thumbnail'] = \
|
||||||
parse_bool(results['qsd'].get('thumbnail', False))
|
parse_bool(results['qsd'].get('thumbnail', False))
|
||||||
|
|
||||||
|
# Webhook
|
||||||
|
results['webhook'] = results['qsd'].get('webhook')
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
|
@ -583,6 +583,47 @@ TEST_URLS = (
|
||||||
'test_requests_exceptions': True,
|
'test_requests_exceptions': True,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
# Matrix supports webhooks too; the following tests this now:
|
||||||
|
('matrix://user:token@localhost?webhook=matrix&format=text', {
|
||||||
|
# user and token correctly specified with webhook
|
||||||
|
'instance': plugins.NotifyMatrix,
|
||||||
|
}),
|
||||||
|
('matrix://user:token@localhost?webhook=matrix&format=html', {
|
||||||
|
# user and token correctly specified with webhook
|
||||||
|
'instance': plugins.NotifyMatrix,
|
||||||
|
}),
|
||||||
|
('matrix://user:token@localhost?webhook=slack&format=text', {
|
||||||
|
# user and token correctly specified with webhook
|
||||||
|
'instance': plugins.NotifyMatrix,
|
||||||
|
}),
|
||||||
|
('matrixs://user:token@localhost?webhook=SLACK&format=markdown', {
|
||||||
|
# user and token specified; slack webhook still detected
|
||||||
|
# despite uppercase characters
|
||||||
|
'instance': plugins.NotifyMatrix,
|
||||||
|
}),
|
||||||
|
('matrix://user:token@localhost?webhook=On', {
|
||||||
|
# invalid webhook specified (unexpected boolean)
|
||||||
|
'instance': TypeError,
|
||||||
|
}),
|
||||||
|
('matrix://token@localhost/?webhook=Matrix', {
|
||||||
|
'instance': plugins.NotifyMatrix,
|
||||||
|
'response': False,
|
||||||
|
'requests_response_code': requests.codes.internal_server_error,
|
||||||
|
}),
|
||||||
|
('matrix://user:token@localhost/webhook=matrix', {
|
||||||
|
'instance': plugins.NotifyMatrix,
|
||||||
|
# throw a bizzare code forcing us to fail to look it up
|
||||||
|
'response': False,
|
||||||
|
'requests_response_code': 999,
|
||||||
|
}),
|
||||||
|
('matrix://token@localhost:8080/?webhook=slack', {
|
||||||
|
'instance': plugins.NotifyMatrix,
|
||||||
|
# Throws a series of connection and transfer exceptions when this flag
|
||||||
|
# is set and tests that we gracfully handle them
|
||||||
|
'test_requests_exceptions': True,
|
||||||
|
}),
|
||||||
|
|
||||||
|
|
||||||
##################################
|
##################################
|
||||||
# NotifyMatterMost
|
# NotifyMatterMost
|
||||||
##################################
|
##################################
|
||||||
|
|
Loading…
Reference in New Issue