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