From 81c12c81f288e6fbb0ceb7e5e227c127d4306f52 Mon Sep 17 00:00:00 2001 From: Christian Pauly Date: Fri, 17 Apr 2020 14:11:13 +0000 Subject: [PATCH] [Client] Implement thumbnails --- lib/src/client.dart | 1 + lib/src/event.dart | 48 +++++++++++++++------- lib/src/room.dart | 73 +++++++++++++++++++++++++++++----- lib/src/utils/matrix_file.dart | 19 +++++++++ pubspec.lock | 28 +++++++++++++ pubspec.yaml | 1 + 6 files changed, 146 insertions(+), 24 deletions(-) diff --git a/lib/src/client.dart b/lib/src/client.dart index 6de86bd..b667ef3 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -488,6 +488,7 @@ class Client { /// Uploads a new user avatar for this user. Future setAvatar(MatrixFile file) async { + await file.resize(width: 128); final uploadResp = await upload(file); await jsonRequest( type: HTTPType.PUT, diff --git a/lib/src/event.dart b/lib/src/event.dart index abf80ac..87d4ccd 100644 --- a/lib/src/event.dart +++ b/lib/src/event.dart @@ -429,31 +429,50 @@ class Event { return; } + bool get hasThumbnail => + content['info'] is Map && + (content['info'].containsKey('thumbnail_url') || + content['info'].containsKey('thumbnail_file')); + /// Downloads (and decryptes if necessary) the attachment of this /// event and returns it as a [MatrixFile]. If this event doesn't - /// contain an attachment, this throws an error. - Future downloadAndDecryptAttachment() async { + /// contain an attachment, this throws an error. Set [getThumbnail] to + /// true to download the thumbnail instead. + Future downloadAndDecryptAttachment( + {bool getThumbnail = false}) async { if (![EventTypes.Message, EventTypes.Sticker].contains(type)) { throw ("This event has the type '$typeKey' and so it can't contain an attachment."); } - if (!content.containsKey('url') && !content.containsKey('file')) { + if (!getThumbnail && + !content.containsKey('url') && + !content.containsKey('file')) { throw ("This event hasn't any attachment."); } - final isEncrypted = !content.containsKey('url'); + if (getThumbnail && !hasThumbnail) { + throw ("This event hasn't any thumbnail."); + } + final isEncrypted = getThumbnail + ? !content['info'].containsKey('thumbnail_url') + : !content.containsKey('url'); if (isEncrypted && !room.client.encryptionEnabled) { throw ('Encryption is not enabled in your Client.'); } - var mxContent = - MxContent(isEncrypted ? content['file']['url'] : content['url']); + var mxContent = getThumbnail + ? MxContent(isEncrypted + ? content['info']['thumbnail_file']['url'] + : content['info']['thumbnail_url']) + : MxContent(isEncrypted ? content['file']['url'] : content['url']); Uint8List uint8list; // Is this file storeable? + final infoMap = + getThumbnail ? content['info']['thumbnail_info'] : content['info']; final storeable = room.client.storeAPI.extended && - content['info'] is Map && - content['info']['size'] is int && - content['info']['size'] <= ExtendedStoreAPI.MAX_FILE_SIZE; + infoMap is Map && + infoMap['size'] is int && + infoMap['size'] <= ExtendedStoreAPI.MAX_FILE_SIZE; if (storeable) { uint8list = await room.client.store.getFile(mxContent.mxc); @@ -470,15 +489,16 @@ class Event { // Decrypt the file if (isEncrypted) { - if (!content.containsKey('file') || - !content['file']['key']['key_ops'].contains('decrypt')) { + final fileMap = + getThumbnail ? content['info']['thumbnail_file'] : content['file']; + if (!fileMap['key']['key_ops'].contains('decrypt')) { throw ("Missing 'decrypt' in 'key_ops'."); } final encryptedFile = EncryptedFile(); encryptedFile.data = uint8list; - encryptedFile.iv = content['file']['iv']; - encryptedFile.k = content['file']['key']['k']; - encryptedFile.sha256 = content['file']['hashes']['sha256']; + encryptedFile.iv = fileMap['iv']; + encryptedFile.k = fileMap['key']['k']; + encryptedFile.sha256 = fileMap['hashes']['sha256']; uint8list = await decryptFile(encryptedFile); } return MatrixFile(bytes: uint8list, path: '/$body'); diff --git a/lib/src/room.dart b/lib/src/room.dart index 5908d55..0a7cc65 100644 --- a/lib/src/room.dart +++ b/lib/src/room.dart @@ -34,6 +34,7 @@ import 'package:famedlysdk/src/utils/matrix_exception.dart'; import 'package:famedlysdk/src/utils/matrix_file.dart'; import 'package:famedlysdk/src/utils/mx_content.dart'; import 'package:famedlysdk/src/utils/session_key.dart'; +import 'package:image/image.dart'; import 'package:matrix_file_e2ee/matrix_file_e2ee.dart'; import 'package:mime_type/mime_type.dart'; import 'package:olm/olm.dart' as olm; @@ -478,17 +479,13 @@ class Room { Map info, bool waitUntilSent = false, }) async { - var fileName = file.path.split('/').last; - final sendEncrypted = encrypted && client.fileEncryptionEnabled; - EncryptedFile encryptedFile; - if (sendEncrypted) { - encryptedFile = await file.encrypt(); - } - final uploadResp = await client.upload( - file, - contentType: sendEncrypted ? 'application/octet-stream' : null, - ); + Image fileImage; + Image thumbnailImage; + MatrixFile thumbnail; + EncryptedFile encryptedThumbnail; + String thumbnailUploadResp; + var fileName = file.path.split('/').last; final mimeType = mime(file.path) ?? ''; if (msgType == null) { final metaType = (mimeType).split('/')[0]; @@ -504,6 +501,35 @@ class Room { } } + if (msgType == 'm.image') { + var thumbnailPathParts = file.path.split('/'); + thumbnailPathParts.last = 'thumbnail_' + thumbnailPathParts.last; + final thumbnailPath = thumbnailPathParts.join('/'); + thumbnail = MatrixFile(bytes: file.bytes, path: thumbnailPath); + await thumbnail.resize(width: 512); + fileImage = decodeImage(file.bytes.toList()); + thumbnailImage = decodeImage(thumbnail.bytes.toList()); + } + + final sendEncrypted = encrypted && client.fileEncryptionEnabled; + EncryptedFile encryptedFile; + if (sendEncrypted) { + encryptedFile = await file.encrypt(); + if (thumbnail != null) { + encryptedThumbnail = await thumbnail.encrypt(); + } + } + final uploadResp = await client.upload( + file, + contentType: sendEncrypted ? 'application/octet-stream' : null, + ); + if (thumbnail != null) { + thumbnailUploadResp = await client.upload( + thumbnail, + contentType: sendEncrypted ? 'application/octet-stream' : null, + ); + } + // Send event var content = { 'msgtype': msgType, @@ -529,6 +555,32 @@ class Room { { 'mimetype': mimeType, 'size': file.size, + if (fileImage != null) 'h': fileImage.height, + if (fileImage != null) 'w': fileImage.width, + if (thumbnailUploadResp != null && !sendEncrypted) + 'thumbnail_url': thumbnailUploadResp, + if (thumbnailUploadResp != null && sendEncrypted) + 'thumbnail_file': { + 'url': thumbnailUploadResp, + 'mimetype': mimeType, + 'v': 'v2', + 'key': { + 'alg': 'A256CTR', + 'ext': true, + 'k': encryptedThumbnail.k, + 'key_ops': ['encrypt', 'decrypt'], + 'kty': 'oct' + }, + 'iv': encryptedThumbnail.iv, + 'hashes': {'sha256': encryptedThumbnail.sha256} + }, + if (thumbnailImage != null) + 'thumbnail_info': { + 'h': thumbnailImage.height, + 'mimetype': mimeType, + 'size': thumbnailImage.length, + 'w': thumbnailImage.width, + } } }; final sendResponse = sendEvent( @@ -1198,6 +1250,7 @@ class Room { /// Uploads a new user avatar for this room. Returns the event ID of the new /// m.room.avatar event. Future setAvatar(MatrixFile file) async { + await file.resize(width: 256); final uploadResp = await client.upload(file); final setAvatarResp = await client.jsonRequest( type: HTTPType.PUT, diff --git a/lib/src/utils/matrix_file.dart b/lib/src/utils/matrix_file.dart index 30ed3a1..15c5347 100644 --- a/lib/src/utils/matrix_file.dart +++ b/lib/src/utils/matrix_file.dart @@ -2,12 +2,31 @@ import 'dart:typed_data'; +import 'package:image/image.dart'; import 'package:matrix_file_e2ee/matrix_file_e2ee.dart'; class MatrixFile { Uint8List bytes; String path; + /// If this file is an Image, this will resize it to the + /// given width and height. Otherwise returns false. + /// At least width or height must be set! + Future resize({int width, int height}) async { + if (width == null && height == null) { + throw ('At least width or height must be set!'); + } + Image image; + try { + image = decodeImage(bytes); + } catch (_) { + return false; + } + final resizedImage = copyResize(image, width: width, height: height); + bytes = encodePng(resizedImage); + return true; + } + /// Encrypts this file, changes the [bytes] and returns the /// encryption information as an [EncryptedFile]. Future encrypt() async { diff --git a/pubspec.lock b/pubspec.lock index 2e5d5f1..94c3e4e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -8,6 +8,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.36.3" + archive: + dependency: transitive + description: + name: archive + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.13" args: dependency: transitive description: @@ -211,6 +218,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.1.3" + image: + dependency: "direct main" + description: + name: image + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.12" io: dependency: transitive description: @@ -334,6 +348,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.9.0" + petitparser: + dependency: transitive + description: + name: petitparser + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.2" pointycastle: dependency: transitive description: @@ -516,6 +537,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.13" + xml: + dependency: transitive + description: + name: xml + url: "https://pub.dartlang.org" + source: hosted + version: "3.7.0" yaml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 75b9a72..cf91ccd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,6 +11,7 @@ dependencies: http: ^0.12.0+4 mime_type: ^0.2.4 canonical_json: ^1.0.0 + image: ^2.1.4 olm: git: