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