famedlysdk/lib/src/client.dart

1654 lines
58 KiB
Dart
Raw Normal View History

2019-06-09 11:57:33 +00:00
/*
2020-06-03 10:16:01 +00:00
* Famedly Matrix SDK
* Copyright (C) 2019, 2020 Famedly GmbH
2019-06-09 11:57:33 +00:00
*
2020-06-03 10:16:01 +00:00
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
2019-06-09 11:57:33 +00:00
*
2020-06-03 10:16:01 +00:00
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
2019-06-09 11:57:33 +00:00
*
2020-06-03 10:16:01 +00:00
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
2019-06-09 11:57:33 +00:00
*/
2019-06-09 10:16:48 +00:00
import 'dart:async';
import 'dart:convert';
2019-06-09 10:16:48 +00:00
import 'dart:core';
import 'package:http/http.dart' as http;
import '../encryption.dart';
import '../famedlysdk.dart';
import 'database/database.dart' show Database;
import 'event.dart';
import 'room.dart';
import 'user.dart';
import 'utils/device_keys_list.dart';
2020-06-03 10:16:01 +00:00
import 'utils/event_update.dart';
import 'utils/logs.dart';
import 'utils/matrix_file.dart';
2020-06-03 10:16:01 +00:00
import 'utils/room_update.dart';
import 'utils/to_device_event.dart';
2019-06-09 10:16:48 +00:00
2020-01-03 13:21:15 +00:00
typedef RoomSorter = int Function(Room a, Room b);
2020-01-02 14:09:49 +00:00
enum LoginState { logged, loggedOut }
2019-06-09 12:33:25 +00:00
/// Represents a Matrix client to communicate with a
2019-06-09 10:16:48 +00:00
/// [Matrix](https://matrix.org) homeserver and is the entry point for this
/// SDK.
2020-08-11 16:11:51 +00:00
class Client extends MatrixApi {
2020-05-15 18:40:17 +00:00
int _id;
int get id => _id;
2019-06-09 10:16:48 +00:00
2020-05-15 18:40:17 +00:00
Database database;
2020-05-20 08:24:48 +00:00
bool enableE2eeRecovery;
2020-08-11 16:11:51 +00:00
@deprecated
MatrixApi get api => this;
2020-06-03 10:16:01 +00:00
Encryption encryption;
2020-06-05 20:03:28 +00:00
Set<KeyVerificationMethod> verificationMethods;
Set<String> importantStateEvents;
2020-09-16 08:18:13 +00:00
Set<String> roomPreviewLastEvents;
2020-09-21 10:28:13 +00:00
int sendMessageTimeoutSeconds;
2020-05-20 08:24:48 +00:00
/// Create a client
2020-09-16 08:18:13 +00:00
/// [clientName] = unique identifier of this client
/// [database]: The database instance to use
/// [enableE2eeRecovery]: Enable additional logic to try to recover from bad e2ee sessions
/// [verificationMethods]: A set of all the verification methods this client can handle. Includes:
2020-05-30 11:55:09 +00:00
/// KeyVerificationMethod.numbers: Compare numbers. Most basic, should be supported
/// KeyVerificationMethod.emoji: Compare emojis
2020-09-16 08:18:13 +00:00
/// [importantStateEvents]: A set of all the important state events to load when the client connects.
/// To speed up performance only a set of state events is loaded on startup, those that are
/// needed to display a room list. All the remaining state events are automatically post-loaded
/// when opening the timeline of a room or manually by calling `room.postLoad()`.
/// This set will always include the following state events:
/// - m.room.name
/// - m.room.avatar
/// - m.room.message
/// - m.room.encrypted
/// - m.room.encryption
/// - m.room.canonical_alias
/// - m.room.tombstone
/// - *some* m.room.member events, where needed
2020-09-16 08:18:13 +00:00
/// [roomPreviewLastEvents]: The event types that should be used to calculate the last event
/// in a room for the room list.
2020-08-06 06:55:35 +00:00
Client(
this.clientName, {
this.database,
this.enableE2eeRecovery = false,
this.verificationMethods,
http.Client httpClient,
this.importantStateEvents,
2020-09-16 08:18:13 +00:00
this.roomPreviewLastEvents,
2020-08-06 06:55:35 +00:00
this.pinUnreadRooms = false,
2020-09-21 10:28:13 +00:00
this.sendMessageTimeoutSeconds = 60,
2020-08-06 06:55:35 +00:00
@deprecated bool debug,
}) {
2020-06-05 20:03:28 +00:00
verificationMethods ??= <KeyVerificationMethod>{};
2020-09-16 08:18:13 +00:00
importantStateEvents ??= {};
importantStateEvents.addAll([
2020-07-02 08:32:11 +00:00
EventTypes.RoomName,
EventTypes.RoomAvatar,
EventTypes.Message,
EventTypes.Encrypted,
EventTypes.Encryption,
EventTypes.RoomCanonicalAlias,
EventTypes.RoomTombstone,
]);
2020-09-16 08:18:13 +00:00
roomPreviewLastEvents ??= {};
roomPreviewLastEvents.addAll([
EventTypes.Message,
EventTypes.Encrypted,
EventTypes.Sticker,
]);
2020-08-14 16:22:31 +00:00
this.httpClient = httpClient ?? http.Client();
2019-06-09 10:16:48 +00:00
}
/// The required name for this client.
final String clientName;
/// The Matrix ID of the current logged user.
2020-01-02 14:09:49 +00:00
String get userID => _userID;
String _userID;
2019-06-09 10:16:48 +00:00
/// This points to the position in the synchronization history.
String prevBatch;
/// The device ID is an unique identifier for this device.
2020-01-02 14:09:49 +00:00
String get deviceID => _deviceID;
String _deviceID;
2019-06-09 10:16:48 +00:00
/// The device name is a human readable identifier for this device.
2020-01-02 14:09:49 +00:00
String get deviceName => _deviceName;
String _deviceName;
2019-06-09 10:16:48 +00:00
/// Returns the current login state.
2020-08-11 16:11:51 +00:00
bool isLogged() => accessToken != null;
2019-06-09 10:16:48 +00:00
2019-08-07 10:06:28 +00:00
/// A list of all rooms the user is participating or invited.
2020-01-02 14:09:49 +00:00
List<Room> get rooms => _rooms;
List<Room> _rooms = [];
2019-08-07 09:38:51 +00:00
/// Whether this client supports end-to-end encryption using olm.
bool get encryptionEnabled => encryption != null && encryption.enabled;
2020-03-16 10:38:03 +00:00
/// Whether this client is able to encrypt and decrypt files.
bool get fileEncryptionEnabled => encryptionEnabled && true;
String get identityKey => encryption?.identityKey ?? '';
String get fingerprintKey => encryption?.fingerprintKey ?? '';
2020-03-16 10:38:03 +00:00
/// Wheather this session is unknown to others
2020-05-27 15:37:14 +00:00
bool get isUnknownSession =>
!userDeviceKeys.containsKey(userID) ||
!userDeviceKeys[userID].deviceKeys.containsKey(deviceID) ||
!userDeviceKeys[userID].deviceKeys[deviceID].signed;
2020-01-04 13:51:00 +00:00
/// Warning! This endpoint is for testing only!
set rooms(List<Room> newList) {
2020-08-06 09:35:02 +00:00
Logs.warning('Warning! This endpoint is for testing only!');
2020-01-04 13:51:00 +00:00
_rooms = newList;
}
2019-08-07 10:06:28 +00:00
/// Key/Value store of account data.
2020-06-03 10:16:01 +00:00
Map<String, BasicEvent> accountData = {};
2019-08-07 10:06:28 +00:00
/// Presences of users by a given matrix ID
Map<String, Presence> presences = {};
int _transactionCounter = 0;
2020-06-03 10:16:01 +00:00
String generateUniqueTransactionId() {
_transactionCounter++;
return '${clientName}-${_transactionCounter}-${DateTime.now().millisecondsSinceEpoch}';
}
2020-01-02 14:09:49 +00:00
Room getRoomByAlias(String alias) {
for (var i = 0; i < rooms.length; i++) {
2020-01-02 14:09:49 +00:00
if (rooms[i].canonicalAlias == alias) return rooms[i];
}
return null;
}
Room getRoomById(String id) {
for (var j = 0; j < rooms.length; j++) {
2020-01-02 14:09:49 +00:00
if (rooms[j].id == id) return rooms[j];
}
return null;
}
2019-08-08 07:58:37 +00:00
Map<String, dynamic> get directChats =>
accountData['m.direct'] != null ? accountData['m.direct'].content : {};
2019-08-07 10:06:28 +00:00
/// Returns the (first) room ID from the store which is a private chat with the user [userId].
/// Returns null if there is none.
2019-08-29 09:12:14 +00:00
String getDirectChatFromUserId(String userId) {
if (accountData['m.direct'] != null &&
accountData['m.direct'].content[userId] is List<dynamic> &&
accountData['m.direct'].content[userId].length > 0) {
for (final roomId in accountData['m.direct'].content[userId]) {
final room = getRoomById(roomId);
if (room != null && room.membership == Membership.join) {
return roomId;
}
2020-01-02 14:33:26 +00:00
}
2019-08-29 09:12:14 +00:00
}
for (var i = 0; i < rooms.length; i++) {
if (rooms[i].membership == Membership.invite &&
rooms[i].states[userID]?.senderId == userId &&
rooms[i].states[userID].content['is_direct'] == true) {
return rooms[i].id;
2020-01-02 14:33:26 +00:00
}
}
2019-08-29 09:12:14 +00:00
return null;
}
2019-08-07 10:06:28 +00:00
2020-05-16 06:42:56 +00:00
/// Gets discovery information about the domain. The file may include additional keys.
Future<WellKnownInformations> getWellKnownInformationsByUserId(
String MatrixIdOrDomain,
) async {
final response = await http
.get('https://${MatrixIdOrDomain.domain}/.well-known/matrix/client');
2020-10-06 20:36:40 +00:00
var wellKnown = WellKnownInformations.fromJson(json.decode(response.body));
if (Uri.parse(wellKnown.mHomeserver.baseUrl).host !=
MatrixIdOrDomain.domain) {
2020-10-14 20:07:36 +00:00
try {
final response = await http.get(
'https://${Uri.parse(wellKnown.mHomeserver.baseUrl).host}/.well-known/matrix/client');
if (response.statusCode == 200) {
wellKnown =
WellKnownInformations.fromJson(json.decode(response.body));
}
} catch (_) {}
2020-06-13 19:39:18 +00:00
}
return wellKnown;
2020-05-16 06:42:56 +00:00
}
Future<WellKnownInformations> getWellKnownInformationsByDomain(
dynamic serverUrl) async {
2020-10-06 20:36:40 +00:00
var homeserver = (serverUrl is Uri) ? serverUrl : Uri.parse(serverUrl);
final response =
await http.get('https://${homeserver.host}/.well-known/matrix/client');
2020-10-06 20:36:40 +00:00
var wellKnown = WellKnownInformations.fromJson(json.decode(response.body));
2020-06-13 19:39:18 +00:00
if (Uri.parse(wellKnown.mHomeserver.baseUrl).host != homeserver.host) {
2020-10-14 20:07:36 +00:00
try {
final response = await http.get(
'https://${Uri.parse(wellKnown.mHomeserver.baseUrl).host}/.well-known/matrix/client');
if (response.statusCode == 200) {
wellKnown =
WellKnownInformations.fromJson(json.decode(response.body));
}
} catch (_) {}
}
2020-06-13 19:39:18 +00:00
return wellKnown;
}
2020-10-23 09:34:08 +00:00
@Deprecated('Use [checkHomeserver] instead.')
2020-06-03 10:16:01 +00:00
Future<bool> checkServer(dynamic serverUrl) async {
2019-12-29 10:28:33 +00:00
try {
2020-10-23 09:34:08 +00:00
await checkHomeserver(serverUrl);
} catch (_) {
return false;
}
return true;
}
/// Checks the supported versions of the Matrix protocol and the supported
/// login types. Throws an exception if the server is not compatible with the
/// client and sets [homeserver] to [serverUrl] if it is. Supports the types [Uri]
/// and [String].
Future<void> checkHomeserver(dynamic homeserverUrl,
{Set<String> supportedLoginTypes = supportedLoginTypes}) async {
try {
if (homeserverUrl is Uri) {
homeserver = homeserverUrl;
2020-07-27 07:40:25 +00:00
} else {
// URLs allow to have whitespace surrounding them, see https://www.w3.org/TR/2011/WD-html5-20110525/urls.html
// As we want to strip a trailing slash, though, we have to trim the url ourself
// and thus can't let Uri.parse() deal with it.
2020-10-23 09:34:08 +00:00
homeserverUrl = homeserverUrl.trim();
2020-07-27 07:40:25 +00:00
// strip a trailing slash
2020-10-23 09:34:08 +00:00
if (homeserverUrl.endsWith('/')) {
homeserverUrl = homeserverUrl.substring(0, homeserverUrl.length - 1);
2020-07-27 07:40:25 +00:00
}
2020-10-23 09:34:08 +00:00
homeserver = Uri.parse(homeserverUrl);
2020-07-27 07:40:25 +00:00
}
2020-08-11 16:11:51 +00:00
final versions = await requestSupportedVersions();
2019-12-29 10:28:33 +00:00
2020-10-23 09:34:08 +00:00
if (!versions.versions
.any((version) => supportedVersions.contains(version))) {
throw Exception(
'Server supports the versions: ${versions.versions.toString()} but this application is only compatible with ${supportedVersions.toString()}.');
2019-06-09 10:16:48 +00:00
}
2020-08-11 16:11:51 +00:00
final loginTypes = await requestLoginTypes();
2020-10-23 09:34:08 +00:00
if (!loginTypes.flows.any((f) => supportedLoginTypes.contains(f.type))) {
throw Exception(
'Server supports the Login Types: ${loginTypes.flows.map((f) => f.toJson).toList().toString()} but this application is only compatible with ${supportedLoginTypes.toString()}.');
2019-06-09 10:16:48 +00:00
}
2020-06-03 10:16:01 +00:00
2020-10-23 09:34:08 +00:00
return;
2019-12-29 10:28:33 +00:00
} catch (_) {
2020-08-11 16:11:51 +00:00
homeserver = null;
2019-12-29 10:28:33 +00:00
rethrow;
2019-06-09 10:16:48 +00:00
}
}
2020-01-14 15:16:24 +00:00
/// Checks to see if a username is available, and valid, for the server.
/// Returns the fully-qualified Matrix user ID (MXID) that has been registered.
2020-10-23 09:34:08 +00:00
/// You have to call [checkHomeserver] first to set a homeserver.
2020-08-11 16:11:51 +00:00
@override
Future<LoginResponse> register({
2020-01-14 15:16:24 +00:00
String username,
String password,
String deviceId,
String initialDeviceDisplayName,
bool inhibitLogin,
2020-08-11 16:11:51 +00:00
Map<String, dynamic> auth,
String kind,
2020-01-14 15:16:24 +00:00
}) async {
2020-08-11 16:11:51 +00:00
final response = await super.register(
2020-06-03 10:16:01 +00:00
username: username,
password: password,
auth: auth,
deviceId: deviceId,
initialDeviceDisplayName: initialDeviceDisplayName,
inhibitLogin: inhibitLogin,
);
2020-01-14 15:16:24 +00:00
// Connect if there is an access token in the response.
2020-06-03 10:16:01 +00:00
if (response.accessToken == null ||
response.deviceId == null ||
response.userId == null) {
throw 'Registered but token, device ID or user ID is null.';
}
await connect(
newToken: response.accessToken,
newUserID: response.userId,
2020-08-11 16:11:51 +00:00
newHomeserver: homeserver,
2020-06-03 10:16:01 +00:00
newDeviceName: initialDeviceDisplayName ?? '',
newDeviceID: response.deviceId);
2020-08-11 16:11:51 +00:00
return response;
2020-01-14 15:16:24 +00:00
}
2019-06-09 10:16:48 +00:00
/// Handles the login and allows the client to call all APIs which require
2019-12-29 10:28:33 +00:00
/// authentication. Returns false if the login was not successful. Throws
/// MatrixException if login was not successful.
2020-10-23 09:34:08 +00:00
/// You have to call [checkHomeserver] first to set a homeserver.
2020-08-11 16:11:51 +00:00
@override
Future<LoginResponse> login({
String type = 'm.login.password',
String userIdentifierType = 'm.id.user',
String user,
String medium,
String address,
String password,
String token,
2020-01-14 15:16:24 +00:00
String deviceId,
2020-08-11 16:11:51 +00:00
String initialDeviceDisplayName,
2020-01-14 15:16:24 +00:00
}) async {
2020-08-11 16:11:51 +00:00
final loginResp = await super.login(
type: type,
userIdentifierType: userIdentifierType,
user: user,
2020-06-03 10:16:01 +00:00
password: password,
deviceId: deviceId,
initialDeviceDisplayName: initialDeviceDisplayName,
2020-08-11 16:11:51 +00:00
medium: medium,
address: address,
token: token,
2020-06-03 10:16:01 +00:00
);
2020-06-03 10:16:01 +00:00
// Connect if there is an access token in the response.
if (loginResp.accessToken == null ||
loginResp.deviceId == null ||
loginResp.userId == null) {
2020-08-11 16:11:51 +00:00
throw Exception('Registered but token, device ID or user ID is null.');
2020-06-03 10:16:01 +00:00
}
await connect(
newToken: loginResp.accessToken,
newUserID: loginResp.userId,
2020-08-11 16:11:51 +00:00
newHomeserver: homeserver,
2020-06-03 10:16:01 +00:00
newDeviceName: initialDeviceDisplayName ?? '',
newDeviceID: loginResp.deviceId,
);
2020-08-11 16:11:51 +00:00
return loginResp;
2019-06-09 10:16:48 +00:00
}
/// Sends a logout command to the homeserver and clears all local data,
/// including all persistent data from the store.
2020-08-11 16:11:51 +00:00
@override
2019-06-09 10:16:48 +00:00
Future<void> logout() async {
2019-12-29 10:28:33 +00:00
try {
2020-08-11 16:11:51 +00:00
await super.logout();
2020-08-06 09:35:02 +00:00
} catch (e, s) {
Logs.error(e, s);
2019-12-29 10:28:33 +00:00
rethrow;
} finally {
await clear();
2019-12-29 10:28:33 +00:00
}
2019-06-09 10:16:48 +00:00
}
2020-08-21 09:02:20 +00:00
/// Sends a logout command to the homeserver and clears all local data,
/// including all persistent data from the store.
@override
Future<void> logoutAll() async {
try {
await super.logoutAll();
} catch (e, s) {
Logs.error(e, s);
2019-12-29 10:28:33 +00:00
rethrow;
} finally {
await clear();
2019-12-29 10:28:33 +00:00
}
2019-06-09 10:16:48 +00:00
}
2020-01-18 14:49:15 +00:00
/// Returns the user's own displayname and avatar url. In Matrix it is possible that
/// one user can have different displaynames and avatar urls in different rooms. So
/// this endpoint first checks if the profile is the same in all rooms. If not, the
/// profile will be requested from the homserver.
Future<Profile> get ownProfile async {
if (rooms.isNotEmpty) {
var profileSet = <Profile>{};
for (var room in rooms) {
2020-01-18 14:49:15 +00:00
final user = room.getUserByMXIDSync(userID);
profileSet.add(Profile.fromJson(user.content));
}
if (profileSet.length == 1) return profileSet.first;
}
return getProfileFromUserId(userID);
}
2020-05-18 11:45:49 +00:00
final Map<String, Profile> _profileCache = {};
/// Get the combined profile information for this user.
/// If [getFromRooms] is true then the profile will first be searched from the
/// room memberships. This is unstable if the given user makes use of different displaynames
/// and avatars per room, which is common for some bots and bridges.
/// If [cache] is true then
/// the profile get cached for this session. Please note that then the profile may
/// become outdated if the user changes the displayname or avatar in this session.
Future<Profile> getProfileFromUserId(String userId,
{bool cache = true, bool getFromRooms = true}) async {
if (getFromRooms) {
final room = rooms.firstWhere(
(Room room) =>
room
.getParticipants()
.indexWhere((User user) => user.id == userId) !=
-1,
orElse: () => null);
if (room != null) {
final user =
room.getParticipants().firstWhere((User user) => user.id == userId);
return Profile(user.displayName, user.avatarUrl);
}
}
if (cache && _profileCache.containsKey(userId)) {
return _profileCache[userId];
}
2020-08-11 16:11:51 +00:00
final profile = await requestProfile(userId);
2020-05-18 11:45:49 +00:00
_profileCache[userId] = profile;
return profile;
2019-11-30 09:36:30 +00:00
}
2019-12-19 11:26:21 +00:00
Future<List<Room>> get archive async {
var archiveList = <Room>[];
2020-08-11 16:11:51 +00:00
final syncResp = await sync(
2020-06-03 10:16:01 +00:00
filter: '{"room":{"include_leave":true,"timeline":{"limit":10}}}',
timeout: 0,
);
2020-08-11 16:11:51 +00:00
if (syncResp.rooms.leave is Map<String, dynamic>) {
for (var entry in syncResp.rooms.leave.entries) {
2020-06-03 10:16:01 +00:00
final id = entry.key;
final room = entry.value;
var leftRoom = Room(
2019-12-19 11:26:21 +00:00
id: id,
membership: Membership.leave,
client: this,
2020-06-03 10:16:01 +00:00
roomAccountData:
room.accountData?.asMap()?.map((k, v) => MapEntry(v.type, v)) ??
<String, BasicRoomEvent>{},
2019-12-19 11:26:21 +00:00
mHeroes: []);
2020-06-03 10:16:01 +00:00
if (room.timeline?.events != null) {
for (var event in room.timeline.events) {
leftRoom.setState(Event.fromMatrixEvent(event, leftRoom));
2019-12-19 11:26:21 +00:00
}
}
2020-06-03 10:16:01 +00:00
if (room.state != null) {
for (var event in room.state) {
leftRoom.setState(Event.fromMatrixEvent(event, leftRoom));
2019-12-19 11:26:21 +00:00
}
}
archiveList.add(leftRoom);
}
}
2019-11-29 16:19:32 +00:00
return archiveList;
2019-09-19 14:00:17 +00:00
}
2019-12-29 10:28:33 +00:00
/// Uploads a new user avatar for this user.
Future<void> setAvatar(MatrixFile file) async {
2020-08-11 16:11:51 +00:00
final uploadResp = await upload(file.bytes, file.name);
await setAvatarUrl(userID, Uri.parse(uploadResp));
2019-12-29 10:28:33 +00:00
return;
2019-09-09 13:22:02 +00:00
}
2020-01-14 11:27:26 +00:00
/// Returns the push rules for the logged in user.
2020-06-03 10:16:01 +00:00
PushRuleSet get pushRules => accountData.containsKey('m.push_rules')
? PushRuleSet.fromJson(accountData['m.push_rules'].content)
2020-01-14 11:27:26 +00:00
: null;
2020-10-23 09:34:08 +00:00
static const Set<String> supportedVersions = {'r0.5.0', 'r0.6.0'};
static const Set<String> supportedLoginTypes = {'m.login.password'};
static const String syncFilters =
'{"room":{"state":{"lazy_load_members":true}}}';
static const String messagesFilters = '{"lazy_load_members":true}';
2020-02-04 13:41:13 +00:00
static const List<String> supportedDirectEncryptionAlgorithms = [
'm.olm.v1.curve25519-aes-sha2'
2020-02-04 13:41:13 +00:00
];
static const List<String> supportedGroupEncryptionAlgorithms = [
'm.megolm.v1.aes-sha2'
2020-02-04 13:41:13 +00:00
];
2020-04-23 08:18:33 +00:00
static const int defaultThumbnailSize = 256;
2020-01-02 14:09:49 +00:00
/// The newEvent signal is the most important signal in this concept. Every time
/// the app receives a new synchronization, this event is called for every signal
/// to update the GUI. For example, for a new message, it is called:
/// onRoomEvent( "m.room.message", "!chat_id:server.com", "timeline", {sender: "@bob:server.com", body: "Hello world"} )
2020-01-02 14:33:26 +00:00
final StreamController<EventUpdate> onEvent = StreamController.broadcast();
2020-01-02 14:09:49 +00:00
/// Outside of the events there are updates for the global chat states which
/// are handled by this signal:
final StreamController<RoomUpdate> onRoomUpdate =
2020-01-02 14:33:26 +00:00
StreamController.broadcast();
2020-01-02 14:09:49 +00:00
/// The onToDeviceEvent is called when there comes a new to device event. It is
/// already decrypted if necessary.
final StreamController<ToDeviceEvent> onToDeviceEvent =
StreamController.broadcast();
2020-01-02 14:09:49 +00:00
/// Called when the login state e.g. user gets logged out.
final StreamController<LoginState> onLoginStateChanged =
2020-01-02 14:33:26 +00:00
StreamController.broadcast();
2020-01-02 14:09:49 +00:00
2020-06-01 18:24:41 +00:00
/// Synchronization erros are coming here.
2020-08-26 07:38:14 +00:00
final StreamController<SdkError> onSyncError = StreamController.broadcast();
2020-06-01 18:24:41 +00:00
2020-05-22 13:51:45 +00:00
/// Synchronization erros are coming here.
final StreamController<ToDeviceEventDecryptionError> onOlmError =
StreamController.broadcast();
2020-01-02 14:09:49 +00:00
/// This is called once, when the first sync has received.
2020-01-02 14:33:26 +00:00
final StreamController<bool> onFirstSync = StreamController.broadcast();
2020-01-02 14:09:49 +00:00
/// When a new sync response is coming in, this gives the complete payload.
2020-06-03 10:16:01 +00:00
final StreamController<SyncUpdate> onSync = StreamController.broadcast();
2020-01-02 14:09:49 +00:00
2020-01-04 10:29:38 +00:00
/// Callback will be called on presences.
final StreamController<Presence> onPresence = StreamController.broadcast();
/// Callback will be called on account data updates.
2020-06-03 10:16:01 +00:00
final StreamController<BasicEvent> onAccountData =
2020-01-04 10:29:38 +00:00
StreamController.broadcast();
2020-01-04 18:36:17 +00:00
/// Will be called on call invites.
final StreamController<Event> onCallInvite = StreamController.broadcast();
/// Will be called on call hangups.
final StreamController<Event> onCallHangup = StreamController.broadcast();
/// Will be called on call candidates.
final StreamController<Event> onCallCandidates = StreamController.broadcast();
/// Will be called on call answers.
final StreamController<Event> onCallAnswer = StreamController.broadcast();
2020-02-21 15:05:19 +00:00
/// Will be called when another device is requesting session keys for a room.
final StreamController<RoomKeyRequest> onRoomKeyRequest =
StreamController.broadcast();
2020-05-17 13:25:42 +00:00
/// Will be called when another device is requesting verification with this device.
2020-05-18 11:45:49 +00:00
final StreamController<KeyVerification> onKeyVerificationRequest =
StreamController.broadcast();
2020-05-17 13:25:42 +00:00
2020-01-02 14:09:49 +00:00
/// How long should the app wait until it retrys the synchronisation after
/// an error?
int syncErrorTimeoutSec = 3;
/// Sets the user credentials and starts the synchronisation.
///
/// Before you can connect you need at least an [accessToken], a [homeserver],
/// a [userID], a [deviceID], and a [deviceName].
///
/// You get this informations
/// by logging in to your Matrix account, using the [login API](https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-login).
///
/// To log in you can use [jsonRequest()] after you have set the [homeserver]
/// to a valid url. For example:
///
/// ```
/// final resp = await matrix
2020-06-03 10:16:01 +00:00
/// .jsonRequest(type: RequestType.POST, action: "/client/r0/login", data: {
2020-01-02 14:09:49 +00:00
/// "type": "m.login.password",
/// "user": "test",
/// "password": "1234",
2020-08-11 16:11:51 +00:00
/// "initial_device_display_name": "Matrix Client"
2020-01-02 14:09:49 +00:00
/// });
/// ```
///
/// Returns:
///
/// ```
/// {
/// "user_id": "@cheeky_monkey:matrix.org",
/// "access_token": "abc123",
/// "device_id": "GHTYAJCE"
/// }
/// ```
///
/// Sends [LoginState.logged] to [onLoginStateChanged].
2020-02-04 13:41:13 +00:00
void connect({
String newToken,
2020-06-03 10:16:01 +00:00
Uri newHomeserver,
2020-02-04 13:41:13 +00:00
String newUserID,
String newDeviceName,
String newDeviceID,
String newPrevBatch,
String newOlmAccount,
2020-02-04 13:41:13 +00:00
}) async {
2020-05-15 18:40:17 +00:00
String olmAccount;
if (database != null) {
final account = await database.getClient(clientName);
if (account != null) {
_id = account.clientId;
2020-08-11 16:11:51 +00:00
homeserver = Uri.parse(account.homeserverUrl);
accessToken = account.token;
2020-05-15 18:40:17 +00:00
_userID = account.userId;
_deviceID = account.deviceId;
_deviceName = account.deviceName;
prevBatch = account.prevBatch;
olmAccount = account.olmAccount;
}
}
2020-08-11 16:11:51 +00:00
accessToken = newToken ?? accessToken;
homeserver = newHomeserver ?? homeserver;
2020-05-15 18:40:17 +00:00
_userID = newUserID ?? _userID;
_deviceID = newDeviceID ?? _deviceID;
_deviceName = newDeviceName ?? _deviceName;
prevBatch = newPrevBatch ?? prevBatch;
olmAccount = newOlmAccount ?? olmAccount;
2020-08-11 16:11:51 +00:00
if (accessToken == null || homeserver == null || _userID == null) {
2020-05-15 18:40:17 +00:00
// we aren't logged in
encryption?.dispose();
encryption = null;
2020-05-16 08:03:59 +00:00
onLoginStateChanged.add(LoginState.loggedOut);
2020-05-15 18:40:17 +00:00
return;
}
2020-01-02 14:09:49 +00:00
2020-08-17 12:25:48 +00:00
encryption?.dispose();
2020-08-06 06:55:35 +00:00
encryption =
Encryption(client: this, enableE2eeRecovery: enableE2eeRecovery);
await encryption.init(olmAccount);
2020-05-15 18:40:17 +00:00
if (database != null) {
if (id != null) {
await database.updateClient(
2020-08-11 16:11:51 +00:00
homeserver.toString(),
accessToken,
2020-05-16 06:42:56 +00:00
_userID,
_deviceID,
_deviceName,
prevBatch,
encryption?.pickledOlmAccount,
2020-05-16 06:42:56 +00:00
id,
2020-05-15 18:40:17 +00:00
);
} else {
_id = await database.insertClient(
2020-05-16 06:42:56 +00:00
clientName,
2020-08-11 16:11:51 +00:00
homeserver.toString(),
accessToken,
2020-05-16 06:42:56 +00:00
_userID,
_deviceID,
_deviceName,
prevBatch,
encryption?.pickledOlmAccount,
2020-05-15 18:40:17 +00:00
);
}
2020-06-05 20:03:28 +00:00
_userDeviceKeys = await database.getUserDeviceKeys(this);
2020-05-15 18:40:17 +00:00
_rooms = await database.getRoomList(this, onlyLeft: false);
_sortRooms();
accountData = await database.getAccountData(id);
presences.clear();
2020-01-02 14:09:49 +00:00
}
onLoginStateChanged.add(LoginState.logged);
2020-08-06 09:35:02 +00:00
Logs.success(
2020-08-11 16:11:51 +00:00
'Successfully connected as ${userID.localpart} with ${homeserver.toString()}',
2020-08-06 09:35:02 +00:00
);
2020-01-02 14:09:49 +00:00
2020-09-04 11:10:09 +00:00
// Always do a _sync after login, even if backgroundSync is set to off
2020-01-02 14:33:26 +00:00
return _sync();
2020-01-02 14:09:49 +00:00
}
/// Used for testing only
void setUserId(String s) {
_userID = s;
}
2020-01-02 14:09:49 +00:00
/// Resets all settings and stops the synchronisation.
void clear() {
2020-05-15 18:40:17 +00:00
database?.clear(id);
2020-08-11 16:11:51 +00:00
_id = accessToken =
homeserver = _userID = _deviceID = _deviceName = prevBatch = null;
2020-05-04 06:19:15 +00:00
_rooms = [];
encryption?.dispose();
encryption = null;
2020-01-02 14:09:49 +00:00
onLoginStateChanged.add(LoginState.loggedOut);
}
2020-09-04 11:10:09 +00:00
bool _backgroundSync = true;
Future<void> _currentSync, _retryDelay = Future.value();
bool get syncPending => _currentSync != null;
2020-01-02 14:09:49 +00:00
2020-09-04 11:10:09 +00:00
/// Controls the background sync (automatically looping forever if turned on).
set backgroundSync(bool enabled) {
_backgroundSync = enabled;
if (_backgroundSync) {
_sync();
}
}
/// Immediately start a sync and wait for completion.
/// If there is an active sync already, wait for the active sync instead.
Future<void> oneShotSync() {
return _sync();
}
2020-01-02 14:09:49 +00:00
2020-09-04 11:10:09 +00:00
Future<void> _sync() {
if (_currentSync == null) {
_currentSync = _innerSync();
_currentSync.whenComplete(() {
_currentSync = null;
if (_backgroundSync && isLogged() && !_disposed) {
_sync();
}
});
}
return _currentSync;
}
Future<void> _innerSync() async {
await _retryDelay;
_retryDelay = Future.delayed(Duration(seconds: syncErrorTimeoutSec));
if (!isLogged() || _disposed) return null;
2020-01-02 14:09:49 +00:00
try {
2020-09-04 11:10:09 +00:00
final syncResp = await sync(
2020-06-03 10:16:01 +00:00
filter: syncFilters,
since: prevBatch,
timeout: prevBatch != null ? 30000 : null,
2020-09-04 11:10:09 +00:00
);
2020-05-18 14:01:14 +00:00
if (_disposed) return;
if (database != null) {
2020-08-21 15:20:26 +00:00
_currentTransaction = database.transaction(() async {
await handleSync(syncResp);
2020-06-03 10:16:01 +00:00
if (prevBatch != syncResp.nextBatch) {
await database.storePrevBatch(syncResp.nextBatch, id);
}
});
2020-08-21 15:20:26 +00:00
await _currentTransaction;
} else {
await handleSync(syncResp);
}
2020-05-18 14:01:14 +00:00
if (_disposed) return;
if (prevBatch == null) {
onFirstSync.add(true);
2020-06-03 10:16:01 +00:00
prevBatch = syncResp.nextBatch;
2020-01-06 20:21:25 +00:00
_sortRooms();
}
2020-06-03 10:16:01 +00:00
prevBatch = syncResp.nextBatch;
2020-10-06 09:48:34 +00:00
await database?.deleteOldFiles(
DateTime.now().subtract(Duration(days: 30)).millisecondsSinceEpoch);
2020-02-20 07:28:15 +00:00
await _updateUserDeviceKeys();
if (encryptionEnabled) {
encryption.onSync();
}
2020-09-04 11:10:09 +00:00
_retryDelay = Future.value();
2020-10-15 07:08:49 +00:00
} on MatrixException catch (e, s) {
onSyncError.add(SdkError(exception: e, stackTrace: s));
if (e.error == MatrixError.M_UNKNOWN_TOKEN) {
Logs.warning('The user has been logged out!');
clear();
}
2020-06-01 18:24:41 +00:00
} catch (e, s) {
2020-09-04 11:10:09 +00:00
if (!isLogged() || _disposed) return;
2020-08-06 09:35:02 +00:00
Logs.error('Error during processing events: ' + e.toString(), s);
2020-08-26 07:38:14 +00:00
onSyncError.add(SdkError(
2020-06-01 18:24:41 +00:00
exception: e is Exception ? e : Exception(e), stackTrace: s));
2020-01-02 14:09:49 +00:00
}
}
/// Use this method only for testing utilities!
2020-07-21 07:34:30 +00:00
Future<void> handleSync(SyncUpdate sync, {bool sortAtTheEnd = false}) async {
2020-06-03 10:16:01 +00:00
if (sync.toDevice != null) {
await _handleToDeviceEvents(sync.toDevice);
2020-06-03 10:16:01 +00:00
}
if (sync.rooms != null) {
if (sync.rooms.join != null) {
2020-07-21 07:34:30 +00:00
await _handleRooms(sync.rooms.join, Membership.join,
sortAtTheEnd: sortAtTheEnd);
2020-01-02 14:33:26 +00:00
}
2020-06-03 10:16:01 +00:00
if (sync.rooms.invite != null) {
2020-07-21 07:34:30 +00:00
await _handleRooms(sync.rooms.invite, Membership.invite,
sortAtTheEnd: sortAtTheEnd);
2020-01-02 14:33:26 +00:00
}
2020-06-03 10:16:01 +00:00
if (sync.rooms.leave != null) {
2020-07-21 07:34:30 +00:00
await _handleRooms(sync.rooms.leave, Membership.leave,
sortAtTheEnd: sortAtTheEnd);
2020-01-02 14:33:26 +00:00
}
2020-09-27 08:54:54 +00:00
_sortRooms();
2020-01-02 14:09:49 +00:00
}
2020-06-03 10:16:01 +00:00
if (sync.presence != null) {
for (final newPresence in sync.presence) {
presences[newPresence.senderId] = newPresence;
onPresence.add(newPresence);
}
2020-01-02 14:09:49 +00:00
}
2020-06-03 10:16:01 +00:00
if (sync.accountData != null) {
for (final newAccountData in sync.accountData) {
if (database != null) {
2020-06-25 07:16:59 +00:00
await database.storeAccountData(
2020-06-03 10:16:01 +00:00
id,
newAccountData.type,
jsonEncode(newAccountData.content),
2020-06-03 10:16:01 +00:00
);
}
accountData[newAccountData.type] = newAccountData;
if (onAccountData != null) onAccountData.add(newAccountData);
}
2020-01-02 14:09:49 +00:00
}
2020-06-03 10:16:01 +00:00
if (sync.deviceLists != null) {
await _handleDeviceListsEvents(sync.deviceLists);
2020-02-04 13:41:13 +00:00
}
if (sync.deviceOneTimeKeysCount != null && encryptionEnabled) {
encryption.handleDeviceOneTimeKeysCount(sync.deviceOneTimeKeysCount);
2020-04-02 08:39:00 +00:00
}
2020-01-02 14:09:49 +00:00
onSync.add(sync);
}
2020-06-03 10:16:01 +00:00
Future<void> _handleDeviceListsEvents(DeviceListsUpdate deviceLists) async {
if (deviceLists.changed is List) {
for (final userId in deviceLists.changed) {
2020-02-04 13:41:13 +00:00
if (_userDeviceKeys.containsKey(userId)) {
_userDeviceKeys[userId].outdated = true;
2020-05-15 18:40:17 +00:00
if (database != null) {
await database.storeUserDeviceKeysInfo(id, userId, true);
2020-05-15 18:40:17 +00:00
}
2020-02-04 13:41:13 +00:00
}
}
2020-06-03 10:16:01 +00:00
for (final userId in deviceLists.left) {
2020-02-04 13:41:13 +00:00
if (_userDeviceKeys.containsKey(userId)) {
_userDeviceKeys.remove(userId);
}
}
}
}
Future<void> _handleToDeviceEvents(List<BasicEventWithSender> events) async {
for (var i = 0; i < events.length; i++) {
2020-06-03 10:16:01 +00:00
var toDeviceEvent = ToDeviceEvent.fromJson(events[i].toJson());
if (toDeviceEvent.type == EventTypes.Encrypted && encryptionEnabled) {
try {
toDeviceEvent = await encryption.decryptToDeviceEvent(toDeviceEvent);
2020-05-22 13:51:45 +00:00
} catch (e, s) {
2020-08-06 09:35:02 +00:00
Logs.error(
'[LibOlm] Could not decrypt to device event from ${toDeviceEvent.sender} with content: ${toDeviceEvent.content}\n${e.toString()}',
s);
2020-05-22 13:51:45 +00:00
onOlmError.add(
ToDeviceEventDecryptionError(
2020-06-01 18:24:41 +00:00
exception: e is Exception ? e : Exception(e),
2020-05-22 13:51:45 +00:00
stackTrace: s,
toDeviceEvent: toDeviceEvent,
),
);
2020-06-03 10:16:01 +00:00
toDeviceEvent = ToDeviceEvent.fromJson(events[i].toJson());
}
}
if (encryptionEnabled) {
await encryption.handleToDeviceEvent(toDeviceEvent);
}
onToDeviceEvent.add(toDeviceEvent);
}
}
2020-05-22 10:12:18 +00:00
Future<void> _handleRooms(
2020-07-21 07:34:30 +00:00
Map<String, SyncRoomUpdate> rooms, Membership membership,
{bool sortAtTheEnd = false}) async {
for (final entry in rooms.entries) {
final id = entry.key;
final room = entry.value;
2020-01-02 14:09:49 +00:00
2020-06-03 10:16:01 +00:00
var update = RoomUpdate.fromSyncRoomUpdate(room, id);
2020-05-19 08:15:23 +00:00
if (database != null) {
// TODO: This method seems to be rather slow for some updates
// Perhaps don't dynamically build that one query?
2020-05-19 08:15:23 +00:00
await database.storeRoomUpdate(this.id, update, getRoomById(id));
}
2020-01-02 14:09:49 +00:00
_updateRoomsByRoomUpdate(update);
2020-05-15 18:40:17 +00:00
final roomObj = getRoomById(id);
2020-06-03 10:16:01 +00:00
if (update.limitedTimeline && roomObj != null) {
2020-05-15 18:40:17 +00:00
roomObj.resetSortOrder();
}
2020-01-02 14:09:49 +00:00
onRoomUpdate.add(update);
2020-05-15 18:40:17 +00:00
var handledEvents = false;
2020-05-16 06:42:56 +00:00
2020-01-02 14:09:49 +00:00
/// Handle now all room events and save them in the database
2020-06-03 10:16:01 +00:00
if (room is JoinedRoomUpdate) {
if (room.state?.isNotEmpty ?? false) {
// TODO: This method seems to be comperatively slow for some updates
2020-06-03 10:16:01 +00:00
await _handleRoomEvents(
2020-10-22 10:21:20 +00:00
id,
room.state.map((i) => i.toJson()).toList(),
EventUpdateType.state);
2020-06-03 10:16:01 +00:00
handledEvents = true;
}
if (room.timeline?.events?.isNotEmpty ?? false) {
2020-07-21 07:34:30 +00:00
await _handleRoomEvents(
id,
room.timeline.events.map((i) => i.toJson()).toList(),
2020-10-22 10:21:20 +00:00
sortAtTheEnd ? EventUpdateType.history : EventUpdateType.timeline,
2020-07-21 07:34:30 +00:00
sortAtTheEnd: sortAtTheEnd);
2020-06-03 10:16:01 +00:00
handledEvents = true;
}
if (room.ephemeral?.isNotEmpty ?? false) {
// TODO: This method seems to be comperatively slow for some updates
2020-06-03 10:16:01 +00:00
await _handleEphemerals(
id, room.ephemeral.map((i) => i.toJson()).toList());
}
if (room.accountData?.isNotEmpty ?? false) {
2020-10-22 10:21:20 +00:00
await _handleRoomEvents(
id,
room.accountData.map((i) => i.toJson()).toList(),
EventUpdateType.accountData);
2020-06-03 10:16:01 +00:00
}
2020-01-02 14:33:26 +00:00
}
2020-06-03 10:16:01 +00:00
if (room is LeftRoomUpdate) {
if (room.timeline?.events?.isNotEmpty ?? false) {
2020-10-22 10:21:20 +00:00
await _handleRoomEvents(
id,
room.timeline.events.map((i) => i.toJson()).toList(),
EventUpdateType.timeline);
2020-06-03 10:16:01 +00:00
handledEvents = true;
}
if (room.accountData?.isNotEmpty ?? false) {
2020-10-22 10:21:20 +00:00
await _handleRoomEvents(
id,
room.accountData.map((i) => i.toJson()).toList(),
EventUpdateType.accountData);
2020-06-03 10:16:01 +00:00
}
if (room.state?.isNotEmpty ?? false) {
await _handleRoomEvents(
2020-10-22 10:21:20 +00:00
id,
room.state.map((i) => i.toJson()).toList(),
EventUpdateType.state);
2020-06-03 10:16:01 +00:00
handledEvents = true;
}
2020-01-02 14:33:26 +00:00
}
2020-06-03 10:16:01 +00:00
if (room is InvitedRoomUpdate &&
(room.inviteState?.isNotEmpty ?? false)) {
2020-10-22 10:21:20 +00:00
await _handleRoomEvents(
id,
room.inviteState.map((i) => i.toJson()).toList(),
EventUpdateType.inviteState);
2020-05-15 18:40:17 +00:00
}
if (handledEvents && database != null && roomObj != null) {
await roomObj.updateSortOrder();
2020-01-02 14:33:26 +00:00
}
}
2020-01-02 14:09:49 +00:00
}
Future<void> _handleEphemerals(String id, List<dynamic> events) async {
2020-01-02 14:09:49 +00:00
for (num i = 0; i < events.length; i++) {
2020-10-22 10:21:20 +00:00
await _handleEvent(events[i], id, EventUpdateType.ephemeral);
2020-01-02 14:09:49 +00:00
// Receipt events are deltas between two states. We will create a
// fake room account data event for this and store the difference
// there.
if (events[i]['type'] == 'm.receipt') {
var room = getRoomById(id);
room ??= Room(id: id);
2020-01-02 14:09:49 +00:00
var receiptStateContent =
room.roomAccountData['m.receipt']?.content ?? {};
for (var eventEntry in events[i]['content'].entries) {
2020-01-02 14:09:49 +00:00
final String eventID = eventEntry.key;
if (events[i]['content'][eventID]['m.read'] != null) {
2020-01-02 14:09:49 +00:00
final Map<String, dynamic> userTimestampMap =
events[i]['content'][eventID]['m.read'];
2020-01-02 14:09:49 +00:00
for (var userTimestampMapEntry in userTimestampMap.entries) {
final mxid = userTimestampMapEntry.key;
2020-01-02 14:09:49 +00:00
// Remove previous receipt event from this user
2020-05-16 06:42:56 +00:00
if (receiptStateContent[eventID] is Map<String, dynamic> &&
receiptStateContent[eventID]['m.read']
is Map<String, dynamic> &&
receiptStateContent[eventID]['m.read'].containsKey(mxid)) {
2020-05-15 18:40:17 +00:00
receiptStateContent[eventID]['m.read'].remove(mxid);
2020-01-02 14:09:49 +00:00
}
if (userTimestampMap[mxid] is Map<String, dynamic> &&
userTimestampMap[mxid].containsKey('ts')) {
2020-01-02 14:09:49 +00:00
receiptStateContent[mxid] = {
'event_id': eventID,
'ts': userTimestampMap[mxid]['ts'],
2020-01-02 14:09:49 +00:00
};
}
}
}
}
events[i]['content'] = receiptStateContent;
2020-10-22 10:21:20 +00:00
await _handleEvent(events[i], id, EventUpdateType.accountData);
2020-01-02 14:09:49 +00:00
}
}
}
2020-05-22 10:12:18 +00:00
Future<void> _handleRoomEvents(
2020-10-22 10:21:20 +00:00
String chat_id, List<dynamic> events, EventUpdateType type,
2020-07-21 07:34:30 +00:00
{bool sortAtTheEnd = false}) async {
2020-01-02 14:09:49 +00:00
for (num i = 0; i < events.length; i++) {
2020-07-21 07:34:30 +00:00
await _handleEvent(events[i], chat_id, type, sortAtTheEnd: sortAtTheEnd);
2020-01-02 14:09:49 +00:00
}
}
2020-05-22 10:12:18 +00:00
Future<void> _handleEvent(
2020-10-22 10:21:20 +00:00
Map<String, dynamic> event, String roomID, EventUpdateType type,
2020-07-21 07:34:30 +00:00
{bool sortAtTheEnd = false}) async {
if (event['type'] is String && event['content'] is Map<String, dynamic>) {
2020-02-04 13:41:13 +00:00
// The client must ignore any new m.room.encryption event to prevent
// man-in-the-middle attacks!
2020-05-15 18:40:17 +00:00
final room = getRoomById(roomID);
2020-05-16 06:42:56 +00:00
if (room == null ||
2020-06-03 10:16:01 +00:00
(event['type'] == EventTypes.Encryption &&
2020-05-22 10:12:18 +00:00
room.encrypted &&
event['content']['algorithm'] !=
2020-06-03 10:16:01 +00:00
room.getState(EventTypes.Encryption)?.content['algorithm'])) {
2020-02-04 13:41:13 +00:00
return;
}
2020-05-15 18:40:17 +00:00
// ephemeral events aren't persisted and don't need a sort order - they are
// expected to be processed as soon as they come in
2020-10-22 10:21:20 +00:00
final sortOrder = type != EventUpdateType.ephemeral
2020-07-21 07:34:30 +00:00
? (sortAtTheEnd ? room.oldSortOrder : room.newSortOrder)
: 0.0;
var update = EventUpdate(
eventType: event['type'],
2020-01-02 14:09:49 +00:00
roomID: roomID,
type: type,
content: event,
2020-05-15 18:40:17 +00:00
sortOrder: sortOrder,
2020-01-02 14:09:49 +00:00
);
if (event['type'] == EventTypes.Encrypted && encryptionEnabled) {
update = await update.decrypt(room);
2020-05-19 07:58:59 +00:00
}
2020-07-02 08:32:11 +00:00
if (event['type'] == EventTypes.Message &&
!room.isDirectChat &&
database != null &&
room.getState(EventTypes.RoomMember, event['sender']) == null) {
// In order to correctly render room list previews we need to fetch the member from the database
final user = await database.getUser(id, event['sender'], room);
if (user != null) {
room.setState(user);
}
}
2020-10-22 10:21:20 +00:00
if (type != EventUpdateType.ephemeral && database != null) {
await database.storeEventUpdate(id, update);
}
_updateRoomsByEventUpdate(update);
2020-06-05 20:03:28 +00:00
if (encryptionEnabled) {
await encryption.handleEventUpdate(update);
}
2020-01-02 14:09:49 +00:00
onEvent.add(update);
2020-01-04 18:36:17 +00:00
2020-08-15 14:05:11 +00:00
final rawUnencryptedEvent = update.content;
2020-10-22 10:21:20 +00:00
if (prevBatch != null && type == EventUpdateType.timeline) {
2020-08-18 08:07:47 +00:00
if (rawUnencryptedEvent['type'] == EventTypes.CallInvite) {
onCallInvite
.add(Event.fromJson(rawUnencryptedEvent, room, sortOrder));
} else if (rawUnencryptedEvent['type'] == EventTypes.CallHangup) {
onCallHangup
.add(Event.fromJson(rawUnencryptedEvent, room, sortOrder));
} else if (rawUnencryptedEvent['type'] == EventTypes.CallAnswer) {
onCallAnswer
.add(Event.fromJson(rawUnencryptedEvent, room, sortOrder));
} else if (rawUnencryptedEvent['type'] == EventTypes.CallCandidates) {
onCallCandidates
.add(Event.fromJson(rawUnencryptedEvent, room, sortOrder));
}
2020-01-04 18:36:17 +00:00
}
2020-01-02 14:09:49 +00:00
}
}
void _updateRoomsByRoomUpdate(RoomUpdate chatUpdate) {
// Update the chat list item.
// Search the room in the rooms
num j = 0;
for (j = 0; j < rooms.length; j++) {
if (rooms[j].id == chatUpdate.id) break;
}
final found = (j < rooms.length && rooms[j].id == chatUpdate.id);
final isLeftRoom = chatUpdate.membership == Membership.leave;
2020-01-02 14:09:49 +00:00
// Does the chat already exist in the list rooms?
if (!found && !isLeftRoom) {
var position = chatUpdate.membership == Membership.invite ? 0 : j;
2020-01-02 14:09:49 +00:00
// Add the new chat to the list
var newRoom = Room(
2020-01-02 14:09:49 +00:00
id: chatUpdate.id,
membership: chatUpdate.membership,
prev_batch: chatUpdate.prev_batch,
highlightCount: chatUpdate.highlight_count,
notificationCount: chatUpdate.notification_count,
mHeroes: chatUpdate.summary?.mHeroes,
mJoinedMemberCount: chatUpdate.summary?.mJoinedMemberCount,
mInvitedMemberCount: chatUpdate.summary?.mInvitedMemberCount,
roomAccountData: {},
client: this,
);
rooms.insert(position, newRoom);
}
// If the membership is "leave" then remove the item and stop here
else if (found && isLeftRoom) {
rooms.removeAt(j);
}
// Update notification, highlight count and/or additional informations
else if (found &&
chatUpdate.membership != Membership.leave &&
(rooms[j].membership != chatUpdate.membership ||
rooms[j].notificationCount != chatUpdate.notification_count ||
rooms[j].highlightCount != chatUpdate.highlight_count ||
chatUpdate.summary != null)) {
rooms[j].membership = chatUpdate.membership;
rooms[j].notificationCount = chatUpdate.notification_count;
rooms[j].highlightCount = chatUpdate.highlight_count;
2020-01-02 14:33:26 +00:00
if (chatUpdate.prev_batch != null) {
2020-01-02 14:09:49 +00:00
rooms[j].prev_batch = chatUpdate.prev_batch;
2020-01-02 14:33:26 +00:00
}
2020-01-02 14:09:49 +00:00
if (chatUpdate.summary != null) {
2020-01-02 14:33:26 +00:00
if (chatUpdate.summary.mHeroes != null) {
2020-01-02 14:09:49 +00:00
rooms[j].mHeroes = chatUpdate.summary.mHeroes;
2020-01-02 14:33:26 +00:00
}
if (chatUpdate.summary.mJoinedMemberCount != null) {
2020-01-02 14:09:49 +00:00
rooms[j].mJoinedMemberCount = chatUpdate.summary.mJoinedMemberCount;
2020-01-02 14:33:26 +00:00
}
if (chatUpdate.summary.mInvitedMemberCount != null) {
2020-01-02 14:09:49 +00:00
rooms[j].mInvitedMemberCount = chatUpdate.summary.mInvitedMemberCount;
2020-01-02 14:33:26 +00:00
}
2020-01-02 14:09:49 +00:00
}
2020-01-04 10:29:38 +00:00
if (rooms[j].onUpdate != null) rooms[j].onUpdate.add(rooms[j].id);
2020-01-02 14:09:49 +00:00
}
}
void _updateRoomsByEventUpdate(EventUpdate eventUpdate) {
2020-10-22 10:21:20 +00:00
if (eventUpdate.type == EventUpdateType.history) return;
2020-09-16 08:18:13 +00:00
final room = getRoomById(eventUpdate.roomID);
if (room == null) return;
switch (eventUpdate.type) {
2020-10-22 10:21:20 +00:00
case EventUpdateType.timeline:
case EventUpdateType.state:
case EventUpdateType.inviteState:
2020-09-16 08:18:13 +00:00
var stateEvent =
Event.fromJson(eventUpdate.content, room, eventUpdate.sortOrder);
var prevState = room.getState(stateEvent.type, stateEvent.stateKey);
2020-09-04 07:48:35 +00:00
if (prevState != null && prevState.sortOrder > stateEvent.sortOrder) {
2020-09-16 08:18:13 +00:00
Logs.warning('''
2020-09-18 08:17:08 +00:00
A new ${eventUpdate.type} event of the type ${stateEvent.type} has arrived with a previews
sort order ${stateEvent.sortOrder} than the current ${stateEvent.type} event with a
2020-09-16 08:18:13 +00:00
sort order of ${prevState.sortOrder}. This should never happen...''');
2020-09-04 07:48:35 +00:00
return;
}
2020-09-16 08:18:13 +00:00
if (stateEvent.type == EventTypes.Redaction) {
final String redacts = eventUpdate.content['redacts'];
room.states.states.forEach(
(String key, Map<String, Event> states) => states.forEach(
(String key, Event state) {
if (state.eventId == redacts) {
state.setRedactionEvent(stateEvent);
}
},
),
);
} else {
room.setState(stateEvent);
}
break;
2020-10-22 10:21:20 +00:00
case EventUpdateType.accountData:
2020-09-16 08:18:13 +00:00
room.roomAccountData[eventUpdate.eventType] =
BasicRoomEvent.fromJson(eventUpdate.content);
break;
2020-10-22 10:21:20 +00:00
case EventUpdateType.ephemeral:
2020-09-16 08:18:13 +00:00
room.ephemerals[eventUpdate.eventType] =
BasicRoomEvent.fromJson(eventUpdate.content);
break;
2020-10-22 10:21:20 +00:00
case EventUpdateType.history:
break;
2020-01-02 14:09:49 +00:00
}
2020-09-16 08:18:13 +00:00
room.onUpdate.add(room.id);
2020-01-02 14:09:49 +00:00
}
2020-01-03 13:21:15 +00:00
bool _sortLock = false;
2020-01-02 14:09:49 +00:00
2020-07-20 07:46:46 +00:00
/// If [true] then unread rooms are pinned at the top of the room list.
bool pinUnreadRooms;
2020-01-03 13:21:15 +00:00
/// The compare function how the rooms should be sorted internally. By default
/// rooms are sorted by timestamp of the last m.room.message event or the last
/// event if there is no known message.
2020-07-20 07:46:46 +00:00
RoomSorter get sortRoomsBy => (a, b) => (a.isFavourite != b.isFavourite)
? (a.isFavourite ? -1 : 1)
: (pinUnreadRooms && a.notificationCount != b.notificationCount)
? b.notificationCount.compareTo(a.notificationCount)
2020-06-25 07:27:01 +00:00
: b.timeCreated.millisecondsSinceEpoch
.compareTo(a.timeCreated.millisecondsSinceEpoch);
2020-01-03 13:21:15 +00:00
void _sortRooms() {
2020-01-06 20:21:25 +00:00
if (prevBatch == null || _sortLock || rooms.length < 2) return;
2020-01-03 13:21:15 +00:00
_sortLock = true;
rooms?.sort(sortRoomsBy);
_sortLock = false;
2020-01-02 14:09:49 +00:00
}
2020-01-12 10:30:05 +00:00
2020-02-04 13:41:13 +00:00
/// A map of known device keys per user.
Map<String, DeviceKeysList> get userDeviceKeys => _userDeviceKeys;
Map<String, DeviceKeysList> _userDeviceKeys = {};
2020-08-17 12:25:48 +00:00
/// Gets user device keys by its curve25519 key. Returns null if it isn't found
DeviceKeys getUserDeviceKeysByCurve25519Key(String senderKey) {
for (final user in userDeviceKeys.values) {
final device = user.deviceKeys.values
.firstWhere((e) => e.curve25519Key == senderKey, orElse: () => null);
if (device != null) {
return device;
}
}
return null;
}
2020-02-04 13:41:13 +00:00
Future<Set<String>> _getUserIdsInEncryptedRooms() async {
var userIds = <String>{};
for (var i = 0; i < rooms.length; i++) {
2020-02-04 13:41:13 +00:00
if (rooms[i].encrypted) {
try {
var userList = await rooms[i].requestParticipants();
for (var user in userList) {
if ([Membership.join, Membership.invite]
.contains(user.membership)) {
userIds.add(user.id);
}
}
2020-08-06 09:35:02 +00:00
} catch (e, s) {
Logs.error('[E2EE] Failed to fetch participants: ' + e.toString(), s);
2020-02-04 13:41:13 +00:00
}
}
}
return userIds;
}
2020-09-18 08:17:08 +00:00
final Map<String, DateTime> _keyQueryFailures = {};
2020-02-04 13:41:13 +00:00
Future<void> _updateUserDeviceKeys() async {
2020-02-18 09:23:55 +00:00
try {
if (!isLogged()) return;
2020-05-15 18:40:17 +00:00
final dbActions = <Future<dynamic> Function()>[];
var trackedUserIds = await _getUserIdsInEncryptedRooms();
trackedUserIds.add(userID);
2020-02-18 09:23:55 +00:00
// Remove all userIds we no longer need to track the devices of.
_userDeviceKeys
.removeWhere((String userId, v) => !trackedUserIds.contains(userId));
// Check if there are outdated device key lists. Add it to the set.
var outdatedLists = <String, dynamic>{};
for (var userId in trackedUserIds) {
2020-02-18 09:23:55 +00:00
if (!userDeviceKeys.containsKey(userId)) {
2020-06-15 08:26:50 +00:00
_userDeviceKeys[userId] = DeviceKeysList(userId, this);
2020-02-18 09:23:55 +00:00
}
var deviceKeysList = userDeviceKeys[userId];
2020-09-18 08:17:08 +00:00
if (deviceKeysList.outdated &&
(!_keyQueryFailures.containsKey(userId.domain) ||
DateTime.now()
.subtract(Duration(minutes: 5))
.isAfter(_keyQueryFailures[userId.domain]))) {
2020-02-18 09:23:55 +00:00
outdatedLists[userId] = [];
}
2020-02-04 13:41:13 +00:00
}
2020-02-18 09:23:55 +00:00
if (outdatedLists.isNotEmpty) {
2020-02-19 09:24:54 +00:00
// Request the missing device key lists from the server.
2020-08-21 09:02:20 +00:00
if (!isLogged()) return;
2020-08-11 16:11:51 +00:00
final response = await requestDeviceKeys(outdatedLists, timeout: 10000);
2020-02-20 07:28:15 +00:00
2020-06-03 10:16:01 +00:00
for (final rawDeviceKeyListEntry in response.deviceKeys.entries) {
final userId = rawDeviceKeyListEntry.key;
2020-06-04 15:51:49 +00:00
if (!userDeviceKeys.containsKey(userId)) {
2020-06-15 08:26:50 +00:00
_userDeviceKeys[userId] = DeviceKeysList(userId, this);
2020-06-04 15:51:49 +00:00
}
final oldKeys =
2020-02-20 07:28:15 +00:00
Map<String, DeviceKeys>.from(_userDeviceKeys[userId].deviceKeys);
2020-02-18 09:23:55 +00:00
_userDeviceKeys[userId].deviceKeys = {};
for (final rawDeviceKeyEntry in rawDeviceKeyListEntry.value.entries) {
2020-06-03 10:16:01 +00:00
final deviceId = rawDeviceKeyEntry.key;
2020-02-19 09:24:54 +00:00
// Set the new device key for this device
2020-06-06 11:47:37 +00:00
final entry =
DeviceKeys.fromMatrixDeviceKeys(rawDeviceKeyEntry.value, this);
2020-06-05 20:03:28 +00:00
if (entry.isValid) {
// is this a new key or the same one as an old one?
// better store an update - the signatures might have changed!
if (!oldKeys.containsKey(deviceId) ||
oldKeys[deviceId].ed25519Key == entry.ed25519Key) {
if (oldKeys.containsKey(deviceId)) {
// be sure to save the verified status
entry.setDirectVerified(oldKeys[deviceId].directVerified);
entry.blocked = oldKeys[deviceId].blocked;
entry.validSignatures = oldKeys[deviceId].validSignatures;
}
2020-05-20 07:37:32 +00:00
_userDeviceKeys[userId].deviceKeys[deviceId] = entry;
if (deviceId == deviceID &&
2020-06-05 20:03:28 +00:00
entry.ed25519Key == fingerprintKey) {
2020-05-22 10:12:18 +00:00
// Always trust the own device
entry.setDirectVerified(true);
2020-05-20 07:37:32 +00:00
}
} else {
// This shouldn't ever happen. The same device ID has gotten
// a new public key. So we ignore the update. TODO: ask krille
// if we should instead use the new key with unknown verified / blocked status
2020-05-22 11:18:45 +00:00
_userDeviceKeys[userId].deviceKeys[deviceId] =
oldKeys[deviceId];
2020-05-20 07:37:32 +00:00
}
2020-06-05 20:03:28 +00:00
}
if (database != null) {
dbActions.add(() => database.storeUserDeviceKey(
id,
userId,
deviceId,
json.encode(entry.toJson()),
entry.directVerified,
entry.blocked,
));
2020-02-18 09:23:55 +00:00
}
2020-05-15 18:40:17 +00:00
}
// delete old/unused entries
2020-05-15 18:40:17 +00:00
if (database != null) {
for (final oldDeviceKeyEntry in oldKeys.entries) {
final deviceId = oldDeviceKeyEntry.key;
if (!_userDeviceKeys[userId].deviceKeys.containsKey(deviceId)) {
// we need to remove an old key
2020-05-16 06:42:56 +00:00
dbActions.add(
() => database.removeUserDeviceKey(id, userId, deviceId));
2020-05-15 18:40:17 +00:00
}
}
2020-02-15 12:33:03 +00:00
}
2020-02-18 09:23:55 +00:00
_userDeviceKeys[userId].outdated = false;
2020-05-15 18:40:17 +00:00
if (database != null) {
2020-05-16 06:42:56 +00:00
dbActions
.add(() => database.storeUserDeviceKeysInfo(id, userId, false));
2020-05-15 18:40:17 +00:00
}
2020-02-04 13:41:13 +00:00
}
// next we parse and persist the cross signing keys
2020-06-05 20:03:28 +00:00
final crossSigningTypes = {
'master': response.masterKeys,
'self_signing': response.selfSigningKeys,
'user_signing': response.userSigningKeys,
};
for (final crossSigningKeysEntry in crossSigningTypes.entries) {
final keyType = crossSigningKeysEntry.key;
final keys = crossSigningKeysEntry.value;
if (keys == null) {
continue;
}
2020-06-05 20:03:28 +00:00
for (final crossSigningKeyListEntry in keys.entries) {
final userId = crossSigningKeyListEntry.key;
if (!userDeviceKeys.containsKey(userId)) {
2020-06-15 08:26:50 +00:00
_userDeviceKeys[userId] = DeviceKeysList(userId, this);
2020-06-05 20:03:28 +00:00
}
2020-06-06 11:47:37 +00:00
final oldKeys = Map<String, CrossSigningKey>.from(
_userDeviceKeys[userId].crossSigningKeys);
_userDeviceKeys[userId].crossSigningKeys = {};
2020-06-05 20:03:28 +00:00
// add the types we aren't handling atm back
for (final oldEntry in oldKeys.entries) {
2020-06-05 20:03:28 +00:00
if (!oldEntry.value.usage.contains(keyType)) {
2020-05-22 11:18:45 +00:00
_userDeviceKeys[userId].crossSigningKeys[oldEntry.key] =
oldEntry.value;
}
}
2020-06-06 11:47:37 +00:00
final entry = CrossSigningKey.fromMatrixCrossSigningKey(
crossSigningKeyListEntry.value, this);
if (entry.isValid) {
final publicKey = entry.publicKey;
2020-05-22 11:18:45 +00:00
if (!oldKeys.containsKey(publicKey) ||
oldKeys[publicKey].ed25519Key == entry.ed25519Key) {
if (oldKeys.containsKey(publicKey)) {
// be sure to save the verification status
entry.setDirectVerified(oldKeys[publicKey].directVerified);
entry.blocked = oldKeys[publicKey].blocked;
entry.validSignatures = oldKeys[publicKey].validSignatures;
}
_userDeviceKeys[userId].crossSigningKeys[publicKey] = entry;
} else {
// This shouldn't ever happen. The same device ID has gotten
// a new public key. So we ignore the update. TODO: ask krille
// if we should instead use the new key with unknown verified / blocked status
2020-05-22 11:18:45 +00:00
_userDeviceKeys[userId].crossSigningKeys[publicKey] =
oldKeys[publicKey];
}
if (database != null) {
dbActions.add(() => database.storeUserCrossSigningKey(
2020-05-22 11:18:45 +00:00
id,
userId,
publicKey,
json.encode(entry.toJson()),
entry.directVerified,
entry.blocked,
));
}
}
_userDeviceKeys[userId].outdated = false;
if (database != null) {
2020-05-22 11:18:45 +00:00
dbActions.add(
() => database.storeUserDeviceKeysInfo(id, userId, false));
}
}
}
2020-09-18 08:17:08 +00:00
// now process all the failures
if (response.failures != null) {
for (final failureDomain in response.failures.keys) {
_keyQueryFailures[failureDomain] = DateTime.now();
}
2020-05-15 18:40:17 +00:00
}
2020-02-04 13:41:13 +00:00
}
if (dbActions.isNotEmpty) {
await database?.transaction(() async {
for (final f in dbActions) {
await f();
}
});
}
2020-08-06 09:35:02 +00:00
} catch (e, s) {
Logs.error(
'[LibOlm] Unable to update user device keys: ' + e.toString(), s);
2020-02-04 13:41:13 +00:00
}
}
2020-08-11 16:11:51 +00:00
/// Send an (unencrypted) to device [message] of a specific [eventType] to all
/// devices of a set of [users].
Future<void> sendToDevicesOfUserIds(
Set<String> users,
String eventType,
Map<String, dynamic> message, {
String messageId,
}) async {
// Send with send-to-device messaging
var data = <String, Map<String, Map<String, dynamic>>>{};
for (var user in users) {
data[user] = {};
data[user]['*'] = message;
2020-02-04 13:41:13 +00:00
}
2020-08-11 16:11:51 +00:00
await sendToDevice(
eventType, messageId ?? generateUniqueTransactionId(), data);
return;
2020-02-04 13:41:13 +00:00
}
2020-02-21 15:05:19 +00:00
/// Sends an encrypted [message] of this [type] to these [deviceKeys]. To send
/// the request to all devices of the current user, pass an empty list to [deviceKeys].
2020-08-11 16:11:51 +00:00
Future<void> sendToDeviceEncrypted(
List<DeviceKeys> deviceKeys,
2020-08-11 16:11:51 +00:00
String eventType,
Map<String, dynamic> message, {
2020-08-11 16:11:51 +00:00
String messageId,
2020-05-23 15:04:27 +00:00
bool onlyVerified = false,
}) async {
2020-08-11 16:11:51 +00:00
if (!encryptionEnabled) return;
2020-05-23 15:04:27 +00:00
// Don't send this message to blocked devices, and if specified onlyVerified
// then only send it to verified devices
2020-02-21 15:05:19 +00:00
if (deviceKeys.isNotEmpty) {
deviceKeys.removeWhere((DeviceKeys deviceKeys) =>
2020-05-23 15:04:27 +00:00
deviceKeys.blocked ||
deviceKeys.deviceId == deviceID ||
(onlyVerified && !deviceKeys.verified));
2020-02-21 15:05:19 +00:00
if (deviceKeys.isEmpty) return;
}
// Send with send-to-device messaging
2020-06-03 10:16:01 +00:00
var data = <String, Map<String, Map<String, dynamic>>>{};
2020-08-11 16:11:51 +00:00
data =
await encryption.encryptToDeviceMessage(deviceKeys, eventType, message);
eventType = EventTypes.Encrypted;
await sendToDevice(
eventType, messageId ?? generateUniqueTransactionId(), data);
}
2020-03-23 10:47:55 +00:00
/// Whether all push notifications are muted using the [.m.rule.master]
/// rule of the push rules: https://matrix.org/docs/spec/client_server/r0.6.0#m-rule-master
bool get allPushNotificationsMuted {
if (!accountData.containsKey('m.push_rules') ||
!(accountData['m.push_rules'].content['global'] is Map)) {
2020-03-23 10:47:55 +00:00
return false;
}
final Map<String, dynamic> globalPushRules =
accountData['m.push_rules'].content['global'];
2020-03-23 10:47:55 +00:00
if (globalPushRules == null) return false;
if (globalPushRules['override'] is List) {
for (var i = 0; i < globalPushRules['override'].length; i++) {
if (globalPushRules['override'][i]['rule_id'] == '.m.rule.master') {
return globalPushRules['override'][i]['enabled'];
2020-03-23 10:47:55 +00:00
}
}
}
return false;
}
Future<void> setMuteAllPushNotifications(bool muted) async {
2020-08-11 16:11:51 +00:00
await enablePushRule(
2020-06-03 10:16:01 +00:00
'global',
PushRuleKind.override,
'.m.rule.master',
muted,
2020-03-23 10:47:55 +00:00
);
return;
}
2020-04-28 14:23:01 +00:00
/// Changes the password. You should either set oldPasswort or another authentication flow.
2020-08-11 16:11:51 +00:00
@override
2020-04-28 14:23:01 +00:00
Future<void> changePassword(String newPassword,
{String oldPassword, Map<String, dynamic> auth}) async {
try {
2020-06-03 10:16:01 +00:00
if (oldPassword != null) {
auth = {
'type': 'm.login.password',
'user': userID,
'password': oldPassword,
};
}
2020-08-11 16:11:51 +00:00
await super.changePassword(newPassword, auth: auth);
2020-04-28 14:23:01 +00:00
} on MatrixException catch (matrixException) {
if (!matrixException.requireAdditionalAuthentication) {
rethrow;
}
if (matrixException.authenticationFlows.length != 1 ||
!matrixException.authenticationFlows.first.stages
.contains('m.login.password')) {
rethrow;
}
if (oldPassword == null) {
rethrow;
}
return changePassword(
newPassword,
auth: {
'type': 'm.login.password',
'user': userID,
'identifier': {'type': 'm.id.user', 'user': userID},
'password': oldPassword,
'session': matrixException.session,
},
);
} catch (_) {
rethrow;
}
}
2020-05-18 14:01:14 +00:00
2020-09-19 10:39:19 +00:00
/// Clear all local cached messages and perform a new clean sync.
Future<void> clearLocalCachedMessages() async {
prevBatch = null;
rooms.forEach((r) => r.prev_batch = null);
await database?.clearCache(id);
}
/// A list of mxids of users who are ignored.
2020-09-19 13:05:43 +00:00
List<String> get ignoredUsers => (accountData
.containsKey('m.ignored_user_list') &&
2020-09-20 08:35:25 +00:00
accountData['m.ignored_user_list'].content['ignored_users'] is Map)
2020-09-19 13:05:43 +00:00
? List<String>.from(
2020-09-20 08:35:25 +00:00
accountData['m.ignored_user_list'].content['ignored_users'].keys)
2020-09-19 13:05:43 +00:00
: [];
2020-09-19 10:39:19 +00:00
/// Ignore another user. This will clear the local cached messages to
/// hide all previous messages from this user.
Future<void> ignoreUser(String userId) async {
if (!userId.isValidMatrixId) {
throw Exception('$userId is not a valid mxid!');
}
2020-09-20 08:35:25 +00:00
await setAccountData(userID, 'm.ignored_user_list', {
'ignored_users': Map.fromEntries(
(ignoredUsers..add(userId)).map((key) => MapEntry(key, {}))),
});
2020-09-19 10:39:19 +00:00
await clearLocalCachedMessages();
return;
}
/// Unignore a user. This will clear the local cached messages and request
/// them again from the server to avoid gaps in the timeline.
Future<void> unignoreUser(String userId) async {
if (!userId.isValidMatrixId) {
throw Exception('$userId is not a valid mxid!');
}
if (!ignoredUsers.contains(userId)) {
throw Exception('$userId is not in the ignore list!');
}
2020-09-20 08:35:25 +00:00
await setAccountData(userID, 'm.ignored_user_list', {
'ignored_users': Map.fromEntries(
(ignoredUsers..remove(userId)).map((key) => MapEntry(key, {}))),
});
2020-09-19 10:39:19 +00:00
await clearLocalCachedMessages();
return;
}
2020-05-18 14:01:14 +00:00
bool _disposed = false;
2020-08-21 15:20:26 +00:00
Future _currentTransaction = Future.sync(() => {});
2020-05-18 14:01:14 +00:00
/// Stops the synchronization and closes the database. After this
/// you can safely make this Client instance null.
2020-05-19 08:05:17 +00:00
Future<void> dispose({bool closeDatabase = false}) async {
2020-05-18 14:01:14 +00:00
_disposed = true;
2020-08-21 15:20:26 +00:00
try {
await _currentTransaction;
} catch (_) {
// No-OP
}
2020-08-17 12:25:48 +00:00
encryption?.dispose();
encryption = null;
try {
if (closeDatabase) await database?.close();
} catch (error, stacktrace) {
Logs.warning('Failed to close database: ' + error.toString(), stacktrace);
}
2020-05-18 14:01:14 +00:00
database = null;
return;
}
2019-06-09 10:16:48 +00:00
}
2020-06-01 18:24:41 +00:00
2020-08-26 07:38:14 +00:00
class SdkError {
2020-06-01 18:24:41 +00:00
Exception exception;
StackTrace stackTrace;
2020-08-26 07:38:14 +00:00
SdkError({this.exception, this.stackTrace});
2020-06-01 18:24:41 +00:00
}