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

Generated by: LCOV version 2.0-1