refactor: Cache profiles in database and refactor API

This commit is contained in:
Krille 2024-07-22 13:16:53 +02:00
parent 191397ebb8
commit cabf357cf7
No known key found for this signature in database
GPG Key ID: E067ECD60F1A0652
11 changed files with 245 additions and 17 deletions

View File

@ -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';

View File

@ -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

View File

@ -911,7 +911,7 @@ class Client extends MatrixApi {
return id;
}
@Deprecated('Use fetchOwnProfile() instead')
@Deprecated('Use getUserProfile(userID) instead')
Future<Profile> 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<Profile> 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<Profile> fetchOwnProfile({
bool getFromRooms = true,
bool cache = true,
@ -955,6 +957,55 @@ class Client extends MatrixApi {
final Map<String, ProfileInformation> _profileRoomsCache = {};
final Map<String, ProfileInformation> _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<CachedProfileInformation> 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<String, Future<ProfileInformation>> _userProfileRequests = {};
final CachedStreamController<String> onUserProfileUpdate =
CachedStreamController<String>();
/// 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<Profile> 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 &&

View File

@ -81,6 +81,13 @@ abstract class DatabaseApi {
Future<void> forgetRoom(String roomId);
Future<CachedProfileInformation?> getUserProfile(String userId);
Future<void> storeUserProfile(
String userId, CachedProfileInformation profile);
Future<void> markUserProfileAsOutdated(String userId);
Future<void> clearCache();
Future<void> clear();

View File

@ -1597,6 +1597,22 @@ class HiveCollectionsDatabase extends DatabaseApi {
@override
Future<void> delete() => _collection.deleteFromDisk();
@override
Future<void> markUserProfileAsOutdated(userId) async {
return;
}
@override
Future<CachedProfileInformation?> getUserProfile(String userId) async {
return null;
}
@override
Future<void> storeUserProfile(
String userId, CachedProfileInformation profile) async {
return;
}
}
class TupleKey {

View File

@ -1403,6 +1403,22 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
@override
Future<void> delete() => Hive.deleteFromDisk();
@override
Future<void> markUserProfileAsOutdated(userId) async {
return;
}
@override
Future<CachedProfileInformation?> getUserProfile(String userId) async {
return null;
}
@override
Future<void> storeUserProfile(
String userId, CachedProfileInformation profile) async {
return;
}
}
dynamic _castValue(dynamic value) {

View File

@ -103,6 +103,8 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
late Box<String> _seenDeviceKeysBox;
late Box<Map> _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<void> 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<CachedProfileInformation?> getUserProfile(String userId) =>
_userProfilesBox.get(userId).then((json) => json == null
? null
: CachedProfileInformation.fromJson(copyMap(json)));
@override
Future<void> storeUserProfile(
String userId, CachedProfileInformation profile) =>
_userProfilesBox.put(
userId,
profile.toJson(),
);
}

View File

@ -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,

View File

@ -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<String, Object?> json) =>
CachedProfileInformation.fromProfile(
ProfileInformation.fromJson(json),
outdated: json['outdated'] as bool,
updated: DateTime.fromMillisecondsSinceEpoch(json['updated'] as int),
);
@override
Map<String, Object?> toJson() => {
...super.toJson(),
'outdated': outdated,
'updated': updated.millisecondsSinceEpoch,
};
}

File diff suppressed because one or more lines are too long

View File

@ -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 {