From 3fba9a55b3a8f68b4ecfeaf3cf0f626da3efcb9f Mon Sep 17 00:00:00 2001 From: td Date: Tue, 20 Feb 2024 06:54:11 +0530 Subject: [PATCH 01/10] chore: remove state events both in imp and preview events list --- lib/src/client.dart | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/src/client.dart b/lib/src/client.dart index e2650f7e..2cd9d9a2 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -161,6 +161,12 @@ class Client extends MatrixApi { Set? verificationMethods, http.Client? httpClient, Set? importantStateEvents, + + /// You probably don't want to add state events which are also + /// in important state events to this list, or get ready to face + /// only having one event of that particular type in preLoad because + /// previewEvents are stored with stateKey '' not the actual state key + /// of your state event Set? roomPreviewLastEvents, this.pinUnreadRooms = false, this.pinInvitedRooms = true, @@ -216,8 +222,12 @@ class Client extends MatrixApi { EventTypes.CallAnswer, EventTypes.CallReject, EventTypes.CallHangup, - EventTypes.GroupCallPrefix, - EventTypes.GroupCallMemberPrefix, + + /// hack because having them both in important events and roomPreivew + /// makes the statekey '' which means you can only have one event of that + /// type + // EventTypes.GroupCallPrefix, + // EventTypes.GroupCallMemberPrefix, ]); // register all the default commands From 8d9591008628cd4e6158097a18fa630fe2264e0e Mon Sep 17 00:00:00 2001 From: Krille Date: Mon, 19 Feb 2024 08:00:55 +0100 Subject: [PATCH 02/10] chore: Add codeowners --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..f3d7629e --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @famedly/instant-messaging \ No newline at end of file From b82d8bff0b576d657805abc5ff7e045b43938bc3 Mon Sep 17 00:00:00 2001 From: Karthikeyan S Date: Thu, 22 Feb 2024 15:22:10 +0530 Subject: [PATCH 03/10] feat: specify history_visibility when creating group chat --- lib/src/client.dart | 12 ++++++++++++ lib/src/room.dart | 12 ++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/lib/src/client.dart b/lib/src/client.dart index 2cd9d9a2..efecef71 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -705,6 +705,7 @@ class Client extends MatrixApi { CreateRoomPreset preset = CreateRoomPreset.privateChat, List? initialState, Visibility? visibility, + HistoryVisibility? historyVisibility, bool waitForSync = true, bool groupCall = false, Map? powerLevelContentOverride, @@ -722,6 +723,17 @@ class Client extends MatrixApi { )); } } + if (historyVisibility != null) { + initialState ??= []; + if (!initialState.any((s) => s.type == EventTypes.HistoryVisibility)) { + initialState.add(StateEvent( + content: { + 'history_visibility': historyVisibility.text, + }, + type: EventTypes.HistoryVisibility, + )); + } + } if (groupCall) { powerLevelContentOverride ??= {}; powerLevelContentOverride['events'] = { diff --git a/lib/src/room.dart b/lib/src/room.dart index c4fffd7c..9a067fbf 100644 --- a/lib/src/room.dart +++ b/lib/src/room.dart @@ -45,6 +45,10 @@ const Map _guestAccessMap = { GuestAccess.forbidden: 'forbidden', }; +extension GuestAccessExtension on GuestAccess { + String get text => _guestAccessMap[this]!; +} + const Map _historyVisibilityMap = { HistoryVisibility.invited: 'invited', HistoryVisibility.joined: 'joined', @@ -52,6 +56,10 @@ const Map _historyVisibilityMap = { HistoryVisibility.worldReadable: 'world_readable', }; +extension HistoryVisibilityExtension on HistoryVisibility { + String get text => _historyVisibilityMap[this]!; +} + const String messageSendingStatusKey = 'com.famedly.famedlysdk.message_sending_status'; @@ -2138,7 +2146,7 @@ class Room { EventTypes.GuestAccess, '', { - 'guest_access': _guestAccessMap[guestAccess], + 'guest_access': guestAccess.text, }, ); return; @@ -2163,7 +2171,7 @@ class Room { EventTypes.HistoryVisibility, '', { - 'history_visibility': _historyVisibilityMap[historyVisibility], + 'history_visibility': historyVisibility.text, }, ); return; From a6479408e456145dfd26e24f1884abd69c0336a8 Mon Sep 17 00:00:00 2001 From: Karthikeyan S Date: Fri, 23 Feb 2024 12:11:36 +0530 Subject: [PATCH 04/10] build: Bump version to 0.25.10 --- CHANGELOG.md | 4 ++++ pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3cd5fa3..89674651 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## [0.25.10] 23rd February 2024 +- chore: remove state events both in imp and preview events list (td) +- feat: specify history_visibility when creating group chat (Karthikeyan S) + ## [0.25.9] 14th February 2024 - fix: group calls terminator having sync glares (td) - fix: ignore expired calls rather than killing them (td) diff --git a/pubspec.yaml b/pubspec.yaml index 11b7b2cc..c5db59c8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: matrix description: Matrix Dart SDK -version: 0.25.9 +version: 0.25.10 homepage: https://famedly.com repository: https://github.com/famedly/matrix-dart-sdk.git issue_tracker: https://github.com/famedly/matrix-dart-sdk/issues From 4a4eb53dad4f82d46aa4266c269e6a8f74efdfef Mon Sep 17 00:00:00 2001 From: Gabby Gurdin Date: Thu, 22 Feb 2024 14:28:28 -0500 Subject: [PATCH 05/10] fix: convert boxNames to List in clear function when creating transaction --- lib/src/database/indexeddb_box.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/database/indexeddb_box.dart b/lib/src/database/indexeddb_box.dart index b0803b3d..fe869f15 100644 --- a/lib/src/database/indexeddb_box.dart +++ b/lib/src/database/indexeddb_box.dart @@ -69,7 +69,7 @@ class BoxCollection with ZoneTransactionMixin { }); Future clear() async { - final txn = _db.transaction(boxNames, 'readwrite'); + final txn = _db.transaction(boxNames.toList(), 'readwrite'); for (final name in boxNames) { unawaited(txn.objectStore(name).clear()); } From 38b1eb75e5708159bb22afbd4cea518f4d415315 Mon Sep 17 00:00:00 2001 From: Krille Date: Fri, 16 Feb 2024 09:16:51 +0100 Subject: [PATCH 06/10] feat: Implement handling soft logout --- lib/src/client.dart | 66 ++++++++++++++++++- lib/src/database/database_api.dart | 2 + .../database/hive_collections_database.dart | 12 ++++ lib/src/database/hive_database.dart | 4 ++ lib/src/database/matrix_sdk_database.dart | 12 ++++ test/client_test.dart | 29 ++++++++ test/database_api_test.dart | 2 + test/fake_client.dart | 2 + test/fake_matrix_api.dart | 25 +++++++ test/matrix_database_test.dart | 1 + 10 files changed, 153 insertions(+), 2 deletions(-) diff --git a/lib/src/client.dart b/lib/src/client.dart index efecef71..a8347ddc 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -89,6 +89,8 @@ class Client extends MatrixApi { bool shareKeysWithUnverifiedDevices; + Future Function(Client client)? onSoftLogout; + // For CommandsClientExtension final Map Function(CommandArgs)> commands = {}; final Filter syncFilter; @@ -184,6 +186,13 @@ class Client extends MatrixApi { this.shareKeysWithUnverifiedDevices = true, this.enableDehydratedDevices = false, this.receiptsPublicByDefault = true, + + /// Implement your https://spec.matrix.org/v1.9/client-server-api/#soft-logout + /// logic here. + /// Set this to `refreshAccessToken()` for the easiest way to handle the + /// most common reason for soft logouts. + /// You can also perform a new login here by passing the existing deviceId. + this.onSoftLogout, }) : syncFilter = syncFilter ?? Filter( room: RoomFilter( @@ -234,6 +243,40 @@ class Client extends MatrixApi { registerDefaultCommands(); } + /// Fetches the refreshToken from the database and tries to get a new + /// access token from the server and then stores it correctly. Unlike the + /// pure API call of `Client.refresh()` this handles the complete soft + /// logout case. + /// Throws an Exception if there is no refresh token available or the + /// client is not logged in. + Future refreshAccessToken() async { + final storedClient = await database?.getClient(clientName); + final refreshToken = storedClient?.tryGet('refresh_token'); + if (refreshToken == null) { + throw Exception('No refresh token available'); + } + final homeserverUrl = homeserver?.toString(); + final userId = userID; + final deviceId = deviceID; + if (homeserverUrl == null || userId == null || deviceId == null) { + throw Exception('Cannot refresh access token when not logged in'); + } + + final tokenResponse = await refresh(refreshToken); + + accessToken = tokenResponse.accessToken; + await database?.updateClient( + homeserverUrl, + tokenResponse.accessToken, + tokenResponse.refreshToken, + userId, + deviceId, + deviceName, + prevBatch, + encryption?.pickledOlmAccount, + ); + } + /// The required name for this client. final String clientName; @@ -485,6 +528,7 @@ class Client extends MatrixApi { deviceId: deviceId, initialDeviceDisplayName: initialDeviceDisplayName, inhibitLogin: inhibitLogin, + refreshToken: refreshToken ?? onSoftLogout != null, ); // Connect if there is an access token in the response. @@ -498,6 +542,7 @@ class Client extends MatrixApi { } await init( newToken: accessToken, + newRefreshToken: response.refreshToken, newUserID: userId, newHomeserver: homeserver, newDeviceName: initialDeviceDisplayName ?? '', @@ -548,6 +593,7 @@ class Client extends MatrixApi { medium: medium, // ignore: deprecated_member_use address: address, + refreshToken: refreshToken ?? onSoftLogout != null, ); // Connect if there is an access token in the response. @@ -560,6 +606,7 @@ class Client extends MatrixApi { } await init( newToken: accessToken, + newRefreshToken: response.refreshToken, newUserID: userId, newHomeserver: homeserver_, newDeviceName: initialDeviceDisplayName ?? '', @@ -1474,6 +1521,7 @@ class Client extends MatrixApi { /// `userDeviceKeysLoading` where it is necessary. Future init({ String? newToken, + String? newRefreshToken, Uri? newHomeserver, String? newUserID, String? newDeviceName, @@ -1587,6 +1635,7 @@ class Client extends MatrixApi { await database.updateClient( homeserver.toString(), accessToken, + newRefreshToken, userID, _deviceID, _deviceName, @@ -1598,6 +1647,7 @@ class Client extends MatrixApi { clientName, homeserver.toString(), accessToken, + newRefreshToken, userID, _deviceID, _deviceName, @@ -1822,8 +1872,19 @@ class Client extends MatrixApi { onSyncStatus.add(SyncStatusUpdate(SyncStatus.error, error: SdkError(exception: e, stackTrace: s))); if (e.error == MatrixError.M_UNKNOWN_TOKEN) { - Logs().w('The user has been logged out!'); - await clear(); + final onSoftLogout = this.onSoftLogout; + if (e.raw.tryGet('soft_logout') == true && onSoftLogout != null) { + Logs().w('The user has been soft logged out! Try to login again...'); + try { + await onSoftLogout(this); + } catch (e, s) { + Logs().e('Unable to login again', e, s); + await clear(); + } + } else { + Logs().w('The user has been logged out!'); + await clear(); + } } } on SyncConnectionException catch (e, s) { Logs().w('Syncloop failed: Client has not connection to the server'); @@ -3112,6 +3173,7 @@ class Client extends MatrixApi { clientName, migrateClient['homeserver_url'], migrateClient['token'], + migrateClient['refresh_token'], migrateClient['user_id'], migrateClient['device_id'], migrateClient['device_name'], diff --git a/lib/src/database/database_api.dart b/lib/src/database/database_api.dart index 2c41c7d4..f6d15054 100644 --- a/lib/src/database/database_api.dart +++ b/lib/src/database/database_api.dart @@ -33,6 +33,7 @@ abstract class DatabaseApi { Future updateClient( String homeserverUrl, String token, + String? refreshToken, String userId, String? deviceId, String? deviceName, @@ -44,6 +45,7 @@ abstract class DatabaseApi { String name, String homeserverUrl, String token, + String? refreshToken, String userId, String? deviceId, String? deviceName, diff --git a/lib/src/database/hive_collections_database.dart b/lib/src/database/hive_collections_database.dart index ad8073a2..9582f40a 100644 --- a/lib/src/database/hive_collections_database.dart +++ b/lib/src/database/hive_collections_database.dart @@ -785,6 +785,7 @@ class HiveCollectionsDatabase extends DatabaseApi { String name, String homeserverUrl, String token, + String? refreshToken, String userId, String? deviceId, String? deviceName, @@ -794,6 +795,11 @@ class HiveCollectionsDatabase extends DatabaseApi { await _clientBox.put('homeserver_url', homeserverUrl); await _clientBox.put('token', token); await _clientBox.put('user_id', userId); + if (refreshToken == null) { + await _clientBox.delete('refresh_token'); + } else { + await _clientBox.put('refresh_token', refreshToken); + } if (deviceId == null) { await _clientBox.delete('device_id'); } else { @@ -1371,6 +1377,7 @@ class HiveCollectionsDatabase extends DatabaseApi { Future updateClient( String homeserverUrl, String token, + String? refreshToken, String userId, String? deviceId, String? deviceName, @@ -1380,6 +1387,11 @@ class HiveCollectionsDatabase extends DatabaseApi { await transaction(() async { await _clientBox.put('homeserver_url', homeserverUrl); await _clientBox.put('token', token); + if (refreshToken == null) { + await _clientBox.delete('refresh_token'); + } else { + await _clientBox.put('refresh_token', refreshToken); + } await _clientBox.put('user_id', userId); if (deviceId == null) { await _clientBox.delete('device_id'); diff --git a/lib/src/database/hive_database.dart b/lib/src/database/hive_database.dart index 8aa3706e..89576c7e 100644 --- a/lib/src/database/hive_database.dart +++ b/lib/src/database/hive_database.dart @@ -750,6 +750,7 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin { String name, String homeserverUrl, String token, + String? refreshToken, String userId, String? deviceId, String? deviceName, @@ -757,6 +758,7 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin { String? olmAccount) async { await _clientBox.put('homeserver_url', homeserverUrl); await _clientBox.put('token', token); + await _clientBox.put('refresh_token', refreshToken); await _clientBox.put('user_id', userId); await _clientBox.put('device_id', deviceId); await _clientBox.put('device_name', deviceName); @@ -1314,6 +1316,7 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin { Future updateClient( String homeserverUrl, String token, + String? refreshToken, String userId, String? deviceId, String? deviceName, @@ -1322,6 +1325,7 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin { ) async { await _clientBox.put('homeserver_url', homeserverUrl); await _clientBox.put('token', token); + await _clientBox.put('refresh_token', refreshToken); await _clientBox.put('user_id', userId); await _clientBox.put('device_id', deviceId); await _clientBox.put('device_name', deviceName); diff --git a/lib/src/database/matrix_sdk_database.dart b/lib/src/database/matrix_sdk_database.dart index 6f17970c..82a7dca3 100644 --- a/lib/src/database/matrix_sdk_database.dart +++ b/lib/src/database/matrix_sdk_database.dart @@ -727,6 +727,7 @@ class MatrixSdkDatabase extends DatabaseApi { String name, String homeserverUrl, String token, + String? refreshToken, String userId, String? deviceId, String? deviceName, @@ -735,6 +736,11 @@ class MatrixSdkDatabase extends DatabaseApi { await transaction(() async { await _clientBox.put('homeserver_url', homeserverUrl); await _clientBox.put('token', token); + if (refreshToken == null) { + await _clientBox.delete('refresh_token'); + } else { + await _clientBox.put('refresh_token', refreshToken); + } await _clientBox.put('user_id', userId); if (deviceId == null) { await _clientBox.delete('device_id'); @@ -1343,6 +1349,7 @@ class MatrixSdkDatabase extends DatabaseApi { Future updateClient( String homeserverUrl, String token, + String? refreshToken, String userId, String? deviceId, String? deviceName, @@ -1352,6 +1359,11 @@ class MatrixSdkDatabase extends DatabaseApi { await transaction(() async { await _clientBox.put('homeserver_url', homeserverUrl); await _clientBox.put('token', token); + if (refreshToken == null) { + await _clientBox.delete('refresh_token'); + } else { + await _clientBox.put('refresh_token', refreshToken); + } await _clientBox.put('user_id', userId); if (deviceId == null) { await _clientBox.delete('device_id'); diff --git a/test/client_test.dart b/test/client_test.dart index db86214e..6244e431 100644 --- a/test/client_test.dart +++ b/test/client_test.dart @@ -964,6 +964,35 @@ void main() { await client.dispose(closeDatabase: true); }); + test('refreshAccessToken', () async { + final client = await getClient(); + expect(client.accessToken, 'abcd'); + await client.refreshAccessToken(); + expect(client.accessToken, 'a_new_token'); + }); + + test('handleSoftLogout', () async { + final client = await getClient(); + expect(client.accessToken, 'abcd'); + var softLoggedOut = 0; + client.onSoftLogout = (client) { + softLoggedOut++; + return client.refreshAccessToken(); + }; + FakeMatrixApi.expectedAccessToken = 'a_new_token'; + await client.oneShotSync(); + await client.oneShotSync(); + FakeMatrixApi.expectedAccessToken = null; + expect(client.accessToken, 'a_new_token'); + expect(softLoggedOut, 1); + final storedClient = await client.database?.getClient(client.clientName); + expect(storedClient?.tryGet('token'), 'a_new_token'); + expect( + storedClient?.tryGet('refresh_token'), + 'another_new_token', + ); + }); + test('object equality', () async { final time1 = DateTime.fromMillisecondsSinceEpoch(1); final time2 = DateTime.fromMillisecondsSinceEpoch(0); diff --git a/test/database_api_test.dart b/test/database_api_test.dart index 0ebc5159..d043912f 100644 --- a/test/database_api_test.dart +++ b/test/database_api_test.dart @@ -133,6 +133,7 @@ void main() { 'name', 'homeserverUrl', 'token', + 'refresh_token', 'userId', 'deviceId', 'deviceName', @@ -147,6 +148,7 @@ void main() { await database.updateClient( 'homeserverUrl', 'token_different', + 'refresh_token', 'userId', 'deviceId', 'deviceName', diff --git a/test/fake_client.dart b/test/fake_client.dart index 15f83d14..e4cc81b6 100644 --- a/test/fake_client.dart +++ b/test/fake_client.dart @@ -32,12 +32,14 @@ Future getClient() async { 'testclient', httpClient: FakeMatrixApi(), databaseBuilder: getDatabase, + onSoftLogout: (client) => client.refreshAccessToken(), ); FakeMatrixApi.client = client; await client.checkHomeserver(Uri.parse('https://fakeServer.notExisting'), checkWellKnown: false); await client.init( newToken: 'abcd', + newRefreshToken: 'refresh_abcd', newUserID: '@test:fakeServer.notExisting', newHomeserver: client.homeserver, newDeviceName: 'Text Matrix Client', diff --git a/test/fake_matrix_api.dart b/test/fake_matrix_api.dart index b9e46353..7ae3a471 100644 --- a/test/fake_matrix_api.dart +++ b/test/fake_matrix_api.dart @@ -39,6 +39,8 @@ Map decodeJson(dynamic data) { } class FakeMatrixApi extends BaseClient { + static String? expectedAccessToken; + static Map> get calledEndpoints => currentApi!._calledEndpoints; static int get eventCounter => currentApi!._eventCounter; @@ -129,6 +131,23 @@ class FakeMatrixApi extends BaseClient { 'Not found...', 404); } + if (!{ + '/client/v3/refresh', + '/client/v3/login', + '/client/v3/register', + }.contains(action) && + expectedAccessToken != null && + request.headers['Authorization'] != 'Bearer $expectedAccessToken') { + return Response( + jsonEncode({ + 'errcode': 'M_UNKNOWN_TOKEN', + 'error': 'Soft logged out', + 'soft_logout': true, + }), + 401, + ); + } + // Call API (_calledEndpoints[action] ??= []).add(data); final act = api[method]?[action]; @@ -2013,6 +2032,11 @@ class FakeMatrixApi extends BaseClient { }, }, 'POST': { + '/client/v3/refresh': (var req) => { + 'access_token': 'a_new_token', + 'expires_in_ms': 60000, + 'refresh_token': 'another_new_token' + }, '/client/v3/delete_devices': (var req) => {}, '/client/v3/account/3pid/add': (var req) => {}, '/client/v3/account/3pid/bind': (var req) => {}, @@ -2397,6 +2421,7 @@ class FakeMatrixApi extends BaseClient { '/client/v3/login': (var req) => { 'user_id': '@test:fakeServer.notExisting', 'access_token': 'abc123', + 'refresh_token': 'refresh_abc123', 'device_id': 'GHTYAJCE', 'well_known': { 'm.homeserver': {'base_url': 'https://example.org'}, diff --git a/test/matrix_database_test.dart b/test/matrix_database_test.dart index 6c333e9b..cc8cb833 100644 --- a/test/matrix_database_test.dart +++ b/test/matrix_database_test.dart @@ -32,6 +32,7 @@ void main() { 'testclient', 'https://example.org', 'blubb', + null, '@test:example.org', null, null, From 65c56f3630da66322aef3b197838b9de4b959f44 Mon Sep 17 00:00:00 2001 From: Krille Date: Fri, 23 Feb 2024 12:50:55 +0100 Subject: [PATCH 07/10] feat: Store accesstokenExpiresIn and call softlogout 5 minutes before --- lib/src/client.dart | 40 +++++++++++++++++++ lib/src/database/database_api.dart | 2 + .../database/hive_collections_database.dart | 16 ++++++++ lib/src/database/hive_database.dart | 6 +++ lib/src/database/matrix_sdk_database.dart | 14 +++++++ test/database_api_test.dart | 7 ++++ test/matrix_database_test.dart | 1 + 7 files changed, 86 insertions(+) diff --git a/lib/src/client.dart b/lib/src/client.dart index a8347ddc..4ba8ce9b 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -91,6 +91,8 @@ class Client extends MatrixApi { Future Function(Client client)? onSoftLogout; + DateTime? accessTokenExpiresAt; + // For CommandsClientExtension final Map Function(CommandArgs)> commands = {}; final Filter syncFilter; @@ -268,6 +270,7 @@ class Client extends MatrixApi { await database?.updateClient( homeserverUrl, tokenResponse.accessToken, + accessTokenExpiresAt, tokenResponse.refreshToken, userId, deviceId, @@ -540,8 +543,14 @@ class Client extends MatrixApi { throw Exception( 'Registered but token, device ID, user ID or homeserver is null.'); } + final expiresInMs = response.expiresInMs; + final tokenExpiresAt = expiresInMs == null + ? null + : DateTime.now().add(Duration(milliseconds: expiresInMs)); + await init( newToken: accessToken, + newTokenExpiresAt: tokenExpiresAt, newRefreshToken: response.refreshToken, newUserID: userId, newHomeserver: homeserver, @@ -604,8 +613,15 @@ class Client extends MatrixApi { if (homeserver_ == null) { throw Exception('Registered but homerserver is null.'); } + + final expiresInMs = response.expiresInMs; + final tokenExpiresAt = expiresInMs == null + ? null + : DateTime.now().add(Duration(milliseconds: expiresInMs)); + await init( newToken: accessToken, + newTokenExpiresAt: tokenExpiresAt, newRefreshToken: response.refreshToken, newUserID: userId, newHomeserver: homeserver_, @@ -1521,6 +1537,7 @@ class Client extends MatrixApi { /// `userDeviceKeysLoading` where it is necessary. Future init({ String? newToken, + DateTime? newTokenExpiresAt, String? newRefreshToken, Uri? newHomeserver, String? newUserID, @@ -1579,6 +1596,11 @@ class Client extends MatrixApi { _id = account['client_id']; homeserver = Uri.parse(account['homeserver_url']); accessToken = this.accessToken = account['token']; + final tokenExpiresAtMs = + int.tryParse(account.tryGet('token_expires_at') ?? ''); + accessTokenExpiresAt = tokenExpiresAtMs == null + ? null + : DateTime.fromMillisecondsSinceEpoch(tokenExpiresAtMs); userID = _userID = account['user_id']; _deviceID = account['device_id']; _deviceName = account['device_name']; @@ -1588,6 +1610,7 @@ class Client extends MatrixApi { } if (newToken != null) { accessToken = this.accessToken = newToken; + accessTokenExpiresAt = newTokenExpiresAt; homeserver = newHomeserver; userID = _userID = newUserID; _deviceID = newDeviceID; @@ -1595,6 +1618,7 @@ class Client extends MatrixApi { olmAccount = newOlmAccount; } else { accessToken = this.accessToken = newToken ?? accessToken; + accessTokenExpiresAt = newTokenExpiresAt ?? accessTokenExpiresAt; homeserver = newHomeserver ?? homeserver; userID = _userID = newUserID ?? userID; _deviceID = newDeviceID ?? _deviceID; @@ -1635,6 +1659,7 @@ class Client extends MatrixApi { await database.updateClient( homeserver.toString(), accessToken, + accessTokenExpiresAt, newRefreshToken, userID, _deviceID, @@ -1647,6 +1672,7 @@ class Client extends MatrixApi { clientName, homeserver.toString(), accessToken, + accessTokenExpiresAt, newRefreshToken, userID, _deviceID, @@ -1794,6 +1820,15 @@ class Client extends MatrixApi { Object? syncError; await _checkSyncFilter(); + // Call onSoftLogout 5 minutes before access token expires to prevent + // failing network requests. + final tokenExpiresAt = accessTokenExpiresAt; + if (onSoftLogout != null && + tokenExpiresAt != null && + tokenExpiresAt.difference(DateTime.now()) <= Duration(minutes: 5)) { + await onSoftLogout?.call(this); + } + // The timeout we send to the server for the sync loop. It says to the // server that we want to receive an empty sync response after this // amount of time if nothing happens. @@ -3169,10 +3204,15 @@ class Client extends MatrixApi { Logs().i('Found data in the legacy database!'); onMigration?.call(); _id = migrateClient['client_id']; + final tokenExpiresAtMs = + int.tryParse(migrateClient.tryGet('token_expires_at') ?? ''); await database.insertClient( clientName, migrateClient['homeserver_url'], migrateClient['token'], + tokenExpiresAtMs == null + ? null + : DateTime.fromMillisecondsSinceEpoch(tokenExpiresAtMs), migrateClient['refresh_token'], migrateClient['user_id'], migrateClient['device_id'], diff --git a/lib/src/database/database_api.dart b/lib/src/database/database_api.dart index f6d15054..b7e79c16 100644 --- a/lib/src/database/database_api.dart +++ b/lib/src/database/database_api.dart @@ -33,6 +33,7 @@ abstract class DatabaseApi { Future updateClient( String homeserverUrl, String token, + DateTime? tokenExpiresAt, String? refreshToken, String userId, String? deviceId, @@ -45,6 +46,7 @@ abstract class DatabaseApi { String name, String homeserverUrl, String token, + DateTime? tokenExpiresAt, String? refreshToken, String userId, String? deviceId, diff --git a/lib/src/database/hive_collections_database.dart b/lib/src/database/hive_collections_database.dart index 9582f40a..1577ac38 100644 --- a/lib/src/database/hive_collections_database.dart +++ b/lib/src/database/hive_collections_database.dart @@ -785,6 +785,7 @@ class HiveCollectionsDatabase extends DatabaseApi { String name, String homeserverUrl, String token, + DateTime? tokenExpiresAt, String? refreshToken, String userId, String? deviceId, @@ -800,6 +801,14 @@ class HiveCollectionsDatabase extends DatabaseApi { } else { await _clientBox.put('refresh_token', refreshToken); } + if (tokenExpiresAt == null) { + await _clientBox.delete('token_expires_at'); + } else { + await _clientBox.put( + 'token_expires_at', + tokenExpiresAt.millisecondsSinceEpoch.toString(), + ); + } if (deviceId == null) { await _clientBox.delete('device_id'); } else { @@ -1377,6 +1386,7 @@ class HiveCollectionsDatabase extends DatabaseApi { Future updateClient( String homeserverUrl, String token, + DateTime? tokenExpiresAt, String? refreshToken, String userId, String? deviceId, @@ -1387,6 +1397,12 @@ class HiveCollectionsDatabase extends DatabaseApi { await transaction(() async { await _clientBox.put('homeserver_url', homeserverUrl); await _clientBox.put('token', token); + if (tokenExpiresAt == null) { + await _clientBox.delete('token_expires_at'); + } else { + await _clientBox.put('token_expires_at', + tokenExpiresAt.millisecondsSinceEpoch.toString()); + } if (refreshToken == null) { await _clientBox.delete('refresh_token'); } else { diff --git a/lib/src/database/hive_database.dart b/lib/src/database/hive_database.dart index 89576c7e..8370de50 100644 --- a/lib/src/database/hive_database.dart +++ b/lib/src/database/hive_database.dart @@ -750,6 +750,7 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin { String name, String homeserverUrl, String token, + DateTime? tokenExpiresAt, String? refreshToken, String userId, String? deviceId, @@ -758,6 +759,8 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin { String? olmAccount) async { await _clientBox.put('homeserver_url', homeserverUrl); await _clientBox.put('token', token); + await _clientBox.put( + 'token_expires_at', tokenExpiresAt?.millisecondsSinceEpoch.toString()); await _clientBox.put('refresh_token', refreshToken); await _clientBox.put('user_id', userId); await _clientBox.put('device_id', deviceId); @@ -1316,6 +1319,7 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin { Future updateClient( String homeserverUrl, String token, + DateTime? tokenExpiresAt, String? refreshToken, String userId, String? deviceId, @@ -1325,6 +1329,8 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin { ) async { await _clientBox.put('homeserver_url', homeserverUrl); await _clientBox.put('token', token); + await _clientBox.put( + 'token_expires_at', tokenExpiresAt?.millisecondsSinceEpoch.toString()); await _clientBox.put('refresh_token', refreshToken); await _clientBox.put('user_id', userId); await _clientBox.put('device_id', deviceId); diff --git a/lib/src/database/matrix_sdk_database.dart b/lib/src/database/matrix_sdk_database.dart index 82a7dca3..ee0bc961 100644 --- a/lib/src/database/matrix_sdk_database.dart +++ b/lib/src/database/matrix_sdk_database.dart @@ -727,6 +727,7 @@ class MatrixSdkDatabase extends DatabaseApi { String name, String homeserverUrl, String token, + DateTime? tokenExpiresAt, String? refreshToken, String userId, String? deviceId, @@ -736,6 +737,12 @@ class MatrixSdkDatabase extends DatabaseApi { await transaction(() async { await _clientBox.put('homeserver_url', homeserverUrl); await _clientBox.put('token', token); + if (tokenExpiresAt == null) { + await _clientBox.delete('token_expires_at'); + } else { + await _clientBox.put('token_expires_at', + tokenExpiresAt.millisecondsSinceEpoch.toString()); + } if (refreshToken == null) { await _clientBox.delete('refresh_token'); } else { @@ -1349,6 +1356,7 @@ class MatrixSdkDatabase extends DatabaseApi { Future updateClient( String homeserverUrl, String token, + DateTime? tokenExpiresAt, String? refreshToken, String userId, String? deviceId, @@ -1359,6 +1367,12 @@ class MatrixSdkDatabase extends DatabaseApi { await transaction(() async { await _clientBox.put('homeserver_url', homeserverUrl); await _clientBox.put('token', token); + if (tokenExpiresAt == null) { + await _clientBox.delete('token_expires_at'); + } else { + await _clientBox.put('token_expires_at', + tokenExpiresAt.millisecondsSinceEpoch.toString()); + } if (refreshToken == null) { await _clientBox.delete('refresh_token'); } else { diff --git a/test/database_api_test.dart b/test/database_api_test.dart index d043912f..c2a48566 100644 --- a/test/database_api_test.dart +++ b/test/database_api_test.dart @@ -129,10 +129,12 @@ void main() { await database.getClient('name'); }); test('insertClient', () async { + final now = DateTime.now(); await database.insertClient( 'name', 'homeserverUrl', 'token', + now, 'refresh_token', 'userId', 'deviceId', @@ -143,11 +145,16 @@ void main() { final client = await database.getClient('name'); expect(client?['token'], 'token'); + expect( + client?['token_expires_at'], + now.millisecondsSinceEpoch.toString(), + ); }); test('updateClient', () async { await database.updateClient( 'homeserverUrl', 'token_different', + DateTime.now(), 'refresh_token', 'userId', 'deviceId', diff --git a/test/matrix_database_test.dart b/test/matrix_database_test.dart index cc8cb833..95a58d8b 100644 --- a/test/matrix_database_test.dart +++ b/test/matrix_database_test.dart @@ -33,6 +33,7 @@ void main() { 'https://example.org', 'blubb', null, + null, '@test:example.org', null, null, From 9876d9c90f28e3bb5c3f6269c5b3e231c637339f Mon Sep 17 00:00:00 2001 From: Karthikeyan S Date: Fri, 23 Feb 2024 19:02:21 +0530 Subject: [PATCH 08/10] ci: add versions.env to unify flutter and dart versions --- .github/workflows/app.yml | 32 +++++++++++++++----------------- .github/workflows/main.yml | 24 +----------------------- .github/workflows/publish.yml | 2 ++ .github/workflows/versions.env | 2 ++ 4 files changed, 20 insertions(+), 40 deletions(-) create mode 100644 .github/workflows/versions.env diff --git a/.github/workflows/app.yml b/.github/workflows/app.yml index a73b9605..74100a3b 100644 --- a/.github/workflows/app.yml +++ b/.github/workflows/app.yml @@ -2,15 +2,6 @@ name: "All the sdk specific jobs" on: workflow_call: - inputs: - flutter_version: - description: "The flutter version used for tests and builds" - type: string - required: true - dart_version: - description: "The dart version used for tests and builds" - type: string - required: true jobs: e2ee_test: @@ -22,13 +13,14 @@ jobs: fail-fast: false steps: - uses: actions/checkout@v3 + - run: cat .github/workflows/versions.env >> $GITHUB_ENV - name: Run tests run: | export NETWORK='--network mynet' docker network create mynet # deploy homeserver instance scripts/integration-server-${{matrix.homeserver}}.sh - docker run $NETWORK --env GITHUB_ACTIONS="${GITHUB_ACTIONS}" --env HOMESERVER_IMPLEMENTATION="${{matrix.homeserver}}" --env HOMESERVER="${{startsWith('dendrite', matrix.homeserver) && format('{0}:8008', matrix.homeserver) || matrix.homeserver }}" --volume="$(pwd):/workdir" --workdir /workdir ghcr.io/famedly/container-image-flutter/flutter:${{inputs.flutter_version}} /bin/bash -c "set -e + docker run $NETWORK --env GITHUB_ACTIONS="${GITHUB_ACTIONS}" --env HOMESERVER_IMPLEMENTATION="${{matrix.homeserver}}" --env HOMESERVER="${{startsWith('dendrite', matrix.homeserver) && format('{0}:8008', matrix.homeserver) || matrix.homeserver }}" --volume="$(pwd):/workdir" --workdir /workdir ghcr.io/famedly/container-image-flutter/flutter:${{env.flutter_version}} /bin/bash -c "set -e scripts/integration-prepare-alpine.sh # create test user environment variables source scripts/integration-create-environment-variables.sh @@ -42,11 +34,13 @@ jobs: # coverage_without_olm is done on dart images because why not :D coverage: runs-on: ubuntu-latest - container: - image: ghcr.io/famedly/container-image-flutter/flutter-linux:${{inputs.flutter_version}} - options: --user root steps: - uses: actions/checkout@v3 + - run: cat .github/workflows/versions.env >> $GITHUB_ENV + - uses: subosito/flutter-action@48cafc24713cca54bbe03cdc3a423187d413aafa + with: + flutter-version: ${{ env.flutter_version }} + cache: true - name: Run tests run: | sed -i 's/#flutter_test/flutter_test/g' pubspec.yaml @@ -56,12 +50,14 @@ jobs: coverage_without_olm: runs-on: ubuntu-latest - container: - image: dart:${{inputs.dart_version}} env: NO_OLM: 1 steps: - uses: actions/checkout@v3 + - run: cat .github/workflows/versions.env >> $GITHUB_ENV + - uses: dart-lang/setup-dart@a57a6c04cf7d4840e88432aad6281d1e125f0d46 + with: + sdk: ${{ env.dart_version }} - name: Run tests run: | apt-get update && apt-get install --no-install-recommends --no-install-suggests -y curl lcov python3 python3-distutils libsqlite3-dev @@ -71,10 +67,12 @@ jobs: pub-dev-dry-run: runs-on: ubuntu-latest - container: - image: dart:${{inputs.dart_version}} steps: - uses: actions/checkout@v3 + - run: cat .github/workflows/versions.env >> $GITHUB_ENV + - uses: dart-lang/setup-dart@a57a6c04cf7d4840e88432aad6281d1e125f0d46 + with: + sdk: ${{ env.dart_version }} - name: pub.dev publish dry run run: | dart pub get diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7f4e5780..9531e678 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,31 +13,13 @@ concurrency: group: ${{ github.ref }} cancel-in-progress: true -env: - FLUTTER_VERSION: 3.10.6 - DART_VERSION: 3.0.6 - jobs: - # because there is no easy way to pass env variables to jobs - versions: - runs-on: ubuntu-latest - outputs: - flutter_version: ${{ steps.flutterver.outputs.FLUTTER_VERSION }} - dart_version: ${{ steps.dartver.outputs.DART_VERSION }} - steps: - - id: flutterver - run: echo "FLUTTER_VERSION=${{ env.FLUTTER_VERSION }}" >> "$GITHUB_OUTPUT" - - id: dartver - run: echo "DART_VERSION=${{ env.DART_VERSION }}" >> "$GITHUB_OUTPUT" - dart: permissions: contents: read uses: famedly/frontend-ci-templates/.github/workflows/dart.yml@main - needs: [versions] with: - flutter_version: ${{ needs.versions.outputs.flutter_version }} - dart_version: ${{ needs.versions.outputs.dart_version }} + env_file: ".github/workflows/versions.env" secrets: ssh_key: "${{ secrets.CI_SSH_PRIVATE_KEY }}" @@ -49,7 +31,3 @@ jobs: app_jobs: secrets: inherit uses: ./.github/workflows/app.yml - needs: [versions] - with: - flutter_version: ${{ needs.versions.outputs.flutter_version }} - dart_version: ${{ needs.versions.outputs.dart_version }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index de10fd3a..b8c82cdb 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -12,3 +12,5 @@ jobs: contents: read id-token: write uses: famedly/frontend-ci-templates/.github/workflows/publish-pub.yml@main + with: + env_file: ".github/workflows/versions.env" diff --git a/.github/workflows/versions.env b/.github/workflows/versions.env new file mode 100644 index 00000000..08f97c5e --- /dev/null +++ b/.github/workflows/versions.env @@ -0,0 +1,2 @@ +flutter_version=3.10.6 +dart_version=3.0.6 From 2687381274d65d220f14eeab152ce153ca15523a Mon Sep 17 00:00:00 2001 From: Krille Date: Mon, 26 Feb 2024 12:34:48 +0100 Subject: [PATCH 09/10] build: Bump version --- CHANGELOG.md | 5 +++++ pubspec.yaml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89674651..908ffdda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## [0.25.11] 26th Februray 2024 +- feat: Implement handling soft logout (Krille) +- feat: Store accesstokenExpiresIn and call softlogout 5 minutes before (Krille) +- fix: convert boxNames to List in clear function when creating transaction (Gabby Gurdin) + ## [0.25.10] 23rd February 2024 - chore: remove state events both in imp and preview events list (td) - feat: specify history_visibility when creating group chat (Karthikeyan S) diff --git a/pubspec.yaml b/pubspec.yaml index c5db59c8..09777f95 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: matrix description: Matrix Dart SDK -version: 0.25.10 +version: 0.25.11 homepage: https://famedly.com repository: https://github.com/famedly/matrix-dart-sdk.git issue_tracker: https://github.com/famedly/matrix-dart-sdk/issues From 1b93471d64720c84f8163e5ccdb04cb3c77f21d6 Mon Sep 17 00:00:00 2001 From: Krille Date: Wed, 28 Feb 2024 12:31:54 +0100 Subject: [PATCH 10/10] refactor: Deprecations after dart upgrade --- .github/workflows/versions.env | 4 ++-- lib/src/models/receipts.dart | 2 +- lib/src/room.dart | 2 +- lib/src/user.dart | 2 +- lib/src/utils/crypto/native.dart | 4 ++++ lib/src/utils/device_keys_list.dart | 2 +- 6 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/versions.env b/.github/workflows/versions.env index 08f97c5e..771a477f 100644 --- a/.github/workflows/versions.env +++ b/.github/workflows/versions.env @@ -1,2 +1,2 @@ -flutter_version=3.10.6 -dart_version=3.0.6 +flutter_version=3.19.0 +dart_version=3.3.0 diff --git a/lib/src/models/receipts.dart b/lib/src/models/receipts.dart index 1130740c..c43fe3c1 100644 --- a/lib/src/models/receipts.dart +++ b/lib/src/models/receipts.dart @@ -44,7 +44,7 @@ class Receipt { const Receipt(this.user, this.time); @override - bool operator ==(dynamic other) => (other is Receipt && + bool operator ==(Object other) => (other is Receipt && other.user == user && other.time.millisecondsSinceEpoch == time.millisecondsSinceEpoch); diff --git a/lib/src/room.dart b/lib/src/room.dart index 9a067fbf..5c4a6ec0 100644 --- a/lib/src/room.dart +++ b/lib/src/room.dart @@ -2364,7 +2364,7 @@ class Room { : setSpaceChild(roomId, via: const []); @override - bool operator ==(dynamic other) => (other is Room && other.id == id); + bool operator ==(Object other) => (other is Room && other.id == id); @override int get hashCode => Object.hashAll([id]); diff --git a/lib/src/user.dart b/lib/src/user.dart index 5948e69e..5cd4563f 100644 --- a/lib/src/user.dart +++ b/lib/src/user.dart @@ -175,7 +175,7 @@ class User extends Event { room.canChangePowerLevel && powerLevel < room.ownPowerLevel; @override - bool operator ==(dynamic other) => (other is User && + bool operator ==(Object other) => (other is User && other.id == id && other.room == room && other.membership == membership); diff --git a/lib/src/utils/crypto/native.dart b/lib/src/utils/crypto/native.dart index 8ef84ef2..1ef0613d 100644 --- a/lib/src/utils/crypto/native.dart +++ b/lib/src/utils/crypto/native.dart @@ -1,3 +1,7 @@ +// ignore_for_file: deprecated_member_use +// ignoring the elementAt deprecation because this would make the SDK +// incompatible with older flutter versions than 3.19.0 or dart 3.3.0 + import 'dart:async'; import 'dart:ffi'; import 'dart:typed_data'; diff --git a/lib/src/utils/device_keys_list.dart b/lib/src/utils/device_keys_list.dart index b4a26585..db235c2a 100644 --- a/lib/src/utils/device_keys_list.dart +++ b/lib/src/utils/device_keys_list.dart @@ -354,7 +354,7 @@ abstract class SignableKey extends MatrixSignableKey { String toString() => json.encode(toJson()); @override - bool operator ==(dynamic other) => (other is SignableKey && + bool operator ==(Object other) => (other is SignableKey && other.userId == userId && other.identifier == identifier);