From cabf357cf781da90a9b6b5aed6aa16154698a396 Mon Sep 17 00:00:00 2001 From: Krille Date: Mon, 22 Jul 2024 13:16:53 +0200 Subject: [PATCH 1/2] refactor: Cache profiles in database and refactor API --- lib/matrix.dart | 1 + .../msc_1236_widgets/src/widget.dart | 6 +- lib/src/client.dart | 69 ++++++++++++++++++- lib/src/database/database_api.dart | 7 ++ .../database/hive_collections_database.dart | 16 +++++ lib/src/database/hive_database.dart | 16 +++++ lib/src/database/matrix_sdk_database.dart | 37 ++++++++++ lib/src/room.dart | 4 +- lib/src/utils/cached_profile_information.dart | 29 ++++++++ test/client_test.dart | 45 +++++++++--- test/database_api_test.dart | 32 +++++++++ 11 files changed, 245 insertions(+), 17 deletions(-) create mode 100644 lib/src/utils/cached_profile_information.dart diff --git a/lib/matrix.dart b/lib/matrix.dart index 4ddc729b..3187ecbf 100644 --- a/lib/matrix.dart +++ b/lib/matrix.dart @@ -52,6 +52,7 @@ export 'src/voip/utils/wrapped_media_stream.dart'; export 'src/room.dart'; export 'src/timeline.dart'; export 'src/user.dart'; +export 'src/utils/cached_profile_information.dart'; export 'src/utils/commands_extension.dart'; export 'src/utils/crypto/encrypted_file.dart'; export 'src/utils/device_keys_list.dart'; diff --git a/lib/msc_extensions/msc_1236_widgets/src/widget.dart b/lib/msc_extensions/msc_1236_widgets/src/widget.dart index bfb36cf7..4f44657d 100644 --- a/lib/msc_extensions/msc_1236_widgets/src/widget.dart +++ b/lib/msc_extensions/msc_1236_widgets/src/widget.dart @@ -91,14 +91,14 @@ class MatrixWidget { // See https://github.com/matrix-org/matrix-doc/issues/1236 for a // description, specifically the section // `What does the other stuff in content mean?` - final userProfile = await room.client.fetchOwnProfile(); + final userProfile = await room.client.getUserProfile(room.client.userID!); var parsedUri = url; // a key-value map with the strings to be replaced final replaceMap = { - r'$matrix_user_id': userProfile.userId, + r'$matrix_user_id': room.client.userID!, r'$matrix_room_id': room.id, - r'$matrix_display_name': userProfile.displayName ?? '', + r'$matrix_display_name': userProfile.displayname ?? '', r'$matrix_avatar_url': userProfile.avatarUrl?.toString() ?? '', // removing potentially dangerous keys containing anything but // `[a-zA-Z0-9_-]` as well as non string values diff --git a/lib/src/client.dart b/lib/src/client.dart index 2522e553..2cf69f80 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -911,7 +911,7 @@ class Client extends MatrixApi { return id; } - @Deprecated('Use fetchOwnProfile() instead') + @Deprecated('Use getUserProfile(userID) instead') Future get ownProfile => fetchOwnProfile(); /// Returns the user's own displayname and avatar url. In Matrix it is possible that @@ -919,6 +919,7 @@ class Client extends MatrixApi { /// Tries to get the profile from homeserver first, if failed, falls back to a profile /// from a room where the user exists. Set `useServerCache` to true to get any /// prior value from this function + @Deprecated('Use getUserProfile() instead') Future fetchOwnProfileFromServer( {bool useServerCache = false}) async { try { @@ -942,6 +943,7 @@ class Client extends MatrixApi { /// one user can have different displaynames and avatar urls in different rooms. /// This returns the profile from the first room by default, override `getFromRooms` /// to false to fetch from homeserver. + @Deprecated('User `getUserProfile(userID)` instead') Future fetchOwnProfile({ bool getFromRooms = true, bool cache = true, @@ -955,6 +957,55 @@ class Client extends MatrixApi { final Map _profileRoomsCache = {}; final Map _profileServerCache = {}; + /// Get the combined profile information for this user. First checks for a + /// non outdated cached profile before requesting from the server. Cached + /// profiles are outdated if they have been cached in a time older than the + /// [maxCacheAge] or they have been marked as outdated by an event in the + /// sync loop. + /// In case of an + /// + /// [userId] The user whose profile information to get. + @override + Future getUserProfile( + String userId, { + Duration timeout = const Duration(seconds: 30), + Duration maxCacheAge = const Duration(days: 1), + }) async { + final cachedProfile = await database?.getUserProfile(userId); + if (cachedProfile != null && + !cachedProfile.outdated && + DateTime.now().difference(cachedProfile.updated) < maxCacheAge) { + return cachedProfile; + } + + final ProfileInformation profile; + try { + profile = await (_userProfileRequests[userId] ??= + super.getUserProfile(userId).timeout(timeout)); + } catch (e) { + Logs().d('Unable to fetch profile from server', e); + if (cachedProfile == null) rethrow; + return cachedProfile; + } finally { + unawaited(_userProfileRequests.remove(userId)); + } + + final newCachedProfile = CachedProfileInformation.fromProfile( + profile, + outdated: false, + updated: DateTime.now(), + ); + + await database?.storeUserProfile(userId, newCachedProfile); + + return newCachedProfile; + } + + final Map> _userProfileRequests = {}; + + final CachedStreamController onUserProfileUpdate = + CachedStreamController(); + /// Get the combined profile information for this user. /// If [getFromRooms] is true then the profile will first be searched from the /// room memberships. This is unstable if the given user makes use of different displaynames @@ -962,6 +1013,7 @@ class Client extends MatrixApi { /// If [cache] is true then /// the profile get cached for this session. Please note that then the profile may /// become outdated if the user changes the displayname or avatar in this session. + @Deprecated('User `getUserProfile(userID)` instead') Future getProfileFromUserId(String userId, {bool cache = true, bool getFromRooms = true}) async { var profile = @@ -993,7 +1045,7 @@ class Client extends MatrixApi { return profileFromRooms; } } - profile = await getUserProfile(userId); + profile = await super.getUserProfile(userId); if (cache || _profileServerCache.containsKey(userId)) { _profileServerCache[userId] = profile; } @@ -2295,6 +2347,19 @@ class Client extends MatrixApi { content: update.content))); } } + + // Any kind of member change? We should invalidate the profile then: + if (event is StrippedStateEvent && event.type == EventTypes.RoomMember) { + final userId = event.stateKey; + if (userId != null) { + // We do not re-request the profile here as this would lead to + // an unknown amount of network requests as we never know how many + // member change events can come down in a single sync update. + await database?.markUserProfileAsOutdated(userId); + onUserProfileUpdate.add(userId); + } + } + if (event.type == EventTypes.Message && !room.isDirectChat && database != null && diff --git a/lib/src/database/database_api.dart b/lib/src/database/database_api.dart index 6f74ca88..c07b1541 100644 --- a/lib/src/database/database_api.dart +++ b/lib/src/database/database_api.dart @@ -81,6 +81,13 @@ abstract class DatabaseApi { Future forgetRoom(String roomId); + Future getUserProfile(String userId); + + Future storeUserProfile( + String userId, CachedProfileInformation profile); + + Future markUserProfileAsOutdated(String userId); + Future clearCache(); Future clear(); diff --git a/lib/src/database/hive_collections_database.dart b/lib/src/database/hive_collections_database.dart index aeb22590..c8454aa5 100644 --- a/lib/src/database/hive_collections_database.dart +++ b/lib/src/database/hive_collections_database.dart @@ -1597,6 +1597,22 @@ class HiveCollectionsDatabase extends DatabaseApi { @override Future delete() => _collection.deleteFromDisk(); + + @override + Future markUserProfileAsOutdated(userId) async { + return; + } + + @override + Future getUserProfile(String userId) async { + return null; + } + + @override + Future storeUserProfile( + String userId, CachedProfileInformation profile) async { + return; + } } class TupleKey { diff --git a/lib/src/database/hive_database.dart b/lib/src/database/hive_database.dart index ddf4f92e..caa1e4d7 100644 --- a/lib/src/database/hive_database.dart +++ b/lib/src/database/hive_database.dart @@ -1403,6 +1403,22 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin { @override Future delete() => Hive.deleteFromDisk(); + + @override + Future markUserProfileAsOutdated(userId) async { + return; + } + + @override + Future getUserProfile(String userId) async { + return null; + } + + @override + Future storeUserProfile( + String userId, CachedProfileInformation profile) async { + return; + } } dynamic _castValue(dynamic value) { diff --git a/lib/src/database/matrix_sdk_database.dart b/lib/src/database/matrix_sdk_database.dart index 1dd5a321..89675c70 100644 --- a/lib/src/database/matrix_sdk_database.dart +++ b/lib/src/database/matrix_sdk_database.dart @@ -103,6 +103,8 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage { late Box _seenDeviceKeysBox; + late Box _userProfilesBox; + @override final int maxFileSize; @@ -159,6 +161,8 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage { static const String _seenDeviceKeysBoxName = 'box_seen_device_keys'; + static const String _userProfilesBoxName = 'box_user_profiles'; + Database? database; /// Custom IdbFactory used to create the indexedDB. On IO platforms it would @@ -214,6 +218,7 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage { _eventsBoxName, _seenDeviceIdsBoxName, _seenDeviceKeysBoxName, + _userProfilesBoxName, }, sqfliteDatabase: database, sqfliteFactory: sqfliteFactory, @@ -283,6 +288,9 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage { _seenDeviceKeysBox = _collection.openBox( _seenDeviceKeysBoxName, ); + _userProfilesBox = _collection.openBox( + _userProfilesBoxName, + ); // Check version and check if we need a migration final currentVersion = int.tryParse(await _clientBox.get('version') ?? ''); @@ -340,6 +348,7 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage { await _timelineFragmentsBox.clear(); await _outboundGroupSessionsBox.clear(); await _presencesBox.clear(); + await _userProfilesBox.clear(); await _clientBox.delete('prev_batch'); }); @@ -1618,4 +1627,32 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage { name, sqfliteFactory ?? idbFactory, ); + + @override + Future markUserProfileAsOutdated(userId) async { + final profile = await getUserProfile(userId); + if (profile == null) return; + await _userProfilesBox.put( + userId, + CachedProfileInformation.fromProfile( + profile as ProfileInformation, + outdated: true, + updated: profile.updated, + ).toJson(), + ); + } + + @override + Future getUserProfile(String userId) => + _userProfilesBox.get(userId).then((json) => json == null + ? null + : CachedProfileInformation.fromJson(copyMap(json))); + + @override + Future storeUserProfile( + String userId, CachedProfileInformation profile) => + _userProfilesBox.put( + userId, + profile.toJson(), + ); } diff --git a/lib/src/room.dart b/lib/src/room.dart index e0453883..adf4875d 100644 --- a/lib/src/room.dart +++ b/lib/src/room.dart @@ -1717,10 +1717,10 @@ class Room { displayName: null )) { try { - final profile = await client.getProfileFromUserId(mxID); + final profile = await client.getUserProfile(mxID); foundUser = User( mxID, - displayName: profile.displayName, + displayName: profile.displayname, avatarUrl: profile.avatarUrl?.toString(), membership: foundUser?.membership.name ?? Membership.leave.name, room: this, diff --git a/lib/src/utils/cached_profile_information.dart b/lib/src/utils/cached_profile_information.dart new file mode 100644 index 00000000..3cca8e5b --- /dev/null +++ b/lib/src/utils/cached_profile_information.dart @@ -0,0 +1,29 @@ +import 'package:matrix/matrix_api_lite.dart'; + +class CachedProfileInformation extends ProfileInformation { + final bool outdated; + final DateTime updated; + + CachedProfileInformation.fromProfile( + ProfileInformation profile, { + required this.outdated, + required this.updated, + }) : super( + avatarUrl: profile.avatarUrl, + displayname: profile.displayname, + ); + + factory CachedProfileInformation.fromJson(Map json) => + CachedProfileInformation.fromProfile( + ProfileInformation.fromJson(json), + outdated: json['outdated'] as bool, + updated: DateTime.fromMillisecondsSinceEpoch(json['updated'] as int), + ); + + @override + Map toJson() => { + ...super.toJson(), + 'outdated': outdated, + 'updated': updated.millisecondsSinceEpoch, + }; +} diff --git a/test/client_test.dart b/test/client_test.dart index 80950b5c..0aaa7e5f 100644 --- a/test/client_test.dart +++ b/test/client_test.dart @@ -623,15 +623,39 @@ void main() { }); test('getProfileFromUserId', () async { - final profile = await matrix.getProfileFromUserId('@getme:example.com', - getFromRooms: false); + final profile = await matrix.getUserProfile('@getme:example.com'); + expect(profile.outdated, false); expect(profile.avatarUrl.toString(), 'mxc://test'); - expect(profile.displayName, 'You got me'); - final aliceProfile = - await matrix.getProfileFromUserId('@alice:example.com'); - expect(aliceProfile.avatarUrl.toString(), - 'mxc://example.org/SEsfnsuifSDFSSEF'); - expect(aliceProfile.displayName, 'Alice Margatroid'); + expect(profile.displayname, 'You got me'); + final aliceProfile = await matrix.getUserProfile('@alice:example.com'); + expect(profile.outdated, false); + expect(aliceProfile.avatarUrl.toString(), 'mxc://test'); + expect(aliceProfile.displayname, 'Alice M'); + await matrix.handleSync( + SyncUpdate( + nextBatch: '', + rooms: RoomsUpdate( + join: { + matrix.rooms.first.id: JoinedRoomUpdate( + timeline: TimelineUpdate(events: [ + MatrixEvent( + eventId: 'abcd', + type: EventTypes.RoomMember, + content: {'membership': 'join'}, + senderId: '@alice:example.com', + stateKey: '@alice:example.com', + originServerTs: DateTime.now(), + ), + ]), + ), + }, + ), + ), + ); + expect(matrix.onUserProfileUpdate.value, '@alice:example.com'); + final cachedProfileFromDb = + await matrix.database?.getUserProfile('@alice:example.com'); + expect(cachedProfileFromDb?.outdated, true); }); test('joinAfterInviteMembership', () async { final client = await getClient(); @@ -666,8 +690,9 @@ void main() { await client.database?.clearCache(); await client.handleSync(SyncUpdate.fromJson(jsonDecode( '{"next_batch":"s82_571_2_6_39_1_2_34_1","account_data":{"events":[{"type":"m.push_rules","content":{"global":{"underride":[{"conditions":[{"kind":"event_match","key":"type","pattern":"m.call.invite"}],"actions":["notify",{"set_tweak":"sound","value":"ring"},{"set_tweak":"highlight","value":false}],"rule_id":".m.rule.call","default":true,"enabled":true},{"conditions":[{"kind":"room_member_count","is":"2"},{"kind":"event_match","key":"type","pattern":"m.room.message"}],"actions":["notify",{"set_tweak":"sound","value":"default"},{"set_tweak":"highlight","value":false}],"rule_id":".m.rule.room_one_to_one","default":true,"enabled":true},{"conditions":[{"kind":"room_member_count","is":"2"},{"kind":"event_match","key":"type","pattern":"m.room.encrypted"}],"actions":["notify",{"set_tweak":"sound","value":"default"},{"set_tweak":"highlight","value":false}],"rule_id":".m.rule.encrypted_room_one_to_one","default":true,"enabled":true},{"conditions":[{"kind":"event_match","key":"type","pattern":"m.room.message"}],"actions":["notify",{"set_tweak":"highlight","value":false}],"rule_id":".m.rule.message","default":true,"enabled":true},{"conditions":[{"kind":"event_match","key":"type","pattern":"m.room.encrypted"}],"actions":["notify",{"set_tweak":"highlight","value":false}],"rule_id":".m.rule.encrypted","default":true,"enabled":true},{"conditions":[{"kind":"event_match","key":"type","pattern":"im.vector.modular.widgets"},{"kind":"event_match","key":"content.type","pattern":"jitsi"},{"kind":"event_match","key":"state_key","pattern":"*"}],"actions":["notify",{"set_tweak":"highlight","value":false}],"rule_id":".im.vector.jitsi","default":true,"enabled":true}],"sender":[],"room":[],"content":[{"actions":["notify",{"set_tweak":"sound","value":"default"},{"set_tweak":"highlight"}],"pattern":"056d6976-fb61-47cf-86f0-147387461565","rule_id":".m.rule.contains_user_name","default":true,"enabled":true}],"override":[{"conditions":[],"actions":["dont_notify"],"rule_id":".m.rule.master","default":true,"enabled":false},{"conditions":[{"kind":"event_match","key":"content.msgtype","pattern":"m.notice"}],"actions":["dont_notify"],"rule_id":".m.rule.suppress_notices","default":true,"enabled":true},{"conditions":[{"kind":"event_match","key":"type","pattern":"m.room.member"},{"kind":"event_match","key":"content.membership","pattern":"invite"},{"kind":"event_match","key":"state_key","pattern":"@056d6976-fb61-47cf-86f0-147387461565:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de"}],"actions":["notify",{"set_tweak":"sound","value":"default"},{"set_tweak":"highlight","value":false}],"rule_id":".m.rule.invite_for_me","default":true,"enabled":true},{"conditions":[{"kind":"event_match","key":"type","pattern":"m.room.member"}],"actions":["dont_notify"],"rule_id":".m.rule.member_event","default":true,"enabled":true},{"conditions":[{"kind":"contains_display_name"}],"actions":["notify",{"set_tweak":"sound","value":"default"},{"set_tweak":"highlight"}],"rule_id":".m.rule.contains_display_name","default":true,"enabled":true},{"conditions":[{"kind":"event_match","key":"content.body","pattern":"@room"},{"kind":"sender_notification_permission","key":"room"}],"actions":["notify",{"set_tweak":"highlight","value":true}],"rule_id":".m.rule.roomnotif","default":true,"enabled":true},{"conditions":[{"kind":"event_match","key":"type","pattern":"m.room.tombstone"},{"kind":"event_match","key":"state_key","pattern":""}],"actions":["notify",{"set_tweak":"highlight","value":true}],"rule_id":".m.rule.tombstone","default":true,"enabled":true},{"conditions":[{"kind":"event_match","key":"type","pattern":"m.reaction"}],"actions":["dont_notify"],"rule_id":".m.rule.reaction","default":true,"enabled":true}]},"device":{}}}]},"presence":{"events":[{"type":"m.presence","sender":"@056d6976-fb61-47cf-86f0-147387461565:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de","content":{"presence":"online","last_active_ago":43,"currently_active":true}}]},"device_one_time_keys_count":{"signed_curve25519":66},"org.matrix.msc2732.device_unused_fallback_key_types":["signed_curve25519"],"device_unused_fallback_key_types":["signed_curve25519"],"rooms":{"join":{"!MEgZosbiZqjSjbHFqI:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de":{"timeline":{"events":[{"type":"m.room.member","sender":"@8640f1e6-a824-4f9c-9924-2d8fc40bc030:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de","content":{"membership":"join","displayname":"Lars Kaiser"},"state_key":"@8640f1e6-a824-4f9c-9924-2d8fc40bc030:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de","origin_server_ts":1647296944593,"unsigned":{"age":545455},"event_id":"\$mk9kFUEAKBZJgarWApLyYqOZQQocLIVV8tWp_gJEZFU"},{"type":"m.room.power_levels","sender":"@8640f1e6-a824-4f9c-9924-2d8fc40bc030:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de","content":{"users":{"@8640f1e6-a824-4f9c-9924-2d8fc40bc030:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de":100},"users_default":0,"events":{"m.room.name":50,"m.room.power_levels":100,"m.room.history_visibility":100,"m.room.canonical_alias":50,"m.room.avatar":50,"m.room.tombstone":100,"m.room.server_acl":100,"m.room.encryption":100},"events_default":0,"state_default":50,"ban":50,"kick":50,"redact":50,"invite":50,"historical":100},"state_key":"","origin_server_ts":1647296944690,"unsigned":{"age":545358},"event_id":"\$3wL2YgVNQzgfl8y_ksi3BPMqRs94jb_m0WRonL1HNpY"},{"type":"m.room.canonical_alias","sender":"@8640f1e6-a824-4f9c-9924-2d8fc40bc030:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de","content":{"alias":"#user-discovery:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de"},"state_key":"","origin_server_ts":1647296944806,"unsigned":{"age":545242},"event_id":"\$yXaVETL9F4jSN9rpRNyT_kUoctzD07n5Z4AIHziP7DQ"},{"type":"m.room.join_rules","sender":"@8640f1e6-a824-4f9c-9924-2d8fc40bc030:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de","content":{"join_rule":"public"},"state_key":"","origin_server_ts":1647296944894,"unsigned":{"age":545154},"event_id":"\$jBDHhgpNqr125eWUsGVw4r7ZG2hgr0BTzzR77S-ubvY"},{"type":"m.room.history_visibility","sender":"@8640f1e6-a824-4f9c-9924-2d8fc40bc030:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de","content":{"history_visibility":"shared"},"state_key":"","origin_server_ts":1647296944965,"unsigned":{"age":545083},"event_id":"\$kMessP7gAphUKW7mzOLlJT6NT8IsVGPmGir3_1uBNCE"},{"type":"m.room.name","sender":"@8640f1e6-a824-4f9c-9924-2d8fc40bc030:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de","content":{"name":"User Discovery"},"state_key":"","origin_server_ts":1647296945062,"unsigned":{"age":544986},"event_id":"\$Bo9Ut_0vcr3FuxCRye4IHEMxUxIIcSwc-ePnMzx-hYU"},{"type":"m.room.member","sender":"@test:fakeServer.notExisting","content":{"membership":"join","displayname":"1c2e5c2b-f958-45a5-9fcb-eef3969c31df"},"state_key":"@test:fakeServer.notExisting","origin_server_ts":1647296989893,"unsigned":{"age":500155},"event_id":"\$fYCf2qtlHwzcdLgwjHb2EOdStv3isAlIUy2Esh5qfVE"},{"type":"m.room.member","sender":"@test:fakeServer.notExisting","content":{"membership":"join","displayname":"Some First Name Some Last Name"},"state_key":"@test:fakeServer.notExisting","origin_server_ts":1647296990076,"unsigned":{"replaces_state":"\$fYCf2qtlHwzcdLgwjHb2EOdStv3isAlIUy2Esh5qfVE","prev_content":{"membership":"join","displayname":"1c2e5c2b-f958-45a5-9fcb-eef3969c31df"},"prev_sender":"@test:fakeServer.notExisting","age":499972},"event_id":"\$3Ut97nFBgOtsrnRPW-pqr28z7ETNMttj7GcjkIv4zWw"},{"type":"m.room.member","sender":"@056d6976-fb61-47cf-86f0-147387461565:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de","content":{"membership":"join","displayname":"056d6976-fb61-47cf-86f0-147387461565"},"state_key":"@056d6976-fb61-47cf-86f0-147387461565:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de","origin_server_ts":1647297489154,"unsigned":{"age":894},"event_id":"\$6EsjHSLQDVDW9WDH1c5Eu57VaPGZmOPtNRjCjtWPLV0"},{"type":"m.room.member","sender":"@056d6976-fb61-47cf-86f0-147387461565:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de","content":{"membership":"join","displayname":"Another User"},"state_key":"@056d6976-fb61-47cf-86f0-147387461565:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de","origin_server_ts":1647297489290,"unsigned":{"replaces_state":"\$6EsjHSLQDVDW9WDH1c5Eu57VaPGZmOPtNRjCjtWPLV0","prev_content":{"membership":"join","displayname":"056d6976-fb61-47cf-86f0-147387461565"},"prev_sender":"@056d6976-fb61-47cf-86f0-147387461565:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de","age":758},"event_id":"\$dtQblqCbjr3TGc3WmrQ4YTkHaXJ2PcO0TAYDr9K7iQc"}],"prev_batch":"t2-62_571_2_6_39_1_2_34_1","limited":true},"state":{"events":[{"type":"m.room.create","sender":"@8640f1e6-a824-4f9c-9924-2d8fc40bc030:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de","content":{"m.federate":false,"room_version":"9","creator":"@8640f1e6-a824-4f9c-9924-2d8fc40bc030:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de"},"state_key":"","origin_server_ts":1647296944511,"unsigned":{"age":545537},"event_id":"\$PAWKKULBVOLnqfrAAtXZz8tHEPXXjgRVbJJLifwQWbE"}]},"account_data":{"events":[]},"ephemeral":{"events":[]},"unread_notifications":{"notification_count":0,"highlight_count":0},"summary":{"m.joined_member_count":3,"m.invited_member_count":0},"org.matrix.msc2654.unread_count":0}}}}'))); - final profile = await client.fetchOwnProfile(); - expect(profile.displayName, 'Some First Name Some Last Name'); + final profile = await client.getUserProfile(client.userID!); + expect(profile.displayname, 'Some First Name Some Last Name'); + expect(profile.outdated, false); await client.dispose(closeDatabase: true); }); test('sendToDeviceEncrypted', () async { diff --git a/test/database_api_test.dart b/test/database_api_test.dart index fe402e74..0cfc44e6 100644 --- a/test/database_api_test.dart +++ b/test/database_api_test.dart @@ -616,6 +616,38 @@ void main() { storedPresence?.toJson(), ); }); + test( + 'storeUserProfile', + () async { + final profile1 = await database.getUserProfile('@alice:example.com'); + expect(profile1, null); + + await database.storeUserProfile( + '@alice:example.com', + CachedProfileInformation.fromProfile( + ProfileInformation( + avatarUrl: Uri.parse('mxc://test'), displayname: 'Alice M'), + outdated: false, + updated: DateTime.now(), + ), + ); + // ignore: deprecated_member_use_from_same_package + if (database is! HiveCollectionsDatabase && + // ignore: deprecated_member_use_from_same_package + database is! FamedlySdkHiveDatabase) { + final profile2 = + await database.getUserProfile('@alice:example.com'); + expect(profile2?.displayname, 'Alice M'); + expect(profile2?.outdated, false); + await database.markUserProfileAsOutdated('@alice:example.com'); + + final profile3 = + await database.getUserProfile('@alice:example.com'); + expect(profile3?.displayname, 'Alice M'); + expect(profile3?.outdated, true); + } + }, + ); // Clearing up from here test('clearSSSSCache', () async { From e7802bd20bdc783eb5bc9178a1fa8b5518e535f2 Mon Sep 17 00:00:00 2001 From: Krille Date: Tue, 23 Jul 2024 09:41:06 +0200 Subject: [PATCH 2/2] fix: Synapse CI job failing because invite state not completely synced --- lib/src/client.dart | 79 +++++++++++---------------------- test/client_test.dart | 13 ++++-- test_driver/matrixsdk_test.dart | 3 ++ 3 files changed, 37 insertions(+), 58 deletions(-) diff --git a/lib/src/client.dart b/lib/src/client.dart index 2cf69f80..6a43a20b 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -919,7 +919,7 @@ class Client extends MatrixApi { /// Tries to get the profile from homeserver first, if failed, falls back to a profile /// from a room where the user exists. Set `useServerCache` to true to get any /// prior value from this function - @Deprecated('Use getUserProfile() instead') + @Deprecated('Use fetchOwnProfile() instead') Future fetchOwnProfileFromServer( {bool useServerCache = false}) async { try { @@ -943,19 +943,11 @@ class Client extends MatrixApi { /// one user can have different displaynames and avatar urls in different rooms. /// This returns the profile from the first room by default, override `getFromRooms` /// to false to fetch from homeserver. - @Deprecated('User `getUserProfile(userID)` instead') Future fetchOwnProfile({ - bool getFromRooms = true, - bool cache = true, + @Deprecated('No longer supported') bool getFromRooms = true, + @Deprecated('No longer supported') bool cache = true, }) => - getProfileFromUserId( - userID!, - getFromRooms: getFromRooms, - cache: cache, - ); - - final Map _profileRoomsCache = {}; - final Map _profileServerCache = {}; + getProfileFromUserId(userID!); /// Get the combined profile information for this user. First checks for a /// non outdated cached profile before requesting from the server. Cached @@ -1006,53 +998,32 @@ class Client extends MatrixApi { final CachedStreamController onUserProfileUpdate = CachedStreamController(); - /// Get the combined profile information for this user. - /// If [getFromRooms] is true then the profile will first be searched from the - /// room memberships. This is unstable if the given user makes use of different displaynames - /// and avatars per room, which is common for some bots and bridges. - /// If [cache] is true then - /// the profile get cached for this session. Please note that then the profile may - /// become outdated if the user changes the displayname or avatar in this session. - @Deprecated('User `getUserProfile(userID)` instead') - Future getProfileFromUserId(String userId, - {bool cache = true, bool getFromRooms = true}) async { - var profile = - getFromRooms ? _profileRoomsCache[userId] : _profileServerCache[userId]; - if (cache && profile != null) { - return Profile( - userId: userId, - displayName: profile.displayname, - avatarUrl: profile.avatarUrl, + /// Get the combined profile information for this user from the server or + /// from the cache depending on the cache value. Returns a `Profile` object + /// including the given userId but without information about how outdated + /// the profile is. If you need those, try using `getUserProfile()` instead. + Future getProfileFromUserId( + String userId, { + @Deprecated('No longer supported') bool? getFromRooms, + @Deprecated('No longer supported') bool? cache, + Duration timeout = const Duration(seconds: 30), + Duration maxCacheAge = const Duration(days: 1), + }) async { + CachedProfileInformation? cachedProfileInformation; + try { + cachedProfileInformation = await getUserProfile( + userId, + timeout: timeout, + maxCacheAge: maxCacheAge, ); + } catch (e) { + Logs().d('Unable to fetch profile for $userId', e); } - if (getFromRooms) { - final room = rooms.firstWhereOrNull((Room room) => - room.getParticipants().indexWhere((User user) => user.id == userId) != - -1); - if (room != null) { - final user = - room.getParticipants().firstWhere((User user) => user.id == userId); - final profileFromRooms = Profile( - userId: userId, - displayName: user.displayName, - avatarUrl: user.avatarUrl, - ); - _profileRoomsCache[userId] = ProfileInformation( - avatarUrl: profileFromRooms.avatarUrl, - displayname: profileFromRooms.displayName, - ); - return profileFromRooms; - } - } - profile = await super.getUserProfile(userId); - if (cache || _profileServerCache.containsKey(userId)) { - _profileServerCache[userId] = profile; - } return Profile( userId: userId, - displayName: profile.displayname, - avatarUrl: profile.avatarUrl, + displayName: cachedProfileInformation?.displayname, + avatarUrl: cachedProfileInformation?.avatarUrl, ); } diff --git a/test/client_test.dart b/test/client_test.dart index 0aaa7e5f..1ac4f568 100644 --- a/test/client_test.dart +++ b/test/client_test.dart @@ -623,12 +623,17 @@ void main() { }); test('getProfileFromUserId', () async { - final profile = await matrix.getUserProfile('@getme:example.com'); - expect(profile.outdated, false); + final cachedProfile = await matrix.getUserProfile('@getme:example.com'); + expect(cachedProfile.outdated, false); + expect(cachedProfile.avatarUrl.toString(), 'mxc://test'); + expect(cachedProfile.displayname, 'You got me'); + + final profile = await matrix.getProfileFromUserId('@getme:example.com'); expect(profile.avatarUrl.toString(), 'mxc://test'); - expect(profile.displayname, 'You got me'); + expect(profile.displayName, 'You got me'); + final aliceProfile = await matrix.getUserProfile('@alice:example.com'); - expect(profile.outdated, false); + expect(aliceProfile.outdated, false); expect(aliceProfile.avatarUrl.toString(), 'mxc://test'); expect(aliceProfile.displayname, 'Alice M'); await matrix.handleSync( diff --git a/test_driver/matrixsdk_test.dart b/test_driver/matrixsdk_test.dart index 1ab27024..a70c38c3 100644 --- a/test_driver/matrixsdk_test.dart +++ b/test_driver/matrixsdk_test.dart @@ -490,6 +490,9 @@ void main() => group('Integration tests', () { if (testClientB.getRoomById(dmRoom) == null) { await testClientB.waitForRoomInSync(dmRoom, invite: true); } + // Wait at least for one additional sync to make sure the invite landed + // correctly. Workaround for synapse CI job failing. + await testClientB.onSync.stream.first; Logs().i('++++ (Bob) Create DM ++++'); final dmRoomFromB =