Merge pull request #1952 from famedly/reza/require-trailing-comma

Add require trailing comma lint
This commit is contained in:
Krille-chan 2024-11-11 11:47:22 +01:00 committed by GitHub
commit 52381cecfb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
135 changed files with 13145 additions and 8902 deletions

View File

@ -36,6 +36,7 @@ linter:
prefer_single_quotes: true
sort_child_properties_last: true
sort_pub_dependencies: true
require_trailing_commas: true
# Be nice to our users and allow them to configure what gets logged.
avoid_print: true

View File

@ -72,11 +72,12 @@ class CrossSigning {
null;
}
Future<void> selfSign(
{String? passphrase,
Future<void> selfSign({
String? passphrase,
String? recoveryKey,
String? keyOrPassphrase,
OpenSSSS? openSsss}) async {
OpenSSSS? openSsss,
}) async {
var handle = openSsss;
if (handle == null) {
handle = encryption.ssss.open(EventTypes.CrossSigningMasterKey);
@ -89,7 +90,8 @@ class CrossSigning {
await handle.maybeCacheAll();
}
final masterPrivateKey = base64decodeUnpadded(
await handle.getStored(EventTypes.CrossSigningMasterKey));
await handle.getStored(EventTypes.CrossSigningMasterKey),
);
final keyObj = olm.PkSigning();
String? masterPubkey;
try {
@ -117,11 +119,13 @@ class CrossSigning {
]);
}
bool signable(List<SignableKey> keys) => keys.any((key) =>
bool signable(List<SignableKey> keys) => keys.any(
(key) =>
key is CrossSigningKey && key.usage.contains('master') ||
key is DeviceKeys &&
key.userId == client.userID &&
key.identifier != client.deviceID);
key.identifier != client.deviceID,
);
Future<void> sign(List<SignableKey> keys) async {
final signedKeys = <MatrixSignableKey>[];
@ -133,7 +137,10 @@ class CrossSigning {
}
void addSignature(
SignableKey key, SignableKey signedWith, String signature) {
SignableKey key,
SignableKey signedWith,
String signature,
) {
final signedKey = key.cloneForSigning();
((signedKey.signatures ??=
<String, Map<String, String>>{})[signedWith.userId] ??=
@ -154,9 +161,11 @@ class CrossSigning {
// we don't care about signing other cross-signing keys
} else {
// okay, we'll sign a device key with our self signing key
selfSigningKey ??= base64decodeUnpadded(await encryption.ssss
selfSigningKey ??= base64decodeUnpadded(
await encryption.ssss
.getCached(EventTypes.CrossSigningSelfSigning) ??
'');
'',
);
if (selfSigningKey.isNotEmpty) {
final signature = _sign(key.signingContent, selfSigningKey);
addSignature(key, userKeys.selfSigningKey!, signature);
@ -164,9 +173,10 @@ class CrossSigning {
}
} else if (key is CrossSigningKey && key.usage.contains('master')) {
// we are signing someone elses master key
userSigningKey ??= base64decodeUnpadded(await encryption.ssss
.getCached(EventTypes.CrossSigningUserSigning) ??
'');
userSigningKey ??= base64decodeUnpadded(
await encryption.ssss.getCached(EventTypes.CrossSigningUserSigning) ??
'',
);
if (userSigningKey.isNotEmpty) {
final signature = _sign(key.signingContent, userSigningKey);
addSignature(key, userKeys.userSigningKey!, signature);

View File

@ -105,9 +105,15 @@ class Encryption {
);
void handleDeviceOneTimeKeysCount(
Map<String, int>? countJson, List<String>? unusedFallbackKeyTypes) {
runInRoot(() async => olmManager.handleDeviceOneTimeKeysCount(
countJson, unusedFallbackKeyTypes));
Map<String, int>? countJson,
List<String>? unusedFallbackKeyTypes,
) {
runInRoot(
() async => olmManager.handleDeviceOneTimeKeysCount(
countJson,
unusedFallbackKeyTypes,
),
);
}
void onSync() {
@ -175,7 +181,8 @@ class Encryption {
Logs().w(
'[LibOlm] Could not decrypt to device event from ${event.sender} with content: ${event.content}',
e,
s);
s,
);
client.onEncryptionError.add(
SdkError(
exception: e is Exception ? e : Exception(e),
@ -241,7 +248,10 @@ class Encryption {
client.database
// ignore: discarded_futures
?.updateInboundGroupSessionIndexes(
json.encode(inboundGroupSession.indexes), roomId, sessionId)
json.encode(inboundGroupSession.indexes),
roomId,
sessionId,
)
// ignore: discarded_futures
.onError((e, _) => Logs().e('Ignoring error for updating indexes'));
}
@ -255,8 +265,10 @@ class Encryption {
?.session_id() ??
'') ==
content.sessionId) {
runInRoot(() async =>
keyManager.clearOrUseOutboundGroupSession(roomId, wipe: true));
runInRoot(
() async =>
keyManager.clearOrUseOutboundGroupSession(roomId, wipe: true),
);
}
if (canRequestSession) {
decryptedPayload = {
@ -295,9 +307,12 @@ class Encryption {
);
}
Future<Event> decryptRoomEvent(String roomId, Event event,
{bool store = false,
EventUpdateType updateType = EventUpdateType.timeline}) async {
Future<Event> decryptRoomEvent(
String roomId,
Event event, {
bool store = false,
EventUpdateType updateType = EventUpdateType.timeline,
}) async {
if (event.type != EventTypes.Encrypted || event.redacted) {
return event;
}
@ -351,8 +366,10 @@ class Encryption {
/// Encrypts the given json payload and creates a send-ready m.room.encrypted
/// payload. This will create a new outgoingGroupSession if necessary.
Future<Map<String, dynamic>> encryptGroupMessagePayload(
String roomId, Map<String, dynamic> payload,
{String type = EventTypes.Message}) async {
String roomId,
Map<String, dynamic> payload, {
String type = EventTypes.Message,
}) async {
payload = copyMap(payload);
final Map<String, dynamic>? mRelatesTo = payload.remove('m.relates_to');
@ -405,7 +422,8 @@ class Encryption {
Future<Map<String, Map<String, Map<String, dynamic>>>> encryptToDeviceMessage(
List<DeviceKeys> deviceKeys,
String type,
Map<String, dynamic> payload) async {
Map<String, dynamic> payload,
) async {
return await olmManager.encryptToDeviceMessage(deviceKeys, type, payload);
}

View File

@ -252,19 +252,23 @@ class KeyManager {
// do e2ee recovery
_requestedSessionIds.add(requestIdent);
runInRoot(() async => request(
runInRoot(
() async => request(
room,
sessionId,
senderKey,
tryOnlineBackup: tryOnlineBackup,
onlineKeyBackupOnly: onlineKeyBackupOnly,
));
),
);
}
}
/// Loads an inbound group session
Future<SessionKey?> loadInboundGroupSession(
String roomId, String sessionId) async {
String roomId,
String sessionId,
) async {
final sess = _inboundGroupSessions[roomId]?[sessionId];
if (sess != null) {
if (sess.sessionId != sessionId && sess.sessionId.isNotEmpty) {
@ -290,7 +294,8 @@ class KeyManager {
}
Map<String, Map<String, bool>> _getDeviceKeyIdMap(
List<DeviceKeys> deviceKeys) {
List<DeviceKeys> deviceKeys,
) {
final deviceKeyIds = <String, Map<String, bool>>{};
for (final device in deviceKeys) {
final deviceId = device.deviceId;
@ -312,8 +317,11 @@ class KeyManager {
/// Clears the existing outboundGroupSession but first checks if the participating
/// devices have been changed. Returns false if the session has not been cleared because
/// it wasn't necessary. Otherwise returns true.
Future<bool> clearOrUseOutboundGroupSession(String roomId,
{bool wipe = false, bool use = true}) async {
Future<bool> clearOrUseOutboundGroupSession(
String roomId, {
bool wipe = false,
bool use = true,
}) async {
final room = client.getRoomById(roomId);
final sess = getOutboundGroupSession(roomId);
if (room == null || sess == null || sess.outboundGroupSession == null) {
@ -336,7 +344,9 @@ class KeyManager {
}
final inboundSess = await loadInboundGroupSession(
room.id, sess.outboundGroupSession!.session_id());
room.id,
sess.outboundGroupSession!.session_id(),
);
if (inboundSess == null) {
wipe = true;
}
@ -365,15 +375,19 @@ class KeyManager {
// we also know that all the old user IDs appear in the old one, else we have already wiped the session
for (final userId in oldUserIds) {
final oldBlockedDevices = sess.devices.containsKey(userId)
? Set.from(sess.devices[userId]!.entries
? Set.from(
sess.devices[userId]!.entries
.where((e) => e.value)
.map((e) => e.key))
.map((e) => e.key),
)
: <String>{};
final newBlockedDevices = newDeviceKeyIds.containsKey(userId)
? Set.from(newDeviceKeyIds[userId]!
? Set.from(
newDeviceKeyIds[userId]!
.entries
.where((e) => e.value)
.map((e) => e.key))
.map((e) => e.key),
)
: <String>{};
// we don't really care about old devices that got dropped (deleted), we only care if new ones got added and if new ones got blocked
// check if new devices got blocked
@ -383,15 +397,19 @@ class KeyManager {
}
// and now add all the new devices!
final oldDeviceIds = sess.devices.containsKey(userId)
? Set.from(sess.devices[userId]!.entries
? Set.from(
sess.devices[userId]!.entries
.where((e) => !e.value)
.map((e) => e.key))
.map((e) => e.key),
)
: <String>{};
final newDeviceIds = newDeviceKeyIds.containsKey(userId)
? Set.from(newDeviceKeyIds[userId]!
? Set.from(
newDeviceKeyIds[userId]!
.entries
.where((e) => !e.value)
.map((e) => e.key))
.map((e) => e.key),
)
: <String>{};
// check if a device got removed
@ -403,8 +421,11 @@ class KeyManager {
// check if any new devices need keys
final newDevices = newDeviceIds.difference(oldDeviceIds);
if (newDeviceIds.isNotEmpty) {
devicesToReceive.addAll(newDeviceKeys.where(
(d) => d.userId == userId && newDevices.contains(d.deviceId)));
devicesToReceive.addAll(
newDeviceKeys.where(
(d) => d.userId == userId && newDevices.contains(d.deviceId),
),
);
}
}
}
@ -440,16 +461,21 @@ class KeyManager {
await client.database?.updateInboundGroupSessionAllowedAtIndex(
json.encode(inboundSess!.allowedAtIndex),
room.id,
sess.outboundGroupSession!.session_id());
sess.outboundGroupSession!.session_id(),
);
// send out the key
await client.sendToDeviceEncryptedChunked(
devicesToReceive, EventTypes.RoomKey, rawSession);
devicesToReceive,
EventTypes.RoomKey,
rawSession,
);
}
} catch (e, s) {
Logs().e(
'[LibOlm] Unable to re-send the session key at later index to new devices',
e,
s);
s,
);
}
return false;
}
@ -462,14 +488,17 @@ class KeyManager {
/// Store an outbound group session in the database
Future<void> storeOutboundGroupSession(
String roomId, OutboundGroupSession sess) async {
String roomId,
OutboundGroupSession sess,
) async {
final userID = client.userID;
if (userID == null) return;
await client.database?.storeOutboundGroupSession(
roomId,
sess.outboundGroupSession!.pickle(userID),
json.encode(sess.devices),
sess.creationTime.millisecondsSinceEpoch);
sess.creationTime.millisecondsSinceEpoch,
);
}
final Map<String, Future<OutboundGroupSession>>
@ -507,18 +536,21 @@ class KeyManager {
}
Future<OutboundGroupSession> _createOutboundGroupSession(
String roomId) async {
String roomId,
) async {
await clearOrUseOutboundGroupSession(roomId, wipe: true);
await client.firstSyncReceived;
final room = client.getRoomById(roomId);
if (room == null) {
throw Exception(
'Tried to create a megolm session in a non-existing room ($roomId)!');
'Tried to create a megolm session in a non-existing room ($roomId)!',
);
}
final userID = client.userID;
if (userID == null) {
throw Exception(
'Tried to create a megolm session without being logged in!');
'Tried to create a megolm session without being logged in!',
);
}
final deviceKeys = await room.getUserDeviceKeys();
@ -549,8 +581,12 @@ class KeyManager {
outboundGroupSession.message_index();
}
await setInboundGroupSession(
roomId, rawSession['session_id'], encryption.identityKey!, rawSession,
allowedAtIndex: allowedAtIndex);
roomId,
rawSession['session_id'],
encryption.identityKey!,
rawSession,
allowedAtIndex: allowedAtIndex,
);
final sess = OutboundGroupSession(
devices: deviceKeyIds,
creationTime: DateTime.now(),
@ -559,14 +595,18 @@ class KeyManager {
);
try {
await client.sendToDeviceEncryptedChunked(
deviceKeys, EventTypes.RoomKey, rawSession);
deviceKeys,
EventTypes.RoomKey,
rawSession,
);
await storeOutboundGroupSession(roomId, sess);
_outboundGroupSessions[roomId] = sess;
} catch (e, s) {
Logs().e(
'[LibOlm] Unable to send the session key to the participating devices',
e,
s);
s,
);
sess.dispose();
rethrow;
}
@ -611,8 +651,9 @@ class KeyManager {
GetRoomKeysVersionCurrentResponse? _roomKeysVersionCache;
DateTime? _roomKeysVersionCacheDate;
Future<GetRoomKeysVersionCurrentResponse> getRoomKeysBackupInfo(
[bool useCache = true]) async {
Future<GetRoomKeysVersionCurrentResponse> getRoomKeysBackupInfo([
bool useCache = true,
]) async {
if (_roomKeysVersionCache != null &&
_roomKeysVersionCacheDate != null &&
useCache &&
@ -650,10 +691,13 @@ class KeyManager {
final sessionData = session.sessionData;
Map<String, Object?>? decrypted;
try {
decrypted = json.decode(decryption.decrypt(
decrypted = json.decode(
decryption.decrypt(
sessionData['ephemeral'] as String,
sessionData['mac'] as String,
sessionData['ciphertext'] as String));
sessionData['ciphertext'] as String,
),
);
} catch (e, s) {
Logs().e('[LibOlm] Error decrypting room key', e, s);
}
@ -662,12 +706,16 @@ class KeyManager {
decrypted['session_id'] = sessionId;
decrypted['room_id'] = roomId;
await setInboundGroupSession(
roomId, sessionId, senderKey, decrypted,
roomId,
sessionId,
senderKey,
decrypted,
forwarded: true,
senderClaimedKeys: decrypted
.tryGetMap<String, String>('sender_claimed_keys') ??
senderClaimedKeys:
decrypted.tryGetMap<String, String>('sender_claimed_keys') ??
<String, String>{},
uploaded: true);
uploaded: true,
);
}
}
}
@ -733,10 +781,14 @@ class KeyManager {
} catch (err, stacktrace) {
if (err is MatrixException && err.errcode == 'M_NOT_FOUND') {
Logs().i(
'[KeyManager] Key not in online key backup, requesting it from other devices...');
'[KeyManager] Key not in online key backup, requesting it from other devices...',
);
} else {
Logs().e('[KeyManager] Failed to access online key backup', err,
stacktrace);
Logs().e(
'[KeyManager] Failed to access online key backup',
err,
stacktrace,
);
}
}
// TODO: also don't request from others if we have an index of 0 now
@ -785,15 +837,17 @@ class KeyManager {
void startAutoUploadKeys() {
_uploadKeysOnSync = encryption.client.onSync.stream.listen(
(_) async => uploadInboundGroupSessions(skipIfInProgress: true));
(_) async => uploadInboundGroupSessions(skipIfInProgress: true),
);
}
/// This task should be performed after sync processing but should not block
/// the sync. To make sure that it never gets executed multiple times, it is
/// skipped when an upload task is already in progress. Set `skipIfInProgress`
/// to `false` to await the pending upload task instead.
Future<void> uploadInboundGroupSessions(
{bool skipIfInProgress = false}) async {
Future<void> uploadInboundGroupSessions({
bool skipIfInProgress = false,
}) async {
final database = client.database;
final userID = client.userID;
if (database == null || userID == null) {
@ -849,10 +903,12 @@ class KeyManager {
for (final dbSession in dbSessions) {
final device =
client.getUserDeviceKeysByCurve25519Key(dbSession.senderKey);
args.dbSessions.add(DbInboundGroupSessionBundle(
args.dbSessions.add(
DbInboundGroupSessionBundle(
dbSession: dbSession,
verified: device?.verified ?? false,
));
),
);
i++;
if (i > 10) {
await Future.delayed(Duration(milliseconds: 1));
@ -868,7 +924,9 @@ class KeyManager {
// no need to optimze this, as we only run it so seldomly and almost never with many keys at once
for (final dbSession in dbSessions) {
await database.markInboundGroupSessionAsUploaded(
dbSession.roomId, dbSession.sessionId);
dbSession.roomId,
dbSession.sessionId,
);
}
} finally {
decryption.free();
@ -895,7 +953,8 @@ class KeyManager {
if (event.content['action'] == 'request') {
// we are *receiving* a request
Logs().i(
'[KeyManager] Received key sharing request from ${event.sender}:${event.content['requesting_device_id']}...');
'[KeyManager] Received key sharing request from ${event.sender}:${event.content['requesting_device_id']}...',
);
if (!event.content.containsKey('body')) {
Logs().w('[KeyManager] No body, doing nothing');
return; // no body
@ -908,7 +967,8 @@ class KeyManager {
final roomId = body.tryGet<String>('room_id');
if (roomId == null) {
Logs().w(
'[KeyManager] Wrong type for room_id or no room_id, doing nothing');
'[KeyManager] Wrong type for room_id or no room_id, doing nothing',
);
return; // wrong type for roomId or no roomId found
}
final device = client.userDeviceKeys[event.sender]
@ -930,7 +990,8 @@ class KeyManager {
final sessionId = body.tryGet<String>('session_id');
if (sessionId == null) {
Logs().w(
'[KeyManager] Wrong type for session_id or no session_id, doing nothing');
'[KeyManager] Wrong type for session_id or no session_id, doing nothing',
);
return; // wrong type for session_id
}
// okay, let's see if we have this session at all
@ -941,7 +1002,8 @@ class KeyManager {
}
if (event.content['request_id'] is! String) {
Logs().w(
'[KeyManager] Wrong type for request_id or no request_id, doing nothing');
'[KeyManager] Wrong type for request_id or no request_id, doing nothing',
);
return; // wrong type for request_id
}
final request = KeyManagerKeyShareRequest(
@ -974,7 +1036,8 @@ class KeyManager {
final index =
session.allowedAtIndex[device.userId]![device.curve25519Key]!;
Logs().i(
'[KeyManager] Valid foreign request, forwarding key at index $index...');
'[KeyManager] Valid foreign request, forwarding key at index $index...',
);
await roomKeyRequest.forwardKey(index);
} else {
Logs()
@ -1002,15 +1065,19 @@ class KeyManager {
);
return;
}
final request = outgoingShareRequests.values.firstWhereOrNull((r) =>
final request = outgoingShareRequests.values.firstWhereOrNull(
(r) =>
r.room.id == event.content['room_id'] &&
r.sessionId == event.content['session_id']);
r.sessionId == event.content['session_id'],
);
if (request == null || request.canceled) {
return; // no associated request found or it got canceled
}
final device = request.devices.firstWhereOrNull((d) =>
final device = request.devices.firstWhereOrNull(
(d) =>
d.userId == event.sender &&
d.curve25519Key == encryptedContent['sender_key']);
d.curve25519Key == encryptedContent['sender_key'],
);
if (device == null) {
return; // someone we didn't send our request to replied....better ignore this
}
@ -1026,14 +1093,19 @@ class KeyManager {
}
// TODO: verify that the keys work to decrypt a message
// alright, all checks out, let's go ahead and store this session
await setInboundGroupSession(request.room.id, request.sessionId,
device.curve25519Key!, event.content,
await setInboundGroupSession(
request.room.id,
request.sessionId,
device.curve25519Key!,
event.content,
forwarded: true,
senderClaimedKeys: {
'ed25519': event.content['sender_claimed_ed25519_key'] as String,
});
},
);
request.devices.removeWhere(
(k) => k.userId == device.userId && k.deviceId == device.deviceId);
(k) => k.userId == device.userId && k.deviceId == device.deviceId,
);
outgoingShareRequests.remove(request.requestId);
// send cancel to all other devices
if (request.devices.isEmpty) {
@ -1057,7 +1129,8 @@ class KeyManager {
);
} else if (event.type == EventTypes.RoomKey) {
Logs().v(
'[KeyManager] Received room key with session ${event.content['session_id']}');
'[KeyManager] Received room key with session ${event.content['session_id']}',
);
final encryptedContent = event.encryptedContent;
if (encryptedContent == null) {
Logs().v('[KeyManager] not encrypted, ignoring...');
@ -1067,7 +1140,8 @@ class KeyManager {
final sessionId = event.content.tryGet<String>('session_id');
if (roomId == null || sessionId == null) {
Logs().w(
'Either room_id or session_id are not the expected type or missing');
'Either room_id or session_id are not the expected type or missing',
);
return;
}
final sender_ed25519 = client.userDeviceKeys[event.sender]
@ -1077,8 +1151,12 @@ class KeyManager {
}
Logs().v('[KeyManager] Keeping room key');
await setInboundGroupSession(
roomId, sessionId, encryptedContent['sender_key'], event.content,
forwarded: false);
roomId,
sessionId,
encryptedContent['sender_key'],
event.content,
forwarded: false,
);
}
}
@ -1105,13 +1183,13 @@ class KeyManagerKeyShareRequest {
final String sessionId;
bool canceled;
KeyManagerKeyShareRequest(
{required this.requestId,
KeyManagerKeyShareRequest({
required this.requestId,
List<DeviceKeys>? devices,
required this.room,
required this.sessionId,
this.canceled = false})
: devices = devices ?? [];
this.canceled = false,
}) : devices = devices ?? [];
}
class RoomKeyRequest extends ToDeviceEvent {
@ -1119,11 +1197,14 @@ class RoomKeyRequest extends ToDeviceEvent {
KeyManagerKeyShareRequest request;
RoomKeyRequest.fromToDeviceEvent(
ToDeviceEvent toDeviceEvent, this.keyManager, this.request)
: super(
ToDeviceEvent toDeviceEvent,
this.keyManager,
this.request,
) : super(
sender: toDeviceEvent.sender,
content: toDeviceEvent.content,
type: toDeviceEvent.type);
type: toDeviceEvent.type,
);
Room get room => request.room;
@ -1155,7 +1236,8 @@ class RoomKeyRequest extends ToDeviceEvent {
? keyManager.encryption.fingerprintKey
: null);
message['session_key'] = session.inboundGroupSession!.export_session(
index ?? session.inboundGroupSession!.first_known_index());
index ?? session.inboundGroupSession!.first_known_index(),
);
// send the actual reply of the key back to the requester
await keyManager.client.sendToDeviceEncrypted(
[requestingDevice],
@ -1217,8 +1299,10 @@ RoomKeys generateUploadKeysImplementation(GenerateUploadKeysArgs args) {
}
class DbInboundGroupSessionBundle {
DbInboundGroupSessionBundle(
{required this.dbSession, required this.verified});
DbInboundGroupSessionBundle({
required this.dbSession,
required this.verified,
});
factory DbInboundGroupSessionBundle.fromJson(Map<dynamic, dynamic> json) =>
DbInboundGroupSessionBundle(
@ -1236,8 +1320,11 @@ class DbInboundGroupSessionBundle {
}
class GenerateUploadKeysArgs {
GenerateUploadKeysArgs(
{required this.pubkey, required this.dbSessions, required this.userId});
GenerateUploadKeysArgs({
required this.pubkey,
required this.dbSessions,
required this.userId,
});
factory GenerateUploadKeysArgs.fromJson(Map<dynamic, dynamic> json) =>
GenerateUploadKeysArgs(

View File

@ -126,9 +126,15 @@ class KeyVerificationManager {
final room = client.getRoomById(update.roomID) ??
Room(id: update.roomID, client: client);
final newKeyRequest = KeyVerification(
encryption: encryption, userId: event['sender'], room: room);
encryption: encryption,
userId: event['sender'],
room: room,
);
await newKeyRequest.handlePayload(
type, event['content'], event['event_id']);
type,
event['content'],
event['event_id'],
);
if (newKeyRequest.state != KeyVerificationState.askAccept) {
// something went wrong, let's just dispose the request
newKeyRequest.dispose();

View File

@ -193,7 +193,7 @@ class OlmManager {
'device_id': ourDeviceId,
'algorithms': [
AlgorithmTypes.olmV1Curve25519AesSha2,
AlgorithmTypes.megolmV1AesSha2
AlgorithmTypes.megolmV1AesSha2,
],
'keys': <String, dynamic>{},
};
@ -247,7 +247,8 @@ class OlmManager {
if (dehydratedDeviceAlgorithm == null ||
dehydratedDevicePickleKey == null) {
throw Exception(
'You need to provide both the pickle key and the algorithm to use dehydrated devices!');
'You need to provide both the pickle key and the algorithm to use dehydrated devices!',
);
}
await client.uploadDehydratedDevice(
@ -265,13 +266,14 @@ class OlmManager {
);
return true;
}
final currentUpload =
this.currentUpload = CancelableOperation.fromFuture(client.uploadKeys(
final currentUpload = this.currentUpload = CancelableOperation.fromFuture(
client.uploadKeys(
deviceKeys:
uploadDeviceKeys ? MatrixDeviceKeys.fromJson(deviceKeys) : null,
oneTimeKeys: signedOneTimeKeys,
fallbackKeys: signedFallbackKeys,
));
),
);
final response = await currentUpload.valueOrCancellation();
if (response == null) {
_uploadKeysLock = false;
@ -318,7 +320,8 @@ class OlmManager {
oldKeyCount: oldKeyCount,
updateDatabase: updateDatabase,
unusedFallbackKey: unusedFallbackKey,
retry: retry - 1);
retry: retry - 1,
);
}
} finally {
_uploadKeysLock = false;
@ -330,13 +333,15 @@ class OlmManager {
final _otkUpdateDedup = AsyncCache<void>.ephemeral();
Future<void> handleDeviceOneTimeKeysCount(
Map<String, int>? countJson, List<String>? unusedFallbackKeyTypes) async {
Map<String, int>? countJson,
List<String>? unusedFallbackKeyTypes,
) async {
if (!enabled) {
return;
}
await _otkUpdateDedup.fetch(() =>
runBenchmarked('handleOtkUpdate', () async {
await _otkUpdateDedup.fetch(
() => runBenchmarked('handleOtkUpdate', () async {
final haveFallbackKeys = encryption.isMinOlmVersion(3, 2, 0);
// Check if there are at least half of max_number_of_one_time_keys left on the server
// and generate and upload more if not.
@ -354,7 +359,7 @@ class OlmManager {
// fixup accidental too many uploads. We delete only one of them so that the server has time to update the counts and because we will get rate limited anyway.
if (keyCount > _olmAccount!.max_number_of_one_time_keys()) {
final requestingKeysFrom = {
client.userID!: {ourDeviceId!: 'signed_curve25519'}
client.userID!: {ourDeviceId!: 'signed_curve25519'},
};
await client.claimKeys(requestingKeysFrom, timeout: 10000);
}
@ -370,7 +375,8 @@ class OlmManager {
unusedFallbackKey: haveFallbackKeys ? unusedFallbackKey : null,
);
}
}));
}),
);
}
Future<void> storeOlmSession(OlmSession session) async {
@ -393,7 +399,8 @@ class OlmManager {
session.sessionId!,
session.pickledSession!,
session.lastReceived?.millisecondsSinceEpoch ??
DateTime.now().millisecondsSinceEpoch);
DateTime.now().millisecondsSinceEpoch,
);
}
Future<ToDeviceEvent> _decryptToDeviceEvent(ToDeviceEvent event) async {
@ -429,7 +436,8 @@ class OlmManager {
await encryption.olmDatabase?.setLastActiveUserDeviceKey(
device.lastActive.millisecondsSinceEpoch,
device.userId,
device.deviceId!);
device.deviceId!,
);
}
} catch (e, s) {
Logs().e('Error while updating olm session timestamp', e, s);
@ -447,7 +455,9 @@ class OlmManager {
} catch (e) {
// The message was encrypted during this session, but is unable to decrypt
throw DecryptException(
DecryptException.decryptionFailed, e.toString());
DecryptException.decryptionFailed,
e.toString(),
);
}
await updateSessionUsage(session);
break;
@ -475,13 +485,15 @@ class OlmManager {
plaintext = newSession.decrypt(type, body);
await storeOlmSession(OlmSession(
await storeOlmSession(
OlmSession(
key: client.userID!,
identityKey: senderKey,
sessionId: newSession.session_id(),
session: newSession,
lastReceived: DateTime.now(),
));
),
);
await updateSessionUsage();
} catch (e) {
newSession.free();
@ -515,7 +527,8 @@ class OlmManager {
}
Future<void> getOlmSessionsForDevicesFromDatabase(
List<String> senderKeys) async {
List<String> senderKeys,
) async {
final rows = await encryption.olmDatabase?.getOlmSessionsForDevices(
senderKeys,
client.userID!,
@ -532,8 +545,10 @@ class OlmManager {
}
}
Future<List<OlmSession>> getOlmSessions(String senderKey,
{bool getFromDb = true}) async {
Future<List<OlmSession>> getOlmSessions(
String senderKey, {
bool getFromDb = true,
}) async {
var sess = olmSessions[senderKey];
if ((getFromDb) && (sess == null || sess.isEmpty)) {
final sessions = await getOlmSessionsFromDatabase(senderKey);
@ -545,10 +560,12 @@ class OlmManager {
if (sess == null) {
return [];
}
sess.sort((a, b) => a.lastReceived == b.lastReceived
sess.sort(
(a, b) => a.lastReceived == b.lastReceived
? (a.sessionId ?? '').compareTo(b.sessionId ?? '')
: (b.lastReceived ?? DateTime(0))
.compareTo(a.lastReceived ?? DateTime(0)));
.compareTo(a.lastReceived ?? DateTime(0)),
);
return sess;
}
@ -570,7 +587,8 @@ class OlmManager {
.subtract(Duration(hours: 1))
.isBefore(_restoredOlmSessionsTime[mapKey]!)) {
Logs().w(
'[OlmManager] Skipping restore session, one was restored in the past hour');
'[OlmManager] Skipping restore session, one was restored in the past hour',
);
return;
}
_restoredOlmSessionsTime[mapKey] = DateTime.now();
@ -608,7 +626,8 @@ class OlmManager {
Future<void> startOutgoingOlmSessions(List<DeviceKeys> deviceKeys) async {
Logs().v(
'[OlmManager] Starting session with ${deviceKeys.length} devices...');
'[OlmManager] Starting session with ${deviceKeys.length} devices...',
);
final requestingKeysFrom = <String, Map<String, String>>{};
for (final device in deviceKeys) {
if (requestingKeysFrom[device.userId] == null) {
@ -644,15 +663,20 @@ class OlmManager {
final session = olm.Session();
try {
session.create_outbound(
_olmAccount!, identityKey, deviceKey.tryGet<String>('key')!);
await storeOlmSession(OlmSession(
_olmAccount!,
identityKey,
deviceKey.tryGet<String>('key')!,
);
await storeOlmSession(
OlmSession(
key: client.userID!,
identityKey: identityKey,
sessionId: session.session_id(),
session: session,
lastReceived:
DateTime.now(), // we want to use a newly created session
));
),
);
} catch (e, s) {
session.free();
Logs()
@ -668,8 +692,11 @@ class OlmManager {
/// Throws `NoOlmSessionFoundException` if there is no olm session with this
/// device and none could be created.
Future<Map<String, dynamic>> encryptToDeviceMessagePayload(
DeviceKeys device, String type, Map<String, dynamic> payload,
{bool getFromDb = true}) async {
DeviceKeys device,
String type,
Map<String, dynamic> payload, {
bool getFromDb = true,
}) async {
final sess =
await getOlmSessions(device.curve25519Key!, getFromDb: getFromDb);
if (sess.isEmpty) {
@ -693,7 +720,8 @@ class OlmManager {
'content': payload,
}),
device.userId,
device.deviceId!);
device.deviceId!,
);
} catch (e, s) {
// we can ignore this error, since it would just make us use a different olm session possibly
Logs().w('Error while updating olm usage timestamp', e, s);
@ -714,16 +742,20 @@ class OlmManager {
Future<Map<String, Map<String, Map<String, dynamic>>>> encryptToDeviceMessage(
List<DeviceKeys> deviceKeys,
String type,
Map<String, dynamic> payload) async {
Map<String, dynamic> payload,
) async {
final data = <String, Map<String, Map<String, dynamic>>>{};
// first check if any of our sessions we want to encrypt for are in the database
if (encryption.olmDatabase != null) {
await getOlmSessionsForDevicesFromDatabase(
deviceKeys.map((d) => d.curve25519Key!).toList());
deviceKeys.map((d) => d.curve25519Key!).toList(),
);
}
final deviceKeysWithoutSession = List<DeviceKeys>.from(deviceKeys);
deviceKeysWithoutSession.removeWhere((DeviceKeys deviceKeys) =>
olmSessions[deviceKeys.curve25519Key]?.isNotEmpty ?? false);
deviceKeysWithoutSession.removeWhere(
(DeviceKeys deviceKeys) =>
olmSessions[deviceKeys.curve25519Key]?.isNotEmpty ?? false,
);
if (deviceKeysWithoutSession.isNotEmpty) {
await startOutgoingOlmSessions(deviceKeysWithoutSession);
}
@ -731,8 +763,11 @@ class OlmManager {
final userData = data[device.userId] ??= {};
try {
userData[device.deviceId!] = await encryptToDeviceMessagePayload(
device, type, payload,
getFromDb: false);
device,
type,
payload,
getFromDb: false,
);
} on NoOlmSessionFoundException catch (e) {
Logs().d('[LibOlm] Error encrypting to-device event', e);
continue;
@ -753,12 +788,14 @@ class OlmManager {
return;
}
final device = client.getUserDeviceKeysByCurve25519Key(
encryptedContent.tryGet<String>('sender_key') ?? '');
encryptedContent.tryGet<String>('sender_key') ?? '',
);
if (device == null) {
return; // device not found
}
Logs().v(
'[OlmManager] Device ${device.userId}:${device.deviceId} generated a new olm session, replaying last sent message...');
'[OlmManager] Device ${device.userId}:${device.deviceId} generated a new olm session, replaying last sent message...',
);
final lastSentMessageRes = await encryption.olmDatabase
?.getLastSentMessageUserDeviceKey(device.userId, device.deviceId!);
if (lastSentMessageRes == null ||
@ -773,7 +810,10 @@ class OlmManager {
if (lastSentMessage['type'] != EventTypes.Dummy) {
// okay, time to send the message!
await client.sendToDeviceEncrypted(
[device], lastSentMessage['type'], lastSentMessage['content']);
[device],
lastSentMessage['type'],
lastSentMessage['content'],
);
}
}
}

View File

@ -83,12 +83,16 @@ class SSSS {
Hmac(sha256, prk.bytes).convert(aesKey.bytes + utf8.encode(name) + b);
return DerivedKeys(
aesKey: Uint8List.fromList(aesKey.bytes),
hmacKey: Uint8List.fromList(hmacKey.bytes));
hmacKey: Uint8List.fromList(hmacKey.bytes),
);
}
static Future<EncryptedContent> encryptAes(
String data, Uint8List key, String name,
[String? ivStr]) async {
String data,
Uint8List key,
String name, [
String? ivStr,
]) async {
Uint8List iv;
if (ivStr != null) {
iv = base64decodeUnpadded(ivStr);
@ -108,11 +112,15 @@ class SSSS {
return EncryptedContent(
iv: base64.encode(iv),
ciphertext: base64.encode(ciphertext),
mac: base64.encode(hmac.bytes));
mac: base64.encode(hmac.bytes),
);
}
static Future<String> decryptAes(
EncryptedContent data, Uint8List key, String name) async {
EncryptedContent data,
Uint8List key,
String name,
) async {
final keys = deriveKeys(key, name);
final cipher = base64decodeUnpadded(data.ciphertext);
final hmac = base64
@ -144,8 +152,12 @@ class SSSS {
throw InvalidPassphraseException('Incorrect length');
}
return Uint8List.fromList(result.sublist(olmRecoveryKeyPrefix.length,
olmRecoveryKeyPrefix.length + ssssKeyLength));
return Uint8List.fromList(
result.sublist(
olmRecoveryKeyPrefix.length,
olmRecoveryKeyPrefix.length + ssssKeyLength,
),
);
}
static String encodeRecoveryKey(Uint8List recoveryKey) {
@ -160,7 +172,9 @@ class SSSS {
}
static Future<Uint8List> keyFromPassphrase(
String passphrase, PassphraseInfo info) async {
String passphrase,
PassphraseInfo info,
) async {
if (info.algorithm != AlgorithmTypes.pbkdf2) {
throw InvalidPassphraseException('Unknown algorithm');
}
@ -175,7 +189,8 @@ class SSSS {
Uint8List.fromList(utf8.encode(info.salt!)),
uc.sha512,
info.iterations!,
info.bits ?? 256);
info.bits ?? 256,
);
}
void setValidator(String type, FutureOr<bool> Function(String) validator) {
@ -252,7 +267,10 @@ class SSSS {
// noooow we set the account data
await client.setAccountData(
client.userID!, accountDataTypeKeyId, content.toJson());
client.userID!,
accountDataTypeKeyId,
content.toJson(),
);
while (!client.accountData.containsKey(accountDataTypeKeyId)) {
Logs().v('Waiting accountData to have $accountDataTypeKeyId');
@ -354,8 +372,13 @@ class SSSS {
return decrypted;
}
Future<void> store(String type, String secret, String keyId, Uint8List key,
{bool add = false}) async {
Future<void> store(
String type,
String secret,
String keyId,
Uint8List key, {
bool add = false,
}) async {
final encrypted = await encryptAes(secret, key, type);
Map<String, dynamic>? content;
if (add && client.accountData[type] != null) {
@ -386,7 +409,11 @@ class SSSS {
}
Future<void> validateAndStripOtherKeys(
String type, String secret, String keyId, Uint8List key) async {
String type,
String secret,
String keyId,
Uint8List key,
) async {
if (await getStored(type, keyId, key) != secret) {
throw Exception('Secrets do not match up!');
}
@ -457,11 +484,13 @@ class SSSS {
devices =
client.userDeviceKeys[client.userID]!.deviceKeys.values.toList();
}
devices.removeWhere((DeviceKeys d) =>
devices.removeWhere(
(DeviceKeys d) =>
d.userId != client.userID ||
!d.verified ||
d.blocked ||
d.deviceId == client.deviceID);
d.deviceId == client.deviceID,
);
if (devices.isEmpty) {
Logs().w('[SSSS] No devices');
return;
@ -555,9 +584,11 @@ class SSSS {
}
final request = pendingShareRequests[event.content['request_id']]!;
// alright, as we received a known request id, let's check if the sender is valid
final device = request.devices.firstWhereOrNull((d) =>
final device = request.devices.firstWhereOrNull(
(d) =>
d.userId == event.sender &&
d.curve25519Key == encryptedContent['sender_key']);
d.curve25519Key == encryptedContent['sender_key'],
);
if (device == null) {
Logs().i('[SSSS] Someone else replied?');
return; // someone replied whom we didn't send the share request to
@ -645,9 +676,11 @@ class _ShareRequest {
final List<DeviceKeys> devices;
final DateTime start;
_ShareRequest(
{required this.requestId, required this.type, required this.devices})
: start = DateTime.now();
_ShareRequest({
required this.requestId,
required this.type,
required this.devices,
}) : start = DateTime.now();
}
class EncryptedContent {
@ -655,8 +688,11 @@ class EncryptedContent {
final String ciphertext;
final String mac;
EncryptedContent(
{required this.iv, required this.ciphertext, required this.mac});
EncryptedContent({
required this.iv,
required this.ciphertext,
required this.mac,
});
}
class DerivedKeys {
@ -682,11 +718,12 @@ class OpenSSSS {
String? get recoveryKey =>
isUnlocked ? SSSS.encodeRecoveryKey(privateKey!) : null;
Future<void> unlock(
{String? passphrase,
Future<void> unlock({
String? passphrase,
String? recoveryKey,
String? keyOrPassphrase,
bool postUnlock = true}) async {
bool postUnlock = true,
}) async {
if (keyOrPassphrase != null) {
try {
await unlock(recoveryKey: keyOrPassphrase, postUnlock: postUnlock);
@ -701,7 +738,8 @@ class OpenSSSS {
} else if (passphrase != null) {
if (!hasPassphrase) {
throw InvalidPassphraseException(
'Tried to unlock with passphrase while key does not have a passphrase');
'Tried to unlock with passphrase while key does not have a passphrase',
);
}
privateKey = await Future.value(
ssss.client.nativeImplementations.keyFromPassphrase(

View File

@ -164,7 +164,8 @@ class Bootstrap {
Set<String> allNeededKeys() {
final secrets = analyzeSecrets();
secrets.removeWhere(
(k, v) => v.isEmpty); // we don't care about the failed secrets here
(k, v) => v.isEmpty,
); // we don't care about the failed secrets here
final keys = <String>{};
final defaultKeyId = encryption.ssss.defaultKeyId;
int removeKey(String key) {
@ -297,7 +298,8 @@ class Bootstrap {
await encryption.ssss.setDefaultKeyId(newSsssKey!.keyId);
while (encryption.ssss.defaultKeyId != newSsssKey!.keyId) {
Logs().v(
'Waiting accountData to have the correct m.secret_storage.default_key');
'Waiting accountData to have the correct m.secret_storage.default_key',
);
await client.oneShotSync();
}
if (oldSsssKeys != null) {
@ -354,10 +356,11 @@ class Bootstrap {
}
}
Future<void> askSetupCrossSigning(
{bool setupMasterKey = false,
Future<void> askSetupCrossSigning({
bool setupMasterKey = false,
bool setupSelfSigningKey = false,
bool setupUserSigningKey = false}) async {
bool setupUserSigningKey = false,
}) async {
if (state != BootstrapState.askSetupCrossSigning) {
throw BootstrapBadStateException();
}
@ -395,8 +398,8 @@ class Bootstrap {
} else {
Logs().v('Get stored key...');
masterSigningKey = base64decodeUnpadded(
await newSsssKey?.getStored(EventTypes.CrossSigningMasterKey) ??
'');
await newSsssKey?.getStored(EventTypes.CrossSigningMasterKey) ?? '',
);
if (masterSigningKey.isEmpty) {
// no master signing key :(
throw BootstrapBadStateException('No master key');
@ -478,7 +481,8 @@ class Bootstrap {
selfSigningKey: selfSigningKey,
userSigningKey: userSigningKey,
auth: auth,
));
),
);
Logs().v('Device signing keys have been uploaded.');
// aaaand set the SSSS secrets
if (masterKey != null) {
@ -503,7 +507,8 @@ class Bootstrap {
if (client.userDeviceKeys[client.userID]?.masterKey?.ed25519Key !=
masterKey.publicKey) {
throw BootstrapBadStateException(
'ERROR: New master key does not match up!');
'ERROR: New master key does not match up!',
);
}
Logs().v('Set own master key to verified...');
await client.userDeviceKeys[client.userID]!.masterKey!
@ -512,7 +517,8 @@ class Bootstrap {
}
if (selfSigningKey != null) {
keysToSign.add(
client.userDeviceKeys[client.userID]!.deviceKeys[client.deviceID]!);
client.userDeviceKeys[client.userID]!.deviceKeys[client.deviceID]!,
);
}
Logs().v('Sign ourself...');
await encryption.crossSigning.sign(keysToSign);
@ -575,7 +581,8 @@ class Bootstrap {
await newSsssKey?.store(megolmKey, base64.encode(privKey));
Logs().v(
'And finally set all megolm keys as needing to be uploaded again...');
'And finally set all megolm keys as needing to be uploaded again...',
);
await client.database?.markInboundGroupSessionsAsNeedingUpload();
Logs().v('And uploading keys...');
await client.encryption?.keyManager.uploadInboundGroupSessions();

View File

@ -145,7 +145,9 @@ List<String> _intersect(List<String>? a, List<dynamic>? b) =>
(b == null || a == null) ? [] : a.where(b.contains).toList();
List<String> _calculatePossibleMethods(
List<String> knownMethods, List<dynamic> payloadMethods) {
List<String> knownMethods,
List<dynamic> payloadMethods,
) {
final output = <String>[];
final copyKnownMethods = List<String>.from(knownMethods);
final copyPayloadMethods = List.from(payloadMethods);
@ -193,7 +195,9 @@ List<int> _bytesToInt(Uint8List bytes, int totalBits) {
}
_KeyVerificationMethod _makeVerificationMethod(
String type, KeyVerification request) {
String type,
KeyVerification request,
) {
if (type == EventTypes.Sas) {
return _KeyVerificationMethodSas(request: request);
}
@ -237,13 +241,13 @@ class KeyVerification {
QRCode? qrCode;
String? randomSharedSecretForQRCode;
SignableKey? keyToVerify;
KeyVerification(
{required this.encryption,
KeyVerification({
required this.encryption,
this.room,
required this.userId,
String? deviceId,
this.onUpdate})
: _deviceId = deviceId,
this.onUpdate,
}) : _deviceId = deviceId,
lastActivity = DateTime.now();
void dispose() {
@ -287,8 +291,10 @@ class KeyVerification {
/// Once you get a ready event, i.e both sides are in a `askChoice` state,
/// send either `m.reciprocate.v1` or `m.sas.v1` here. If you continue with
/// qr, send the qrData you just scanned
Future<void> continueVerification(String type,
{Uint8List? qrDataRawBytes}) async {
Future<void> continueVerification(
String type, {
Uint8List? qrDataRawBytes,
}) async {
bool qrChecksOut = false;
if (possibleMethods.contains(type)) {
if (qrDataRawBytes != null) {
@ -307,7 +313,8 @@ class KeyVerification {
}
} else {
Logs().e(
'[KeyVerification] tried to continue verification with a unknown method');
'[KeyVerification] tried to continue verification with a unknown method',
);
await cancel('m.unknown_method');
}
}
@ -356,8 +363,11 @@ class KeyVerification {
return mode;
}
Future<void> handlePayload(String type, Map<String, dynamic> payload,
[String? eventId]) async {
Future<void> handlePayload(
String type,
Map<String, dynamic> payload, [
String? eventId,
]) async {
if (isDone) {
return; // no need to do anything with already canceled requests
}
@ -380,8 +390,10 @@ class KeyVerification {
now.add(Duration(minutes: 5)).isBefore(verifyTime)) {
// if the request is more than 20min in the past we just silently fail it
// to not generate too many cancels
await cancel('m.timeout',
now.subtract(Duration(minutes: 20)).isAfter(verifyTime));
await cancel(
'm.timeout',
now.subtract(Duration(minutes: 20)).isAfter(verifyTime),
);
return;
}
@ -397,7 +409,9 @@ class KeyVerification {
oppositePossibleMethods = List<String>.from(payload['methods']);
// verify it has a method we can use
possibleMethods = _calculatePossibleMethods(
knownVerificationMethods, payload['methods']);
knownVerificationMethods,
payload['methods'],
);
if (possibleMethods.isEmpty) {
// reject it outright
await cancel('m.unknown_method');
@ -413,16 +427,21 @@ class KeyVerification {
// and broadcast the cancel to the other devices
final devices = List<DeviceKeys>.from(
client.userDeviceKeys[userId]?.deviceKeys.values ??
Iterable.empty());
Iterable.empty(),
);
devices.removeWhere(
(d) => {deviceId, client.deviceID}.contains(d.deviceId));
(d) => {deviceId, client.deviceID}.contains(d.deviceId),
);
final cancelPayload = <String, dynamic>{
'reason': 'Another device accepted the request',
'code': 'm.accepted',
};
makePayload(cancelPayload);
await client.sendToDeviceEncrypted(
devices, EventTypes.KeyVerificationCancel, cancelPayload);
devices,
EventTypes.KeyVerificationCancel,
cancelPayload,
);
}
_deviceId ??= payload['from_device'];
@ -437,7 +456,9 @@ class KeyVerification {
oppositePossibleMethods = List<String>.from(payload['methods']);
possibleMethods = _calculatePossibleMethods(
knownVerificationMethods, payload['methods']);
knownVerificationMethods,
payload['methods'],
);
if (possibleMethods.isEmpty) {
// reject it outright
await cancel('m.unknown_method');
@ -577,11 +598,12 @@ class KeyVerification {
setState(KeyVerificationState.error);
}
Future<void> openSSSS(
{String? passphrase,
Future<void> openSSSS({
String? passphrase,
String? recoveryKey,
String? keyOrPassphrase,
bool skip = false}) async {
bool skip = false,
}) async {
Future<void> next() async {
if (_nextAction == 'request') {
await sendRequest();
@ -602,7 +624,8 @@ class KeyVerification {
await handle.unlock(
passphrase: passphrase,
recoveryKey: recoveryKey,
keyOrPassphrase: keyOrPassphrase);
keyOrPassphrase: keyOrPassphrase,
);
await handle.maybeCacheAll();
await next();
}
@ -611,7 +634,7 @@ class KeyVerification {
Future<void> acceptVerification() async {
if (!(await verifyLastStep([
EventTypes.KeyVerificationRequest,
EventTypes.KeyVerificationStart
EventTypes.KeyVerificationStart,
]))) {
return;
}
@ -633,7 +656,9 @@ class KeyVerification {
// we are removing stuff only using the old possibleMethods should be ok here.
final copyPossibleMethods = List<String>.from(possibleMethods);
possibleMethods = _calculatePossibleMethods(
copyKnownVerificationMethods, copyPossibleMethods);
copyKnownVerificationMethods,
copyPossibleMethods,
);
}
}
// we need to send a ready event
@ -657,7 +682,7 @@ class KeyVerification {
}
if (!(await verifyLastStep([
EventTypes.KeyVerificationRequest,
EventTypes.KeyVerificationStart
EventTypes.KeyVerificationStart,
]))) {
return;
}
@ -728,12 +753,16 @@ class KeyVerification {
if (requestInterval.length <= i) {
return;
}
Timer(Duration(seconds: requestInterval[i]),
() => maybeRequestSSSSSecrets(i + 1));
Timer(
Duration(seconds: requestInterval[i]),
() => maybeRequestSSSSSecrets(i + 1),
);
}
Future<void> verifyKeysSAS(Map<String, String> keys,
Future<bool> Function(String, SignableKey) verifier) async {
Future<void> verifyKeysSAS(
Map<String, String> keys,
Future<bool> Function(String, SignableKey) verifier,
) async {
_verifiedDevices = <SignableKey>[];
final userDeviceKey = client.userDeviceKeys[userId];
@ -759,7 +788,9 @@ class KeyVerification {
final wasUnknownSession = client.isUnknownSession;
for (final key in _verifiedDevices) {
await key.setVerified(
true, false); // we don't want to sign the keys juuuust yet
true,
false,
); // we don't want to sign the keys juuuust yet
if (key is CrossSigningKey && key.usage.contains('master')) {
verifiedMasterKey = true;
}
@ -859,7 +890,8 @@ class KeyVerification {
return true;
}
Logs().e(
'[KeyVerificaton] lastStep mismatch cancelling, expected from ${checkLastStep.toString()} was ${lastStep.toString()}');
'[KeyVerificaton] lastStep mismatch cancelling, expected from ${checkLastStep.toString()} was ${lastStep.toString()}',
);
await cancel('m.unexpected_message');
return false;
}
@ -917,9 +949,12 @@ class KeyVerification {
EventTypes.KeyVerificationRequest,
EventTypes.KeyVerificationCancel,
}.contains(type)) {
final deviceKeys = client.userDeviceKeys[userId]?.deviceKeys.values
.where((deviceKey) => deviceKey.hasValidSignatureChain(
verifiedByTheirMasterKey: true));
final deviceKeys =
client.userDeviceKeys[userId]?.deviceKeys.values.where(
(deviceKey) => deviceKey.hasValidSignatureChain(
verifiedByTheirMasterKey: true,
),
);
if (deviceKeys != null) {
await client.sendToDeviceEncrypted(
@ -930,14 +965,16 @@ class KeyVerification {
}
} else {
Logs().e(
'[Key Verification] Tried to broadcast and un-broadcastable type: $type');
'[Key Verification] Tried to broadcast and un-broadcastable type: $type',
);
}
} else {
if (client.userDeviceKeys[userId]?.deviceKeys[deviceId] != null) {
await client.sendToDeviceEncrypted(
[client.userDeviceKeys[userId]!.deviceKeys[deviceId]!],
type,
payload);
payload,
);
} else {
Logs().e('[Key Verification] Unknown device');
}
@ -982,7 +1019,8 @@ class KeyVerification {
final otherUserMasterKey = otherUserKeys?.masterKey;
final secondKey = encodeBase64Unpadded(
data.sublist(10 + encodedTxnLen + 32, 10 + encodedTxnLen + 32 + 32));
data.sublist(10 + encodedTxnLen + 32, 10 + encodedTxnLen + 32 + 32),
);
final randomSharedSecret =
encodeBase64Unpadded(data.sublist(10 + encodedTxnLen + 32 + 32));
@ -991,7 +1029,8 @@ class KeyVerification {
.contains(remoteQrMode)) {
if (!(ownMasterKey?.verified ?? false)) {
Logs().e(
'[KeyVerification] verifyQrData because you were in mode 0/2 and had untrusted msk');
'[KeyVerification] verifyQrData because you were in mode 0/2 and had untrusted msk',
);
return false;
}
}
@ -1244,7 +1283,7 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
if (!(await request.verifyLastStep([
EventTypes.KeyVerificationReady,
EventTypes.KeyVerificationRequest,
EventTypes.KeyVerificationStart
EventTypes.KeyVerificationStart,
]))) {
return; // abort
}
@ -1257,7 +1296,7 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
case EventTypes.KeyVerificationAccept:
if (!(await request.verifyLastStep([
EventTypes.KeyVerificationReady,
EventTypes.KeyVerificationRequest
EventTypes.KeyVerificationRequest,
]))) {
return;
}
@ -1270,7 +1309,7 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
case 'm.key.verification.key':
if (!(await request.verifyLastStep([
EventTypes.KeyVerificationAccept,
EventTypes.KeyVerificationStart
EventTypes.KeyVerificationStart,
]))) {
return;
}
@ -1338,7 +1377,9 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
return false;
}
final possibleKeyAgreementProtocols = _intersect(
knownKeyAgreementProtocols, payload['key_agreement_protocols']);
knownKeyAgreementProtocols,
payload['key_agreement_protocols'],
);
if (possibleKeyAgreementProtocols.isEmpty) {
return false;
}
@ -1350,13 +1391,16 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
hash = possibleHashes.first;
final possibleMessageAuthenticationCodes = _intersect(
knownHashesAuthentificationCodes,
payload['message_authentication_codes']);
payload['message_authentication_codes'],
);
if (possibleMessageAuthenticationCodes.isEmpty) {
return false;
}
messageAuthenticationCode = possibleMessageAuthenticationCodes.first;
final possibleAuthenticationTypes = _intersect(
knownAuthentificationTypes, payload['short_authentication_string']);
knownAuthentificationTypes,
payload['short_authentication_string'],
);
if (possibleAuthenticationTypes.isEmpty) {
return false;
}
@ -1394,7 +1438,9 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
}
messageAuthenticationCode = payload['message_authentication_code'];
final possibleAuthenticationTypes = _intersect(
knownAuthentificationTypes, payload['short_authentication_string']);
knownAuthentificationTypes,
payload['short_authentication_string'],
);
if (possibleAuthenticationTypes.isEmpty) {
return false;
}
@ -1497,7 +1543,9 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
await request.verifyKeysSAS(mac, (String mac, SignableKey key) async {
return mac ==
_calculateMac(
key.ed25519Key!, '${baseInfo}ed25519:${key.identifier!}');
key.ed25519Key!,
'${baseInfo}ed25519:${key.identifier!}',
);
});
}

View File

@ -35,11 +35,12 @@ class OutboundGroupSession {
bool get isValid => outboundGroupSession != null;
final String key;
OutboundGroupSession(
{required this.devices,
OutboundGroupSession({
required this.devices,
required this.creationTime,
required this.outboundGroupSession,
required this.key});
required this.key,
});
OutboundGroupSession.fromJson(Map<String, dynamic> dbEntry, this.key) {
try {
@ -49,7 +50,8 @@ class OutboundGroupSession {
} catch (e) {
// devices is bad (old data), so just not use this session
Logs().i(
'[OutboundGroupSession] Session in database is old, not using it. $e');
'[OutboundGroupSession] Session in database is old, not using it. $e',
);
return;
}
outboundGroupSession = olm.OutboundGroupSession();

View File

@ -60,8 +60,8 @@ class SessionKey {
/// Id of this session
String sessionId;
SessionKey(
{required this.content,
SessionKey({
required this.content,
required this.inboundGroupSession,
required this.key,
Map<String, String>? indexes,
@ -69,8 +69,8 @@ class SessionKey {
required this.roomId,
required this.sessionId,
required this.senderKey,
required this.senderClaimedKeys})
: indexes = indexes ?? <String, String>{},
required this.senderClaimedKeys,
}) : indexes = indexes ?? <String, String>{},
allowedAtIndex = allowedAtIndex ?? <String, Map<String, int>>{};
SessionKey.fromDb(StoredInboundGroupSession dbEntry, this.key)
@ -94,7 +94,7 @@ class SessionKey {
?.catchMap((k, v) => MapEntry<String, String>(k, v)) ??
(content['sender_claimed_ed25519_key'] is String
? <String, String>{
'ed25519': content['sender_claimed_ed25519_key']
'ed25519': content['sender_claimed_ed25519_key'],
}
: <String, String>{}));

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -80,14 +80,18 @@ class DiscoveryInformation {
DiscoveryInformation.fromJson(Map<String, Object?> json)
: mHomeserver = HomeserverInformation.fromJson(
json['m.homeserver'] as Map<String, Object?>),
json['m.homeserver'] as Map<String, Object?>,
),
mIdentityServer = ((v) => v != null
? IdentityServerInformation.fromJson(v as Map<String, Object?>)
: null)(json['m.identity_server']),
additionalProperties = Map.fromEntries(json.entries
additionalProperties = Map.fromEntries(
json.entries
.where(
(e) => !['m.homeserver', 'm.identity_server'].contains(e.key))
.map((e) => MapEntry(e.key, e.value)));
(e) => !['m.homeserver', 'm.identity_server'].contains(e.key),
)
.map((e) => MapEntry(e.key, e.value)),
);
Map<String, Object?> toJson() {
final mIdentityServer = this.mIdentityServer;
return {
@ -462,8 +466,18 @@ class PublicRoomsChunk {
other.worldReadable == worldReadable);
@dart.override
int get hashCode => Object.hash(avatarUrl, canonicalAlias, guestCanJoin,
joinRule, name, numJoinedMembers, roomId, roomType, topic, worldReadable);
int get hashCode => Object.hash(
avatarUrl,
canonicalAlias,
guestCanJoin,
joinRule,
name,
numJoinedMembers,
roomId,
roomType,
topic,
worldReadable,
);
}
///
@ -643,7 +657,8 @@ class SpaceRoomsChunk implements PublicRoomsChunk, SpaceHierarchyRoomsChunk {
roomType,
topic,
worldReadable,
childrenState);
childrenState,
);
}
///
@ -831,8 +846,8 @@ class GetRelatingEventsWithRelTypeAndEventTypeResponse {
});
GetRelatingEventsWithRelTypeAndEventTypeResponse.fromJson(
Map<String, Object?> json)
: chunk = (json['chunk'] as List)
Map<String, Object?> json,
) : chunk = (json['chunk'] as List)
.map((v) => MatrixEvent.fromJson(v as Map<String, Object?>))
.toList(),
nextBatch = ((v) => v != null ? v as String : null)(json['next_batch']),
@ -1320,8 +1335,10 @@ class WhoIsInfo {
WhoIsInfo.fromJson(Map<String, Object?> json)
: devices = ((v) => v != null
? (v as Map<String, Object?>).map((k, v) =>
MapEntry(k, DeviceInfo.fromJson(v as Map<String, Object?>)))
? (v as Map<String, Object?>).map(
(k, v) =>
MapEntry(k, DeviceInfo.fromJson(v as Map<String, Object?>)),
)
: null)(json['devices']),
userId = ((v) => v != null ? v as String : null)(json['user_id']);
Map<String, Object?> toJson() {
@ -1398,8 +1415,10 @@ class RoomVersionsCapability {
});
RoomVersionsCapability.fromJson(Map<String, Object?> json)
: available = (json['available'] as Map<String, Object?>).map((k, v) =>
MapEntry(k, RoomVersionAvailable.values.fromString(v as String)!)),
: available = (json['available'] as Map<String, Object?>).map(
(k, v) =>
MapEntry(k, RoomVersionAvailable.values.fromString(v as String)!),
),
default$ = json['default'] as String;
Map<String, Object?> toJson() => {
'available': available.map((k, v) => MapEntry(k, v.name)),
@ -1456,16 +1475,20 @@ class Capabilities {
mSetDisplayname = ((v) => v != null
? BooleanCapability.fromJson(v as Map<String, Object?>)
: null)(json['m.set_displayname']),
additionalProperties = Map.fromEntries(json.entries
.where((e) => ![
additionalProperties = Map.fromEntries(
json.entries
.where(
(e) => ![
'm.3pid_changes',
'm.change_password',
'm.get_login_token',
'm.room_versions',
'm.set_avatar_url',
'm.set_displayname'
].contains(e.key))
.map((e) => MapEntry(e.key, e.value)));
'm.set_displayname',
].contains(e.key),
)
.map((e) => MapEntry(e.key, e.value)),
);
Map<String, Object?> toJson() {
final m3pidChanges = this.m3pidChanges;
final mChangePassword = this.mChangePassword;
@ -1519,8 +1542,14 @@ class Capabilities {
other.mSetDisplayname == mSetDisplayname);
@dart.override
int get hashCode => Object.hash(m3pidChanges, mChangePassword, mGetLoginToken,
mRoomVersions, mSetAvatarUrl, mSetDisplayname);
int get hashCode => Object.hash(
m3pidChanges,
mChangePassword,
mGetLoginToken,
mRoomVersions,
mSetAvatarUrl,
mSetDisplayname,
);
}
///
@ -1858,11 +1887,12 @@ class ThirdPartySigned {
ThirdPartySigned.fromJson(Map<String, Object?> json)
: mxid = json['mxid'] as String,
sender = json['sender'] as String,
signatures = (json['signatures'] as Map<String, Object?>).map((k, v) =>
MapEntry(
signatures = (json['signatures'] as Map<String, Object?>).map(
(k, v) => MapEntry(
k,
(v as Map<String, Object?>)
.map((k, v) => MapEntry(k, v as String)))),
(v as Map<String, Object?>).map((k, v) => MapEntry(k, v as String)),
),
),
token = json['token'] as String;
Map<String, Object?> toJson() => {
'mxid': mxid,
@ -1960,7 +1990,9 @@ class ClaimKeysResponse {
(k, v) => MapEntry(
k,
(v as Map<String, Object?>)
.map((k, v) => MapEntry(k, v as Map<String, Object?>))));
.map((k, v) => MapEntry(k, v as Map<String, Object?>)),
),
);
Map<String, Object?> toJson() {
final failures = this.failures;
return {
@ -2014,26 +2046,45 @@ class QueryKeysResponse {
QueryKeysResponse.fromJson(Map<String, Object?> json)
: deviceKeys = ((v) => v != null
? (v as Map<String, Object?>).map((k, v) => MapEntry(
? (v as Map<String, Object?>).map(
(k, v) => MapEntry(
k,
(v as Map<String, Object?>).map((k, v) => MapEntry(
k, MatrixDeviceKeys.fromJson(v as Map<String, Object?>)))))
(v as Map<String, Object?>).map(
(k, v) => MapEntry(
k,
MatrixDeviceKeys.fromJson(v as Map<String, Object?>),
),
),
),
)
: null)(json['device_keys']),
failures = ((v) => v != null
? (v as Map<String, Object?>)
.map((k, v) => MapEntry(k, v as Map<String, Object?>))
: null)(json['failures']),
masterKeys = ((v) => v != null
? (v as Map<String, Object?>).map((k, v) => MapEntry(
k, MatrixCrossSigningKey.fromJson(v as Map<String, Object?>)))
? (v as Map<String, Object?>).map(
(k, v) => MapEntry(
k,
MatrixCrossSigningKey.fromJson(v as Map<String, Object?>),
),
)
: null)(json['master_keys']),
selfSigningKeys = ((v) => v != null
? (v as Map<String, Object?>).map((k, v) => MapEntry(
k, MatrixCrossSigningKey.fromJson(v as Map<String, Object?>)))
? (v as Map<String, Object?>).map(
(k, v) => MapEntry(
k,
MatrixCrossSigningKey.fromJson(v as Map<String, Object?>),
),
)
: null)(json['self_signing_keys']),
userSigningKeys = ((v) => v != null
? (v as Map<String, Object?>).map((k, v) => MapEntry(
k, MatrixCrossSigningKey.fromJson(v as Map<String, Object?>)))
? (v as Map<String, Object?>).map(
(k, v) => MapEntry(
k,
MatrixCrossSigningKey.fromJson(v as Map<String, Object?>),
),
)
: null)(json['user_signing_keys']);
Map<String, Object?> toJson() {
final deviceKeys = this.deviceKeys;
@ -2044,7 +2095,8 @@ class QueryKeysResponse {
return {
if (deviceKeys != null)
'device_keys': deviceKeys.map(
(k, v) => MapEntry(k, v.map((k, v) => MapEntry(k, v.toJson())))),
(k, v) => MapEntry(k, v.map((k, v) => MapEntry(k, v.toJson()))),
),
if (failures != null) 'failures': failures.map((k, v) => MapEntry(k, v)),
if (masterKeys != null)
'master_keys': masterKeys.map((k, v) => MapEntry(k, v.toJson())),
@ -2107,7 +2159,12 @@ class QueryKeysResponse {
@dart.override
int get hashCode => Object.hash(
deviceKeys, failures, masterKeys, selfSigningKeys, userSigningKeys);
deviceKeys,
failures,
masterKeys,
selfSigningKeys,
userSigningKeys,
);
}
///
@ -2123,9 +2180,11 @@ class LoginFlow {
: getLoginToken =
((v) => v != null ? v as bool : null)(json['get_login_token']),
type = json['type'] as String,
additionalProperties = Map.fromEntries(json.entries
additionalProperties = Map.fromEntries(
json.entries
.where((e) => !['get_login_token', 'type'].contains(e.key))
.map((e) => MapEntry(e.key, e.value)));
.map((e) => MapEntry(e.key, e.value)),
);
Map<String, Object?> toJson() {
final getLoginToken = this.getLoginToken;
return {
@ -2255,8 +2314,15 @@ class LoginResponse {
other.wellKnown == wellKnown);
@dart.override
int get hashCode => Object.hash(accessToken, deviceId, expiresInMs,
homeServer, refreshToken, userId, wellKnown);
int get hashCode => Object.hash(
accessToken,
deviceId,
expiresInMs,
homeServer,
refreshToken,
userId,
wellKnown,
);
}
///
@ -2663,9 +2729,11 @@ class PusherData {
PusherData.fromJson(Map<String, Object?> json)
: format = ((v) => v != null ? v as String : null)(json['format']),
url = ((v) => v != null ? Uri.parse(v as String) : null)(json['url']),
additionalProperties = Map.fromEntries(json.entries
additionalProperties = Map.fromEntries(
json.entries
.where((e) => !['format', 'url'].contains(e.key))
.map((e) => MapEntry(e.key, e.value)));
.map((e) => MapEntry(e.key, e.value)),
);
Map<String, Object?> toJson() {
final format = this.format;
final url = this.url;
@ -2824,8 +2892,16 @@ class Pusher implements PusherId {
other.profileTag == profileTag);
@dart.override
int get hashCode => Object.hash(appId, pushkey, appDisplayName, data,
deviceDisplayName, kind, lang, profileTag);
int get hashCode => Object.hash(
appId,
pushkey,
appDisplayName,
data,
deviceDisplayName,
kind,
lang,
profileTag,
);
}
///
@ -3316,7 +3392,13 @@ class RegisterResponse {
@dart.override
int get hashCode => Object.hash(
accessToken, deviceId, expiresInMs, homeServer, refreshToken, userId);
accessToken,
deviceId,
expiresInMs,
homeServer,
refreshToken,
userId,
);
}
///
@ -3414,8 +3496,10 @@ class RoomKeyBackup {
});
RoomKeyBackup.fromJson(Map<String, Object?> json)
: sessions = (json['sessions'] as Map<String, Object?>).map((k, v) =>
MapEntry(k, KeyBackupData.fromJson(v as Map<String, Object?>)));
: sessions = (json['sessions'] as Map<String, Object?>).map(
(k, v) =>
MapEntry(k, KeyBackupData.fromJson(v as Map<String, Object?>)),
);
Map<String, Object?> toJson() => {
'sessions': sessions.map((k, v) => MapEntry(k, v.toJson())),
};
@ -3442,8 +3526,10 @@ class RoomKeys {
});
RoomKeys.fromJson(Map<String, Object?> json)
: rooms = (json['rooms'] as Map<String, Object?>).map((k, v) =>
MapEntry(k, RoomKeyBackup.fromJson(v as Map<String, Object?>)));
: rooms = (json['rooms'] as Map<String, Object?>).map(
(k, v) =>
MapEntry(k, RoomKeyBackup.fromJson(v as Map<String, Object?>)),
);
Map<String, Object?> toJson() => {
'rooms': rooms.map((k, v) => MapEntry(k, v.toJson())),
};
@ -4039,8 +4125,14 @@ class RoomEventFilter {
other.unreadThreadNotifications == unreadThreadNotifications);
@dart.override
int get hashCode => Object.hash(containsUrl, includeRedundantMembers,
lazyLoadMembers, notRooms, rooms, unreadThreadNotifications);
int get hashCode => Object.hash(
containsUrl,
includeRedundantMembers,
lazyLoadMembers,
notRooms,
rooms,
unreadThreadNotifications,
);
}
///
@ -4202,7 +4294,8 @@ class SearchFilter implements EventFilter, RoomEventFilter {
lazyLoadMembers,
notRooms,
rooms,
unreadThreadNotifications);
unreadThreadNotifications,
);
}
///
@ -4393,7 +4486,14 @@ class RoomEventsCriteria {
@dart.override
int get hashCode => Object.hash(
eventContext, filter, groupings, includeState, keys, orderBy, searchTerm);
eventContext,
filter,
groupings,
includeState,
keys,
orderBy,
searchTerm,
);
}
///
@ -4545,8 +4645,12 @@ class SearchResultsEventContext {
.toList()
: null)(json['events_before']),
profileInfo = ((v) => v != null
? (v as Map<String, Object?>).map((k, v) =>
MapEntry(k, UserProfile.fromJson(v as Map<String, Object?>)))
? (v as Map<String, Object?>).map(
(k, v) => MapEntry(
k,
UserProfile.fromJson(v as Map<String, Object?>),
),
)
: null)(json['profile_info']),
start = ((v) => v != null ? v as String : null)(json['start']);
Map<String, Object?> toJson() {
@ -4667,10 +4771,17 @@ class ResultRoomEvents {
ResultRoomEvents.fromJson(Map<String, Object?> json)
: count = ((v) => v != null ? v as int : null)(json['count']),
groups = ((v) => v != null
? (v as Map<String, Object?>).map((k, v) => MapEntry(
? (v as Map<String, Object?>).map(
(k, v) => MapEntry(
k,
(v as Map<String, Object?>).map((k, v) => MapEntry(
k, GroupValue.fromJson(v as Map<String, Object?>)))))
(v as Map<String, Object?>).map(
(k, v) => MapEntry(
k,
GroupValue.fromJson(v as Map<String, Object?>),
),
),
),
)
: null)(json['groups']),
highlights = ((v) => v != null
? (v as List).map((v) => v as String).toList()
@ -4682,11 +4793,16 @@ class ResultRoomEvents {
.toList()
: null)(json['results']),
state = ((v) => v != null
? (v as Map<String, Object?>).map((k, v) => MapEntry(
? (v as Map<String, Object?>).map(
(k, v) => MapEntry(
k,
(v as List)
.map((v) => MatrixEvent.fromJson(v as Map<String, Object?>))
.toList()))
.map(
(v) => MatrixEvent.fromJson(v as Map<String, Object?>),
)
.toList(),
),
)
: null)(json['state']);
Map<String, Object?> toJson() {
final count = this.count;
@ -4699,7 +4815,8 @@ class ResultRoomEvents {
if (count != null) 'count': count,
if (groups != null)
'groups': groups.map(
(k, v) => MapEntry(k, v.map((k, v) => MapEntry(k, v.toJson())))),
(k, v) => MapEntry(k, v.map((k, v) => MapEntry(k, v.toJson()))),
),
if (highlights != null) 'highlights': highlights.map((v) => v).toList(),
if (nextBatch != null) 'next_batch': nextBatch,
if (results != null) 'results': results.map((v) => v.toJson()).toList(),
@ -4797,7 +4914,8 @@ class SearchResults {
SearchResults.fromJson(Map<String, Object?> json)
: searchCategories = ResultCategories.fromJson(
json['search_categories'] as Map<String, Object?>);
json['search_categories'] as Map<String, Object?>,
);
Map<String, Object?> toJson() => {
'search_categories': searchCategories.toJson(),
};
@ -4957,8 +5075,9 @@ class Protocol {
});
Protocol.fromJson(Map<String, Object?> json)
: fieldTypes = (json['field_types'] as Map<String, Object?>).map((k, v) =>
MapEntry(k, FieldType.fromJson(v as Map<String, Object?>))),
: fieldTypes = (json['field_types'] as Map<String, Object?>).map(
(k, v) => MapEntry(k, FieldType.fromJson(v as Map<String, Object?>)),
),
icon = json['icon'] as String,
instances = (json['instances'] as List)
.map((v) => ProtocolInstance.fromJson(v as Map<String, Object?>))
@ -5228,7 +5347,8 @@ class StateFilter implements EventFilter, RoomEventFilter {
lazyLoadMembers,
notRooms,
rooms,
unreadThreadNotifications);
unreadThreadNotifications,
);
}
///
@ -5320,7 +5440,14 @@ class RoomFilter {
@dart.override
int get hashCode => Object.hash(
accountData, ephemeral, includeLeave, notRooms, rooms, state, timeline);
accountData,
ephemeral,
includeLeave,
notRooms,
rooms,
state,
timeline,
);
}
///
@ -5461,9 +5588,11 @@ class Tag {
Tag.fromJson(Map<String, Object?> json)
: order =
((v) => v != null ? (v as num).toDouble() : null)(json['order']),
additionalProperties = Map.fromEntries(json.entries
additionalProperties = Map.fromEntries(
json.entries
.where((e) => !['order'].contains(e.key))
.map((e) => MapEntry(e.key, e.value)));
.map((e) => MapEntry(e.key, e.value)),
);
Map<String, Object?> toJson() {
final order = this.order;
return {

View File

@ -202,19 +202,29 @@ class MatrixApi extends Api {
@Deprecated('Use [deleteRoomKeyBySessionId] instead')
Future<RoomKeysUpdateResponse> deleteRoomKeysBySessionId(
String roomId, String sessionId, String version) async {
String roomId,
String sessionId,
String version,
) async {
return deleteRoomKeyBySessionId(roomId, sessionId, version);
}
@Deprecated('Use [deleteRoomKeyBySessionId] instead')
Future<RoomKeysUpdateResponse> putRoomKeysBySessionId(String roomId,
String sessionId, String version, KeyBackupData data) async {
Future<RoomKeysUpdateResponse> putRoomKeysBySessionId(
String roomId,
String sessionId,
String version,
KeyBackupData data,
) async {
return putRoomKeyBySessionId(roomId, sessionId, version, data);
}
@Deprecated('Use [getRoomKeyBySessionId] instead')
Future<KeyBackupData> getRoomKeysBySessionId(
String roomId, String sessionId, String version) async {
String roomId,
String sessionId,
String version,
) async {
return getRoomKeyBySessionId(roomId, sessionId, version);
}
}

View File

@ -33,16 +33,19 @@ class AuthenticationPassword extends AuthenticationData {
/// Identifier classes extending AuthenticationIdentifier.
AuthenticationIdentifier identifier;
AuthenticationPassword(
{super.session, required this.password, required this.identifier})
: super(
AuthenticationPassword({
super.session,
required this.password,
required this.identifier,
}) : super(
type: AuthenticationTypes.password,
);
AuthenticationPassword.fromJson(super.json)
: password = json['password'] as String,
identifier = AuthenticationIdentifier.subFromJson(
json['identifier'] as Map<String, Object?>),
json['identifier'] as Map<String, Object?>,
),
super.fromJson();
@override

View File

@ -28,9 +28,10 @@ class AuthenticationThirdPartyIdentifier extends AuthenticationIdentifier {
String medium;
String address;
AuthenticationThirdPartyIdentifier(
{required this.medium, required this.address})
: super(type: AuthenticationIdentifierTypes.thirdParty);
AuthenticationThirdPartyIdentifier({
required this.medium,
required this.address,
}) : super(type: AuthenticationIdentifierTypes.thirdParty);
AuthenticationThirdPartyIdentifier.fromJson(super.json)
: medium = json['medium'] as String,

View File

@ -30,8 +30,11 @@ import 'package:matrix/matrix_api_lite/model/auth/authentication_data.dart';
class AuthenticationThreePidCreds extends AuthenticationData {
late ThreepidCreds threepidCreds;
AuthenticationThreePidCreds(
{super.session, required String super.type, required this.threepidCreds});
AuthenticationThreePidCreds({
super.session,
required String super.type,
required this.threepidCreds,
});
AuthenticationThreePidCreds.fromJson(Map<String, Object?> json)
: super.fromJson(json) {
@ -55,11 +58,12 @@ class ThreepidCreds {
String? idServer;
String? idAccessToken;
ThreepidCreds(
{required this.sid,
ThreepidCreds({
required this.sid,
required this.clientSecret,
this.idServer,
this.idAccessToken});
this.idAccessToken,
});
ThreepidCreds.fromJson(Map<String, Object?> json)
: sid = json['sid'] as String,

View File

@ -26,8 +26,11 @@ import 'package:matrix/matrix_api_lite/model/basic_event.dart';
class BasicEventWithSender extends BasicEvent {
String senderId;
BasicEventWithSender(
{required super.type, required super.content, required this.senderId});
BasicEventWithSender({
required super.type,
required super.content,
required this.senderId,
});
BasicEventWithSender.fromJson(super.json)
: senderId = json['sender'] as String,

View File

@ -36,7 +36,8 @@ class ChildrenState extends StrippedStateEvent {
ChildrenState.fromJson(super.json)
: originServerTs = DateTime.fromMillisecondsSinceEpoch(
json['origin_server_ts'] as int),
json['origin_server_ts'] as int,
),
super.fromJson();
@override

View File

@ -40,7 +40,9 @@ class ForwardedRoomKeyContent extends RoomKeyContent {
senderClaimedEd25519Key =
json.tryGet('sender_claimed_ed25519_key', TryGet.required) ?? '',
forwardingCurve25519KeyChain = json.tryGetList(
'forwarding_curve25519_key_chain', TryGet.required) ??
'forwarding_curve25519_key_chain',
TryGet.required,
) ??
[],
super.fromJson();

View File

@ -36,21 +36,27 @@ enum ImagePackUsage {
}
List<ImagePackUsage>? imagePackUsageFromJson(List<String>? json) => json
?.map((v) => {
?.map(
(v) => {
'sticker': ImagePackUsage.sticker,
'emoticon': ImagePackUsage.emoticon,
}[v])
}[v],
)
.whereType<ImagePackUsage>()
.toList();
List<String> imagePackUsageToJson(
List<ImagePackUsage>? usage, List<String>? prevUsage) {
List<ImagePackUsage>? usage,
List<String>? prevUsage,
) {
final knownUsages = <String>{'sticker', 'emoticon'};
final usagesStr = usage
?.map((v) => {
?.map(
(v) => {
ImagePackUsage.sticker: 'sticker',
ImagePackUsage.emoticon: 'emoticon',
}[v])
}[v],
)
.whereType<String>()
.toList() ??
[];
@ -74,30 +80,42 @@ class ImagePackContent {
ImagePackContent({required this.images, required this.pack}) : _json = {};
ImagePackContent.fromJson(Map<String, Object?> json)
: _json = Map.fromEntries(json.entries.where(
(e) => !['images', 'pack', 'emoticons', 'short'].contains(e.key))),
: _json = Map.fromEntries(
json.entries.where(
(e) => !['images', 'pack', 'emoticons', 'short'].contains(e.key),
),
),
pack = ImagePackPackContent.fromJson(
json.tryGetMap<String, Object?>('pack') ?? {}),
images = json.tryGetMap<String, Object?>('images')?.catchMap((k, v) =>
MapEntry(
json.tryGetMap<String, Object?>('pack') ?? {},
),
images = json.tryGetMap<String, Object?>('images')?.catchMap(
(k, v) => MapEntry(
k,
ImagePackImageContent.fromJson(
v as Map<String, Object?>))) ??
v as Map<String, Object?>,
),
),
) ??
// the "emoticons" key needs a small migration on the key, ":string:" --> "string"
json.tryGetMap<String, Object?>('emoticons')?.catchMap((k, v) =>
MapEntry(
json.tryGetMap<String, Object?>('emoticons')?.catchMap(
(k, v) => MapEntry(
k.startsWith(':') && k.endsWith(':')
? k.substring(1, k.length - 1)
: k,
ImagePackImageContent.fromJson(
v as Map<String, Object?>))) ??
v as Map<String, Object?>,
),
),
) ??
// the "short" key was still just a map from shortcode to mxc uri
json.tryGetMap<String, String>('short')?.catchMap((k, v) =>
MapEntry(
json.tryGetMap<String, String>('short')?.catchMap(
(k, v) => MapEntry(
k.startsWith(':') && k.endsWith(':')
? k.substring(1, k.length - 1)
: k,
ImagePackImageContent(url: Uri.parse(v)))) ??
ImagePackImageContent(url: Uri.parse(v)),
),
) ??
{};
Map<String, Object?> toJson() => {
@ -120,8 +138,9 @@ class ImagePackImageContent {
: _json = {};
ImagePackImageContent.fromJson(Map<String, Object?> json)
: _json = Map.fromEntries(json.entries
.where((e) => !['url', 'body', 'info'].contains(e.key))),
: _json = Map.fromEntries(
json.entries.where((e) => !['url', 'body', 'info'].contains(e.key)),
),
url = Uri.parse(json['url'] as String),
body = json.tryGet('body'),
info = json.tryGetMap<String, Object?>('info'),
@ -148,13 +167,20 @@ class ImagePackPackContent {
List<ImagePackUsage>? usage;
String? attribution;
ImagePackPackContent(
{this.displayName, this.avatarUrl, this.usage, this.attribution})
: _json = {};
ImagePackPackContent({
this.displayName,
this.avatarUrl,
this.usage,
this.attribution,
}) : _json = {};
ImagePackPackContent.fromJson(Map<String, Object?> json)
: _json = Map.fromEntries(json.entries.where((e) =>
!['display_name', 'avatar_url', 'attribution'].contains(e.key))),
: _json = Map.fromEntries(
json.entries.where(
(e) =>
!['display_name', 'avatar_url', 'attribution'].contains(e.key),
),
),
displayName = json.tryGet('display_name'),
// we default to an invalid uri
avatarUrl = Uri.tryParse(json.tryGet('avatar_url') ?? '.::'),

View File

@ -45,8 +45,12 @@ class RoomEncryptedContent {
// filter out invalid/incomplete CiphertextInfos
ciphertextOlm = json
.tryGet<Map<String, Object?>>('ciphertext', TryGet.silent)
?.catchMap((k, v) => MapEntry(
k, CiphertextInfo.fromJson(v as Map<String, Object?>)));
?.catchMap(
(k, v) => MapEntry(
k,
CiphertextInfo.fromJson(v as Map<String, Object?>),
),
);
Map<String, Object?> toJson() {
final data = <String, Object?>{};
@ -66,7 +70,8 @@ class RoomEncryptedContent {
ciphertextOlm!.map((k, v) => MapEntry(k, v.toJson()));
if (ciphertextMegolm != null) {
Logs().wtf(
'ciphertextOlm and ciphertextMegolm are both set, which should never happen!');
'ciphertextOlm and ciphertextMegolm are both set, which should never happen!',
);
}
}
return data;

View File

@ -34,11 +34,12 @@ class RoomKeyContent {
String sessionId;
String sessionKey;
RoomKeyContent(
{required this.algorithm,
RoomKeyContent({
required this.algorithm,
required this.roomId,
required this.sessionId,
required this.sessionKey});
required this.sessionKey,
});
RoomKeyContent.fromJson(Map<String, Object?> json)
: algorithm = json.tryGet('algorithm', TryGet.required) ?? '',

View File

@ -60,11 +60,12 @@ class RequestedKeyInfo {
String sessionId;
String senderKey;
RequestedKeyInfo(
{required this.algorithm,
RequestedKeyInfo({
required this.algorithm,
required this.roomId,
required this.sessionId,
required this.senderKey});
required this.senderKey,
});
RequestedKeyInfo.fromJson(Map<String, Object?> json)
: algorithm = json.tryGet('algorithm', TryGet.required) ?? '',

View File

@ -63,11 +63,12 @@ class PassphraseInfo {
int? iterations;
int? bits;
PassphraseInfo(
{required this.algorithm,
PassphraseInfo({
required this.algorithm,
required this.salt,
required this.iterations,
this.bits});
this.bits,
});
PassphraseInfo.fromJson(Map<String, Object?> json)
: algorithm = json.tryGet('algorithm', TryGet.required),

View File

@ -49,7 +49,8 @@ class MatrixEvent extends StrippedStateEvent {
: eventId = json['event_id'] as String,
roomId = json['room_id'] as String?,
originServerTs = DateTime.fromMillisecondsSinceEpoch(
json['origin_server_ts'] as int),
json['origin_server_ts'] as int,
),
unsigned = (json['unsigned'] as Map<String, Object?>?)?.copy(),
prevContent = (json['prev_content'] as Map<String, Object?>?)?.copy(),
redacts = json['redacts'] as String?,

View File

@ -115,8 +115,10 @@ class MatrixException implements Exception {
?.whereType<Map<String, Object?>>()
.map((flow) => flow['stages'])
.whereType<List<Object?>>()
.map((stages) =>
AuthenticationFlow(List<String>.from(stages.whereType<String>())))
.map(
(stages) =>
AuthenticationFlow(List<String>.from(stages.whereType<String>())),
)
.toList();
/// This section contains any information that the client will need to know in order to use a given type

View File

@ -32,7 +32,8 @@ class PresenceContent {
PresenceContent.fromJson(Map<String, Object?> json)
: presence = PresenceType.values.firstWhere(
(p) => p.toString().split('.').last == json['presence'],
orElse: () => PresenceType.offline),
orElse: () => PresenceType.offline,
),
lastActiveAgo = json.tryGet<int>('last_active_ago'),
statusMsg = json.tryGet<String>('status_msg'),
currentlyActive = json.tryGet<bool>('currently_active');

View File

@ -29,11 +29,12 @@ class RoomKeysSingleKey {
bool isVerified;
Map<String, Object?> sessionData;
RoomKeysSingleKey(
{required this.firstMessageIndex,
RoomKeysSingleKey({
required this.firstMessageIndex,
required this.forwardedCount,
required this.isVerified,
required this.sessionData});
required this.sessionData,
});
RoomKeysSingleKey.fromJson(Map<String, Object?> json)
: firstMessageIndex = json['first_message_index'] as int,
@ -57,8 +58,12 @@ class RoomKeysRoom {
RoomKeysRoom({required this.sessions});
RoomKeysRoom.fromJson(Map<String, Object?> json)
: sessions = (json['sessions'] as Map<String, Object?>).map((k, v) =>
MapEntry(k, RoomKeysSingleKey.fromJson(v as Map<String, Object?>)));
: sessions = (json['sessions'] as Map<String, Object?>).map(
(k, v) => MapEntry(
k,
RoomKeysSingleKey.fromJson(v as Map<String, Object?>),
),
);
Map<String, Object?> toJson() {
final data = <String, Object?>{};

View File

@ -26,11 +26,12 @@ import 'package:matrix/matrix_api_lite.dart';
class StrippedStateEvent extends BasicEventWithSender {
String? stateKey;
StrippedStateEvent(
{required super.type,
StrippedStateEvent({
required super.type,
required super.content,
required super.senderId,
this.stateKey});
this.stateKey,
});
StrippedStateEvent.fromJson(super.json)
: stateKey = json.tryGet<String>('state_key'),

View File

@ -61,7 +61,8 @@ class SyncUpdate {
toDevice = json
.tryGetMap<String, List<Object?>>('to_device')?['events']
?.map(
(i) => BasicEventWithSender.fromJson(i as Map<String, Object?>))
(i) => BasicEventWithSender.fromJson(i as Map<String, Object?>),
)
.toList(),
deviceLists = (() {
final temp = json.tryGetMap<String, Object?>('device_lists');
@ -72,7 +73,8 @@ class SyncUpdate {
deviceUnusedFallbackKeyTypes =
json.tryGetList<String>('device_unused_fallback_key_types') ??
json.tryGetList<String>(
'org.matrix.msc2732.device_unused_fallback_key_types');
'org.matrix.msc2732.device_unused_fallback_key_types',
);
Map<String, Object?> toJson() {
final data = <String, Object?>{};
@ -124,14 +126,24 @@ class RoomsUpdate {
});
RoomsUpdate.fromJson(Map<String, Object?> json) {
join = json.tryGetMap<String, Object?>('join')?.catchMap((k, v) =>
MapEntry(k, JoinedRoomUpdate.fromJson(v as Map<String, Object?>)));
invite = json.tryGetMap<String, Object?>('invite')?.catchMap((k, v) =>
MapEntry(k, InvitedRoomUpdate.fromJson(v as Map<String, Object?>)));
leave = json.tryGetMap<String, Object?>('leave')?.catchMap((k, v) =>
MapEntry(k, LeftRoomUpdate.fromJson(v as Map<String, Object?>)));
knock = json.tryGetMap<String, Object?>('knock')?.catchMap((k, v) =>
MapEntry(k, KnockRoomUpdate.fromJson(v as Map<String, Object?>)));
join = json.tryGetMap<String, Object?>('join')?.catchMap(
(k, v) =>
MapEntry(k, JoinedRoomUpdate.fromJson(v as Map<String, Object?>)),
);
invite = json.tryGetMap<String, Object?>('invite')?.catchMap(
(k, v) => MapEntry(
k,
InvitedRoomUpdate.fromJson(v as Map<String, Object?>),
),
);
leave = json.tryGetMap<String, Object?>('leave')?.catchMap(
(k, v) =>
MapEntry(k, LeftRoomUpdate.fromJson(v as Map<String, Object?>)),
);
knock = json.tryGetMap<String, Object?>('knock')?.catchMap(
(k, v) =>
MapEntry(k, KnockRoomUpdate.fromJson(v as Map<String, Object?>)),
);
}
Map<String, Object?> toJson() {
@ -187,7 +199,9 @@ class JoinedRoomUpdate extends SyncRoomUpdate {
?.map((i) => BasicRoomEvent.fromJson(i as Map<String, Object?>))
.toList(),
unreadNotifications = json.tryGetFromJson(
'unread_notifications', UnreadNotificationCounts.fromJson);
'unread_notifications',
UnreadNotificationCounts.fromJson,
);
Map<String, Object?> toJson() {
final data = <String, Object?>{};

View File

@ -1,7 +1,8 @@
extension FilterMap<K, V> on Map<K, V> {
Map<K2, V2> filterMap<K2, V2>(MapEntry<K2, V2>? Function(K, V) f) =>
Map.fromEntries(
entries.map((e) => f(e.key, e.value)).whereType<MapEntry<K2, V2>>());
entries.map((e) => f(e.key, e.value)).whereType<MapEntry<K2, V2>>(),
);
Map<K2, V2> catchMap<K2, V2>(MapEntry<K2, V2> Function(K, V) f) =>
filterMap((k, v) {

View File

@ -39,7 +39,8 @@ class _RequiredLog implements TryGet {
const _RequiredLog();
@override
void call(String key, Type expected, Type actual) => Logs().w(
'Expected required "$expected" in event content for the Key "$key" but got "$actual" at ${StackTrace.current.firstLine}');
'Expected required "$expected" in event content for the Key "$key" but got "$actual" at ${StackTrace.current.firstLine}',
);
}
class _OptionalLog implements TryGet {
@ -48,7 +49,8 @@ class _OptionalLog implements TryGet {
void call(String key, Type expected, Type actual) {
if (actual != Null) {
Logs().w(
'Expected optional "$expected" in event content for the Key "$key" but got "$actual" at ${StackTrace.current.firstLine}');
'Expected optional "$expected" in event content for the Key "$key" but got "$actual" at ${StackTrace.current.firstLine}',
);
}
}
}
@ -80,7 +82,8 @@ extension TryGetMapExtension on Map<String, Object?> {
return value.cast<T>().toList();
} catch (_) {
Logs().v(
'Unable to create "List<$T>" in event content for the key "$key" at ${StackTrace.current.firstLine}');
'Unable to create "List<$T>" in event content for the key "$key" at ${StackTrace.current.firstLine}',
);
return null;
}
}
@ -96,13 +99,17 @@ extension TryGetMapExtension on Map<String, Object?> {
return Map.from(value.cast<A, B>());
} catch (_) {
Logs().v(
'Unable to create "Map<$A,$B>" in event content for the key "$key" at ${StackTrace.current.firstLine}');
'Unable to create "Map<$A,$B>" in event content for the key "$key" at ${StackTrace.current.firstLine}',
);
return null;
}
}
A? tryGetFromJson<A>(String key, A Function(Map<String, Object?>) fromJson,
[TryGet log = TryGet.optional]) {
A? tryGetFromJson<A>(
String key,
A Function(Map<String, Object?>) fromJson, [
TryGet log = TryGet.optional,
]) {
final value = tryGetMap<String, Object?>(key, log);
return value != null ? fromJson(value) : null;

View File

@ -56,6 +56,9 @@ extension RecentEmojiExtension on Client {
if (userID == null) return;
final content = List.from(data.entries.map((e) => [e.key, e.value]));
return setAccountData(
userID!, 'io.element.recent_emoji', {'recent_emoji': content});
userID!,
'io.element.recent_emoji',
{'recent_emoji': content},
);
}
}

View File

@ -51,8 +51,12 @@ class MatrixWidget {
);
/// creates an `m.jitsi` [MatrixWidget]
factory MatrixWidget.jitsi(Room room, String name, Uri url,
{bool isAudioOnly = false}) =>
factory MatrixWidget.jitsi(
Room room,
String name,
Uri url, {
bool isAudioOnly = false,
}) =>
MatrixWidget(
room: room,
name: name,
@ -104,8 +108,10 @@ class MatrixWidget {
// `[a-zA-Z0-9_-]` as well as non string values
if (data != null)
...Map.from(data!)
..removeWhere((key, value) =>
!RegExp(r'^[\w-]+$').hasMatch(key) || !value is String)
..removeWhere(
(key, value) =>
!RegExp(r'^[\w-]+$').hasMatch(key) || !value is String,
)
..map((key, value) => MapEntry('\$key', value)),
};

View File

@ -29,7 +29,8 @@ extension UiaLogin on Client {
final requestUri = Uri(path: '_matrix/client/$pathVersion/login');
final request = Request('POST', baseUri!.resolveUri(requestUri));
request.headers['content-type'] = 'application/json';
request.bodyBytes = utf8.encode(jsonEncode({
request.bodyBytes = utf8.encode(
jsonEncode({
if (address != null) 'address': address,
if (deviceId != null) 'device_id': deviceId,
if (identifier != null) 'identifier': identifier.toJson(),
@ -42,7 +43,8 @@ extension UiaLogin on Client {
if (user != null) 'user': user,
if (auth != null) 'auth': auth.toJson(),
if (refreshToken != null) 'refresh_token': refreshToken,
}));
}),
);
final response = await httpClient.send(request);
final responseBody = await response.stream.toBytes();
if (response.statusCode != 200) unexpectedResponse(response, responseBody);

View File

@ -68,16 +68,21 @@ extension DehydratedDeviceMatrixApi on MatrixApi {
/// fetch events sent to a dehydrated device.
/// https://github.com/matrix-org/matrix-spec-proposals/pull/3814
Future<DehydratedDeviceEvents> getDehydratedDeviceEvents(String deviceId,
{String? nextBatch, int limit = 100}) async {
final response = await request(RequestType.POST,
Future<DehydratedDeviceEvents> getDehydratedDeviceEvents(
String deviceId, {
String? nextBatch,
int limit = 100,
}) async {
final response = await request(
RequestType.POST,
'/client/unstable/org.matrix.msc3814.v1/dehydrated_device/$deviceId/events',
query: {
'limit': limit.toString(),
},
data: {
if (nextBatch != null) 'next_batch': nextBatch,
});
},
);
return DehydratedDeviceEvents.fromJson(response);
}
}

View File

@ -62,7 +62,8 @@ extension DehydratedDeviceHandler on Client {
if (dehydratedDeviceIdentity == null ||
!dehydratedDeviceIdentity.hasValidSignatureChain()) {
Logs().w(
'Dehydrated device ${device.deviceId} is unknown or unverified, replacing it');
'Dehydrated device ${device.deviceId} is unknown or unverified, replacing it',
);
await _uploadNewDevice(secureStorage);
return;
}
@ -101,8 +102,10 @@ extension DehydratedDeviceHandler on Client {
DehydratedDeviceEvents? events;
do {
events = await getDehydratedDeviceEvents(device.deviceId,
nextBatch: events?.nextBatch);
events = await getDehydratedDeviceEvents(
device.deviceId,
nextBatch: events?.nextBatch,
);
for (final e in events.events ?? []) {
// We are only interested in roomkeys, which ALWAYS need to be encrypted.
@ -144,15 +147,21 @@ extension DehydratedDeviceHandler on Client {
Logs().i('Dehydrated device key not found, creating new one.');
pickleDeviceKey = base64.encode(uc.secureRandomBytes(128));
await secureStorage.store(
_ssssSecretNameForDehydratedDevice, pickleDeviceKey);
_ssssSecretNameForDehydratedDevice,
pickleDeviceKey,
);
}
const chars =
'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890';
final rnd = Random();
final deviceIdSuffix = String.fromCharCodes(Iterable.generate(
10, (_) => chars.codeUnitAt(rnd.nextInt(chars.length))));
final deviceIdSuffix = String.fromCharCodes(
Iterable.generate(
10,
(_) => chars.codeUnitAt(rnd.nextInt(chars.length)),
),
);
final String device = 'FAM$deviceIdSuffix';
// Generate a new olm account for the dehydrated device.

View File

@ -6,12 +6,12 @@ abstract class CuteEventContent {
static Map<String, dynamic> get googlyEyes => {
'msgtype': CuteEventContent.eventType,
'cute_type': 'googly_eyes',
'body': '👀'
'body': '👀',
};
static Map<String, dynamic> get cuddle => {
'msgtype': CuteEventContent.eventType,
'cute_type': 'cuddle',
'body': '😊'
'body': '😊',
};
static Map<String, dynamic> get hug => {
'msgtype': CuteEventContent.eventType,

View File

@ -41,11 +41,13 @@ extension MscUnpublishedCustomRefreshTokenLifetime on MatrixApi {
final requestUri = Uri(path: '_matrix/client/v3/refresh');
final request = Request('POST', baseUri!.resolveUri(requestUri));
request.headers['content-type'] = 'application/json';
request.bodyBytes = utf8.encode(jsonEncode({
request.bodyBytes = utf8.encode(
jsonEncode({
'refresh_token': refreshToken,
if (refreshTokenLifetimeMs != null)
customFieldKey: refreshTokenLifetimeMs,
}));
}),
);
final response = await httpClient.send(request);
final responseBody = await response.stream.toBytes();
if (response.statusCode != 200) unexpectedResponse(response, responseBody);

View File

@ -111,7 +111,9 @@ class Client extends MatrixApi {
@Deprecated('Use [nativeImplementations] instead')
Future<T> runInBackground<T, U>(
FutureOr<T> Function(U arg) function, U arg) async {
FutureOr<T> Function(U arg) function,
U arg,
) async {
final compute = this.compute;
if (compute != null) {
return await compute(function, arg);
@ -143,7 +145,8 @@ class Client extends MatrixApi {
}
Future<MatrixImageFileResizedResponse?> Function(
MatrixImageFileResizeArguments)? customImageResizer;
MatrixImageFileResizeArguments,
)? customImageResizer;
/// Create a client
/// [clientName] = unique identifier of this client
@ -246,7 +249,10 @@ class Client extends MatrixApi {
: nativeImplementations,
super(
httpClient: FixedTimeoutHttpClient(
httpClient ?? http.Client(), defaultNetworkRequestTimeout)) {
httpClient ?? http.Client(),
defaultNetworkRequestTimeout,
),
) {
if (logLevel != null) Logs().level = logLevel;
importantStateEvents.addAll([
EventTypes.RoomName,
@ -402,7 +408,8 @@ class Client extends MatrixApi {
_accountData[EventTypes.PushRules]
?.content
.tryGetMap<String, Object?>('global') ??
{});
{},
);
_pushruleEvaluator = PushruleEvaluator.fromRuleset(ruleset);
}
@ -476,8 +483,12 @@ class Client extends MatrixApi {
String MatrixIdOrDomain,
) async {
try {
final response = await httpClient.get(Uri.https(
MatrixIdOrDomain.domain ?? '', '/.well-known/matrix/client'));
final response = await httpClient.get(
Uri.https(
MatrixIdOrDomain.domain ?? '',
'/.well-known/matrix/client',
),
);
var respBody = response.body;
try {
respBody = utf8.decode(response.bodyBytes);
@ -491,7 +502,8 @@ class Client extends MatrixApi {
// provide a reasonable fallback.
return DiscoveryInformation(
mHomeserver: HomeserverInformation(
baseUrl: Uri.https(MatrixIdOrDomain.domain ?? '', '')),
baseUrl: Uri.https(MatrixIdOrDomain.domain ?? '', ''),
),
);
}
}
@ -539,7 +551,9 @@ class Client extends MatrixApi {
final loginTypes = await getLoginFlows() ?? [];
if (!loginTypes.any((f) => supportedLoginTypes.contains(f.type))) {
throw BadServerLoginTypesException(
loginTypes.map((f) => f.type).toSet(), supportedLoginTypes);
loginTypes.map((f) => f.type).toSet(),
supportedLoginTypes,
);
}
return (wellKnown, versions, loginTypes);
@ -602,7 +616,8 @@ class Client extends MatrixApi {
final homeserver = this.homeserver;
if (accessToken == null || deviceId_ == null || homeserver == null) {
throw Exception(
'Registered but token, device ID, user ID or homeserver is null.');
'Registered but token, device ID, user ID or homeserver is null.',
);
}
final expiresInMs = response.expiresInMs;
final tokenExpiresAt = expiresInMs == null
@ -616,7 +631,8 @@ class Client extends MatrixApi {
newUserID: userId,
newHomeserver: homeserver,
newDeviceName: initialDeviceDisplayName ?? '',
newDeviceID: deviceId_);
newDeviceID: deviceId_,
);
return response;
}
@ -726,7 +742,8 @@ class Client extends MatrixApi {
/// Run any request and react on user interactive authentication flows here.
Future<T> uiaRequestBackground<T>(
Future<T> Function(AuthenticationData? auth) request) {
Future<T> Function(AuthenticationData? auth) request,
) {
final completer = Completer<T>();
UiaRequest? uia;
uia = UiaRequest(
@ -788,12 +805,14 @@ class Client extends MatrixApi {
if (enableEncryption) {
initialState ??= [];
if (!initialState.any((s) => s.type == EventTypes.Encryption)) {
initialState.add(StateEvent(
initialState.add(
StateEvent(
content: {
'algorithm': supportedGroupEncryptionAlgorithms.first,
},
type: EventTypes.Encryption,
));
),
);
}
}
@ -840,23 +859,27 @@ class Client extends MatrixApi {
if (enableEncryption) {
initialState ??= [];
if (!initialState.any((s) => s.type == EventTypes.Encryption)) {
initialState.add(StateEvent(
initialState.add(
StateEvent(
content: {
'algorithm': supportedGroupEncryptionAlgorithms.first,
},
type: EventTypes.Encryption,
));
),
);
}
}
if (historyVisibility != null) {
initialState ??= [];
if (!initialState.any((s) => s.type == EventTypes.HistoryVisibility)) {
initialState.add(StateEvent(
initialState.add(
StateEvent(
content: {
'history_visibility': historyVisibility.text,
},
type: EventTypes.HistoryVisibility,
));
),
);
}
}
if (groupCall) {
@ -888,8 +911,12 @@ class Client extends MatrixApi {
/// Wait for the room to appear into the enabled section of the room sync.
/// By default, the function will listen for room in invite, join and leave
/// sections of the sync.
Future<SyncUpdate> waitForRoomInSync(String roomId,
{bool join = false, bool invite = false, bool leave = false}) async {
Future<SyncUpdate> waitForRoomInSync(
String roomId, {
bool join = false,
bool invite = false,
bool leave = false,
}) async {
if (!join && !invite && !leave) {
join = true;
invite = true;
@ -897,10 +924,12 @@ class Client extends MatrixApi {
}
// Wait for the next sync where this room appears.
final syncUpdate = await onSync.stream.firstWhere((sync) =>
final syncUpdate = await onSync.stream.firstWhere(
(sync) =>
invite && (sync.rooms?.invite?.containsKey(roomId) ?? false) ||
join && (sync.rooms?.join?.containsKey(roomId) ?? false) ||
leave && (sync.rooms?.leave?.containsKey(roomId) ?? false));
leave && (sync.rooms?.leave?.containsKey(roomId) ?? false),
);
// Wait for this sync to be completely processed.
await onSyncStatus.stream.firstWhere(
@ -927,15 +956,16 @@ class Client extends MatrixApi {
/// room as a space with `room.toSpace()`.
///
/// https://github.com/matrix-org/matrix-doc/blob/matthew/msc1772/proposals/1772-groups-as-rooms.md
Future<String> createSpace(
{String? name,
Future<String> createSpace({
String? name,
String? topic,
Visibility visibility = Visibility.public,
String? spaceAliasName,
List<String>? invite,
List<Invite3pid>? invite3pid,
String? roomVersion,
bool waitForSync = false}) async {
bool waitForSync = false,
}) async {
final id = await createRoom(
name: name,
topic: topic,
@ -964,8 +994,9 @@ class Client extends MatrixApi {
/// from a room where the user exists. Set `useServerCache` to true to get any
/// prior value from this function
@Deprecated('Use fetchOwnProfile() instead')
Future<Profile> fetchOwnProfileFromServer(
{bool useServerCache = false}) async {
Future<Profile> fetchOwnProfileFromServer({
bool useServerCache = false,
}) async {
try {
return await getProfileFromUserId(
userID!,
@ -974,7 +1005,8 @@ class Client extends MatrixApi {
);
} catch (e) {
Logs().w(
'[Matrix] getting profile from homeserver failed, falling back to first room with required profile');
'[Matrix] getting profile from homeserver failed, falling back to first room with required profile',
);
return await getProfileFromUserId(
userID!,
getFromRooms: true,
@ -1111,13 +1143,15 @@ class Client extends MatrixApi {
Future<List<ArchivedRoom>> loadArchiveWithTimeline() async {
_archivedRooms.clear();
final filter = jsonEncode(Filter(
final filter = jsonEncode(
Filter(
room: RoomFilter(
state: StateFilter(lazyLoadMembers: true),
includeLeave: true,
timeline: StateFilter(limit: 10),
),
).toJson());
).toJson(),
);
final syncResp = await sync(
filter: filter,
@ -1138,9 +1172,10 @@ class Client extends MatrixApi {
// best indicator we have to sort them. For archived rooms where we don't
// have any, we move them to the bottom.
final beginningOfTime = DateTime.fromMillisecondsSinceEpoch(0);
_archivedRooms.sort((b, a) =>
(a.room.lastEvent?.originServerTs ?? beginningOfTime)
.compareTo(b.room.lastEvent?.originServerTs ?? beginningOfTime));
_archivedRooms.sort(
(b, a) => (a.room.lastEvent?.originServerTs ?? beginningOfTime)
.compareTo(b.room.lastEvent?.originServerTs ?? beginningOfTime),
);
return _archivedRooms;
}
@ -1173,21 +1208,30 @@ class Client extends MatrixApi {
.toList() // we display the event in the other sence
.map((e) => Event.fromMatrixEvent(e, archivedRoom))
.toList() ??
[]));
[],
),
);
archivedRoom.prev_batch = update.timeline?.prevBatch;
final stateEvents = roomUpdate.state;
if (stateEvents != null) {
await _handleRoomEvents(archivedRoom, stateEvents, EventUpdateType.state,
store: false);
await _handleRoomEvents(
archivedRoom,
stateEvents,
EventUpdateType.state,
store: false,
);
}
final timelineEvents = roomUpdate.timeline?.events;
if (timelineEvents != null) {
await _handleRoomEvents(archivedRoom, timelineEvents.reversed.toList(),
await _handleRoomEvents(
archivedRoom,
timelineEvents.reversed.toList(),
EventUpdateType.timeline,
store: false);
store: false,
);
}
for (var i = 0; i < timeline.events.length; i++) {
@ -1233,11 +1277,12 @@ class Client extends MatrixApi {
/// repository APIs, for example, proxies may enforce a lower upload size limit
/// than is advertised by the server on this endpoint.
@override
Future<MediaConfig> getConfig() => _serverConfigCache
.tryFetch(() async => (await authenticatedMediaSupported())
Future<MediaConfig> getConfig() => _serverConfigCache.tryFetch(
() async => (await authenticatedMediaSupported())
? getConfigAuthed()
// ignore: deprecated_member_use_from_same_package
: super.getConfig());
: super.getConfig(),
);
///
///
@ -1461,8 +1506,11 @@ class Client extends MatrixApi {
/// Returns the mxc url. Please note, that this does **not** encrypt
/// the content. Use `Room.sendFileEvent()` for end to end encryption.
@override
Future<Uri> uploadContent(Uint8List file,
{String? filename, String? contentType}) async {
Future<Uri> uploadContent(
Uint8List file, {
String? filename,
String? contentType,
}) async {
final mediaConfig = await getConfig();
final maxMediaSize = mediaConfig.mUploadSize;
if (maxMediaSize != null && maxMediaSize < file.lengthInBytes) {
@ -1476,7 +1524,10 @@ class Client extends MatrixApi {
final database = this.database;
if (database != null && file.length <= database.maxFileSize) {
await database.storeFile(
mxc, file, DateTime.now().millisecondsSinceEpoch);
mxc,
file,
DateTime.now().millisecondsSinceEpoch,
);
}
return mxc;
}
@ -1584,10 +1635,10 @@ class Client extends MatrixApi {
static const Set<String> supportedVersions = {'v1.1', 'v1.2'};
static const List<String> supportedDirectEncryptionAlgorithms = [
AlgorithmTypes.olmV1Curve25519AesSha2
AlgorithmTypes.olmV1Curve25519AesSha2,
];
static const List<String> supportedGroupEncryptionAlgorithms = [
AlgorithmTypes.megolmV1AesSha2
AlgorithmTypes.megolmV1AesSha2,
];
static const int defaultThumbnailSize = 800;
@ -1626,7 +1677,8 @@ class Client extends MatrixApi {
/// Callback will be called on presences.
@Deprecated(
'Deprecated, use onPresenceChanged instead which has a timestamp.')
'Deprecated, use onPresenceChanged instead which has a timestamp.',
)
final CachedStreamController<Presence> onPresence = CachedStreamController();
/// Callback will be called on presence updates.
@ -1688,7 +1740,8 @@ class Client extends MatrixApi {
if (!isLogged()) {
if (database == null) {
throw Exception(
'Can not execute getEventByPushNotification() without a database');
'Can not execute getEventByPushNotification() without a database',
);
}
final clientInfoMap = await database.getClient(clientName);
final token = clientInfoMap?.tryGet<String>('token');
@ -1715,7 +1768,8 @@ class Client extends MatrixApi {
final roomName = notification.roomName;
final roomAlias = notification.roomAlias;
if (roomName != null) {
room.setState(Event(
room.setState(
Event(
eventId: 'TEMP',
stateKey: '',
type: EventTypes.RoomName,
@ -1723,10 +1777,12 @@ class Client extends MatrixApi {
room: room,
senderId: 'UNKNOWN',
originServerTs: DateTime.now(),
));
),
);
}
if (roomAlias != null) {
room.setState(Event(
room.setState(
Event(
eventId: 'TEMP',
stateKey: '',
type: EventTypes.RoomCanonicalAlias,
@ -1734,7 +1790,8 @@ class Client extends MatrixApi {
room: room,
senderId: 'UNKNOWN',
originServerTs: DateTime.now(),
));
),
);
}
// Load the event from the notification or from the database or from server:
@ -1763,9 +1820,11 @@ class Client extends MatrixApi {
// No access to the MatrixEvent. Search in /notifications
final notificationsResponse = await getNotifications();
matrixEvent ??= notificationsResponse.notifications
.firstWhereOrNull((notification) =>
.firstWhereOrNull(
(notification) =>
notification.roomId == roomId &&
notification.event.eventId == eventId)
notification.event.eventId == eventId,
)
?.event;
}
@ -1834,7 +1893,8 @@ class Client extends MatrixApi {
type: EventUpdateType.timeline,
content: event.toJson(),
),
this);
this,
);
});
}
@ -2196,8 +2256,9 @@ class Client extends MatrixApi {
/// Checks if the token expires in under [expiresIn] time and calls the
/// given `onSoftLogout()` if so. You have to provide `onSoftLogout` in the
/// Client constructor. Otherwise this will do nothing.
Future<void> ensureNotSoftLoggedOut(
[Duration expiresIn = const Duration(minutes: 1)]) async {
Future<void> ensureNotSoftLoggedOut([
Duration expiresIn = const Duration(minutes: 1),
]) async {
final tokenExpiresAt = accessTokenExpiresAt;
if (onSoftLogout != null &&
tokenExpiresAt != null &&
@ -2284,7 +2345,8 @@ class Client extends MatrixApi {
onSyncStatus.add(SyncStatusUpdate(SyncStatus.cleaningUp));
// ignore: unawaited_futures
database?.deleteOldFiles(
DateTime.now().subtract(Duration(days: 30)).millisecondsSinceEpoch);
DateTime.now().subtract(Duration(days: 30)).millisecondsSinceEpoch,
);
await updateUserDeviceKeys();
if (encryptionEnabled) {
encryption?.onSync();
@ -2298,8 +2360,12 @@ class Client extends MatrixApi {
_retryDelay = Future.value();
onSyncStatus.add(SyncStatusUpdate(SyncStatus.finished));
} on MatrixException catch (e, s) {
onSyncStatus.add(SyncStatusUpdate(SyncStatus.error,
error: SdkError(exception: e, stackTrace: s)));
onSyncStatus.add(
SyncStatusUpdate(
SyncStatus.error,
error: SdkError(exception: e, stackTrace: s),
),
);
if (e.error == MatrixError.M_UNKNOWN_TOKEN) {
if (e.raw.tryGet<bool>('soft_logout') == true) {
Logs().w(
@ -2313,14 +2379,24 @@ class Client extends MatrixApi {
}
} on SyncConnectionException catch (e, s) {
Logs().w('Syncloop failed: Client has not connection to the server');
onSyncStatus.add(SyncStatusUpdate(SyncStatus.error,
error: SdkError(exception: e, stackTrace: s)));
onSyncStatus.add(
SyncStatusUpdate(
SyncStatus.error,
error: SdkError(exception: e, stackTrace: s),
),
);
} catch (e, s) {
if (!isLogged() || _disposed || _aborted) return;
Logs().e('Error during processing events', e, s);
onSyncStatus.add(SyncStatusUpdate(SyncStatus.error,
onSyncStatus.add(
SyncStatusUpdate(
SyncStatus.error,
error: SdkError(
exception: e is Exception ? e : Exception(e), stackTrace: s)));
exception: e is Exception ? e : Exception(e),
stackTrace: s,
),
),
);
}
}
@ -2386,7 +2462,9 @@ class Client extends MatrixApi {
}
if (encryptionEnabled) {
encryption?.handleDeviceOneTimeKeysCount(
sync.deviceOneTimeKeysCount, sync.deviceUnusedFallbackKeyTypes);
sync.deviceOneTimeKeysCount,
sync.deviceUnusedFallbackKeyTypes,
);
}
_sortRooms();
onSync.add(sync);
@ -2453,7 +2531,8 @@ class Client extends MatrixApi {
for (final event in _eventsPendingDecryption) {
if (event.event.roomID != roomId) continue;
if (!sessionIds.contains(
event.event.content['content']?['session_id'])) continue;
event.event.content['content']?['session_id'],
)) continue;
final decryptedEvent = await event.event.decrypt(room);
if (decryptedEvent.content.tryGet<String>('type') !=
@ -2463,25 +2542,35 @@ class Client extends MatrixApi {
}
await _handleRoomEvents(
room, events, EventUpdateType.decryptedTimelineQueue);
room,
events,
EventUpdateType.decryptedTimelineQueue,
);
_eventsPendingDecryption.removeWhere((e) => events.any(
_eventsPendingDecryption.removeWhere(
(e) => events.any(
(decryptedEvent) =>
decryptedEvent.content['event_id'] ==
e.event.content['event_id']));
e.event.content['event_id'],
),
);
}
}
_eventsPendingDecryption.removeWhere((e) => e.timedOut);
}
Future<void> _handleRooms(Map<String, SyncRoomUpdate> rooms,
{Direction? direction}) async {
Future<void> _handleRooms(
Map<String, SyncRoomUpdate> rooms, {
Direction? direction,
}) async {
var handledRooms = 0;
for (final entry in rooms.entries) {
onSyncStatus.add(SyncStatusUpdate(
onSyncStatus.add(
SyncStatusUpdate(
SyncStatus.processing,
progress: ++handledRooms / rooms.length,
));
),
);
final id = entry.key;
final syncRoomUpdate = entry.value;
@ -2539,19 +2628,30 @@ class Client extends MatrixApi {
if (syncRoomUpdate is LeftRoomUpdate) {
final timelineEvents = syncRoomUpdate.timeline?.events;
if (timelineEvents != null && timelineEvents.isNotEmpty) {
await _handleRoomEvents(room, timelineEvents, timelineUpdateType,
store: false);
await _handleRoomEvents(
room,
timelineEvents,
timelineUpdateType,
store: false,
);
}
final accountData = syncRoomUpdate.accountData;
if (accountData != null && accountData.isNotEmpty) {
await _handleRoomEvents(
room, accountData, EventUpdateType.accountData,
store: false);
room,
accountData,
EventUpdateType.accountData,
store: false,
);
}
final state = syncRoomUpdate.state;
if (state != null && state.isNotEmpty) {
await _handleRoomEvents(room, state, EventUpdateType.state,
store: false);
await _handleRoomEvents(
room,
state,
EventUpdateType.state,
store: false,
);
}
}
@ -2593,9 +2693,10 @@ class Client extends MatrixApi {
type: LatestReceiptState.eventType,
roomId: room.id,
content: receiptStateContent.toJson(),
)
),
],
EventUpdateType.accountData);
EventUpdateType.accountData,
);
}
}
@ -2603,8 +2704,11 @@ class Client extends MatrixApi {
final List<_EventPendingDecryption> _eventsPendingDecryption = [];
Future<void> _handleRoomEvents(
Room room, List<BasicEvent> events, EventUpdateType type,
{bool store = true}) async {
Room room,
List<BasicEvent> events,
EventUpdateType type, {
bool store = true,
}) async {
// Calling events can be omitted if they are outdated from the same sync. So
// we collect them first before we handle them.
final callEvents = <Event>[];
@ -2629,10 +2733,15 @@ class Client extends MatrixApi {
// if the event failed to decrypt, add it to the queue
if (update.content.tryGet<String>('type') == EventTypes.Encrypted) {
_eventsPendingDecryption.add(_EventPendingDecryption(EventUpdate(
_eventsPendingDecryption.add(
_EventPendingDecryption(
EventUpdate(
roomID: update.roomID,
type: EventUpdateType.decryptedTimelineQueue,
content: update.content)));
content: update.content,
),
),
);
}
}
@ -2689,7 +2798,9 @@ class Client extends MatrixApi {
DateTime lastStaleCallRun = DateTime(0);
Future<Room> _updateRoomsByRoomUpdate(
String roomId, SyncRoomUpdate chatUpdate) async {
String roomId,
SyncRoomUpdate chatUpdate,
) async {
// Update the chat list item.
// Search the room in the rooms
final roomIndex = rooms.indexWhere((r) => r.id == roomId);
@ -2765,7 +2876,8 @@ class Client extends MatrixApi {
if ((chatUpdate.timeline?.limited ?? false) &&
requestHistoryOnLimitedTimeline) {
Logs().v(
'Limited timeline for ${rooms[roomIndex].id} request history now');
'Limited timeline for ${rooms[roomIndex].id} request history now',
);
runInRoot(rooms[roomIndex].requestHistory);
}
}
@ -2801,9 +2913,10 @@ class Client extends MatrixApi {
if (event.type == EventTypes.Redaction &&
({
room.lastEvent?.eventId,
room.lastEvent?.relationshipEventId
room.lastEvent?.relationshipEventId,
}.contains(
event.redacts ?? event.content.tryGet<String>('redacts')))) {
event.redacts ?? event.content.tryGet<String>('redacts'),
))) {
room.lastEvent?.setRedactionEvent(event);
break;
}
@ -2994,7 +3107,10 @@ class Client extends MatrixApi {
// Set the new device key for this device
final entry = DeviceKeys.fromMatrixDeviceKeys(
rawDeviceKeyEntry.value, this, oldKeys[deviceId]?.lastActive);
rawDeviceKeyEntry.value,
this,
oldKeys[deviceId]?.lastActive,
);
final ed25519Key = entry.ed25519Key;
final curve25519Key = entry.curve25519Key;
if (entry.isValid &&
@ -3008,24 +3124,30 @@ class Client extends MatrixApi {
if (oldPublicKeys != null &&
oldPublicKeys != curve25519Key + ed25519Key) {
Logs().w(
'Already seen Device ID has been added again. This might be an attack!');
'Already seen Device ID has been added again. This might be an attack!',
);
continue;
}
final oldDeviceId = await database.publicKeySeen(ed25519Key);
if (oldDeviceId != null && oldDeviceId != deviceId) {
Logs().w(
'Already seen ED25519 has been added again. This might be an attack!');
'Already seen ED25519 has been added again. This might be an attack!',
);
continue;
}
final oldDeviceId2 =
await database.publicKeySeen(curve25519Key);
if (oldDeviceId2 != null && oldDeviceId2 != deviceId) {
Logs().w(
'Already seen Curve25519 has been added again. This might be an attack!');
'Already seen Curve25519 has been added again. This might be an attack!',
);
continue;
}
await database.addSeenDeviceId(
userId, deviceId, curve25519Key + ed25519Key);
userId,
deviceId,
curve25519Key + ed25519Key,
);
await database.addSeenPublicKey(ed25519Key, deviceId);
await database.addSeenPublicKey(curve25519Key, deviceId);
}
@ -3048,14 +3170,16 @@ class Client extends MatrixApi {
// Always trust the own device
entry.setDirectVerified(true);
}
dbActions.add(() => database.storeUserDeviceKey(
dbActions.add(
() => database.storeUserDeviceKey(
userId,
deviceId,
json.encode(entry.toJson()),
entry.directVerified,
entry.blocked,
entry.lastActive.millisecondsSinceEpoch,
));
),
);
} else if (oldKeys.containsKey(deviceId)) {
// This shouldn't ever happen. The same device ID has gotten
// a new public key. So we ignore the update. TODO: ask krille
@ -3106,12 +3230,16 @@ class Client extends MatrixApi {
} else {
// There is a previous cross-signing key with this usage, that we no
// longer need/use. Clear it from the database.
dbActions.add(() =>
database.removeUserCrossSigningKey(userId, oldEntry.key));
dbActions.add(
() =>
database.removeUserCrossSigningKey(userId, oldEntry.key),
);
}
}
final entry = CrossSigningKey.fromMatrixCrossSigningKey(
crossSigningKeyListEntry.value, this);
crossSigningKeyListEntry.value,
this,
);
final publicKey = entry.publicKey;
if (entry.isValid && publicKey != null) {
final oldKey = oldKeys[publicKey];
@ -3129,13 +3257,15 @@ class Client extends MatrixApi {
// if we should instead use the new key with unknown verified / blocked status
userKeys.crossSigningKeys[publicKey] = oldKey;
}
dbActions.add(() => database.storeUserCrossSigningKey(
dbActions.add(
() => database.storeUserCrossSigningKey(
userId,
publicKey,
json.encode(entry.toJson()),
entry.directVerified,
entry.blocked,
));
),
);
}
_userDeviceKeys[userId]?.outdated = false;
dbActions
@ -3182,17 +3312,24 @@ class Client extends MatrixApi {
for (final entry in entries) {
// Convert the Json Map to the correct format regarding
// https: //matrix.org/docs/spec/client_server/r0.6.1#put-matrix-client-r0-sendtodevice-eventtype-txnid
final data = entry.content.map((k, v) =>
MapEntry<String, Map<String, Map<String, dynamic>>>(
final data = entry.content.map(
(k, v) => MapEntry<String, Map<String, Map<String, dynamic>>>(
k,
(v as Map).map((k, v) => MapEntry<String, Map<String, dynamic>>(
k, Map<String, dynamic>.from(v)))));
(v as Map).map(
(k, v) => MapEntry<String, Map<String, dynamic>>(
k,
Map<String, dynamic>.from(v),
),
),
),
);
try {
await super.sendToDevice(entry.type, entry.txnId, data);
} on MatrixException catch (e) {
Logs().w(
'[To-Device] failed to to_device message from the queue to the server. Ignoring error: $e');
'[To-Device] failed to to_device message from the queue to the server. Ignoring error: $e',
);
Logs().w('Payload: $data');
}
await database.deleteFromToDeviceQueue(entry.id);
@ -3215,12 +3352,16 @@ class Client extends MatrixApi {
Logs().w(
'[Client] Problem while sending to_device event, retrying later...',
e,
s);
s,
);
final database = this.database;
if (database != null) {
_toDeviceQueueNeedsProcessing = true;
await database.insertIntoToDeviceQueue(
eventType, txnId, json.encode(messages));
eventType,
txnId,
json.encode(messages),
);
}
rethrow;
}
@ -3240,7 +3381,10 @@ class Client extends MatrixApi {
data[user] = {'*': message};
}
await sendToDevice(
eventType, messageId ?? generateUniqueTransactionId(), data);
eventType,
messageId ?? generateUniqueTransactionId(),
data,
);
return;
}
@ -3259,10 +3403,12 @@ class Client extends MatrixApi {
// Don't send this message to blocked devices, and if specified onlyVerified
// then only send it to verified devices
if (deviceKeys.isNotEmpty) {
deviceKeys.removeWhere((DeviceKeys deviceKeys) =>
deviceKeys.removeWhere(
(DeviceKeys deviceKeys) =>
deviceKeys.blocked ||
(deviceKeys.userId == userID && deviceKeys.deviceId == deviceID) ||
(onlyVerified && !deviceKeys.verified));
(onlyVerified && !deviceKeys.verified),
);
if (deviceKeys.isEmpty) return;
}
@ -3285,7 +3431,10 @@ class Client extends MatrixApi {
);
eventType = EventTypes.Encrypted;
await sendToDevice(
eventType, messageId ?? generateUniqueTransactionId(), data);
eventType,
messageId ?? generateUniqueTransactionId(),
data,
);
} finally {
_sendToDeviceEncryptedLock.unlock(deviceKeys);
}
@ -3303,8 +3452,10 @@ class Client extends MatrixApi {
if (!encryptionEnabled) return;
// be sure to copy our device keys list
deviceKeys = List<DeviceKeys>.from(deviceKeys);
deviceKeys.removeWhere((DeviceKeys k) =>
k.blocked || (k.userId == userID && k.deviceId == deviceID));
deviceKeys.removeWhere(
(DeviceKeys k) =>
k.blocked || (k.userId == userID && k.deviceId == deviceID),
);
if (deviceKeys.isEmpty) return;
message = message.copy(); // make sure we deep-copy the message
// make sure all the olm sessions are loaded from database
@ -3322,9 +3473,8 @@ class Client extends MatrixApi {
Logs().v('Sending chunk $i...');
final chunk = deviceKeys.sublist(
i,
i + chunkSize > deviceKeys.length
? deviceKeys.length
: i + chunkSize);
i + chunkSize > deviceKeys.length ? deviceKeys.length : i + chunkSize,
);
// and send
await sendToDeviceEncrypted(chunk, eventType, message);
}
@ -3340,7 +3490,8 @@ class Client extends MatrixApi {
i,
i + chunkSize > deviceKeys.length
? deviceKeys.length
: i + chunkSize);
: i + chunkSize,
);
// and send
await sendToDeviceEncrypted(chunk, eventType, message);
}
@ -3380,11 +3531,13 @@ class Client extends MatrixApi {
/// preference is always given to via over serverName, irrespective of what field
/// you are trying to use
@override
Future<String> joinRoom(String roomIdOrAlias,
{List<String>? serverName,
Future<String> joinRoom(
String roomIdOrAlias, {
List<String>? serverName,
List<String>? via,
String? reason,
ThirdPartySigned? thirdPartySigned}) =>
ThirdPartySigned? thirdPartySigned,
}) =>
super.joinRoom(
roomIdOrAlias,
serverName: via ?? serverName,
@ -3409,8 +3562,11 @@ class Client extends MatrixApi {
password: oldPassword,
);
}
await super.changePassword(newPassword,
auth: auth, logoutDevices: logoutDevices);
await super.changePassword(
newPassword,
auth: auth,
logoutDevices: logoutDevices,
);
} on MatrixException catch (matrixException) {
if (!matrixException.requireAdditionalAuthentication) {
rethrow;
@ -3453,12 +3609,13 @@ class Client extends MatrixApi {
}
/// A list of mxids of users who are ignored.
List<String> get ignoredUsers =>
List<String>.from(_accountData['m.ignored_user_list']
List<String> get ignoredUsers => List<String>.from(
_accountData['m.ignored_user_list']
?.content
.tryGetMap<String, Object?>('ignored_users')
?.keys ??
<String>[]);
<String>[],
);
/// Ignore another user. This will clear the local cached messages to
/// hide all previous messages from this user.
@ -3468,7 +3625,8 @@ class Client extends MatrixApi {
}
await setAccountData(userID!, 'm.ignored_user_list', {
'ignored_users': Map.fromEntries(
(ignoredUsers..add(userId)).map((key) => MapEntry(key, {}))),
(ignoredUsers..add(userId)).map((key) => MapEntry(key, {})),
),
});
await clearCache();
return;
@ -3485,7 +3643,8 @@ class Client extends MatrixApi {
}
await setAccountData(userID!, 'm.ignored_user_list', {
'ignored_users': Map.fromEntries(
(ignoredUsers..remove(userId)).map((key) => MapEntry(key, {}))),
(ignoredUsers..remove(userId)).map((key) => MapEntry(key, {})),
),
});
await clearCache();
return;
@ -3637,7 +3796,8 @@ class Client extends MatrixApi {
final pubKey = crossSigningKey.publicKey;
if (pubKey != null) {
Logs().d(
'Migrate cross signing key with usage ${crossSigningKey.usage} and verified ${crossSigningKey.directVerified}...');
'Migrate cross signing key with usage ${crossSigningKey.usage} and verified ${crossSigningKey.directVerified}...',
);
await database.storeUserCrossSigningKey(
userId,
pubKey,
@ -3768,7 +3928,7 @@ class FileTooBigMatrixException extends MatrixException {
: super.fromJson({
'errcode': MatrixError.M_TOO_LARGE,
'error':
'File size ${_formatFileSize(actualFileSize)} exceeds allowed maximum of ${_formatFileSize(maxFileSize)}'
'File size ${_formatFileSize(actualFileSize)} exceeds allowed maximum of ${_formatFileSize(maxFileSize)}',
});
@override

View File

@ -59,8 +59,11 @@ abstract class DatabaseApi {
Future<List<Room>> getRoomList(Client client);
Future<Room?> getSingleRoom(Client client, String roomId,
{bool loadImportantStates = true});
Future<Room?> getSingleRoom(
Client client,
String roomId, {
bool loadImportantStates = true,
});
Future<Map<String, BasicEvent>> getAccountData();
@ -86,7 +89,9 @@ abstract class DatabaseApi {
Future<CachedProfileInformation?> getUserProfile(String userId);
Future<void> storeUserProfile(
String userId, CachedProfileInformation profile);
String userId,
CachedProfileInformation profile,
);
Future<void> markUserProfileAsOutdated(String userId);
@ -316,7 +321,10 @@ abstract class DatabaseApi {
Future<List<StoredInboundGroupSession>> getInboundGroupSessionsToUpload();
Future<void> addSeenDeviceId(
String userId, String deviceId, String publicKeys);
String userId,
String deviceId,
String publicKeys,
);
Future<void> addSeenPublicKey(String publicKey, String deviceId);

View File

@ -35,7 +35,8 @@ import 'package:matrix/src/utils/run_benchmarked.dart';
/// This database does not support file caching!
@Deprecated(
'Use [MatrixSdkDatabase] instead. Don\'t forget to properly migrate!')
'Use [MatrixSdkDatabase] instead. Don\'t forget to properly migrate!',
)
class HiveCollectionsDatabase extends DatabaseApi {
static const int version = 7;
final String name;
@ -374,8 +375,10 @@ class HiveCollectionsDatabase extends DatabaseApi {
.toList();
final rawEvents = await _eventsBox.getAll(keys);
return rawEvents
.map((rawEvent) =>
rawEvent != null ? Event.fromJson(copyMap(rawEvent), room) : null)
.map(
(rawEvent) =>
rawEvent != null ? Event.fromJson(copyMap(rawEvent), room) : null,
)
.whereNotNull()
.toList();
}
@ -391,7 +394,8 @@ class HiveCollectionsDatabase extends DatabaseApi {
// Get the synced event IDs from the store
final timelineKey = TupleKey(room.id, '').toString();
final timelineEventIds = List<String>.from(
(await _timelineFragmentsBox.get(timelineKey)) ?? []);
(await _timelineFragmentsBox.get(timelineKey)) ?? [],
);
// Get the local stored SENDING events from the store
late final List<String> sendingEventIds;
@ -400,17 +404,20 @@ class HiveCollectionsDatabase extends DatabaseApi {
} else {
final sendingTimelineKey = TupleKey(room.id, 'SENDING').toString();
sendingEventIds = List<String>.from(
(await _timelineFragmentsBox.get(sendingTimelineKey)) ?? []);
(await _timelineFragmentsBox.get(sendingTimelineKey)) ?? [],
);
}
// Combine those two lists while respecting the start and limit parameters.
final end = min(timelineEventIds.length,
start + (limit ?? timelineEventIds.length));
final end = min(
timelineEventIds.length,
start + (limit ?? timelineEventIds.length),
);
final eventIds = List<String>.from([
...sendingEventIds,
...(start < timelineEventIds.length && !onlySending
? timelineEventIds.getRange(start, end).toList()
: [])
: []),
]);
return await _getEventsByIds(eventIds, room);
@ -427,7 +434,8 @@ class HiveCollectionsDatabase extends DatabaseApi {
// Get the synced event IDs from the store
final timelineKey = TupleKey(room.id, '').toString();
final timelineEventIds = List<String>.from(
(await _timelineFragmentsBox.get(timelineKey)) ?? []);
(await _timelineFragmentsBox.get(timelineKey)) ?? [],
);
// Get the local stored SENDING events from the store
late final List<String> sendingEventIds;
@ -436,7 +444,8 @@ class HiveCollectionsDatabase extends DatabaseApi {
} else {
final sendingTimelineKey = TupleKey(room.id, 'SENDING').toString();
sendingEventIds = List<String>.from(
(await _timelineFragmentsBox.get(sendingTimelineKey)) ?? []);
(await _timelineFragmentsBox.get(sendingTimelineKey)) ?? [],
);
}
// Combine those two lists while respecting the start and limit parameters.
@ -481,7 +490,9 @@ class HiveCollectionsDatabase extends DatabaseApi {
@override
Future<List<String>> getLastSentMessageUserDeviceKey(
String userId, String deviceId) async {
String userId,
String deviceId,
) async {
final raw =
await _userDeviceKeysBox.get(TupleKey(userId, deviceId).toString());
if (raw == null) return <String>[];
@ -489,8 +500,12 @@ class HiveCollectionsDatabase extends DatabaseApi {
}
@override
Future<void> storeOlmSession(String identityKey, String sessionId,
String pickle, int lastReceived) async {
Future<void> storeOlmSession(
String identityKey,
String sessionId,
String pickle,
int lastReceived,
) async {
final rawSessions = (await _olmSessionsBox.get(identityKey)) ?? {};
rawSessions[sessionId] = <String, dynamic>{
'identity_key': identityKey,
@ -504,7 +519,9 @@ class HiveCollectionsDatabase extends DatabaseApi {
@override
Future<List<OlmSession>> getOlmSessions(
String identityKey, String userId) async {
String identityKey,
String userId,
) async {
final rawSessions = await _olmSessionsBox.get(identityKey);
if (rawSessions == null || rawSessions.isEmpty) return <OlmSession>[];
return rawSessions.values
@ -518,23 +535,31 @@ class HiveCollectionsDatabase extends DatabaseApi {
@override
Future<List<OlmSession>> getOlmSessionsForDevices(
List<String> identityKeys, String userId) async {
List<String> identityKeys,
String userId,
) async {
final sessions = await Future.wait(
identityKeys.map((identityKey) => getOlmSessions(identityKey, userId)));
identityKeys.map((identityKey) => getOlmSessions(identityKey, userId)),
);
return <OlmSession>[for (final sublist in sessions) ...sublist];
}
@override
Future<OutboundGroupSession?> getOutboundGroupSession(
String roomId, String userId) async {
String roomId,
String userId,
) async {
final raw = await _outboundGroupSessionsBox.get(roomId);
if (raw == null) return null;
return OutboundGroupSession.fromJson(copyMap(raw), userId);
}
@override
Future<Room?> getSingleRoom(Client client, String roomId,
{bool loadImportantStates = true}) async {
Future<Room?> getSingleRoom(
Client client,
String roomId, {
bool loadImportantStates = true,
}) async {
// Get raw room from database:
final roomData = await _roomsBox.get(roomId);
if (roomData == null) return null;
@ -589,9 +614,11 @@ class HiveCollectionsDatabase extends DatabaseApi {
for (final states in statesList) {
if (states == null) continue;
final stateEvents = states.values
.map((raw) => room.membership == Membership.invite
.map(
(raw) => room.membership == Membership.invite
? StrippedStateEvent.fromJson(copyMap(raw))
: Event.fromJson(copyMap(raw), room))
: Event.fromJson(copyMap(raw), room),
)
.toList();
for (final state in stateEvents) {
room.setState(state);
@ -637,9 +664,11 @@ class HiveCollectionsDatabase extends DatabaseApi {
if (members != null) {
for (final member in members) {
if (member == null) continue;
room.setState(room.membership == Membership.invite
room.setState(
room.membership == Membership.invite
? StrippedStateEvent.fromJson(copyMap(member))
: Event.fromJson(copyMap(member), room));
: Event.fromJson(copyMap(member), room),
);
}
}
}
@ -657,7 +686,8 @@ class HiveCollectionsDatabase extends DatabaseApi {
basicRoomEvent;
} else {
Logs().w(
'Found account data for unknown room $roomId. Delete now...');
'Found account data for unknown room $roomId. Delete now...',
);
await _roomAccountDataBox
.delete(TupleKey(roomId, basicRoomEvent.type).toString());
}
@ -687,7 +717,9 @@ class HiveCollectionsDatabase extends DatabaseApi {
@override
Future<List<Event>> getUnimportantRoomEventStatesForRoom(
List<String> events, Room room) async {
List<String> events,
Room room,
) async {
final keys = (await _roomStateBox.getAllKeys()).where((key) {
final tuple = TupleKey.fromString(key);
return tuple.parts.first == room.id && !events.contains(tuple.parts[1]);
@ -698,7 +730,8 @@ class HiveCollectionsDatabase extends DatabaseApi {
final states = await _roomStateBox.get(key);
if (states == null) continue;
unimportantEvents.addAll(
states.values.map((raw) => Event.fromJson(copyMap(raw), room)));
states.values.map((raw) => Event.fromJson(copyMap(raw), room)),
);
}
return unimportantEvents.where((event) => event.stateKey != null).toList();
}
@ -766,7 +799,8 @@ class HiveCollectionsDatabase extends DatabaseApi {
.where((c) => c != null)
.toList()
.cast<Map<String, dynamic>>(),
client);
client,
);
}
return res;
});
@ -797,7 +831,8 @@ class HiveCollectionsDatabase extends DatabaseApi {
String? deviceId,
String? deviceName,
String? prevBatch,
String? olmAccount) async {
String? olmAccount,
) async {
await transaction(() async {
await _clientBox.put('homeserver_url', homeserverUrl);
await _clientBox.put('token', token);
@ -842,7 +877,10 @@ class HiveCollectionsDatabase extends DatabaseApi {
@override
Future<int> insertIntoToDeviceQueue(
String type, String txnId, String content) async {
String type,
String txnId,
String content,
) async {
final id = DateTime.now().millisecondsSinceEpoch;
await _toDeviceQueueBox.put(id.toString(), {
'type': type,
@ -854,11 +892,14 @@ class HiveCollectionsDatabase extends DatabaseApi {
@override
Future<void> markInboundGroupSessionAsUploaded(
String roomId, String sessionId) async {
String roomId,
String sessionId,
) async {
final raw = await _inboundGroupSessionsBox.get(sessionId);
if (raw == null) {
Logs().w(
'Tried to mark inbound group session as uploaded which was not found in the database!');
'Tried to mark inbound group session as uploaded which was not found in the database!',
);
return;
}
raw['uploaded'] = true;
@ -903,7 +944,9 @@ class HiveCollectionsDatabase extends DatabaseApi {
@override
Future<void> removeUserCrossSigningKey(
String userId, String publicKey) async {
String userId,
String publicKey,
) async {
await _userCrossSigningKeysBox
.delete(TupleKey(userId, publicKey).toString());
return;
@ -917,7 +960,10 @@ class HiveCollectionsDatabase extends DatabaseApi {
@override
Future<void> setBlockedUserCrossSigningKey(
bool blocked, String userId, String publicKey) async {
bool blocked,
String userId,
String publicKey,
) async {
final raw = await _userCrossSigningKeysBox
.get(TupleKey(userId, publicKey).toString());
raw!['blocked'] = blocked;
@ -930,7 +976,10 @@ class HiveCollectionsDatabase extends DatabaseApi {
@override
Future<void> setBlockedUserDeviceKey(
bool blocked, String userId, String deviceId) async {
bool blocked,
String userId,
String deviceId,
) async {
final raw =
await _userDeviceKeysBox.get(TupleKey(userId, deviceId).toString());
raw!['blocked'] = blocked;
@ -943,7 +992,10 @@ class HiveCollectionsDatabase extends DatabaseApi {
@override
Future<void> setLastActiveUserDeviceKey(
int lastActive, String userId, String deviceId) async {
int lastActive,
String userId,
String deviceId,
) async {
final raw =
await _userDeviceKeysBox.get(TupleKey(userId, deviceId).toString());
raw!['last_active'] = lastActive;
@ -955,7 +1007,10 @@ class HiveCollectionsDatabase extends DatabaseApi {
@override
Future<void> setLastSentMessageUserDeviceKey(
String lastSentMessage, String userId, String deviceId) async {
String lastSentMessage,
String userId,
String deviceId,
) async {
final raw =
await _userDeviceKeysBox.get(TupleKey(userId, deviceId).toString());
raw!['last_sent_message'] = lastSentMessage;
@ -967,7 +1022,10 @@ class HiveCollectionsDatabase extends DatabaseApi {
@override
Future<void> setRoomPrevBatch(
String? prevBatch, String roomId, Client client) async {
String? prevBatch,
String roomId,
Client client,
) async {
final raw = await _roomsBox.get(roomId);
if (raw == null) return;
final room = Room.fromJson(copyMap(raw), client);
@ -978,7 +1036,10 @@ class HiveCollectionsDatabase extends DatabaseApi {
@override
Future<void> setVerifiedUserCrossSigningKey(
bool verified, String userId, String publicKey) async {
bool verified,
String userId,
String publicKey,
) async {
final raw = (await _userCrossSigningKeysBox
.get(TupleKey(userId, publicKey).toString())) ??
{};
@ -992,7 +1053,10 @@ class HiveCollectionsDatabase extends DatabaseApi {
@override
Future<void> setVerifiedUserDeviceKey(
bool verified, String userId, String deviceId) async {
bool verified,
String userId,
String deviceId,
) async {
final raw =
await _userDeviceKeysBox.get(TupleKey(userId, deviceId).toString());
raw!['verified'] = verified;
@ -1026,7 +1090,8 @@ class HiveCollectionsDatabase extends DatabaseApi {
event.setRedactionEvent(Event.fromJson(eventUpdate.content, tmpRoom));
await _eventsBox.put(
TupleKey(eventUpdate.roomID, event.eventId).toString(),
event.toJson());
event.toJson(),
);
if (tmpRoom.lastEvent?.eventId == event.eventId) {
await _roomStateBox.put(
@ -1041,7 +1106,7 @@ class HiveCollectionsDatabase extends DatabaseApi {
if ({
EventUpdateType.timeline,
EventUpdateType.history,
EventUpdateType.decryptedTimelineQueue
EventUpdateType.decryptedTimelineQueue,
}.contains(eventUpdate.type)) {
final eventId = eventUpdate.content['event_id'];
// Is this ID already in the store?
@ -1089,8 +1154,10 @@ class HiveCollectionsDatabase extends DatabaseApi {
final transactionId = eventUpdate.content
.tryGetMap<String, dynamic>('unsigned')
?.tryGet<String>('transaction_id');
await _eventsBox.put(TupleKey(eventUpdate.roomID, eventId).toString(),
eventUpdate.content);
await _eventsBox.put(
TupleKey(eventUpdate.roomID, eventId).toString(),
eventUpdate.content,
);
// Update timeline fragments
final key = TupleKey(eventUpdate.roomID, status.isSent ? '' : 'SENDING')
@ -1145,7 +1212,8 @@ class HiveCollectionsDatabase extends DatabaseApi {
eventUpdate.roomID,
eventUpdate.content['state_key'],
).toString(),
eventUpdate.content);
eventUpdate.content,
);
} else {
final key = TupleKey(
eventUpdate.roomID,
@ -1184,7 +1252,8 @@ class HiveCollectionsDatabase extends DatabaseApi {
String indexes,
String allowedAtIndex,
String senderKey,
String senderClaimedKey) async {
String senderClaimedKey,
) async {
await _inboundGroupSessionsBox.put(
sessionId,
StoredInboundGroupSession(
@ -1197,13 +1266,18 @@ class HiveCollectionsDatabase extends DatabaseApi {
senderKey: senderKey,
senderClaimedKeys: senderClaimedKey,
uploaded: false,
).toJson());
).toJson(),
);
return;
}
@override
Future<void> storeOutboundGroupSession(
String roomId, String pickle, String deviceIds, int creationTime) async {
String roomId,
String pickle,
String deviceIds,
int creationTime,
) async {
await _outboundGroupSessionsBox.put(roomId, <String, dynamic>{
'room_id': roomId,
'pickle': pickle,
@ -1265,7 +1339,8 @@ class HiveCollectionsDatabase extends DatabaseApi {
id: roomId,
membership: membership,
lastEvent: lastEvent,
).toJson());
).toJson(),
);
} else if (roomUpdate is JoinedRoomUpdate) {
final currentRoom = Room.fromJson(copyMap(currentRawRoom), client);
await _roomsBox.put(
@ -1280,12 +1355,14 @@ class HiveCollectionsDatabase extends DatabaseApi {
notificationCount:
roomUpdate.unreadNotifications?.notificationCount?.toInt() ??
currentRoom.notificationCount,
prev_batch:
roomUpdate.timeline?.prevBatch ?? currentRoom.prev_batch,
summary: RoomSummary.fromJson(currentRoom.summary.toJson()
..addAll(roomUpdate.summary?.toJson() ?? {})),
prev_batch: roomUpdate.timeline?.prevBatch ?? currentRoom.prev_batch,
summary: RoomSummary.fromJson(
currentRoom.summary.toJson()
..addAll(roomUpdate.summary?.toJson() ?? {}),
),
lastEvent: lastEvent,
).toJson());
).toJson(),
);
}
}
@ -1295,7 +1372,11 @@ class HiveCollectionsDatabase extends DatabaseApi {
@override
Future<void> storeSSSSCache(
String type, String keyId, String ciphertext, String content) async {
String type,
String keyId,
String ciphertext,
String content,
) async {
await _ssssCacheBox.put(
type,
SSSSCache(
@ -1303,7 +1384,8 @@ class HiveCollectionsDatabase extends DatabaseApi {
keyId: keyId,
ciphertext: ciphertext,
content: content,
).toJson());
).toJson(),
);
}
@override
@ -1314,8 +1396,13 @@ class HiveCollectionsDatabase extends DatabaseApi {
}
@override
Future<void> storeUserCrossSigningKey(String userId, String publicKey,
String content, bool verified, bool blocked) async {
Future<void> storeUserCrossSigningKey(
String userId,
String publicKey,
String content,
bool verified,
bool blocked,
) async {
await _userCrossSigningKeysBox.put(
TupleKey(userId, publicKey).toString(),
{
@ -1329,8 +1416,14 @@ class HiveCollectionsDatabase extends DatabaseApi {
}
@override
Future<void> storeUserDeviceKey(String userId, String deviceId,
String content, bool verified, bool blocked, int lastActive) async {
Future<void> storeUserDeviceKey(
String userId,
String deviceId,
String content,
bool verified,
bool blocked,
int lastActive,
) async {
await _userDeviceKeysBox.put(TupleKey(userId, deviceId).toString(), {
'user_id': userId,
'device_id': deviceId,
@ -1371,8 +1464,10 @@ class HiveCollectionsDatabase extends DatabaseApi {
if (tokenExpiresAt == null) {
await _clientBox.delete('token_expires_at');
} else {
await _clientBox.put('token_expires_at',
tokenExpiresAt.millisecondsSinceEpoch.toString());
await _clientBox.put(
'token_expires_at',
tokenExpiresAt.millisecondsSinceEpoch.toString(),
);
}
if (refreshToken == null) {
await _clientBox.delete('refresh_token');
@ -1414,11 +1509,15 @@ class HiveCollectionsDatabase extends DatabaseApi {
@override
Future<void> updateInboundGroupSessionAllowedAtIndex(
String allowedAtIndex, String roomId, String sessionId) async {
String allowedAtIndex,
String roomId,
String sessionId,
) async {
final raw = await _inboundGroupSessionsBox.get(sessionId);
if (raw == null) {
Logs().w(
'Tried to update inbound group session as uploaded which wasnt found in the database!');
'Tried to update inbound group session as uploaded which wasnt found in the database!',
);
return;
}
raw['allowed_at_index'] = allowedAtIndex;
@ -1428,11 +1527,15 @@ class HiveCollectionsDatabase extends DatabaseApi {
@override
Future<void> updateInboundGroupSessionIndexes(
String indexes, String roomId, String sessionId) async {
String indexes,
String roomId,
String sessionId,
) async {
final raw = await _inboundGroupSessionsBox.get(sessionId);
if (raw == null) {
Logs().w(
'Tried to update inbound group session indexes of a session which was not found in the database!');
'Tried to update inbound group session indexes of a session which was not found in the database!',
);
return;
}
final json = copyMap(raw);
@ -1552,11 +1655,15 @@ class HiveCollectionsDatabase extends DatabaseApi {
}
for (final key in json[_inboundGroupSessionsBoxName]!.keys) {
await _inboundGroupSessionsBox.put(
key, json[_inboundGroupSessionsBoxName]![key]);
key,
json[_inboundGroupSessionsBoxName]![key],
);
}
for (final key in json[_outboundGroupSessionsBoxName]!.keys) {
await _outboundGroupSessionsBox.put(
key, json[_outboundGroupSessionsBoxName]![key]);
key,
json[_outboundGroupSessionsBoxName]![key],
);
}
for (final key in json[_olmSessionsBoxName]!.keys) {
await _olmSessionsBox.put(key, json[_olmSessionsBoxName]![key]);
@ -1566,11 +1673,15 @@ class HiveCollectionsDatabase extends DatabaseApi {
}
for (final key in json[_userDeviceKeysOutdatedBoxName]!.keys) {
await _userDeviceKeysOutdatedBox.put(
key, json[_userDeviceKeysOutdatedBoxName]![key]);
key,
json[_userDeviceKeysOutdatedBoxName]![key],
);
}
for (final key in json[_userCrossSigningKeysBoxName]!.keys) {
await _userCrossSigningKeysBox.put(
key, json[_userCrossSigningKeysBoxName]![key]);
key,
json[_userCrossSigningKeysBoxName]![key],
);
}
for (final key in json[_ssssCacheBoxName]!.keys) {
await _ssssCacheBox.put(key, json[_ssssCacheBoxName]![key]);
@ -1580,7 +1691,9 @@ class HiveCollectionsDatabase extends DatabaseApi {
}
for (final key in json[_timelineFragmentsBoxName]!.keys) {
await _timelineFragmentsBox.put(
key, json[_timelineFragmentsBoxName]![key]);
key,
json[_timelineFragmentsBoxName]![key],
);
}
for (final key in json[_seenDeviceIdsBoxName]!.keys) {
await _seenDeviceIdsBox.put(key, json[_seenDeviceIdsBoxName]![key]);
@ -1629,7 +1742,9 @@ class HiveCollectionsDatabase extends DatabaseApi {
@override
Future<void> storeUserProfile(
String userId, CachedProfileInformation profile) async {
String userId,
CachedProfileInformation profile,
) async {
return;
}
}

View File

@ -39,7 +39,8 @@ import 'package:matrix/src/utils/run_benchmarked.dart';
///
/// This database does not support file caching!
@Deprecated(
'Use [MatrixSdkDatabase] instead. Don\'t forget to properly migrate!')
'Use [MatrixSdkDatabase] instead. Don\'t forget to properly migrate!',
)
class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
static const int version = 6;
final String name;
@ -254,11 +255,16 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
convertToJson(raw),
Client(''),
);
await addSeenDeviceId(deviceKeys.userId, deviceKeys.deviceId!,
deviceKeys.curve25519Key! + deviceKeys.ed25519Key!);
await addSeenDeviceId(
deviceKeys.userId,
deviceKeys.deviceId!,
deviceKeys.curve25519Key! + deviceKeys.ed25519Key!,
);
await addSeenPublicKey(deviceKeys.ed25519Key!, deviceKeys.deviceId!);
await addSeenPublicKey(
deviceKeys.curve25519Key!, deviceKeys.deviceId!);
deviceKeys.curve25519Key!,
deviceKeys.deviceId!,
);
} catch (e) {
Logs().w('Can not migrate device $key', e);
}
@ -344,7 +350,8 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
@override
Future<Map<String, BasicEvent>> getAccountData() =>
runBenchmarked<Map<String, BasicEvent>>('Get all account data from Hive',
runBenchmarked<Map<String, BasicEvent>>(
'Get all account data from Hive',
() async {
final accountData = <String, BasicEvent>{};
for (final key in _accountDataBox.keys) {
@ -355,7 +362,9 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
);
}
return accountData;
}, _accountDataBox.keys.length);
},
_accountDataBox.keys.length,
);
@override
Future<Map<String, dynamic>?> getClient(String name) =>
@ -378,10 +387,13 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
/// Loads a whole list of events at once from the store for a specific room
Future<List<Event>> _getEventsByIds(List<String> eventIds, Room room) async {
final events = await Future.wait(eventIds.map((String eventId) async {
final entry = await _eventsBox.get(MultiKey(room.id, eventId).toString());
final events = await Future.wait(
eventIds.map((String eventId) async {
final entry =
await _eventsBox.get(MultiKey(room.id, eventId).toString());
return entry is Map ? Event.fromJson(convertToJson(entry), room) : null;
}));
}),
);
return events.whereType<Event>().toList();
}
@ -397,7 +409,8 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
// Get the synced event IDs from the store
final timelineKey = MultiKey(room.id, '').toString();
final timelineEventIds = List<String>.from(
(await _timelineFragmentsBox.get(timelineKey)) ?? []);
(await _timelineFragmentsBox.get(timelineKey)) ?? [],
);
// Get the local stored SENDING events from the store
late final List sendingEventIds;
if (start != 0) {
@ -405,18 +418,21 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
} else {
final sendingTimelineKey = MultiKey(room.id, 'SENDING').toString();
sendingEventIds = List<String>.from(
(await _timelineFragmentsBox.get(sendingTimelineKey)) ?? []);
(await _timelineFragmentsBox.get(sendingTimelineKey)) ?? [],
);
}
// Combine those two lists while respecting the start and limit parameters.
final end = min(timelineEventIds.length,
start + (limit ?? timelineEventIds.length));
final end = min(
timelineEventIds.length,
start + (limit ?? timelineEventIds.length),
);
final eventIds = List<String>.from(
[
...sendingEventIds,
...(start < timelineEventIds.length && !onlySending
? timelineEventIds.getRange(start, end).toList()
: [])
: []),
],
);
@ -435,7 +451,8 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
final timelineKey = MultiKey(room.id, '').toString();
final timelineEventIds = List<String>.from(
(await _timelineFragmentsBox.get(timelineKey)) ?? []);
(await _timelineFragmentsBox.get(timelineKey)) ?? [],
);
// Get the local stored SENDING events from the store
late final List<String> sendingEventIds;
@ -444,7 +461,8 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
} else {
final sendingTimelineKey = MultiKey(room.id, 'SENDING').toString();
sendingEventIds = List<String>.from(
(await _timelineFragmentsBox.get(sendingTimelineKey)) ?? []);
(await _timelineFragmentsBox.get(sendingTimelineKey)) ?? [],
);
}
// Combine those two lists while respecting the start and limit parameters.
@ -474,9 +492,11 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
@override
Future<List<StoredInboundGroupSession>>
getInboundGroupSessionsToUpload() async {
final sessions = (await Future.wait(_inboundGroupSessionsBox.keys.map(
(sessionId) async =>
await _inboundGroupSessionsBox.get(sessionId))))
final sessions = (await Future.wait(
_inboundGroupSessionsBox.keys.map(
(sessionId) async => await _inboundGroupSessionsBox.get(sessionId),
),
))
.where((rawSession) => rawSession['uploaded'] == false)
.take(500)
.map(
@ -490,7 +510,9 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
@override
Future<List<String>> getLastSentMessageUserDeviceKey(
String userId, String deviceId) async {
String userId,
String deviceId,
) async {
final raw =
await _userDeviceKeysBox.get(MultiKey(userId, deviceId).toString());
if (raw == null) return <String>[];
@ -498,8 +520,12 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
}
@override
Future<void> storeOlmSession(String identityKey, String sessionId,
String pickle, int lastReceived) async {
Future<void> storeOlmSession(
String identityKey,
String sessionId,
String pickle,
int lastReceived,
) async {
final rawSessions =
(await _olmSessionsBox.get(identityKey.toHiveKey) as Map?) ?? {};
rawSessions[sessionId] = <String, dynamic>{
@ -514,7 +540,9 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
@override
Future<List<OlmSession>> getOlmSessions(
String identityKey, String userId) async {
String identityKey,
String userId,
) async {
final rawSessions =
await _olmSessionsBox.get(identityKey.toHiveKey) as Map? ?? {};
@ -540,23 +568,31 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
@override
Future<List<OlmSession>> getOlmSessionsForDevices(
List<String> identityKeys, String userId) async {
List<String> identityKeys,
String userId,
) async {
final sessions = await Future.wait(
identityKeys.map((identityKey) => getOlmSessions(identityKey, userId)));
identityKeys.map((identityKey) => getOlmSessions(identityKey, userId)),
);
return <OlmSession>[for (final sublist in sessions) ...sublist];
}
@override
Future<OutboundGroupSession?> getOutboundGroupSession(
String roomId, String userId) async {
String roomId,
String userId,
) async {
final raw = await _outboundGroupSessionsBox.get(roomId.toHiveKey);
if (raw == null) return null;
return OutboundGroupSession.fromJson(convertToJson(raw), userId);
}
@override
Future<Room?> getSingleRoom(Client client, String roomId,
{bool loadImportantStates = true}) async {
Future<Room?> getSingleRoom(
Client client,
String roomId, {
bool loadImportantStates = true,
}) async {
// Get raw room from database:
final roomData = await _roomsBox.get(roomId);
if (roomData == null) return null;
@ -580,8 +616,9 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
}
@override
Future<List<Room>> getRoomList(Client client) =>
runBenchmarked<List<Room>>('Get room list from hive', () async {
Future<List<Room>> getRoomList(Client client) => runBenchmarked<List<Room>>(
'Get room list from hive',
() async {
final rooms = <String, Room>{};
final userID = client.userID;
final importantRoomStates = client.importantStateEvents;
@ -612,15 +649,17 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
}
// Load members
for (final userId in membersToPostload) {
final state =
await _roomMembersBox.get(MultiKey(room.id, userId).toString());
final state = await _roomMembersBox
.get(MultiKey(room.id, userId).toString());
if (state == null) {
Logs().w('Unable to post load member $userId');
continue;
}
room.setState(room.membership == Membership.invite
room.setState(
room.membership == Membership.invite
? StrippedStateEvent.fromJson(copyMap(raw))
: Event.fromJson(convertToJson(state), room));
: Event.fromJson(convertToJson(state), room),
);
}
// Get the "important" room states. All other states will be loaded once
@ -630,9 +669,11 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
.get(MultiKey(room.id, type).toString()) as Map?;
if (states == null) continue;
final stateEvents = states.values
.map((raw) => room.membership == Membership.invite
.map(
(raw) => room.membership == Membership.invite
? StrippedStateEvent.fromJson(copyMap(raw))
: Event.fromJson(convertToJson(raw), room))
: Event.fromJson(convertToJson(raw), room),
)
.toList();
for (final state in stateEvents) {
room.setState(state);
@ -655,13 +696,16 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
basicRoomEvent;
} else {
Logs().w(
'Found account data for unknown room $roomId. Delete now...');
'Found account data for unknown room $roomId. Delete now...',
);
await _roomAccountDataBox.delete(key);
}
}
return rooms.values.toList();
}, _roomsBox.keys.length);
},
_roomsBox.keys.length,
);
@override
Future<SSSSCache?> getSSSSCache(String type) async {
@ -672,15 +716,19 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
@override
Future<List<QueuedToDeviceEvent>> getToDeviceEventQueue() async =>
await Future.wait(_toDeviceQueueBox.keys.map((i) async {
await Future.wait(
_toDeviceQueueBox.keys.map((i) async {
final raw = await _toDeviceQueueBox.get(i);
raw['id'] = i;
return QueuedToDeviceEvent.fromJson(convertToJson(raw));
}).toList());
}).toList(),
);
@override
Future<List<Event>> getUnimportantRoomEventStatesForRoom(
List<String> events, Room room) async {
List<String> events,
Room room,
) async {
final keys = _roomStateBox.keys.where((key) {
final tuple = MultiKey.fromString(key);
return tuple.parts.first == room.id && !events.contains(tuple.parts[1]);
@ -690,7 +738,8 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
for (final key in keys) {
final Map states = await _roomStateBox.get(key);
unimportantEvents.addAll(
states.values.map((raw) => Event.fromJson(convertToJson(raw), room)));
states.values.map((raw) => Event.fromJson(convertToJson(raw), room)),
);
}
return unimportantEvents.where((event) => event.stateKey != null).toList();
}
@ -706,7 +755,8 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
@override
Future<Map<String, DeviceKeysList>> getUserDeviceKeys(Client client) =>
runBenchmarked<Map<String, DeviceKeysList>>(
'Get all user device keys from Hive', () async {
'Get all user device keys from Hive',
() async {
final deviceKeysOutdated = _userDeviceKeysOutdatedBox.keys;
if (deviceKeysOutdated.isEmpty) {
return {};
@ -728,14 +778,25 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
'user_id': userId,
'outdated': await _userDeviceKeysOutdatedBox.get(userId),
},
await Future.wait(deviceKeysBoxKeys.map((key) async =>
convertToJson(await _userDeviceKeysBox.get(key)))),
await Future.wait(crossSigningKeysBoxKeys.map((key) async =>
convertToJson(await _userCrossSigningKeysBox.get(key)))),
client);
await Future.wait(
deviceKeysBoxKeys.map(
(key) async =>
convertToJson(await _userDeviceKeysBox.get(key)),
),
),
await Future.wait(
crossSigningKeysBoxKeys.map(
(key) async =>
convertToJson(await _userCrossSigningKeysBox.get(key)),
),
),
client,
);
}
return res;
}, _userDeviceKeysBox.keys.length);
},
_userDeviceKeysBox.keys.length,
);
@override
Future<List<User>> getUsers(Room room) async {
@ -760,11 +821,14 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
String? deviceId,
String? deviceName,
String? prevBatch,
String? olmAccount) async {
String? olmAccount,
) async {
await _clientBox.put('homeserver_url', homeserverUrl);
await _clientBox.put('token', token);
await _clientBox.put(
'token_expires_at', tokenExpiresAt?.millisecondsSinceEpoch.toString());
'token_expires_at',
tokenExpiresAt?.millisecondsSinceEpoch.toString(),
);
await _clientBox.put('refresh_token', refreshToken);
await _clientBox.put('user_id', userId);
await _clientBox.put('device_id', deviceId);
@ -777,7 +841,10 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
@override
Future<int> insertIntoToDeviceQueue(
String type, String txnId, String content) async {
String type,
String txnId,
String content,
) async {
return await _toDeviceQueueBox.add(<String, dynamic>{
'type': type,
'txn_id': txnId,
@ -787,11 +854,14 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
@override
Future<void> markInboundGroupSessionAsUploaded(
String roomId, String sessionId) async {
String roomId,
String sessionId,
) async {
final raw = await _inboundGroupSessionsBox.get(sessionId.toHiveKey);
if (raw == null) {
Logs().w(
'Tried to mark inbound group session as uploaded which was not found in the database!');
'Tried to mark inbound group session as uploaded which was not found in the database!',
);
return;
}
raw['uploaded'] = true;
@ -833,7 +903,9 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
@override
Future<void> removeUserCrossSigningKey(
String userId, String publicKey) async {
String userId,
String publicKey,
) async {
await _userCrossSigningKeysBox
.delete(MultiKey(userId, publicKey).toString());
return;
@ -847,7 +919,10 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
@override
Future<void> setBlockedUserCrossSigningKey(
bool blocked, String userId, String publicKey) async {
bool blocked,
String userId,
String publicKey,
) async {
final raw = await _userCrossSigningKeysBox
.get(MultiKey(userId, publicKey).toString());
raw['blocked'] = blocked;
@ -860,7 +935,10 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
@override
Future<void> setBlockedUserDeviceKey(
bool blocked, String userId, String deviceId) async {
bool blocked,
String userId,
String deviceId,
) async {
final raw =
await _userDeviceKeysBox.get(MultiKey(userId, deviceId).toString());
raw['blocked'] = blocked;
@ -873,7 +951,10 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
@override
Future<void> setLastActiveUserDeviceKey(
int lastActive, String userId, String deviceId) async {
int lastActive,
String userId,
String deviceId,
) async {
final raw =
await _userDeviceKeysBox.get(MultiKey(userId, deviceId).toString());
raw['last_active'] = lastActive;
@ -885,7 +966,10 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
@override
Future<void> setLastSentMessageUserDeviceKey(
String lastSentMessage, String userId, String deviceId) async {
String lastSentMessage,
String userId,
String deviceId,
) async {
final raw =
await _userDeviceKeysBox.get(MultiKey(userId, deviceId).toString());
raw['last_sent_message'] = lastSentMessage;
@ -897,7 +981,10 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
@override
Future<void> setRoomPrevBatch(
String? prevBatch, String roomId, Client client) async {
String? prevBatch,
String roomId,
Client client,
) async {
final raw = await _roomsBox.get(roomId.toHiveKey);
if (raw == null) return;
final room = Room.fromJson(convertToJson(raw), client);
@ -908,7 +995,10 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
@override
Future<void> setVerifiedUserCrossSigningKey(
bool verified, String userId, String publicKey) async {
bool verified,
String userId,
String publicKey,
) async {
final raw = (await _userCrossSigningKeysBox
.get(MultiKey(userId, publicKey).toString()) as Map?) ??
{};
@ -922,7 +1012,10 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
@override
Future<void> setVerifiedUserDeviceKey(
bool verified, String userId, String deviceId) async {
bool verified,
String userId,
String deviceId,
) async {
final raw =
await _userDeviceKeysBox.get(MultiKey(userId, deviceId).toString());
raw['verified'] = verified;
@ -936,7 +1029,9 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
@override
Future<void> storeAccountData(String type, String content) async {
await _accountDataBox.put(
type.toHiveKey, convertToJson(jsonDecode(content)));
type.toHiveKey,
convertToJson(jsonDecode(content)),
);
return;
}
@ -956,7 +1051,8 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
event.setRedactionEvent(Event.fromJson(eventUpdate.content, tmpRoom));
await _eventsBox.put(
MultiKey(eventUpdate.roomID, event.eventId).toString(),
event.toJson());
event.toJson(),
);
if (tmpRoom.lastEvent?.eventId == event.eventId) {
await _roomStateBox.put(
@ -971,7 +1067,7 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
if ({
EventUpdateType.timeline,
EventUpdateType.history,
EventUpdateType.decryptedTimelineQueue
EventUpdateType.decryptedTimelineQueue,
}.contains(eventUpdate.type)) {
final eventId = eventUpdate.content['event_id'];
// Is this ID already in the store?
@ -1020,8 +1116,10 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
.tryGetMap<String, dynamic>('unsigned')
?.tryGet<String>('transaction_id');
await _eventsBox.put(MultiKey(eventUpdate.roomID, eventId).toString(),
eventUpdate.content);
await _eventsBox.put(
MultiKey(eventUpdate.roomID, eventId).toString(),
eventUpdate.content,
);
// Update timeline fragments
final key = MultiKey(eventUpdate.roomID, status.isSent ? '' : 'SENDING')
@ -1074,7 +1172,8 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
eventUpdate.roomID,
eventUpdate.content['state_key'],
).toString(),
eventUpdate.content);
eventUpdate.content,
);
} else {
final key = MultiKey(
eventUpdate.roomID,
@ -1113,7 +1212,8 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
String indexes,
String allowedAtIndex,
String senderKey,
String senderClaimedKey) async {
String senderClaimedKey,
) async {
await _inboundGroupSessionsBox.put(
sessionId.toHiveKey,
StoredInboundGroupSession(
@ -1126,13 +1226,18 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
senderKey: senderKey,
senderClaimedKeys: senderClaimedKey,
uploaded: false,
).toJson());
).toJson(),
);
return;
}
@override
Future<void> storeOutboundGroupSession(
String roomId, String pickle, String deviceIds, int creationTime) async {
String roomId,
String pickle,
String deviceIds,
int creationTime,
) async {
await _outboundGroupSessionsBox.put(roomId.toHiveKey, <String, dynamic>{
'room_id': roomId,
'pickle': pickle,
@ -1152,8 +1257,12 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
}
@override
Future<void> storeRoomUpdate(String roomId, SyncRoomUpdate roomUpdate,
Event? lastEvent, Client client) async {
Future<void> storeRoomUpdate(
String roomId,
SyncRoomUpdate roomUpdate,
Event? lastEvent,
Client client,
) async {
// Leave room if membership is leave
if (roomUpdate is LeftRoomUpdate) {
await forgetRoom(roomId);
@ -1189,7 +1298,8 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
id: roomId,
membership: membership,
lastEvent: lastEvent,
).toJson());
).toJson(),
);
} else if (roomUpdate is JoinedRoomUpdate) {
final currentRawRoom = await _roomsBox.get(roomId.toHiveKey);
final currentRoom = Room.fromJson(convertToJson(currentRawRoom), client);
@ -1205,11 +1315,13 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
notificationCount:
roomUpdate.unreadNotifications?.notificationCount?.toInt() ??
currentRoom.notificationCount,
prev_batch:
roomUpdate.timeline?.prevBatch ?? currentRoom.prev_batch,
summary: RoomSummary.fromJson(currentRoom.summary.toJson()
..addAll(roomUpdate.summary?.toJson() ?? {})),
).toJson());
prev_batch: roomUpdate.timeline?.prevBatch ?? currentRoom.prev_batch,
summary: RoomSummary.fromJson(
currentRoom.summary.toJson()
..addAll(roomUpdate.summary?.toJson() ?? {}),
),
).toJson(),
);
}
}
@ -1219,7 +1331,11 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
@override
Future<void> storeSSSSCache(
String type, String keyId, String ciphertext, String content) async {
String type,
String keyId,
String ciphertext,
String content,
) async {
await _ssssCacheBox.put(
type,
SSSSCache(
@ -1227,7 +1343,8 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
keyId: keyId,
ciphertext: ciphertext,
content: content,
).toJson());
).toJson(),
);
}
@override
@ -1238,8 +1355,13 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
}
@override
Future<void> storeUserCrossSigningKey(String userId, String publicKey,
String content, bool verified, bool blocked) async {
Future<void> storeUserCrossSigningKey(
String userId,
String publicKey,
String content,
bool verified,
bool blocked,
) async {
await _userCrossSigningKeysBox.put(
MultiKey(userId, publicKey).toString(),
{
@ -1253,8 +1375,14 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
}
@override
Future<void> storeUserDeviceKey(String userId, String deviceId,
String content, bool verified, bool blocked, int lastActive) async {
Future<void> storeUserDeviceKey(
String userId,
String deviceId,
String content,
bool verified,
bool blocked,
int lastActive,
) async {
await _userDeviceKeysBox.put(MultiKey(userId, deviceId).toString(), {
'user_id': userId,
'device_id': deviceId,
@ -1292,7 +1420,9 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
await _clientBox.put('homeserver_url', homeserverUrl);
await _clientBox.put('token', token);
await _clientBox.put(
'token_expires_at', tokenExpiresAt?.millisecondsSinceEpoch.toString());
'token_expires_at',
tokenExpiresAt?.millisecondsSinceEpoch.toString(),
);
await _clientBox.put('refresh_token', refreshToken);
await _clientBox.put('user_id', userId);
await _clientBox.put('device_id', deviceId);
@ -1312,11 +1442,15 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
@override
Future<void> updateInboundGroupSessionAllowedAtIndex(
String allowedAtIndex, String roomId, String sessionId) async {
String allowedAtIndex,
String roomId,
String sessionId,
) async {
final raw = await _inboundGroupSessionsBox.get(sessionId.toHiveKey);
if (raw == null) {
Logs().w(
'Tried to update inbound group session as uploaded which wasnt found in the database!');
'Tried to update inbound group session as uploaded which wasnt found in the database!',
);
return;
}
raw['allowed_at_index'] = allowedAtIndex;
@ -1326,11 +1460,15 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
@override
Future<void> updateInboundGroupSessionIndexes(
String indexes, String roomId, String sessionId) async {
String indexes,
String roomId,
String sessionId,
) async {
final raw = await _inboundGroupSessionsBox.get(sessionId.toHiveKey);
if (raw == null) {
Logs().w(
'Tried to update inbound group session indexes of a session which was not found in the database!');
'Tried to update inbound group session indexes of a session which was not found in the database!',
);
return;
}
raw['indexes'] = indexes;
@ -1340,8 +1478,10 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
@override
Future<List<StoredInboundGroupSession>> getAllInboundGroupSessions() async {
final rawSessions = await Future.wait(_inboundGroupSessionsBox.keys
.map((key) => _inboundGroupSessionsBox.get(key)));
final rawSessions = await Future.wait(
_inboundGroupSessionsBox.keys
.map((key) => _inboundGroupSessionsBox.get(key)),
);
return rawSessions
.map((raw) => StoredInboundGroupSession.fromJson(convertToJson(raw)))
.toList();
@ -1435,7 +1575,9 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
@override
Future<void> storeUserProfile(
String userId, CachedProfileInformation profile) async {
String userId,
CachedProfileInformation profile,
) async {
return;
}
}

View File

@ -22,7 +22,9 @@ class BoxCollection with ZoneTransactionMixin {
int version = 1,
}) async {
idbFactory ??= window.indexedDB!;
final db = await idbFactory.open(name, version: version,
final db = await idbFactory.open(
name,
version: version,
onUpgradeNeeded: (VersionChangeEvent event) {
final db = event.target.result;
for (final name in boxNames) {
@ -30,7 +32,8 @@ class BoxCollection with ZoneTransactionMixin {
db.createObjectStore(name, autoIncrement: true);
}
});
},
);
return BoxCollection(db, boxNames, name);
}
@ -147,7 +150,8 @@ class Box<V> {
txn ??= boxCollection._db.transaction(name, 'readonly');
final store = txn.objectStore(name);
final list = await Future.wait(
keys.map((key) => store.getObject(key).then(_fromValue)));
keys.map((key) => store.getObject(key).then(_fromValue)),
);
for (var i = 0; i < keys.length; i++) {
_cache[keys[i]] = list[i];
}

View File

@ -112,7 +112,8 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
// there was a field of type `dart:io:Directory` here. This one broke the
// dart js standalone compiler. Migration via URI as file system identifier.
@Deprecated(
'Breaks support for web standalone. Use [fileStorageLocation] instead.')
'Breaks support for web standalone. Use [fileStorageLocation] instead.',
)
Object? get fileStoragePath => fileStorageLocation?.toFilePath();
static const String _clientBoxName = 'box_client';
@ -183,7 +184,8 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
this.maxFileSize = 0,
// TODO : remove deprecated member migration on next major release
@Deprecated(
'Breaks support for web standalone. Use [fileStorageLocation] instead.')
'Breaks support for web standalone. Use [fileStorageLocation] instead.',
)
dynamic fileStoragePath,
Uri? fileStorageLocation,
Duration? deleteFilesAfterDuration,
@ -315,7 +317,8 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
.where((session) => session.uploaded == false)
.toList();
Logs().i(
'Move ${allInboundGroupSessions.length} inbound group sessions to upload to their own queue...');
'Move ${allInboundGroupSessions.length} inbound group sessions to upload to their own queue...',
);
await transaction(() async {
for (final session in sessionsToUpload) {
await _inboundGroupSessionsUploadQueueBox.put(
@ -476,8 +479,10 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
}
// Combine those two lists while respecting the start and limit parameters.
final end = min(timelineEventIds.length,
start + (limit ?? timelineEventIds.length));
final end = min(
timelineEventIds.length,
start + (limit ?? timelineEventIds.length),
);
final eventIds = [
...sendingEventIds,
if (!onlySending && start < timelineEventIds.length)
@ -511,7 +516,9 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
@override
Future<List<String>> getLastSentMessageUserDeviceKey(
String userId, String deviceId) async {
String userId,
String deviceId,
) async {
final raw =
await _userDeviceKeysBox.get(TupleKey(userId, deviceId).toString());
if (raw == null) return <String>[];
@ -519,8 +526,12 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
}
@override
Future<void> storeOlmSession(String identityKey, String sessionId,
String pickle, int lastReceived) async {
Future<void> storeOlmSession(
String identityKey,
String sessionId,
String pickle,
int lastReceived,
) async {
final rawSessions = copyMap((await _olmSessionsBox.get(identityKey)) ?? {});
rawSessions[sessionId] = {
'identity_key': identityKey,
@ -534,7 +545,9 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
@override
Future<List<OlmSession>> getOlmSessions(
String identityKey, String userId) async {
String identityKey,
String userId,
) async {
final rawSessions = await _olmSessionsBox.get(identityKey);
if (rawSessions == null || rawSessions.isEmpty) return <OlmSession>[];
return rawSessions.values
@ -548,23 +561,31 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
@override
Future<List<OlmSession>> getOlmSessionsForDevices(
List<String> identityKeys, String userId) async {
List<String> identityKeys,
String userId,
) async {
final sessions = await Future.wait(
identityKeys.map((identityKey) => getOlmSessions(identityKey, userId)));
identityKeys.map((identityKey) => getOlmSessions(identityKey, userId)),
);
return <OlmSession>[for (final sublist in sessions) ...sublist];
}
@override
Future<OutboundGroupSession?> getOutboundGroupSession(
String roomId, String userId) async {
String roomId,
String userId,
) async {
final raw = await _outboundGroupSessionsBox.get(roomId);
if (raw == null) return null;
return OutboundGroupSession.fromJson(copyMap(raw), userId);
}
@override
Future<Room?> getSingleRoom(Client client, String roomId,
{bool loadImportantStates = true}) async {
Future<Room?> getSingleRoom(
Client client,
String roomId, {
bool loadImportantStates = true,
}) async {
// Get raw room from database:
final roomData = await _roomsBox.get(roomId);
if (roomData == null) return null;
@ -611,9 +632,11 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
}
final states = entry.value;
final stateEvents = states.values
.map((raw) => room.membership == Membership.invite
.map(
(raw) => room.membership == Membership.invite
? StrippedStateEvent.fromJson(copyMap(raw))
: Event.fromJson(copyMap(raw), room))
: Event.fromJson(copyMap(raw), room),
)
.toList();
for (final state in stateEvents) {
room.setState(state);
@ -633,7 +656,8 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
basicRoomEvent;
} else {
Logs().w(
'Found account data for unknown room $roomId. Delete now...');
'Found account data for unknown room $roomId. Delete now...',
);
await _roomAccountDataBox
.delete(TupleKey(roomId, basicRoomEvent.type).toString());
}
@ -663,7 +687,9 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
@override
Future<List<Event>> getUnimportantRoomEventStatesForRoom(
List<String> events, Room room) async {
List<String> events,
Room room,
) async {
final keys = (await _nonPreloadRoomStateBox.getAllKeys()).where((key) {
final tuple = TupleKey.fromString(key);
return tuple.parts.first == room.id && !events.contains(tuple.parts[1]);
@ -674,7 +700,8 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
final states = await _nonPreloadRoomStateBox.get(key);
if (states == null) continue;
unimportantEvents.addAll(
states.values.map((raw) => Event.fromJson(copyMap(raw), room)));
states.values.map((raw) => Event.fromJson(copyMap(raw), room)),
);
}
return unimportantEvents.where((event) => event.stateKey != null).toList();
@ -739,7 +766,8 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
.where((c) => c != null)
.toList()
.cast<Map<String, dynamic>>(),
client);
client,
);
}
return res;
});
@ -770,15 +798,18 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
String? deviceId,
String? deviceName,
String? prevBatch,
String? olmAccount) async {
String? olmAccount,
) async {
await transaction(() async {
await _clientBox.put('homeserver_url', homeserverUrl);
await _clientBox.put('token', token);
if (tokenExpiresAt == null) {
await _clientBox.delete('token_expires_at');
} else {
await _clientBox.put('token_expires_at',
tokenExpiresAt.millisecondsSinceEpoch.toString());
await _clientBox.put(
'token_expires_at',
tokenExpiresAt.millisecondsSinceEpoch.toString(),
);
}
if (refreshToken == null) {
await _clientBox.delete('refresh_token');
@ -813,7 +844,10 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
@override
Future<int> insertIntoToDeviceQueue(
String type, String txnId, String content) async {
String type,
String txnId,
String content,
) async {
final id = DateTime.now().millisecondsSinceEpoch;
await _toDeviceQueueBox.put(id.toString(), {
'type': type,
@ -825,7 +859,9 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
@override
Future<void> markInboundGroupSessionAsUploaded(
String roomId, String sessionId) async {
String roomId,
String sessionId,
) async {
await _inboundGroupSessionsUploadQueueBox.delete(sessionId);
return;
}
@ -871,7 +907,9 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
@override
Future<void> removeUserCrossSigningKey(
String userId, String publicKey) async {
String userId,
String publicKey,
) async {
await _userCrossSigningKeysBox
.delete(TupleKey(userId, publicKey).toString());
return;
@ -885,7 +923,10 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
@override
Future<void> setBlockedUserCrossSigningKey(
bool blocked, String userId, String publicKey) async {
bool blocked,
String userId,
String publicKey,
) async {
final raw = copyMap(
await _userCrossSigningKeysBox
.get(TupleKey(userId, publicKey).toString()) ??
@ -901,7 +942,10 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
@override
Future<void> setBlockedUserDeviceKey(
bool blocked, String userId, String deviceId) async {
bool blocked,
String userId,
String deviceId,
) async {
final raw = copyMap(
await _userDeviceKeysBox.get(TupleKey(userId, deviceId).toString()) ?? {},
);
@ -915,7 +959,10 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
@override
Future<void> setLastActiveUserDeviceKey(
int lastActive, String userId, String deviceId) async {
int lastActive,
String userId,
String deviceId,
) async {
final raw = copyMap(
await _userDeviceKeysBox.get(TupleKey(userId, deviceId).toString()) ?? {},
);
@ -929,7 +976,10 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
@override
Future<void> setLastSentMessageUserDeviceKey(
String lastSentMessage, String userId, String deviceId) async {
String lastSentMessage,
String userId,
String deviceId,
) async {
final raw = copyMap(
await _userDeviceKeysBox.get(TupleKey(userId, deviceId).toString()) ?? {},
);
@ -942,7 +992,10 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
@override
Future<void> setRoomPrevBatch(
String? prevBatch, String roomId, Client client) async {
String? prevBatch,
String roomId,
Client client,
) async {
final raw = await _roomsBox.get(roomId);
if (raw == null) return;
final room = Room.fromJson(copyMap(raw), client);
@ -953,7 +1006,10 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
@override
Future<void> setVerifiedUserCrossSigningKey(
bool verified, String userId, String publicKey) async {
bool verified,
String userId,
String publicKey,
) async {
final raw = copyMap(
(await _userCrossSigningKeysBox
.get(TupleKey(userId, publicKey).toString())) ??
@ -969,7 +1025,10 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
@override
Future<void> setVerifiedUserDeviceKey(
bool verified, String userId, String deviceId) async {
bool verified,
String userId,
String deviceId,
) async {
final raw = copyMap(
await _userDeviceKeysBox.get(TupleKey(userId, deviceId).toString()) ?? {},
);
@ -1003,7 +1062,8 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
event.setRedactionEvent(Event.fromJson(eventUpdate.content, tmpRoom));
await _eventsBox.put(
TupleKey(eventUpdate.roomID, event.eventId).toString(),
event.toJson());
event.toJson(),
);
if (tmpRoom.lastEvent?.eventId == event.eventId) {
if (client.importantStateEvents.contains(event.type)) {
@ -1070,8 +1130,10 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
final transactionId = eventUpdate.content
.tryGetMap<String, dynamic>('unsigned')
?.tryGet<String>('transaction_id');
await _eventsBox.put(TupleKey(eventUpdate.roomID, eventId).toString(),
eventUpdate.content);
await _eventsBox.put(
TupleKey(eventUpdate.roomID, eventId).toString(),
eventUpdate.content,
);
// Update timeline fragments
final key = TupleKey(eventUpdate.roomID, status.isSent ? '' : 'SENDING')
@ -1126,7 +1188,8 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
eventUpdate.roomID,
eventUpdate.content['state_key'],
).toString(),
eventUpdate.content);
eventUpdate.content,
);
} else {
final type = eventUpdate.content['type'] as String;
final roomStateBox = client.importantStateEvents.contains(type)
@ -1164,7 +1227,8 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
String indexes,
String allowedAtIndex,
String senderKey,
String senderClaimedKey) async {
String senderClaimedKey,
) async {
final json = StoredInboundGroupSession(
roomId: roomId,
sessionId: sessionId,
@ -1186,7 +1250,11 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
@override
Future<void> storeOutboundGroupSession(
String roomId, String pickle, String deviceIds, int creationTime) async {
String roomId,
String pickle,
String deviceIds,
int creationTime,
) async {
await _outboundGroupSessionsBox.put(roomId, <String, dynamic>{
'room_id': roomId,
'pickle': pickle,
@ -1206,8 +1274,12 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
}
@override
Future<void> storeRoomUpdate(String roomId, SyncRoomUpdate roomUpdate,
Event? lastEvent, Client client) async {
Future<void> storeRoomUpdate(
String roomId,
SyncRoomUpdate roomUpdate,
Event? lastEvent,
Client client,
) async {
// Leave room if membership is leave
if (roomUpdate is LeftRoomUpdate) {
await forgetRoom(roomId);
@ -1244,7 +1316,8 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
id: roomId,
membership: membership,
lastEvent: lastEvent,
).toJson());
).toJson(),
);
} else if (roomUpdate is JoinedRoomUpdate) {
final currentRoom = Room.fromJson(copyMap(currentRawRoom), client);
await _roomsBox.put(
@ -1259,12 +1332,14 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
notificationCount:
roomUpdate.unreadNotifications?.notificationCount?.toInt() ??
currentRoom.notificationCount,
prev_batch:
roomUpdate.timeline?.prevBatch ?? currentRoom.prev_batch,
summary: RoomSummary.fromJson(currentRoom.summary.toJson()
..addAll(roomUpdate.summary?.toJson() ?? {})),
prev_batch: roomUpdate.timeline?.prevBatch ?? currentRoom.prev_batch,
summary: RoomSummary.fromJson(
currentRoom.summary.toJson()
..addAll(roomUpdate.summary?.toJson() ?? {}),
),
lastEvent: lastEvent,
).toJson());
).toJson(),
);
}
}
@ -1274,7 +1349,11 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
@override
Future<void> storeSSSSCache(
String type, String keyId, String ciphertext, String content) async {
String type,
String keyId,
String ciphertext,
String content,
) async {
await _ssssCacheBox.put(
type,
SSSSCache(
@ -1282,7 +1361,8 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
keyId: keyId,
ciphertext: ciphertext,
content: content,
).toJson());
).toJson(),
);
}
@override
@ -1293,8 +1373,13 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
}
@override
Future<void> storeUserCrossSigningKey(String userId, String publicKey,
String content, bool verified, bool blocked) async {
Future<void> storeUserCrossSigningKey(
String userId,
String publicKey,
String content,
bool verified,
bool blocked,
) async {
await _userCrossSigningKeysBox.put(
TupleKey(userId, publicKey).toString(),
{
@ -1308,8 +1393,14 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
}
@override
Future<void> storeUserDeviceKey(String userId, String deviceId,
String content, bool verified, bool blocked, int lastActive) async {
Future<void> storeUserDeviceKey(
String userId,
String deviceId,
String content,
bool verified,
bool blocked,
int lastActive,
) async {
await _userDeviceKeysBox.put(TupleKey(userId, deviceId).toString(), {
'user_id': userId,
'device_id': deviceId,
@ -1350,8 +1441,10 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
if (tokenExpiresAt == null) {
await _clientBox.delete('token_expires_at');
} else {
await _clientBox.put('token_expires_at',
tokenExpiresAt.millisecondsSinceEpoch.toString());
await _clientBox.put(
'token_expires_at',
tokenExpiresAt.millisecondsSinceEpoch.toString(),
);
}
if (refreshToken == null) {
await _clientBox.delete('refresh_token');
@ -1393,11 +1486,15 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
@override
Future<void> updateInboundGroupSessionAllowedAtIndex(
String allowedAtIndex, String roomId, String sessionId) async {
String allowedAtIndex,
String roomId,
String sessionId,
) async {
final raw = await _inboundGroupSessionsBox.get(sessionId);
if (raw == null) {
Logs().w(
'Tried to update inbound group session as uploaded which wasnt found in the database!');
'Tried to update inbound group session as uploaded which wasnt found in the database!',
);
return;
}
raw['allowed_at_index'] = allowedAtIndex;
@ -1407,11 +1504,15 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
@override
Future<void> updateInboundGroupSessionIndexes(
String indexes, String roomId, String sessionId) async {
String indexes,
String roomId,
String sessionId,
) async {
final raw = await _inboundGroupSessionsBox.get(sessionId);
if (raw == null) {
Logs().w(
'Tried to update inbound group session indexes of a session which was not found in the database!');
'Tried to update inbound group session indexes of a session which was not found in the database!',
);
return;
}
final json = copyMap(raw);
@ -1510,11 +1611,15 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
}
for (final key in json[_preloadRoomStateBoxName]!.keys) {
await _preloadRoomStateBox.put(
key, json[_preloadRoomStateBoxName]![key]);
key,
json[_preloadRoomStateBoxName]![key],
);
}
for (final key in json[_nonPreloadRoomStateBoxName]!.keys) {
await _nonPreloadRoomStateBox.put(
key, json[_nonPreloadRoomStateBoxName]![key]);
key,
json[_nonPreloadRoomStateBoxName]![key],
);
}
for (final key in json[_roomMembersBoxName]!.keys) {
await _roomMembersBox.put(key, json[_roomMembersBoxName]![key]);
@ -1527,15 +1632,21 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
}
for (final key in json[_inboundGroupSessionsBoxName]!.keys) {
await _inboundGroupSessionsBox.put(
key, json[_inboundGroupSessionsBoxName]![key]);
key,
json[_inboundGroupSessionsBoxName]![key],
);
}
for (final key in json[_inboundGroupSessionsUploadQueueBoxName]!.keys) {
await _inboundGroupSessionsUploadQueueBox.put(
key, json[_inboundGroupSessionsUploadQueueBoxName]![key]);
key,
json[_inboundGroupSessionsUploadQueueBoxName]![key],
);
}
for (final key in json[_outboundGroupSessionsBoxName]!.keys) {
await _outboundGroupSessionsBox.put(
key, json[_outboundGroupSessionsBoxName]![key]);
key,
json[_outboundGroupSessionsBoxName]![key],
);
}
for (final key in json[_olmSessionsBoxName]!.keys) {
await _olmSessionsBox.put(key, json[_olmSessionsBoxName]![key]);
@ -1545,11 +1656,15 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
}
for (final key in json[_userDeviceKeysOutdatedBoxName]!.keys) {
await _userDeviceKeysOutdatedBox.put(
key, json[_userDeviceKeysOutdatedBoxName]![key]);
key,
json[_userDeviceKeysOutdatedBoxName]![key],
);
}
for (final key in json[_userCrossSigningKeysBoxName]!.keys) {
await _userCrossSigningKeysBox.put(
key, json[_userCrossSigningKeysBoxName]![key]);
key,
json[_userCrossSigningKeysBoxName]![key],
);
}
for (final key in json[_ssssCacheBoxName]!.keys) {
await _ssssCacheBox.put(key, json[_ssssCacheBoxName]![key]);
@ -1559,7 +1674,9 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
}
for (final key in json[_timelineFragmentsBoxName]!.keys) {
await _timelineFragmentsBox.put(
key, json[_timelineFragmentsBoxName]![key]);
key,
json[_timelineFragmentsBoxName]![key],
);
}
for (final key in json[_seenDeviceIdsBoxName]!.keys) {
await _seenDeviceIdsBox.put(key, json[_seenDeviceIdsBoxName]![key]);
@ -1585,7 +1702,8 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
// Get the synced event IDs from the store
final timelineKey = TupleKey(room.id, '').toString();
final timelineEventIds = List<String>.from(
(await _timelineFragmentsBox.get(timelineKey)) ?? []);
(await _timelineFragmentsBox.get(timelineKey)) ?? [],
);
// Get the local stored SENDING events from the store
late final List<String> sendingEventIds;
@ -1594,7 +1712,8 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
} else {
final sendingTimelineKey = TupleKey(room.id, 'SENDING').toString();
sendingEventIds = List<String>.from(
(await _timelineFragmentsBox.get(sendingTimelineKey)) ?? []);
(await _timelineFragmentsBox.get(sendingTimelineKey)) ?? [],
);
}
// Combine those two lists while respecting the start and limit parameters.
@ -1667,13 +1786,17 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
@override
Future<CachedProfileInformation?> getUserProfile(String userId) =>
_userProfilesBox.get(userId).then((json) => json == null
_userProfilesBox.get(userId).then(
(json) => json == null
? null
: CachedProfileInformation.fromJson(copyMap(json)));
: CachedProfileInformation.fromJson(copyMap(json)),
);
@override
Future<void> storeUserProfile(
String userId, CachedProfileInformation profile) =>
String userId,
CachedProfileInformation profile,
) =>
_userProfilesBox.put(
userId,
profile.toJson(),

View File

@ -127,7 +127,8 @@ class Box<V> {
if (value == null) return null;
if (value is! String) {
throw Exception(
'Wrong database type! Expected String but got one of type ${value.runtimeType}');
'Wrong database type! Expected String but got one of type ${value.runtimeType}',
);
}
switch (V) {
case const (int):

View File

@ -111,7 +111,8 @@ class SQfLiteEncryptionHelper {
}
Logs().d(
'Warning: Found unencrypted sqlite database. Encrypting using SQLCipher.');
'Warning: Found unencrypted sqlite database. Encrypting using SQLCipher.',
);
// hell, it's unencrypted. This should not happen. Time to encrypt it.
final plainDb = await factory.openDatabase(path);
@ -119,7 +120,8 @@ class SQfLiteEncryptionHelper {
final encryptedPath = '$path.encrypted';
await plainDb.execute(
"ATTACH DATABASE '$encryptedPath' AS encrypted KEY '$cipher';");
"ATTACH DATABASE '$encryptedPath' AS encrypted KEY '$cipher';",
);
await plainDb.execute("SELECT sqlcipher_export('encrypted');");
// ignore: prefer_single_quotes
await plainDb.execute("DETACH DATABASE encrypted;");
@ -164,7 +166,8 @@ class SQfLiteEncryptionHelper {
} else {
final version = cipherVersion.singleOrNull?['cipher_version'];
Logs().d(
'PRAGMA supported by bundled SQLite. Encryption supported. SQLCipher version: $version.');
'PRAGMA supported by bundled SQLite. Encryption supported. SQLCipher version: $version.',
);
}
final result = await database.rawQuery("PRAGMA KEY='$cipher';");

View File

@ -44,7 +44,8 @@ class Event extends MatrixEvent {
);
@Deprecated(
'Use eventSender instead or senderFromMemoryOrFallback for a synchronous alternative')
'Use eventSender instead or senderFromMemoryOrFallback for a synchronous alternative',
)
User get sender => senderFromMemoryOrFallback;
User get senderFromMemoryOrFallback =>
@ -138,7 +139,7 @@ class Event extends MatrixEvent {
timeline: TimelineUpdate(
events: [MatrixEvent.fromJson(json)],
),
)
),
},
),
),
@ -188,9 +189,11 @@ class Event extends MatrixEvent {
final originalSource =
Event.getMapFromPayload(jsonPayload['original_source']);
return Event(
status: eventStatusFromInt(jsonPayload['status'] ??
status: eventStatusFromInt(
jsonPayload['status'] ??
unsigned[messageSendingStatusKey] ??
defaultStatus.intValue),
defaultStatus.intValue,
),
stateKey: jsonPayload['state_key'],
prevContent: prevContent,
content: content,
@ -198,12 +201,13 @@ class Event extends MatrixEvent {
eventId: jsonPayload['event_id'] ?? '',
senderId: jsonPayload['sender'],
originServerTs: DateTime.fromMillisecondsSinceEpoch(
jsonPayload['origin_server_ts'] ?? 0),
jsonPayload['origin_server_ts'] ?? 0,
),
unsigned: unsigned,
room: room,
originalSource: originalSource.isEmpty
? null
: MatrixEvent.fromJson(originalSource));
originalSource:
originalSource.isEmpty ? null : MatrixEvent.fromJson(originalSource),
);
}
@override
@ -311,9 +315,12 @@ class Event extends MatrixEvent {
final receipts = room.receiptState;
final receiptsList = receipts.global.otherUsers.entries
.where((entry) => entry.value.eventId == eventId)
.map((entry) => Receipt(
.map(
(entry) => Receipt(
room.unsafeGetUserFromMemoryOrFallback(entry.key),
entry.value.timestamp))
entry.value.timestamp,
),
)
.toList();
// add your own only once
@ -321,21 +328,31 @@ class Event extends MatrixEvent {
receipts.mainThread?.latestOwnReceipt;
if (own != null && own.eventId == eventId) {
receiptsList.add(
Receipt(room.unsafeGetUserFromMemoryOrFallback(room.client.userID!),
own.timestamp),
Receipt(
room.unsafeGetUserFromMemoryOrFallback(room.client.userID!),
own.timestamp,
),
);
}
// also add main thread. https://github.com/famedly/product-management/issues/1020
// also deduplicate.
receiptsList.addAll(receipts.mainThread?.otherUsers.entries
.where((entry) =>
receiptsList.addAll(
receipts.mainThread?.otherUsers.entries
.where(
(entry) =>
entry.value.eventId == eventId &&
receiptsList.every((element) => element.user.id != entry.key))
.map((entry) => Receipt(
receiptsList
.every((element) => element.user.id != entry.key),
)
.map(
(entry) => Receipt(
room.unsafeGetUserFromMemoryOrFallback(entry.key),
entry.value.timestamp)) ??
[]);
entry.value.timestamp,
),
) ??
[],
);
return receiptsList;
}
@ -383,7 +400,7 @@ class Event extends MatrixEvent {
timeline: TimelineUpdate(
events: [redactedBecause],
),
)
),
},
),
),
@ -553,14 +570,15 @@ class Event extends MatrixEvent {
///
/// Important! To use this link you have to set a http header like this:
/// `headers: {"authorization": "Bearer ${client.accessToken}"}`
Future<Uri?> getAttachmentUri(
{bool getThumbnail = false,
Future<Uri?> getAttachmentUri({
bool getThumbnail = false,
bool useThumbnailMxcUrl = false,
double width = 800.0,
double height = 800.0,
ThumbnailMethod method = ThumbnailMethod.scale,
int minNoThumbSize = _minNoThumbSize,
bool animated = false}) async {
bool animated = false,
}) async {
if (![EventTypes.Message, EventTypes.Sticker].contains(type) ||
!hasAttachment ||
isAttachmentEncrypted) {
@ -607,14 +625,15 @@ class Event extends MatrixEvent {
/// Important! To use this link you have to set a http header like this:
/// `headers: {"authorization": "Bearer ${client.accessToken}"}`
@Deprecated('Use getAttachmentUri() instead')
Uri? getAttachmentUrl(
{bool getThumbnail = false,
Uri? getAttachmentUrl({
bool getThumbnail = false,
bool useThumbnailMxcUrl = false,
double width = 800.0,
double height = 800.0,
ThumbnailMethod method = ThumbnailMethod.scale,
int minNoThumbSize = _minNoThumbSize,
bool animated = false}) {
bool animated = false,
}) {
if (![EventTypes.Message, EventTypes.Sticker].contains(type) ||
!hasAttachment ||
isAttachmentEncrypted) {
@ -680,10 +699,11 @@ class Event extends MatrixEvent {
/// true to download the thumbnail instead. Set [fromLocalStoreOnly] to true
/// if you want to retrieve the attachment from the local store only without
/// making http request.
Future<MatrixFile> downloadAndDecryptAttachment(
{bool getThumbnail = false,
Future<MatrixFile> downloadAndDecryptAttachment({
bool getThumbnail = false,
Future<Uint8List> Function(Uri)? downloadCallback,
bool fromLocalStoreOnly = false}) async {
bool fromLocalStoreOnly = false,
}) async {
if (![EventTypes.Message, EventTypes.Sticker].contains(type)) {
throw ("This event has the type '$type' and so it can't contain an attachment.");
}
@ -730,7 +750,10 @@ class Event extends MatrixEvent {
uint8list.lengthInBytes < database.maxFileSize;
if (storeable) {
await database.storeFile(
mxcUrl, uint8list, DateTime.now().millisecondsSinceEpoch);
mxcUrl,
uint8list,
DateTime.now().millisecondsSinceEpoch,
);
}
} else if (uint8list == null) {
throw ('Unable to download file from local store.');
@ -771,12 +794,14 @@ class Event extends MatrixEvent {
/// it to plain text.
/// [removeMarkdown] allow to remove the markdown formating from the event body.
/// Usefull form message preview or notifications text.
Future<String> calcLocalizedBody(MatrixLocalizations i18n,
{bool withSenderNamePrefix = false,
Future<String> calcLocalizedBody(
MatrixLocalizations i18n, {
bool withSenderNamePrefix = false,
bool hideReply = false,
bool hideEdit = false,
bool plaintextBody = false,
bool removeMarkdown = false}) async {
bool removeMarkdown = false,
}) async {
if (redacted) {
await redactedBecause?.fetchSenderUser();
}
@ -799,30 +824,36 @@ class Event extends MatrixEvent {
}
@Deprecated('Use calcLocalizedBody or calcLocalizedBodyFallback')
String getLocalizedBody(MatrixLocalizations i18n,
{bool withSenderNamePrefix = false,
String getLocalizedBody(
MatrixLocalizations i18n, {
bool withSenderNamePrefix = false,
bool hideReply = false,
bool hideEdit = false,
bool plaintextBody = false,
bool removeMarkdown = false}) =>
calcLocalizedBodyFallback(i18n,
bool removeMarkdown = false,
}) =>
calcLocalizedBodyFallback(
i18n,
withSenderNamePrefix: withSenderNamePrefix,
hideReply: hideReply,
hideEdit: hideEdit,
plaintextBody: plaintextBody,
removeMarkdown: removeMarkdown);
removeMarkdown: removeMarkdown,
);
/// Works similar to `calcLocalizedBody()` but does not wait for the sender
/// user to be fetched. If it is not in the cache it will just use the
/// fallback and display the localpart of the MXID according to the
/// values of `formatLocalpart` and `mxidLocalPartFallback` in the `Client`
/// class.
String calcLocalizedBodyFallback(MatrixLocalizations i18n,
{bool withSenderNamePrefix = false,
String calcLocalizedBodyFallback(
MatrixLocalizations i18n, {
bool withSenderNamePrefix = false,
bool hideReply = false,
bool hideEdit = false,
bool plaintextBody = false,
bool removeMarkdown = false}) {
bool removeMarkdown = false,
}) {
if (redacted) {
if (status.intValue < EventStatus.synced.intValue) {
return i18n.cancelledSend;
@ -896,7 +927,9 @@ class Event extends MatrixEvent {
// if the message is formatted
if (hideReply && mayHaveReplyFallback) {
body = body.replaceFirst(
RegExp(r'^>( \*)? <[^>]+>[^\n\r]+\r?\n(> [^\n]*\r?\n)*\r?\n'), '');
RegExp(r'^>( \*)? <[^>]+>[^\n\r]+\r?\n(> [^\n]*\r?\n)*\r?\n'),
'',
);
}
// return the html tags free body
@ -979,11 +1012,13 @@ class Event extends MatrixEvent {
// we need to check again if it isn't empty, as we potentially removed all
// aggregated edits
if (allEditEvents.isNotEmpty) {
allEditEvents.sort((a, b) => a.originServerTs.millisecondsSinceEpoch -
allEditEvents.sort(
(a, b) => a.originServerTs.millisecondsSinceEpoch -
b.originServerTs.millisecondsSinceEpoch >
0
? 1
: -1);
: -1,
);
final rawEvent = allEditEvents.last.toJson();
// update the content of the new event to render
if (rawEvent['content']['m.new_content'] is Map) {
@ -1051,9 +1086,14 @@ class Event extends MatrixEvent {
if (isRichMessage) {
// calcUnlocalizedBody strips out the <img /> tags in favor of a :placeholder:
final formattedTextStripped = formattedText.replaceAll(
RegExp('<mx-reply>.*</mx-reply>',
caseSensitive: false, multiLine: false, dotAll: true),
'');
RegExp(
'<mx-reply>.*</mx-reply>',
caseSensitive: false,
multiLine: false,
dotAll: true,
),
'',
);
return _onlyEmojiEmoteRegex.hasMatch(formattedTextStripped);
} else {
return _onlyEmojiRegex.hasMatch(plaintextBody);
@ -1068,9 +1108,14 @@ class Event extends MatrixEvent {
if (isRichMessage) {
// calcUnlocalizedBody strips out the <img /> tags in favor of a :placeholder:
final formattedTextStripped = formattedText.replaceAll(
RegExp('<mx-reply>.*</mx-reply>',
caseSensitive: false, multiLine: false, dotAll: true),
'');
RegExp(
'<mx-reply>.*</mx-reply>',
caseSensitive: false,
multiLine: false,
dotAll: true,
),
'',
);
return _countEmojiEmoteRegex.allMatches(formattedTextStripped).length;
} else {
return _countEmojiRegex.allMatches(plaintextBody).length;
@ -1083,7 +1128,8 @@ class Event extends MatrixEvent {
final status = unsigned?.tryGet<String>(fileSendingStatusKey);
if (status == null) return null;
return FileSendingStatus.values.singleWhereOrNull(
(fileSendingStatus) => fileSendingStatus.name == status);
(fileSendingStatus) => fileSendingStatus.name == status,
);
}
}

View File

@ -52,7 +52,7 @@ extension EventStatusExtension on EventStatus {
bool get isSent => [
EventStatus.sent,
EventStatus.synced,
EventStatus.roomState
EventStatus.roomState,
].contains(this);
/// Returns `true` if the status is `synced` or `roomState`:

View File

@ -170,7 +170,8 @@ class LatestReceiptStateForTimeline {
ownPrivate: null,
ownPublic: null,
latestOwnReceipt: null,
otherUsers: {});
otherUsers: {},
);
factory LatestReceiptStateForTimeline.fromJson(Map<String, dynamic> json) {
final private = json['private'];
@ -229,7 +230,8 @@ class LatestReceiptState {
mainThread:
main.isNotEmpty ? LatestReceiptStateForTimeline.fromJson(main) : null,
byThread: byThread.map(
(k, v) => MapEntry(k, LatestReceiptStateForTimeline.fromJson(v))),
(k, v) => MapEntry(k, LatestReceiptStateForTimeline.fromJson(v)),
),
);
}

View File

@ -5,6 +5,9 @@ class TimelineChunk {
String nextBatch;
List<Event> events;
TimelineChunk(
{required this.events, this.prevBatch = '', this.nextBatch = ''});
TimelineChunk({
required this.events,
this.prevBatch = '',
this.nextBatch = '',
});
}

View File

@ -31,7 +31,8 @@ class CachedPresence {
.singleWhere((type) => type.name == json['presence']),
lastActiveTimestamp: json['last_active_timestamp'] != null
? DateTime.fromMillisecondsSinceEpoch(
json['last_active_timestamp'] as int)
json['last_active_timestamp'] as int,
)
: null,
statusMsg: json['status_msg'] as String?,
currentlyActive: json['currently_active'] as bool?,
@ -55,8 +56,13 @@ class CachedPresence {
this.currentlyActive,
});
CachedPresence(this.presence, int? lastActiveAgo, this.statusMsg,
this.currentlyActive, this.userid) {
CachedPresence(
this.presence,
int? lastActiveAgo,
this.statusMsg,
this.currentlyActive,
this.userid,
) {
if (lastActiveAgo != null) {
lastActiveTimestamp =
DateTime.now().subtract(Duration(milliseconds: lastActiveAgo));
@ -69,11 +75,17 @@ class CachedPresence {
event.presence.lastActiveAgo,
event.presence.statusMsg,
event.presence.currentlyActive,
event.senderId);
event.senderId,
);
CachedPresence.fromPresenceResponse(GetPresenceResponse event, String userid)
: this(event.presence, event.lastActiveAgo, event.statusMsg,
event.currentlyActive, userid);
: this(
event.presence,
event.lastActiveAgo,
event.statusMsg,
event.currentlyActive,
userid,
);
CachedPresence.neverSeen(this.userid) : presence = PresenceType.offline;
@ -91,7 +103,7 @@ class CachedPresence {
final json = {
'content': content,
'sender': '@example:localhost',
'type': 'm.presence'
'type': 'm.presence',
};
return Presence.fromJson(json);

View File

@ -115,9 +115,11 @@ class Room {
if (!partial) {
return;
}
final allStates = await client.database
?.getUnimportantRoomEventStatesForRoom(
client.importantStateEvents.toList(), this);
final allStates =
await client.database?.getUnimportantRoomEventStatesForRoom(
client.importantStateEvents.toList(),
this,
);
if (allStates != null) {
for (final state in allStates) {
@ -216,12 +218,16 @@ class Room {
if (heroes == null) return [];
return await Future.wait(heroes.map((hero) async =>
return await Future.wait(
heroes.map(
(hero) async =>
(await requestUser(
hero,
ignoreErrors: true,
)) ??
User(hero, room: this)));
User(hero, room: this),
),
);
}
/// Returns a localized displayname for this server. If the room is a groupchat
@ -251,8 +257,10 @@ class Room {
// removing oneself from the hero list
(hero) => hero.isNotEmpty && hero != client.userID,
)
.map((hero) => unsafeGetUserFromMemoryOrFallback(hero)
.calcDisplayname(i18n: i18n))
.map(
(hero) => unsafeGetUserFromMemoryOrFallback(hero)
.calcDisplayname(i18n: i18n),
)
.join(', ');
if (isAbandonedDMRoom) {
return i18n.wasDirectChatDisplayName(result);
@ -274,7 +282,8 @@ class Room {
if (directChatMatrixID != null) {
return i18n.wasDirectChatDisplayName(
unsafeGetUserFromMemoryOrFallback(directChatMatrixID)
.calcDisplayname(i18n: i18n));
.calcDisplayname(i18n: i18n),
);
}
}
return i18n.emptyChat;
@ -455,7 +464,8 @@ class Room {
tag,
Tag(
order: order,
));
),
);
/// Removes a tag from the room.
Future<void> removeTag(String tag) => client.deleteRoomTag(
@ -495,8 +505,8 @@ class Room {
return MarkedUnread.fromJson(
roomAccountData[EventType.markedUnread]?.content ??
roomAccountData[EventType.oldMarkedUnread]?.content ??
{})
.unread;
{},
).unread;
}
/// Checks if the last event has a read marker of the user.
@ -526,7 +536,8 @@ class Room {
LatestReceiptState get receiptState => LatestReceiptState.fromJson(
roomAccountData[LatestReceiptState.eventType]?.content ??
<String, dynamic>{});
<String, dynamic>{},
);
/// Returns true if this room is unread. To check if there are new messages
/// in muted rooms, use [hasNewMessages].
@ -564,7 +575,7 @@ class Room {
type: EventType.markedUnread,
),
],
)
),
},
),
),
@ -600,31 +611,38 @@ class Room {
/// Sends a normal text message to this room. Returns the event ID generated
/// by the server for this message.
Future<String?> sendTextEvent(String message,
{String? txid,
Future<String?> sendTextEvent(
String message, {
String? txid,
Event? inReplyTo,
String? editEventId,
bool parseMarkdown = true,
bool parseCommands = true,
String msgtype = MessageTypes.Text,
String? threadRootEventId,
String? threadLastEventId}) {
String? threadLastEventId,
}) {
if (parseCommands) {
return client.parseAndRunCommand(this, message,
return client.parseAndRunCommand(
this,
message,
inReplyTo: inReplyTo,
editEventId: editEventId,
txid: txid,
threadRootEventId: threadRootEventId,
threadLastEventId: threadLastEventId);
threadLastEventId: threadLastEventId,
);
}
final event = <String, dynamic>{
'msgtype': msgtype,
'body': message,
};
if (parseMarkdown) {
final html = markdown(event['body'],
final html = markdown(
event['body'],
getEmotePacks: () => getImagePacksFlat(ImagePackUsage.emoticon),
getMention: getMention);
getMention: getMention,
);
// if the decoded html is the same as the body, there is no need in sending a formatted message
if (HtmlUnescape().convert(html.replaceAll(RegExp(r'<br />\n?'), '\n')) !=
event['body']) {
@ -645,13 +663,17 @@ class Room {
/// Sends a reaction to an event with an [eventId] and the content [key] into a room.
/// Returns the event ID generated by the server for this reaction.
Future<String?> sendReaction(String eventId, String key, {String? txid}) {
return sendEvent({
return sendEvent(
{
'm.relates_to': {
'rel_type': RelationshipTypes.reaction,
'event_id': eventId,
'key': key,
},
}, type: EventTypes.Reaction, txid: txid);
},
type: EventTypes.Reaction,
txid: txid,
);
}
/// Sends the location with description [body] and geo URI [geoUri] into a room.
@ -845,10 +867,10 @@ class Room {
'ext': true,
'k': encryptedFile.k,
'key_ops': ['encrypt', 'decrypt'],
'kty': 'oct'
'kty': 'oct',
},
'iv': encryptedFile.iv,
'hashes': {'sha256': encryptedFile.sha256}
'hashes': {'sha256': encryptedFile.sha256},
},
'info': {
...file.info,
@ -864,16 +886,16 @@ class Room {
'ext': true,
'k': encryptedThumbnail.k,
'key_ops': ['encrypt', 'decrypt'],
'kty': 'oct'
'kty': 'oct',
},
'iv': encryptedThumbnail.iv,
'hashes': {'sha256': encryptedThumbnail.sha256}
'hashes': {'sha256': encryptedThumbnail.sha256},
},
if (thumbnail != null) 'thumbnail_info': thumbnail.info,
if (thumbnail?.blurhash != null &&
file is MatrixImageFile &&
file.blurhash == null)
'xyz.amorgan.blurhash': thumbnail!.blurhash
'xyz.amorgan.blurhash': thumbnail!.blurhash,
},
if (extraContent != null) ...extraContent,
};
@ -898,12 +920,16 @@ class Room {
/// encryption security level.
Future<EncryptionHealthState> calcEncryptionHealthState() async {
final users = await requestParticipants();
users.removeWhere((u) =>
users.removeWhere(
(u) =>
!{Membership.invite, Membership.join}.contains(u.membership) ||
!client.userDeviceKeys.containsKey(u.id));
!client.userDeviceKeys.containsKey(u.id),
);
if (users.any((u) =>
client.userDeviceKeys[u.id]!.verified != UserVerifiedStatus.verified)) {
if (users.any(
(u) =>
client.userDeviceKeys[u.id]!.verified != UserVerifiedStatus.verified,
)) {
return EncryptionHealthState.unverifiedDevices;
}
@ -983,9 +1009,14 @@ class Room {
? inReplyTo.formattedText
: htmlEscape.convert(inReplyTo.body).replaceAll('\n', '<br>'))
.replaceAll(
RegExp(r'<mx-reply>.*</mx-reply>',
caseSensitive: false, multiLine: false, dotAll: true),
'');
RegExp(
r'<mx-reply>.*</mx-reply>',
caseSensitive: false,
multiLine: false,
dotAll: true,
),
'',
);
final repliedHtml = content.tryGet<String>('formatted_body') ??
htmlEscape
.convert(content.tryGet<String>('body') ?? '')
@ -1017,7 +1048,7 @@ class Room {
'm.in_reply_to': {
'event_id': threadLastEventId,
},
}
},
};
}
@ -1085,7 +1116,8 @@ class Room {
.add(Duration(milliseconds: e.retryAfterMs!))
.isAfter(timeoutDate)) {
Logs().w(
'Ratelimited while sending message, waiting for ${e.retryAfterMs}ms');
'Ratelimited while sending message, waiting for ${e.retryAfterMs}ms',
);
await Future.delayed(Duration(milliseconds: e.retryAfterMs!));
} else if (e is MatrixException ||
e is EventTooLarge ||
@ -1202,7 +1234,8 @@ class Room {
if (users is! Map<String, Object?>) {
if (users != null) {
Logs().v(
'Repairing Power Level "users" has the wrong type "${powerLevelMapCopy['users'].runtimeType}"');
'Repairing Power Level "users" has the wrong type "${powerLevelMapCopy['users'].runtimeType}"',
);
}
users = powerLevelMapCopy['users'] = <String, Object?>{};
}
@ -1232,10 +1265,11 @@ class Room {
/// be received maximum. When the request is answered, [onHistoryReceived] will be triggered **before**
/// the historical events will be published in the onEvent stream.
/// Returns the actual count of received timeline events.
Future<int> requestHistory(
{int historyCount = defaultHistoryCount,
Future<int> requestHistory({
int historyCount = defaultHistoryCount,
void Function()? onHistoryReceived,
direction = Direction.b}) async {
direction = Direction.b,
}) async {
final prev_batch = this.prev_batch;
final storeInDatabase = !isArchived;
@ -1270,11 +1304,10 @@ class Room {
events: direction == Direction.b
? resp.chunk
: resp.chunk.reversed.toList(),
prevBatch: direction == Direction.b
? resp.end
: resp.start,
prevBatch:
direction == Direction.b ? resp.end : resp.start,
),
),
)
}
: null,
leave: membership != Membership.join
@ -1286,15 +1319,16 @@ class Room {
events: direction == Direction.b
? resp.chunk
: resp.chunk.reversed.toList(),
prevBatch: direction == Direction.b
? resp.end
: resp.start,
prevBatch:
direction == Direction.b ? resp.end : resp.start,
),
),
}
: null),
: null,
),
direction: Direction.b);
),
direction: Direction.b,
);
}
if (client.database != null) {
@ -1368,8 +1402,11 @@ class Room {
/// read receipt's location.
/// If you set `public` to false, only a private receipt will be sent. A private receipt is always sent if `mRead` is set. If no value is provided, the default from the `client` is used.
/// You can leave out the `eventId`, which will not update the read marker but just send receipts, but there are few cases where that makes sense.
Future<void> setReadMarker(String? eventId,
{String? mRead, bool? public}) async {
Future<void> setReadMarker(
String? eventId, {
String? mRead,
bool? public,
}) async {
await client.setReadMarker(
id,
mFullyRead: eventId,
@ -1381,15 +1418,16 @@ class Room {
}
Future<TimelineChunk?> getEventContext(String eventId) async {
final resp = await client.getEventContext(id, eventId,
limit: Room.defaultHistoryCount
final resp = await client.getEventContext(
id, eventId,
limit: Room.defaultHistoryCount,
// filter: jsonEncode(StateFilter(lazyLoadMembers: true).toJson()),
);
final events = [
if (resp.eventsAfter != null) ...resp.eventsAfter!.reversed,
if (resp.event != null) resp.event!,
if (resp.eventsBefore != null) ...resp.eventsBefore!
if (resp.eventsBefore != null) ...resp.eventsBefore!,
].map((e) => Event.fromMatrixEvent(e, this)).toList();
// Try again to decrypt encrypted events but don't update the database.
@ -1406,7 +1444,10 @@ class Room {
}
final chunk = TimelineChunk(
nextBatch: resp.end ?? '', prevBatch: resp.start ?? '', events: events);
nextBatch: resp.end ?? '',
prevBatch: resp.start ?? '',
events: events,
);
return chunk;
}
@ -1415,9 +1456,12 @@ class Room {
/// specified. In general you want to use `setReadMarker` instead to set private
/// and public receipt as well as the marker at the same time.
@Deprecated(
'Use setReadMarker with mRead set instead. That allows for more control and there are few cases to not send a marker at the same time.')
Future<void> postReceipt(String eventId,
{ReceiptType type = ReceiptType.mRead}) async {
'Use setReadMarker with mRead set instead. That allows for more control and there are few cases to not send a marker at the same time.',
)
Future<void> postReceipt(
String eventId, {
ReceiptType type = ReceiptType.mRead,
}) async {
await client.postReceipt(
id,
ReceiptType.mRead,
@ -1435,13 +1479,14 @@ class Room {
/// [onChange], [onRemove], [onInsert] and the [onHistoryReceived] callbacks.
/// This method can also retrieve the timeline at a specific point by setting
/// the [eventContextId]
Future<Timeline> getTimeline(
{void Function(int index)? onChange,
Future<Timeline> getTimeline({
void Function(int index)? onChange,
void Function(int index)? onRemove,
void Function(int insertID)? onInsert,
void Function()? onNewEvent,
void Function()? onUpdate,
String? eventContextId}) async {
String? eventContextId,
}) async {
await postLoad();
List<Event> events;
@ -1484,7 +1529,8 @@ class Room {
onRemove: onRemove,
onInsert: onInsert,
onNewEvent: onNewEvent,
onUpdate: onUpdate);
onUpdate: onUpdate,
);
// Fetch all users from database we have got here.
if (eventContextId == null) {
@ -1536,12 +1582,13 @@ class Room {
/// List `membershipFilter` defines with what membership do you want the
/// participants, default set to
/// [[Membership.join, Membership.invite, Membership.knock]]
List<User> getParticipants(
[List<Membership> membershipFilter = const [
List<User> getParticipants([
List<Membership> membershipFilter = const [
Membership.join,
Membership.invite,
Membership.knock,
]]) {
],
]) {
final members = states[EventTypes.RoomMember];
if (members != null) {
return members.entries
@ -1560,14 +1607,15 @@ class Room {
/// [[Membership.join, Membership.invite, Membership.knock]]
/// Set [cache] to `false` if you do not want to cache the users in memory
/// for this session which is highly recommended for large public rooms.
Future<List<User>> requestParticipants(
[List<Membership> membershipFilter = const [
Future<List<User>> requestParticipants([
List<Membership> membershipFilter = const [
Membership.join,
Membership.invite,
Membership.knock,
],
bool suppressWarning = false,
bool cache = true]) async {
bool cache = true,
]) async {
if (!participantListComplete || partial) {
// we aren't fully loaded, maybe the users are in the database
// We always need to check the database in the partial case, since state
@ -1624,7 +1672,8 @@ class Room {
}
@Deprecated(
'The method was renamed unsafeGetUserFromMemoryOrFallback. Please prefer requestParticipants.')
'The method was renamed unsafeGetUserFromMemoryOrFallback. Please prefer requestParticipants.',
)
User getUserByMXIDSync(String mxID) {
return unsafeGetUserFromMemoryOrFallback(mxID);
}
@ -1801,12 +1850,14 @@ class Room {
final cache = _inflightUserRequests[parameters] ??= AsyncCache.ephemeral();
try {
final user = await cache.fetch(() => _requestUser(
final user = await cache.fetch(
() => _requestUser(
mxID,
ignoreErrors: ignoreErrors,
requestState: requestState,
requestProfile: requestProfile,
));
),
);
_inflightUserRequests.remove(parameters);
return user;
} catch (_) {
@ -1937,7 +1988,7 @@ class Room {
final eventsMap = newPowerLevelMap.tryGetMap<String, Object?>('events') ??
<String, Object?>{};
eventsMap.addAll({
EventTypes.GroupCallMember: getDefaultPowerLevel(currentPowerLevelsMap)
EventTypes.GroupCallMember: getDefaultPowerLevel(currentPowerLevelsMap),
});
newPowerLevelMap.addAll({'events': eventsMap});
await client.setRoomStateWithKey(
@ -2099,7 +2150,7 @@ class Room {
id,
[PushRuleAction.dontNotify],
conditions: [
PushCondition(kind: 'event_match', key: 'room_id', pattern: id)
PushCondition(kind: 'event_match', key: 'room_id', pattern: id),
],
);
}
@ -2107,8 +2158,11 @@ class Room {
}
/// Redacts this event. Throws `ErrorResponse` on error.
Future<String?> redactEvent(String eventId,
{String? reason, String? txid}) async {
Future<String?> redactEvent(
String eventId, {
String? reason,
String? txid,
}) async {
// Create new transaction id
String messageID;
final now = DateTime.now().millisecondsSinceEpoch;
@ -2167,7 +2221,8 @@ class Room {
?.content
.tryGet<String>('guest_access');
return GuestAccess.values.singleWhereOrNull(
(element) => element.text == guestAccessString) ??
(element) => element.text == guestAccessString,
) ??
GuestAccess.forbidden;
}
@ -2193,7 +2248,8 @@ class Room {
?.content
.tryGet<String>('history_visibility');
return HistoryVisibility.values.singleWhereOrNull(
(element) => element.text == historyVisibilityString);
(element) => element.text == historyVisibilityString,
);
}
/// Changes the history visibility. You should check first if the user is able to change it.
@ -2259,8 +2315,10 @@ class Room {
await client.encryption?.keyManager.request(this, sessionId, senderKey);
}
Future<void> _handleFakeSync(SyncUpdate syncUpdate,
{Direction? direction}) async {
Future<void> _handleFakeSync(
SyncUpdate syncUpdate, {
Direction? direction,
}) async {
if (client.database != null) {
await client.database?.transaction(() async {
await client.handleSync(syncUpdate, direction: direction);
@ -2309,9 +2367,11 @@ class Room {
.where((child) => child.via.isNotEmpty)
.toList() ??
[])
..sort((a, b) => a.order.isEmpty || b.order.isEmpty
..sort(
(a, b) => a.order.isEmpty || b.order.isEmpty
? b.order.compareTo(a.order)
: a.order.compareTo(b.order));
: a.order.compareTo(b.order),
);
/// Adds or edits a child of this space.
Future<void> setSpaceChild(
@ -2337,7 +2397,8 @@ class Room {
Future<Uri> matrixToInviteLink() async {
if (canonicalAlias.isNotEmpty) {
return Uri.parse(
'https://matrix.to/#/${Uri.encodeComponent(canonicalAlias)}');
'https://matrix.to/#/${Uri.encodeComponent(canonicalAlias)}',
);
}
final List queryParameters = [];
final users = await requestParticipants([Membership.join]);
@ -2347,8 +2408,9 @@ class Room {
temp.removeWhere((user) => user.powerLevel < 50);
if (currentPowerLevelsMap != null) {
// just for weird rooms
temp.removeWhere((user) =>
user.powerLevel < getDefaultPowerLevel(currentPowerLevelsMap));
temp.removeWhere(
(user) => user.powerLevel < getDefaultPowerLevel(currentPowerLevelsMap),
);
}
if (temp.isNotEmpty) {
@ -2368,10 +2430,9 @@ class Room {
}
}
}
final sortedServers = Map.fromEntries(servers.entries.toList()
..sort((e1, e2) => e2.value.compareTo(e1.value)))
.keys
.take(3);
final sortedServers = Map.fromEntries(
servers.entries.toList()..sort((e1, e2) => e2.value.compareTo(e1.value)),
).keys.take(3);
for (final server in sortedServers) {
if (!queryParameters.contains(server)) {
queryParameters.add(server);
@ -2386,7 +2447,8 @@ class Room {
queryString += 'via=${queryParameters[i]}';
}
return Uri.parse(
'https://matrix.to/#/${Uri.encodeComponent(id)}$queryString');
'https://matrix.to/#/${Uri.encodeComponent(id)}$queryString',
);
}
/// Remove a child from this space by setting the `via` to an empty list.

View File

@ -84,8 +84,9 @@ class Timeline {
(room.prev_batch != null && events.last.type != EventTypes.RoomCreate);
}
Future<void> requestHistory(
{int historyCount = Room.defaultHistoryCount}) async {
Future<void> requestHistory({
int historyCount = Room.defaultHistoryCount,
}) async {
if (isRequestingHistory) {
return;
}
@ -97,8 +98,9 @@ class Timeline {
bool get canRequestFuture => !allowNewEvent;
Future<void> requestFuture(
{int historyCount = Room.defaultHistoryCount}) async {
Future<void> requestFuture({
int historyCount = Room.defaultHistoryCount,
}) async {
if (allowNewEvent) {
return; // we shouldn't force to add new events if they will autatically be added
}
@ -109,9 +111,10 @@ class Timeline {
isRequestingFuture = false;
}
Future<void> _requestEvents(
{int historyCount = Room.defaultHistoryCount,
required Direction direction}) async {
Future<void> _requestEvents({
int historyCount = Room.defaultHistoryCount,
required Direction direction,
}) async {
onUpdate?.call();
try {
@ -184,9 +187,10 @@ class Timeline {
/// be received maximum. When the request is answered, [onHistoryReceived] will be triggered **before**
/// the historical events will be published in the onEvent stream.
/// Returns the actual count of received timeline events.
Future<int> getRoomEvents(
{int historyCount = Room.defaultHistoryCount,
direction = Direction.b}) async {
Future<int> getRoomEvents({
int historyCount = Room.defaultHistoryCount,
direction = Direction.b,
}) async {
final resp = await room.client.getRoomEvents(
room.id,
direction,
@ -212,10 +216,12 @@ class Timeline {
newNextBatch != null) {
if (type == EventUpdateType.history) {
Logs().w(
'[nav] we can still request history prevBatch: $type $newPrevBatch');
'[nav] we can still request history prevBatch: $type $newPrevBatch',
);
} else {
Logs().w(
'[nav] we can still request timeline nextBatch: $type $newNextBatch');
'[nav] we can still request timeline nextBatch: $type $newNextBatch',
);
}
}
@ -230,7 +236,8 @@ class Timeline {
Logs().d('We now allow sync update into the timeline.');
newEvents.addAll(
await room.client.database?.getEventList(room, onlySending: true) ??
[]);
[],
);
}
}
@ -272,14 +279,15 @@ class Timeline {
return resp.chunk.length;
}
Timeline(
{required this.room,
Timeline({
required this.room,
this.onUpdate,
this.onChange,
this.onInsert,
this.onRemove,
this.onNewEvent,
required this.chunk}) {
required this.chunk,
}) {
sub = room.client.onEvent.stream.listen(_handleEventUpdate);
// If the timeline is limited we want to clear our events cache
@ -430,11 +438,14 @@ class Timeline {
}
void _removeEventFromSet(Set<Event> eventSet, Event event) {
eventSet.removeWhere((e) =>
eventSet.removeWhere(
(e) =>
e.matchesEventOrTransactionId(event.eventId) ||
event.unsigned != null &&
e.matchesEventOrTransactionId(
event.unsigned?.tryGet<String>('transaction_id')));
event.unsigned?.tryGet<String>('transaction_id'),
),
);
}
void addAggregatedEvent(Event event) {
@ -483,17 +494,20 @@ class Timeline {
if (!allowNewEvent) return;
final status = eventStatusFromInt(eventUpdate.content['status'] ??
final status = eventStatusFromInt(
eventUpdate.content['status'] ??
(eventUpdate.content['unsigned'] is Map<String, dynamic>
? eventUpdate.content['unsigned'][messageSendingStatusKey]
: null) ??
EventStatus.synced.intValue);
EventStatus.synced.intValue,
);
final i = _findEvent(
event_id: eventUpdate.content['event_id'],
unsigned_txid: eventUpdate.content['unsigned'] is Map
? eventUpdate.content['unsigned']['transaction_id']
: null);
: null,
);
if (i < events.length) {
// if the old status is larger than the new one, we also want to preserve the old status
@ -517,7 +531,8 @@ class Timeline {
if (eventUpdate.type == EventUpdateType.history &&
events.indexWhere(
(e) => e.eventId == eventUpdate.content['event_id']) !=
(e) => e.eventId == eventUpdate.content['event_id'],
) !=
-1) return;
var index = events.length;
if (eventUpdate.type == EventUpdateType.history) {
@ -548,10 +563,12 @@ class Timeline {
}
}
events[index].setRedactionEvent(Event.fromJson(
events[index].setRedactionEvent(
Event.fromJson(
eventUpdate.content,
room,
));
),
);
onChange?.call(index);
}
}

View File

@ -72,12 +72,15 @@ class User extends StrippedStateEvent {
/// invite
/// leave
/// ban
Membership get membership => Membership.values.firstWhere((e) {
Membership get membership => Membership.values.firstWhere(
(e) {
if (content['membership'] != null) {
return e.toString() == 'Membership.${content['membership']}';
}
return false;
}, orElse: () => Membership.join);
},
orElse: () => Membership.join,
);
/// The avatar if the user has one.
Uri? get avatarUrl {
@ -94,10 +97,11 @@ class User extends StrippedStateEvent {
/// the first character of each word becomes uppercase.
/// If [mxidLocalPartFallback] is true, then the local part of the mxid will be shown
/// if there is no other displayname available. If not then this will return "Unknown user".
String calcDisplayname(
{bool? formatLocalpart,
String calcDisplayname({
bool? formatLocalpart,
bool? mxidLocalPartFallback,
MatrixLocalizations i18n = const MatrixDefaultLocalizations()}) {
MatrixLocalizations i18n = const MatrixDefaultLocalizations(),
}) {
formatLocalpart ??= room.client.formatLocalpart;
mxidLocalPartFallback ??= room.client.mxidLocalPartFallback;
final displayName = this.displayName;
@ -203,10 +207,12 @@ class User extends StrippedStateEvent {
// get all the users with the same display name
final allUsersWithSameDisplayname = room.getParticipants();
allUsersWithSameDisplayname.removeWhere((user) =>
allUsersWithSameDisplayname.removeWhere(
(user) =>
user.id == id ||
(user.displayName?.isEmpty ?? true) ||
user.displayName != displayName);
user.displayName != displayName,
);
if (allUsersWithSameDisplayname.isEmpty) {
return identifier;
}

View File

@ -25,7 +25,9 @@ extension CommandsClientExtension on Client {
/// Add a command to the command handler. `command` is its name, and `callback` is the
/// callback to invoke
void addCommand(
String command, FutureOr<String?> Function(CommandArgs) callback) {
String command,
FutureOr<String?> Function(CommandArgs) callback,
) {
commands[command.toLowerCase()] = callback;
}
@ -313,12 +315,13 @@ class CommandArgs {
String? threadRootEventId;
String? threadLastEventId;
CommandArgs(
{required this.msg,
CommandArgs({
required this.msg,
this.editEventId,
this.inReplyTo,
required this.room,
this.txid,
this.threadRootEventId,
this.threadLastEventId});
this.threadLastEventId,
});
}

View File

@ -1,15 +1,22 @@
import 'dart:async';
typedef ComputeCallback = Future<R> Function<Q, R>(
FutureOr<R> Function(Q message) callback, Q message,
{String? debugLabel});
FutureOr<R> Function(Q message) callback,
Q message, {
String? debugLabel,
});
// keep types in sync with [computeCallbackFromRunInBackground]
typedef ComputeRunner = Future<T> Function<T, U>(
FutureOr<T> Function(U arg) function, U arg);
FutureOr<T> Function(U arg) function,
U arg,
);
ComputeCallback computeCallbackFromRunInBackground(ComputeRunner runner) {
return <U, T>(FutureOr<T> Function(U arg) callback, U arg,
{String? debugLabel}) =>
return <U, T>(
FutureOr<T> Function(U arg) callback,
U arg, {
String? debugLabel,
}) =>
runner.call(callback, arg);
}

View File

@ -50,7 +50,8 @@ final PKCS5_PBKDF2_HMAC = libcrypto.lookupFunction<
IntPtr iter,
Pointer<NativeType> digest,
IntPtr keylen,
Pointer<Uint8> out),
Pointer<Uint8> out,
),
int Function(
Pointer<Uint8> pass,
int passlen,
@ -59,7 +60,8 @@ final PKCS5_PBKDF2_HMAC = libcrypto.lookupFunction<
int iter,
Pointer<NativeType> digest,
int keylen,
Pointer<Uint8> out)>('PKCS5_PBKDF2_HMAC');
Pointer<Uint8> out,
)>('PKCS5_PBKDF2_HMAC');
final EVP_sha1 = libcrypto.lookupFunction<Pointer<NativeType> Function(),
Pointer<NativeType> Function()>('EVP_sha1');
@ -86,34 +88,49 @@ final EVP_EncryptInit_ex = libcrypto.lookupFunction<
Pointer<NativeType> alg,
Pointer<NativeType> some,
Pointer<Uint8> key,
Pointer<Uint8> iv),
Pointer<Uint8> iv,
),
Pointer<NativeType> Function(
Pointer<NativeType> ctx,
Pointer<NativeType> alg,
Pointer<NativeType> some,
Pointer<Uint8> key,
Pointer<Uint8> iv)>('EVP_EncryptInit_ex');
Pointer<Uint8> iv,
)>('EVP_EncryptInit_ex');
final EVP_EncryptUpdate = libcrypto.lookupFunction<
Pointer<NativeType> Function(Pointer<NativeType> ctx, Pointer<Uint8> output,
Pointer<IntPtr> outputLen, Pointer<Uint8> input, IntPtr inputLen),
Pointer<NativeType> Function(
Pointer<NativeType> ctx,
Pointer<Uint8> output,
Pointer<IntPtr> outputLen,
Pointer<Uint8> input,
int inputLen)>('EVP_EncryptUpdate');
IntPtr inputLen,
),
Pointer<NativeType> Function(
Pointer<NativeType> ctx,
Pointer<Uint8> output,
Pointer<IntPtr> outputLen,
Pointer<Uint8> input,
int inputLen,
)>('EVP_EncryptUpdate');
final EVP_EncryptFinal_ex = libcrypto.lookupFunction<
Pointer<NativeType> Function(
Pointer<NativeType> ctx, Pointer<Uint8> data, Pointer<IntPtr> len),
Pointer<NativeType> Function(Pointer<NativeType> ctx, Pointer<Uint8> data,
Pointer<IntPtr> len)>('EVP_EncryptFinal_ex');
Pointer<NativeType> ctx,
Pointer<Uint8> data,
Pointer<IntPtr> len,
),
Pointer<NativeType> Function(
Pointer<NativeType> ctx,
Pointer<Uint8> data,
Pointer<IntPtr> len,
)>('EVP_EncryptFinal_ex');
final EVP_CIPHER_CTX_free = libcrypto.lookupFunction<
Pointer<NativeType> Function(Pointer<NativeType> ctx),
Pointer<NativeType> Function(
Pointer<NativeType> ctx)>('EVP_CIPHER_CTX_free');
Pointer<NativeType> ctx,
)>('EVP_CIPHER_CTX_free');
final EVP_Digest = libcrypto.lookupFunction<
IntPtr Function(
@ -122,14 +139,16 @@ final EVP_Digest = libcrypto.lookupFunction<
Pointer<Uint8> hash,
Pointer<IntPtr> hsize,
Pointer<NativeType> alg,
Pointer<NativeType> engine),
Pointer<NativeType> engine,
),
int Function(
Pointer<Uint8> data,
int len,
Pointer<Uint8> hash,
Pointer<IntPtr> hsize,
Pointer<NativeType> alg,
Pointer<NativeType> engine)>('EVP_Digest');
Pointer<NativeType> engine,
)>('EVP_Digest');
final EVP_MD_size = () {
// EVP_MD_size was renamed to EVP_MD_get_size in Openssl3.0.

View File

@ -35,7 +35,10 @@ abstract class Cipher {
String name;
Object params(Uint8List iv);
Future<Uint8List> encrypt(
Uint8List input, Uint8List key, Uint8List iv) async {
Uint8List input,
Uint8List key,
Uint8List iv,
) async {
final subtleKey = await importKey('raw', key, name, false, ['encrypt']);
return (await subtle.encrypt(params(iv), subtleKey, input)).asUint8List();
}
@ -51,14 +54,24 @@ class _AesCtr extends Cipher {
AesCtrParams(name: name, counter: iv, length: 64);
}
Future<Uint8List> pbkdf2(Uint8List passphrase, Uint8List salt, Hash hash,
int iterations, int bits) async {
Future<Uint8List> pbkdf2(
Uint8List passphrase,
Uint8List salt,
Hash hash,
int iterations,
int bits,
) async {
final raw =
await importKey('raw', passphrase, 'PBKDF2', false, ['deriveBits']);
final res = await deriveBits(
Pbkdf2Params(
name: 'PBKDF2', hash: hash.name, salt: salt, iterations: iterations),
name: 'PBKDF2',
hash: hash.name,
salt: salt,
iterations: iterations,
),
raw,
bits);
bits,
);
return Uint8List.view(res);
}

View File

@ -90,7 +90,12 @@ class _AesCtr extends Cipher {
}
FutureOr<Uint8List> pbkdf2(
Uint8List passphrase, Uint8List salt, Hash hash, int iterations, int bits) {
Uint8List passphrase,
Uint8List salt,
Hash hash,
int iterations,
int bits,
) {
final outLen = bits ~/ 8;
final mem = malloc.call<Uint8>(passphrase.length + salt.length + outLen);
final saltMem = mem.elementAt(passphrase.length);
@ -98,8 +103,16 @@ FutureOr<Uint8List> pbkdf2(
try {
mem.asTypedList(passphrase.length).setAll(0, passphrase);
saltMem.asTypedList(salt.length).setAll(0, salt);
PKCS5_PBKDF2_HMAC(mem, passphrase.length, saltMem, salt.length, iterations,
hash.ptr, outLen, outMem);
PKCS5_PBKDF2_HMAC(
mem,
passphrase.length,
saltMem,
salt.length,
iterations,
hash.ptr,
outLen,
outMem,
);
return Uint8List.fromList(outMem.asTypedList(outLen));
} finally {
malloc.free(mem);

View File

@ -50,13 +50,24 @@ Future<ByteBuffer> decrypt(dynamic algorithm, dynamic key, Uint8List data) {
}
@JS('crypto.subtle.importKey')
external dynamic _importKey(String format, dynamic keyData, dynamic algorithm,
bool extractable, List<String> keyUsages);
external dynamic _importKey(
String format,
dynamic keyData,
dynamic algorithm,
bool extractable,
List<String> keyUsages,
);
Future<dynamic> importKey(String format, dynamic keyData, dynamic algorithm,
bool extractable, List<String> keyUsages) {
Future<dynamic> importKey(
String format,
dynamic keyData,
dynamic algorithm,
bool extractable,
List<String> keyUsages,
) {
return promiseToFuture(
_importKey(format, keyData, algorithm, extractable, keyUsages));
_importKey(format, keyData, algorithm, extractable, keyUsages),
);
}
@JS('crypto.subtle.exportKey')
@ -67,13 +78,30 @@ Future<dynamic> exportKey(String algorithm, dynamic key) {
}
@JS('crypto.subtle.deriveKey')
external dynamic _deriveKey(dynamic algorithm, dynamic baseKey,
dynamic derivedKeyAlgorithm, bool extractable, List<String> keyUsages);
external dynamic _deriveKey(
dynamic algorithm,
dynamic baseKey,
dynamic derivedKeyAlgorithm,
bool extractable,
List<String> keyUsages,
);
Future<ByteBuffer> deriveKey(dynamic algorithm, dynamic baseKey,
dynamic derivedKeyAlgorithm, bool extractable, List<String> keyUsages) {
return promiseToFuture(_deriveKey(
algorithm, baseKey, derivedKeyAlgorithm, extractable, keyUsages));
Future<ByteBuffer> deriveKey(
dynamic algorithm,
dynamic baseKey,
dynamic derivedKeyAlgorithm,
bool extractable,
List<String> keyUsages,
) {
return promiseToFuture(
_deriveKey(
algorithm,
baseKey,
derivedKeyAlgorithm,
extractable,
keyUsages,
),
);
}
@JS('crypto.subtle.deriveBits')

View File

@ -95,7 +95,10 @@ class DeviceKeysList {
} else {
// start verification with verified devices
final request = KeyVerification(
encryption: encryption, userId: userId, deviceId: '*');
encryption: encryption,
userId: userId,
deviceId: '*',
);
await request.start();
encryption.keyVerificationManager.addRequest(request);
return request;
@ -106,8 +109,8 @@ class DeviceKeysList {
Map<String, dynamic> dbEntry,
List<Map<String, dynamic>> childEntries,
List<Map<String, dynamic>> crossSigningEntries,
this.client)
: userId = dbEntry['user_id'] ?? '' {
this.client,
) : userId = dbEntry['user_id'] ?? '' {
outdated = dbEntry['outdated'];
deviceKeys = {};
for (final childEntry in childEntries) {
@ -195,8 +198,11 @@ abstract class SignableKey extends MatrixSignableKey {
return String.fromCharCodes(canonicalJson.encode(data));
}
bool _verifySignature(String pubKey, String signature,
{bool isSignatureWithoutLibolmValid = false}) {
bool _verifySignature(
String pubKey,
String signature, {
bool isSignatureWithoutLibolmValid = false,
}) {
olm.Utility olmutil;
try {
olmutil = olm.Utility();
@ -316,7 +322,8 @@ abstract class SignableKey extends MatrixSignableKey {
verifiedOnly: verifiedOnly,
visited: visited_,
onlyValidateUserIds: onlyValidateUserIds,
verifiedByTheirMasterKey: verifiedByTheirMasterKey);
verifiedByTheirMasterKey: verifiedByTheirMasterKey,
);
if (haveChain) {
return true;
}
@ -396,8 +403,9 @@ class CrossSigningKey extends SignableKey {
}
CrossSigningKey.fromMatrixCrossSigningKey(
MatrixCrossSigningKey key, Client client)
: super.fromJson(key.toJson().copy(), client) {
MatrixCrossSigningKey key,
Client client,
) : super.fromJson(key.toJson().copy(), client) {
final json = toJson();
identifier = key.publicKey;
usage = json['usage'].cast<String>();
@ -445,8 +453,10 @@ class DeviceKeys extends SignableKey {
// without libolm we still want to be able to add devices. In that case we ofc just can't
// verify the signature
_verifySignature(
ed25519Key!, signatures![userId]!['ed25519:$deviceId']!,
isSignatureWithoutLibolmValid: true));
ed25519Key!,
signatures![userId]!['ed25519:$deviceId']!,
isSignatureWithoutLibolmValid: true,
));
@override
bool get blocked => super.blocked || !selfSigned;
@ -480,9 +490,11 @@ class DeviceKeys extends SignableKey {
?.setBlockedUserDeviceKey(newBlocked, userId, deviceId!);
}
DeviceKeys.fromMatrixDeviceKeys(MatrixDeviceKeys keys, Client client,
[DateTime? lastActiveTs])
: super.fromJson(keys.toJson().copy(), client) {
DeviceKeys.fromMatrixDeviceKeys(
MatrixDeviceKeys keys,
Client client, [
DateTime? lastActiveTs,
]) : super.fromJson(keys.toJson().copy(), client) {
final json = toJson();
identifier = keys.deviceId;
algorithms = json['algorithms'].cast<String>();
@ -518,7 +530,10 @@ class DeviceKeys extends SignableKey {
}
final request = KeyVerification(
encryption: encryption, userId: userId, deviceId: deviceId!);
encryption: encryption,
userId: userId,
deviceId: deviceId!,
);
await request.start();
encryption.keyVerificationManager.addRequest(request);

View File

@ -28,46 +28,61 @@ abstract class EventLocalizations {
// Thus, it seems easier to offload that logic into `Event.getLocalizedBody()` and pass the
// `body` variable around here.
static String _localizedBodyNormalMessage(
Event event, MatrixLocalizations i18n, String body) {
Event event,
MatrixLocalizations i18n,
String body,
) {
switch (event.messageType) {
case MessageTypes.Image:
return i18n.sentAPicture(
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n));
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
);
case MessageTypes.File:
return i18n.sentAFile(
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n));
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
);
case MessageTypes.Audio:
return i18n.sentAnAudio(
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n));
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
);
case MessageTypes.Video:
return i18n.sentAVideo(
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n));
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
);
case MessageTypes.Location:
return i18n.sharedTheLocation(
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n));
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
);
case MessageTypes.Sticker:
return i18n.sentASticker(
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n));
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
);
case MessageTypes.Emote:
return '* $body';
case EventTypes.KeyVerificationRequest:
return i18n.requestedKeyVerification(
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n));
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
);
case EventTypes.KeyVerificationCancel:
return i18n.canceledKeyVerification(
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n));
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
);
case EventTypes.KeyVerificationDone:
return i18n.completedKeyVerification(
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n));
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
);
case EventTypes.KeyVerificationReady:
return i18n.isReadyForKeyVerification(
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n));
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
);
case EventTypes.KeyVerificationAccept:
return i18n.acceptedKeyVerification(
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n));
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
);
case EventTypes.KeyVerificationStart:
return i18n.startedKeyVerification(
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n));
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
);
case MessageTypes.BadEncrypted:
String errorText;
switch (event.body) {
@ -102,27 +117,35 @@ abstract class EventLocalizations {
String Function(Event event, MatrixLocalizations i18n, String body)?>
localizationsMap = {
EventTypes.Sticker: (event, i18n, body) => i18n.sentASticker(
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n)),
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
),
EventTypes.Redaction: (event, i18n, body) => i18n.redactedAnEvent(event),
EventTypes.RoomAliases: (event, i18n, body) => i18n.changedTheRoomAliases(
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n)),
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
),
EventTypes.RoomCanonicalAlias: (event, i18n, body) =>
i18n.changedTheRoomInvitationLink(
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n)),
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
),
EventTypes.RoomCreate: (event, i18n, body) => i18n.createdTheChat(
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n)),
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
),
EventTypes.RoomTombstone: (event, i18n, body) => i18n.roomHasBeenUpgraded,
EventTypes.RoomJoinRules: (event, i18n, body) {
final joinRules = JoinRules.values.firstWhereOrNull((r) =>
final joinRules = JoinRules.values.firstWhereOrNull(
(r) =>
r.toString().replaceAll('JoinRules.', '') ==
event.content['join_rule']);
event.content['join_rule'],
);
if (joinRules == null) {
return i18n.changedTheJoinRules(
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n));
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
);
} else {
return i18n.changedTheJoinRulesTo(
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
joinRules.getLocalizedString(i18n));
joinRules.getLocalizedString(i18n),
);
}
},
EventTypes.RoomMember: (event, i18n, body) {
@ -188,58 +211,75 @@ abstract class EventLocalizations {
},
EventTypes.RoomPowerLevels: (event, i18n, body) =>
i18n.changedTheChatPermissions(
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n)),
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
),
EventTypes.RoomName: (event, i18n, body) => i18n.changedTheChatNameTo(
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
event.content.tryGet<String>('name') ?? ''),
event.content.tryGet<String>('name') ?? '',
),
EventTypes.RoomTopic: (event, i18n, body) =>
i18n.changedTheChatDescriptionTo(
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
event.content.tryGet<String>('topic') ?? ''),
event.content.tryGet<String>('topic') ?? '',
),
EventTypes.RoomAvatar: (event, i18n, body) => i18n.changedTheChatAvatar(
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n)),
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
),
EventTypes.GuestAccess: (event, i18n, body) {
final guestAccess = GuestAccess.values.firstWhereOrNull((r) =>
final guestAccess = GuestAccess.values.firstWhereOrNull(
(r) =>
r.toString().replaceAll('GuestAccess.', '') ==
event.content['guest_access']);
event.content['guest_access'],
);
if (guestAccess == null) {
return i18n.changedTheGuestAccessRules(
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n));
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
);
} else {
return i18n.changedTheGuestAccessRulesTo(
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
guestAccess.getLocalizedString(i18n));
guestAccess.getLocalizedString(i18n),
);
}
},
EventTypes.HistoryVisibility: (event, i18n, body) {
final historyVisibility = HistoryVisibility.values.firstWhereOrNull((r) =>
final historyVisibility = HistoryVisibility.values.firstWhereOrNull(
(r) =>
r.toString().replaceAll('HistoryVisibility.', '') ==
event.content['history_visibility']);
event.content['history_visibility'],
);
if (historyVisibility == null) {
return i18n.changedTheHistoryVisibility(
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n));
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
);
} else {
return i18n.changedTheHistoryVisibilityTo(
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
historyVisibility.getLocalizedString(i18n));
historyVisibility.getLocalizedString(i18n),
);
}
},
EventTypes.Encryption: (event, i18n, body) {
var localizedBody = i18n.activatedEndToEndEncryption(
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n));
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
);
if (event.room.client.encryptionEnabled == false) {
localizedBody += '. ${i18n.needPantalaimonWarning}';
}
return localizedBody;
},
EventTypes.CallAnswer: (event, i18n, body) => i18n.answeredTheCall(
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n)),
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
),
EventTypes.CallHangup: (event, i18n, body) => i18n.endedTheCall(
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n)),
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
),
EventTypes.CallInvite: (event, i18n, body) => i18n.startedACall(
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n)),
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
),
EventTypes.CallCandidates: (event, i18n, body) => i18n.sentCallInformations(
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n)),
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
),
EventTypes.Encrypted: (event, i18n, body) =>
_localizedBodyNormalMessage(event, i18n, body),
EventTypes.Message: (event, i18n, body) =>

View File

@ -53,8 +53,11 @@ class EventUpdate {
// The json payload of the content of this event.
final Map<String, dynamic> content;
EventUpdate(
{required this.roomID, required this.type, required this.content});
EventUpdate({
required this.roomID,
required this.type,
required this.content,
});
Future<EventUpdate> decrypt(Room room, {bool store = false}) async {
final encryption = room.client.encryption;
@ -65,10 +68,16 @@ class EventUpdate {
}
try {
final decrpytedEvent = await encryption.decryptRoomEvent(
room.id, Event.fromJson(content, room),
store: store, updateType: type);
room.id,
Event.fromJson(content, room),
store: store,
updateType: type,
);
return EventUpdate(
roomID: roomID, type: type, content: decrpytedEvent.toJson());
roomID: roomID,
type: type,
content: decrpytedEvent.toJson(),
);
} catch (e, s) {
Logs().e('[LibOlm] Could not decrypt megolm event', e, s);
return this;

View File

@ -32,9 +32,14 @@ class HtmlToText {
// miss-matching tags, and this way we actually correctly identify what we want to strip and, well,
// strip it.
final renderHtml = html.replaceAll(
RegExp('<mx-reply>.*</mx-reply>',
caseSensitive: false, multiLine: false, dotAll: true),
'');
RegExp(
'<mx-reply>.*</mx-reply>',
caseSensitive: false,
multiLine: false,
dotAll: true,
),
'',
);
final opts = _ConvertOpts();
var reply = _walkNode(opts, parseFragment(renderHtml));
@ -63,7 +68,9 @@ class HtmlToText {
text = text.substring(match.end);
// remove the </code> closing tag
text = text.replaceAll(
RegExp(r'</code>$', multiLine: false, caseSensitive: false), '');
RegExp(r'</code>$', multiLine: false, caseSensitive: false),
'',
);
text = HtmlUnescape().convert(text);
if (text.isNotEmpty) {
if (text[0] != '\n') {
@ -108,8 +115,10 @@ class HtmlToText {
_listBulletPoints[opts.listDepth % _listBulletPoints.length];
return entries
.map((s) =>
'${' ' * opts.listDepth}$bulletPoint ${s.replaceAll('\n', '\n${' ' * opts.listDepth} ')}')
.map(
(s) =>
'${' ' * opts.listDepth}$bulletPoint ${s.replaceAll('\n', '\n${' ' * opts.listDepth} ')}',
)
.join('\n');
}
@ -124,15 +133,20 @@ class HtmlToText {
: 1;
return entries
.mapIndexed((index, s) =>
'${' ' * opts.listDepth}${start + index}. ${s.replaceAll('\n', '\n${' ' * opts.listDepth} ')}')
.mapIndexed(
(index, s) =>
'${' ' * opts.listDepth}${start + index}. ${s.replaceAll('\n', '\n${' ' * opts.listDepth} ')}',
)
.join('\n');
}
static const _listBulletPoints = <String>['', '', '', ''];
static List<String> _listChildNodes(_ConvertOpts opts, Element node,
[Iterable<String>? types]) {
static List<String> _listChildNodes(
_ConvertOpts opts,
Element node, [
Iterable<String>? types,
]) {
final replies = <String>[];
for (final child in node.nodes) {
if (types != null &&

View File

@ -21,7 +21,9 @@ import 'dart:async';
import 'package:http/http.dart' as http;
http.StreamedResponse replaceStream(
http.StreamedResponse base, Stream<List<int>> stream) =>
http.StreamedResponse base,
Stream<List<int>> stream,
) =>
http.StreamedResponse(
http.ByteStream(stream),
base.statusCode,

View File

@ -50,7 +50,8 @@ extension ImagePackRoomExtension on Room {
room?.getLocalizedDisplayname() ??
finalSlug
..pack.avatarUrl = imagePack.pack.avatarUrl ?? room?.avatar
..pack.attribution = imagePack.pack.attribution)
..pack.attribution = imagePack.pack.attribution,
)
.images[entry.key] = image;
allMxcs.add(image.url);
}
@ -71,8 +72,11 @@ extension ImagePackRoomExtension on Room {
final stateKey = stateKeyEntry.key;
final fallbackSlug =
'${room.getLocalizedDisplayname()}-${stateKey.isNotEmpty ? '$stateKey-' : ''}${room.id}';
addImagePack(room.getState('im.ponies.room_emotes', stateKey),
room: room, slug: fallbackSlug);
addImagePack(
room.getState('im.ponies.room_emotes', stateKey),
room: room,
slug: fallbackSlug,
);
}
}
}
@ -81,11 +85,13 @@ extension ImagePackRoomExtension on Room {
final allRoomEmotes = states['im.ponies.room_emotes'];
if (allRoomEmotes != null) {
for (final entry in allRoomEmotes.entries) {
addImagePack(entry.value,
addImagePack(
entry.value,
room: this,
slug: (entry.value.stateKey?.isNotEmpty == true)
? entry.value.stateKey
: 'room');
: 'room',
);
}
}
return packs;
@ -94,6 +100,8 @@ extension ImagePackRoomExtension on Room {
/// Get a flat view of all the image packs of a specified [usage], that is a map of all
/// slugs to a map of the image code to their mxc url
Map<String, Map<String, String>> getImagePacksFlat([ImagePackUsage? usage]) =>
getImagePacks(usage).map((k, v) =>
MapEntry(k, v.images.map((k, v) => MapEntry(k, v.url.toString()))));
getImagePacks(usage).map(
(k, v) =>
MapEntry(k, v.images.map((k, v) => MapEntry(k, v.url.toString()))),
);
}

View File

@ -155,7 +155,7 @@ class BlockLatexSyntax extends BlockSyntax {
// we use .substring(2) as childLines will *always* contain the first two '$$'
final latex = childLines.join('\n').trim().substring(2).trim();
final element = Element('div', [
Element('pre', [Element.text('code', htmlEscape.convert(latex))])
Element('pre', [Element.text('code', htmlEscape.convert(latex))]),
]);
element.attributes['data-mx-maths'] = htmlAttrEscape.convert(latex);
return element;
@ -165,7 +165,8 @@ class BlockLatexSyntax extends BlockSyntax {
class PillSyntax extends InlineSyntax {
PillSyntax()
: super(
r'([@#!][^\s:]*:(?:[^\s]+\.\w+|[\d\.]+|\[[a-fA-F0-9:]+\])(?::\d+)?)');
r'([@#!][^\s:]*:(?:[^\s]+\.\w+|[\d\.]+|\[[a-fA-F0-9:]+\])(?::\d+)?)',
);
@override
bool onMatch(InlineParser parser, Match match) {
@ -269,8 +270,11 @@ String markdown(
}
extension on String {
String convertLinebreaksToBr(String tagName,
{bool exclude = false, String replaceWith = '<br/>'}) {
String convertLinebreaksToBr(
String tagName, {
bool exclude = false,
String replaceWith = '<br/>',
}) {
final parts = split('$tagName>');
var convertLinebreaks = exclude;
for (var i = 0; i < parts.length; i++) {

View File

@ -61,7 +61,9 @@ class MatrixDefaultLocalizations extends MatrixLocalizations {
@override
String changedTheGuestAccessRulesTo(
String senderName, String localizedString) =>
String senderName,
String localizedString,
) =>
'$senderName changed the guest access rules to $localizedString';
@override
@ -70,7 +72,9 @@ class MatrixDefaultLocalizations extends MatrixLocalizations {
@override
String changedTheHistoryVisibilityTo(
String senderName, String localizedString) =>
String senderName,
String localizedString,
) =>
'$senderName changed the history visibility to $localizedString';
@override

View File

@ -48,11 +48,16 @@ class MatrixFile {
/// derivatives the MIME type from the [bytes] and correspondingly creates a
/// [MatrixFile], [MatrixImageFile], [MatrixAudioFile] or a [MatrixVideoFile]
factory MatrixFile.fromMimeType(
{required Uint8List bytes, required String name, String? mimeType}) {
final msgType = msgTypeFromMime(mimeType ??
factory MatrixFile.fromMimeType({
required Uint8List bytes,
required String name,
String? mimeType,
}) {
final msgType = msgTypeFromMime(
mimeType ??
lookupMimeType(name, headerBytes: bytes) ??
'application/octet-stream');
'application/octet-stream',
);
if (msgType == MessageTypes.Image) {
return MatrixImageFile(bytes: bytes, name: name, mimeType: mimeType);
}
@ -134,8 +139,8 @@ class MatrixImageFile extends MatrixFile {
int maxDimension = 1600,
String? mimeType,
Future<MatrixImageFileResizedResponse?> Function(
MatrixImageFileResizeArguments)?
customImageResizer,
MatrixImageFileResizeArguments,
)? customImageResizer,
@Deprecated('Use [nativeImplementations] instead') ComputeRunner? compute,
NativeImplementations nativeImplementations = NativeImplementations.dummy,
}) async {
@ -148,7 +153,8 @@ class MatrixImageFile extends MatrixFile {
return await image.generateThumbnail(
dimension: maxDimension,
customImageResizer: customImageResizer,
nativeImplementations: nativeImplementations) ??
nativeImplementations: nativeImplementations,
) ??
image;
}
@ -187,8 +193,8 @@ class MatrixImageFile extends MatrixFile {
Future<MatrixImageFile?> generateThumbnail({
int dimension = Client.defaultThumbnailSize,
Future<MatrixImageFileResizedResponse?> Function(
MatrixImageFileResizeArguments)?
customImageResizer,
MatrixImageFileResizeArguments,
)? customImageResizer,
@Deprecated('Use [nativeImplementations] instead') ComputeRunner? compute,
NativeImplementations nativeImplementations = NativeImplementations.dummy,
}) async {
@ -212,7 +218,9 @@ class MatrixImageFile extends MatrixFile {
// we should take the opportunity to update the image dimension
setImageSizeIfNull(
width: resizedData.originalWidth, height: resizedData.originalHeight);
width: resizedData.originalWidth,
height: resizedData.originalHeight,
);
// the thumbnail should rather return null than the enshrined image
if (resizedData.width > dimension || resizedData.height > dimension) {
@ -233,7 +241,8 @@ class MatrixImageFile extends MatrixFile {
/// you would likely want to use [NativeImplementations] and
/// [Client.nativeImplementations] instead
static MatrixImageFileResizedResponse? calcMetadataImplementation(
Uint8List bytes) {
Uint8List bytes,
) {
final image = decodeImage(bytes);
if (image == null) return null;
@ -252,12 +261,15 @@ class MatrixImageFile extends MatrixFile {
/// you would likely want to use [NativeImplementations] and
/// [Client.nativeImplementations] instead
static MatrixImageFileResizedResponse? resizeImplementation(
MatrixImageFileResizeArguments arguments) {
MatrixImageFileResizeArguments arguments,
) {
final image = decodeImage(arguments.bytes);
final resized = copyResize(image!,
final resized = copyResize(
image!,
height: image.height > image.width ? arguments.maxDimension : null,
width: image.width >= image.height ? arguments.maxDimension : null);
width: image.width >= image.height ? arguments.maxDimension : null,
);
final encoded = encodeNamedImage(arguments.fileName, resized);
if (encoded == null) return null;
@ -302,7 +314,8 @@ class MatrixImageFileResizedResponse {
) =>
MatrixImageFileResizedResponse(
bytes: Uint8List.fromList(
(json['bytes'] as Iterable<dynamic>).whereType<int>().toList()),
(json['bytes'] as Iterable<dynamic>).whereType<int>().toList(),
),
width: json['width'],
height: json['height'],
originalHeight: json['originalHeight'],
@ -354,13 +367,14 @@ class MatrixVideoFile extends MatrixFile {
final int? height;
final int? duration;
MatrixVideoFile(
{required super.bytes,
MatrixVideoFile({
required super.bytes,
required super.name,
super.mimeType,
this.width,
this.height,
this.duration});
this.duration,
});
@override
String get msgType => 'm.video';
@ -377,11 +391,12 @@ class MatrixVideoFile extends MatrixFile {
class MatrixAudioFile extends MatrixFile {
final int? duration;
MatrixAudioFile(
{required super.bytes,
MatrixAudioFile({
required super.bytes,
required super.name,
super.mimeType,
this.duration});
this.duration,
});
@override
String get msgType => 'm.audio';

View File

@ -82,14 +82,16 @@ extension MatrixIdExtension on String {
return uri.replace(pathSegments: identifiers);
} else if (toLowerCase().startsWith(matrixToPrefix)) {
return Uri.tryParse(
'//${substring(matrixToPrefix.length - 1).replaceAllMapped(RegExp(r'(?<=/)[#!@+][^:]*:|(\?.*$)'), (m) => m[0]!.replaceAllMapped(RegExp(m.group(1) != null ? '' : '[/?]'), (m) => Uri.encodeComponent(m.group(0)!))).replaceAll('#', '%23')}');
'//${substring(matrixToPrefix.length - 1).replaceAllMapped(RegExp(r'(?<=/)[#!@+][^:]*:|(\?.*$)'), (m) => m[0]!.replaceAllMapped(RegExp(m.group(1) != null ? '' : '[/?]'), (m) => Uri.encodeComponent(m.group(0)!))).replaceAll('#', '%23')}',
);
} else {
return Uri(
pathSegments: RegExp(r'/((?:[#!@+][^:]*:)?[^/?]*)(?:\?.*$)?')
.allMatches('/$this')
.map((m) => m[1]!),
query: RegExp(r'(?:/(?:[#!@+][^:]*:)?[^/?]*)*\?(.*$)')
.firstMatch('/$this')?[1]);
.firstMatch('/$this')?[1],
);
}
}
@ -121,10 +123,11 @@ class MatrixIdentifierStringExtensionResults {
final Set<String> via;
final String? action;
MatrixIdentifierStringExtensionResults(
{required this.primaryIdentifier,
MatrixIdentifierStringExtensionResults({
required this.primaryIdentifier,
this.secondaryIdentifier,
this.queryString,
this.via = const {},
this.action});
this.action,
});
}

View File

@ -131,12 +131,16 @@ abstract class MatrixLocalizations {
String changedTheGuestAccessRules(String senderName);
String changedTheGuestAccessRulesTo(
String senderName, String localizedString);
String senderName,
String localizedString,
);
String changedTheHistoryVisibility(String senderName);
String changedTheHistoryVisibilityTo(
String senderName, String localizedString);
String senderName,
String localizedString,
);
String activatedEndToEndEncryption(String senderName);

View File

@ -140,14 +140,17 @@ class NativeImplementationsIsolate extends NativeImplementations {
// ignore: deprecated_member_use_from_same_package
/// known from [Client.runInBackground]
factory NativeImplementationsIsolate.fromRunInBackground(
ComputeRunner runInBackground) {
ComputeRunner runInBackground,
) {
return NativeImplementationsIsolate(
computeCallbackFromRunInBackground(runInBackground),
);
}
Future<T> runInBackground<T, U>(
FutureOr<T> Function(U arg) function, U arg) async {
FutureOr<T> Function(U arg) function,
U arg,
) async {
final compute = this.compute;
return await compute(function, arg);
}

View File

@ -193,8 +193,12 @@ class _OptimizedRules {
actions = EvaluatedPushRuleAction.fromActions(rule.actions);
}
EvaluatedPushRuleAction? match(Map<String, String> event, String? displayName,
int memberCount, Room room) {
EvaluatedPushRuleAction? match(
Map<String, String> event,
String? displayName,
int memberCount,
Room room,
) {
if (patterns.any((pat) => !pat.match(event))) {
return null;
}
@ -207,8 +211,10 @@ class _OptimizedRules {
return null;
}
final regex = RegExp('(^|\\W)${RegExp.escape(displayName)}(\$|\\W)',
caseSensitive: false);
final regex = RegExp(
'(^|\\W)${RegExp.escape(displayName)}(\$|\\W)',
caseSensitive: false,
);
if (!regex.hasMatch(body)) {
return null;
}
@ -217,9 +223,12 @@ class _OptimizedRules {
if (notificationPermissions.isNotEmpty) {
final sender = event.tryGet<String>('sender');
if (sender == null ||
notificationPermissions.any((notificationType) =>
!room.canSendNotification(sender,
notificationType: notificationType))) {
notificationPermissions.any(
(notificationType) => !room.canSendNotification(
sender,
notificationType: notificationType,
),
)) {
return null;
}
}
@ -258,7 +267,10 @@ class PushruleEvaluator {
actions: c.actions,
conditions: [
PushCondition(
kind: 'event_match', key: 'content.body', pattern: c.pattern)
kind: 'event_match',
key: 'content.body',
pattern: c.pattern,
),
],
ruleId: c.ruleId,
default$: c.default$,
@ -284,7 +296,10 @@ class PushruleEvaluator {
}
Map<String, String> _flattenJson(
Map<String, dynamic> obj, Map<String, String> flattened, String prefix) {
Map<String, dynamic> obj,
Map<String, String> flattened,
String prefix,
) {
for (final entry in obj.entries) {
final key = prefix == '' ? entry.key : '$prefix.${entry.key}';
final value = entry.value;

View File

@ -21,12 +21,15 @@ extension SyncUpdateItemCount on SyncUpdate {
prev +
(room.accountData?.length ?? 0) +
(room.state?.length ?? 0) +
(room.timeline?.events?.length ?? 0)) ??
(room.timeline?.events?.length ?? 0),
) ??
0;
int get _inviteRoomsItemCount =>
rooms?.invite?.values.fold<int>(
0, (prev, room) => prev + (room.inviteState?.length ?? 0)) ??
0,
(prev, room) => prev + (room.inviteState?.length ?? 0),
) ??
0;
int get _leaveRoomsItemCount =>
@ -36,6 +39,7 @@ extension SyncUpdateItemCount on SyncUpdate {
prev +
(room.accountData?.length ?? 0) +
(room.state?.length ?? 0) +
(room.timeline?.events?.length ?? 0)) ??
(room.timeline?.events?.length ?? 0),
) ??
0;
}

View File

@ -34,7 +34,10 @@ class ToDeviceEvent extends BasicEventWithSender {
factory ToDeviceEvent.fromJson(Map<String, dynamic> json) {
final event = BasicEventWithSender.fromJson(json);
return ToDeviceEvent(
sender: event.senderId, type: event.type, content: event.content);
sender: event.senderId,
type: event.type,
content: event.content,
);
}
}

View File

@ -100,7 +100,9 @@ class UiaRequest<T> {
}
Set<String> getNextStages(
List<AuthenticationFlow> flows, List<String> completed) {
List<AuthenticationFlow> flows,
List<String> completed,
) {
final nextStages = <String>{};
for (final flow in flows) {
// check the flow starts with the completed stages

View File

@ -59,11 +59,13 @@ extension MxcUriExtension on Uri {
///
/// Important! To use this link you have to set a http header like this:
/// `headers: {"authorization": "Bearer ${client.accessToken}"}`
Future<Uri> getThumbnailUri(Client client,
{num? width,
Future<Uri> getThumbnailUri(
Client client, {
num? width,
num? height,
ThumbnailMethod? method = ThumbnailMethod.crop,
bool? animated = false}) async {
bool? animated = false,
}) async {
if (!isScheme('mxc')) return Uri();
final homeserver = client.homeserver;
if (homeserver == null) {
@ -97,7 +99,8 @@ extension MxcUriExtension on Uri {
Uri getDownloadLink(Client matrix) => isScheme('mxc')
? matrix.homeserver != null
? matrix.homeserver?.resolve(
'_matrix/media/v3/download/$host${hasPort ? ':$port' : ''}$path') ??
'_matrix/media/v3/download/$host${hasPort ? ':$port' : ''}$path',
) ??
Uri()
: Uri()
: Uri();
@ -108,11 +111,13 @@ extension MxcUriExtension on Uri {
/// If `animated` (default false) is set to true, an animated thumbnail is requested
/// as per MSC2705. Thumbnails only animate if the media repository supports that.
@Deprecated('Use `getThumbnailUri()` instead')
Uri getThumbnail(Client matrix,
{num? width,
Uri getThumbnail(
Client matrix, {
num? width,
num? height,
ThumbnailMethod? method = ThumbnailMethod.crop,
bool? animated = false}) {
bool? animated = false,
}) {
if (!isScheme('mxc')) return Uri();
final homeserver = matrix.homeserver;
if (homeserver == null) {

View File

@ -71,7 +71,10 @@ class NativeImplementationsWebWorker extends NativeImplementations {
} catch (e, s) {
if (!retryInDummy) {
Logs().e(
'Web worker computation error. Ignoring and returning null', e, s);
'Web worker computation error. Ignoring and returning null',
e,
s,
);
return null;
}
Logs().e('Web worker computation error. Fallback to main thread', e, s);
@ -94,7 +97,10 @@ class NativeImplementationsWebWorker extends NativeImplementations {
} catch (e, s) {
if (!retryInDummy) {
Logs().e(
'Web worker computation error. Ignoring and returning null', e, s);
'Web worker computation error. Ignoring and returning null',
e,
s,
);
return null;
}
Logs().e('Web worker computation error. Fallback to main thread', e, s);
@ -149,4 +155,5 @@ class WebWorkerError extends Error {
/// converts a stringifyed, obfuscated [StackTrace] into a [StackTrace]
typedef WebWorkerStackTraceCallback = FutureOr<StackTrace> Function(
String obfuscatedStackTrace);
String obfuscatedStackTrace,
);

View File

@ -33,4 +33,5 @@ class WebWorkerError extends Error {
/// converts a stringifyed, obfuscated [StackTrace] into a [StackTrace]
typedef WebWorkerStackTraceCallback = FutureOr<StackTrace> Function(
String obfuscatedStackTrace);
String obfuscatedStackTrace,
);

View File

@ -47,13 +47,17 @@ Future<void> startWebWorker() async {
case WebWorkerOperations.shrinkImage:
final result = MatrixImageFile.resizeImplementation(
MatrixImageFileResizeArguments.fromJson(
Map.from(operation.data as Map)));
Map.from(operation.data as Map),
),
);
sendResponse(operation.label as double, result?.toJson());
break;
case WebWorkerOperations.calcImageMetadata:
final result = MatrixImageFile.calcMetadataImplementation(
Uint8List.fromList(
(operation.data as JsArray).whereType<int>().toList()));
(operation.data as JsArray).whereType<int>().toList(),
),
);
sendResponse(operation.label as double, result?.toJson());
break;
default:

View File

@ -24,7 +24,8 @@ abstract class CallBackend {
);
} else {
throw MatrixSDKVoipException(
'Invalid type: $type in CallBackend.fromJson');
'Invalid type: $type in CallBackend.fromJson',
);
}
}

View File

@ -75,7 +75,9 @@ class LiveKitBackend extends CallBackend {
///
/// also does the sending for you
Future<void> _makeNewSenderKey(
GroupCallSession groupCall, bool delayBeforeUsingKeyOurself) async {
GroupCallSession groupCall,
bool delayBeforeUsingKeyOurself,
) async {
final key = secureRandomBytes(32);
final keyIndex = _getNewEncryptionKeyIndex();
Logs().i('[VOIP E2EE] Generated new key $key at index $keyIndex');
@ -99,7 +101,8 @@ class LiveKitBackend extends CallBackend {
if (keyProvider == null) {
throw MatrixSDKVoipException(
'_ratchetKey called but KeyProvider was null');
'_ratchetKey called but KeyProvider was null',
);
}
final myKeys = _encryptionKeysMap[groupCall.localParticipant];
@ -120,7 +123,8 @@ class LiveKitBackend extends CallBackend {
}
Logs().i(
'[VOIP E2EE] Ratched latest key to $ratchetedKey at idx $latestLocalKeyIndex');
'[VOIP E2EE] Ratched latest key to $ratchetedKey at idx $latestLocalKeyIndex',
);
await _setEncryptionKey(
groupCall,
@ -179,9 +183,13 @@ class LiveKitBackend extends CallBackend {
// stil decrypt everything
final useKeyTimeout = Future.delayed(useKeyDelay, () async {
Logs().i(
'[VOIP E2EE] setting key changed event for ${participant.id} idx $encryptionKeyIndex key $encryptionKeyBin');
'[VOIP E2EE] setting key changed event for ${participant.id} idx $encryptionKeyIndex key $encryptionKeyBin',
);
await groupCall.voip.delegate.keyProvider?.onSetEncryptionKey(
participant, encryptionKeyBin, encryptionKeyIndex);
participant,
encryptionKeyBin,
encryptionKeyIndex,
);
if (participant.isLocal) {
_currentLocalKeyIndex = encryptionKeyIndex;
}
@ -189,9 +197,13 @@ class LiveKitBackend extends CallBackend {
_setNewKeyTimeouts.add(useKeyTimeout);
} else {
Logs().i(
'[VOIP E2EE] setting key changed event for ${participant.id} idx $encryptionKeyIndex key $encryptionKeyBin');
'[VOIP E2EE] setting key changed event for ${participant.id} idx $encryptionKeyIndex key $encryptionKeyBin',
);
await groupCall.voip.delegate.keyProvider?.onSetEncryptionKey(
participant, encryptionKeyBin, encryptionKeyIndex);
participant,
encryptionKeyBin,
encryptionKeyIndex,
);
if (participant.isLocal) {
_currentLocalKeyIndex = encryptionKeyIndex;
}
@ -214,7 +226,8 @@ class LiveKitBackend extends CallBackend {
if (myKeys == null || myLatestKey == null) {
Logs().w(
'[VOIP E2EE] _sendEncryptionKeysEvent Tried to send encryption keys event but no keys found!');
'[VOIP E2EE] _sendEncryptionKeysEvent Tried to send encryption keys event but no keys found!',
);
await _makeNewSenderKey(groupCall, false);
await _sendEncryptionKeysEvent(
groupCall,
@ -261,7 +274,8 @@ class LiveKitBackend extends CallBackend {
) async {
if (remoteParticipants.isEmpty) return;
Logs().v(
'[VOIP] _sendToDeviceEvent: sending ${data.toString()} to ${remoteParticipants.map((e) => e.id)} ');
'[VOIP] _sendToDeviceEvent: sending ${data.toString()} to ${remoteParticipants.map((e) => e.id)} ',
);
final txid =
VoIP.customTxid ?? groupCall.client.generateUniqueTransactionId();
final mustEncrypt =
@ -284,7 +298,7 @@ class LiveKitBackend extends CallBackend {
}
} else {
unencryptedDataToSend.addAll({
participant.userId: {participant.deviceId!: data}
participant.userId: {participant.deviceId!: data},
});
}
}
@ -352,11 +366,13 @@ class LiveKitBackend extends CallBackend {
if (keyContent.keys.isEmpty) {
Logs().w(
'[VOIP E2EE] Received m.call.encryption_keys where keys is empty: callId=$callId');
'[VOIP E2EE] Received m.call.encryption_keys where keys is empty: callId=$callId',
);
return;
} else {
Logs().i(
'[VOIP E2EE]: onCallEncryption, got keys from ${p.id} ${keyContent.toJson()}');
'[VOIP E2EE]: onCallEncryption, got keys from ${p.id} ${keyContent.toJson()}',
);
}
for (final key in keyContent.keys) {
@ -400,7 +416,8 @@ class LiveKitBackend extends CallBackend {
)
.isNotEmpty) {
Logs().d(
'[VOIP] onCallEncryptionKeyRequest: request checks out, sending key on index: $latestLocalKeyIndex to $userId:$deviceId');
'[VOIP] onCallEncryptionKeyRequest: request checks out, sending key on index: $latestLocalKeyIndex to $userId:$deviceId',
);
await _sendEncryptionKeysEvent(
groupCall,
_latestLocalKeyIndex,
@ -409,7 +426,7 @@ class LiveKitBackend extends CallBackend {
groupCall.voip,
userId: userId,
deviceId: deviceId,
)
),
],
);
}
@ -470,8 +487,10 @@ class LiveKitBackend extends CallBackend {
/// get everything else from your livekit sdk in your client
@override
Future<WrappedMediaStream?> initLocalStream(GroupCallSession groupCall,
{WrappedMediaStream? stream}) async {
Future<WrappedMediaStream?> initLocalStream(
GroupCallSession groupCall, {
WrappedMediaStream? stream,
}) async {
return null;
}
@ -506,25 +525,35 @@ class LiveKitBackend extends CallBackend {
@override
Future<void> setDeviceMuted(
GroupCallSession groupCall, bool muted, MediaInputKind kind) async {
GroupCallSession groupCall,
bool muted,
MediaInputKind kind,
) async {
return;
}
@override
Future<void> setScreensharingEnabled(GroupCallSession groupCall, bool enabled,
String desktopCapturerSourceId) async {
Future<void> setScreensharingEnabled(
GroupCallSession groupCall,
bool enabled,
String desktopCapturerSourceId,
) async {
return;
}
@override
Future<void> setupP2PCallWithNewMember(GroupCallSession groupCall,
CallParticipant rp, CallMembership mem) async {
Future<void> setupP2PCallWithNewMember(
GroupCallSession groupCall,
CallParticipant rp,
CallMembership mem,
) async {
return;
}
@override
Future<void> setupP2PCallsWithExistingMembers(
GroupCallSession groupCall) async {
GroupCallSession groupCall,
) async {
return;
}

View File

@ -61,7 +61,9 @@ class MeshBackend extends CallBackend {
}
Future<MediaStream> _getUserMedia(
GroupCallSession groupCall, CallType type) async {
GroupCallSession groupCall,
CallType type,
) async {
final mediaConstraints = {
'audio': UserMediaConstraints.micMediaConstraints,
'video': type == CallType.kVideo
@ -92,15 +94,19 @@ class MeshBackend extends CallBackend {
}
CallSession? _getCallForParticipant(
GroupCallSession groupCall, CallParticipant participant) {
return _callSessions.singleWhereOrNull((call) =>
GroupCallSession groupCall,
CallParticipant participant,
) {
return _callSessions.singleWhereOrNull(
(call) =>
call.groupCallId == groupCall.groupCallId &&
CallParticipant(
groupCall.voip,
userId: call.remoteUserId!,
deviceId: call.remoteDeviceId,
) ==
participant);
participant,
);
}
Future<void> _addCall(GroupCallSession groupCall, CallSession call) async {
@ -113,12 +119,15 @@ class MeshBackend extends CallBackend {
Future<void> _initCall(GroupCallSession groupCall, CallSession call) async {
if (call.remoteUserId == null) {
throw MatrixSDKVoipException(
'Cannot init call without proper invitee user and device Id');
'Cannot init call without proper invitee user and device Id',
);
}
call.onCallStateChanged.stream.listen(((event) async {
call.onCallStateChanged.stream.listen(
((event) async {
await _onCallStateChanged(call, event);
}));
}),
);
call.onCallReplaced.stream.listen((CallSession newCall) async {
await _replaceCall(groupCall, call, newCall);
@ -168,8 +177,11 @@ class MeshBackend extends CallBackend {
}
/// Removes a peer call from group calls.
Future<void> _removeCall(GroupCallSession groupCall, CallSession call,
CallErrorCode hangupReason) async {
Future<void> _removeCall(
GroupCallSession groupCall,
CallSession call,
CallErrorCode hangupReason,
) async {
await _disposeCall(groupCall, call, hangupReason);
_callSessions.removeWhere((element) => call.callId == element.callId);
@ -177,11 +189,15 @@ class MeshBackend extends CallBackend {
groupCall.onGroupCallEvent.add(GroupCallStateChange.callsChanged);
}
Future<void> _disposeCall(GroupCallSession groupCall, CallSession call,
CallErrorCode hangupReason) async {
Future<void> _disposeCall(
GroupCallSession groupCall,
CallSession call,
CallErrorCode hangupReason,
) async {
if (call.remoteUserId == null) {
throw MatrixSDKVoipException(
'Cannot init call without proper invitee user and device Id');
'Cannot init call without proper invitee user and device Id',
);
}
if (call.hangupReason == CallErrorCode.replaced) {
@ -220,10 +236,13 @@ class MeshBackend extends CallBackend {
}
Future<void> _onStreamsChanged(
GroupCallSession groupCall, CallSession call) async {
GroupCallSession groupCall,
CallSession call,
) async {
if (call.remoteUserId == null) {
throw MatrixSDKVoipException(
'Cannot init call without proper invitee user and device Id');
'Cannot init call without proper invitee user and device Id',
);
}
final currentUserMediaStream = _getUserMediaStreamByParticipantId(
@ -243,19 +262,23 @@ class MeshBackend extends CallBackend {
} else if (currentUserMediaStream != null &&
remoteUsermediaStream != null) {
await _replaceUserMediaStream(
groupCall, currentUserMediaStream, remoteUsermediaStream);
groupCall,
currentUserMediaStream,
remoteUsermediaStream,
);
} else if (currentUserMediaStream != null &&
remoteUsermediaStream == null) {
await _removeUserMediaStream(groupCall, currentUserMediaStream);
}
}
final currentScreenshareStream =
_getScreenshareStreamByParticipantId(CallParticipant(
final currentScreenshareStream = _getScreenshareStreamByParticipantId(
CallParticipant(
groupCall.voip,
userId: call.remoteUserId!,
deviceId: call.remoteDeviceId,
).id);
).id,
);
final remoteScreensharingStream = call.remoteScreenSharingStream;
final remoteScreenshareStreamChanged =
remoteScreensharingStream != currentScreenshareStream;
@ -267,7 +290,10 @@ class MeshBackend extends CallBackend {
} else if (currentScreenshareStream != null &&
remoteScreensharingStream != null) {
await _replaceScreenshareStream(
groupCall, currentScreenshareStream, remoteScreensharingStream);
groupCall,
currentScreenshareStream,
remoteScreensharingStream,
);
} else if (currentScreenshareStream != null &&
remoteScreensharingStream == null) {
await _removeScreenshareStream(groupCall, currentScreenshareStream);
@ -302,9 +328,11 @@ class MeshBackend extends CallBackend {
// https://www.w3.org/TR/webrtc-stats/#summary
final otherPartyAudioLevel = statsReport
.singleWhereOrNull((element) =>
.singleWhereOrNull(
(element) =>
element.type == 'inbound-rtp' &&
element.values['kind'] == 'audio')
element.values['kind'] == 'audio',
)
?.values['audioLevel'];
if (otherPartyAudioLevel != null) {
_audioLevelsMap[stream.participant] = otherPartyAudioLevel;
@ -313,9 +341,11 @@ class MeshBackend extends CallBackend {
// https://www.w3.org/TR/webrtc-stats/#dom-rtcstatstype-media-source
// firefox does not seem to have this though. Works on chrome and android
final ownAudioLevel = statsReport
.singleWhereOrNull((element) =>
.singleWhereOrNull(
(element) =>
element.type == 'media-source' &&
element.values['kind'] == 'audio')
element.values['kind'] == 'audio',
)
?.values['audioLevel'];
if (groupCall.localParticipant != null &&
ownAudioLevel != null &&
@ -345,7 +375,8 @@ class MeshBackend extends CallBackend {
}
WrappedMediaStream? _getScreenshareStreamByParticipantId(
String participantId) {
String participantId,
) {
final stream = _screenshareStreams
.where((stream) => stream.participant.id == participantId);
if (stream.isNotEmpty) {
@ -355,7 +386,9 @@ class MeshBackend extends CallBackend {
}
void _addScreenshareStream(
GroupCallSession groupCall, WrappedMediaStream stream) {
GroupCallSession groupCall,
WrappedMediaStream stream,
) {
_screenshareStreams.add(stream);
onStreamAdd.add(stream);
groupCall.onGroupCallEvent
@ -368,11 +401,13 @@ class MeshBackend extends CallBackend {
WrappedMediaStream replacementStream,
) async {
final streamIndex = _screenshareStreams.indexWhere(
(stream) => stream.participant.id == existingStream.participant.id);
(stream) => stream.participant.id == existingStream.participant.id,
);
if (streamIndex == -1) {
throw MatrixSDKVoipException(
'Couldn\'t find screenshare stream to replace');
'Couldn\'t find screenshare stream to replace',
);
}
_screenshareStreams.replaceRange(streamIndex, 1, [replacementStream]);
@ -391,11 +426,13 @@ class MeshBackend extends CallBackend {
if (streamIndex == -1) {
throw MatrixSDKVoipException(
'Couldn\'t find screenshare stream to remove');
'Couldn\'t find screenshare stream to remove',
);
}
_screenshareStreams.removeWhere(
(element) => element.participant.id == stream.participant.id);
(element) => element.participant.id == stream.participant.id,
);
onStreamRemoved.add(stream);
@ -449,11 +486,13 @@ class MeshBackend extends CallBackend {
WrappedMediaStream replacementStream,
) async {
final streamIndex = _userMediaStreams.indexWhere(
(stream) => stream.participant.id == existingStream.participant.id);
(stream) => stream.participant.id == existingStream.participant.id,
);
if (streamIndex == -1) {
throw MatrixSDKVoipException(
'Couldn\'t find user media stream to replace');
'Couldn\'t find user media stream to replace',
);
}
_userMediaStreams.replaceRange(streamIndex, 1, [replacementStream]);
@ -468,15 +507,18 @@ class MeshBackend extends CallBackend {
WrappedMediaStream stream,
) async {
final streamIndex = _userMediaStreams.indexWhere(
(element) => element.participant.id == stream.participant.id);
(element) => element.participant.id == stream.participant.id,
);
if (streamIndex == -1) {
throw MatrixSDKVoipException(
'Couldn\'t find user media stream to remove');
'Couldn\'t find user media stream to remove',
);
}
_userMediaStreams.removeWhere(
(element) => element.participant.id == stream.participant.id);
(element) => element.participant.id == stream.participant.id,
);
_audioLevelsMap.remove(stream.participant);
onStreamRemoved.add(stream);
@ -527,11 +569,14 @@ class MeshBackend extends CallBackend {
/// This allows you to configure the camera before joining the call without
/// having to reopen the stream and possibly losing settings.
@override
Future<WrappedMediaStream?> initLocalStream(GroupCallSession groupCall,
{WrappedMediaStream? stream}) async {
Future<WrappedMediaStream?> initLocalStream(
GroupCallSession groupCall, {
WrappedMediaStream? stream,
}) async {
if (groupCall.state != GroupCallState.localCallFeedUninitialized) {
throw MatrixSDKVoipException(
'Cannot initialize local call feed in the ${groupCall.state} state.');
'Cannot initialize local call feed in the ${groupCall.state} state.',
);
}
groupCall.setState(GroupCallState.initializingLocalCallFeed);
@ -575,7 +620,10 @@ class MeshBackend extends CallBackend {
@override
Future<void> setDeviceMuted(
GroupCallSession groupCall, bool muted, MediaInputKind kind) async {
GroupCallSession groupCall,
bool muted,
MediaInputKind kind,
) async {
if (!await hasMediaDevice(groupCall.voip.delegate, kind)) {
return;
}
@ -585,7 +633,9 @@ class MeshBackend extends CallBackend {
case MediaInputKind.audioinput:
localUserMediaStream!.setAudioMuted(muted);
setTracksEnabled(
localUserMediaStream!.stream!.getAudioTracks(), !muted);
localUserMediaStream!.stream!.getAudioTracks(),
!muted,
);
for (final call in _callSessions) {
await call.setMicrophoneMuted(muted);
}
@ -593,7 +643,9 @@ class MeshBackend extends CallBackend {
case MediaInputKind.videoinput:
localUserMediaStream!.setVideoMuted(muted);
setTracksEnabled(
localUserMediaStream!.stream!.getVideoTracks(), !muted);
localUserMediaStream!.stream!.getVideoTracks(),
!muted,
);
for (final call in _callSessions) {
await call.setLocalVideoMuted(muted);
}
@ -607,7 +659,9 @@ class MeshBackend extends CallBackend {
}
Future<void> _onIncomingCall(
GroupCallSession groupCall, CallSession newCall) async {
GroupCallSession groupCall,
CallSession newCall,
) async {
// The incoming calls may be for another room, which we will ignore.
if (newCall.room.id != groupCall.room.id) {
return;
@ -621,7 +675,8 @@ class MeshBackend extends CallBackend {
if (newCall.groupCallId == null ||
newCall.groupCallId != groupCall.groupCallId) {
Logs().v(
'Incoming call with groupCallId ${newCall.groupCallId} ignored because it doesn\'t match the current group call');
'Incoming call with groupCallId ${newCall.groupCallId} ignored because it doesn\'t match the current group call',
);
await newCall.reject();
return;
}
@ -640,7 +695,8 @@ class MeshBackend extends CallBackend {
}
Logs().v(
'GroupCallSession: incoming call from: ${newCall.remoteUserId}${newCall.remoteDeviceId}${newCall.remotePartyId}');
'GroupCallSession: incoming call from: ${newCall.remoteUserId}${newCall.remoteDeviceId}${newCall.remotePartyId}',
);
// Check if the user calling has an existing call and use this call instead.
if (existingCall != null) {
@ -674,7 +730,8 @@ class MeshBackend extends CallBackend {
};
}
Logs().v(
'Screensharing permissions granted. Setting screensharing enabled on all calls');
'Screensharing permissions granted. Setting screensharing enabled on all calls',
);
_localScreenshareStream = WrappedMediaStream(
stream: stream,
participant: groupCall.localParticipant!,
@ -694,7 +751,8 @@ class MeshBackend extends CallBackend {
for (final call in _callSessions) {
await call.addLocalStream(
await localScreenshareStream!.stream!.clone(),
localScreenshareStream!.purpose);
localScreenshareStream!.purpose,
);
}
await groupCall.sendMemberStateEvent();
@ -766,7 +824,8 @@ class MeshBackend extends CallBackend {
@override
Future<void> setupP2PCallsWithExistingMembers(
GroupCallSession groupCall) async {
GroupCallSession groupCall,
) async {
for (final call in _callSessions) {
await _onIncomingCall(groupCall, call);
}
@ -790,7 +849,8 @@ class MeshBackend extends CallBackend {
await existingCall.hangup(reason: CallErrorCode.unknownError);
} else {
Logs().e(
'[VOIP] onMemberStateChanged Not updating _participants list, already have a ongoing call with ${rp.id}');
'[VOIP] onMemberStateChanged Not updating _participants list, already have a ongoing call with ${rp.id}',
);
return;
}
}
@ -824,10 +884,14 @@ class MeshBackend extends CallBackend {
// party id set to when answered
newCall.remoteSessionId = mem.membershipId;
await newCall.placeCallWithStreams(_getLocalStreams(),
requestScreenSharing: mem.feeds?.any((element) =>
element['purpose'] == SDPStreamMetadataPurpose.Screenshare) ??
false);
await newCall.placeCallWithStreams(
_getLocalStreams(),
requestScreenSharing: mem.feeds?.any(
(element) =>
element['purpose'] == SDPStreamMetadataPurpose.Screenshare,
) ??
false,
);
await _addCall(groupCall, newCall);
}
@ -835,9 +899,11 @@ class MeshBackend extends CallBackend {
@override
List<Map<String, String>>? getCurrentFeeds() {
return _getLocalStreams()
.map((feed) => ({
.map(
(feed) => ({
'purpose': feed.purpose,
}))
}),
)
.toList();
}
@ -849,32 +915,46 @@ class MeshBackend extends CallBackend {
/// get everything is livekit specific mesh calls shouldn't be affected by these
@override
Future<void> onCallEncryption(GroupCallSession groupCall, String userId,
String deviceId, Map<String, dynamic> content) async {
Future<void> onCallEncryption(
GroupCallSession groupCall,
String userId,
String deviceId,
Map<String, dynamic> content,
) async {
return;
}
@override
Future<void> onCallEncryptionKeyRequest(GroupCallSession groupCall,
String userId, String deviceId, Map<String, dynamic> content) async {
Future<void> onCallEncryptionKeyRequest(
GroupCallSession groupCall,
String userId,
String deviceId,
Map<String, dynamic> content,
) async {
return;
}
@override
Future<void> onLeftParticipant(
GroupCallSession groupCall, List<CallParticipant> anyLeft) async {
GroupCallSession groupCall,
List<CallParticipant> anyLeft,
) async {
return;
}
@override
Future<void> onNewParticipant(
GroupCallSession groupCall, List<CallParticipant> anyJoined) async {
GroupCallSession groupCall,
List<CallParticipant> anyJoined,
) async {
return;
}
@override
Future<void> requestEncrytionKey(GroupCallSession groupCall,
List<CallParticipant> remoteParticipants) async {
Future<void> requestEncrytionKey(
GroupCallSession groupCall,
List<CallParticipant> remoteParticipants,
) async {
return;
}

View File

@ -147,7 +147,8 @@ class CallSession {
WrappedMediaStream? get localUserMediaStream {
final stream = getLocalStreams.where(
(element) => element.purpose == SDPStreamMetadataPurpose.Usermedia);
(element) => element.purpose == SDPStreamMetadataPurpose.Usermedia,
);
if (stream.isNotEmpty) {
return stream.first;
}
@ -156,7 +157,8 @@ class CallSession {
WrappedMediaStream? get localScreenSharingStream {
final stream = getLocalStreams.where(
(element) => element.purpose == SDPStreamMetadataPurpose.Screenshare);
(element) => element.purpose == SDPStreamMetadataPurpose.Screenshare,
);
if (stream.isNotEmpty) {
return stream.first;
}
@ -165,7 +167,8 @@ class CallSession {
WrappedMediaStream? get remoteUserMediaStream {
final stream = getRemoteStreams.where(
(element) => element.purpose == SDPStreamMetadataPurpose.Usermedia);
(element) => element.purpose == SDPStreamMetadataPurpose.Usermedia,
);
if (stream.isNotEmpty) {
return stream.first;
}
@ -174,7 +177,8 @@ class CallSession {
WrappedMediaStream? get remoteScreenSharingStream {
final stream = getRemoteStreams.where(
(element) => element.purpose == SDPStreamMetadataPurpose.Screenshare);
(element) => element.purpose == SDPStreamMetadataPurpose.Screenshare,
);
if (stream.isNotEmpty) {
return stream.first;
}
@ -190,8 +194,10 @@ class CallSession {
// check if we have a video track locally and have transceivers setup correctly.
return localUserMediaVideoTrack != null &&
transceivers.singleWhereOrNull((transceiver) =>
transceiver.sender.track?.id == localUserMediaVideoTrack.id) !=
transceivers.singleWhereOrNull(
(transceiver) =>
transceiver.sender.track?.id == localUserMediaVideoTrack.id,
) !=
null;
}
@ -209,8 +215,13 @@ class CallSession {
}
// incoming call
Future<void> initWithInvite(CallType type, RTCSessionDescription offer,
SDPStreamMetadata? metadata, int lifetime, bool isGroupCall) async {
Future<void> initWithInvite(
CallType type,
RTCSessionDescription offer,
SDPStreamMetadata? metadata,
int lifetime,
bool isGroupCall,
) async {
if (!isGroupCall) {
// glare fixes
final prevCallId = voip.incomingCallRoomId[room.id];
@ -223,13 +234,15 @@ class CallSession {
Logs().d('[glare] invite or answer sent, lex compare now');
if (callId.compareTo(prevCall.callId) > 0) {
Logs().d(
'[glare] new call $callId needs to be canceled because the older one ${prevCall.callId} has a smaller lex');
'[glare] new call $callId needs to be canceled because the older one ${prevCall.callId} has a smaller lex',
);
await hangup(reason: CallErrorCode.unknownError);
voip.currentCID =
VoipId(roomId: room.id, callId: prevCall.callId);
} else {
Logs().d(
'[glare] nice, lex of newer call $callId is smaller auto accept this here');
'[glare] nice, lex of newer call $callId is smaller auto accept this here',
);
/// These fixes do not work all the time because sometimes the code
/// is at an unrecoverable stage (invite already sent when we were
@ -240,7 +253,8 @@ class CallSession {
}
} else {
Logs().d(
'[glare] ${prevCall.callId} was still preparing prev call, nvm now cancel it');
'[glare] ${prevCall.callId} was still preparing prev call, nvm now cancel it',
);
await prevCall.hangup(reason: CallErrorCode.unknownError);
}
}
@ -310,11 +324,19 @@ class CallSession {
localUserMediaStream!.stream!.id: SDPStreamPurpose(
purpose: SDPStreamMetadataPurpose.Usermedia,
audio_muted: localUserMediaStream!.stream!.getAudioTracks().isEmpty,
video_muted: localUserMediaStream!.stream!.getVideoTracks().isEmpty)
video_muted: localUserMediaStream!.stream!.getVideoTracks().isEmpty,
),
});
final res = await sendAnswerCall(room, callId, answer.sdp!, localPartyId,
type: answer.type!, capabilities: callCapabilities, metadata: metadata);
final res = await sendAnswerCall(
room,
callId,
answer.sdp!,
localPartyId,
type: answer.type!,
capabilities: callCapabilities,
metadata: metadata,
);
Logs().v('[VOIP] answer res => $res');
}
@ -361,8 +383,8 @@ class CallSession {
if (requestScreenSharing) {
await pc!.addTransceiver(
kind: RTCRtpMediaType.RTCRtpMediaTypeVideo,
init:
RTCRtpTransceiverInit(direction: TransceiverDirection.RecvOnly));
init: RTCRtpTransceiverInit(direction: TransceiverDirection.RecvOnly),
);
}
setCallState(CallState.kCreateOffer);
@ -372,7 +394,9 @@ class CallSession {
}
Future<void> onAnswerReceived(
RTCSessionDescription answer, SDPStreamMetadata? metadata) async {
RTCSessionDescription answer,
SDPStreamMetadata? metadata,
) async {
if (metadata != null) {
_updateRemoteSDPStreamMetadata(metadata);
}
@ -387,12 +411,18 @@ class CallSession {
if (remotePartyId != null) {
/// Send select_answer event.
await sendSelectCallAnswer(
opts.room, callId, localPartyId, remotePartyId!);
opts.room,
callId,
localPartyId,
remotePartyId!,
);
}
}
Future<void> onNegotiateReceived(
SDPStreamMetadata? metadata, RTCSessionDescription description) async {
SDPStreamMetadata? metadata,
RTCSessionDescription description,
) async {
final polite = direction == CallDirection.kIncoming;
// Here we follow the perfect negotiation logic from
@ -430,7 +460,8 @@ class CallSession {
CallTimeouts.defaultCallEventLifetime.inMilliseconds,
localPartyId,
answer.sdp!,
type: answer.type!);
type: answer.type!,
);
await pc!.setLocalDescription(answer);
}
} catch (e, s) {
@ -464,7 +495,8 @@ class CallSession {
_remoteSDPStreamMetadata?.sdpStreamMetadatas
.forEach((streamId, sdpStreamMetadata) {
Logs().i(
'Stream purpose update: \nid = "$streamId", \npurpose = "${sdpStreamMetadata.purpose}", \naudio_muted = ${sdpStreamMetadata.audio_muted}, \nvideo_muted = ${sdpStreamMetadata.video_muted}');
'Stream purpose update: \nid = "$streamId", \npurpose = "${sdpStreamMetadata.purpose}", \naudio_muted = ${sdpStreamMetadata.audio_muted}, \nvideo_muted = ${sdpStreamMetadata.video_muted}',
);
});
for (final wpstream in getRemoteStreams) {
final streamId = wpstream.stream!.id;
@ -498,7 +530,8 @@ class CallSession {
if (!candidate.isValid) {
Logs().w(
'[VOIP] onCandidatesReceived => skip invalid candidate ${candidate.toMap()}');
'[VOIP] onCandidatesReceived => skip invalid candidate ${candidate.toMap()}',
);
continue;
}
@ -530,11 +563,13 @@ class CallSession {
// Skip if there is nothing to do
if (enabled && localScreenSharingStream != null) {
Logs().w(
'There is already a screensharing stream - there is nothing to do!');
'There is already a screensharing stream - there is nothing to do!',
);
return true;
} else if (!enabled && localScreenSharingStream == null) {
Logs().w(
'There already isn\'t a screensharing stream - there is nothing to do!');
'There already isn\'t a screensharing stream - there is nothing to do!',
);
return false;
}
@ -635,7 +670,8 @@ class CallSession {
final metadata = _remoteSDPStreamMetadata?.sdpStreamMetadatas[stream.id];
if (metadata == null) {
Logs().i(
'Ignoring stream with id ${stream.id} because we didn\'t get any metadata about it');
'Ignoring stream with id ${stream.id} because we didn\'t get any metadata about it',
);
return;
}
@ -762,24 +798,28 @@ class CallSession {
// remove any screen sharing or remote transceivers, these don't need
// to be replaced anyway.
final transceivers = await pc!.getTransceivers();
transceivers.removeWhere((transceiver) =>
transceivers.removeWhere(
(transceiver) =>
transceiver.sender.track == null ||
(localScreenSharingStream != null &&
localScreenSharingStream!.stream != null &&
localScreenSharingStream!.stream!
.getTracks()
.map((e) => e.id)
.contains(transceiver.sender.track?.id)));
.contains(transceiver.sender.track?.id)),
);
// in an ideal case the following should happen
// - audio track gets replaced
// - new video track gets added
for (final newTrack in streamTracks) {
final transceiver = transceivers.singleWhereOrNull(
(transceiver) => transceiver.sender.track!.kind == newTrack.kind);
(transceiver) => transceiver.sender.track!.kind == newTrack.kind,
);
if (transceiver != null) {
Logs().d(
'[VOIP] replacing ${transceiver.sender.track} in transceiver');
'[VOIP] replacing ${transceiver.sender.track} in transceiver',
);
final oldSender = transceiver.sender;
await oldSender.replaceTrack(newTrack);
await transceiver.setDirection(
@ -811,9 +851,9 @@ class CallSession {
_remoteOnHold = onHold;
final transceivers = await pc!.getTransceivers();
for (final transceiver in transceivers) {
await transceiver.setDirection(onHold
? TransceiverDirection.SendOnly
: TransceiverDirection.SendRecv);
await transceiver.setDirection(
onHold ? TransceiverDirection.SendOnly : TransceiverDirection.SendRecv,
);
}
await updateMuteStatus();
fireCallEvent(CallStateChange.kRemoteHoldUnhold);
@ -860,12 +900,14 @@ class CallSession {
localUserMediaStream!.stream!.id: SDPStreamPurpose(
purpose: SDPStreamMetadataPurpose.Usermedia,
audio_muted: localUserMediaStream!.audioMuted,
video_muted: localUserMediaStream!.videoMuted),
video_muted: localUserMediaStream!.videoMuted,
),
if (localScreenSharingStream != null)
localScreenSharingStream!.stream!.id: SDPStreamPurpose(
purpose: SDPStreamMetadataPurpose.Screenshare,
audio_muted: localScreenSharingStream!.audioMuted,
video_muted: localScreenSharingStream!.videoMuted),
video_muted: localScreenSharingStream!.videoMuted,
),
});
await pc!.setLocalDescription(answer);
@ -898,7 +940,8 @@ class CallSession {
setCallState(CallState.kEnding);
if (state != CallState.kRinging && state != CallState.kFledgling) {
Logs().e(
'[VOIP] Call must be in \'ringing|fledgling\' state to reject! (current state was: ${state.toString()}) Calling hangup instead');
'[VOIP] Call must be in \'ringing|fledgling\' state to reject! (current state was: ${state.toString()}) Calling hangup instead',
);
await hangup(reason: CallErrorCode.userHangup, shouldEmit: shouldEmit);
return;
}
@ -909,8 +952,10 @@ class CallSession {
}
}
Future<void> hangup(
{required CallErrorCode reason, bool shouldEmit = true}) async {
Future<void> hangup({
required CallErrorCode reason,
bool shouldEmit = true,
}) async {
setCallState(CallState.kEnding);
await terminate(CallParty.kLocal, reason, shouldEmit);
try {
@ -1002,7 +1047,10 @@ class CallSession {
if (shouldTerminate) {
await terminate(
CallParty.kRemote, reason ?? CallErrorCode.userHangup, true);
CallParty.kRemote,
reason ?? CallErrorCode.userHangup,
true,
);
} else {
Logs().e('[VOIP] Call is in state: ${state.toString()}: ignoring reject');
}
@ -1011,7 +1059,8 @@ class CallSession {
Future<void> _gotLocalOffer(RTCSessionDescription offer) async {
if (callHasEnded) {
Logs().d(
'Ignoring newly created offer on call ID ${opts.callId} because the call has ended');
'Ignoring newly created offer on call ID ${opts.callId} because the call has ended',
);
return;
}
@ -1020,7 +1069,10 @@ class CallSession {
} catch (err) {
Logs().d('Error setting local description! ${err.toString()}');
await terminate(
CallParty.kLocal, CallErrorCode.setLocalDescription, true);
CallParty.kLocal,
CallErrorCode.setLocalDescription,
true,
);
return;
}
@ -1044,7 +1096,8 @@ class CallSession {
localPartyId,
offer.sdp!,
capabilities: callCapabilities,
metadata: metadata);
metadata: metadata,
);
// just incase we ended the call but already sent the invite
// raraley happens during glares
if (state == CallState.kEnded) {
@ -1076,7 +1129,8 @@ class CallSession {
offer.sdp!,
type: offer.type!,
capabilities: callCapabilities,
metadata: metadata);
metadata: metadata,
);
}
}
@ -1151,7 +1205,7 @@ class CallSession {
_missedCall = false;
} else if ({
RTCIceConnectionState.RTCIceConnectionStateFailed,
RTCIceConnectionState.RTCIceConnectionStateDisconnected
RTCIceConnectionState.RTCIceConnectionStateDisconnected,
}.contains(state)) {
if (iceRestartedCount < 3) {
await restartIce();
@ -1199,10 +1253,14 @@ class CallSession {
localUserMediaStream!.isVideoMuted()) ||
_remoteOnHold;
_setTracksEnabled(localUserMediaStream?.stream?.getAudioTracks() ?? [],
!micShouldBeMuted);
_setTracksEnabled(localUserMediaStream?.stream?.getVideoTracks() ?? [],
!vidShouldBeMuted);
_setTracksEnabled(
localUserMediaStream?.stream?.getAudioTracks() ?? [],
!micShouldBeMuted,
);
_setTracksEnabled(
localUserMediaStream?.stream?.getVideoTracks() ?? [],
!vidShouldBeMuted,
);
await sendSDPStreamMetadataChanged(
room,
@ -1270,7 +1328,7 @@ class CallSession {
Future<RTCPeerConnection> _createPeerConnection() async {
final configuration = <String, dynamic>{
'iceServers': opts.iceServers,
'sdpSemantics': 'unified-plan'
'sdpSemantics': 'unified-plan',
};
final pc = await voip.delegate.createPeerConnection(configuration);
pc.onTrack = (RTCTrackEvent event) async {
@ -1290,7 +1348,9 @@ class CallSession {
}
Future<void> createDataChannel(
String label, RTCDataChannelInit dataChannelDict) async {
String label,
RTCDataChannelInit dataChannelDict,
) async {
await pc?.createDataChannel(label, dataChannelDict);
}
@ -1339,7 +1399,11 @@ class CallSession {
}
_localCandidates.clear();
final res = await sendCallCandidates(
opts.room, callId, localPartyId, candidates);
opts.room,
callId,
localPartyId,
candidates,
);
Logs().v('[VOIP] sendCallCandidates res => $res');
}
} catch (e) {
@ -1350,7 +1414,8 @@ class CallSession {
if (_candidateSendTries > 5) {
Logs().d(
'Failed to send candidates on attempt $_candidateSendTries Giving up on this call.');
'Failed to send candidates on attempt $_candidateSendTries Giving up on this call.',
);
await hangup(reason: CallErrorCode.iceTimeout);
return;
}
@ -1407,13 +1472,15 @@ class CallSession {
}
if (selectedPartyId == null) {
Logs().w(
'Got nonsensical select_answer with null/undefined selected_party_id: ignoring');
'Got nonsensical select_answer with null/undefined selected_party_id: ignoring',
);
return;
}
if (selectedPartyId != localPartyId) {
Logs().w(
'Got select_answer for party ID $selectedPartyId: we are party ID $localPartyId.');
'Got select_answer for party ID $selectedPartyId: we are party ID $localPartyId.',
);
// The other party has picked somebody else's answer
await terminate(CallParty.kRemote, CallErrorCode.answeredElsewhere, true);
}
@ -1430,12 +1497,17 @@ class CallSession {
/// intended for any member of the room other than the sender of the event.
/// [party_id] The party ID for call, Can be set to client.deviceId.
Future<String?> sendInviteToCall(
Room room, String callId, int lifetime, String party_id, String sdp,
{String type = 'offer',
Room room,
String callId,
int lifetime,
String party_id,
String sdp, {
String type = 'offer',
String version = voipProtoVersion,
String? txid,
CallCapabilities? capabilities,
SDPStreamMetadata? metadata}) async {
SDPStreamMetadata? metadata,
}) async {
final content = {
'call_id': callId,
'party_id': party_id,
@ -1471,8 +1543,13 @@ class CallSession {
/// [party_id] The party ID for call, Can be set to client.deviceId.
/// [selected_party_id] The party ID for the selected answer.
Future<String?> sendSelectCallAnswer(
Room room, String callId, String party_id, String selected_party_id,
{String version = voipProtoVersion, String? txid}) async {
Room room,
String callId,
String party_id,
String selected_party_id, {
String version = voipProtoVersion,
String? txid,
}) async {
final content = {
'call_id': callId,
'party_id': party_id,
@ -1495,8 +1572,13 @@ class CallSession {
/// [callId] is a unique identifier for the call.
/// [version] is the version of the VoIP specification this message adheres to. This specification is version 1.
/// [party_id] The party ID for call, Can be set to client.deviceId.
Future<String?> sendCallReject(Room room, String callId, String party_id,
{String version = voipProtoVersion, String? txid}) async {
Future<String?> sendCallReject(
Room room,
String callId,
String party_id, {
String version = voipProtoVersion,
String? txid,
}) async {
final content = {
'call_id': callId,
'party_id': party_id,
@ -1518,12 +1600,17 @@ class CallSession {
/// [version] is the version of the VoIP specification this message adheres to. This specification is version 1.
/// [party_id] The party ID for call, Can be set to client.deviceId.
Future<String?> sendCallNegotiate(
Room room, String callId, int lifetime, String party_id, String sdp,
{String type = 'offer',
Room room,
String callId,
int lifetime,
String party_id,
String sdp, {
String type = 'offer',
String version = voipProtoVersion,
String? txid,
CallCapabilities? capabilities,
SDPStreamMetadata? metadata}) async {
SDPStreamMetadata? metadata,
}) async {
final content = {
'call_id': callId,
'party_id': party_id,
@ -1596,12 +1683,16 @@ class CallSession {
/// [sdp] The SDP text of the session description.
/// [party_id] The party ID for call, Can be set to client.deviceId.
Future<String?> sendAnswerCall(
Room room, String callId, String sdp, String party_id,
{String type = 'answer',
Room room,
String callId,
String sdp,
String party_id, {
String type = 'answer',
String version = voipProtoVersion,
String? txid,
CallCapabilities? capabilities,
SDPStreamMetadata? metadata}) async {
SDPStreamMetadata? metadata,
}) async {
final content = {
'call_id': callId,
'party_id': party_id,
@ -1624,8 +1715,13 @@ class CallSession {
/// [version] is the version of the VoIP specification this message adheres to. This specification is version 1.
/// [party_id] The party ID for call, Can be set to client.deviceId.
Future<String?> sendHangupCall(
Room room, String callId, String party_id, String? hangupCause,
{String version = voipProtoVersion, String? txid}) async {
Room room,
String callId,
String party_id,
String? hangupCause, {
String version = voipProtoVersion,
String? txid,
}) async {
final content = {
'call_id': callId,
'party_id': party_id,
@ -1656,8 +1752,13 @@ class CallSession {
/// [party_id] The party ID for call, Can be set to client.deviceId.
/// [metadata] The sdp_stream_metadata object.
Future<String?> sendSDPStreamMetadataChanged(
Room room, String callId, String party_id, SDPStreamMetadata metadata,
{String version = voipProtoVersion, String? txid}) async {
Room room,
String callId,
String party_id,
SDPStreamMetadata metadata, {
String version = voipProtoVersion,
String? txid,
}) async {
final content = {
'call_id': callId,
'party_id': party_id,
@ -1682,8 +1783,13 @@ class CallSession {
/// [party_id] The party ID for call, Can be set to client.deviceId.
/// [callReplaces] transfer info
Future<String?> sendCallReplaces(
Room room, String callId, String party_id, CallReplaces callReplaces,
{String version = voipProtoVersion, String? txid}) async {
Room room,
String callId,
String party_id,
CallReplaces callReplaces, {
String version = voipProtoVersion,
String? txid,
}) async {
final content = {
'call_id': callId,
'party_id': party_id,
@ -1707,9 +1813,14 @@ class CallSession {
/// [version] is the version of the VoIP specification this message adheres to. This specification is version 1.
/// [party_id] The party ID for call, Can be set to client.deviceId.
/// [assertedIdentity] the asserted identity
Future<String?> sendAssertedIdentity(Room room, String callId,
String party_id, AssertedIdentity assertedIdentity,
{String version = voipProtoVersion, String? txid}) async {
Future<String?> sendAssertedIdentity(
Room room,
String callId,
String party_id,
AssertedIdentity assertedIdentity, {
String version = voipProtoVersion,
String? txid,
}) async {
final content = {
'call_id': callId,
'party_id': party_id,
@ -1753,19 +1864,24 @@ class CallSession {
await client.userDeviceKeysLoading;
if (client.userDeviceKeys[remoteUserId]?.deviceKeys[remoteDeviceId] !=
null) {
await client.sendToDeviceEncrypted([
client.userDeviceKeys[remoteUserId]!.deviceKeys[remoteDeviceId]!
], type, data);
await client.sendToDeviceEncrypted(
[
client.userDeviceKeys[remoteUserId]!.deviceKeys[remoteDeviceId]!,
],
type,
data,
);
} else {
Logs().w(
'[VOIP] _sendCallContent missing device keys for $remoteUserId');
'[VOIP] _sendCallContent missing device keys for $remoteUserId',
);
}
} else {
await client.sendToDevice(
type,
txid,
{
remoteUserId!: {remoteDeviceId!: data}
remoteUserId!: {remoteDeviceId!: data},
},
);
}

View File

@ -173,17 +173,20 @@ class GroupCallSession {
_resendMemberStateEventTimer!.cancel();
}
_resendMemberStateEventTimer = Timer.periodic(
CallTimeouts.updateExpireTsTimerDuration, ((timer) async {
CallTimeouts.updateExpireTsTimerDuration,
((timer) async {
Logs().d('sendMemberStateEvent updating member event with timer');
if (state != GroupCallState.ended ||
state != GroupCallState.localCallFeedUninitialized) {
await sendMemberStateEvent();
} else {
Logs().d(
'[VOIP] deteceted groupCall in state $state, removing state event');
'[VOIP] deteceted groupCall in state $state, removing state event',
);
await removeMemberStateEvent();
}
}));
}),
);
}
Future<void> removeMemberStateEvent() {
@ -218,7 +221,8 @@ class GroupCallSession {
for (final mem in ignoredMems) {
Logs().v(
'[VOIP] Ignored ${mem.userId}\'s mem event ${mem.toJson()} while updating _participants list for callId: $groupCallId, expiry status: ${mem.isExpired}');
'[VOIP] Ignored ${mem.userId}\'s mem event ${mem.toJson()} while updating _participants list for callId: $groupCallId, expiry status: ${mem.isExpired}',
);
}
final Set<CallParticipant> newP = {};
@ -236,7 +240,8 @@ class GroupCallSession {
if (state != GroupCallState.entered) {
Logs().w(
'[VOIP] onMemberStateChanged groupCall state is currently $state, skipping member update');
'[VOIP] onMemberStateChanged groupCall state is currently $state, skipping member update',
);
continue;
}
@ -253,7 +258,8 @@ class GroupCallSession {
..remove(localParticipant);
if (nonLocalAnyJoined.isNotEmpty && state == GroupCallState.entered) {
Logs().v(
'nonLocalAnyJoined: ${nonLocalAnyJoined.map((e) => e.id).toString()} roomId: ${room.id} groupCallId: $groupCallId');
'nonLocalAnyJoined: ${nonLocalAnyJoined.map((e) => e.id).toString()} roomId: ${room.id} groupCallId: $groupCallId',
);
await backend.onNewParticipant(this, nonLocalAnyJoined.toList());
}
_participants.addAll(anyJoined);
@ -265,7 +271,8 @@ class GroupCallSession {
..remove(localParticipant);
if (nonLocalAnyLeft.isNotEmpty && state == GroupCallState.entered) {
Logs().v(
'nonLocalAnyLeft: ${nonLocalAnyLeft.map((e) => e.id).toString()} roomId: ${room.id} groupCallId: $groupCallId');
'nonLocalAnyLeft: ${nonLocalAnyLeft.map((e) => e.id).toString()} roomId: ${room.id} groupCallId: $groupCallId',
);
await backend.onLeftParticipant(this, nonLocalAnyLeft.toList());
}
_participants.removeAll(anyLeft);
@ -275,7 +282,8 @@ class GroupCallSession {
onGroupCallEvent.add(GroupCallStateChange.participantsChanged);
Logs().d(
'[VOIP] onMemberStateChanged current list: ${_participants.map((e) => e.id).toString()}');
'[VOIP] onMemberStateChanged current list: ${_participants.map((e) => e.id).toString()}',
);
}
}
}

View File

@ -105,10 +105,11 @@ class SDPStreamPurpose {
bool audio_muted;
bool video_muted;
SDPStreamPurpose(
{required this.purpose,
SDPStreamPurpose({
required this.purpose,
this.audio_muted = false,
this.video_muted = false});
this.video_muted = false,
});
factory SDPStreamPurpose.fromJson(Map<String, dynamic> json) =>
SDPStreamPurpose(
audio_muted: json['audio_muted'] as bool? ?? false,
@ -133,8 +134,11 @@ class SDPStreamMetadata {
SDPStreamMetadata(this.sdpStreamMetadatas);
factory SDPStreamMetadata.fromJson(Map<String, dynamic> json) =>
SDPStreamMetadata(json.map(
(key, value) => MapEntry(key, SDPStreamPurpose.fromJson(value))));
SDPStreamMetadata(
json.map(
(key, value) => MapEntry(key, SDPStreamPurpose.fromJson(value)),
),
);
Map<String, dynamic> toJson() =>
sdpStreamMetadatas.map((key, value) => MapEntry(key, value.toJson()));
}

View File

@ -10,7 +10,10 @@ enum E2EEKeyMode {
abstract class EncryptionKeyProvider {
Future<void> onSetEncryptionKey(
CallParticipant participant, Uint8List key, int index);
CallParticipant participant,
Uint8List key,
int index,
);
Future<Uint8List> onRatchetKey(CallParticipant participant, int index);
@ -44,9 +47,11 @@ class EncryptionKeysEventContent {
EncryptionKeysEventContent(
(json['keys'] as List<dynamic>)
.map(
(e) => EncryptionKeyEntry.fromJson(e as Map<String, dynamic>))
(e) => EncryptionKeyEntry.fromJson(e as Map<String, dynamic>),
)
.toList(),
json['call_id'] as String);
json['call_id'] as String,
);
Map<String, dynamic> toJson() => {
'keys': keys.map((e) => e.toJson()).toList(),

View File

@ -6,8 +6,9 @@ import 'package:matrix/matrix.dart';
abstract class WebRTCDelegate {
MediaDevices get mediaDevices;
Future<RTCPeerConnection> createPeerConnection(
Map<String, dynamic> configuration,
[Map<String, dynamic> constraints = const {}]);
Map<String, dynamic> configuration, [
Map<String, dynamic> constraints = const {},
]);
Future<void> playRingtone();
Future<void> stopRingtone();
Future<void> handleNewCall(CallSession session);

View File

@ -17,7 +17,7 @@ class ConnectionTester {
'iceServers': iceServers,
'sdpSemantics': 'unified-plan',
'iceCandidatePoolSize': 1,
'iceTransportPolicy': 'relay'
'iceTransportPolicy': 'relay',
};
pc1 = await delegate.createPeerConnection(configuration);
pc2 = await delegate.createPeerConnection(configuration);
@ -78,9 +78,11 @@ class ConnectionTester {
return connected;
}
Future<int> waitUntilAsync(Future<bool> Function() test,
{final int maxIterations = 1000,
final Duration step = const Duration(milliseconds: 10)}) async {
Future<int> waitUntilAsync(
Future<bool> Function() test, {
final int maxIterations = 1000,
final Duration step = const Duration(milliseconds: 10),
}) async {
int iterations = 0;
for (; iterations < maxIterations; iterations++) {
await Future.delayed(step);
@ -90,7 +92,8 @@ class ConnectionTester {
}
if (iterations >= maxIterations) {
throw TimeoutException(
'Condition not reached within ${iterations * step.inMilliseconds}ms');
'Condition not reached within ${iterations * step.inMilliseconds}ms',
);
}
return iterations;
}
@ -112,7 +115,7 @@ class ConnectionTester {
{
'username': _turnServerCredentials!.username,
'credential': _turnServerCredentials!.password,
'url': _turnServerCredentials!.uris[0]
'url': _turnServerCredentials!.uris[0],
}
];
}

View File

@ -80,7 +80,8 @@ extension FamedlyCallMemberEventsExtension on Room {
/// passing no `CallMembership` removes it from the state event.
Future<void> updateFamedlyCallMemberStateEvent(
CallMembership callMembership) async {
CallMembership callMembership,
) async {
final ownMemberships = getCallMembershipsForUser(client.userID!);
// do not bother removing other deviceId expired events because we have no
@ -93,7 +94,7 @@ extension FamedlyCallMemberEventsExtension on Room {
ownMemberships.add(callMembership);
final newContent = {
'memberships': List.from(ownMemberships.map((e) => e.toJson()))
'memberships': List.from(ownMemberships.map((e) => e.toJson())),
};
await setFamedlyCallMemberEvent(newContent);
@ -107,14 +108,16 @@ extension FamedlyCallMemberEventsExtension on Room {
}) async {
final ownMemberships = getCallMembershipsForUser(client.userID!);
ownMemberships.removeWhere((mem) =>
ownMemberships.removeWhere(
(mem) =>
mem.callId == groupCallId &&
mem.deviceId == deviceId &&
mem.application == application &&
mem.scope == scope);
mem.scope == scope,
);
final newContent = {
'memberships': List.from(ownMemberships.map((e) => e.toJson()))
'memberships': List.from(ownMemberships.map((e) => e.toJson())),
};
await setFamedlyCallMemberEvent(newContent);
}
@ -145,12 +148,18 @@ extension FamedlyCallMemberEventsExtension on Room {
List<CallMembership> getCallMembershipsFromEvent(MatrixEvent event) {
if (event.roomId != id) return [];
return getCallMembershipsFromEventContent(
event.content, event.senderId, event.roomId!);
event.content,
event.senderId,
event.roomId!,
);
}
/// returns a list of memberships from a famedly call matrix event
List<CallMembership> getCallMembershipsFromEventContent(
Map<String, Object?> content, String senderId, String roomId) {
Map<String, Object?> content,
String senderId,
String roomId,
) {
final mems = content.tryGetList<Map>('memberships');
final callMems = <CallMembership>[];
for (final m in mems ?? []) {

View File

@ -28,7 +28,9 @@ void setTracksEnabled(List<MediaStreamTrack> tracks, bool enabled) {
}
Future<bool> hasMediaDevice(
WebRTCDelegate delegate, MediaInputKind mediaInputKind) async {
WebRTCDelegate delegate,
MediaInputKind mediaInputKind,
) async {
final devices = await delegate.mediaDevices.enumerateDevices();
return devices
.where((device) => device.kind == mediaInputKind.name)

View File

@ -2,7 +2,7 @@ class UserMediaConstraints {
static const Map<String, Object> micMediaConstraints = {
'echoCancellation': true,
'noiseSuppression': true,
'autoGainControl': false
'autoGainControl': false,
};
static const Map<String, Object> camMediaConstraints = {

View File

@ -40,7 +40,8 @@ class CallTimeouts {
class CallConstants {
static final callEventsRegxp = RegExp(
r'm.call.|org.matrix.call.|org.matrix.msc3401.call.|com.famedly.call.');
r'm.call.|org.matrix.call.|org.matrix.msc3401.call.|com.famedly.call.',
);
static const callEndedEventTypes = {
EventTypes.CallAnswer,

View File

@ -125,7 +125,8 @@ class VoIP {
if (CallConstants.omitWhenCallEndedTypes.contains(event.type) &&
event.content.tryGet<String>('call_id') == callId) {
Logs().v(
'Ommit "${event.type}" event for an already terminated call');
'Ommit "${event.type}" event for an already terminated call',
);
return true;
}
@ -146,7 +147,8 @@ class VoIP {
(callEvent.content.tryGet<int>('lifetime') ??
CallTimeouts.callInviteLifetime.inMilliseconds)) {
Logs().w(
'[VOIP] Ommiting invite event ${callEvent.eventId} as age was older than lifetime');
'[VOIP] Ommiting invite event ${callEvent.eventId} as age was older than lifetime',
);
return true;
}
return false;
@ -186,7 +188,8 @@ class VoIP {
groupCallSession = groupCalls[VoipId(roomId: roomId, callId: confId)];
} else {
Logs().w(
'[VOIP] Ignoring to_device event of type ${event.type} but did not find group call for id: $confId');
'[VOIP] Ignoring to_device event of type ${event.type} but did not find group call for id: $confId',
);
return;
}
@ -195,17 +198,20 @@ class VoIP {
final destSessionId = event.content.tryGet<String>('dest_session_id');
if (destSessionId != currentSessionId) {
Logs().w(
'[VOIP] Ignoring to_device event of type ${event.type} did not match currentSessionId: $currentSessionId, dest_session_id was set to $destSessionId');
'[VOIP] Ignoring to_device event of type ${event.type} did not match currentSessionId: $currentSessionId, dest_session_id was set to $destSessionId',
);
return;
}
} else if (groupCallSession == null || remoteDeviceId == null) {
Logs().w(
'[VOIP] _handleCallEvent ${event.type} recieved but either groupCall ${groupCallSession?.groupCallId} or deviceId $remoteDeviceId was null, ignoring');
'[VOIP] _handleCallEvent ${event.type} recieved but either groupCall ${groupCallSession?.groupCallId} or deviceId $remoteDeviceId was null, ignoring',
);
return;
}
} else {
Logs().w(
'[VOIP] _handleCallEvent can only handle Event or ToDeviceEvent, it got ${event.runtimeType}');
'[VOIP] _handleCallEvent can only handle Event or ToDeviceEvent, it got ${event.runtimeType}',
);
return;
}
@ -213,14 +219,16 @@ class VoIP {
if (room == null) {
Logs().w(
'[VOIP] _handleCallEvent call event does not contain a room_id, ignoring');
'[VOIP] _handleCallEvent call event does not contain a room_id, ignoring',
);
return;
} else if (client.userID != null &&
client.deviceID != null &&
remoteUserId == client.userID &&
remoteDeviceId == client.deviceID) {
Logs().v(
'Ignoring call event ${event.type} for room ${room.id} from our own device');
'Ignoring call event ${event.type} for room ${room.id} from our own device',
);
return;
} else if (!event.type
.startsWith(EventTypes.GroupCallMemberEncryptionKeys)) {
@ -237,37 +245,43 @@ class VoIP {
!{EventTypes.CallInvite, EventTypes.GroupCallMemberInvite}
.contains(event.type)) {
Logs().w(
'Ignoring call event ${event.type} for room ${room.id} because we do not have the call');
'Ignoring call event ${event.type} for room ${room.id} because we do not have the call',
);
return;
} else if (call != null) {
// multiple checks to make sure the events sent are from the the
// expected party
if (call.room.id != room.id) {
Logs().w(
'Ignoring call event ${event.type} for room ${room.id} claiming to be for call in room ${call.room.id}');
'Ignoring call event ${event.type} for room ${room.id} claiming to be for call in room ${call.room.id}',
);
return;
}
if (call.remoteUserId != null && call.remoteUserId != remoteUserId) {
Logs().d(
'Ignoring call event ${event.type} for room ${room.id} from sender $remoteUserId, expected sender: ${call.remoteUserId}');
'Ignoring call event ${event.type} for room ${room.id} from sender $remoteUserId, expected sender: ${call.remoteUserId}',
);
return;
}
if (call.remotePartyId != null && call.remotePartyId != partyId) {
Logs().w(
'Ignoring call event ${event.type} for room ${room.id} from sender with a different party_id $partyId, expected party_id: ${call.remotePartyId}');
'Ignoring call event ${event.type} for room ${room.id} from sender with a different party_id $partyId, expected party_id: ${call.remotePartyId}',
);
return;
}
if ((call.remotePartyId != null &&
call.remotePartyId == localPartyId)) {
Logs().v(
'Ignoring call event ${event.type} for room ${room.id} from our own partyId');
'Ignoring call event ${event.type} for room ${room.id} from our own partyId',
);
return;
}
}
}
}
Logs().v(
'[VOIP] Handling event of type: ${event.type}, content ${event.content} from sender ${event.senderId} rp: $remoteUserId:$remoteDeviceId');
'[VOIP] Handling event of type: ${event.type}, content ${event.content} from sender ${event.senderId} rp: $remoteUserId:$remoteDeviceId',
);
switch (event.type) {
case EventTypes.CallInvite:
@ -313,11 +327,19 @@ class VoIP {
break;
case EventTypes.GroupCallMemberEncryptionKeys:
await groupCallSession!.backend.onCallEncryption(
groupCallSession, remoteUserId, remoteDeviceId!, content);
groupCallSession,
remoteUserId,
remoteDeviceId!,
content,
);
break;
case EventTypes.GroupCallMemberEncryptionKeysRequest:
await groupCallSession!.backend.onCallEncryptionKeyRequest(
groupCallSession, remoteUserId, remoteDeviceId!, content);
groupCallSession,
remoteUserId,
remoteDeviceId!,
content,
);
break;
}
}
@ -336,10 +358,15 @@ class VoIP {
}
}
Future<void> onCallInvite(Room room, String remoteUserId,
String? remoteDeviceId, Map<String, dynamic> content) async {
Future<void> onCallInvite(
Room room,
String remoteUserId,
String? remoteDeviceId,
Map<String, dynamic> content,
) async {
Logs().v(
'[VOIP] onCallInvite $remoteUserId:$remoteDeviceId => ${client.userID}:${client.deviceID}, \ncontent => ${content.toString()}');
'[VOIP] onCallInvite $remoteUserId:$remoteDeviceId => ${client.userID}:${client.deviceID}, \ncontent => ${content.toString()}',
);
final String callId = content['call_id'];
final int lifetime = content['lifetime'];
@ -348,7 +375,8 @@ class VoIP {
final call = calls[VoipId(roomId: room.id, callId: callId)];
Logs().d(
'[glare] got new call ${content.tryGet('call_id')} and currently room id is mapped to ${incomingCallRoomId.tryGet(room.id)}');
'[glare] got new call ${content.tryGet('call_id')} and currently room id is mapped to ${incomingCallRoomId.tryGet(room.id)}',
);
if (call != null && call.state == CallState.kEnded) {
// Session already exist.
@ -371,7 +399,8 @@ class VoIP {
if (content['capabilities'] != null) {
final capabilities = CallCapabilities.fromJson(content['capabilities']);
Logs().v(
'[VOIP] CallCapabilities: dtmf => ${capabilities.dtmf}, transferee => ${capabilities.transferee}');
'[VOIP] CallCapabilities: dtmf => ${capabilities.dtmf}, transferee => ${capabilities.transferee}',
);
}
var callType = CallType.kVoice;
@ -382,7 +411,8 @@ class VoIP {
sdpStreamMetadata.sdpStreamMetadatas
.forEach((streamId, SDPStreamPurpose purpose) {
Logs().v(
'[VOIP] [$streamId] => purpose: ${purpose.purpose}, audioMuted: ${purpose.audio_muted}, videoMuted: ${purpose.video_muted}');
'[VOIP] [$streamId] => purpose: ${purpose.purpose}, audioMuted: ${purpose.audio_muted}, videoMuted: ${purpose.video_muted}',
);
if (!purpose.video_muted) {
callType = CallType.kVideo;
@ -419,7 +449,8 @@ class VoIP {
(confId == null ||
currentGroupCID != VoipId(roomId: room.id, callId: confId))) {
Logs().v(
'[VOIP] onCallInvite: Unable to handle new calls, maybe user is busy.');
'[VOIP] onCallInvite: Unable to handle new calls, maybe user is busy.',
);
// no need to emit here because handleNewCall was never triggered yet
await newCall.reject(reason: CallErrorCode.userBusy, shouldEmit: false);
await delegate.handleMissedCall(newCall);
@ -449,7 +480,12 @@ class VoIP {
currentCID = VoipId(roomId: room.id, callId: callId);
await newCall.initWithInvite(
callType, offer, sdpStreamMetadata, lifetime, confId != null);
callType,
offer,
sdpStreamMetadata,
lifetime,
confId != null,
);
// Popup CallingPage for incoming call.
if (confId == null && !newCall.callHasEnded) {
@ -462,8 +498,12 @@ class VoIP {
}
}
Future<void> onCallAnswer(Room room, String remoteUserId,
String? remoteDeviceId, Map<String, dynamic> content) async {
Future<void> onCallAnswer(
Room room,
String remoteUserId,
String? remoteDeviceId,
Map<String, dynamic> content,
) async {
Logs().v('[VOIP] onCallAnswer => ${content.toString()}');
final String callId = content['call_id'];
@ -478,31 +518,37 @@ class VoIP {
if (call.room.id != room.id) {
Logs().w(
'Ignoring call answer for room ${room.id} claiming to be for call in room ${call.room.id}');
'Ignoring call answer for room ${room.id} claiming to be for call in room ${call.room.id}',
);
return;
}
if (call.remoteUserId == null) {
Logs().i(
'[VOIP] you probably called the room without setting a userId in invite, setting the calls remote user id to what I get from m.call.answer now');
'[VOIP] you probably called the room without setting a userId in invite, setting the calls remote user id to what I get from m.call.answer now',
);
call.remoteUserId = remoteUserId;
}
if (call.remoteDeviceId == null) {
Logs().i(
'[VOIP] you probably called the room without setting a userId in invite, setting the calls remote user id to what I get from m.call.answer now');
'[VOIP] you probably called the room without setting a userId in invite, setting the calls remote user id to what I get from m.call.answer now',
);
call.remoteDeviceId = remoteDeviceId;
}
if (call.remotePartyId != null) {
Logs().d(
'Ignoring call answer from party ${content['party_id']}, we are already with ${call.remotePartyId}');
'Ignoring call answer from party ${content['party_id']}, we are already with ${call.remotePartyId}',
);
return;
} else {
call.remotePartyId = content['party_id'];
}
final answer = RTCSessionDescription(
content['answer']['sdp'], content['answer']['type']);
content['answer']['sdp'],
content['answer']['type'],
);
SDPStreamMetadata? metadata;
if (content[sdpStreamMetadataKey] != null) {
@ -537,9 +583,11 @@ class VoIP {
await call.terminate(
CallParty.kRemote,
CallErrorCode.values.firstWhereOrNull(
(element) => element.reason == content['reason']) ??
(element) => element.reason == content['reason'],
) ??
CallErrorCode.userHangup,
true);
true,
);
} else {
Logs().v('[VOIP] onCallHangup: Session [$callId] not found!');
}
@ -556,7 +604,8 @@ class VoIP {
if (call != null) {
await call.onRejectReceived(
CallErrorCode.values.firstWhereOrNull(
(element) => element.reason == content['reason']) ??
(element) => element.reason == content['reason'],
) ??
CallErrorCode.userHangup,
);
} else {
@ -565,7 +614,9 @@ class VoIP {
}
Future<void> onCallSelectAnswer(
Room room, Map<String, dynamic> content) async {
Room room,
Map<String, dynamic> content,
) async {
final String callId = content['call_id'];
Logs().d('SelectAnswer received for call ID $callId');
final String selectedPartyId = content['selected_party_id'];
@ -574,7 +625,8 @@ class VoIP {
if (call != null) {
if (call.room.id != room.id) {
Logs().w(
'Ignoring call select answer for room ${room.id} claiming to be for call in room ${call.room.id}');
'Ignoring call select answer for room ${room.id} claiming to be for call in room ${call.room.id}',
);
return;
}
await call.onSelectAnswerReceived(selectedPartyId);
@ -582,7 +634,9 @@ class VoIP {
}
Future<void> onSDPStreamMetadataChangedReceived(
Room room, Map<String, dynamic> content) async {
Room room,
Map<String, dynamic> content,
) async {
final String callId = content['call_id'];
Logs().d('SDP Stream metadata received for call ID $callId');
@ -593,12 +647,15 @@ class VoIP {
return;
}
await call.onSDPStreamMetadataReceived(
SDPStreamMetadata.fromJson(content[sdpStreamMetadataKey]));
SDPStreamMetadata.fromJson(content[sdpStreamMetadataKey]),
);
}
}
Future<void> onAssertedIdentityReceived(
Room room, Map<String, dynamic> content) async {
Room room,
Map<String, dynamic> content,
) async {
final String callId = content['call_id'];
Logs().d('Asserted identity received for call ID $callId');
@ -609,7 +666,8 @@ class VoIP {
return;
}
call.onAssertedIdentityReceived(
AssertedIdentity.fromJson(content['asserted_identity']));
AssertedIdentity.fromJson(content['asserted_identity']),
);
}
}
@ -630,8 +688,10 @@ class VoIP {
if (content[sdpStreamMetadataKey] != null) {
metadata = SDPStreamMetadata.fromJson(content[sdpStreamMetadataKey]);
}
await call.onNegotiateReceived(metadata,
RTCSessionDescription(description['sdp'], description['type']));
await call.onNegotiateReceived(
metadata,
RTCSessionDescription(description['sdp'], description['type']),
);
} catch (e, s) {
Logs().e('[VOIP] Failed to complete negotiation', e, s);
}
@ -668,7 +728,7 @@ class VoIP {
{
'username': _turnServerCredentials!.username,
'credential': _turnServerCredentials!.password,
'urls': _turnServerCredentials!.uris
'urls': _turnServerCredentials!.uris,
}
];
}
@ -832,7 +892,8 @@ class VoIP {
}) async {
if (membership.isExpired) {
Logs().d(
'Ignoring expired membership in passive groupCall creator. ${membership.toJson()}');
'Ignoring expired membership in passive groupCall creator. ${membership.toJson()}',
);
return;
}
@ -859,7 +920,8 @@ class VoIP {
);
if (groupCalls.containsKey(
VoipId(roomId: membership.roomId, callId: membership.callId))) {
VoipId(roomId: membership.roomId, callId: membership.callId),
)) {
return;
}

View File

@ -36,17 +36,39 @@ void main() {
iceServers: [],
),
);
await call.sendInviteToCall(room, '1234', 1234, '4567', 'sdp',
txid: '1234');
await call.sendInviteToCall(
room,
'1234',
1234,
'4567',
'sdp',
txid: '1234',
);
await call.sendAnswerCall(room, '1234', 'sdp', '4567', txid: '1234');
await call.sendCallCandidates(room, '1234', '4567', [], txid: '1234');
await call.sendSelectCallAnswer(room, '1234', '4567', '6789',
txid: '1234');
await call.sendSelectCallAnswer(
room,
'1234',
'4567',
'6789',
txid: '1234',
);
await call.sendCallReject(room, '1234', '4567', txid: '1234');
await call.sendCallNegotiate(room, '1234', 1234, '4567', 'sdp',
txid: '1234');
await call.sendHangupCall(room, '1234', '4567', 'userHangup',
txid: '1234');
await call.sendCallNegotiate(
room,
'1234',
1234,
'4567',
'sdp',
txid: '1234',
);
await call.sendHangupCall(
room,
'1234',
'4567',
'userHangup',
txid: '1234',
);
await call.sendAssertedIdentity(
room,
'1234',
@ -54,45 +76,66 @@ void main() {
AssertedIdentity()
..displayName = 'name'
..id = 'some_id',
txid: '1234');
await call.sendCallReplaces(room, '1234', '4567', CallReplaces(),
txid: '1234');
txid: '1234',
);
await call.sendCallReplaces(
room,
'1234',
'4567',
CallReplaces(),
txid: '1234',
);
await call.sendSDPStreamMetadataChanged(
room, '1234', '4567', SDPStreamMetadata({}),
txid: '1234');
room,
'1234',
'4567',
SDPStreamMetadata({}),
txid: '1234',
);
});
test('Call lifetime and age', () async {
expect(voip.currentCID, null);
await matrix.handleSync(SyncUpdate(
await matrix.handleSync(
SyncUpdate(
nextBatch: 'something',
rooms: RoomsUpdate(join: {
rooms: RoomsUpdate(
join: {
room.id: JoinedRoomUpdate(
timeline: TimelineUpdate(events: [
timeline: TimelineUpdate(
events: [
MatrixEvent(
type: 'm.call.invite',
content: {
'lifetime': 60000,
'call_id': '1702472924955oq1uQbNAfU7wAaEA',
'party_id': 'DPCIPPBGPO',
'offer': {'type': 'offer', 'sdp': 'sdp'}
'offer': {'type': 'offer', 'sdp': 'sdp'},
},
senderId: '@alice:testing.com',
eventId: 'newevent',
originServerTs: DateTime.utc(1969),
)
]))
})));
),
],
),
),
},
),
),
);
await Future.delayed(Duration(seconds: 2));
// confirm that no call got created after 3 seconds, which is
// expected in this case because the originTs was old asf
expect(voip.currentCID, null);
await matrix.handleSync(SyncUpdate(
await matrix.handleSync(
SyncUpdate(
nextBatch: 'something',
rooms: RoomsUpdate(join: {
rooms: RoomsUpdate(
join: {
room.id: JoinedRoomUpdate(
timeline: TimelineUpdate(events: [
timeline: TimelineUpdate(
events: [
MatrixEvent(
unsigned: {'age': 60001},
type: 'm.call.invite',
@ -100,14 +143,19 @@ void main() {
'lifetime': 60000,
'call_id': 'unsignedTsInvalidCall',
'party_id': 'DPCIPPBGPO',
'offer': {'type': 'offer', 'sdp': 'sdp'}
'offer': {'type': 'offer', 'sdp': 'sdp'},
},
senderId: '@alice:testing.com',
eventId: 'newevent',
originServerTs: DateTime.now(),
)
]))
})));
),
],
),
),
},
),
),
);
await Future.delayed(Duration(seconds: 2));
// confirm that no call got created after 3 seconds, which is
// expected in this case because age was older than lifetime
@ -115,11 +163,14 @@ void main() {
});
test('Call connection and hanging up', () async {
expect(voip.currentCID, null);
await matrix.handleSync(SyncUpdate(
await matrix.handleSync(
SyncUpdate(
nextBatch: 'something',
rooms: RoomsUpdate(join: {
rooms: RoomsUpdate(
join: {
room.id: JoinedRoomUpdate(
timeline: TimelineUpdate(events: [
timeline: TimelineUpdate(
events: [
MatrixEvent(
type: 'm.call.invite',
content: {
@ -127,19 +178,27 @@ void main() {
'call_id': 'originTsValidCall',
'party_id': 'GHTYAJCE_caller',
'version': '1',
'offer': {'type': 'offer', 'sdp': 'sdp'}
'offer': {'type': 'offer', 'sdp': 'sdp'},
},
senderId: '@alice:testing.com',
eventId: 'callerInviteEvent',
originServerTs: DateTime.now(),
)
]))
})));
await matrix.handleSync(SyncUpdate(
),
],
),
),
},
),
),
);
await matrix.handleSync(
SyncUpdate(
nextBatch: 'something',
rooms: RoomsUpdate(join: {
rooms: RoomsUpdate(
join: {
room.id: JoinedRoomUpdate(
timeline: TimelineUpdate(events: [
timeline: TimelineUpdate(
events: [
MatrixEvent(
type: 'm.call.candidates',
content: {
@ -148,32 +207,40 @@ void main() {
'version': '1',
'candidates': [
{
'candidate': 'candidate:01UDP2122252543uwu50184typhost',
'candidate':
'candidate:01UDP2122252543uwu50184typhost',
'sdpMid': '0',
'sdpMLineIndex': 0
'sdpMLineIndex': 0,
},
{
'candidate':
'candidate:31TCP2105524479uwu9typhosttcptypeactive',
'sdpMid': '0',
'sdpMLineIndex': 0
'sdpMLineIndex': 0,
}
],
},
senderId: '@alice:testing.com',
eventId: 'callerCallCandidatesEvent',
originServerTs: DateTime.now(),
)
]))
})));
),
],
),
),
},
),
),
);
while (voip.currentCID !=
VoipId(roomId: room.id, callId: 'originTsValidCall')) {
// call invite looks valid, call should be created now :D
await Future.delayed(Duration(milliseconds: 50));
Logs().d('Waiting for currentCID to update');
}
expect(voip.currentCID,
VoipId(roomId: room.id, callId: 'originTsValidCall'));
expect(
voip.currentCID,
VoipId(roomId: room.id, callId: 'originTsValidCall'),
);
final call = voip.calls[voip.currentCID]!;
expect(call.state, CallState.kRinging);
await call.answer(txid: '1234');
@ -190,25 +257,29 @@ void main() {
{
'candidate': 'candidate:0 1 UDP 2122252543 uwu 50184 typ host',
'sdpMid': '0',
'sdpMLineIndex': 0
'sdpMLineIndex': 0,
},
{
'candidate':
'candidate:3 1 TCP 2105524479 uwu 9 typ host tcptype active',
'sdpMid': '0',
'sdpMLineIndex': 0
'sdpMLineIndex': 0,
}
],
txid: '1234');
txid: '1234',
);
expect(call.state, CallState.kConnecting);
// caller sends select answer
await matrix.handleSync(SyncUpdate(
await matrix.handleSync(
SyncUpdate(
nextBatch: 'something',
rooms: RoomsUpdate(join: {
rooms: RoomsUpdate(
join: {
room.id: JoinedRoomUpdate(
timeline: TimelineUpdate(events: [
timeline: TimelineUpdate(
events: [
MatrixEvent(
type: 'm.call.select_answer',
content: {
@ -216,14 +287,19 @@ void main() {
'party_id': 'GHTYAJCE_caller',
'version': '1',
'lifetime': 10000,
'selected_party_id': 'GHTYAJCE'
'selected_party_id': 'GHTYAJCE',
},
senderId: '@alice:testing.com',
eventId: 'callerSelectAnswerEvent',
originServerTs: DateTime.now(),
)
]))
})));
),
],
),
),
},
),
),
);
call.pc!.onIceConnectionState!
.call(RTCIceConnectionState.RTCIceConnectionStateChecking);
@ -242,11 +318,14 @@ void main() {
test('Call answered elsewhere', () async {
expect(voip.currentCID, null);
await matrix.handleSync(SyncUpdate(
await matrix.handleSync(
SyncUpdate(
nextBatch: 'something',
rooms: RoomsUpdate(join: {
rooms: RoomsUpdate(
join: {
room.id: JoinedRoomUpdate(
timeline: TimelineUpdate(events: [
timeline: TimelineUpdate(
events: [
MatrixEvent(
type: 'm.call.invite',
content: {
@ -254,19 +333,27 @@ void main() {
'call_id': 'answer_elseWhere',
'party_id': 'GHTYAJCE_caller',
'version': '1',
'offer': {'type': 'offer', 'sdp': 'sdp'}
'offer': {'type': 'offer', 'sdp': 'sdp'},
},
senderId: '@alice:testing.com',
eventId: 'callerInviteEvent',
originServerTs: DateTime.now(),
)
]))
})));
await matrix.handleSync(SyncUpdate(
),
],
),
),
},
),
),
);
await matrix.handleSync(
SyncUpdate(
nextBatch: 'something',
rooms: RoomsUpdate(join: {
rooms: RoomsUpdate(
join: {
room.id: JoinedRoomUpdate(
timeline: TimelineUpdate(events: [
timeline: TimelineUpdate(
events: [
MatrixEvent(
type: 'm.call.candidates',
content: {
@ -275,24 +362,30 @@ void main() {
'version': '1',
'candidates': [
{
'candidate': 'candidate:01UDP2122252543uwu50184typhost',
'candidate':
'candidate:01UDP2122252543uwu50184typhost',
'sdpMid': '0',
'sdpMLineIndex': 0
'sdpMLineIndex': 0,
},
{
'candidate':
'candidate:31TCP2105524479uwu9typhosttcptypeactive',
'sdpMid': '0',
'sdpMLineIndex': 0
'sdpMLineIndex': 0,
}
],
},
senderId: '@alice:testing.com',
eventId: 'callerCallCandidatesEvent',
originServerTs: DateTime.now(),
)
]))
})));
),
],
),
),
},
),
),
);
while (voip.currentCID !=
VoipId(roomId: room.id, callId: 'answer_elseWhere')) {
// call invite looks valid, call should be created now :D
@ -300,16 +393,21 @@ void main() {
Logs().d('Waiting for currentCID to update');
}
expect(
voip.currentCID, VoipId(roomId: room.id, callId: 'answer_elseWhere'));
voip.currentCID,
VoipId(roomId: room.id, callId: 'answer_elseWhere'),
);
final call = voip.calls[voip.currentCID]!;
expect(call.state, CallState.kRinging);
// caller sends select answer
await matrix.handleSync(SyncUpdate(
await matrix.handleSync(
SyncUpdate(
nextBatch: 'something',
rooms: RoomsUpdate(join: {
rooms: RoomsUpdate(
join: {
room.id: JoinedRoomUpdate(
timeline: TimelineUpdate(events: [
timeline: TimelineUpdate(
events: [
MatrixEvent(
type: 'm.call.select_answer',
content: {
@ -318,14 +416,19 @@ void main() {
'version': '1',
'lifetime': 10000,
'selected_party_id':
'not_us' // selected some other device for answer
'not_us', // selected some other device for answer
},
senderId: '@alice:testing.com',
eventId: 'callerSelectAnswerEvent',
originServerTs: DateTime.now(),
)
]))
})));
),
],
),
),
},
),
),
);
// wait for select answer to end the call
await Future.delayed(Duration(seconds: 2));
// call ended because answered elsewhere
@ -335,11 +438,14 @@ void main() {
test('Reject incoming call', () async {
expect(voip.currentCID, null);
await matrix.handleSync(SyncUpdate(
await matrix.handleSync(
SyncUpdate(
nextBatch: 'something',
rooms: RoomsUpdate(join: {
rooms: RoomsUpdate(
join: {
room.id: JoinedRoomUpdate(
timeline: TimelineUpdate(events: [
timeline: TimelineUpdate(
events: [
MatrixEvent(
type: 'm.call.invite',
content: {
@ -347,19 +453,27 @@ void main() {
'call_id': 'reject_call',
'party_id': 'GHTYAJCE_caller',
'version': '1',
'offer': {'type': 'offer', 'sdp': 'sdp'}
'offer': {'type': 'offer', 'sdp': 'sdp'},
},
senderId: '@alice:testing.com',
eventId: 'callerInviteEvent',
originServerTs: DateTime.now(),
)
]))
})));
await matrix.handleSync(SyncUpdate(
),
],
),
),
},
),
),
);
await matrix.handleSync(
SyncUpdate(
nextBatch: 'something',
rooms: RoomsUpdate(join: {
rooms: RoomsUpdate(
join: {
room.id: JoinedRoomUpdate(
timeline: TimelineUpdate(events: [
timeline: TimelineUpdate(
events: [
MatrixEvent(
type: 'm.call.candidates',
content: {
@ -368,24 +482,30 @@ void main() {
'version': '1',
'candidates': [
{
'candidate': 'candidate:01UDP2122252543uwu50184typhost',
'candidate':
'candidate:01UDP2122252543uwu50184typhost',
'sdpMid': '0',
'sdpMLineIndex': 0
'sdpMLineIndex': 0,
},
{
'candidate':
'candidate:31TCP2105524479uwu9typhosttcptypeactive',
'sdpMid': '0',
'sdpMLineIndex': 0
'sdpMLineIndex': 0,
}
],
},
senderId: '@alice:testing.com',
eventId: 'callerCallCandidatesEvent',
originServerTs: DateTime.now(),
)
]))
})));
),
],
),
),
},
),
),
);
while (
voip.currentCID != VoipId(roomId: room.id, callId: 'reject_call')) {
// call invite looks valid, call should be created now :D
@ -414,11 +534,14 @@ void main() {
expect(firstCall.state, CallState.kInviteSent);
// KABOOM YOU JUST GLARED
await Future.delayed(Duration(seconds: 3));
await matrix.handleSync(SyncUpdate(
await matrix.handleSync(
SyncUpdate(
nextBatch: 'something',
rooms: RoomsUpdate(join: {
rooms: RoomsUpdate(
join: {
room.id: JoinedRoomUpdate(
timeline: TimelineUpdate(events: [
timeline: TimelineUpdate(
events: [
MatrixEvent(
type: 'm.call.invite',
content: {
@ -426,17 +549,24 @@ void main() {
'call_id': 'zzzz_glare_2nd_call',
'party_id': 'GHTYAJCE_caller',
'version': '1',
'offer': {'type': 'offer', 'sdp': 'sdp'}
'offer': {'type': 'offer', 'sdp': 'sdp'},
},
senderId: '@alice:testing.com',
eventId: 'callerInviteEvent',
originServerTs: DateTime.now(),
)
]))
})));
),
],
),
),
},
),
),
);
await Future.delayed(Duration(seconds: 3));
expect(
voip.currentCID, VoipId(roomId: room.id, callId: firstCall.callId));
voip.currentCID,
VoipId(roomId: room.id, callId: firstCall.callId),
);
await firstCall.hangup(reason: CallErrorCode.userBusy);
});
test('Glare before invite was sent', () async {
@ -450,11 +580,14 @@ void main() {
// KABOOM YOU JUST GLARED, but this tiem you were still preparing your call
// so just cancel that instead
await Future.delayed(Duration(seconds: 3));
await matrix.handleSync(SyncUpdate(
await matrix.handleSync(
SyncUpdate(
nextBatch: 'something',
rooms: RoomsUpdate(join: {
rooms: RoomsUpdate(
join: {
room.id: JoinedRoomUpdate(
timeline: TimelineUpdate(events: [
timeline: TimelineUpdate(
events: [
MatrixEvent(
type: 'm.call.invite',
content: {
@ -462,17 +595,24 @@ void main() {
'call_id': 'zzzz_glare_2nd_call',
'party_id': 'GHTYAJCE_caller',
'version': '1',
'offer': {'type': 'offer', 'sdp': 'sdp'}
'offer': {'type': 'offer', 'sdp': 'sdp'},
},
senderId: '@alice:testing.com',
eventId: 'callerInviteEvent',
originServerTs: DateTime.now(),
)
]))
})));
),
],
),
),
},
),
),
);
await Future.delayed(Duration(seconds: 3));
expect(voip.currentCID,
VoipId(roomId: room.id, callId: 'zzzz_glare_2nd_call'));
expect(
voip.currentCID,
VoipId(roomId: room.id, callId: 'zzzz_glare_2nd_call'),
);
});
test('getFamedlyCallEvents sort order', () {
@ -491,7 +631,7 @@ void main() {
roomId: room.id,
membershipId: voip.currentSessionId,
).toJson(),
]
],
},
type: EventTypes.GroupCallMember,
eventId: 'asdfasdf',
@ -514,7 +654,7 @@ void main() {
roomId: room.id,
membershipId: voip.currentSessionId,
).toJson(),
]
],
},
type: EventTypes.GroupCallMember,
eventId: 'asdfasdf',
@ -537,7 +677,7 @@ void main() {
roomId: room.id,
membershipId: voip.currentSessionId,
).toJson(),
]
],
},
type: EventTypes.GroupCallMember,
eventId: 'asdfasdf',
@ -562,7 +702,7 @@ void main() {
roomId: room.id,
membershipId: voip.currentSessionId,
).toJson(),
]
],
},
type: EventTypes.GroupCallMember,
eventId: 'asdfasdf',
@ -572,22 +712,38 @@ void main() {
stateKey: '@test3:example.com',
),
);
expect(room.getFamedlyCallEvents().entries.elementAt(0).key,
'@test3:example.com');
expect(room.getFamedlyCallEvents().entries.elementAt(1).key,
'@test2:example.com');
expect(room.getFamedlyCallEvents().entries.elementAt(2).key,
'@test2.0:example.com');
expect(room.getFamedlyCallEvents().entries.elementAt(3).key,
'@test1:example.com');
expect(room.getCallMembershipsFromRoom().entries.elementAt(0).key,
'@test3:example.com');
expect(room.getCallMembershipsFromRoom().entries.elementAt(1).key,
'@test2:example.com');
expect(room.getCallMembershipsFromRoom().entries.elementAt(2).key,
'@test2.0:example.com');
expect(room.getCallMembershipsFromRoom().entries.elementAt(3).key,
'@test1:example.com');
expect(
room.getFamedlyCallEvents().entries.elementAt(0).key,
'@test3:example.com',
);
expect(
room.getFamedlyCallEvents().entries.elementAt(1).key,
'@test2:example.com',
);
expect(
room.getFamedlyCallEvents().entries.elementAt(2).key,
'@test2.0:example.com',
);
expect(
room.getFamedlyCallEvents().entries.elementAt(3).key,
'@test1:example.com',
);
expect(
room.getCallMembershipsFromRoom().entries.elementAt(0).key,
'@test3:example.com',
);
expect(
room.getCallMembershipsFromRoom().entries.elementAt(1).key,
'@test2:example.com',
);
expect(
room.getCallMembershipsFromRoom().entries.elementAt(2).key,
'@test2.0:example.com',
);
expect(
room.getCallMembershipsFromRoom().entries.elementAt(3).key,
'@test1:example.com',
);
});
test('Enabling group calls', () async {
@ -601,7 +757,7 @@ void main() {
content: {
'events': {EventTypes.GroupCallMember: 100},
'state_default': 50,
'users_default': 0
'users_default': 0,
},
originServerTs: DateTime.now(),
stateKey: '',
@ -619,7 +775,7 @@ void main() {
content: {
'events': {EventTypes.GroupCallMember: 27},
'state_default': 50,
'users_default': 49
'users_default': 49,
},
originServerTs: DateTime.now(),
stateKey: '',
@ -638,10 +794,11 @@ void main() {
content: {
'state_default': 50,
'users': {'@test:fakeServer.notExisting': 100},
'users_default': 0
'users_default': 0,
},
originServerTs: DateTime.now(),
stateKey: ''),
stateKey: '',
),
);
expect(room.canJoinGroupCall, true); // because admin
expect(room.groupCallsEnabledForEveryone, false);
@ -710,10 +867,11 @@ void main() {
roomId: room.id,
membershipId: voip.currentSessionId,
).toJson(),
]
],
},
originServerTs: DateTime.now(),
stateKey: '@test1:example.com'),
stateKey: '@test1:example.com',
),
);
expect(room.groupCallParticipantCount('participants_count'), 0);
@ -738,10 +896,11 @@ void main() {
roomId: room.id,
membershipId: voip.currentSessionId,
).toJson(),
]
],
},
originServerTs: DateTime.now(),
stateKey: '@test2:example.com'),
stateKey: '@test2:example.com',
),
);
expect(room.groupCallParticipantCount('participants_count'), 1);
expect(room.hasActiveGroupCall, true);
@ -763,10 +922,11 @@ void main() {
roomId: room.id,
membershipId: voip.currentSessionId,
).toJson(),
]
],
},
originServerTs: DateTime.now(),
stateKey: '@test3:example.com'),
stateKey: '@test3:example.com',
),
);
expect(room.groupCallParticipantCount('participants_count'), 2);

View File

@ -38,17 +38,19 @@ void main() {
'display_name': 'John Doe',
'three_pids': [
{'medium': 'email', 'address': 'john.doe@example.org'},
{'medium': 'msisdn', 'address': '123456789'}
]
}
}
{'medium': 'msisdn', 'address': '123456789'},
],
},
},
},
'{"a":null}': {'a': null},
};
for (final entry in textMap.entries) {
test(entry.key, () async {
expect(
entry.key, String.fromCharCodes(canonicalJson.encode(entry.value)));
entry.key,
String.fromCharCodes(canonicalJson.encode(entry.value)),
);
});
}
});

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More