Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Matrix Attachment support #921

Merged
merged 3 commits into from
Aug 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 143 additions & 17 deletions apprise/plugins/NotifyMatrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,11 @@
from ..AppriseLocale import gettext_lazy as _

# Define default path
MATRIX_V2_API_PATH = '/_matrix/client/r0'
MATRIX_V1_WEBHOOK_PATH = '/api/v1/matrix/hook'
MATRIX_V2_API_PATH = '/_matrix/client/r0'
MATRIX_V3_API_PATH = '/_matrix/client/v3'
MATRIX_V3_MEDIA_PATH = '/_matrix/media/v3'
MATRIX_V2_MEDIA_PATH = '/_matrix/media/r0'

# Extend HTTP Error Messages
MATRIX_HTTP_ERROR_MAP = {
Expand Down Expand Up @@ -88,6 +91,21 @@ class MatrixMessageType:
)


class MatrixVersion:
# Version 2
V2 = "2"

# Version 3
V3 = "3"


# webhook modes are placed into this list for validation purposes
MATRIX_VERSIONS = (
MatrixVersion.V2,
MatrixVersion.V3,
)


class MatrixWebhookMode:
# Webhook Mode is disabled
DISABLED = "off"
Expand Down Expand Up @@ -128,6 +146,9 @@ class NotifyMatrix(NotifyBase):
# The default secure protocol
secure_protocol = 'matrixs'

# Support Attachments
attachment_support = True

# A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_matrix'

Expand All @@ -147,6 +168,9 @@ class NotifyMatrix(NotifyBase):
# Throttle a wee-bit to avoid thrashing
request_rate_per_sec = 0.5

# Our Matrix API Version
matrix_api_version = '3'

# How many retry attempts we'll make in the event the server asks us to
# throttle back.
default_retries = 2
Expand Down Expand Up @@ -234,6 +258,12 @@ class NotifyMatrix(NotifyBase):
'values': MATRIX_WEBHOOK_MODES,
'default': MatrixWebhookMode.DISABLED,
},
'version': {
'name': _('Matrix API Verion'),
'type': 'choice:string',
'values': MATRIX_VERSIONS,
'default': MatrixVersion.V3,
},
'msgtype': {
'name': _('Message Type'),
'type': 'choice:string',
Expand All @@ -248,7 +278,7 @@ class NotifyMatrix(NotifyBase):
},
})

