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

Generated by: LCOV version 1.14