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

Generated by: LCOV version 1.14