def __init__(self, targets=None, mode=None, msgtype=None,
def __init__(self, targets=None, mode=None, msgtype=None, version=None,
include_image=False, **kwargs):
"""
Initialize Matrix Object
Expand Down Expand Up @@ -282,6 +312,14 @@ def __init__(self, targets=None, mode=None, msgtype=None,
self.logger.warning(msg)
raise TypeError(msg)

# Setup our version
self.version = self.template_args['version']['default'] \
if not isinstance(version, str) else version
if self.version not in MATRIX_VERSIONS:
msg = 'The version specified ({}) is invalid.'.format(version)
self.logger.warning(msg)
raise TypeError(msg)

# Setup our message type
self.msgtype = self.template_args['msgtype']['default'] \
if not isinstance(msgtype, str) else msgtype.lower()
Expand Down Expand Up @@ -521,7 +559,8 @@ def _t2bot_webhook_payload(self, body, title='',
return payload

def _send_server_notification(self, body, title='',
notify_type=NotifyType.INFO, **kwargs):
notify_type=NotifyType.INFO, attach=None,
**kwargs):
"""
Perform Direct Matrix Server Notification (no webhook)
"""
Expand All @@ -548,6 +587,13 @@ def _send_server_notification(self, body, title='',
# Initiaize our error tracking
has_error = False

attachments = None
if attach and self.attachment_support:
attachments = self._send_attachments(attach)
if not attachments:
# take an early exit
return False

while len(rooms) > 0:

# Get our room
Expand All @@ -568,16 +614,17 @@ def _send_server_notification(self, body, title='',
image_url = None if not self.include_image else \
self.image_url(notify_type)

# Build our path
path = '/rooms/{}/send/m.room.message'.format(
NotifyMatrix.quote(room_id))

if image_url:
# Define our payload
image_payload = {
'msgtype': 'm.image',
'url': image_url,
'body': '{}'.format(notify_type if not title else title),
}
# Build our path
path = '/rooms/{}/send/m.room.message'.format(
NotifyMatrix.quote(room_id))

# Post our content
postokay, response = self._fetch(path, payload=image_payload)
Expand All @@ -586,6 +633,14 @@ def _send_server_notification(self, body, title='',
has_error = True
continue

if attachments:
for attachment in attachments:
postokay, response = self._fetch(path, payload=attachment)
if not postokay:
# Mark our failure
has_error = True
continue

# Define our payload
payload = {
'msgtype': 'm.{}'.format(self.msgtype),
Expand Down Expand Up @@ -615,10 +670,6 @@ def _send_server_notification(self, body, title='',
)
})

# Build our path
path = '/rooms/{}/send/m.room.message'.format(
NotifyMatrix.quote(room_id))

# Post our content
postokay, response = self._fetch(path, payload=payload)
if not postokay:
Expand All @@ -632,6 +683,44 @@ def _send_server_notification(self, body, title='',

return not has_error

def _send_attachments(self, attach):
"""
Posts all of the provided attachments
"""

payloads = []
for attachment in attach:
if not attachment:
# invalid attachment (bad file)
return False

if not re.match(r'^image/', attachment.mimetype, re.I):
# unsuppored at this time
continue

postokay, response = \
self._fetch('/upload', attachment=attachment)
if not (postokay and isinstance(response, dict)):
# Failed to perform upload
return False

# If we get here, we'll have a response that looks like:
# {
# "content_uri": "mxc://example.com/a-unique-key"
# }

# Prepare our payload
payloads.append({
"info": {
"mimetype": attachment.mimetype,
},
"msgtype": "m.image",
"body": "tta.webp",
"url": response.get('content_uri'),
})

return payloads

def _register(self):
"""
Register with the service if possible.
Expand Down Expand Up @@ -970,7 +1059,8 @@ def _room_id(self, room):

return None

def _fetch(self, path, payload=None, params=None, method='POST'):
def _fetch(self, path, payload=None, params=None, attachment=None,
method='POST'):
"""
Wrapper to request.post() to manage it's response better and make
the send() function cleaner and easier to maintain.
Expand All @@ -983,6 +1073,7 @@ def _fetch(self, path, payload=None, params=None, method='POST'):
headers = {
'User-Agent': self.app_id,
'Content-Type': 'application/json',
'Accept': 'application/json',
}

if self.access_token is not None:
Expand All @@ -991,13 +1082,32 @@ def _fetch(self, path, payload=None, params=None, method='POST'):
default_port = 443 if self.secure else 80

url = \
'{schema}://{hostname}{port}{matrix_api}{path}'.format(
'{schema}://{hostname}{port}'.format(
schema='https' if self.secure else 'http',
hostname=self.host,
port='' if self.port is None
or self.port == default_port else f':{self.port}',
matrix_api=MATRIX_V2_API_PATH,
path=path)
or self.port == default_port else f':{self.port}')

if path == '/upload':
if self.version == MatrixVersion.V3:
url += MATRIX_V3_MEDIA_PATH + path

else:
url += MATRIX_V2_MEDIA_PATH + path

params = {'filename': attachment.name}
with open(attachment.path, 'rb') as fp:
payload = fp.read()

# Update our content type
headers['Content-Type'] = attachment.mimetype

else:
if self.version == MatrixVersion.V3:
url += MATRIX_V3_API_PATH + path

else:
url += MATRIX_V2_API_PATH + path

# Our response object
response = {}
Expand All @@ -1024,7 +1134,7 @@ def _fetch(self, path, payload=None, params=None, method='POST'):
try:
r = fn(
url,
data=dumps(payload),
data=dumps(payload) if not attachment else payload,
params=params,
headers=headers,
verify=self.verify_certificate,
Expand Down Expand Up @@ -1095,6 +1205,13 @@ def _fetch(self, path, payload=None, params=None, method='POST'):
# Return; we're done
return (False, response)

except (OSError, IOError) as e:
self.logger.warning(
'An I/O error occurred while reading {}.'.format(
attachment.name if attachment else 'unknown file'))
self.logger.debug('I/O Exception: %s' % str(e))
return (False, {})

return (True, response)

# If we get here, we ran out of retries
Expand Down Expand Up @@ -1161,6 +1278,7 @@ def url(self, privacy=False, *args, **kwargs):
params = {
'image': 'yes' if self.include_image else 'no',
'mode': self.mode,
'version': self.version,
'msgtype': self.msgtype,
}

Expand Down Expand Up @@ -1258,6 +1376,14 @@ def parse_url(url):
if 'token' in results['qsd'] and len(results['qsd']['token']):
results['password'] = NotifyMatrix.unquote(results['qsd']['token'])

# Support the use of the version= or v= keyword
if 'version' in results['qsd'] and len(results['qsd']['version']):
results['version'] = \
NotifyMatrix.unquote(results['qsd']['version'])

elif 'v' in results['qsd'] and len(results['qsd']['v']):
results['version'] = NotifyMatrix.unquote(results['qsd']['v'])

return results

@staticmethod
Expand All @@ -1267,7 +1393,7 @@ def parse_native_url(url):
"""

result = re.match(
r'^https?://webhooks\.t2bot\.io/api/v1/matrix/hook/'
r'^https?://webhooks\.t2bot\.io/api/v[0-9]+/matrix/hook/'
r'(?P<webhook_token>[A-Z0-9_-]+)/?'
r'(?P<params>\?.+)?$', url, re.I)

Expand Down
Loading