Skip to content

Commit

Permalink
Added Matrix Attachment support
Browse files Browse the repository at this point in the history
  • Loading branch information
caronc committed Aug 7, 2023
1 parent 236e67f commit 0d81de4
Showing 1 changed file with 126 additions and 17 deletions.
143 changes: 126 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 @@ -147,6 +165,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 +255,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 +275,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 +309,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.lower()
if self.version and 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 +556,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 +584,10 @@ def _send_server_notification(self, body, title='',
# Initiaize our error tracking
has_error = False

attachments = None
if attach:
attachments = self._send_attachments(attach)

while len(rooms) > 0:

# Get our room
Expand All @@ -568,16 +608,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 +627,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 +664,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 +677,40 @@ 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 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 +1049,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 +1063,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 +1072,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 +1124,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 @@ -1161,6 +1261,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 +1359,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 +1376,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

0 comments on commit 0d81de4

Please sign in to comment.