Implemented support for file upload

This commit is contained in:
Fabio Manganiello 2022-08-27 15:12:50 +02:00
parent 48ec6ef68b
commit 513195b396
Signed by: blacklight
GPG key ID: D90FBA7F76362774
2 changed files with 129 additions and 5 deletions

View file

@ -292,6 +292,8 @@ autodoc_mock_imports = [
'irc.events', 'irc.events',
'defusedxml', 'defusedxml',
'nio', 'nio',
'aiofiles',
'aiofiles.os',
] ]
sys.path.insert(0, os.path.abspath('../..')) sys.path.insert(0, os.path.abspath('../..'))

View file

@ -56,6 +56,9 @@ from nio import (
UnknownEvent, UnknownEvent,
) )
import aiofiles
import aiofiles.os
from nio.client.async_client import client_session from nio.client.async_client import client_session
from nio.crypto import decrypt_attachment from nio.crypto import decrypt_attachment
from nio.crypto.device import OlmDevice from nio.crypto.device import OlmDevice
@ -93,6 +96,8 @@ from platypush.schemas.matrix import (
MatrixRoomSchema, MatrixRoomSchema,
) )
from platypush.utils import get_mime_type
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -730,6 +735,27 @@ class MatrixClient(AsyncClient):
'Received an unknown event on room %s: %r', room.room_id, event 'Received an unknown event on room %s: %r', room.room_id, event
) )
async def upload_file(
self,
file: str,
name: str | None = None,
content_type: str | None = None,
encrypt: bool = False,
):
file = os.path.expanduser(file)
file_stat = await aiofiles.os.stat(file)
async with aiofiles.open(file, 'rb') as f:
return await super().upload(
f, # type: ignore
content_type=(
content_type or get_mime_type(file) or 'application/octet-stream'
),
filename=name or os.path.basename(file),
encrypt=encrypt,
filesize=file_stat.st_size,
)
class MatrixPlugin(AsyncRunnablePlugin): class MatrixPlugin(AsyncRunnablePlugin):
""" """
@ -955,12 +981,65 @@ class MatrixPlugin(AsyncRunnablePlugin):
return ret return ret
def _process_local_attachment(self, attachment: str, room_id: str) -> dict:
attachment = os.path.expanduser(attachment)
assert os.path.isfile(attachment), f'{attachment} is not a valid file'
filename = os.path.basename(attachment)
mime_type = get_mime_type(attachment) or 'application/octet-stream'
message_type = mime_type.split('/')[0]
if message_type not in {'audio', 'video', 'image'}:
message_type = 'text'
encrypted = self.get_room(room_id).output.get('encrypted', False) # type: ignore
url = self.upload(
attachment, name=filename, content_type=mime_type, encrypt=encrypted
).output # type: ignore
return {
'url': url,
'msgtype': 'm.' + message_type,
'body': filename,
'info': {
'size': os.stat(attachment).st_size,
'mimetype': mime_type,
},
}
def _process_remote_attachment(self, attachment: str) -> dict:
parsed_url = urlparse(attachment)
server = parsed_url.netloc.strip('/')
media_id = parsed_url.path.strip('/')
response = self._loop_execute(self.client.download(server, media_id))
content_type = response.content_type
message_type = content_type.split('/')[0]
if message_type not in {'audio', 'video', 'image'}:
message_type = 'text'
return {
'url': attachment,
'msgtype': 'm.' + message_type,
'body': response.filename,
'info': {
'size': len(response.body),
'mimetype': content_type,
},
}
def _process_attachment(self, attachment: str, room_id: str):
if attachment.startswith('mxc://'):
return self._process_remote_attachment(attachment)
return self._process_local_attachment(attachment, room_id=room_id)
@action @action
def send_message( def send_message(
self, self,
room_id: str, room_id: str,
message_type: str = 'text', message_type: str = 'text',
body: str | None = None, body: str | None = None,
attachment: str | None = None,
tx_id: str | None = None, tx_id: str | None = None,
ignore_unverified_devices: bool = False, ignore_unverified_devices: bool = False,
): ):
@ -969,6 +1048,12 @@ class MatrixPlugin(AsyncRunnablePlugin):
:param room_id: Room ID. :param room_id: Room ID.
:param body: Message body. :param body: Message body.
:param attachment: Path to a local file to send as an attachment, or
URL of an existing Matrix media ID in the format
``mxc://<server>/<media_id>``. If the attachment is a local file,
the file will be automatically uploaded, ``message_type`` will be
automatically inferred from the file and the ``body`` will be
replaced by the filename.
:param message_type: Message type. Supported: `text`, `audio`, `video`, :param message_type: Message type. Supported: `text`, `audio`, `video`,
`image`. Default: `text`. `image`. Default: `text`.
:param tx_id: Unique transaction ID to associate to this message. :param tx_id: Unique transaction ID to associate to this message.
@ -978,16 +1063,21 @@ class MatrixPlugin(AsyncRunnablePlugin):
delivery may fail (default: False). delivery may fail (default: False).
:return: .. schema:: matrix.MatrixEventIdSchema :return: .. schema:: matrix.MatrixEventIdSchema
""" """
content = {
'msgtype': 'm.' + message_type,
'body': body,
}
if attachment:
content.update(self._process_attachment(attachment, room_id=room_id))
ret = self._loop_execute( ret = self._loop_execute(
self.client.room_send( self.client.room_send(
message_type='m.room.message', message_type='m.room.message',
room_id=room_id, room_id=room_id,
tx_id=tx_id, tx_id=tx_id,
ignore_unverified_devices=ignore_unverified_devices, ignore_unverified_devices=ignore_unverified_devices,
content={ content=content,
'msgtype': 'm.' + message_type,
'body': body,
},
) )
) )
@ -1126,7 +1216,14 @@ class MatrixPlugin(AsyncRunnablePlugin):
Note that URLs that point to encrypted resources will be automatically Note that URLs that point to encrypted resources will be automatically
decrypted only if they were received on a room joined by this account. decrypted only if they were received on a room joined by this account.
:param url: Matrix URL, in the format :param url: Matrix URL, in the format ``mxc://<server>/<media_id>``.
:param download_path: Override the default ``download_path`` (output
directory for the downloaded file).
:param filename: Name of the output file (default: inferred from the
remote resource).
:param allow_remote: Indicates to the server that it should not attempt
to fetch the media if it is deemed remote. This is to prevent
routing loops where the server contacts itself.
:return: .. schema:: matrix.MatrixDownloadedFileSchema :return: .. schema:: matrix.MatrixDownloadedFileSchema
""" """
parsed_url = urlparse(url) parsed_url = urlparse(url)
@ -1159,5 +1256,30 @@ class MatrixPlugin(AsyncRunnablePlugin):
} }
) )
@action
def upload(
self,
file: str,
name: str | None = None,
content_type: str | None = None,
encrypt: bool = False,
) -> str:
"""
Upload a file to the server.
:param file: Path to the file to upload.
:param name: Filename to be used for the remote file (default: same as
the local file).
:param content_type: Specify a content type for the file (default:
inferred from the file's extension and content).
:param encrypt: Encrypt the file (default: False).
:return: The Matrix URL of the uploaded resource.
"""
rs = self._loop_execute(
self.client.upload_file(file, name, content_type, encrypt)
)
return rs[0].content_uri
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et: