feat: More advanced attchment handling methods

This commit is contained in:
Sorunome 2020-11-18 13:50:32 +01:00
parent ed8f0f9b53
commit b1709ca8c3
No known key found for this signature in database
GPG Key ID: B19471D07FC9BE9C
2 changed files with 183 additions and 26 deletions

View File

@ -370,10 +370,135 @@ class Event extends MatrixEvent {
return;
}
/// Gets the info map of file events, or a blank map if none present
Map get infoMap =>
content['info'] is Map ? content['info'] : <String, dynamic>{};
/// Gets the thumbnail info map of file events, or a blank map if nonepresent
Map get thumbnailInfoMap => infoMap['thumbnail_info'] is Map
? infoMap['thumbnail_info']
: <String, dynamic>{};
/// Returns if a file event has an attachment
bool get hasAttachment => content['url'] is String || content['file'] is Map;
/// Returns if a file event has a thumbnail
bool get hasThumbnail =>
content['info'] is Map<String, dynamic> &&
(content['info']['thumbnail_url'] is String ||
content['info']['thumbnail_file'] is Map);
infoMap['thumbnail_url'] is String || infoMap['thumbnail_file'] is Map;
/// Returns if a file events attachment is encrypted
bool get isAttachmentEncrypted => content['file'] is Map;
/// Returns if a file events thumbnail is encrypted
bool get isThumbnailEncrypted => infoMap['thumbnail_file'] is Map;
/// Gets the mimetipe of the attachment of a file event, or a blank string if not present
String get attachmentMimetype => infoMap['mimetype'] is String
? infoMap['mimetype'].toLowerCase()
: (content['file'] is Map && content['file']['mimetype'] is String
? content['file']['mimetype']
: '');
/// Gets the mimetype of the thumbnail of a file event, or a blank string if not present
String get thumbnailMimetype => thumbnailInfoMap['mimetype'] is String
? thumbnailInfoMap['mimetype'].toLowerCase()
: (infoMap['thumbnail_file'] is Map &&
infoMap['thumbnail_file']['mimetype'] is String
? infoMap['thumbnail_file']['mimetype']
: '');
/// Gets the underyling mxc url of an attachment of a file event, or null if not present
String get attachmentMxcUrl =>
isAttachmentEncrypted ? content['file']['url'] : content['url'];
/// Gets the underyling mxc url of a thumbnail of a file event, or null if not present
String get thumbnailMxcUrl => isThumbnailEncrypted
? infoMap['thumbnail_file']['url']
: infoMap['thumbnail_url'];
/// Gets the mxc url of an attachemnt/thumbnail of a file event, taking sizes into account, or null if not present
String attachmentOrThumbnailMxcUrl({bool getThumbnail = false}) {
if (getThumbnail &&
infoMap['size'] is int &&
thumbnailInfoMap['size'] is int &&
infoMap['size'] <= thumbnailInfoMap['size']) {
getThumbnail = false;
}
if (getThumbnail && !hasThumbnail) {
getThumbnail = false;
}
return getThumbnail ? thumbnailMxcUrl : attachmentMxcUrl;
}
// size determined from an approximate 800x800 jpeg thumbnail with method=scale
static const _minNoThumbSize = 80 * 1024;
/// Gets the attachment https URL to display in the timeline, taking into account if the original image is tiny.
/// Returns null for encrypted rooms, if the image can't be fetched via http url or if the event does not contain an attachment.
/// Set [getThumbnail] to true to fetch the thumbnail, set [width], [height] and [method]
/// for the respective thumbnailing properties.
/// [minNoThumbSize] is the minimum size that an original image may be to not fetch its thumbnail, defaults to 80k
/// [useThumbnailMxcUrl] says weather to use the mxc url of the thumbnail, rather than the original attachment
String getAttachmentUrl(
{bool getThumbnail = false,
bool useThumbnailMxcUrl = false,
double width = 800.0,
double height = 800.0,
ThumbnailMethod method = ThumbnailMethod.scale,
int minNoThumbSize = _minNoThumbSize}) {
if (![EventTypes.Message, EventTypes.Sticker].contains(type) ||
!hasAttachment ||
isAttachmentEncrypted) {
return null; // can't url-thumbnail in encrypted rooms
}
if (useThumbnailMxcUrl && !hasThumbnail) {
return null; // can't fetch from thumbnail
}
final thisInfoMap = useThumbnailMxcUrl ? thumbnailInfoMap : infoMap;
final thisMxcUrl =
useThumbnailMxcUrl ? infoMap['thumbnail_url'] : content['url'];
// if we have as method scale, we can return safely the original image, should it be small enough
if (getThumbnail &&
method == ThumbnailMethod.scale &&
thisInfoMap['size'] is int &&
thisInfoMap['size'] < minNoThumbSize) {
getThumbnail = false;
}
// now generate the actual URLs
if (getThumbnail) {
return Uri.parse(thisMxcUrl).getThumbnail(
room.client,
width: width,
height: height,
method: method,
);
} else {
return Uri.parse(thisMxcUrl).getDownloadLink(room.client);
}
}
/// Returns if an attachment is in the local store
Future<bool> isAttachmentInLocalStore({bool getThumbnail = false}) async {
if (![EventTypes.Message, EventTypes.Sticker].contains(type)) {
throw ("This event has the type '$type' and so it can't contain an attachment.");
}
final mxcUrl = attachmentOrThumbnailMxcUrl(getThumbnail: getThumbnail);
if (!(mxcUrl is String)) {
throw ("This event hasn't any attachment or thumbnail.");
}
getThumbnail = mxcUrl != attachmentMxcUrl;
// Is this file storeable?
final thisInfoMap = getThumbnail ? thumbnailInfoMap : infoMap;
var storeable = room.client.database != null &&
thisInfoMap['size'] is int &&
thisInfoMap['size'] <= room.client.database.maxFileSize;
Uint8List uint8list;
if (storeable) {
uint8list = await room.client.database.getFile(mxcUrl);
}
return uint8list != null;
}
/// Downloads (and decryptes if necessary) the attachment of this
/// event and returns it as a [MatrixFile]. If this event doesn't
@ -385,36 +510,26 @@ class Event extends MatrixEvent {
if (![EventTypes.Message, EventTypes.Sticker].contains(type)) {
throw ("This event has the type '$type' and so it can't contain an attachment.");
}
if (!getThumbnail &&
!(content['url'] is String) &&
!(content['file'] is Map)) {
throw ("This event hasn't any attachment.");
final mxcUrl = attachmentOrThumbnailMxcUrl(getThumbnail: getThumbnail);
if (!(mxcUrl is String)) {
throw ("This event hasn't any attachment or thumbnail.");
}
if (getThumbnail && !hasThumbnail) {
throw ("This event hasn't any thumbnail.");
}
final isEncrypted = getThumbnail
? !(content['info']['thumbnail_url'] is String)
: !(content['url'] is String);
getThumbnail = mxcUrl != attachmentMxcUrl;
final isEncrypted =
getThumbnail ? isThumbnailEncrypted : isAttachmentEncrypted;
if (isEncrypted && !room.client.encryptionEnabled) {
throw ('Encryption is not enabled in your Client.');
}
var mxContent = getThumbnail
? Uri.parse(isEncrypted
? content['info']['thumbnail_file']['url']
: content['info']['thumbnail_url'])
: Uri.parse(isEncrypted ? content['file']['url'] : content['url']);
final mxContent = Uri.parse(mxcUrl);
Uint8List uint8list;
// Is this file storeable?
final infoMap =
getThumbnail ? content['info']['thumbnail_info'] : content['info'];
final thisInfoMap = getThumbnail ? thumbnailInfoMap : infoMap;
var storeable = room.client.database != null &&
infoMap is Map<String, dynamic> &&
infoMap['size'] is int &&
infoMap['size'] <= room.client.database.maxFileSize;
thisInfoMap['size'] is int &&
thisInfoMap['size'] <= room.client.database.maxFileSize;
if (storeable) {
uint8list = await room.client.database.getFile(mxContent.toString());
@ -438,7 +553,7 @@ class Event extends MatrixEvent {
// Decrypt the file
if (isEncrypted) {
final fileMap =
getThumbnail ? content['info']['thumbnail_file'] : content['file'];
getThumbnail ? infoMap['thumbnail_file'] : content['file'];
if (!fileMap['key']['key_ops'].contains('decrypt')) {
throw ("Missing 'decrypt' in 'key_ops'.");
}

View File

@ -1040,7 +1040,7 @@ void main() {
Timeline(events: <Event>[event, edit1, edit2, edit3], room: room));
expect(displayEvent.body, 'Redacted');
});
test('downloadAndDecryptAttachment', () async {
test('attachments', () async {
final FILE_BUFF = Uint8List.fromList([0]);
final THUMBNAIL_BUFF = Uint8List.fromList([2]);
final downloadCallback = (String url) async {
@ -1066,6 +1066,9 @@ void main() {
var buffer = await event.downloadAndDecryptAttachment(
downloadCallback: downloadCallback);
expect(buffer.bytes, FILE_BUFF);
expect(event.attachmentOrThumbnailMxcUrl(), 'mxc://example.org/file');
expect(event.attachmentOrThumbnailMxcUrl(getThumbnail: true),
'mxc://example.org/file');
event = Event.fromJson({
'type': EventTypes.Message,
@ -1074,12 +1077,41 @@ void main() {
'msgtype': 'm.image',
'url': 'mxc://example.org/file',
'info': {
'size': 8000000,
'thumbnail_url': 'mxc://example.org/thumb',
'thumbnail_info': {
'mimetype': 'thumbnail/mimetype',
},
'mimetype': 'application/octet-stream',
},
},
'event_id': '\$edit2',
'sender': '@alice:example.org',
}, room);
expect(event.hasAttachment, true);
expect(event.hasThumbnail, true);
expect(event.isAttachmentEncrypted, false);
expect(event.isThumbnailEncrypted, false);
expect(event.attachmentMimetype, 'application/octet-stream');
expect(event.thumbnailMimetype, 'thumbnail/mimetype');
expect(event.attachmentMxcUrl, 'mxc://example.org/file');
expect(event.thumbnailMxcUrl, 'mxc://example.org/thumb');
expect(event.attachmentOrThumbnailMxcUrl(), 'mxc://example.org/file');
expect(event.attachmentOrThumbnailMxcUrl(getThumbnail: true),
'mxc://example.org/thumb');
expect(event.getAttachmentUrl(),
'https://fakeserver.notexisting/_matrix/media/r0/download/example.org/file');
expect(event.getAttachmentUrl(getThumbnail: true),
'https://fakeserver.notexisting/_matrix/media/r0/thumbnail/example.org/file?width=800&height=800&method=scale');
expect(event.getAttachmentUrl(useThumbnailMxcUrl: true),
'https://fakeserver.notexisting/_matrix/media/r0/download/example.org/thumb');
expect(
event.getAttachmentUrl(getThumbnail: true, useThumbnailMxcUrl: true),
'https://fakeserver.notexisting/_matrix/media/r0/thumbnail/example.org/thumb?width=800&height=800&method=scale');
expect(
event.getAttachmentUrl(getThumbnail: true, minNoThumbSize: 9000000),
'https://fakeserver.notexisting/_matrix/media/r0/download/example.org/file');
buffer = await event.downloadAndDecryptAttachment(
downloadCallback: downloadCallback);
expect(buffer.bytes, FILE_BUFF);
@ -1088,7 +1120,7 @@ void main() {
getThumbnail: true, downloadCallback: downloadCallback);
expect(buffer.bytes, THUMBNAIL_BUFF);
});
test('downloadAndDecryptAttachment encrypted', () async {
test('encrypted attachments', () async {
if (!olmEnabled) return;
final FILE_BUFF_ENC = Uint8List.fromList([0x3B, 0x6B, 0xB2, 0x8C, 0xAF]);
@ -1174,6 +1206,14 @@ void main() {
'event_id': '\$edit2',
'sender': '@alice:example.org',
}, room);
expect(event.hasAttachment, true);
expect(event.hasThumbnail, true);
expect(event.isAttachmentEncrypted, true);
expect(event.isThumbnailEncrypted, true);
expect(event.attachmentMimetype, 'text/plain');
expect(event.thumbnailMimetype, 'text/plain');
expect(event.attachmentMxcUrl, 'mxc://example.com/file');
expect(event.thumbnailMxcUrl, 'mxc://example.com/thumb');
buffer = await event.downloadAndDecryptAttachment(
downloadCallback: downloadCallback);
expect(buffer.bytes, FILE_BUFF_DEC);
@ -1209,8 +1249,10 @@ void main() {
'event_id': '\$edit2',
'sender': '@alice:example.org',
}, room);
expect(await event.isAttachmentInLocalStore(), false);
var buffer = await event.downloadAndDecryptAttachment(
downloadCallback: downloadCallback);
expect(await event.isAttachmentInLocalStore(), true);
expect(buffer.bytes, FILE_BUFF);
expect(serverHits, 1);
buffer = await event.downloadAndDecryptAttachment(