From cfa633b489bea54c603a841073d8143fa0e058e4 Mon Sep 17 00:00:00 2001 From: Lukas Lihotzki Date: Thu, 22 Jul 2021 17:08:36 +0200 Subject: [PATCH] feat: Add image pack event content models --- lib/matrix_api_lite.dart | 1 + lib/src/model/events/image_pack_content.dart | 178 +++++++++++++++++++ test/event_content_test.dart | 87 ++++++++- 3 files changed, 262 insertions(+), 4 deletions(-) create mode 100644 lib/src/model/events/image_pack_content.dart diff --git a/lib/matrix_api_lite.dart b/lib/matrix_api_lite.dart index d6251d92..838e242e 100644 --- a/lib/matrix_api_lite.dart +++ b/lib/matrix_api_lite.dart @@ -41,6 +41,7 @@ export 'src/model/basic_event_with_sender.dart'; export 'src/model/basic_room_event.dart'; export 'src/model/event_types.dart'; export 'src/model/events/forwarded_room_key_content.dart'; +export 'src/model/events/image_pack_content.dart'; export 'src/model/events/olm_plaintext_payload.dart'; export 'src/model/events/room_encrypted_content.dart'; export 'src/model/events/room_encryption_content.dart'; diff --git a/lib/src/model/events/image_pack_content.dart b/lib/src/model/events/image_pack_content.dart new file mode 100644 index 00000000..6c1ae0af --- /dev/null +++ b/lib/src/model/events/image_pack_content.dart @@ -0,0 +1,178 @@ +/* MIT License +* +* Copyright (C) 2019, 2020, 2021 Famedly GmbH +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +*/ + +import '../basic_event.dart'; +import '../../utils/filter_map_extension.dart'; +import '../../utils/try_get_map_extension.dart'; + +extension ImagePackContentBasicEventExtension on BasicEvent { + ImagePackContent get parsedImagePackContent => + ImagePackContent.fromJson(content); +} + +enum ImagePackUsage { + sticker, + emoticon, +} + +List? imagePackUsageFromJson(List? json) => json + ?.map((v) => { + 'sticker': ImagePackUsage.sticker, + 'emoticon': ImagePackUsage.emoticon, + }[v]) + .whereType() + .toList(); + +List imagePackUsageToJson( + List? usage, List? prevUsage) { + final knownUsages = {'sticker', 'emoticon'}; + final usagesStr = usage + ?.map((v) => { + ImagePackUsage.sticker: 'sticker', + ImagePackUsage.emoticon: 'emoticon', + }[v]) + .whereType() + .toList() ?? + []; + // first we add all the unknown usages and the previous known usages which are new again + final newUsages = prevUsage + ?.where((v) => !knownUsages.contains(v) || usagesStr.contains(v)) + .toList() ?? + []; + // now we need to add the new usages that we didn't add yet + newUsages.addAll(usagesStr.where((v) => !newUsages.contains(v))); + return newUsages; +} + +class ImagePackContent { + // we want to preserve potential custom keys in this object + final Map _json; + + Map images; + ImagePackPackContent pack; + + ImagePackContent({required this.images, required this.pack}) : _json = {}; + + ImagePackContent.fromJson(Map json) + : _json = Map.fromEntries(json.entries.where( + (e) => !['images', 'pack', 'emoticons', 'short'].contains(e.key))), + pack = ImagePackPackContent.fromJson( + json.tryGetMap('pack', TryGet.optional) ?? {}), + images = json + .tryGetMap('images', TryGet.optional) + ?.catchMap( + (k, v) => MapEntry(k, ImagePackImageContent.fromJson(v))) ?? + // the "emoticons" key needs a small migration on the key, ":string:" --> "string" + json + .tryGetMap('emoticons', TryGet.optional) + ?.catchMap((k, v) => MapEntry( + k.startsWith(':') && k.endsWith(':') + ? k.substring(1, k.length - 1) + : k, + ImagePackImageContent.fromJson(v))) ?? + // the "short" key was still just a map from shortcode to mxc uri + json.tryGetMap('short', TryGet.optional)?.catchMap( + (k, v) => MapEntry( + k.startsWith(':') && k.endsWith(':') + ? k.substring(1, k.length - 1) + : k, + ImagePackImageContent(url: Uri.parse(v)))) ?? + {}; + + Map toJson() => { + ..._json, + 'images': images.map((k, v) => MapEntry(k, v.toJson())), + 'pack': pack.toJson(), + }; +} + +class ImagePackImageContent { + // we want to preserve potential custom keys in this object + final Map _json; + + Uri url; + String? body; + Map? info; + List? usage; + + ImagePackImageContent({required this.url, this.body, this.info, this.usage}) + : _json = {}; + + ImagePackImageContent.fromJson(Map json) + : _json = Map.fromEntries(json.entries + .where((e) => !['url', 'body', 'info'].contains(e.key))), + url = Uri.parse(json['url']), + body = json.tryGet('body', TryGet.optional), + info = json.tryGetMap('info', TryGet.optional), + usage = imagePackUsageFromJson( + json.tryGetList('usage', TryGet.optional)); + + Map toJson() { + return { + ...Map.from(_json)..remove('usage'), + 'url': url.toString(), + if (body != null) 'body': body, + if (info != null) 'info': info, + if (usage != null) + 'usage': imagePackUsageToJson( + usage, _json.tryGetList('usage', TryGet.optional)), + }; + } +} + +class ImagePackPackContent { + // we want to preserve potential custom keys in this object + final Map _json; + + String? displayName; + Uri? avatarUrl; + List? usage; + String? attribution; + + ImagePackPackContent( + {this.displayName, this.avatarUrl, this.usage, this.attribution}) + : _json = {}; + + ImagePackPackContent.fromJson(Map json) + : _json = Map.fromEntries(json.entries.where((e) => + !['display_name', 'avatar_url', 'attribution'].contains(e.key))), + displayName = json.tryGet('display_name', TryGet.optional), + // we default to an invalid uri + avatarUrl = + Uri.tryParse(json.tryGet('avatar_url', TryGet.optional) ?? '.::'), + usage = imagePackUsageFromJson( + json.tryGetList('usage', TryGet.optional)), + attribution = json.tryGet('attribution', TryGet.optional); + + Map toJson() { + return { + ...Map.from(_json)..remove('usage'), + if (displayName != null) 'display_name': displayName, + if (avatarUrl != null) 'avatar_url': avatarUrl.toString(), + if (usage != null) + 'usage': imagePackUsageToJson( + usage, _json.tryGetList('usage', TryGet.optional)), + if (attribution != null) 'attribution': attribution, + }; + } +} diff --git a/test/event_content_test.dart b/test/event_content_test.dart index 82099ac7..67fe9cc9 100644 --- a/test/event_content_test.dart +++ b/test/event_content_test.dart @@ -1,17 +1,17 @@ /* MIT License -* +* * Copyright (C) 2019, 2020, 2021 Famedly GmbH -* +* * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: -* +* * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. -* +* * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -167,5 +167,84 @@ void main() { json = jsonDecode(jsonEncode(json)); expect(OlmPlaintextPayload.fromJson(json!).toJson(), json); }); + test('Image Pack Content', () { + // basic parse / unparse + var json = { + 'type': 'some type', + 'content': { + 'images': { + 'emote': { + 'url': 'mxc://example.org/beep', + 'usage': ['emoticon'], + 'org.custom': 'beep', + }, + 'sticker': { + 'url': 'mxc://example.org/boop', + 'usage': ['org.custom', 'sticker', 'org.other.custom'], + }, + }, + 'pack': { + 'display_name': 'Awesome Pack', + 'org.custom': 'boop', + }, + 'org.custom': 'blah', + }, + }; + json = jsonDecode(jsonEncode(json)); + expect(BasicEvent.fromJson(json).parsedImagePackContent.toJson(), + json['content']); + + // emoticons migration + json = { + 'type': 'some type', + 'content': { + 'emoticons': { + ':emote:': { + 'url': 'mxc://example.org/beep', + }, + }, + }, + }; + json = jsonDecode(jsonEncode(json)); + expect( + BasicEvent.fromJson(json) + .parsedImagePackContent + .images['emote'] + ?.toJson(), + { + 'url': 'mxc://example.org/beep', + }); + + json = { + 'type': 'some type', + 'content': { + 'short': { + ':emote:': 'mxc://example.org/beep', + }, + }, + }; + json = jsonDecode(jsonEncode(json)); + expect( + BasicEvent.fromJson(json) + .parsedImagePackContent + .images['emote'] + ?.toJson(), + { + 'url': 'mxc://example.org/beep', + }); + + // invalid url for image + json = { + 'type': 'some type', + 'content': { + 'images': { + 'emote': {}, + }, + }, + }; + json = jsonDecode(jsonEncode(json)); + expect(BasicEvent.fromJson(json).parsedImagePackContent.images['emote'], + null); + }); }); }