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 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..771a477f --- /dev/null +++ b/.github/workflows/versions.env @@ -0,0 +1,2 @@ +flutter_version=3.19.0 +dart_version=3.3.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index b3cd5fa3..908ffdda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## [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) + ## [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/lib/src/client.dart b/lib/src/client.dart index e2650f7e..4ba8ce9b 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -89,6 +89,10 @@ class Client extends MatrixApi { bool shareKeysWithUnverifiedDevices; + Future Function(Client client)? onSoftLogout; + + DateTime? accessTokenExpiresAt; + // For CommandsClientExtension final Map Function(CommandArgs)> commands = {}; final Filter syncFilter; @@ -161,6 +165,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, @@ -178,6 +188,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( @@ -216,14 +233,53 @@ 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 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, + accessTokenExpiresAt, + tokenResponse.refreshToken, + userId, + deviceId, + deviceName, + prevBatch, + encryption?.pickledOlmAccount, + ); + } + /// The required name for this client. final String clientName; @@ -475,6 +531,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. @@ -486,8 +543,15 @@ 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, newDeviceName: initialDeviceDisplayName ?? '', @@ -538,6 +602,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. @@ -548,8 +613,16 @@ 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_, newDeviceName: initialDeviceDisplayName ?? '', @@ -695,6 +768,7 @@ class Client extends MatrixApi { CreateRoomPreset preset = CreateRoomPreset.privateChat, List? initialState, Visibility? visibility, + HistoryVisibility? historyVisibility, bool waitForSync = true, bool groupCall = false, Map? powerLevelContentOverride, @@ -712,6 +786,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'] = { @@ -1452,6 +1537,8 @@ class Client extends MatrixApi { /// `userDeviceKeysLoading` where it is necessary. Future init({ String? newToken, + DateTime? newTokenExpiresAt, + String? newRefreshToken, Uri? newHomeserver, String? newUserID, String? newDeviceName, @@ -1509,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']; @@ -1518,6 +1610,7 @@ class Client extends MatrixApi { } if (newToken != null) { accessToken = this.accessToken = newToken; + accessTokenExpiresAt = newTokenExpiresAt; homeserver = newHomeserver; userID = _userID = newUserID; _deviceID = newDeviceID; @@ -1525,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; @@ -1565,6 +1659,8 @@ class Client extends MatrixApi { await database.updateClient( homeserver.toString(), accessToken, + accessTokenExpiresAt, + newRefreshToken, userID, _deviceID, _deviceName, @@ -1576,6 +1672,8 @@ class Client extends MatrixApi { clientName, homeserver.toString(), accessToken, + accessTokenExpiresAt, + newRefreshToken, userID, _deviceID, _deviceName, @@ -1722,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. @@ -1800,8 +1907,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'); @@ -3086,10 +3204,16 @@ 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'], migrateClient['device_name'], diff --git a/lib/src/database/database_api.dart b/lib/src/database/database_api.dart index 2c41c7d4..b7e79c16 100644 --- a/lib/src/database/database_api.dart +++ b/lib/src/database/database_api.dart @@ -33,6 +33,8 @@ abstract class DatabaseApi { Future updateClient( String homeserverUrl, String token, + DateTime? tokenExpiresAt, + String? refreshToken, String userId, String? deviceId, String? deviceName, @@ -44,6 +46,8 @@ abstract class DatabaseApi { String name, String homeserverUrl, String token, + DateTime? tokenExpiresAt, + 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..1577ac38 100644 --- a/lib/src/database/hive_collections_database.dart +++ b/lib/src/database/hive_collections_database.dart @@ -785,6 +785,8 @@ class HiveCollectionsDatabase extends DatabaseApi { String name, String homeserverUrl, String token, + DateTime? tokenExpiresAt, + String? refreshToken, String userId, String? deviceId, String? deviceName, @@ -794,6 +796,19 @@ 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 (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 { @@ -1371,6 +1386,8 @@ class HiveCollectionsDatabase extends DatabaseApi { Future updateClient( String homeserverUrl, String token, + DateTime? tokenExpiresAt, + String? refreshToken, String userId, String? deviceId, String? deviceName, @@ -1380,6 +1397,17 @@ 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 { + 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..8370de50 100644 --- a/lib/src/database/hive_database.dart +++ b/lib/src/database/hive_database.dart @@ -750,6 +750,8 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin { String name, String homeserverUrl, String token, + DateTime? tokenExpiresAt, + String? refreshToken, String userId, String? deviceId, String? deviceName, @@ -757,6 +759,9 @@ 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); await _clientBox.put('device_name', deviceName); @@ -1314,6 +1319,8 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin { Future updateClient( String homeserverUrl, String token, + DateTime? tokenExpiresAt, + String? refreshToken, String userId, String? deviceId, String? deviceName, @@ -1322,6 +1329,9 @@ 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); await _clientBox.put('device_name', deviceName); 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()); } diff --git a/lib/src/database/matrix_sdk_database.dart b/lib/src/database/matrix_sdk_database.dart index 6f17970c..ee0bc961 100644 --- a/lib/src/database/matrix_sdk_database.dart +++ b/lib/src/database/matrix_sdk_database.dart @@ -727,6 +727,8 @@ class MatrixSdkDatabase extends DatabaseApi { String name, String homeserverUrl, String token, + DateTime? tokenExpiresAt, + String? refreshToken, String userId, String? deviceId, String? deviceName, @@ -735,6 +737,17 @@ 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 { + await _clientBox.put('refresh_token', refreshToken); + } await _clientBox.put('user_id', userId); if (deviceId == null) { await _clientBox.delete('device_id'); @@ -1343,6 +1356,8 @@ class MatrixSdkDatabase extends DatabaseApi { Future updateClient( String homeserverUrl, String token, + DateTime? tokenExpiresAt, + String? refreshToken, String userId, String? deviceId, String? deviceName, @@ -1352,6 +1367,17 @@ 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 { + 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/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 2600e297..f330b72a 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'; @@ -2135,7 +2143,7 @@ class Room { EventTypes.GuestAccess, '', { - 'guest_access': _guestAccessMap[guestAccess], + 'guest_access': guestAccess.text, }, ); return; @@ -2160,7 +2168,7 @@ class Room { EventTypes.HistoryVisibility, '', { - 'history_visibility': _historyVisibilityMap[historyVisibility], + 'history_visibility': historyVisibility.text, }, ); return; @@ -2353,7 +2361,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); diff --git a/pubspec.yaml b/pubspec.yaml index 11b7b2cc..09777f95 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: matrix description: Matrix Dart SDK -version: 0.25.9 +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 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..c2a48566 100644 --- a/test/database_api_test.dart +++ b/test/database_api_test.dart @@ -129,10 +129,13 @@ void main() { await database.getClient('name'); }); test('insertClient', () async { + final now = DateTime.now(); await database.insertClient( 'name', 'homeserverUrl', 'token', + now, + 'refresh_token', 'userId', 'deviceId', 'deviceName', @@ -142,11 +145,17 @@ 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', '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..95a58d8b 100644 --- a/test/matrix_database_test.dart +++ b/test/matrix_database_test.dart @@ -32,6 +32,8 @@ void main() { 'testclient', 'https://example.org', 'blubb', + null, + null, '@test:example.org', null, null,