LCOV - code coverage report
Current view: top level - lib/src - client.dart (source / functions) Hit Total Coverage
Test: merged.info Lines: 1020 1344 75.9 %
Date: 2024-07-12 20:20:16 Functions: 0 0 -

          Line data    Source code
       1             : /*
       2             :  *   Famedly Matrix SDK
       3             :  *   Copyright (C) 2019, 2020, 2021 Famedly GmbH
       4             :  *
       5             :  *   This program is free software: you can redistribute it and/or modify
       6             :  *   it under the terms of the GNU Affero General Public License as
       7             :  *   published by the Free Software Foundation, either version 3 of the
       8             :  *   License, or (at your option) any later version.
       9             :  *
      10             :  *   This program is distributed in the hope that it will be useful,
      11             :  *   but WITHOUT ANY WARRANTY; without even the implied warranty of
      12             :  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
      13             :  *   GNU Affero General Public License for more details.
      14             :  *
      15             :  *   You should have received a copy of the GNU Affero General Public License
      16             :  *   along with this program.  If not, see <https://www.gnu.org/licenses/>.
      17             :  */
      18             : 
      19             : import 'dart:async';
      20             : import 'dart:convert';
      21             : import 'dart:core';
      22             : import 'dart:math';
      23             : import 'dart:typed_data';
      24             : 
      25             : import 'package:async/async.dart';
      26             : import 'package:collection/collection.dart' show IterableExtension;
      27             : import 'package:http/http.dart' as http;
      28             : import 'package:mime/mime.dart';
      29             : import 'package:olm/olm.dart' as olm;
      30             : import 'package:random_string/random_string.dart';
      31             : 
      32             : import 'package:matrix/encryption.dart';
      33             : import 'package:matrix/matrix.dart';
      34             : import 'package:matrix/msc_extensions/msc_unpublished_custom_refresh_token_lifetime/msc_unpublished_custom_refresh_token_lifetime.dart';
      35             : import 'package:matrix/src/models/timeline_chunk.dart';
      36             : import 'package:matrix/src/utils/cached_stream_controller.dart';
      37             : import 'package:matrix/src/utils/client_init_exception.dart';
      38             : import 'package:matrix/src/utils/compute_callback.dart';
      39             : import 'package:matrix/src/utils/multilock.dart';
      40             : import 'package:matrix/src/utils/run_benchmarked.dart';
      41             : import 'package:matrix/src/utils/run_in_root.dart';
      42             : import 'package:matrix/src/utils/sync_update_item_count.dart';
      43             : import 'package:matrix/src/utils/try_get_push_rule.dart';
      44             : 
      45             : typedef RoomSorter = int Function(Room a, Room b);
      46             : 
      47             : enum LoginState { loggedIn, loggedOut, softLoggedOut }
      48             : 
      49             : extension TrailingSlash on Uri {
      50          99 :   Uri stripTrailingSlash() => path.endsWith('/')
      51           0 :       ? replace(path: path.substring(0, path.length - 1))
      52             :       : this;
      53             : }
      54             : 
      55             : /// Represents a Matrix client to communicate with a
      56             : /// [Matrix](https://matrix.org) homeserver and is the entry point for this
      57             : /// SDK.
      58             : class Client extends MatrixApi {
      59             :   int? _id;
      60             : 
      61             :   // Keeps track of the currently ongoing syncRequest
      62             :   // in case we want to cancel it.
      63             :   int _currentSyncId = -1;
      64             : 
      65          58 :   int? get id => _id;
      66             : 
      67             :   final FutureOr<DatabaseApi> Function(Client)? databaseBuilder;
      68             :   final FutureOr<DatabaseApi> Function(Client)? legacyDatabaseBuilder;
      69             :   DatabaseApi? _database;
      70             : 
      71          66 :   DatabaseApi? get database => _database;
      72             : 
      73          62 :   Encryption? get encryption => _encryption;
      74             :   Encryption? _encryption;
      75             : 
      76             :   Set<KeyVerificationMethod> verificationMethods;
      77             : 
      78             :   Set<String> importantStateEvents;
      79             : 
      80             :   Set<String> roomPreviewLastEvents;
      81             : 
      82             :   Set<String> supportedLoginTypes;
      83             : 
      84             :   bool requestHistoryOnLimitedTimeline;
      85             : 
      86             :   final bool formatLocalpart;
      87             : 
      88             :   final bool mxidLocalPartFallback;
      89             : 
      90             :   bool shareKeysWithUnverifiedDevices;
      91             : 
      92             :   Future<void> Function(Client client)? onSoftLogout;
      93             : 
      94          62 :   DateTime? get accessTokenExpiresAt => _accessTokenExpiresAt;
      95             :   DateTime? _accessTokenExpiresAt;
      96             : 
      97             :   // For CommandsClientExtension
      98             :   final Map<String, FutureOr<String?> Function(CommandArgs)> commands = {};
      99             :   final Filter syncFilter;
     100             : 
     101             :   final NativeImplementations nativeImplementations;
     102             : 
     103             :   String? _syncFilterId;
     104             : 
     105          62 :   String? get syncFilterId => _syncFilterId;
     106             : 
     107             :   final ComputeCallback? compute;
     108             : 
     109           0 :   @Deprecated('Use [nativeImplementations] instead')
     110             :   Future<T> runInBackground<T, U>(
     111             :       FutureOr<T> Function(U arg) function, U arg) async {
     112           0 :     final compute = this.compute;
     113             :     if (compute != null) {
     114           0 :       return await compute(function, arg);
     115             :     }
     116           0 :     return await function(arg);
     117             :   }
     118             : 
     119             :   final Duration sendTimelineEventTimeout;
     120             : 
     121             :   /// The timeout until a typing indicator gets removed automatically.
     122             :   final Duration typingIndicatorTimeout;
     123             : 
     124             :   Future<MatrixImageFileResizedResponse?> Function(
     125             :       MatrixImageFileResizeArguments)? customImageResizer;
     126             : 
     127             :   /// Create a client
     128             :   /// [clientName] = unique identifier of this client
     129             :   /// [databaseBuilder]: A function that creates the database instance, that will be used.
     130             :   /// [legacyDatabaseBuilder]: Use this for your old database implementation to perform an automatic migration
     131             :   /// [databaseDestroyer]: A function that can be used to destroy a database instance, for example by deleting files from disk.
     132             :   /// [verificationMethods]: A set of all the verification methods this client can handle. Includes:
     133             :   ///    KeyVerificationMethod.numbers: Compare numbers. Most basic, should be supported
     134             :   ///    KeyVerificationMethod.emoji: Compare emojis
     135             :   /// [importantStateEvents]: A set of all the important state events to load when the client connects.
     136             :   ///    To speed up performance only a set of state events is loaded on startup, those that are
     137             :   ///    needed to display a room list. All the remaining state events are automatically post-loaded
     138             :   ///    when opening the timeline of a room or manually by calling `room.postLoad()`.
     139             :   ///    This set will always include the following state events:
     140             :   ///     - m.room.name
     141             :   ///     - m.room.avatar
     142             :   ///     - m.room.message
     143             :   ///     - m.room.encrypted
     144             :   ///     - m.room.encryption
     145             :   ///     - m.room.canonical_alias
     146             :   ///     - m.room.tombstone
     147             :   ///     - *some* m.room.member events, where needed
     148             :   /// [roomPreviewLastEvents]: The event types that should be used to calculate the last event
     149             :   ///     in a room for the room list.
     150             :   /// Set [requestHistoryOnLimitedTimeline] to controll the automatic behaviour if the client
     151             :   /// receives a limited timeline flag for a room.
     152             :   /// If [mxidLocalPartFallback] is true, then the local part of the mxid will be shown
     153             :   /// if there is no other displayname available. If not then this will return "Unknown user".
     154             :   /// If [formatLocalpart] is true, then the localpart of an mxid will
     155             :   /// be formatted in the way, that all "_" characters are becomming white spaces and
     156             :   /// the first character of each word becomes uppercase.
     157             :   /// If your client supports more login types like login with token or SSO, then add this to
     158             :   /// [supportedLoginTypes]. Set a custom [syncFilter] if you like. By default the app
     159             :   /// will use lazy_load_members.
     160             :   /// Set [nativeImplementations] to [NativeImplementationsIsolate] in order to
     161             :   /// enable the SDK to compute some code in background.
     162             :   /// Set [timelineEventTimeout] to the preferred time the Client should retry
     163             :   /// sending events on connection problems or to `Duration.zero` to disable it.
     164             :   /// Set [customImageResizer] to your own implementation for a more advanced
     165             :   /// and faster image resizing experience.
     166             :   /// Set [enableDehydratedDevices] to enable experimental support for enabling MSC3814 dehydrated devices.
     167          38 :   Client(
     168             :     this.clientName, {
     169             :     this.databaseBuilder,
     170             :     this.legacyDatabaseBuilder,
     171             :     Set<KeyVerificationMethod>? verificationMethods,
     172             :     http.Client? httpClient,
     173             :     Set<String>? importantStateEvents,
     174             : 
     175             :     /// You probably don't want to add state events which are also
     176             :     /// in important state events to this list, or get ready to face
     177             :     /// only having one event of that particular type in preLoad because
     178             :     /// previewEvents are stored with stateKey '' not the actual state key
     179             :     /// of your state event
     180             :     Set<String>? roomPreviewLastEvents,
     181             :     this.pinUnreadRooms = false,
     182             :     this.pinInvitedRooms = true,
     183             :     @Deprecated('Use [sendTimelineEventTimeout] instead.')
     184             :     int? sendMessageTimeoutSeconds,
     185             :     this.requestHistoryOnLimitedTimeline = false,
     186             :     Set<String>? supportedLoginTypes,
     187             :     this.mxidLocalPartFallback = true,
     188             :     this.formatLocalpart = true,
     189             :     @Deprecated('Use [nativeImplementations] instead') this.compute,
     190             :     NativeImplementations nativeImplementations = NativeImplementations.dummy,
     191             :     Level? logLevel,
     192             :     Filter? syncFilter,
     193             :     Duration defaultNetworkRequestTimeout = const Duration(seconds: 35),
     194             :     this.sendTimelineEventTimeout = const Duration(minutes: 1),
     195             :     this.customImageResizer,
     196             :     this.shareKeysWithUnverifiedDevices = true,
     197             :     this.enableDehydratedDevices = false,
     198             :     this.receiptsPublicByDefault = true,
     199             : 
     200             :     /// Implement your https://spec.matrix.org/v1.9/client-server-api/#soft-logout
     201             :     /// logic here.
     202             :     /// Set this to `refreshAccessToken()` for the easiest way to handle the
     203             :     /// most common reason for soft logouts.
     204             :     /// You can also perform a new login here by passing the existing deviceId.
     205             :     this.onSoftLogout,
     206             : 
     207             :     /// Experimental feature which allows to send a custom refresh token
     208             :     /// lifetime to the server which overrides the default one. Needs server
     209             :     /// support.
     210             :     this.customRefreshTokenLifetime,
     211             :     this.typingIndicatorTimeout = const Duration(seconds: 30),
     212             :   })  : syncFilter = syncFilter ??
     213          38 :             Filter(
     214          38 :               room: RoomFilter(
     215          38 :                 state: StateFilter(lazyLoadMembers: true),
     216             :               ),
     217             :             ),
     218             :         importantStateEvents = importantStateEvents ??= {},
     219             :         roomPreviewLastEvents = roomPreviewLastEvents ??= {},
     220             :         supportedLoginTypes =
     221          38 :             supportedLoginTypes ?? {AuthenticationTypes.password},
     222             :         verificationMethods = verificationMethods ?? <KeyVerificationMethod>{},
     223             :         nativeImplementations = compute != null
     224           0 :             ? NativeImplementationsIsolate(compute)
     225             :             : nativeImplementations,
     226          38 :         super(
     227          38 :             httpClient: FixedTimeoutHttpClient(
     228           5 :                 httpClient ?? http.Client(), defaultNetworkRequestTimeout)) {
     229           0 :     if (logLevel != null) Logs().level = logLevel;
     230          76 :     importantStateEvents.addAll([
     231             :       EventTypes.RoomName,
     232             :       EventTypes.RoomAvatar,
     233             :       EventTypes.Encryption,
     234             :       EventTypes.RoomCanonicalAlias,
     235             :       EventTypes.RoomTombstone,
     236             :       EventTypes.SpaceChild,
     237             :       EventTypes.SpaceParent,
     238             :       EventTypes.RoomCreate,
     239             :     ]);
     240          76 :     roomPreviewLastEvents.addAll([
     241             :       EventTypes.Message,
     242             :       EventTypes.Encrypted,
     243             :       EventTypes.Sticker,
     244             :       EventTypes.CallInvite,
     245             :       EventTypes.CallAnswer,
     246             :       EventTypes.CallReject,
     247             :       EventTypes.CallHangup,
     248             :       EventTypes.GroupCallMember,
     249             :     ]);
     250             : 
     251             :     // register all the default commands
     252          38 :     registerDefaultCommands();
     253             :   }
     254             : 
     255             :   Duration? customRefreshTokenLifetime;
     256             : 
     257             :   /// Fetches the refreshToken from the database and tries to get a new
     258             :   /// access token from the server and then stores it correctly. Unlike the
     259             :   /// pure API call of `Client.refresh()` this handles the complete soft
     260             :   /// logout case.
     261             :   /// Throws an Exception if there is no refresh token available or the
     262             :   /// client is not logged in.
     263           1 :   Future<void> refreshAccessToken() async {
     264           3 :     final storedClient = await database?.getClient(clientName);
     265           1 :     final refreshToken = storedClient?.tryGet<String>('refresh_token');
     266             :     if (refreshToken == null) {
     267           0 :       throw Exception('No refresh token available');
     268             :     }
     269           2 :     final homeserverUrl = homeserver?.toString();
     270           1 :     final userId = userID;
     271           1 :     final deviceId = deviceID;
     272             :     if (homeserverUrl == null || userId == null || deviceId == null) {
     273           0 :       throw Exception('Cannot refresh access token when not logged in');
     274             :     }
     275             : 
     276           1 :     final tokenResponse = await refreshWithCustomRefreshTokenLifetime(
     277             :       refreshToken,
     278           1 :       refreshTokenLifetimeMs: customRefreshTokenLifetime?.inMilliseconds,
     279             :     );
     280             : 
     281           2 :     accessToken = tokenResponse.accessToken;
     282           1 :     final expiresInMs = tokenResponse.expiresInMs;
     283             :     final tokenExpiresAt = expiresInMs == null
     284             :         ? null
     285           3 :         : DateTime.now().add(Duration(milliseconds: expiresInMs));
     286           1 :     _accessTokenExpiresAt = tokenExpiresAt;
     287           2 :     await database?.updateClient(
     288             :       homeserverUrl,
     289           1 :       tokenResponse.accessToken,
     290             :       tokenExpiresAt,
     291           1 :       tokenResponse.refreshToken,
     292             :       userId,
     293             :       deviceId,
     294           1 :       deviceName,
     295           1 :       prevBatch,
     296           2 :       encryption?.pickledOlmAccount,
     297             :     );
     298             :   }
     299             : 
     300             :   /// The required name for this client.
     301             :   final String clientName;
     302             : 
     303             :   /// The Matrix ID of the current logged user.
     304          64 :   String? get userID => _userID;
     305             :   String? _userID;
     306             : 
     307             :   /// This points to the position in the synchronization history.
     308          62 :   String? get prevBatch => _prevBatch;
     309             :   String? _prevBatch;
     310             : 
     311             :   /// The device ID is an unique identifier for this device.
     312          60 :   String? get deviceID => _deviceID;
     313             :   String? _deviceID;
     314             : 
     315             :   /// The device name is a human readable identifier for this device.
     316           2 :   String? get deviceName => _deviceName;
     317             :   String? _deviceName;
     318             : 
     319             :   // for group calls
     320             :   // A unique identifier used for resolving duplicate group call
     321             :   // sessions from a given device. When the session_id field changes from
     322             :   // an incoming m.call.member event, any existing calls from this device in
     323             :   // this call should be terminated. The id is generated once per client load.
     324           0 :   String? get groupCallSessionId => _groupCallSessionId;
     325             :   String? _groupCallSessionId;
     326             : 
     327             :   /// Returns the current login state.
     328           0 :   @Deprecated('Use [onLoginStateChanged.value] instead')
     329             :   LoginState get loginState =>
     330           0 :       onLoginStateChanged.value ?? LoginState.loggedOut;
     331             : 
     332          62 :   bool isLogged() => accessToken != null;
     333             : 
     334             :   /// A list of all rooms the user is participating or invited.
     335          68 :   List<Room> get rooms => _rooms;
     336             :   List<Room> _rooms = [];
     337             : 
     338             :   /// Get a list of the archived rooms
     339             :   ///
     340             :   /// Attention! Archived rooms are only returned if [loadArchive()] was called
     341             :   /// beforehand! The state refers to the last retrieval via [loadArchive()]!
     342           2 :   List<ArchivedRoom> get archivedRooms => _archivedRooms;
     343             : 
     344             :   bool enableDehydratedDevices = false;
     345             : 
     346             :   /// Whether read receipts are sent as public receipts by default or just as private receipts.
     347             :   bool receiptsPublicByDefault = true;
     348             : 
     349             :   /// Whether this client supports end-to-end encryption using olm.
     350         117 :   bool get encryptionEnabled => encryption?.enabled == true;
     351             : 
     352             :   /// Whether this client is able to encrypt and decrypt files.
     353           0 :   bool get fileEncryptionEnabled => encryptionEnabled;
     354             : 
     355          18 :   String get identityKey => encryption?.identityKey ?? '';
     356             : 
     357          81 :   String get fingerprintKey => encryption?.fingerprintKey ?? '';
     358             : 
     359             :   /// Whether this session is unknown to others
     360          24 :   bool get isUnknownSession =>
     361         132 :       userDeviceKeys[userID]?.deviceKeys[deviceID]?.signed != true;
     362             : 
     363             :   /// Warning! This endpoint is for testing only!
     364           0 :   set rooms(List<Room> newList) {
     365           0 :     Logs().w('Warning! This endpoint is for testing only!');
     366           0 :     _rooms = newList;
     367             :   }
     368             : 
     369             :   /// Key/Value store of account data.
     370             :   Map<String, BasicEvent> _accountData = {};
     371             : 
     372          62 :   Map<String, BasicEvent> get accountData => _accountData;
     373             : 
     374             :   /// Evaluate if an event should notify quickly
     375           0 :   PushruleEvaluator get pushruleEvaluator =>
     376           0 :       _pushruleEvaluator ?? PushruleEvaluator.fromRuleset(PushRuleSet());
     377             :   PushruleEvaluator? _pushruleEvaluator;
     378             : 
     379          31 :   void _updatePushrules() {
     380          31 :     final ruleset = TryGetPushRule.tryFromJson(
     381          62 :         _accountData[EventTypes.PushRules]
     382          31 :                 ?.content
     383          31 :                 .tryGetMap<String, Object?>('global') ??
     384          29 :             {});
     385          62 :     _pushruleEvaluator = PushruleEvaluator.fromRuleset(ruleset);
     386             :   }
     387             : 
     388             :   /// Presences of users by a given matrix ID
     389             :   @Deprecated('Use `fetchCurrentPresence(userId)` instead.')
     390             :   Map<String, CachedPresence> presences = {};
     391             : 
     392             :   int _transactionCounter = 0;
     393             : 
     394          13 :   String generateUniqueTransactionId() {
     395          26 :     _transactionCounter++;
     396          65 :     return '$clientName-$_transactionCounter-${DateTime.now().millisecondsSinceEpoch}';
     397             :   }
     398             : 
     399           1 :   Room? getRoomByAlias(String alias) {
     400           2 :     for (final room in rooms) {
     401           2 :       if (room.canonicalAlias == alias) return room;
     402             :     }
     403             :     return null;
     404             :   }
     405             : 
     406             :   /// Searches in the local cache for the given room and returns null if not
     407             :   /// found. If you have loaded the [loadArchive()] before, it can also return
     408             :   /// archived rooms.
     409          32 :   Room? getRoomById(String id) {
     410         161 :     for (final room in <Room>[...rooms, ..._archivedRooms.map((e) => e.room)]) {
     411          58 :       if (room.id == id) return room;
     412             :     }
     413             : 
     414             :     return null;
     415             :   }
     416             : 
     417          32 :   Map<String, dynamic> get directChats =>
     418         113 :       _accountData['m.direct']?.content ?? {};
     419             : 
     420             :   /// Returns the (first) room ID from the store which is a private chat with the user [userId].
     421             :   /// Returns null if there is none.
     422           6 :   String? getDirectChatFromUserId(String userId) {
     423          24 :     final directChats = _accountData['m.direct']?.content[userId];
     424           7 :     if (directChats is List<dynamic> && directChats.isNotEmpty) {
     425             :       final potentialRooms = directChats
     426           1 :           .cast<String>()
     427           2 :           .map(getRoomById)
     428           4 :           .where((room) => room != null && room.membership == Membership.join);
     429           1 :       if (potentialRooms.isNotEmpty) {
     430           2 :         return potentialRooms.fold<Room>(potentialRooms.first!,
     431           1 :             (Room prev, Room? r) {
     432             :           if (r == null) {
     433             :             return prev;
     434             :           }
     435           2 :           final prevLast = prev.lastEvent?.originServerTs ?? DateTime(0);
     436           2 :           final rLast = r.lastEvent?.originServerTs ?? DateTime(0);
     437             : 
     438           1 :           return rLast.isAfter(prevLast) ? r : prev;
     439           1 :         }).id;
     440             :       }
     441             :     }
     442          10 :     for (final room in rooms) {
     443           8 :       if (room.membership == Membership.invite &&
     444          12 :           room.getState(EventTypes.RoomMember, userID!)?.senderId == userId &&
     445           0 :           room.getState(EventTypes.RoomMember, userID!)?.content['is_direct'] ==
     446             :               true) {
     447           0 :         return room.id;
     448             :       }
     449             :     }
     450             :     return null;
     451             :   }
     452             : 
     453             :   /// Gets discovery information about the domain. The file may include additional keys.
     454           0 :   Future<DiscoveryInformation> getDiscoveryInformationsByUserId(
     455             :     String MatrixIdOrDomain,
     456             :   ) async {
     457             :     try {
     458           0 :       final response = await httpClient.get(Uri.https(
     459           0 :           MatrixIdOrDomain.domain ?? '', '/.well-known/matrix/client'));
     460           0 :       var respBody = response.body;
     461             :       try {
     462           0 :         respBody = utf8.decode(response.bodyBytes);
     463             :       } catch (_) {
     464             :         // No-OP
     465             :       }
     466           0 :       final rawJson = json.decode(respBody);
     467           0 :       return DiscoveryInformation.fromJson(rawJson);
     468             :     } catch (_) {
     469             :       // we got an error processing or fetching the well-known information, let's
     470             :       // provide a reasonable fallback.
     471           0 :       return DiscoveryInformation(
     472           0 :         mHomeserver: HomeserverInformation(
     473           0 :             baseUrl: Uri.https(MatrixIdOrDomain.domain ?? '', '')),
     474             :       );
     475             :     }
     476             :   }
     477             : 
     478             :   /// Checks the supported versions of the Matrix protocol and the supported
     479             :   /// login types. Throws an exception if the server is not compatible with the
     480             :   /// client and sets [homeserver] to [homeserverUrl] if it is. Supports the
     481             :   /// types `Uri` and `String`.
     482          33 :   Future<
     483             :       (
     484             :         DiscoveryInformation?,
     485             :         GetVersionsResponse versions,
     486             :         List<LoginFlow>,
     487             :       )> checkHomeserver(
     488             :     Uri homeserverUrl, {
     489             :     bool checkWellKnown = true,
     490             :     Set<String>? overrideSupportedVersions,
     491             :   }) async {
     492             :     final supportedVersions =
     493             :         overrideSupportedVersions ?? Client.supportedVersions;
     494             :     try {
     495          66 :       homeserver = homeserverUrl.stripTrailingSlash();
     496             : 
     497             :       // Look up well known
     498             :       DiscoveryInformation? wellKnown;
     499             :       if (checkWellKnown) {
     500             :         try {
     501           1 :           wellKnown = await getWellknown();
     502           0 :           homeserver = wellKnown.mHomeserver.baseUrl.stripTrailingSlash();
     503             :         } catch (e) {
     504           2 :           Logs().v('Found no well known information', e);
     505             :         }
     506             :       }
     507             : 
     508             :       // Check if server supports at least one supported version
     509          33 :       final versions = await getVersions();
     510          33 :       if (!versions.versions
     511          99 :           .any((version) => supportedVersions.contains(version))) {
     512           0 :         throw BadServerVersionsException(
     513           0 :           versions.versions.toSet(),
     514             :           supportedVersions,
     515             :         );
     516             :       }
     517             : 
     518          33 :       final loginTypes = await getLoginFlows() ?? [];
     519         165 :       if (!loginTypes.any((f) => supportedLoginTypes.contains(f.type))) {
     520           0 :         throw BadServerLoginTypesException(
     521           0 :             loginTypes.map((f) => f.type ?? '').toSet(), supportedLoginTypes);
     522             :       }
     523             : 
     524             :       return (wellKnown, versions, loginTypes);
     525             :     } catch (_) {
     526           1 :       homeserver = null;
     527             :       rethrow;
     528             :     }
     529             :   }
     530             : 
     531             :   /// Checks to see if a username is available, and valid, for the server.
     532             :   /// Returns the fully-qualified Matrix user ID (MXID) that has been registered.
     533             :   /// You have to call [checkHomeserver] first to set a homeserver.
     534           0 :   @override
     535             :   Future<RegisterResponse> register({
     536             :     String? username,
     537             :     String? password,
     538             :     String? deviceId,
     539             :     String? initialDeviceDisplayName,
     540             :     bool? inhibitLogin,
     541             :     bool? refreshToken,
     542             :     AuthenticationData? auth,
     543             :     AccountKind? kind,
     544             :   }) async {
     545           0 :     final response = await super.register(
     546             :       kind: kind,
     547             :       username: username,
     548             :       password: password,
     549             :       auth: auth,
     550             :       deviceId: deviceId,
     551             :       initialDeviceDisplayName: initialDeviceDisplayName,
     552             :       inhibitLogin: inhibitLogin,
     553           0 :       refreshToken: refreshToken ?? onSoftLogout != null,
     554             :     );
     555             : 
     556             :     // Connect if there is an access token in the response.
     557           0 :     final accessToken = response.accessToken;
     558           0 :     final deviceId_ = response.deviceId;
     559           0 :     final userId = response.userId;
     560           0 :     final homeserver = this.homeserver;
     561             :     if (accessToken == null || deviceId_ == null || homeserver == null) {
     562           0 :       throw Exception(
     563             :           'Registered but token, device ID, user ID or homeserver is null.');
     564             :     }
     565           0 :     final expiresInMs = response.expiresInMs;
     566             :     final tokenExpiresAt = expiresInMs == null
     567             :         ? null
     568           0 :         : DateTime.now().add(Duration(milliseconds: expiresInMs));
     569             : 
     570           0 :     await init(
     571             :         newToken: accessToken,
     572             :         newTokenExpiresAt: tokenExpiresAt,
     573           0 :         newRefreshToken: response.refreshToken,
     574             :         newUserID: userId,
     575             :         newHomeserver: homeserver,
     576             :         newDeviceName: initialDeviceDisplayName ?? '',
     577             :         newDeviceID: deviceId_);
     578             :     return response;
     579             :   }
     580             : 
     581             :   /// Handles the login and allows the client to call all APIs which require
     582             :   /// authentication. Returns false if the login was not successful. Throws
     583             :   /// MatrixException if login was not successful.
     584             :   /// To just login with the username 'alice' you set [identifier] to:
     585             :   /// `AuthenticationUserIdentifier(user: 'alice')`
     586             :   /// Maybe you want to set [user] to the same String to stay compatible with
     587             :   /// older server versions.
     588           4 :   @override
     589             :   Future<LoginResponse> login(
     590             :     LoginType type, {
     591             :     AuthenticationIdentifier? identifier,
     592             :     String? password,
     593             :     String? token,
     594             :     String? deviceId,
     595             :     String? initialDeviceDisplayName,
     596             :     bool? refreshToken,
     597             :     @Deprecated('Deprecated in favour of identifier.') String? user,
     598             :     @Deprecated('Deprecated in favour of identifier.') String? medium,
     599             :     @Deprecated('Deprecated in favour of identifier.') String? address,
     600             :   }) async {
     601           4 :     if (homeserver == null) {
     602           0 :       final domain = identifier is AuthenticationUserIdentifier
     603           0 :           ? identifier.user.domain
     604             :           : null;
     605             :       if (domain != null) {
     606           0 :         await checkHomeserver(Uri.https(domain, ''));
     607             :       } else {
     608           0 :         throw Exception('No homeserver specified!');
     609             :       }
     610             :     }
     611           4 :     final response = await super.login(
     612             :       type,
     613             :       identifier: identifier,
     614             :       password: password,
     615             :       token: token,
     616             :       deviceId: deviceId,
     617             :       initialDeviceDisplayName: initialDeviceDisplayName,
     618             :       // ignore: deprecated_member_use
     619             :       user: user,
     620             :       // ignore: deprecated_member_use
     621             :       medium: medium,
     622             :       // ignore: deprecated_member_use
     623             :       address: address,
     624           4 :       refreshToken: refreshToken ?? onSoftLogout != null,
     625             :     );
     626             : 
     627             :     // Connect if there is an access token in the response.
     628           4 :     final accessToken = response.accessToken;
     629           4 :     final deviceId_ = response.deviceId;
     630           4 :     final userId = response.userId;
     631           4 :     final homeserver_ = homeserver;
     632             :     if (homeserver_ == null) {
     633           0 :       throw Exception('Registered but homerserver is null.');
     634             :     }
     635             : 
     636           4 :     final expiresInMs = response.expiresInMs;
     637             :     final tokenExpiresAt = expiresInMs == null
     638             :         ? null
     639           0 :         : DateTime.now().add(Duration(milliseconds: expiresInMs));
     640             : 
     641           4 :     await init(
     642             :       newToken: accessToken,
     643             :       newTokenExpiresAt: tokenExpiresAt,
     644           4 :       newRefreshToken: response.refreshToken,
     645             :       newUserID: userId,
     646             :       newHomeserver: homeserver_,
     647             :       newDeviceName: initialDeviceDisplayName ?? '',
     648             :       newDeviceID: deviceId_,
     649             :     );
     650             :     return response;
     651             :   }
     652             : 
     653             :   /// Sends a logout command to the homeserver and clears all local data,
     654             :   /// including all persistent data from the store.
     655           9 :   @override
     656             :   Future<void> logout() async {
     657             :     try {
     658             :       // Upload keys to make sure all are cached on the next login.
     659          21 :       await encryption?.keyManager.uploadInboundGroupSessions();
     660           9 :       await super.logout();
     661             :     } catch (e, s) {
     662           2 :       Logs().e('Logout failed', e, s);
     663             :       rethrow;
     664             :     } finally {
     665           9 :       await clear();
     666             :     }
     667             :   }
     668             : 
     669             :   /// Sends a logout command to the homeserver and clears all local data,
     670             :   /// including all persistent data from the store.
     671           0 :   @override
     672             :   Future<void> logoutAll() async {
     673             :     // Upload keys to make sure all are cached on the next login.
     674           0 :     await encryption?.keyManager.uploadInboundGroupSessions();
     675             : 
     676           0 :     final futures = <Future>[];
     677           0 :     futures.add(super.logoutAll());
     678           0 :     futures.add(clear());
     679           0 :     await Future.wait(futures).catchError((e, s) {
     680           0 :       Logs().e('Logout all failed', e, s);
     681             :       throw e;
     682             :     });
     683             :   }
     684             : 
     685             :   /// Run any request and react on user interactive authentication flows here.
     686           1 :   Future<T> uiaRequestBackground<T>(
     687             :       Future<T> Function(AuthenticationData? auth) request) {
     688           1 :     final completer = Completer<T>();
     689             :     UiaRequest? uia;
     690           1 :     uia = UiaRequest(
     691             :       request: request,
     692           1 :       onUpdate: (state) {
     693             :         if (uia != null) {
     694           1 :           if (state == UiaRequestState.done) {
     695           2 :             completer.complete(uia.result);
     696           0 :           } else if (state == UiaRequestState.fail) {
     697           0 :             completer.completeError(uia.error!);
     698             :           } else {
     699           0 :             onUiaRequest.add(uia);
     700             :           }
     701             :         }
     702             :       },
     703             :     );
     704           1 :     return completer.future;
     705             :   }
     706             : 
     707             :   /// Returns an existing direct room ID with this user or creates a new one.
     708             :   /// By default encryption will be enabled if the client supports encryption
     709             :   /// and the other user has uploaded any encryption keys.
     710           6 :   Future<String> startDirectChat(
     711             :     String mxid, {
     712             :     bool? enableEncryption,
     713             :     List<StateEvent>? initialState,
     714             :     bool waitForSync = true,
     715             :     Map<String, dynamic>? powerLevelContentOverride,
     716             :     CreateRoomPreset? preset = CreateRoomPreset.trustedPrivateChat,
     717             :   }) async {
     718             :     // Try to find an existing direct chat
     719           6 :     final directChatRoomId = getDirectChatFromUserId(mxid);
     720             :     if (directChatRoomId != null) {
     721           0 :       final room = getRoomById(directChatRoomId);
     722             :       if (room != null) {
     723           0 :         if (room.membership == Membership.join) {
     724             :           return directChatRoomId;
     725           0 :         } else if (room.membership == Membership.invite) {
     726             :           // we might already have an invite into a DM room. If that is the case, we should try to join. If the room is
     727             :           // unjoinable, that will automatically leave the room, so in that case we need to continue creating a new
     728             :           // room. (This implicitly also prevents the room from being returned as a DM room by getDirectChatFromUserId,
     729             :           // because it only returns joined or invited rooms atm.)
     730           0 :           await room.join();
     731           0 :           if (room.membership != Membership.leave) {
     732             :             if (waitForSync) {
     733           0 :               if (room.membership != Membership.join) {
     734             :                 // Wait for room actually appears in sync with the right membership
     735           0 :                 await waitForRoomInSync(directChatRoomId, join: true);
     736             :               }
     737             :             }
     738             :             return directChatRoomId;
     739             :           }
     740             :         }
     741             :       }
     742             :     }
     743             : 
     744             :     enableEncryption ??=
     745           5 :         encryptionEnabled && await userOwnsEncryptionKeys(mxid);
     746             :     if (enableEncryption) {
     747           2 :       initialState ??= [];
     748           2 :       if (!initialState.any((s) => s.type == EventTypes.Encryption)) {
     749           4 :         initialState.add(StateEvent(
     750           2 :           content: {
     751           2 :             'algorithm': supportedGroupEncryptionAlgorithms.first,
     752             :           },
     753             :           type: EventTypes.Encryption,
     754             :         ));
     755             :       }
     756             :     }
     757             : 
     758             :     // Start a new direct chat
     759           6 :     final roomId = await createRoom(
     760           6 :       invite: [mxid],
     761             :       isDirect: true,
     762             :       preset: preset,
     763             :       initialState: initialState,
     764             :       powerLevelContentOverride: powerLevelContentOverride,
     765             :     );
     766             : 
     767             :     if (waitForSync) {
     768           1 :       final room = getRoomById(roomId);
     769           2 :       if (room == null || room.membership != Membership.join) {
     770             :         // Wait for room actually appears in sync
     771           0 :         await waitForRoomInSync(roomId, join: true);
     772             :       }
     773             :     }
     774             : 
     775          12 :     await Room(id: roomId, client: this).addToDirectChat(mxid);
     776             : 
     777             :     return roomId;
     778             :   }
     779             : 
     780             :   /// Simplified method to create a new group chat. By default it is a private
     781             :   /// chat. The encryption is enabled if this client supports encryption and
     782             :   /// the preset is not a public chat.
     783           2 :   Future<String> createGroupChat({
     784             :     String? groupName,
     785             :     bool? enableEncryption,
     786             :     List<String>? invite,
     787             :     CreateRoomPreset preset = CreateRoomPreset.privateChat,
     788             :     List<StateEvent>? initialState,
     789             :     Visibility? visibility,
     790             :     HistoryVisibility? historyVisibility,
     791             :     bool waitForSync = true,
     792             :     bool groupCall = false,
     793             :     Map<String, dynamic>? powerLevelContentOverride,
     794             :   }) async {
     795             :     enableEncryption ??=
     796           2 :         encryptionEnabled && preset != CreateRoomPreset.publicChat;
     797             :     if (enableEncryption) {
     798           1 :       initialState ??= [];
     799           1 :       if (!initialState.any((s) => s.type == EventTypes.Encryption)) {
     800           2 :         initialState.add(StateEvent(
     801           1 :           content: {
     802           1 :             'algorithm': supportedGroupEncryptionAlgorithms.first,
     803             :           },
     804             :           type: EventTypes.Encryption,
     805             :         ));
     806             :       }
     807             :     }
     808             :     if (historyVisibility != null) {
     809           0 :       initialState ??= [];
     810           0 :       if (!initialState.any((s) => s.type == EventTypes.HistoryVisibility)) {
     811           0 :         initialState.add(StateEvent(
     812           0 :           content: {
     813           0 :             'history_visibility': historyVisibility.text,
     814             :           },
     815             :           type: EventTypes.HistoryVisibility,
     816             :         ));
     817             :       }
     818             :     }
     819             :     if (groupCall) {
     820           1 :       powerLevelContentOverride ??= {};
     821           2 :       powerLevelContentOverride['events'] ??= {};
     822           2 :       powerLevelContentOverride['events'][EventTypes.GroupCallMember] ??=
     823           1 :           powerLevelContentOverride['events_default'] ?? 0;
     824             :     }
     825             : 
     826           2 :     final roomId = await createRoom(
     827             :       invite: invite,
     828             :       preset: preset,
     829             :       name: groupName,
     830             :       initialState: initialState,
     831             :       visibility: visibility,
     832             :       powerLevelContentOverride: powerLevelContentOverride,
     833             :     );
     834             : 
     835             :     if (waitForSync) {
     836           1 :       if (getRoomById(roomId) == null) {
     837             :         // Wait for room actually appears in sync
     838           0 :         await waitForRoomInSync(roomId, join: true);
     839             :       }
     840             :     }
     841             :     return roomId;
     842             :   }
     843             : 
     844             :   /// Wait for the room to appear into the enabled section of the room sync.
     845             :   /// By default, the function will listen for room in invite, join and leave
     846             :   /// sections of the sync.
     847           0 :   Future<SyncUpdate> waitForRoomInSync(String roomId,
     848             :       {bool join = false, bool invite = false, bool leave = false}) async {
     849             :     if (!join && !invite && !leave) {
     850             :       join = true;
     851             :       invite = true;
     852             :       leave = true;
     853             :     }
     854             : 
     855             :     // Wait for the next sync where this room appears.
     856           0 :     final syncUpdate = await onSync.stream.firstWhere((sync) =>
     857           0 :         invite && (sync.rooms?.invite?.containsKey(roomId) ?? false) ||
     858           0 :         join && (sync.rooms?.join?.containsKey(roomId) ?? false) ||
     859           0 :         leave && (sync.rooms?.leave?.containsKey(roomId) ?? false));
     860             : 
     861             :     // Wait for this sync to be completely processed.
     862           0 :     await onSyncStatus.stream.firstWhere(
     863           0 :       (syncStatus) => syncStatus.status == SyncStatus.finished,
     864             :     );
     865             :     return syncUpdate;
     866             :   }
     867             : 
     868             :   /// Checks if the given user has encryption keys. May query keys from the
     869             :   /// server to answer this.
     870           2 :   Future<bool> userOwnsEncryptionKeys(String userId) async {
     871           4 :     if (userId == userID) return encryptionEnabled;
     872           6 :     if (_userDeviceKeys[userId]?.deviceKeys.isNotEmpty ?? false) {
     873             :       return true;
     874             :     }
     875           3 :     final keys = await queryKeys({userId: []});
     876           3 :     return keys.deviceKeys?[userId]?.isNotEmpty ?? false;
     877             :   }
     878             : 
     879             :   /// Creates a new space and returns the Room ID. The parameters are mostly
     880             :   /// the same like in [createRoom()].
     881             :   /// Be aware that spaces appear in the [rooms] list. You should check if a
     882             :   /// room is a space by using the `room.isSpace` getter and then just use the
     883             :   /// room as a space with `room.toSpace()`.
     884             :   ///
     885             :   /// https://github.com/matrix-org/matrix-doc/blob/matthew/msc1772/proposals/1772-groups-as-rooms.md
     886           1 :   Future<String> createSpace(
     887             :       {String? name,
     888             :       String? topic,
     889             :       Visibility visibility = Visibility.public,
     890             :       String? spaceAliasName,
     891             :       List<String>? invite,
     892             :       List<Invite3pid>? invite3pid,
     893             :       String? roomVersion,
     894             :       bool waitForSync = false}) async {
     895           1 :     final id = await createRoom(
     896             :       name: name,
     897             :       topic: topic,
     898             :       visibility: visibility,
     899             :       roomAliasName: spaceAliasName,
     900           1 :       creationContent: {'type': 'm.space'},
     901           1 :       powerLevelContentOverride: {'events_default': 100},
     902             :       invite: invite,
     903             :       invite3pid: invite3pid,
     904             :       roomVersion: roomVersion,
     905             :     );
     906             : 
     907             :     if (waitForSync) {
     908           0 :       await waitForRoomInSync(id, join: true);
     909             :     }
     910             : 
     911             :     return id;
     912             :   }
     913             : 
     914           0 :   @Deprecated('Use fetchOwnProfile() instead')
     915           0 :   Future<Profile> get ownProfile => fetchOwnProfile();
     916             : 
     917             :   /// Returns the user's own displayname and avatar url. In Matrix it is possible that
     918             :   /// one user can have different displaynames and avatar urls in different rooms.
     919             :   /// Tries to get the profile from homeserver first, if failed, falls back to a profile
     920             :   /// from a room where the user exists. Set `useServerCache` to true to get any
     921             :   /// prior value from this function
     922           0 :   Future<Profile> fetchOwnProfileFromServer(
     923             :       {bool useServerCache = false}) async {
     924             :     try {
     925           0 :       return await getProfileFromUserId(
     926           0 :         userID!,
     927             :         getFromRooms: false,
     928             :         cache: useServerCache,
     929             :       );
     930             :     } catch (e) {
     931           0 :       Logs().w(
     932             :           '[Matrix] getting profile from homeserver failed, falling back to first room with required profile');
     933           0 :       return await getProfileFromUserId(
     934           0 :         userID!,
     935             :         getFromRooms: true,
     936             :         cache: true,
     937             :       );
     938             :     }
     939             :   }
     940             : 
     941             :   /// Returns the user's own displayname and avatar url. In Matrix it is possible that
     942             :   /// one user can have different displaynames and avatar urls in different rooms.
     943             :   /// This returns the profile from the first room by default, override `getFromRooms`
     944             :   /// to false to fetch from homeserver.
     945           1 :   Future<Profile> fetchOwnProfile({
     946             :     bool getFromRooms = true,
     947             :     bool cache = true,
     948             :   }) =>
     949           1 :       getProfileFromUserId(
     950           1 :         userID!,
     951             :         getFromRooms: getFromRooms,
     952             :         cache: cache,
     953             :       );
     954             : 
     955             :   final Map<String, ProfileInformation> _profileRoomsCache = {};
     956             :   final Map<String, ProfileInformation> _profileServerCache = {};
     957             : 
     958             :   /// Get the combined profile information for this user.
     959             :   /// If [getFromRooms] is true then the profile will first be searched from the
     960             :   /// room memberships. This is unstable if the given user makes use of different displaynames
     961             :   /// and avatars per room, which is common for some bots and bridges.
     962             :   /// If [cache] is true then
     963             :   /// the profile get cached for this session. Please note that then the profile may
     964             :   /// become outdated if the user changes the displayname or avatar in this session.
     965           4 :   Future<Profile> getProfileFromUserId(String userId,
     966             :       {bool cache = true, bool getFromRooms = true}) async {
     967             :     var profile =
     968          10 :         getFromRooms ? _profileRoomsCache[userId] : _profileServerCache[userId];
     969             :     if (cache && profile != null) {
     970           0 :       return Profile(
     971             :         userId: userId,
     972           0 :         displayName: profile.displayname,
     973           0 :         avatarUrl: profile.avatarUrl,
     974             :       );
     975             :     }
     976             : 
     977             :     if (getFromRooms) {
     978          11 :       final room = rooms.firstWhereOrNull((Room room) =>
     979          18 :           room.getParticipants().indexWhere((User user) => user.id == userId) !=
     980           3 :           -1);
     981             :       if (room != null) {
     982             :         final user =
     983           5 :             room.getParticipants().firstWhere((User user) => user.id == userId);
     984           1 :         final profileFromRooms = Profile(
     985             :           userId: userId,
     986           1 :           displayName: user.displayName,
     987           1 :           avatarUrl: user.avatarUrl,
     988             :         );
     989           3 :         _profileRoomsCache[userId] = ProfileInformation(
     990           1 :           avatarUrl: profileFromRooms.avatarUrl,
     991           1 :           displayname: profileFromRooms.displayName,
     992             :         );
     993             :         return profileFromRooms;
     994             :       }
     995             :     }
     996           4 :     profile = await getUserProfile(userId);
     997           0 :     if (cache || _profileServerCache.containsKey(userId)) {
     998           6 :       _profileServerCache[userId] = profile;
     999             :     }
    1000           3 :     return Profile(
    1001             :       userId: userId,
    1002           3 :       displayName: profile.displayname,
    1003           3 :       avatarUrl: profile.avatarUrl,
    1004             :     );
    1005             :   }
    1006             : 
    1007             :   final List<ArchivedRoom> _archivedRooms = [];
    1008             : 
    1009             :   /// Return an archive room containing the room and the timeline for a specific archived room.
    1010           2 :   ArchivedRoom? getArchiveRoomFromCache(String roomId) {
    1011           8 :     for (var i = 0; i < _archivedRooms.length; i++) {
    1012           4 :       final archive = _archivedRooms[i];
    1013           6 :       if (archive.room.id == roomId) return archive;
    1014             :     }
    1015             :     return null;
    1016             :   }
    1017             : 
    1018             :   /// Remove all the archives stored in cache.
    1019           2 :   void clearArchivesFromCache() {
    1020           4 :     _archivedRooms.clear();
    1021             :   }
    1022             : 
    1023           0 :   @Deprecated('Use [loadArchive()] instead.')
    1024           0 :   Future<List<Room>> get archive => loadArchive();
    1025             : 
    1026             :   /// Fetch all the archived rooms from the server and return the list of the
    1027             :   /// room. If you want to have the Timelines bundled with it, use
    1028             :   /// loadArchiveWithTimeline instead.
    1029           1 :   Future<List<Room>> loadArchive() async {
    1030           5 :     return (await loadArchiveWithTimeline()).map((e) => e.room).toList();
    1031             :   }
    1032             : 
    1033             :   // Synapse caches sync responses. Documentation:
    1034             :   // https://matrix-org.github.io/synapse/latest/usage/configuration/config_documentation.html#caches-and-associated-values
    1035             :   // At the time of writing, the cache key consists of the following fields:  user, timeout, since, filter_id,
    1036             :   // full_state, device_id, last_ignore_accdata_streampos.
    1037             :   // Since we can't pass a since token, the easiest field to vary is the timeout to bust through the synapse cache and
    1038             :   // give us the actual currently left rooms. Since the timeout doesn't matter for initial sync, this should actually
    1039             :   // not make any visible difference apart from properly fetching the cached rooms.
    1040             :   int _archiveCacheBusterTimeout = 0;
    1041             : 
    1042             :   /// Fetch the archived rooms from the server and return them as a list of
    1043             :   /// [ArchivedRoom] objects containing the [Room] and the associated [Timeline].
    1044           3 :   Future<List<ArchivedRoom>> loadArchiveWithTimeline() async {
    1045           6 :     _archivedRooms.clear();
    1046           3 :     final syncResp = await sync(
    1047             :       filter: '{"room":{"include_leave":true,"timeline":{"limit":10}}}',
    1048           3 :       timeout: _archiveCacheBusterTimeout,
    1049           3 :       setPresence: syncPresence,
    1050             :     );
    1051             :     // wrap around and hope there are not more than 30 leaves in 2 minutes :)
    1052          12 :     _archiveCacheBusterTimeout = (_archiveCacheBusterTimeout + 1) % 30;
    1053             : 
    1054           6 :     final leave = syncResp.rooms?.leave;
    1055             :     if (leave != null) {
    1056           6 :       for (final entry in leave.entries) {
    1057           9 :         await _storeArchivedRoom(entry.key, entry.value);
    1058             :       }
    1059             :     }
    1060             : 
    1061             :     // Sort the archived rooms by last event originServerTs as this is the
    1062             :     // best indicator we have to sort them. For archived rooms where we don't
    1063             :     // have any, we move them to the bottom.
    1064           3 :     final beginningOfTime = DateTime.fromMillisecondsSinceEpoch(0);
    1065           9 :     _archivedRooms.sort((b, a) =>
    1066           6 :         (a.room.lastEvent?.originServerTs ?? beginningOfTime)
    1067          12 :             .compareTo(b.room.lastEvent?.originServerTs ?? beginningOfTime));
    1068             : 
    1069           3 :     return _archivedRooms;
    1070             :   }
    1071             : 
    1072             :   /// [_storeArchivedRoom]
    1073             :   /// @leftRoom we can pass a room which was left so that we don't loose states
    1074           3 :   Future<void> _storeArchivedRoom(
    1075             :     String id,
    1076             :     LeftRoomUpdate update, {
    1077             :     Room? leftRoom,
    1078             :   }) async {
    1079             :     final roomUpdate = update;
    1080             :     final archivedRoom = leftRoom ??
    1081           3 :         Room(
    1082             :           id: id,
    1083             :           membership: Membership.leave,
    1084             :           client: this,
    1085           3 :           roomAccountData: roomUpdate.accountData
    1086           3 :                   ?.asMap()
    1087          12 :                   .map((k, v) => MapEntry(v.type, v)) ??
    1088           3 :               <String, BasicRoomEvent>{},
    1089             :         );
    1090             :     // Set membership of room to leave, in the case we got a left room passed, otherwise
    1091             :     // the left room would have still membership join, which would be wrong for the setState later
    1092           3 :     archivedRoom.membership = Membership.leave;
    1093           3 :     final timeline = Timeline(
    1094             :         room: archivedRoom,
    1095           3 :         chunk: TimelineChunk(
    1096           9 :             events: roomUpdate.timeline?.events?.reversed
    1097           3 :                     .toList() // we display the event in the other sence
    1098           9 :                     .map((e) => Event.fromMatrixEvent(e, archivedRoom))
    1099           3 :                     .toList() ??
    1100           0 :                 []));
    1101             : 
    1102           9 :     archivedRoom.prev_batch = update.timeline?.prevBatch;
    1103             : 
    1104           3 :     final stateEvents = roomUpdate.state;
    1105             :     if (stateEvents != null) {
    1106           3 :       await _handleRoomEvents(archivedRoom, stateEvents, EventUpdateType.state,
    1107             :           store: false);
    1108             :     }
    1109             : 
    1110           6 :     final timelineEvents = roomUpdate.timeline?.events;
    1111             :     if (timelineEvents != null) {
    1112           9 :       await _handleRoomEvents(archivedRoom, timelineEvents.reversed.toList(),
    1113             :           EventUpdateType.timeline,
    1114             :           store: false);
    1115             :     }
    1116             : 
    1117          12 :     for (var i = 0; i < timeline.events.length; i++) {
    1118             :       // Try to decrypt encrypted events but don't update the database.
    1119           3 :       if (archivedRoom.encrypted && archivedRoom.client.encryptionEnabled) {
    1120           0 :         if (timeline.events[i].type == EventTypes.Encrypted) {
    1121           0 :           await archivedRoom.client.encryption!
    1122           0 :               .decryptRoomEvent(
    1123           0 :                 archivedRoom.id,
    1124           0 :                 timeline.events[i],
    1125             :               )
    1126           0 :               .then(
    1127           0 :                 (decrypted) => timeline.events[i] = decrypted,
    1128             :               );
    1129             :         }
    1130             :       }
    1131             :     }
    1132             : 
    1133           9 :     _archivedRooms.add(ArchivedRoom(room: archivedRoom, timeline: timeline));
    1134             :   }
    1135             : 
    1136             :   final _serverConfigCache = AsyncCache<ServerConfig>(const Duration(hours: 1));
    1137             : 
    1138             :   /// Gets the config of the content repository, such as upload limit.
    1139           4 :   @override
    1140             :   Future<ServerConfig> getConfig() =>
    1141          16 :       _serverConfigCache.fetch(() => super.getConfig());
    1142             : 
    1143             :   /// Uploads a file and automatically caches it in the database, if it is small enough
    1144             :   /// and returns the mxc url.
    1145           4 :   @override
    1146             :   Future<Uri> uploadContent(Uint8List file,
    1147             :       {String? filename, String? contentType}) async {
    1148           4 :     final mediaConfig = await getConfig();
    1149           4 :     final maxMediaSize = mediaConfig.mUploadSize;
    1150           8 :     if (maxMediaSize != null && maxMediaSize < file.lengthInBytes) {
    1151           0 :       throw FileTooBigMatrixException(file.lengthInBytes, maxMediaSize);
    1152             :     }
    1153             : 
    1154           3 :     contentType ??= lookupMimeType(filename ?? '', headerBytes: file);
    1155             :     final mxc = await super
    1156           4 :         .uploadContent(file, filename: filename, contentType: contentType);
    1157             : 
    1158           4 :     final database = this.database;
    1159          12 :     if (database != null && file.length <= database.maxFileSize) {
    1160           4 :       await database.storeFile(
    1161           8 :           mxc, file, DateTime.now().millisecondsSinceEpoch);
    1162             :     }
    1163             :     return mxc;
    1164             :   }
    1165             : 
    1166             :   /// Sends a typing notification and initiates a megolm session, if needed
    1167           0 :   @override
    1168             :   Future<void> setTyping(
    1169             :     String userId,
    1170             :     String roomId,
    1171             :     bool typing, {
    1172             :     int? timeout,
    1173             :   }) async {
    1174           0 :     await super.setTyping(userId, roomId, typing, timeout: timeout);
    1175           0 :     final room = getRoomById(roomId);
    1176           0 :     if (typing && room != null && encryptionEnabled && room.encrypted) {
    1177             :       // ignore: unawaited_futures
    1178           0 :       encryption?.keyManager.prepareOutboundGroupSession(roomId);
    1179             :     }
    1180             :   }
    1181             : 
    1182             :   /// dumps the local database and exports it into a String.
    1183             :   ///
    1184             :   /// WARNING: never re-import the dump twice
    1185             :   ///
    1186             :   /// This can be useful to migrate a session from one device to a future one.
    1187           0 :   Future<String?> exportDump() async {
    1188           0 :     if (database != null) {
    1189           0 :       await abortSync();
    1190           0 :       await dispose(closeDatabase: false);
    1191             : 
    1192           0 :       final export = await database!.exportDump();
    1193             : 
    1194           0 :       await clear();
    1195             :       return export;
    1196             :     }
    1197             :     return null;
    1198             :   }
    1199             : 
    1200             :   /// imports a dumped session
    1201             :   ///
    1202             :   /// WARNING: never re-import the dump twice
    1203           0 :   Future<bool> importDump(String export) async {
    1204             :     try {
    1205             :       // stopping sync loop and subscriptions while keeping DB open
    1206           0 :       await dispose(closeDatabase: false);
    1207             :     } catch (_) {
    1208             :       // Client was probably not initialized yet.
    1209             :     }
    1210             : 
    1211           0 :     _database ??= await databaseBuilder!.call(this);
    1212             : 
    1213           0 :     final success = await database!.importDump(export);
    1214             : 
    1215             :     if (success) {
    1216             :       // closing including DB
    1217           0 :       await dispose();
    1218             : 
    1219             :       try {
    1220           0 :         bearerToken = null;
    1221             : 
    1222           0 :         await init(
    1223             :           waitForFirstSync: false,
    1224             :           waitUntilLoadCompletedLoaded: false,
    1225             :         );
    1226             :       } catch (e) {
    1227             :         return false;
    1228             :       }
    1229             :     }
    1230             :     return success;
    1231             :   }
    1232             : 
    1233             :   /// Uploads a new user avatar for this user. Leave file null to remove the
    1234             :   /// current avatar.
    1235           1 :   Future<void> setAvatar(MatrixFile? file) async {
    1236             :     if (file == null) {
    1237             :       // We send an empty String to remove the avatar. Sending Null **should**
    1238             :       // work but it doesn't with Synapse. See:
    1239             :       // https://gitlab.com/famedly/company/frontend/famedlysdk/-/issues/254
    1240           0 :       return setAvatarUrl(userID!, Uri.parse(''));
    1241             :     }
    1242           1 :     final uploadResp = await uploadContent(
    1243           1 :       file.bytes,
    1244           1 :       filename: file.name,
    1245           1 :       contentType: file.mimeType,
    1246             :     );
    1247           2 :     await setAvatarUrl(userID!, uploadResp);
    1248             :     return;
    1249             :   }
    1250             : 
    1251             :   /// Returns the global push rules for the logged in user.
    1252           0 :   PushRuleSet? get globalPushRules {
    1253           0 :     final pushrules = _accountData['m.push_rules']
    1254           0 :         ?.content
    1255           0 :         .tryGetMap<String, Object?>('global');
    1256           0 :     return pushrules != null ? TryGetPushRule.tryFromJson(pushrules) : null;
    1257             :   }
    1258             : 
    1259             :   /// Returns the device push rules for the logged in user.
    1260           0 :   PushRuleSet? get devicePushRules {
    1261           0 :     final pushrules = _accountData['m.push_rules']
    1262           0 :         ?.content
    1263           0 :         .tryGetMap<String, Object?>('device');
    1264           0 :     return pushrules != null ? TryGetPushRule.tryFromJson(pushrules) : null;
    1265             :   }
    1266             : 
    1267             :   static const Set<String> supportedVersions = {'v1.1', 'v1.2'};
    1268             :   static const List<String> supportedDirectEncryptionAlgorithms = [
    1269             :     AlgorithmTypes.olmV1Curve25519AesSha2
    1270             :   ];
    1271             :   static const List<String> supportedGroupEncryptionAlgorithms = [
    1272             :     AlgorithmTypes.megolmV1AesSha2
    1273             :   ];
    1274             :   static const int defaultThumbnailSize = 800;
    1275             : 
    1276             :   /// The newEvent signal is the most important signal in this concept. Every time
    1277             :   /// the app receives a new synchronization, this event is called for every signal
    1278             :   /// to update the GUI. For example, for a new message, it is called:
    1279             :   /// onRoomEvent( "m.room.message", "!chat_id:server.com", "timeline", {sender: "@bob:server.com", body: "Hello world"} )
    1280             :   final CachedStreamController<EventUpdate> onEvent = CachedStreamController();
    1281             : 
    1282             :   /// The onToDeviceEvent is called when there comes a new to device event. It is
    1283             :   /// already decrypted if necessary.
    1284             :   final CachedStreamController<ToDeviceEvent> onToDeviceEvent =
    1285             :       CachedStreamController();
    1286             : 
    1287             :   /// Tells you about to-device and room call specific events in sync
    1288             :   final CachedStreamController<List<BasicEventWithSender>> onCallEvents =
    1289             :       CachedStreamController();
    1290             : 
    1291             :   /// Called when the login state e.g. user gets logged out.
    1292             :   final CachedStreamController<LoginState> onLoginStateChanged =
    1293             :       CachedStreamController();
    1294             : 
    1295             :   /// Called when the local cache is reset
    1296             :   final CachedStreamController<bool> onCacheCleared = CachedStreamController();
    1297             : 
    1298             :   /// Encryption errors are coming here.
    1299             :   final CachedStreamController<SdkError> onEncryptionError =
    1300             :       CachedStreamController();
    1301             : 
    1302             :   /// When a new sync response is coming in, this gives the complete payload.
    1303             :   final CachedStreamController<SyncUpdate> onSync = CachedStreamController();
    1304             : 
    1305             :   /// This gives the current status of the synchronization
    1306             :   final CachedStreamController<SyncStatusUpdate> onSyncStatus =
    1307             :       CachedStreamController();
    1308             : 
    1309             :   /// Callback will be called on presences.
    1310             :   @Deprecated(
    1311             :       'Deprecated, use onPresenceChanged instead which has a timestamp.')
    1312             :   final CachedStreamController<Presence> onPresence = CachedStreamController();
    1313             : 
    1314             :   /// Callback will be called on presence updates.
    1315             :   final CachedStreamController<CachedPresence> onPresenceChanged =
    1316             :       CachedStreamController();
    1317             : 
    1318             :   /// Callback will be called on account data updates.
    1319             :   @Deprecated('Use `client.onSync` instead')
    1320             :   final CachedStreamController<BasicEvent> onAccountData =
    1321             :       CachedStreamController();
    1322             : 
    1323             :   /// Will be called when another device is requesting session keys for a room.
    1324             :   final CachedStreamController<RoomKeyRequest> onRoomKeyRequest =
    1325             :       CachedStreamController();
    1326             : 
    1327             :   /// Will be called when another device is requesting verification with this device.
    1328             :   final CachedStreamController<KeyVerification> onKeyVerificationRequest =
    1329             :       CachedStreamController();
    1330             : 
    1331             :   /// When the library calls an endpoint that needs UIA the `UiaRequest` is passed down this screen.
    1332             :   /// The client can open a UIA prompt based on this.
    1333             :   final CachedStreamController<UiaRequest> onUiaRequest =
    1334             :       CachedStreamController();
    1335             : 
    1336             :   @Deprecated('This is not in use anywhere anymore')
    1337             :   final CachedStreamController<Event> onGroupMember = CachedStreamController();
    1338             : 
    1339             :   final CachedStreamController<String> onCancelSendEvent =
    1340             :       CachedStreamController();
    1341             : 
    1342             :   /// When a state in a room has been updated this will return the room ID
    1343             :   /// and the state event.
    1344             :   final CachedStreamController<({String roomId, StrippedStateEvent state})>
    1345             :       onRoomState = CachedStreamController();
    1346             : 
    1347             :   /// How long should the app wait until it retrys the synchronisation after
    1348             :   /// an error?
    1349             :   int syncErrorTimeoutSec = 3;
    1350             : 
    1351             :   bool _initLock = false;
    1352             : 
    1353             :   /// Fetches the corresponding Event object from a notification including a
    1354             :   /// full Room object with the sender User object in it. Returns null if this
    1355             :   /// push notification is not corresponding to an existing event.
    1356             :   /// The client does **not** need to be initialized first. If it is not
    1357             :   /// initialized, it will only fetch the necessary parts of the database. This
    1358             :   /// should make it possible to run this parallel to another client with the
    1359             :   /// same client name.
    1360             :   /// This also checks if the given event has a readmarker and returns null
    1361             :   /// in this case.
    1362           1 :   Future<Event?> getEventByPushNotification(
    1363             :     PushNotification notification, {
    1364             :     bool storeInDatabase = true,
    1365             :     Duration timeoutForServerRequests = const Duration(seconds: 8),
    1366             :     bool returnNullIfSeen = true,
    1367             :   }) async {
    1368             :     // Get access token if necessary:
    1369           3 :     final database = _database ??= await databaseBuilder?.call(this);
    1370           1 :     if (!isLogged()) {
    1371             :       if (database == null) {
    1372           0 :         throw Exception(
    1373             :             'Can not execute getEventByPushNotification() without a database');
    1374             :       }
    1375           0 :       final clientInfoMap = await database.getClient(clientName);
    1376           0 :       final token = clientInfoMap?.tryGet<String>('token');
    1377             :       if (token == null) {
    1378           0 :         throw Exception('Client is not logged in.');
    1379             :       }
    1380           0 :       accessToken = token;
    1381             :     }
    1382             : 
    1383           1 :     await ensureNotSoftLoggedOut();
    1384             : 
    1385             :     // Check if the notification contains an event at all:
    1386           1 :     final eventId = notification.eventId;
    1387           1 :     final roomId = notification.roomId;
    1388             :     if (eventId == null || roomId == null) return null;
    1389             : 
    1390             :     // Create the room object:
    1391           1 :     final room = getRoomById(roomId) ??
    1392           1 :         await database?.getSingleRoom(this, roomId) ??
    1393           1 :         Room(
    1394             :           id: roomId,
    1395             :           client: this,
    1396             :         );
    1397           1 :     final roomName = notification.roomName;
    1398           1 :     final roomAlias = notification.roomAlias;
    1399             :     if (roomName != null) {
    1400           2 :       room.setState(Event(
    1401             :         eventId: 'TEMP',
    1402             :         stateKey: '',
    1403             :         type: EventTypes.RoomName,
    1404           1 :         content: {'name': roomName},
    1405             :         room: room,
    1406             :         senderId: 'UNKNOWN',
    1407           1 :         originServerTs: DateTime.now(),
    1408             :       ));
    1409             :     }
    1410             :     if (roomAlias != null) {
    1411           2 :       room.setState(Event(
    1412             :         eventId: 'TEMP',
    1413             :         stateKey: '',
    1414             :         type: EventTypes.RoomCanonicalAlias,
    1415           1 :         content: {'alias': roomAlias},
    1416             :         room: room,
    1417             :         senderId: 'UNKNOWN',
    1418           1 :         originServerTs: DateTime.now(),
    1419             :       ));
    1420             :     }
    1421             : 
    1422             :     // Load the event from the notification or from the database or from server:
    1423             :     MatrixEvent? matrixEvent;
    1424           1 :     final content = notification.content;
    1425           1 :     final sender = notification.sender;
    1426           1 :     final type = notification.type;
    1427             :     if (content != null && sender != null && type != null) {
    1428           1 :       matrixEvent = MatrixEvent(
    1429             :         content: content,
    1430             :         senderId: sender,
    1431             :         type: type,
    1432           1 :         originServerTs: DateTime.now(),
    1433             :         eventId: eventId,
    1434             :         roomId: roomId,
    1435             :       );
    1436             :     }
    1437             :     matrixEvent ??= await database
    1438           1 :         ?.getEventById(eventId, room)
    1439           1 :         .timeout(timeoutForServerRequests);
    1440             : 
    1441             :     try {
    1442           1 :       matrixEvent ??= await getOneRoomEvent(roomId, eventId)
    1443           1 :           .timeout(timeoutForServerRequests);
    1444           0 :     } on MatrixException catch (_) {
    1445             :       // No access to the MatrixEvent. Search in /notifications
    1446           0 :       final notificationsResponse = await getNotifications();
    1447           0 :       matrixEvent ??= notificationsResponse.notifications
    1448           0 :           .firstWhereOrNull((notification) =>
    1449           0 :               notification.roomId == roomId &&
    1450           0 :               notification.event.eventId == eventId)
    1451           0 :           ?.event;
    1452             :     }
    1453             : 
    1454             :     if (matrixEvent == null) {
    1455           0 :       throw Exception('Unable to find event for this push notification!');
    1456             :     }
    1457             : 
    1458             :     // If the event was already in database, check if it has a read marker
    1459             :     // before displaying it.
    1460             :     if (returnNullIfSeen) {
    1461           3 :       if (room.fullyRead == matrixEvent.eventId) {
    1462             :         return null;
    1463             :       }
    1464             :       final readMarkerEvent = await database
    1465           2 :           ?.getEventById(room.fullyRead, room)
    1466           1 :           .timeout(timeoutForServerRequests);
    1467             :       if (readMarkerEvent != null &&
    1468           0 :           readMarkerEvent.originServerTs.isAfter(
    1469           0 :             matrixEvent.originServerTs
    1470             :               // As origin server timestamps are not always correct data in
    1471             :               // a federated environment, we add 10 minutes to the calculation
    1472             :               // to reduce the possibility that an event is marked as read which
    1473             :               // isn't.
    1474           0 :               ..add(Duration(minutes: 10)),
    1475             :           )) {
    1476             :         return null;
    1477             :       }
    1478             :     }
    1479             : 
    1480             :     // Load the sender of this event
    1481             :     try {
    1482             :       await room
    1483           2 :           .requestUser(matrixEvent.senderId)
    1484           1 :           .timeout(timeoutForServerRequests);
    1485             :     } catch (e, s) {
    1486           2 :       Logs().w('Unable to request user for push helper', e, s);
    1487           1 :       final senderDisplayName = notification.senderDisplayName;
    1488             :       if (senderDisplayName != null && sender != null) {
    1489           2 :         room.setState(User(sender, displayName: senderDisplayName, room: room));
    1490             :       }
    1491             :     }
    1492             : 
    1493             :     // Create Event object and decrypt if necessary
    1494           1 :     var event = Event.fromMatrixEvent(
    1495             :       matrixEvent,
    1496             :       room,
    1497             :       status: EventStatus.sent,
    1498             :     );
    1499             : 
    1500           1 :     final encryption = this.encryption;
    1501           2 :     if (event.type == EventTypes.Encrypted && encryption != null) {
    1502           0 :       var decrypted = await encryption.decryptRoomEvent(roomId, event);
    1503           0 :       if (decrypted.messageType == MessageTypes.BadEncrypted &&
    1504           0 :           prevBatch != null) {
    1505           0 :         await oneShotSync();
    1506           0 :         decrypted = await encryption.decryptRoomEvent(roomId, event);
    1507             :       }
    1508             :       event = decrypted;
    1509             :     }
    1510             : 
    1511             :     if (storeInDatabase) {
    1512           2 :       await database?.transaction(() async {
    1513           1 :         await database.storeEventUpdate(
    1514           1 :             EventUpdate(
    1515             :               roomID: roomId,
    1516             :               type: EventUpdateType.timeline,
    1517           1 :               content: event.toJson(),
    1518             :             ),
    1519             :             this);
    1520             :       });
    1521             :     }
    1522             : 
    1523             :     return event;
    1524             :   }
    1525             : 
    1526             :   /// Sets the user credentials and starts the synchronisation.
    1527             :   ///
    1528             :   /// Before you can connect you need at least an [accessToken], a [homeserver],
    1529             :   /// a [userID], a [deviceID], and a [deviceName].
    1530             :   ///
    1531             :   /// Usually you don't need to call this method yourself because [login()], [register()]
    1532             :   /// and even the constructor calls it.
    1533             :   ///
    1534             :   /// Sends [LoginState.loggedIn] to [onLoginStateChanged].
    1535             :   ///
    1536             :   /// If one of [newToken], [newUserID], [newDeviceID], [newDeviceName] is set then
    1537             :   /// all of them must be set! If you don't set them, this method will try to
    1538             :   /// get them from the database.
    1539             :   ///
    1540             :   /// Set [waitForFirstSync] and [waitUntilLoadCompletedLoaded] to false to speed this
    1541             :   /// up. You can then wait for `roomsLoading`, `_accountDataLoading` and
    1542             :   /// `userDeviceKeysLoading` where it is necessary.
    1543          31 :   Future<void> init({
    1544             :     String? newToken,
    1545             :     DateTime? newTokenExpiresAt,
    1546             :     String? newRefreshToken,
    1547             :     Uri? newHomeserver,
    1548             :     String? newUserID,
    1549             :     String? newDeviceName,
    1550             :     String? newDeviceID,
    1551             :     String? newOlmAccount,
    1552             :     bool waitForFirstSync = true,
    1553             :     bool waitUntilLoadCompletedLoaded = true,
    1554             : 
    1555             :     /// Will be called if the app performs a migration task from the [legacyDatabaseBuilder]
    1556             :     void Function()? onMigration,
    1557             :   }) async {
    1558             :     if ((newToken != null ||
    1559             :             newUserID != null ||
    1560             :             newDeviceID != null ||
    1561             :             newDeviceName != null) &&
    1562             :         (newToken == null ||
    1563             :             newUserID == null ||
    1564             :             newDeviceID == null ||
    1565             :             newDeviceName == null)) {
    1566           0 :       throw ClientInitPreconditionError(
    1567             :         'If one of [newToken, newUserID, newDeviceID, newDeviceName] is set then all of them must be set!',
    1568             :       );
    1569             :     }
    1570             : 
    1571          31 :     if (_initLock) {
    1572           0 :       throw ClientInitPreconditionError(
    1573             :         '[init()] has been called multiple times!',
    1574             :       );
    1575             :     }
    1576          31 :     _initLock = true;
    1577             :     String? olmAccount;
    1578             :     String? accessToken;
    1579             :     String? userID;
    1580             :     try {
    1581         124 :       Logs().i('Initialize client $clientName');
    1582          93 :       if (onLoginStateChanged.value == LoginState.loggedIn) {
    1583           0 :         throw ClientInitPreconditionError(
    1584             :           'User is already logged in! Call [logout()] first!',
    1585             :         );
    1586             :       }
    1587             : 
    1588          31 :       final databaseBuilder = this.databaseBuilder;
    1589             :       if (databaseBuilder != null) {
    1590          58 :         _database ??= await runBenchmarked<DatabaseApi>(
    1591             :           'Build database',
    1592          58 :           () async => await databaseBuilder(this),
    1593             :         );
    1594             :       }
    1595             : 
    1596          62 :       _groupCallSessionId = randomAlpha(12);
    1597          62 :       _serverConfigCache.invalidate();
    1598             : 
    1599          89 :       final account = await this.database?.getClient(clientName);
    1600           1 :       newRefreshToken ??= account?.tryGet<String>('refresh_token');
    1601             :       if (account != null) {
    1602           2 :         _id = account['client_id'];
    1603           3 :         homeserver = Uri.parse(account['homeserver_url']);
    1604           2 :         accessToken = this.accessToken = account['token'];
    1605             :         final tokenExpiresAtMs =
    1606           2 :             int.tryParse(account.tryGet<String>('token_expires_at') ?? '');
    1607           1 :         _accessTokenExpiresAt = tokenExpiresAtMs == null
    1608             :             ? null
    1609           0 :             : DateTime.fromMillisecondsSinceEpoch(tokenExpiresAtMs);
    1610           2 :         userID = _userID = account['user_id'];
    1611           2 :         _deviceID = account['device_id'];
    1612           2 :         _deviceName = account['device_name'];
    1613           2 :         _syncFilterId = account['sync_filter_id'];
    1614           2 :         _prevBatch = account['prev_batch'];
    1615           1 :         olmAccount = account['olm_account'];
    1616             :       }
    1617             :       if (newToken != null) {
    1618          31 :         accessToken = this.accessToken = newToken;
    1619          31 :         _accessTokenExpiresAt = newTokenExpiresAt;
    1620          31 :         homeserver = newHomeserver;
    1621          31 :         userID = _userID = newUserID;
    1622          31 :         _deviceID = newDeviceID;
    1623          31 :         _deviceName = newDeviceName;
    1624             :         olmAccount = newOlmAccount;
    1625             :       } else {
    1626           1 :         accessToken = this.accessToken = newToken ?? accessToken;
    1627           2 :         _accessTokenExpiresAt = newTokenExpiresAt ?? accessTokenExpiresAt;
    1628           2 :         homeserver = newHomeserver ?? homeserver;
    1629           1 :         userID = _userID = newUserID ?? userID;
    1630           2 :         _deviceID = newDeviceID ?? _deviceID;
    1631           2 :         _deviceName = newDeviceName ?? _deviceName;
    1632             :         olmAccount = newOlmAccount ?? olmAccount;
    1633             :       }
    1634             : 
    1635             :       // If we are refreshing the session, we are done here:
    1636          93 :       if (onLoginStateChanged.value == LoginState.softLoggedOut) {
    1637             :         if (newRefreshToken != null && accessToken != null && userID != null) {
    1638             :           // Store the new tokens:
    1639           0 :           await _database?.updateClient(
    1640           0 :             homeserver.toString(),
    1641             :             accessToken,
    1642           0 :             accessTokenExpiresAt,
    1643             :             newRefreshToken,
    1644             :             userID,
    1645           0 :             _deviceID,
    1646           0 :             _deviceName,
    1647           0 :             prevBatch,
    1648           0 :             encryption?.pickledOlmAccount,
    1649             :           );
    1650             :         }
    1651           0 :         onLoginStateChanged.add(LoginState.loggedIn);
    1652             :         return;
    1653             :       }
    1654             : 
    1655          31 :       if (accessToken == null || homeserver == null || userID == null) {
    1656           1 :         if (legacyDatabaseBuilder != null) {
    1657           1 :           await _migrateFromLegacyDatabase(onMigration: onMigration);
    1658           1 :           if (isLogged()) return;
    1659             :         }
    1660             :         // we aren't logged in
    1661           0 :         await encryption?.dispose();
    1662           0 :         _encryption = null;
    1663           0 :         onLoginStateChanged.add(LoginState.loggedOut);
    1664           0 :         Logs().i('User is not logged in.');
    1665           0 :         _initLock = false;
    1666             :         return;
    1667             :       }
    1668             : 
    1669          31 :       await encryption?.dispose();
    1670             :       try {
    1671             :         // make sure to throw an exception if libolm doesn't exist
    1672          31 :         await olm.init();
    1673          24 :         olm.get_library_version();
    1674          48 :         _encryption = Encryption(client: this);
    1675             :       } catch (e) {
    1676          21 :         Logs().e('Error initializing encryption $e');
    1677           7 :         await encryption?.dispose();
    1678           7 :         _encryption = null;
    1679             :       }
    1680          55 :       await encryption?.init(olmAccount);
    1681             : 
    1682          31 :       final database = this.database;
    1683             :       if (database != null) {
    1684          29 :         if (id != null) {
    1685           0 :           await database.updateClient(
    1686           0 :             homeserver.toString(),
    1687             :             accessToken,
    1688           0 :             accessTokenExpiresAt,
    1689             :             newRefreshToken,
    1690             :             userID,
    1691           0 :             _deviceID,
    1692           0 :             _deviceName,
    1693           0 :             prevBatch,
    1694           0 :             encryption?.pickledOlmAccount,
    1695             :           );
    1696             :         } else {
    1697          58 :           _id = await database.insertClient(
    1698          29 :             clientName,
    1699          58 :             homeserver.toString(),
    1700             :             accessToken,
    1701          29 :             accessTokenExpiresAt,
    1702             :             newRefreshToken,
    1703             :             userID,
    1704          29 :             _deviceID,
    1705          29 :             _deviceName,
    1706          29 :             prevBatch,
    1707          52 :             encryption?.pickledOlmAccount,
    1708             :           );
    1709             :         }
    1710          29 :         userDeviceKeysLoading = database
    1711          29 :             .getUserDeviceKeys(this)
    1712          87 :             .then((keys) => _userDeviceKeys = keys);
    1713         116 :         roomsLoading = database.getRoomList(this).then((rooms) {
    1714          29 :           _rooms = rooms;
    1715          29 :           _sortRooms();
    1716             :         });
    1717         116 :         _accountDataLoading = database.getAccountData().then((data) {
    1718          29 :           _accountData = data;
    1719          29 :           _updatePushrules();
    1720             :         });
    1721             :         // ignore: deprecated_member_use_from_same_package
    1722          58 :         presences.clear();
    1723             :         if (waitUntilLoadCompletedLoaded) {
    1724          29 :           await userDeviceKeysLoading;
    1725          29 :           await roomsLoading;
    1726          29 :           await _accountDataLoading;
    1727             :         }
    1728             :       }
    1729          31 :       _initLock = false;
    1730          62 :       onLoginStateChanged.add(LoginState.loggedIn);
    1731          62 :       Logs().i(
    1732         124 :         'Successfully connected as ${userID.localpart} with ${homeserver.toString()}',
    1733             :       );
    1734             : 
    1735             :       /// Timeout of 0, so that we don't see a spinner for 30 seconds.
    1736          62 :       firstSyncReceived = _sync(timeout: Duration.zero);
    1737             :       if (waitForFirstSync) {
    1738          31 :         await firstSyncReceived;
    1739             :       }
    1740             :       return;
    1741           1 :     } on ClientInitPreconditionError {
    1742             :       rethrow;
    1743             :     } catch (e, s) {
    1744           2 :       Logs().wtf('Client initialization failed', e, s);
    1745           2 :       onLoginStateChanged.addError(e, s);
    1746           1 :       final clientInitException = ClientInitException(
    1747             :         e,
    1748           1 :         homeserver: homeserver,
    1749             :         accessToken: accessToken,
    1750             :         userId: userID,
    1751           1 :         deviceId: deviceID,
    1752           1 :         deviceName: deviceName,
    1753             :         olmAccount: olmAccount,
    1754             :       );
    1755           1 :       await clear();
    1756             :       throw clientInitException;
    1757             :     } finally {
    1758          31 :       _initLock = false;
    1759             :     }
    1760             :   }
    1761             : 
    1762             :   /// Used for testing only
    1763           1 :   void setUserId(String s) {
    1764           1 :     _userID = s;
    1765             :   }
    1766             : 
    1767             :   /// Resets all settings and stops the synchronisation.
    1768           9 :   Future<void> clear() async {
    1769          27 :     Logs().outputEvents.clear();
    1770             :     try {
    1771           9 :       await abortSync();
    1772          16 :       await database?.clear();
    1773           9 :       _backgroundSync = true;
    1774             :     } catch (e, s) {
    1775           0 :       Logs().e('Unable to clear database', e, s);
    1776             :     } finally {
    1777           9 :       _database = null;
    1778             :     }
    1779             : 
    1780          27 :     _id = accessToken = _syncFilterId =
    1781          45 :         homeserver = _userID = _deviceID = _deviceName = _prevBatch = null;
    1782          18 :     _rooms = [];
    1783          18 :     _eventsPendingDecryption.clear();
    1784          15 :     await encryption?.dispose();
    1785           9 :     _encryption = null;
    1786          18 :     onLoginStateChanged.add(LoginState.loggedOut);
    1787             :   }
    1788             : 
    1789             :   bool _backgroundSync = true;
    1790             :   Future<void>? _currentSync;
    1791             :   Future<void> _retryDelay = Future.value();
    1792             : 
    1793           0 :   bool get syncPending => _currentSync != null;
    1794             : 
    1795             :   /// Controls the background sync (automatically looping forever if turned on).
    1796             :   /// If you use soft logout, you need to manually call
    1797             :   /// `ensureNotSoftLoggedOut()` before doing any API request after setting
    1798             :   /// the background sync to false, as the soft logout is handeld automatically
    1799             :   /// in the sync loop.
    1800          25 :   set backgroundSync(bool enabled) {
    1801          25 :     _backgroundSync = enabled;
    1802          25 :     if (_backgroundSync) {
    1803           6 :       runInRoot(() async => _sync());
    1804             :     }
    1805             :   }
    1806             : 
    1807             :   /// Immediately start a sync and wait for completion.
    1808             :   /// If there is an active sync already, wait for the active sync instead.
    1809           1 :   Future<void> oneShotSync() {
    1810           1 :     return _sync();
    1811             :   }
    1812             : 
    1813             :   /// Pass a timeout to set how long the server waits before sending an empty response.
    1814             :   /// (Corresponds to the timeout param on the /sync request.)
    1815          31 :   Future<void> _sync({Duration? timeout}) {
    1816             :     final currentSync =
    1817         124 :         _currentSync ??= _innerSync(timeout: timeout).whenComplete(() {
    1818          31 :       _currentSync = null;
    1819          93 :       if (_backgroundSync && isLogged() && !_disposed) {
    1820          31 :         _sync();
    1821             :       }
    1822             :     });
    1823             :     return currentSync;
    1824             :   }
    1825             : 
    1826             :   /// Presence that is set on sync.
    1827             :   PresenceType? syncPresence;
    1828             : 
    1829          31 :   Future<void> _checkSyncFilter() async {
    1830          31 :     final userID = this.userID;
    1831          31 :     if (syncFilterId == null && userID != null) {
    1832             :       final syncFilterId =
    1833          93 :           _syncFilterId = await defineFilter(userID, syncFilter);
    1834          60 :       await database?.storeSyncFilterId(syncFilterId);
    1835             :     }
    1836             :     return;
    1837             :   }
    1838             : 
    1839             :   Future<void>? _handleSoftLogoutFuture;
    1840             : 
    1841           1 :   Future<void> _handleSoftLogout() async {
    1842           1 :     final onSoftLogout = this.onSoftLogout;
    1843             :     if (onSoftLogout == null) return;
    1844             : 
    1845           2 :     _handleSoftLogoutFuture ??= () async {
    1846           2 :       onLoginStateChanged.add(LoginState.softLoggedOut);
    1847             :       try {
    1848           1 :         await onSoftLogout(this);
    1849           2 :         onLoginStateChanged.add(LoginState.loggedIn);
    1850             :       } catch (e, s) {
    1851           0 :         Logs().w('Unable to refresh session after soft logout', e, s);
    1852           0 :         await clear();
    1853             :         rethrow;
    1854             :       }
    1855           1 :     }();
    1856           1 :     await _handleSoftLogoutFuture;
    1857           1 :     _handleSoftLogoutFuture = null;
    1858             :   }
    1859             : 
    1860             :   /// Checks if the token expires in under [expiresIn] time and calls the
    1861             :   /// given `onSoftLogout()` if so. You have to provide `onSoftLogout` in the
    1862             :   /// Client constructor. Otherwise this will do nothing.
    1863          31 :   Future<void> ensureNotSoftLoggedOut(
    1864             :       [Duration expiresIn = const Duration(minutes: 1)]) async {
    1865          31 :     final tokenExpiresAt = accessTokenExpiresAt;
    1866          31 :     if (onSoftLogout != null &&
    1867             :         tokenExpiresAt != null &&
    1868           3 :         tokenExpiresAt.difference(DateTime.now()) <= expiresIn) {
    1869           0 :       await _handleSoftLogout();
    1870             :     }
    1871             :   }
    1872             : 
    1873             :   /// Pass a timeout to set how long the server waits before sending an empty response.
    1874             :   /// (Corresponds to the timeout param on the /sync request.)
    1875          31 :   Future<void> _innerSync({Duration? timeout}) async {
    1876          31 :     await _retryDelay;
    1877         124 :     _retryDelay = Future.delayed(Duration(seconds: syncErrorTimeoutSec));
    1878          93 :     if (!isLogged() || _disposed || _aborted) return;
    1879             :     try {
    1880          31 :       if (_initLock) {
    1881           0 :         Logs().d('Running sync while init isn\'t done yet, dropping request');
    1882             :         return;
    1883             :       }
    1884             :       Object? syncError;
    1885             : 
    1886             :       // The timeout we send to the server for the sync loop. It says to the
    1887             :       // server that we want to receive an empty sync response after this
    1888             :       // amount of time if nothing happens.
    1889             :       timeout ??= const Duration(seconds: 30);
    1890             : 
    1891          62 :       await ensureNotSoftLoggedOut(timeout * 2);
    1892             : 
    1893          31 :       await _checkSyncFilter();
    1894             : 
    1895          31 :       final syncRequest = sync(
    1896          31 :         filter: syncFilterId,
    1897          31 :         since: prevBatch,
    1898          31 :         timeout: timeout.inMilliseconds,
    1899          31 :         setPresence: syncPresence,
    1900         125 :       ).then((v) => Future<SyncUpdate?>.value(v)).catchError((e) {
    1901           1 :         if (e is MatrixException) {
    1902             :           syncError = e;
    1903             :         } else {
    1904           0 :           syncError = SyncConnectionException(e);
    1905             :         }
    1906             :         return null;
    1907             :       });
    1908          62 :       _currentSyncId = syncRequest.hashCode;
    1909          93 :       onSyncStatus.add(SyncStatusUpdate(SyncStatus.waitingForResponse));
    1910             : 
    1911             :       // The timeout for the response from the server. If we do not set a sync
    1912             :       // timeout (for initial sync) we give the server a longer time to
    1913             :       // responde.
    1914          31 :       final responseTimeout = timeout == Duration.zero
    1915             :           ? const Duration(minutes: 2)
    1916          31 :           : timeout + const Duration(seconds: 10);
    1917             : 
    1918          31 :       final syncResp = await syncRequest.timeout(responseTimeout);
    1919          93 :       onSyncStatus.add(SyncStatusUpdate(SyncStatus.processing));
    1920             :       if (syncResp == null) throw syncError ?? 'Unknown sync error';
    1921          93 :       if (_currentSyncId != syncRequest.hashCode) {
    1922          12 :         Logs()
    1923          12 :             .w('Current sync request ID has changed. Dropping this sync loop!');
    1924             :         return;
    1925             :       }
    1926             : 
    1927          31 :       final database = this.database;
    1928             :       if (database != null) {
    1929          29 :         await userDeviceKeysLoading;
    1930          29 :         await roomsLoading;
    1931          29 :         await _accountDataLoading;
    1932          87 :         _currentTransaction = database.transaction(() async {
    1933          29 :           await _handleSync(syncResp, direction: Direction.f);
    1934          87 :           if (prevBatch != syncResp.nextBatch) {
    1935          58 :             await database.storePrevBatch(syncResp.nextBatch);
    1936             :           }
    1937             :         });
    1938          29 :         await runBenchmarked(
    1939             :           'Process sync',
    1940          58 :           () async => await _currentTransaction,
    1941          29 :           syncResp.itemCount,
    1942             :         );
    1943             :       } else {
    1944           4 :         await _handleSync(syncResp, direction: Direction.f);
    1945             :       }
    1946          62 :       if (_disposed || _aborted) return;
    1947          62 :       _prevBatch = syncResp.nextBatch;
    1948          93 :       onSyncStatus.add(SyncStatusUpdate(SyncStatus.cleaningUp));
    1949             :       // ignore: unawaited_futures
    1950          29 :       database?.deleteOldFiles(
    1951         116 :           DateTime.now().subtract(Duration(days: 30)).millisecondsSinceEpoch);
    1952          31 :       await updateUserDeviceKeys();
    1953          31 :       if (encryptionEnabled) {
    1954          48 :         encryption?.onSync();
    1955             :       }
    1956             : 
    1957             :       // try to process the to_device queue
    1958             :       try {
    1959          31 :         await processToDeviceQueue();
    1960             :       } catch (_) {} // we want to dispose any errors this throws
    1961             : 
    1962          62 :       _retryDelay = Future.value();
    1963          93 :       onSyncStatus.add(SyncStatusUpdate(SyncStatus.finished));
    1964           1 :     } on MatrixException catch (e, s) {
    1965           3 :       onSyncStatus.add(SyncStatusUpdate(SyncStatus.error,
    1966           1 :           error: SdkError(exception: e, stackTrace: s)));
    1967           2 :       if (e.error == MatrixError.M_UNKNOWN_TOKEN) {
    1968           1 :         final onSoftLogout = this.onSoftLogout;
    1969           3 :         if (e.raw.tryGet<bool>('soft_logout') == true && onSoftLogout != null) {
    1970           2 :           Logs().w('The user has been soft logged out! Try to login again...');
    1971             : 
    1972           1 :           await _handleSoftLogout();
    1973             :         } else {
    1974           0 :           Logs().w('The user has been logged out!');
    1975           0 :           await clear();
    1976             :         }
    1977             :       }
    1978           0 :     } on SyncConnectionException catch (e, s) {
    1979           0 :       Logs().w('Syncloop failed: Client has not connection to the server');
    1980           0 :       onSyncStatus.add(SyncStatusUpdate(SyncStatus.error,
    1981           0 :           error: SdkError(exception: e, stackTrace: s)));
    1982             :     } catch (e, s) {
    1983           0 :       if (!isLogged() || _disposed || _aborted) return;
    1984           0 :       Logs().e('Error during processing events', e, s);
    1985           0 :       onSyncStatus.add(SyncStatusUpdate(SyncStatus.error,
    1986           0 :           error: SdkError(
    1987           0 :               exception: e is Exception ? e : Exception(e), stackTrace: s)));
    1988             :     }
    1989             :   }
    1990             : 
    1991             :   /// Use this method only for testing utilities!
    1992          18 :   Future<void> handleSync(SyncUpdate sync, {Direction? direction}) async {
    1993             :     // ensure we don't upload keys because someone forgot to set a key count
    1994          36 :     sync.deviceOneTimeKeysCount ??= {
    1995          46 :       'signed_curve25519': encryption?.olmManager.maxNumberOfOneTimeKeys ?? 100,
    1996             :     };
    1997          18 :     await _handleSync(sync, direction: direction);
    1998             :   }
    1999             : 
    2000          31 :   Future<void> _handleSync(SyncUpdate sync, {Direction? direction}) async {
    2001          31 :     final syncToDevice = sync.toDevice;
    2002             :     if (syncToDevice != null) {
    2003          31 :       await _handleToDeviceEvents(syncToDevice);
    2004             :     }
    2005             : 
    2006          31 :     if (sync.rooms != null) {
    2007          62 :       final join = sync.rooms?.join;
    2008             :       if (join != null) {
    2009          31 :         await _handleRooms(join, direction: direction);
    2010             :       }
    2011          62 :       final invite = sync.rooms?.invite;
    2012             :       if (invite != null) {
    2013          31 :         await _handleRooms(invite, direction: direction);
    2014             :       }
    2015          62 :       final leave = sync.rooms?.leave;
    2016             :       if (leave != null) {
    2017          31 :         await _handleRooms(leave, direction: direction);
    2018             :       }
    2019             :     }
    2020         111 :     for (final newPresence in sync.presence ?? <Presence>[]) {
    2021          31 :       final cachedPresence = CachedPresence.fromMatrixEvent(newPresence);
    2022             :       // ignore: deprecated_member_use_from_same_package
    2023          93 :       presences[newPresence.senderId] = cachedPresence;
    2024             :       // ignore: deprecated_member_use_from_same_package
    2025          62 :       onPresence.add(newPresence);
    2026          62 :       onPresenceChanged.add(cachedPresence);
    2027          89 :       await database?.storePresence(newPresence.senderId, cachedPresence);
    2028             :     }
    2029         112 :     for (final newAccountData in sync.accountData ?? []) {
    2030          60 :       await database?.storeAccountData(
    2031          29 :         newAccountData.type,
    2032          58 :         jsonEncode(newAccountData.content),
    2033             :       );
    2034          93 :       accountData[newAccountData.type] = newAccountData;
    2035             :       // ignore: deprecated_member_use_from_same_package
    2036          62 :       onAccountData.add(newAccountData);
    2037             : 
    2038          62 :       if (newAccountData.type == EventTypes.PushRules) {
    2039          31 :         _updatePushrules();
    2040             :       }
    2041             :     }
    2042             : 
    2043          31 :     final syncDeviceLists = sync.deviceLists;
    2044             :     if (syncDeviceLists != null) {
    2045          31 :       await _handleDeviceListsEvents(syncDeviceLists);
    2046             :     }
    2047          31 :     if (encryptionEnabled) {
    2048          48 :       encryption?.handleDeviceOneTimeKeysCount(
    2049          48 :           sync.deviceOneTimeKeysCount, sync.deviceUnusedFallbackKeyTypes);
    2050             :     }
    2051          31 :     _sortRooms();
    2052          62 :     onSync.add(sync);
    2053             :   }
    2054             : 
    2055          31 :   Future<void> _handleDeviceListsEvents(DeviceListsUpdate deviceLists) async {
    2056          62 :     if (deviceLists.changed is List) {
    2057          93 :       for (final userId in deviceLists.changed ?? []) {
    2058          62 :         final userKeys = _userDeviceKeys[userId];
    2059             :         if (userKeys != null) {
    2060           1 :           userKeys.outdated = true;
    2061           2 :           await database?.storeUserDeviceKeysInfo(userId, true);
    2062             :         }
    2063             :       }
    2064          93 :       for (final userId in deviceLists.left ?? []) {
    2065          62 :         if (_userDeviceKeys.containsKey(userId)) {
    2066           0 :           _userDeviceKeys.remove(userId);
    2067             :         }
    2068             :       }
    2069             :     }
    2070             :   }
    2071             : 
    2072          31 :   Future<void> _handleToDeviceEvents(List<BasicEventWithSender> events) async {
    2073          31 :     final Map<String, List<String>> roomsWithNewKeyToSessionId = {};
    2074          31 :     final List<ToDeviceEvent> callToDeviceEvents = [];
    2075          62 :     for (final event in events) {
    2076          62 :       var toDeviceEvent = ToDeviceEvent.fromJson(event.toJson());
    2077         124 :       Logs().v('Got to_device event of type ${toDeviceEvent.type}');
    2078          31 :       if (encryptionEnabled) {
    2079          48 :         if (toDeviceEvent.type == EventTypes.Encrypted) {
    2080          48 :           toDeviceEvent = await encryption!.decryptToDeviceEvent(toDeviceEvent);
    2081          96 :           Logs().v('Decrypted type is: ${toDeviceEvent.type}');
    2082             : 
    2083             :           /// collect new keys so that we can find those events in the decryption queue
    2084          48 :           if (toDeviceEvent.type == EventTypes.ForwardedRoomKey ||
    2085          48 :               toDeviceEvent.type == EventTypes.RoomKey) {
    2086          46 :             final roomId = event.content['room_id'];
    2087          46 :             final sessionId = event.content['session_id'];
    2088          23 :             if (roomId is String && sessionId is String) {
    2089           0 :               (roomsWithNewKeyToSessionId[roomId] ??= []).add(sessionId);
    2090             :             }
    2091             :           }
    2092             :         }
    2093          48 :         await encryption?.handleToDeviceEvent(toDeviceEvent);
    2094             :       }
    2095          93 :       if (toDeviceEvent.type.startsWith(CallConstants.callEventsRegxp)) {
    2096           0 :         callToDeviceEvents.add(toDeviceEvent);
    2097             :       }
    2098          62 :       onToDeviceEvent.add(toDeviceEvent);
    2099             :     }
    2100             : 
    2101          31 :     if (callToDeviceEvents.isNotEmpty) {
    2102           0 :       onCallEvents.add(callToDeviceEvents);
    2103             :     }
    2104             : 
    2105             :     // emit updates for all events in the queue
    2106          31 :     for (final entry in roomsWithNewKeyToSessionId.entries) {
    2107           0 :       final roomId = entry.key;
    2108           0 :       final sessionIds = entry.value;
    2109             : 
    2110           0 :       final room = getRoomById(roomId);
    2111             :       if (room != null) {
    2112           0 :         final List<BasicEvent> events = [];
    2113           0 :         for (final event in _eventsPendingDecryption) {
    2114           0 :           if (event.event.roomID != roomId) continue;
    2115           0 :           if (!sessionIds.contains(
    2116           0 :               event.event.content['content']?['session_id'])) continue;
    2117             : 
    2118           0 :           final decryptedEvent = await event.event.decrypt(room);
    2119           0 :           if (decryptedEvent.content.tryGet<String>('type') !=
    2120             :               EventTypes.Encrypted) {
    2121           0 :             events.add(BasicEvent.fromJson(decryptedEvent.content));
    2122             :           }
    2123             :         }
    2124             : 
    2125           0 :         await _handleRoomEvents(
    2126             :             room, events, EventUpdateType.decryptedTimelineQueue);
    2127             : 
    2128           0 :         _eventsPendingDecryption.removeWhere((e) => events.any(
    2129           0 :             (decryptedEvent) =>
    2130           0 :                 decryptedEvent.content['event_id'] ==
    2131           0 :                 e.event.content['event_id']));
    2132             :       }
    2133             :     }
    2134          62 :     _eventsPendingDecryption.removeWhere((e) => e.timedOut);
    2135             :   }
    2136             : 
    2137          31 :   Future<void> _handleRooms(Map<String, SyncRoomUpdate> rooms,
    2138             :       {Direction? direction}) async {
    2139             :     var handledRooms = 0;
    2140          62 :     for (final entry in rooms.entries) {
    2141          93 :       onSyncStatus.add(SyncStatusUpdate(
    2142             :         SyncStatus.processing,
    2143          93 :         progress: ++handledRooms / rooms.length,
    2144             :       ));
    2145          31 :       final id = entry.key;
    2146          31 :       final syncRoomUpdate = entry.value;
    2147             : 
    2148             :       // Is the timeline limited? Then all previous messages should be
    2149             :       // removed from the database!
    2150          31 :       if (syncRoomUpdate is JoinedRoomUpdate &&
    2151          93 :           syncRoomUpdate.timeline?.limited == true) {
    2152          60 :         await database?.deleteTimelineForRoom(id);
    2153             :       }
    2154          31 :       final room = await _updateRoomsByRoomUpdate(id, syncRoomUpdate);
    2155             : 
    2156             :       final timelineUpdateType = direction != null
    2157          31 :           ? (direction == Direction.b
    2158             :               ? EventUpdateType.history
    2159             :               : EventUpdateType.timeline)
    2160             :           : EventUpdateType.timeline;
    2161             : 
    2162             :       /// Handle now all room events and save them in the database
    2163          31 :       if (syncRoomUpdate is JoinedRoomUpdate) {
    2164          31 :         final state = syncRoomUpdate.state;
    2165             : 
    2166          31 :         if (state != null && state.isNotEmpty) {
    2167             :           // TODO: This method seems to be comperatively slow for some updates
    2168          31 :           await _handleRoomEvents(
    2169             :             room,
    2170             :             state,
    2171             :             EventUpdateType.state,
    2172             :           );
    2173             :         }
    2174             : 
    2175          62 :         final timelineEvents = syncRoomUpdate.timeline?.events;
    2176          31 :         if (timelineEvents != null && timelineEvents.isNotEmpty) {
    2177          31 :           await _handleRoomEvents(room, timelineEvents, timelineUpdateType);
    2178             :         }
    2179             : 
    2180          31 :         final ephemeral = syncRoomUpdate.ephemeral;
    2181          31 :         if (ephemeral != null && ephemeral.isNotEmpty) {
    2182             :           // TODO: This method seems to be comperatively slow for some updates
    2183          31 :           await _handleEphemerals(
    2184             :             room,
    2185             :             ephemeral,
    2186             :           );
    2187             :         }
    2188             : 
    2189          31 :         final accountData = syncRoomUpdate.accountData;
    2190          31 :         if (accountData != null && accountData.isNotEmpty) {
    2191          31 :           await _handleRoomEvents(
    2192             :             room,
    2193             :             accountData,
    2194             :             EventUpdateType.accountData,
    2195             :           );
    2196             :         }
    2197             :       }
    2198             : 
    2199          31 :       if (syncRoomUpdate is LeftRoomUpdate) {
    2200          62 :         final timelineEvents = syncRoomUpdate.timeline?.events;
    2201          31 :         if (timelineEvents != null && timelineEvents.isNotEmpty) {
    2202          31 :           await _handleRoomEvents(room, timelineEvents, timelineUpdateType,
    2203             :               store: false);
    2204             :         }
    2205          31 :         final accountData = syncRoomUpdate.accountData;
    2206          31 :         if (accountData != null && accountData.isNotEmpty) {
    2207          31 :           await _handleRoomEvents(
    2208             :               room, accountData, EventUpdateType.accountData,
    2209             :               store: false);
    2210             :         }
    2211          31 :         final state = syncRoomUpdate.state;
    2212          31 :         if (state != null && state.isNotEmpty) {
    2213          31 :           await _handleRoomEvents(room, state, EventUpdateType.state,
    2214             :               store: false);
    2215             :         }
    2216             :       }
    2217             : 
    2218          31 :       if (syncRoomUpdate is InvitedRoomUpdate) {
    2219          31 :         final state = syncRoomUpdate.inviteState;
    2220          31 :         if (state != null && state.isNotEmpty) {
    2221          31 :           await _handleRoomEvents(room, state, EventUpdateType.inviteState);
    2222             :         }
    2223             :       }
    2224          89 :       await database?.storeRoomUpdate(id, syncRoomUpdate, room.lastEvent, this);
    2225             :     }
    2226             :   }
    2227             : 
    2228          31 :   Future<void> _handleEphemerals(Room room, List<BasicRoomEvent> events) async {
    2229          31 :     final List<ReceiptEventContent> receipts = [];
    2230             : 
    2231          62 :     for (final event in events) {
    2232          62 :       await _handleRoomEvents(room, [event], EventUpdateType.ephemeral);
    2233             : 
    2234             :       // Receipt events are deltas between two states. We will create a
    2235             :       // fake room account data event for this and store the difference
    2236             :       // there.
    2237          62 :       if (event.type != 'm.receipt') continue;
    2238             : 
    2239          93 :       receipts.add(ReceiptEventContent.fromJson(event.content));
    2240             :     }
    2241             : 
    2242          31 :     if (receipts.isNotEmpty) {
    2243          31 :       final receiptStateContent = room.receiptState;
    2244             : 
    2245          62 :       for (final e in receipts) {
    2246          31 :         await receiptStateContent.update(e, room);
    2247             :       }
    2248             : 
    2249          31 :       await _handleRoomEvents(
    2250             :           room,
    2251          31 :           [
    2252          31 :             BasicRoomEvent(
    2253             :               type: LatestReceiptState.eventType,
    2254          31 :               roomId: room.id,
    2255          31 :               content: receiptStateContent.toJson(),
    2256             :             )
    2257             :           ],
    2258             :           EventUpdateType.accountData);
    2259             :     }
    2260             :   }
    2261             : 
    2262             :   /// Stores event that came down /sync but didn't get decrypted because of missing keys yet.
    2263             :   final List<_EventPendingDecryption> _eventsPendingDecryption = [];
    2264             : 
    2265          31 :   Future<void> _handleRoomEvents(
    2266             :       Room room, List<BasicEvent> events, EventUpdateType type,
    2267             :       {bool store = true}) async {
    2268             :     // Calling events can be omitted if they are outdated from the same sync. So
    2269             :     // we collect them first before we handle them.
    2270          31 :     final callEvents = <Event>[];
    2271             : 
    2272          62 :     for (final event in events) {
    2273             :       // The client must ignore any new m.room.encryption event to prevent
    2274             :       // man-in-the-middle attacks!
    2275          62 :       if ((event.type == EventTypes.Encryption &&
    2276          31 :           room.encrypted &&
    2277           3 :           event.content.tryGet<String>('algorithm') !=
    2278             :               room
    2279           1 :                   .getState(EventTypes.Encryption)
    2280           1 :                   ?.content
    2281           1 :                   .tryGet<String>('algorithm'))) {
    2282             :         continue;
    2283             :       }
    2284             : 
    2285             :       var update =
    2286          93 :           EventUpdate(roomID: room.id, type: type, content: event.toJson());
    2287          65 :       if (event.type == EventTypes.Encrypted && encryptionEnabled) {
    2288           2 :         update = await update.decrypt(room);
    2289             : 
    2290             :         // if the event failed to decrypt, add it to the queue
    2291           6 :         if (update.content.tryGet<String>('type') == EventTypes.Encrypted) {
    2292           8 :           _eventsPendingDecryption.add(_EventPendingDecryption(EventUpdate(
    2293           2 :               roomID: update.roomID,
    2294             :               type: EventUpdateType.decryptedTimelineQueue,
    2295           2 :               content: update.content)));
    2296             :         }
    2297             :       }
    2298          62 :       if (event.type == EventTypes.Message &&
    2299          31 :           !room.isDirectChat &&
    2300          31 :           database != null &&
    2301          29 :           event is MatrixEvent &&
    2302          58 :           room.getState(EventTypes.RoomMember, event.senderId) == null) {
    2303             :         // In order to correctly render room list previews we need to fetch the member from the database
    2304          87 :         final user = await database?.getUser(event.senderId, room);
    2305             :         if (user != null) {
    2306          29 :           room.setState(user);
    2307             :         }
    2308             :       }
    2309          31 :       _updateRoomsByEventUpdate(room, update);
    2310          31 :       if (type != EventUpdateType.ephemeral && store) {
    2311          60 :         await database?.storeEventUpdate(update, this);
    2312             :       }
    2313          31 :       if (encryptionEnabled) {
    2314          48 :         await encryption?.handleEventUpdate(update);
    2315             :       }
    2316          62 :       onEvent.add(update);
    2317             : 
    2318          31 :       if (prevBatch != null &&
    2319          14 :           (type == EventUpdateType.timeline ||
    2320           6 :               type == EventUpdateType.decryptedTimelineQueue)) {
    2321          14 :         if ((update.content
    2322          14 :                 .tryGet<String>('type')
    2323          28 :                 ?.startsWith(CallConstants.callEventsRegxp) ??
    2324             :             false)) {
    2325           4 :           final callEvent = Event.fromJson(update.content, room);
    2326           2 :           callEvents.add(callEvent);
    2327             :         }
    2328             :       }
    2329             :     }
    2330          31 :     if (callEvents.isNotEmpty) {
    2331           4 :       onCallEvents.add(callEvents);
    2332             :     }
    2333             :   }
    2334             : 
    2335             :   /// stores when we last checked for stale calls
    2336             :   DateTime lastStaleCallRun = DateTime(0);
    2337             : 
    2338          31 :   Future<Room> _updateRoomsByRoomUpdate(
    2339             :       String roomId, SyncRoomUpdate chatUpdate) async {
    2340             :     // Update the chat list item.
    2341             :     // Search the room in the rooms
    2342         155 :     final roomIndex = rooms.indexWhere((r) => r.id == roomId);
    2343          62 :     final found = roomIndex != -1;
    2344          31 :     final membership = chatUpdate is LeftRoomUpdate
    2345             :         ? Membership.leave
    2346          31 :         : chatUpdate is InvitedRoomUpdate
    2347             :             ? Membership.invite
    2348             :             : Membership.join;
    2349             : 
    2350             :     final room = found
    2351          24 :         ? rooms[roomIndex]
    2352          31 :         : (chatUpdate is JoinedRoomUpdate
    2353          31 :             ? Room(
    2354             :                 id: roomId,
    2355             :                 membership: membership,
    2356          62 :                 prev_batch: chatUpdate.timeline?.prevBatch,
    2357             :                 highlightCount:
    2358          62 :                     chatUpdate.unreadNotifications?.highlightCount ?? 0,
    2359             :                 notificationCount:
    2360          62 :                     chatUpdate.unreadNotifications?.notificationCount ?? 0,
    2361          31 :                 summary: chatUpdate.summary,
    2362             :                 client: this,
    2363             :               )
    2364          31 :             : Room(id: roomId, membership: membership, client: this));
    2365             : 
    2366             :     // Does the chat already exist in the list rooms?
    2367          31 :     if (!found && membership != Membership.leave) {
    2368             :       // Check if the room is not in the rooms in the invited list
    2369          62 :       if (_archivedRooms.isNotEmpty) {
    2370          12 :         _archivedRooms.removeWhere((archive) => archive.room.id == roomId);
    2371             :       }
    2372          93 :       final position = membership == Membership.invite ? 0 : rooms.length;
    2373             :       // Add the new chat to the list
    2374          62 :       rooms.insert(position, room);
    2375             :     }
    2376             :     // If the membership is "leave" then remove the item and stop here
    2377          12 :     else if (found && membership == Membership.leave) {
    2378           0 :       rooms.removeAt(roomIndex);
    2379             : 
    2380             :       // in order to keep the archive in sync, add left room to archive
    2381           0 :       if (chatUpdate is LeftRoomUpdate) {
    2382           0 :         await _storeArchivedRoom(room.id, chatUpdate, leftRoom: room);
    2383             :       }
    2384             :     }
    2385             :     // Update notification, highlight count and/or additional information
    2386             :     else if (found &&
    2387          12 :         chatUpdate is JoinedRoomUpdate &&
    2388          48 :         (rooms[roomIndex].membership != membership ||
    2389          48 :             rooms[roomIndex].notificationCount !=
    2390          12 :                 (chatUpdate.unreadNotifications?.notificationCount ?? 0) ||
    2391          48 :             rooms[roomIndex].highlightCount !=
    2392          12 :                 (chatUpdate.unreadNotifications?.highlightCount ?? 0) ||
    2393          12 :             chatUpdate.summary != null ||
    2394          24 :             chatUpdate.timeline?.prevBatch != null)) {
    2395          12 :       rooms[roomIndex].membership = membership;
    2396          12 :       rooms[roomIndex].notificationCount =
    2397           5 :           chatUpdate.unreadNotifications?.notificationCount ?? 0;
    2398          12 :       rooms[roomIndex].highlightCount =
    2399           5 :           chatUpdate.unreadNotifications?.highlightCount ?? 0;
    2400           8 :       if (chatUpdate.timeline?.prevBatch != null) {
    2401          10 :         rooms[roomIndex].prev_batch = chatUpdate.timeline?.prevBatch;
    2402             :       }
    2403             : 
    2404           4 :       final summary = chatUpdate.summary;
    2405             :       if (summary != null) {
    2406           4 :         final roomSummaryJson = rooms[roomIndex].summary.toJson()
    2407           2 :           ..addAll(summary.toJson());
    2408           4 :         rooms[roomIndex].summary = RoomSummary.fromJson(roomSummaryJson);
    2409             :       }
    2410             :       // ignore: deprecated_member_use_from_same_package
    2411          28 :       rooms[roomIndex].onUpdate.add(rooms[roomIndex].id);
    2412           8 :       if ((chatUpdate.timeline?.limited ?? false) &&
    2413           1 :           requestHistoryOnLimitedTimeline) {
    2414           0 :         Logs().v(
    2415           0 :             'Limited timeline for ${rooms[roomIndex].id} request history now');
    2416           0 :         runInRoot(rooms[roomIndex].requestHistory);
    2417             :       }
    2418             :     }
    2419             :     return room;
    2420             :   }
    2421             : 
    2422          31 :   void _updateRoomsByEventUpdate(Room room, EventUpdate eventUpdate) {
    2423          62 :     if (eventUpdate.type == EventUpdateType.history) return;
    2424             : 
    2425          31 :     switch (eventUpdate.type) {
    2426          31 :       case EventUpdateType.inviteState:
    2427          93 :         room.setState(StrippedStateEvent.fromJson(eventUpdate.content));
    2428             :         break;
    2429          31 :       case EventUpdateType.state:
    2430          31 :       case EventUpdateType.timeline:
    2431          62 :         final event = Event.fromJson(eventUpdate.content, room);
    2432             : 
    2433             :         // Update the room state:
    2434          31 :         if (event.stateKey != null &&
    2435         124 :             (!room.partial || importantStateEvents.contains(event.type))) {
    2436          31 :           room.setState(event);
    2437             :         }
    2438          62 :         if (eventUpdate.type != EventUpdateType.timeline) break;
    2439             : 
    2440             :         // If last event is null or not a valid room preview event anyway,
    2441             :         // just use this:
    2442          31 :         if (room.lastEvent == null) {
    2443          31 :           room.lastEvent = event;
    2444             :           break;
    2445             :         }
    2446             : 
    2447             :         // Is this event redacting the last event?
    2448          62 :         if (event.type == EventTypes.Redaction &&
    2449             :             ({
    2450           4 :               room.lastEvent?.eventId,
    2451           4 :               room.lastEvent?.relationshipEventId
    2452           2 :             }.contains(
    2453           6 :                 event.redacts ?? event.content.tryGet<String>('redacts')))) {
    2454           4 :           room.lastEvent?.setRedactionEvent(event);
    2455             :           break;
    2456             :         }
    2457             : 
    2458             :         // Is this event an edit of the last event? Otherwise ignore it.
    2459          62 :         if (event.relationshipType == RelationshipTypes.edit) {
    2460          12 :           if (event.relationshipEventId == room.lastEvent?.eventId ||
    2461           9 :               (room.lastEvent?.relationshipType == RelationshipTypes.edit &&
    2462           6 :                   event.relationshipEventId ==
    2463           6 :                       room.lastEvent?.relationshipEventId)) {
    2464           3 :             room.lastEvent = event;
    2465             :           }
    2466             :           break;
    2467             :         }
    2468             : 
    2469             :         // Is this event of an important type for the last event?
    2470          93 :         if (!roomPreviewLastEvents.contains(event.type)) break;
    2471             : 
    2472             :         // Event is a valid new lastEvent:
    2473          31 :         room.lastEvent = event;
    2474             : 
    2475             :         break;
    2476          31 :       case EventUpdateType.accountData:
    2477         124 :         room.roomAccountData[eventUpdate.content['type']] =
    2478          62 :             BasicRoomEvent.fromJson(eventUpdate.content);
    2479             :         break;
    2480          31 :       case EventUpdateType.ephemeral:
    2481          93 :         room.setEphemeral(BasicRoomEvent.fromJson(eventUpdate.content));
    2482             :         break;
    2483           0 :       case EventUpdateType.history:
    2484           0 :       case EventUpdateType.decryptedTimelineQueue:
    2485             :         break;
    2486             :     }
    2487             :     // ignore: deprecated_member_use_from_same_package
    2488          93 :     room.onUpdate.add(room.id);
    2489             :   }
    2490             : 
    2491             :   bool _sortLock = false;
    2492             : 
    2493             :   /// If `true` then unread rooms are pinned at the top of the room list.
    2494             :   bool pinUnreadRooms;
    2495             : 
    2496             :   /// If `true` then unread rooms are pinned at the top of the room list.
    2497             :   bool pinInvitedRooms;
    2498             : 
    2499             :   /// The compare function how the rooms should be sorted internally. By default
    2500             :   /// rooms are sorted by timestamp of the last m.room.message event or the last
    2501             :   /// event if there is no known message.
    2502          62 :   RoomSorter get sortRoomsBy => (a, b) {
    2503          31 :         if (pinInvitedRooms &&
    2504          93 :             a.membership != b.membership &&
    2505         186 :             [a.membership, b.membership].any((m) => m == Membership.invite)) {
    2506          93 :           return a.membership == Membership.invite ? -1 : 1;
    2507          93 :         } else if (a.isFavourite != b.isFavourite) {
    2508           4 :           return a.isFavourite ? -1 : 1;
    2509          31 :         } else if (pinUnreadRooms &&
    2510           0 :             a.notificationCount != b.notificationCount) {
    2511           0 :           return b.notificationCount.compareTo(a.notificationCount);
    2512             :         } else {
    2513          62 :           return b.timeCreated.millisecondsSinceEpoch
    2514          93 :               .compareTo(a.timeCreated.millisecondsSinceEpoch);
    2515             :         }
    2516             :       };
    2517             : 
    2518          31 :   void _sortRooms() {
    2519         124 :     if (_sortLock || rooms.length < 2) return;
    2520          31 :     _sortLock = true;
    2521          93 :     rooms.sort(sortRoomsBy);
    2522          31 :     _sortLock = false;
    2523             :   }
    2524             : 
    2525             :   Future? userDeviceKeysLoading;
    2526             :   Future? roomsLoading;
    2527             :   Future? _accountDataLoading;
    2528             :   Future? firstSyncReceived;
    2529             : 
    2530          46 :   Future? get accountDataLoading => _accountDataLoading;
    2531             : 
    2532             :   /// A map of known device keys per user.
    2533          50 :   Map<String, DeviceKeysList> get userDeviceKeys => _userDeviceKeys;
    2534             :   Map<String, DeviceKeysList> _userDeviceKeys = {};
    2535             : 
    2536             :   /// A list of all not verified and not blocked device keys. Clients should
    2537             :   /// display a warning if this list is not empty and suggest the user to
    2538             :   /// verify or block those devices.
    2539           0 :   List<DeviceKeys> get unverifiedDevices {
    2540           0 :     final userId = userID;
    2541           0 :     if (userId == null) return [];
    2542           0 :     return userDeviceKeys[userId]
    2543           0 :             ?.deviceKeys
    2544           0 :             .values
    2545           0 :             .where((deviceKey) => !deviceKey.verified && !deviceKey.blocked)
    2546           0 :             .toList() ??
    2547           0 :         [];
    2548             :   }
    2549             : 
    2550             :   /// Gets user device keys by its curve25519 key. Returns null if it isn't found
    2551          23 :   DeviceKeys? getUserDeviceKeysByCurve25519Key(String senderKey) {
    2552          55 :     for (final user in userDeviceKeys.values) {
    2553          18 :       final device = user.deviceKeys.values
    2554          36 :           .firstWhereOrNull((e) => e.curve25519Key == senderKey);
    2555             :       if (device != null) {
    2556             :         return device;
    2557             :       }
    2558             :     }
    2559             :     return null;
    2560             :   }
    2561             : 
    2562          29 :   Future<Set<String>> _getUserIdsInEncryptedRooms() async {
    2563             :     final userIds = <String>{};
    2564          58 :     for (final room in rooms) {
    2565          87 :       if (room.encrypted && room.membership == Membership.join) {
    2566             :         try {
    2567          29 :           final userList = await room.requestParticipants();
    2568          58 :           for (final user in userList) {
    2569          29 :             if ([Membership.join, Membership.invite]
    2570          58 :                 .contains(user.membership)) {
    2571          58 :               userIds.add(user.id);
    2572             :             }
    2573             :           }
    2574             :         } catch (e, s) {
    2575           0 :           Logs().e('[E2EE] Failed to fetch participants', e, s);
    2576             :         }
    2577             :       }
    2578             :     }
    2579             :     return userIds;
    2580             :   }
    2581             : 
    2582             :   final Map<String, DateTime> _keyQueryFailures = {};
    2583             : 
    2584          31 :   Future<void> updateUserDeviceKeys({Set<String>? additionalUsers}) async {
    2585             :     try {
    2586          31 :       final database = this.database;
    2587          31 :       if (!isLogged() || database == null) return;
    2588          29 :       final dbActions = <Future<dynamic> Function()>[];
    2589          29 :       final trackedUserIds = await _getUserIdsInEncryptedRooms();
    2590          29 :       if (!isLogged()) return;
    2591          58 :       trackedUserIds.add(userID!);
    2592           1 :       if (additionalUsers != null) trackedUserIds.addAll(additionalUsers);
    2593             : 
    2594             :       // Remove all userIds we no longer need to track the devices of.
    2595          29 :       _userDeviceKeys
    2596          47 :           .removeWhere((String userId, v) => !trackedUserIds.contains(userId));
    2597             : 
    2598             :       // Check if there are outdated device key lists. Add it to the set.
    2599          29 :       final outdatedLists = <String, List<String>>{};
    2600          59 :       for (final userId in (additionalUsers ?? <String>[])) {
    2601           2 :         outdatedLists[userId] = [];
    2602             :       }
    2603          58 :       for (final userId in trackedUserIds) {
    2604             :         final deviceKeysList =
    2605          87 :             _userDeviceKeys[userId] ??= DeviceKeysList(userId, this);
    2606          87 :         final failure = _keyQueryFailures[userId.domain];
    2607             : 
    2608             :         // deviceKeysList.outdated is not nullable but we have seen this error
    2609             :         // in production: `Failed assertion: boolean expression must not be null`
    2610             :         // So this could either be a null safety bug in Dart or a result of
    2611             :         // using unsound null safety. The extra equal check `!= false` should
    2612             :         // save us here.
    2613          58 :         if (deviceKeysList.outdated != false &&
    2614             :             (failure == null ||
    2615           0 :                 DateTime.now()
    2616           0 :                     .subtract(Duration(minutes: 5))
    2617           0 :                     .isAfter(failure))) {
    2618          58 :           outdatedLists[userId] = [];
    2619             :         }
    2620             :       }
    2621             : 
    2622          29 :       if (outdatedLists.isNotEmpty) {
    2623             :         // Request the missing device key lists from the server.
    2624          29 :         final response = await queryKeys(outdatedLists, timeout: 10000);
    2625          29 :         if (!isLogged()) return;
    2626             : 
    2627          29 :         final deviceKeys = response.deviceKeys;
    2628             :         if (deviceKeys != null) {
    2629          58 :           for (final rawDeviceKeyListEntry in deviceKeys.entries) {
    2630          29 :             final userId = rawDeviceKeyListEntry.key;
    2631             :             final userKeys =
    2632          87 :                 _userDeviceKeys[userId] ??= DeviceKeysList(userId, this);
    2633          58 :             final oldKeys = Map<String, DeviceKeys>.from(userKeys.deviceKeys);
    2634          58 :             userKeys.deviceKeys = {};
    2635             :             for (final rawDeviceKeyEntry
    2636          87 :                 in rawDeviceKeyListEntry.value.entries) {
    2637          29 :               final deviceId = rawDeviceKeyEntry.key;
    2638             : 
    2639             :               // Set the new device key for this device
    2640          29 :               final entry = DeviceKeys.fromMatrixDeviceKeys(
    2641          61 :                   rawDeviceKeyEntry.value, this, oldKeys[deviceId]?.lastActive);
    2642          29 :               final ed25519Key = entry.ed25519Key;
    2643          29 :               final curve25519Key = entry.curve25519Key;
    2644          29 :               if (entry.isValid &&
    2645          58 :                   deviceId == entry.deviceId &&
    2646             :                   ed25519Key != null &&
    2647             :                   curve25519Key != null) {
    2648             :                 // Check if deviceId or deviceKeys are known
    2649          29 :                 if (!oldKeys.containsKey(deviceId)) {
    2650             :                   final oldPublicKeys =
    2651          29 :                       await database.deviceIdSeen(userId, deviceId);
    2652             :                   if (oldPublicKeys != null &&
    2653           4 :                       oldPublicKeys != curve25519Key + ed25519Key) {
    2654           2 :                     Logs().w(
    2655             :                         'Already seen Device ID has been added again. This might be an attack!');
    2656             :                     continue;
    2657             :                   }
    2658          29 :                   final oldDeviceId = await database.publicKeySeen(ed25519Key);
    2659           2 :                   if (oldDeviceId != null && oldDeviceId != deviceId) {
    2660           0 :                     Logs().w(
    2661             :                         'Already seen ED25519 has been added again. This might be an attack!');
    2662             :                     continue;
    2663             :                   }
    2664             :                   final oldDeviceId2 =
    2665          29 :                       await database.publicKeySeen(curve25519Key);
    2666           2 :                   if (oldDeviceId2 != null && oldDeviceId2 != deviceId) {
    2667           0 :                     Logs().w(
    2668             :                         'Already seen Curve25519 has been added again. This might be an attack!');
    2669             :                     continue;
    2670             :                   }
    2671          29 :                   await database.addSeenDeviceId(
    2672          29 :                       userId, deviceId, curve25519Key + ed25519Key);
    2673          29 :                   await database.addSeenPublicKey(ed25519Key, deviceId);
    2674          29 :                   await database.addSeenPublicKey(curve25519Key, deviceId);
    2675             :                 }
    2676             : 
    2677             :                 // is this a new key or the same one as an old one?
    2678             :                 // better store an update - the signatures might have changed!
    2679          29 :                 final oldKey = oldKeys[deviceId];
    2680             :                 if (oldKey == null ||
    2681           9 :                     (oldKey.ed25519Key == entry.ed25519Key &&
    2682           9 :                         oldKey.curve25519Key == entry.curve25519Key)) {
    2683             :                   if (oldKey != null) {
    2684             :                     // be sure to save the verified status
    2685           6 :                     entry.setDirectVerified(oldKey.directVerified);
    2686           6 :                     entry.blocked = oldKey.blocked;
    2687           6 :                     entry.validSignatures = oldKey.validSignatures;
    2688             :                   }
    2689          58 :                   userKeys.deviceKeys[deviceId] = entry;
    2690          58 :                   if (deviceId == deviceID &&
    2691          87 :                       entry.ed25519Key == fingerprintKey) {
    2692             :                     // Always trust the own device
    2693          23 :                     entry.setDirectVerified(true);
    2694             :                   }
    2695          87 :                   dbActions.add(() => database.storeUserDeviceKey(
    2696             :                         userId,
    2697             :                         deviceId,
    2698          58 :                         json.encode(entry.toJson()),
    2699          29 :                         entry.directVerified,
    2700          29 :                         entry.blocked,
    2701          58 :                         entry.lastActive.millisecondsSinceEpoch,
    2702             :                       ));
    2703           0 :                 } else if (oldKeys.containsKey(deviceId)) {
    2704             :                   // This shouldn't ever happen. The same device ID has gotten
    2705             :                   // a new public key. So we ignore the update. TODO: ask krille
    2706             :                   // if we should instead use the new key with unknown verified / blocked status
    2707           0 :                   userKeys.deviceKeys[deviceId] = oldKeys[deviceId]!;
    2708             :                 }
    2709             :               } else {
    2710           0 :                 Logs().w('Invalid device ${entry.userId}:${entry.deviceId}');
    2711             :               }
    2712             :             }
    2713             :             // delete old/unused entries
    2714          32 :             for (final oldDeviceKeyEntry in oldKeys.entries) {
    2715           3 :               final deviceId = oldDeviceKeyEntry.key;
    2716           6 :               if (!userKeys.deviceKeys.containsKey(deviceId)) {
    2717             :                 // we need to remove an old key
    2718             :                 dbActions
    2719           3 :                     .add(() => database.removeUserDeviceKey(userId, deviceId));
    2720             :               }
    2721             :             }
    2722          29 :             userKeys.outdated = false;
    2723             :             dbActions
    2724          87 :                 .add(() => database.storeUserDeviceKeysInfo(userId, false));
    2725             :           }
    2726             :         }
    2727             :         // next we parse and persist the cross signing keys
    2728          29 :         final crossSigningTypes = {
    2729          29 :           'master': response.masterKeys,
    2730          29 :           'self_signing': response.selfSigningKeys,
    2731          29 :           'user_signing': response.userSigningKeys,
    2732             :         };
    2733          58 :         for (final crossSigningKeysEntry in crossSigningTypes.entries) {
    2734          29 :           final keyType = crossSigningKeysEntry.key;
    2735          29 :           final keys = crossSigningKeysEntry.value;
    2736             :           if (keys == null) {
    2737             :             continue;
    2738             :           }
    2739          58 :           for (final crossSigningKeyListEntry in keys.entries) {
    2740          29 :             final userId = crossSigningKeyListEntry.key;
    2741             :             final userKeys =
    2742          58 :                 _userDeviceKeys[userId] ??= DeviceKeysList(userId, this);
    2743             :             final oldKeys =
    2744          58 :                 Map<String, CrossSigningKey>.from(userKeys.crossSigningKeys);
    2745          58 :             userKeys.crossSigningKeys = {};
    2746             :             // add the types we aren't handling atm back
    2747          58 :             for (final oldEntry in oldKeys.entries) {
    2748          87 :               if (!oldEntry.value.usage.contains(keyType)) {
    2749         116 :                 userKeys.crossSigningKeys[oldEntry.key] = oldEntry.value;
    2750             :               } else {
    2751             :                 // There is a previous cross-signing key with  this usage, that we no
    2752             :                 // longer need/use. Clear it from the database.
    2753           6 :                 dbActions.add(() =>
    2754           6 :                     database.removeUserCrossSigningKey(userId, oldEntry.key));
    2755             :               }
    2756             :             }
    2757          29 :             final entry = CrossSigningKey.fromMatrixCrossSigningKey(
    2758          29 :                 crossSigningKeyListEntry.value, this);
    2759          29 :             final publicKey = entry.publicKey;
    2760          29 :             if (entry.isValid && publicKey != null) {
    2761          29 :               final oldKey = oldKeys[publicKey];
    2762           9 :               if (oldKey == null || oldKey.ed25519Key == entry.ed25519Key) {
    2763             :                 if (oldKey != null) {
    2764             :                   // be sure to save the verification status
    2765           6 :                   entry.setDirectVerified(oldKey.directVerified);
    2766           6 :                   entry.blocked = oldKey.blocked;
    2767           6 :                   entry.validSignatures = oldKey.validSignatures;
    2768             :                 }
    2769          58 :                 userKeys.crossSigningKeys[publicKey] = entry;
    2770             :               } else {
    2771             :                 // This shouldn't ever happen. The same device ID has gotten
    2772             :                 // a new public key. So we ignore the update. TODO: ask krille
    2773             :                 // if we should instead use the new key with unknown verified / blocked status
    2774           0 :                 userKeys.crossSigningKeys[publicKey] = oldKey;
    2775             :               }
    2776          87 :               dbActions.add(() => database.storeUserCrossSigningKey(
    2777             :                     userId,
    2778             :                     publicKey,
    2779          58 :                     json.encode(entry.toJson()),
    2780          29 :                     entry.directVerified,
    2781          29 :                     entry.blocked,
    2782             :                   ));
    2783             :             }
    2784          87 :             _userDeviceKeys[userId]?.outdated = false;
    2785             :             dbActions
    2786          87 :                 .add(() => database.storeUserDeviceKeysInfo(userId, false));
    2787             :           }
    2788             :         }
    2789             : 
    2790             :         // now process all the failures
    2791          29 :         if (response.failures != null) {
    2792          87 :           for (final failureDomain in response.failures?.keys ?? <String>[]) {
    2793           0 :             _keyQueryFailures[failureDomain] = DateTime.now();
    2794             :           }
    2795             :         }
    2796             :       }
    2797             : 
    2798          29 :       if (dbActions.isNotEmpty) {
    2799          29 :         if (!isLogged()) return;
    2800          58 :         await database.transaction(() async {
    2801          58 :           for (final f in dbActions) {
    2802          29 :             await f();
    2803             :           }
    2804             :         });
    2805             :       }
    2806             :     } catch (e, s) {
    2807           0 :       Logs().e('[LibOlm] Unable to update user device keys', e, s);
    2808             :     }
    2809             :   }
    2810             : 
    2811             :   bool _toDeviceQueueNeedsProcessing = true;
    2812             : 
    2813             :   /// Processes the to_device queue and tries to send every entry.
    2814             :   /// This function MAY throw an error, which just means the to_device queue wasn't
    2815             :   /// proccessed all the way.
    2816          31 :   Future<void> processToDeviceQueue() async {
    2817          31 :     final database = this.database;
    2818          29 :     if (database == null || !_toDeviceQueueNeedsProcessing) {
    2819             :       return;
    2820             :     }
    2821          29 :     final entries = await database.getToDeviceEventQueue();
    2822          29 :     if (entries.isEmpty) {
    2823          29 :       _toDeviceQueueNeedsProcessing = false;
    2824             :       return;
    2825             :     }
    2826           2 :     for (final entry in entries) {
    2827             :       // Convert the Json Map to the correct format regarding
    2828             :       // https: //matrix.org/docs/spec/client_server/r0.6.1#put-matrix-client-r0-sendtodevice-eventtype-txnid
    2829           3 :       final data = entry.content.map((k, v) =>
    2830           1 :           MapEntry<String, Map<String, Map<String, dynamic>>>(
    2831             :               k,
    2832           3 :               (v as Map).map((k, v) => MapEntry<String, Map<String, dynamic>>(
    2833           1 :                   k, Map<String, dynamic>.from(v)))));
    2834             : 
    2835             :       try {
    2836           3 :         await super.sendToDevice(entry.type, entry.txnId, data);
    2837           1 :       } on MatrixException catch (e) {
    2838           0 :         Logs().w(
    2839           0 :             '[To-Device] failed to to_device message from the queue to the server. Ignoring error: $e');
    2840           0 :         Logs().w('Payload: $data');
    2841             :       }
    2842           2 :       await database.deleteFromToDeviceQueue(entry.id);
    2843             :     }
    2844             :   }
    2845             : 
    2846             :   /// Sends a raw to_device event with a [eventType], a [txnId] and a content
    2847             :   /// [messages]. Before sending, it tries to re-send potentially queued
    2848             :   /// to_device events and adds the current one to the queue, should it fail.
    2849          10 :   @override
    2850             :   Future<void> sendToDevice(
    2851             :     String eventType,
    2852             :     String txnId,
    2853             :     Map<String, Map<String, Map<String, dynamic>>> messages,
    2854             :   ) async {
    2855             :     try {
    2856          10 :       await processToDeviceQueue();
    2857          10 :       await super.sendToDevice(eventType, txnId, messages);
    2858             :     } catch (e, s) {
    2859           2 :       Logs().w(
    2860             :           '[Client] Problem while sending to_device event, retrying later...',
    2861             :           e,
    2862             :           s);
    2863           1 :       final database = this.database;
    2864             :       if (database != null) {
    2865           1 :         _toDeviceQueueNeedsProcessing = true;
    2866           1 :         await database.insertIntoToDeviceQueue(
    2867           1 :             eventType, txnId, json.encode(messages));
    2868             :       }
    2869             :       rethrow;
    2870             :     }
    2871             :   }
    2872             : 
    2873             :   /// Send an (unencrypted) to device [message] of a specific [eventType] to all
    2874             :   /// devices of a set of [users].
    2875           2 :   Future<void> sendToDevicesOfUserIds(
    2876             :     Set<String> users,
    2877             :     String eventType,
    2878             :     Map<String, dynamic> message, {
    2879             :     String? messageId,
    2880             :   }) async {
    2881             :     // Send with send-to-device messaging
    2882           2 :     final data = <String, Map<String, Map<String, dynamic>>>{};
    2883           3 :     for (final user in users) {
    2884           2 :       data[user] = {'*': message};
    2885             :     }
    2886           2 :     await sendToDevice(
    2887           2 :         eventType, messageId ?? generateUniqueTransactionId(), data);
    2888             :     return;
    2889             :   }
    2890             : 
    2891             :   final MultiLock<DeviceKeys> _sendToDeviceEncryptedLock = MultiLock();
    2892             : 
    2893             :   /// Sends an encrypted [message] of this [eventType] to these [deviceKeys].
    2894           9 :   Future<void> sendToDeviceEncrypted(
    2895             :     List<DeviceKeys> deviceKeys,
    2896             :     String eventType,
    2897             :     Map<String, dynamic> message, {
    2898             :     String? messageId,
    2899             :     bool onlyVerified = false,
    2900             :   }) async {
    2901           9 :     final encryption = this.encryption;
    2902           9 :     if (!encryptionEnabled || encryption == null) return;
    2903             :     // Don't send this message to blocked devices, and if specified onlyVerified
    2904             :     // then only send it to verified devices
    2905           9 :     if (deviceKeys.isNotEmpty) {
    2906          18 :       deviceKeys.removeWhere((DeviceKeys deviceKeys) =>
    2907           9 :           deviceKeys.blocked ||
    2908          42 :           (deviceKeys.userId == userID && deviceKeys.deviceId == deviceID) ||
    2909           0 :           (onlyVerified && !deviceKeys.verified));
    2910           9 :       if (deviceKeys.isEmpty) return;
    2911             :     }
    2912             : 
    2913             :     // So that we can guarantee order of encrypted to_device messages to be preserved we
    2914             :     // must ensure that we don't attempt to encrypt multiple concurrent to_device messages
    2915             :     // to the same device at the same time.
    2916             :     // A failure to do so can result in edge-cases where encryption and sending order of
    2917             :     // said to_device messages does not match up, resulting in an olm session corruption.
    2918             :     // As we send to multiple devices at the same time, we may only proceed here if the lock for
    2919             :     // *all* of them is freed and lock *all* of them while sending.
    2920             : 
    2921             :     try {
    2922          18 :       await _sendToDeviceEncryptedLock.lock(deviceKeys);
    2923             : 
    2924             :       // Send with send-to-device messaging
    2925           9 :       final data = await encryption.encryptToDeviceMessage(
    2926             :         deviceKeys,
    2927             :         eventType,
    2928             :         message,
    2929             :       );
    2930             :       eventType = EventTypes.Encrypted;
    2931           9 :       await sendToDevice(
    2932           9 :           eventType, messageId ?? generateUniqueTransactionId(), data);
    2933             :     } finally {
    2934          18 :       _sendToDeviceEncryptedLock.unlock(deviceKeys);
    2935             :     }
    2936             :   }
    2937             : 
    2938             :   /// Sends an encrypted [message] of this [eventType] to these [deviceKeys].
    2939             :   /// This request happens partly in the background and partly in the
    2940             :   /// foreground. It automatically chunks sending to device keys based on
    2941             :   /// activity.
    2942           5 :   Future<void> sendToDeviceEncryptedChunked(
    2943             :     List<DeviceKeys> deviceKeys,
    2944             :     String eventType,
    2945             :     Map<String, dynamic> message,
    2946             :   ) async {
    2947           5 :     if (!encryptionEnabled) return;
    2948             :     // be sure to copy our device keys list
    2949           5 :     deviceKeys = List<DeviceKeys>.from(deviceKeys);
    2950           9 :     deviceKeys.removeWhere((DeviceKeys k) =>
    2951          19 :         k.blocked || (k.userId == userID && k.deviceId == deviceID));
    2952           5 :     if (deviceKeys.isEmpty) return;
    2953           4 :     message = message.copy(); // make sure we deep-copy the message
    2954             :     // make sure all the olm sessions are loaded from database
    2955          16 :     Logs().v('Sending to device chunked... (${deviceKeys.length} devices)');
    2956             :     // sort so that devices we last received messages from get our message first
    2957          16 :     deviceKeys.sort((keyA, keyB) => keyB.lastActive.compareTo(keyA.lastActive));
    2958             :     // and now send out in chunks of 20
    2959             :     const chunkSize = 20;
    2960             : 
    2961             :     // first we send out all the chunks that we await
    2962             :     var i = 0;
    2963             :     // we leave this in a for-loop for now, so that we can easily adjust the break condition
    2964             :     // based on other things, if we want to hard-`await` more devices in the future
    2965          16 :     for (; i < deviceKeys.length && i <= 0; i += chunkSize) {
    2966          12 :       Logs().v('Sending chunk $i...');
    2967           4 :       final chunk = deviceKeys.sublist(
    2968             :           i,
    2969          12 :           i + chunkSize > deviceKeys.length
    2970           4 :               ? deviceKeys.length
    2971           1 :               : i + chunkSize);
    2972             :       // and send
    2973           4 :       await sendToDeviceEncrypted(chunk, eventType, message);
    2974             :     }
    2975             :     // now send out the background chunks
    2976           8 :     if (i < deviceKeys.length) {
    2977             :       // ignore: unawaited_futures
    2978           1 :       () async {
    2979           3 :         for (; i < deviceKeys.length; i += chunkSize) {
    2980             :           // wait 50ms to not freeze the UI
    2981           2 :           await Future.delayed(Duration(milliseconds: 50));
    2982           3 :           Logs().v('Sending chunk $i...');
    2983           1 :           final chunk = deviceKeys.sublist(
    2984             :               i,
    2985           3 :               i + chunkSize > deviceKeys.length
    2986           1 :                   ? deviceKeys.length
    2987           0 :                   : i + chunkSize);
    2988             :           // and send
    2989           1 :           await sendToDeviceEncrypted(chunk, eventType, message);
    2990             :         }
    2991           1 :       }();
    2992             :     }
    2993             :   }
    2994             : 
    2995             :   /// Whether all push notifications are muted using the [.m.rule.master]
    2996             :   /// rule of the push rules: https://matrix.org/docs/spec/client_server/r0.6.0#m-rule-master
    2997           0 :   bool get allPushNotificationsMuted {
    2998             :     final Map<String, Object?>? globalPushRules =
    2999           0 :         _accountData[EventTypes.PushRules]
    3000           0 :             ?.content
    3001           0 :             .tryGetMap<String, Object?>('global');
    3002             :     if (globalPushRules == null) return false;
    3003             : 
    3004           0 :     final globalPushRulesOverride = globalPushRules.tryGetList('override');
    3005             :     if (globalPushRulesOverride != null) {
    3006           0 :       for (final pushRule in globalPushRulesOverride) {
    3007           0 :         if (pushRule['rule_id'] == '.m.rule.master') {
    3008           0 :           return pushRule['enabled'];
    3009             :         }
    3010             :       }
    3011             :     }
    3012             :     return false;
    3013             :   }
    3014             : 
    3015           1 :   Future<void> setMuteAllPushNotifications(bool muted) async {
    3016           1 :     await setPushRuleEnabled(
    3017             :       'global',
    3018             :       PushRuleKind.override,
    3019             :       '.m.rule.master',
    3020             :       muted,
    3021             :     );
    3022             :     return;
    3023             :   }
    3024             : 
    3025             :   /// Changes the password. You should either set oldPasswort or another authentication flow.
    3026           1 :   @override
    3027             :   Future<void> changePassword(String newPassword,
    3028             :       {String? oldPassword,
    3029             :       AuthenticationData? auth,
    3030             :       bool? logoutDevices}) async {
    3031           1 :     final userID = this.userID;
    3032             :     try {
    3033             :       if (oldPassword != null && userID != null) {
    3034           1 :         auth = AuthenticationPassword(
    3035           1 :           identifier: AuthenticationUserIdentifier(user: userID),
    3036             :           password: oldPassword,
    3037             :         );
    3038             :       }
    3039           1 :       await super.changePassword(newPassword,
    3040             :           auth: auth, logoutDevices: logoutDevices);
    3041           0 :     } on MatrixException catch (matrixException) {
    3042           0 :       if (!matrixException.requireAdditionalAuthentication) {
    3043             :         rethrow;
    3044             :       }
    3045           0 :       if (matrixException.authenticationFlows?.length != 1 ||
    3046           0 :           !(matrixException.authenticationFlows?.first.stages
    3047           0 :                   .contains(AuthenticationTypes.password) ??
    3048             :               false)) {
    3049             :         rethrow;
    3050             :       }
    3051             :       if (oldPassword == null || userID == null) {
    3052             :         rethrow;
    3053             :       }
    3054           0 :       return changePassword(
    3055             :         newPassword,
    3056           0 :         auth: AuthenticationPassword(
    3057           0 :           identifier: AuthenticationUserIdentifier(user: userID),
    3058             :           password: oldPassword,
    3059           0 :           session: matrixException.session,
    3060             :         ),
    3061             :         logoutDevices: logoutDevices,
    3062             :       );
    3063             :     } catch (_) {
    3064             :       rethrow;
    3065             :     }
    3066             :   }
    3067             : 
    3068             :   /// Clear all local cached messages, room information and outbound group
    3069             :   /// sessions and perform a new clean sync.
    3070           2 :   Future<void> clearCache() async {
    3071           2 :     await abortSync();
    3072           2 :     _prevBatch = null;
    3073           4 :     rooms.clear();
    3074           4 :     await database?.clearCache();
    3075           6 :     encryption?.keyManager.clearOutboundGroupSessions();
    3076           4 :     _eventsPendingDecryption.clear();
    3077           4 :     onCacheCleared.add(true);
    3078             :     // Restart the syncloop
    3079           2 :     backgroundSync = true;
    3080             :   }
    3081             : 
    3082             :   /// A list of mxids of users who are ignored.
    3083           1 :   List<String> get ignoredUsers =>
    3084           3 :       List<String>.from(_accountData['m.ignored_user_list']
    3085           1 :               ?.content
    3086           1 :               .tryGetMap<String, Object?>('ignored_users')
    3087           1 :               ?.keys ??
    3088           1 :           <String>[]);
    3089             : 
    3090             :   /// Ignore another user. This will clear the local cached messages to
    3091             :   /// hide all previous messages from this user.
    3092           1 :   Future<void> ignoreUser(String userId) async {
    3093           1 :     if (!userId.isValidMatrixId) {
    3094           0 :       throw Exception('$userId is not a valid mxid!');
    3095             :     }
    3096           3 :     await setAccountData(userID!, 'm.ignored_user_list', {
    3097           1 :       'ignored_users': Map.fromEntries(
    3098           6 :           (ignoredUsers..add(userId)).map((key) => MapEntry(key, {}))),
    3099             :     });
    3100           1 :     await clearCache();
    3101             :     return;
    3102             :   }
    3103             : 
    3104             :   /// Unignore a user. This will clear the local cached messages and request
    3105             :   /// them again from the server to avoid gaps in the timeline.
    3106           1 :   Future<void> unignoreUser(String userId) async {
    3107           1 :     if (!userId.isValidMatrixId) {
    3108           0 :       throw Exception('$userId is not a valid mxid!');
    3109             :     }
    3110           2 :     if (!ignoredUsers.contains(userId)) {
    3111           0 :       throw Exception('$userId is not in the ignore list!');
    3112             :     }
    3113           3 :     await setAccountData(userID!, 'm.ignored_user_list', {
    3114           1 :       'ignored_users': Map.fromEntries(
    3115           3 :           (ignoredUsers..remove(userId)).map((key) => MapEntry(key, {}))),
    3116             :     });
    3117           1 :     await clearCache();
    3118             :     return;
    3119             :   }
    3120             : 
    3121             :   /// The newest presence of this user if there is any. Fetches it from the
    3122             :   /// database first and then from the server if necessary or returns offline.
    3123           2 :   Future<CachedPresence> fetchCurrentPresence(
    3124             :     String userId, {
    3125             :     bool fetchOnlyFromCached = false,
    3126             :   }) async {
    3127             :     // ignore: deprecated_member_use_from_same_package
    3128           4 :     final cachedPresence = presences[userId];
    3129             :     if (cachedPresence != null) {
    3130             :       return cachedPresence;
    3131             :     }
    3132             : 
    3133           0 :     final dbPresence = await database?.getPresence(userId);
    3134             :     // ignore: deprecated_member_use_from_same_package
    3135           0 :     if (dbPresence != null) return presences[userId] = dbPresence;
    3136             : 
    3137           0 :     if (fetchOnlyFromCached) return CachedPresence.neverSeen(userId);
    3138             : 
    3139             :     try {
    3140           0 :       final result = await getPresence(userId);
    3141           0 :       final presence = CachedPresence.fromPresenceResponse(result, userId);
    3142           0 :       await database?.storePresence(userId, presence);
    3143             :       // ignore: deprecated_member_use_from_same_package
    3144           0 :       return presences[userId] = presence;
    3145             :     } catch (e) {
    3146           0 :       final presence = CachedPresence.neverSeen(userId);
    3147           0 :       await database?.storePresence(userId, presence);
    3148             :       // ignore: deprecated_member_use_from_same_package
    3149           0 :       return presences[userId] = presence;
    3150             :     }
    3151             :   }
    3152             : 
    3153             :   bool _disposed = false;
    3154             :   bool _aborted = false;
    3155          76 :   Future _currentTransaction = Future.sync(() => {});
    3156             : 
    3157             :   /// Blackholes any ongoing sync call. Currently ongoing sync *processing* is
    3158             :   /// still going to be finished, new data is ignored.
    3159          25 :   Future<void> abortSync() async {
    3160          25 :     _aborted = true;
    3161          25 :     backgroundSync = false;
    3162          50 :     _currentSyncId = -1;
    3163             :     try {
    3164          25 :       await _currentTransaction;
    3165             :     } catch (_) {
    3166             :       // No-OP
    3167             :     }
    3168          25 :     _currentSync = null;
    3169             :     // reset _aborted for being able to restart the sync.
    3170          25 :     _aborted = false;
    3171             :   }
    3172             : 
    3173             :   /// Stops the synchronization and closes the database. After this
    3174             :   /// you can safely make this Client instance null.
    3175          23 :   Future<void> dispose({bool closeDatabase = true}) async {
    3176          23 :     _disposed = true;
    3177          23 :     await abortSync();
    3178          43 :     await encryption?.dispose();
    3179          23 :     _encryption = null;
    3180             :     try {
    3181             :       if (closeDatabase) {
    3182          21 :         final database = _database;
    3183          21 :         _database = null;
    3184             :         await database
    3185          19 :             ?.close()
    3186          19 :             .catchError((e, s) => Logs().w('Failed to close database: ', e, s));
    3187             :       }
    3188             :     } catch (error, stacktrace) {
    3189           0 :       Logs().w('Failed to close database: ', error, stacktrace);
    3190             :     }
    3191             :     return;
    3192             :   }
    3193             : 
    3194           1 :   Future<void> _migrateFromLegacyDatabase({
    3195             :     void Function()? onMigration,
    3196             :   }) async {
    3197           2 :     Logs().i('Check legacy database for migration data...');
    3198           2 :     final legacyDatabase = await legacyDatabaseBuilder?.call(this);
    3199           2 :     final migrateClient = await legacyDatabase?.getClient(clientName);
    3200           1 :     final database = this.database;
    3201             : 
    3202             :     if (migrateClient == null || legacyDatabase == null || database == null) {
    3203           0 :       await legacyDatabase?.close();
    3204           0 :       _initLock = false;
    3205             :       return;
    3206             :     }
    3207           2 :     Logs().i('Found data in the legacy database!');
    3208           0 :     onMigration?.call();
    3209           2 :     _id = migrateClient['client_id'];
    3210             :     final tokenExpiresAtMs =
    3211           2 :         int.tryParse(migrateClient.tryGet<String>('token_expires_at') ?? '');
    3212           1 :     await database.insertClient(
    3213           1 :       clientName,
    3214           1 :       migrateClient['homeserver_url'],
    3215           1 :       migrateClient['token'],
    3216             :       tokenExpiresAtMs == null
    3217             :           ? null
    3218           0 :           : DateTime.fromMillisecondsSinceEpoch(tokenExpiresAtMs),
    3219           1 :       migrateClient['refresh_token'],
    3220           1 :       migrateClient['user_id'],
    3221           1 :       migrateClient['device_id'],
    3222           1 :       migrateClient['device_name'],
    3223             :       null,
    3224           1 :       migrateClient['olm_account'],
    3225             :     );
    3226           2 :     Logs().d('Migrate SSSSCache...');
    3227           2 :     for (final type in cacheTypes) {
    3228           1 :       final ssssCache = await legacyDatabase.getSSSSCache(type);
    3229             :       if (ssssCache != null) {
    3230           0 :         Logs().d('Migrate $type...');
    3231           0 :         await database.storeSSSSCache(
    3232             :           type,
    3233           0 :           ssssCache.keyId ?? '',
    3234           0 :           ssssCache.ciphertext ?? '',
    3235           0 :           ssssCache.content ?? '',
    3236             :         );
    3237             :       }
    3238             :     }
    3239           2 :     Logs().d('Migrate OLM sessions...');
    3240             :     try {
    3241           1 :       final olmSessions = await legacyDatabase.getAllOlmSessions();
    3242           2 :       for (final identityKey in olmSessions.keys) {
    3243           1 :         final sessions = olmSessions[identityKey]!;
    3244           2 :         for (final sessionId in sessions.keys) {
    3245           1 :           final session = sessions[sessionId]!;
    3246           1 :           await database.storeOlmSession(
    3247             :             identityKey,
    3248           1 :             session['session_id'] as String,
    3249           1 :             session['pickle'] as String,
    3250           1 :             session['last_received'] as int,
    3251             :           );
    3252             :         }
    3253             :       }
    3254             :     } catch (e, s) {
    3255           0 :       Logs().e('Unable to migrate OLM sessions!', e, s);
    3256             :     }
    3257           2 :     Logs().d('Migrate Device Keys...');
    3258           1 :     final userDeviceKeys = await legacyDatabase.getUserDeviceKeys(this);
    3259           2 :     for (final userId in userDeviceKeys.keys) {
    3260           3 :       Logs().d('Migrate Device Keys of user $userId...');
    3261           1 :       final deviceKeysList = userDeviceKeys[userId];
    3262             :       for (final crossSigningKey
    3263           4 :           in deviceKeysList?.crossSigningKeys.values ?? <CrossSigningKey>[]) {
    3264           1 :         final pubKey = crossSigningKey.publicKey;
    3265             :         if (pubKey != null) {
    3266           2 :           Logs().d(
    3267           3 :               'Migrate cross signing key with usage ${crossSigningKey.usage} and verified ${crossSigningKey.directVerified}...');
    3268           1 :           await database.storeUserCrossSigningKey(
    3269             :             userId,
    3270             :             pubKey,
    3271           2 :             jsonEncode(crossSigningKey.toJson()),
    3272           1 :             crossSigningKey.directVerified,
    3273           1 :             crossSigningKey.blocked,
    3274             :           );
    3275             :         }
    3276             :       }
    3277             : 
    3278             :       if (deviceKeysList != null) {
    3279           3 :         for (final deviceKeys in deviceKeysList.deviceKeys.values) {
    3280           1 :           final deviceId = deviceKeys.deviceId;
    3281             :           if (deviceId != null) {
    3282           4 :             Logs().d('Migrate device keys for ${deviceKeys.deviceId}...');
    3283           1 :             await database.storeUserDeviceKey(
    3284             :               userId,
    3285             :               deviceId,
    3286           2 :               jsonEncode(deviceKeys.toJson()),
    3287           1 :               deviceKeys.directVerified,
    3288           1 :               deviceKeys.blocked,
    3289           2 :               deviceKeys.lastActive.millisecondsSinceEpoch,
    3290             :             );
    3291             :           }
    3292             :         }
    3293           2 :         Logs().d('Migrate user device keys info...');
    3294           2 :         await database.storeUserDeviceKeysInfo(userId, deviceKeysList.outdated);
    3295             :       }
    3296             :     }
    3297           2 :     Logs().d('Migrate inbound group sessions...');
    3298             :     try {
    3299           1 :       final sessions = await legacyDatabase.getAllInboundGroupSessions();
    3300           3 :       for (var i = 0; i < sessions.length; i++) {
    3301           4 :         Logs().d('$i / ${sessions.length}');
    3302           1 :         final session = sessions[i];
    3303           1 :         await database.storeInboundGroupSession(
    3304           1 :           session.roomId,
    3305           1 :           session.sessionId,
    3306           1 :           session.pickle,
    3307           1 :           session.content,
    3308           1 :           session.indexes,
    3309           1 :           session.allowedAtIndex,
    3310           1 :           session.senderKey,
    3311           1 :           session.senderClaimedKeys,
    3312             :         );
    3313             :       }
    3314             :     } catch (e, s) {
    3315           0 :       Logs().e('Unable to migrate inbound group sessions!', e, s);
    3316             :     }
    3317             : 
    3318           1 :     await legacyDatabase.delete();
    3319             : 
    3320           1 :     _initLock = false;
    3321           1 :     return init(
    3322             :       waitForFirstSync: false,
    3323             :       waitUntilLoadCompletedLoaded: false,
    3324             :     );
    3325             :   }
    3326             : }
    3327             : 
    3328             : class SdkError {
    3329             :   dynamic exception;
    3330             :   StackTrace? stackTrace;
    3331             : 
    3332           6 :   SdkError({this.exception, this.stackTrace});
    3333             : }
    3334             : 
    3335             : class SyncConnectionException implements Exception {
    3336             :   final Object originalException;
    3337           0 :   SyncConnectionException(this.originalException);
    3338             : }
    3339             : 
    3340             : class SyncStatusUpdate {
    3341             :   final SyncStatus status;
    3342             :   final SdkError? error;
    3343             :   final double? progress;
    3344             : 
    3345          31 :   const SyncStatusUpdate(this.status, {this.error, this.progress});
    3346             : }
    3347             : 
    3348             : enum SyncStatus {
    3349             :   waitingForResponse,
    3350             :   processing,
    3351             :   cleaningUp,
    3352             :   finished,
    3353             :   error,
    3354             : }
    3355             : 
    3356             : class BadServerVersionsException implements Exception {
    3357             :   final Set<String> serverVersions, supportedVersions;
    3358             : 
    3359           0 :   BadServerVersionsException(this.serverVersions, this.supportedVersions);
    3360             : 
    3361           0 :   @override
    3362             :   String toString() =>
    3363           0 :       'Server supports the versions: ${serverVersions.toString()} but this application is only compatible with ${supportedVersions.toString()}.';
    3364             : }
    3365             : 
    3366             : class BadServerLoginTypesException implements Exception {
    3367             :   final Set<String> serverLoginTypes, supportedLoginTypes;
    3368             : 
    3369           0 :   BadServerLoginTypesException(this.serverLoginTypes, this.supportedLoginTypes);
    3370             : 
    3371           0 :   @override
    3372             :   String toString() =>
    3373           0 :       'Server supports the Login Types: ${serverLoginTypes.toString()} but this application is only compatible with ${supportedLoginTypes.toString()}.';
    3374             : }
    3375             : 
    3376             : class FileTooBigMatrixException extends MatrixException {
    3377             :   int actualFileSize;
    3378             :   int maxFileSize;
    3379             : 
    3380           0 :   static String _formatFileSize(int size) {
    3381           0 :     if (size < 1024) return '$size B';
    3382           0 :     final i = (log(size) / log(1024)).floor();
    3383           0 :     final num = (size / pow(1024, i));
    3384           0 :     final round = num.round();
    3385           0 :     final numString = round < 10
    3386           0 :         ? num.toStringAsFixed(2)
    3387           0 :         : round < 100
    3388           0 :             ? num.toStringAsFixed(1)
    3389           0 :             : round.toString();
    3390           0 :     return '$numString ${'kMGTPEZY'[i - 1]}B';
    3391             :   }
    3392             : 
    3393           0 :   FileTooBigMatrixException(this.actualFileSize, this.maxFileSize)
    3394           0 :       : super.fromJson({
    3395             :           'errcode': MatrixError.M_TOO_LARGE,
    3396             :           'error':
    3397           0 :               'File size ${_formatFileSize(actualFileSize)} exceeds allowed maximum of ${_formatFileSize(maxFileSize)}'
    3398           0 :         });
    3399             : 
    3400           0 :   @override
    3401             :   String toString() =>
    3402           0 :       'File size ${_formatFileSize(actualFileSize)} exceeds allowed maximum of ${_formatFileSize(maxFileSize)}';
    3403             : }
    3404             : 
    3405             : class ArchivedRoom {
    3406             :   final Room room;
    3407             :   final Timeline timeline;
    3408             : 
    3409           3 :   ArchivedRoom({required this.room, required this.timeline});
    3410             : }
    3411             : 
    3412             : /// An event that is waiting for a key to arrive to decrypt. Times out after some time.
    3413             : class _EventPendingDecryption {
    3414             :   DateTime addedAt = DateTime.now();
    3415             : 
    3416             :   EventUpdate event;
    3417             : 
    3418           0 :   bool get timedOut =>
    3419           0 :       addedAt.add(Duration(minutes: 5)).isBefore(DateTime.now());
    3420             : 
    3421           2 :   _EventPendingDecryption(this.event);
    3422             : }

Generated by: LCOV version 1.14