Merge branch 'client-enhance-implement-thumbnails' into 'master'

[Client] Implement thumbnails

See merge request famedly/famedlysdk!261
This commit is contained in:
Christian Pauly 2020-04-17 14:11:13 +00:00
commit f2d4a39fc0
6 changed files with 146 additions and 24 deletions

View file

@ -488,6 +488,7 @@ class Client {
/// Uploads a new user avatar for this user. /// Uploads a new user avatar for this user.
Future<void> setAvatar(MatrixFile file) async { Future<void> setAvatar(MatrixFile file) async {
await file.resize(width: 128);
final uploadResp = await upload(file); final uploadResp = await upload(file);
await jsonRequest( await jsonRequest(
type: HTTPType.PUT, type: HTTPType.PUT,

View file

@ -429,31 +429,50 @@ class Event {
return; return;
} }
bool get hasThumbnail =>
content['info'] is Map<String, dynamic> &&
(content['info'].containsKey('thumbnail_url') ||
content['info'].containsKey('thumbnail_file'));
/// Downloads (and decryptes if necessary) the attachment of this /// Downloads (and decryptes if necessary) the attachment of this
/// event and returns it as a [MatrixFile]. If this event doesn't /// event and returns it as a [MatrixFile]. If this event doesn't
/// contain an attachment, this throws an error. /// contain an attachment, this throws an error. Set [getThumbnail] to
Future<MatrixFile> downloadAndDecryptAttachment() async { /// true to download the thumbnail instead.
Future<MatrixFile> downloadAndDecryptAttachment(
{bool getThumbnail = false}) async {
if (![EventTypes.Message, EventTypes.Sticker].contains(type)) { if (![EventTypes.Message, EventTypes.Sticker].contains(type)) {
throw ("This event has the type '$typeKey' and so it can't contain an attachment."); 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."); 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) { if (isEncrypted && !room.client.encryptionEnabled) {
throw ('Encryption is not enabled in your Client.'); throw ('Encryption is not enabled in your Client.');
} }
var mxContent = var mxContent = getThumbnail
MxContent(isEncrypted ? content['file']['url'] : content['url']); ? MxContent(isEncrypted
? content['info']['thumbnail_file']['url']
: content['info']['thumbnail_url'])
: MxContent(isEncrypted ? content['file']['url'] : content['url']);
Uint8List uint8list; Uint8List uint8list;
// Is this file storeable? // Is this file storeable?
final infoMap =
getThumbnail ? content['info']['thumbnail_info'] : content['info'];
final storeable = room.client.storeAPI.extended && final storeable = room.client.storeAPI.extended &&
content['info'] is Map<String, dynamic> && infoMap is Map<String, dynamic> &&
content['info']['size'] is int && infoMap['size'] is int &&
content['info']['size'] <= ExtendedStoreAPI.MAX_FILE_SIZE; infoMap['size'] <= ExtendedStoreAPI.MAX_FILE_SIZE;
if (storeable) { if (storeable) {
uint8list = await room.client.store.getFile(mxContent.mxc); uint8list = await room.client.store.getFile(mxContent.mxc);
@ -470,15 +489,16 @@ class Event {
// Decrypt the file // Decrypt the file
if (isEncrypted) { if (isEncrypted) {
if (!content.containsKey('file') || final fileMap =
!content['file']['key']['key_ops'].contains('decrypt')) { getThumbnail ? content['info']['thumbnail_file'] : content['file'];
if (!fileMap['key']['key_ops'].contains('decrypt')) {
throw ("Missing 'decrypt' in 'key_ops'."); throw ("Missing 'decrypt' in 'key_ops'.");
} }
final encryptedFile = EncryptedFile(); final encryptedFile = EncryptedFile();
encryptedFile.data = uint8list; encryptedFile.data = uint8list;
encryptedFile.iv = content['file']['iv']; encryptedFile.iv = fileMap['iv'];
encryptedFile.k = content['file']['key']['k']; encryptedFile.k = fileMap['key']['k'];
encryptedFile.sha256 = content['file']['hashes']['sha256']; encryptedFile.sha256 = fileMap['hashes']['sha256'];
uint8list = await decryptFile(encryptedFile); uint8list = await decryptFile(encryptedFile);
} }
return MatrixFile(bytes: uint8list, path: '/$body'); return MatrixFile(bytes: uint8list, path: '/$body');

View file

@ -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/matrix_file.dart';
import 'package:famedlysdk/src/utils/mx_content.dart'; import 'package:famedlysdk/src/utils/mx_content.dart';
import 'package:famedlysdk/src/utils/session_key.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:matrix_file_e2ee/matrix_file_e2ee.dart';
import 'package:mime_type/mime_type.dart'; import 'package:mime_type/mime_type.dart';
import 'package:olm/olm.dart' as olm; import 'package:olm/olm.dart' as olm;
@ -478,17 +479,13 @@ class Room {
Map<String, dynamic> info, Map<String, dynamic> info,
bool waitUntilSent = false, bool waitUntilSent = false,
}) async { }) async {
var fileName = file.path.split('/').last; Image fileImage;
final sendEncrypted = encrypted && client.fileEncryptionEnabled; Image thumbnailImage;
EncryptedFile encryptedFile; MatrixFile thumbnail;
if (sendEncrypted) { EncryptedFile encryptedThumbnail;
encryptedFile = await file.encrypt(); String thumbnailUploadResp;
}
final uploadResp = await client.upload(
file,
contentType: sendEncrypted ? 'application/octet-stream' : null,
);
var fileName = file.path.split('/').last;
final mimeType = mime(file.path) ?? ''; final mimeType = mime(file.path) ?? '';
if (msgType == null) { if (msgType == null) {
final metaType = (mimeType).split('/')[0]; 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 // Send event
var content = <String, dynamic>{ var content = <String, dynamic>{
'msgtype': msgType, 'msgtype': msgType,
@ -529,6 +555,32 @@ class Room {
{ {
'mimetype': mimeType, 'mimetype': mimeType,
'size': file.size, '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( final sendResponse = sendEvent(
@ -1198,6 +1250,7 @@ class Room {
/// Uploads a new user avatar for this room. Returns the event ID of the new /// Uploads a new user avatar for this room. Returns the event ID of the new
/// m.room.avatar event. /// m.room.avatar event.
Future<String> setAvatar(MatrixFile file) async { Future<String> setAvatar(MatrixFile file) async {
await file.resize(width: 256);
final uploadResp = await client.upload(file); final uploadResp = await client.upload(file);
final setAvatarResp = await client.jsonRequest( final setAvatarResp = await client.jsonRequest(
type: HTTPType.PUT, type: HTTPType.PUT,

View file

@ -2,12 +2,31 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:image/image.dart';
import 'package:matrix_file_e2ee/matrix_file_e2ee.dart'; import 'package:matrix_file_e2ee/matrix_file_e2ee.dart';
class MatrixFile { class MatrixFile {
Uint8List bytes; Uint8List bytes;
String path; 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<bool> 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 /// Encrypts this file, changes the [bytes] and returns the
/// encryption information as an [EncryptedFile]. /// encryption information as an [EncryptedFile].
Future<EncryptedFile> encrypt() async { Future<EncryptedFile> encrypt() async {

View file

@ -8,6 +8,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.36.3" version: "0.36.3"
archive:
dependency: transitive
description:
name: archive
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.13"
args: args:
dependency: transitive dependency: transitive
description: description:
@ -211,6 +218,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.1.3" version: "3.1.3"
image:
dependency: "direct main"
description:
name: image
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.12"
io: io:
dependency: transitive dependency: transitive
description: description:
@ -334,6 +348,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.9.0" version: "1.9.0"
petitparser:
dependency: transitive
description:
name: petitparser
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.2"
pointycastle: pointycastle:
dependency: transitive dependency: transitive
description: description:
@ -516,6 +537,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.13" version: "1.0.13"
xml:
dependency: transitive
description:
name: xml
url: "https://pub.dartlang.org"
source: hosted
version: "3.7.0"
yaml: yaml:
dependency: transitive dependency: transitive
description: description:

View file

@ -11,6 +11,7 @@ dependencies:
http: ^0.12.0+4 http: ^0.12.0+4
mime_type: ^0.2.4 mime_type: ^0.2.4
canonical_json: ^1.0.0 canonical_json: ^1.0.0
image: ^2.1.4
olm: olm:
git: git: