Merge branch 'malin/add-type-casts-for-refactored-maps' into 'main'

Add type casts for refactored Maps

See merge request famedly/company/frontend/famedlysdk!1313
This commit is contained in:
Malin Errenst 2023-06-21 13:22:40 +00:00
commit 69292b1fd1
15 changed files with 226 additions and 134 deletions

View File

@ -69,11 +69,14 @@ class KeyManager {
if (lastEvent != null && if (lastEvent != null &&
lastEvent.type == EventTypes.Encrypted && lastEvent.type == EventTypes.Encrypted &&
lastEvent.content['can_request_session'] == true) { lastEvent.content['can_request_session'] == true) {
try { final sessionId = lastEvent.content.tryGet<String>('session_id');
maybeAutoRequest(room.id, lastEvent.content['session_id'], final senderKey = lastEvent.content.tryGet<String>('sender_key');
lastEvent.content['sender_key']); if (sessionId != null && senderKey != null) {
} catch (_) { maybeAutoRequest(
// dispose room.id,
sessionId,
senderKey,
);
} }
} }
} }
@ -638,23 +641,25 @@ class KeyManager {
final sessionId = sessionEntry.key; final sessionId = sessionEntry.key;
final session = sessionEntry.value; final session = sessionEntry.value;
final sessionData = session.sessionData; final sessionData = session.sessionData;
Map<String, dynamic>? decrypted; Map<String, Object?>? decrypted;
try { try {
decrypted = json.decode(decryption.decrypt(sessionData['ephemeral'], decrypted = json.decode(decryption.decrypt(
sessionData['mac'], sessionData['ciphertext'])); sessionData['ephemeral'] as String,
sessionData['mac'] as String,
sessionData['ciphertext'] as String));
} catch (e, s) { } catch (e, s) {
Logs().e('[LibOlm] Error decrypting room key', e, s); Logs().e('[LibOlm] Error decrypting room key', e, s);
} }
if (decrypted != null) { final senderKey = decrypted?.tryGet<String>('sender_key');
if (decrypted != null && senderKey != null) {
decrypted['session_id'] = sessionId; decrypted['session_id'] = sessionId;
decrypted['room_id'] = roomId; decrypted['room_id'] = roomId;
await setInboundGroupSession( await setInboundGroupSession(
roomId, sessionId, decrypted['sender_key'], decrypted, roomId, sessionId, senderKey, decrypted,
forwarded: true, forwarded: true,
senderClaimedKeys: decrypted['sender_claimed_keys'] != null senderClaimedKeys: decrypted
? Map<String, String>.from( .tryGetMap<String, String>('sender_claimed_keys') ??
decrypted['sender_claimed_keys']!) <String, String>{},
: <String, String>{},
uploaded: true); uploaded: true);
} }
} }
@ -831,13 +836,24 @@ class KeyManager {
Logs().i( 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')) { if (!event.content.containsKey('body')) {
Logs().i('[KeyManager] No body, doing nothing'); Logs().w('[KeyManager] No body, doing nothing');
return; // no body return; // no body
} }
final body = event.content.tryGetMap<String, Object?>('body');
if (body == null) {
Logs().w('[KeyManager] Wrong type for body, doing nothing');
return; // wrong type for body
}
final roomId = body.tryGet<String>('room_id');
if (roomId == null) {
Logs().w(
'[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] final device = client.userDeviceKeys[event.sender]
?.deviceKeys[event.content['requesting_device_id']]; ?.deviceKeys[event.content['requesting_device_id']];
if (device == null) { if (device == null) {
Logs().i('[KeyManager] Device not found, doing nothing'); Logs().w('[KeyManager] Device not found, doing nothing');
return; // device not found return; // device not found
} }
if (device.userId == client.userID && if (device.userId == client.userID &&
@ -845,20 +861,30 @@ class KeyManager {
Logs().i('[KeyManager] Request is by ourself, ignoring'); Logs().i('[KeyManager] Request is by ourself, ignoring');
return; // ignore requests by ourself return; // ignore requests by ourself
} }
final room = client.getRoomById(event.content['body']['room_id']); final room = client.getRoomById(roomId);
if (room == null) { if (room == null) {
Logs().i('[KeyManager] Unknown room, ignoring'); Logs().i('[KeyManager] Unknown room, ignoring');
return; // unknown room return; // unknown room
} }
final sessionId = event.content['body']['session_id']; final sessionId = body.tryGet<String>('session_id');
if (sessionId == null) {
Logs().w(
'[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 // okay, let's see if we have this session at all
final session = await loadInboundGroupSession(room.id, sessionId); final session = await loadInboundGroupSession(room.id, sessionId);
if (session == null) { if (session == null) {
Logs().i('[KeyManager] Unknown session, ignoring'); Logs().i('[KeyManager] Unknown session, ignoring');
return; // we don't have this session anyways return; // we don't have this session anyways
} }
if (event.content['request_id'] is! String) {
Logs().w(
'[KeyManager] Wrong type for request_id or no request_id, doing nothing');
return; // wrong type for request_id
}
final request = KeyManagerKeyShareRequest( final request = KeyManagerKeyShareRequest(
requestId: event.content['request_id'], requestId: event.content.tryGet<String>('request_id')!,
devices: [device], devices: [device],
room: room, room: room,
sessionId: sessionId, sessionId: sessionId,
@ -878,7 +904,7 @@ class KeyManager {
await roomKeyRequest.forwardKey(); await roomKeyRequest.forwardKey();
} else if (device.encryptToDevice && } else if (device.encryptToDevice &&
session.allowedAtIndex session.allowedAtIndex
.tryGet<Map<String, dynamic>>(device.userId) .tryGet<Map<String, Object?>>(device.userId)
?.tryGet(device.curve25519Key!) != ?.tryGet(device.curve25519Key!) !=
null) { null) {
// if we know the user may see the message, then we can just forward the key. // if we know the user may see the message, then we can just forward the key.
@ -931,15 +957,19 @@ class KeyManager {
if (event.content['forwarding_curve25519_key_chain'] is! List) { if (event.content['forwarding_curve25519_key_chain'] is! List) {
event.content['forwarding_curve25519_key_chain'] = <String>[]; event.content['forwarding_curve25519_key_chain'] = <String>[];
} }
event.content['forwarding_curve25519_key_chain'] (event.content['forwarding_curve25519_key_chain'] as List)
.add(encryptedContent['sender_key']); .add(encryptedContent['sender_key']);
if (event.content['sender_claimed_ed25519_key'] is! String) {
Logs().w('sender_claimed_ed255519_key has wrong type');
return; // wrong type
}
// TODO: verify that the keys work to decrypt a message // TODO: verify that the keys work to decrypt a message
// alright, all checks out, let's go ahead and store this session // alright, all checks out, let's go ahead and store this session
await setInboundGroupSession(request.room.id, request.sessionId, await setInboundGroupSession(request.room.id, request.sessionId,
device.curve25519Key!, event.content, device.curve25519Key!, event.content,
forwarded: true, forwarded: true,
senderClaimedKeys: { senderClaimedKeys: {
'ed25519': event.content['sender_claimed_ed25519_key'], 'ed25519': event.content['sender_claimed_ed25519_key'] as String,
}); });
request.devices.removeWhere( request.devices.removeWhere(
(k) => k.userId == device.userId && k.deviceId == device.deviceId); (k) => k.userId == device.userId && k.deviceId == device.deviceId);
@ -972,8 +1002,13 @@ class KeyManager {
Logs().v('[KeyManager] not encrypted, ignoring...'); Logs().v('[KeyManager] not encrypted, ignoring...');
return; // the event wasn't encrypted, this is a security risk; return; // the event wasn't encrypted, this is a security risk;
} }
final String roomId = event.content['room_id']; final roomId = event.content.tryGet<String>('room_id');
final String sessionId = event.content['session_id']; 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');
return;
}
final sender_ed25519 = client.userDeviceKeys[event.sender] final sender_ed25519 = client.userDeviceKeys[event.sender]
?.deviceKeys[event.content['requesting_device_id']]?.ed25519Key; ?.deviceKeys[event.content['requesting_device_id']]?.ed25519Key;
if (sender_ed25519 != null) { if (sender_ed25519 != null) {

View File

@ -597,11 +597,12 @@ class OlmManager {
client.userDeviceKeys[userId]!.deviceKeys[deviceId]!.ed25519Key; client.userDeviceKeys[userId]!.deviceKeys[deviceId]!.ed25519Key;
final identityKey = final identityKey =
client.userDeviceKeys[userId]!.deviceKeys[deviceId]!.curve25519Key; client.userDeviceKeys[userId]!.deviceKeys[deviceId]!.curve25519Key;
for (final Map<String, dynamic> deviceKey for (final deviceKey in deviceKeysEntry.value.values) {
in deviceKeysEntry.value.values) {
if (fingerprintKey == null || if (fingerprintKey == null ||
identityKey == null || identityKey == null ||
!deviceKey.checkJsonSignature(fingerprintKey, userId, deviceId)) { deviceKey is! Map<String, Object?> ||
!deviceKey.checkJsonSignature(fingerprintKey, userId, deviceId) ||
deviceKey['key'] is! String) {
Logs().w( Logs().w(
'Skipping invalid device key from $userId:$deviceId', 'Skipping invalid device key from $userId:$deviceId',
deviceKey, deviceKey,
@ -612,7 +613,7 @@ class OlmManager {
final session = olm.Session(); final session = olm.Session();
try { try {
session.create_outbound( session.create_outbound(
_olmAccount!, identityKey, deviceKey['key']); _olmAccount!, identityKey, deviceKey.tryGet<String>('key')!);
await storeOlmSession(OlmSession( await storeOlmSession(OlmSession(
key: client.userID!, key: client.userID!,
identityKey: identityKey, identityKey: identityKey,

View File

@ -280,8 +280,7 @@ class SSSS {
} }
bool isSecret(String type) => bool isSecret(String type) =>
client.accountData[type] != null && client.accountData[type]?.content['encrypted'] is Map;
client.accountData[type]!.content['encrypted'] is Map;
Future<String?> getCached(String type) async { Future<String?> getCached(String type) async {
if (client.database == null) { if (client.database == null) {
@ -295,8 +294,11 @@ class SSSS {
bool isValid(SSSSCache dbEntry) => bool isValid(SSSSCache dbEntry) =>
keys.contains(dbEntry.keyId) && keys.contains(dbEntry.keyId) &&
dbEntry.ciphertext != null && dbEntry.ciphertext != null &&
client.accountData[type]?.content['encrypted'][dbEntry.keyId] dbEntry.keyId != null &&
['ciphertext'] == client.accountData[type]?.content
.tryGetMap<String, Object?>('encrypted')
?.tryGetMap<String, Object?>(dbEntry.keyId!)
?.tryGet<String>('ciphertext') ==
dbEntry.ciphertext; dbEntry.ciphertext;
final fromCache = _cache[type]; final fromCache = _cache[type];
@ -319,20 +321,31 @@ class SSSS {
if (secretInfo == null) { if (secretInfo == null) {
throw Exception('Not found'); throw Exception('Not found');
} }
if (secretInfo.content['encrypted'] is! Map) { final encryptedContent =
secretInfo.content.tryGetMap<String, Object?>('encrypted');
if (encryptedContent == null) {
throw Exception('Content is not encrypted'); throw Exception('Content is not encrypted');
} }
if (secretInfo.content['encrypted'][keyId] is! Map) { final enc = encryptedContent.tryGetMap<String, Object?>(keyId);
if (enc == null) {
throw Exception('Wrong / unknown key'); throw Exception('Wrong / unknown key');
} }
final enc = secretInfo.content['encrypted'][keyId]; final ciphertext = enc.tryGet<String>('ciphertext');
final iv = enc.tryGet<String>('iv');
final mac = enc.tryGet<String>('mac');
if (ciphertext == null || iv == null || mac == null) {
throw Exception('Wrong types for encrypted content or missing keys.');
}
final encryptInfo = EncryptedContent( final encryptInfo = EncryptedContent(
iv: enc['iv'], ciphertext: enc['ciphertext'], mac: enc['mac']); iv: iv,
ciphertext: ciphertext,
mac: mac,
);
final decrypted = await decryptAes(encryptInfo, key, type); final decrypted = await decryptAes(encryptInfo, key, type);
final db = client.database; final db = client.database;
if (cacheTypes.contains(type) && db != null) { if (cacheTypes.contains(type) && db != null) {
// cache the thing // cache the thing
await db.storeSSSSCache(type, keyId, enc['ciphertext'], decrypted); await db.storeSSSSCache(type, keyId, ciphertext, decrypted);
onSecretStored.add(keyId); onSecretStored.add(keyId);
if (_cacheCallbacks.containsKey(type) && await getCached(type) == null) { if (_cacheCallbacks.containsKey(type) && await getCached(type) == null) {
_cacheCallbacks[type]!(decrypted); _cacheCallbacks[type]!(decrypted);
@ -382,10 +395,14 @@ class SSSS {
if (content == null) { if (content == null) {
throw InvalidPassphraseException('Key has no content!'); throw InvalidPassphraseException('Key has no content!');
} }
final encryptedContent = content.tryGetMap<String, Object?>('encrypted');
if (encryptedContent == null) {
throw Exception('Wrong type for encrypted content!');
}
final otherKeys = final otherKeys =
Set<String>.from(content['encrypted'].keys.where((k) => k != keyId)); Set<String>.from(encryptedContent.keys.where((k) => k != keyId));
content['encrypted'].removeWhere((k, v) => otherKeys.contains(k)); encryptedContent.removeWhere((k, v) => otherKeys.contains(k));
// yes, we are paranoid... // yes, we are paranoid...
if (await getStored(type, keyId, key) != secret) { if (await getStored(type, keyId, key) != secret) {
throw Exception('Secrets do not match up!'); throw Exception('Secrets do not match up!');
@ -394,8 +411,13 @@ class SSSS {
await client.setAccountData(client.userID!, type, content); await client.setAccountData(client.userID!, type, content);
if (cacheTypes.contains(type)) { if (cacheTypes.contains(type)) {
// cache the thing // cache the thing
await client.database?.storeSSSSCache( final ciphertext = encryptedContent
type, keyId, content['encrypted'][keyId]['ciphertext'], secret); .tryGetMap<String, Object?>(keyId)
?.tryGet<String>('ciphertext');
if (ciphertext == null) {
throw Exception('Wrong type for ciphertext!');
}
await client.database?.storeSSSSCache(type, keyId, ciphertext, secret);
onSecretStored.add(keyId); onSecretStored.add(keyId);
} }
} }
@ -501,7 +523,11 @@ class SSSS {
return; // nope....unknown or untrusted device return; // nope....unknown or untrusted device
} }
// alright, all seems fine...let's check if we actually have the secret they are asking for // alright, all seems fine...let's check if we actually have the secret they are asking for
final type = event.content['name']; final type = event.content.tryGet<String>('name');
if (type == null) {
Logs().i('[SSSS] Wrong data type for type param, ignoring');
return;
}
final secret = await getCached(type); final secret = await getCached(type);
if (secret == null) { if (secret == null) {
Logs() Logs()
@ -536,8 +562,8 @@ class SSSS {
Logs().i('[SSSS] Someone else replied?'); Logs().i('[SSSS] Someone else replied?');
return; // someone replied whom we didn't send the share request to return; // someone replied whom we didn't send the share request to
} }
final secret = event.content['secret']; final secret = event.content.tryGet<String>('secret');
if (event.content['secret'] is! String) { if (secret == null) {
Logs().i('[SSSS] Secret wasn\'t a string'); Logs().i('[SSSS] Secret wasn\'t a string');
return; // the secret wasn't a string....wut? return; // the secret wasn't a string....wut?
} }
@ -557,8 +583,14 @@ class SSSS {
if (db != null) { if (db != null) {
final keyId = keyIdFromType(request.type); final keyId = keyIdFromType(request.type);
if (keyId != null) { if (keyId != null) {
final ciphertext = client.accountData[request.type]! final ciphertext = (client.accountData[request.type]!.content
.content['encrypted'][keyId]['ciphertext']; .tryGetMap<String, Object?>('encrypted'))
?.tryGetMap<String, Object?>(keyId)
?.tryGet<String>('ciphertext');
if (ciphertext == null) {
Logs().i('[SSSS] Ciphertext is empty or not a String');
return;
}
await db.storeSSSSCache(request.type, keyId, ciphertext, secret); await db.storeSSSSCache(request.type, keyId, ciphertext, secret);
if (_cacheCallbacks.containsKey(request.type)) { if (_cacheCallbacks.containsKey(request.type)) {
_cacheCallbacks[request.type]!(secret); _cacheCallbacks[request.type]!(secret);
@ -574,8 +606,10 @@ class SSSS {
if (data == null) { if (data == null) {
return null; return null;
} }
if (data.content['encrypted'] is Map) { final contentEncrypted =
return data.content['encrypted'].keys.toSet(); data.content.tryGetMap<String, Object?>('encrypted');
if (contentEncrypted != null) {
return contentEncrypted.keys.toSet();
} }
return null; return null;
} }

View File

@ -104,12 +104,14 @@ class Bootstrap {
for (final entry in client.accountData.entries) { for (final entry in client.accountData.entries) {
final type = entry.key; final type = entry.key;
final event = entry.value; final event = entry.value;
if (event.content['encrypted'] is! Map) { final encryptedContent =
event.content.tryGetMap<String, Object?>('encrypted');
if (encryptedContent == null) {
continue; continue;
} }
final validKeys = <String>{}; final validKeys = <String>{};
final invalidKeys = <String>{}; final invalidKeys = <String>{};
for (final keyEntry in event.content['encrypted'].entries) { for (final keyEntry in encryptedContent.entries) {
final key = keyEntry.key; final key = keyEntry.key;
final value = keyEntry.value; final value = keyEntry.value;
if (value is! Map) { if (value is! Map) {

View File

@ -47,7 +47,9 @@ extension DehydratedDeviceMatrixApi on MatrixApi {
}, },
}, },
); );
return Map<String, int>.from(response['one_time_key_counts']); return Map<String, int>.from(
response.tryGetMap<String, Object?>('one_time_key_counts') ??
<String, int>{});
} }
/// uploads a dehydrated device. /// uploads a dehydrated device.

View File

@ -298,7 +298,7 @@ class Client extends MatrixApi {
final ruleset = TryGetPushRule.tryFromJson( final ruleset = TryGetPushRule.tryFromJson(
_accountData[EventTypes.PushRules] _accountData[EventTypes.PushRules]
?.content ?.content
.tryGetMap<String, dynamic>('global') ?? .tryGetMap<String, Object?>('global') ??
{}); {});
_pushruleEvaluator = PushruleEvaluator.fromRuleset(ruleset); _pushruleEvaluator = PushruleEvaluator.fromRuleset(ruleset);
} }
@ -1068,7 +1068,7 @@ class Client extends MatrixApi {
PushRuleSet? get globalPushRules { PushRuleSet? get globalPushRules {
final pushrules = _accountData['m.push_rules'] final pushrules = _accountData['m.push_rules']
?.content ?.content
.tryGetMap<String, dynamic>('global'); .tryGetMap<String, Object?>('global');
return pushrules != null ? TryGetPushRule.tryFromJson(pushrules) : null; return pushrules != null ? TryGetPushRule.tryFromJson(pushrules) : null;
} }
@ -1076,7 +1076,7 @@ class Client extends MatrixApi {
PushRuleSet? get devicePushRules { PushRuleSet? get devicePushRules {
final pushrules = _accountData['m.push_rules'] final pushrules = _accountData['m.push_rules']
?.content ?.content
.tryGetMap<String, dynamic>('device'); .tryGetMap<String, Object?>('device');
return pushrules != null ? TryGetPushRule.tryFromJson(pushrules) : null; return pushrules != null ? TryGetPushRule.tryFromJson(pushrules) : null;
} }
@ -2731,12 +2731,15 @@ class Client extends MatrixApi {
/// Whether all push notifications are muted using the [.m.rule.master] /// Whether all push notifications are muted using the [.m.rule.master]
/// rule of the push rules: https://matrix.org/docs/spec/client_server/r0.6.0#m-rule-master /// rule of the push rules: https://matrix.org/docs/spec/client_server/r0.6.0#m-rule-master
bool get allPushNotificationsMuted { bool get allPushNotificationsMuted {
final Map<String, dynamic>? globalPushRules = final Map<String, Object?>? globalPushRules =
_accountData[EventTypes.PushRules]?.content['global']; _accountData[EventTypes.PushRules]
?.content
.tryGetMap<String, Object?>('global');
if (globalPushRules == null) return false; if (globalPushRules == null) return false;
if (globalPushRules['override'] is List) { final globalPushRulesOverride = globalPushRules.tryGetList('override');
for (final pushRule in globalPushRules['override']) { if (globalPushRulesOverride != null) {
for (final pushRule in globalPushRulesOverride) {
if (pushRule['rule_id'] == '.m.rule.master') { if (pushRule['rule_id'] == '.m.rule.master') {
return pushRule['enabled']; return pushRule['enabled'];
} }
@ -2813,12 +2816,12 @@ class Client extends MatrixApi {
} }
/// A list of mxids of users who are ignored. /// A list of mxids of users who are ignored.
List<String> get ignoredUsers => (_accountData List<String> get ignoredUsers =>
.containsKey('m.ignored_user_list') && List<String>.from(_accountData['m.ignored_user_list']
_accountData['m.ignored_user_list']?.content['ignored_users'] is Map) ?.content
? List<String>.from( .tryGetMap<String, Object?>('ignored_users')
_accountData['m.ignored_user_list']?.content['ignored_users'].keys) ?.keys ??
: []; <String>[]);
/// Ignore another user. This will clear the local cached messages to /// Ignore another user. This will clear the local cached messages to
/// hide all previous messages from this user. /// hide all previous messages from this user.

View File

@ -259,7 +259,7 @@ class Event extends MatrixEvent {
String get messageType => type == EventTypes.Sticker String get messageType => type == EventTypes.Sticker
? MessageTypes.Sticker ? MessageTypes.Sticker
: (content['msgtype'] is String ? content['msgtype'] : MessageTypes.Text); : (content.tryGet<String>('msgtype') ?? MessageTypes.Text);
void setRedactionEvent(Event redactedBecause) { void setRedactionEvent(Event redactedBecause) {
unsigned = { unsigned = {
@ -301,11 +301,10 @@ class Event extends MatrixEvent {
} }
/// Returns the body of this event if it has a body. /// Returns the body of this event if it has a body.
String get text => content['body'] is String ? content['body'] : ''; String get text => content.tryGet<String>('body') ?? '';
/// Returns the formatted boy of this event if it has a formatted body. /// Returns the formatted boy of this event if it has a formatted body.
String get formattedText => String get formattedText => content.tryGet<String>('formatted_body') ?? '';
content['formatted_body'] is String ? content['formatted_body'] : '';
/// Use this to get the body. /// Use this to get the body.
String get body { String get body {
@ -402,7 +401,7 @@ class Event extends MatrixEvent {
// in the `sendEvent` method to transition -1 -> 0 -> 1 -> 2 // in the `sendEvent` method to transition -1 -> 0 -> 1 -> 2
return await room.sendEvent( return await room.sendEvent(
content, content,
txid: txid ?? unsigned?['transaction_id'] ?? eventId, txid: txid ?? unsigned?.tryGet<String>('transaction_id') ?? eventId,
); );
} }
@ -432,13 +431,19 @@ class Event extends MatrixEvent {
content['can_request_session'] != true) { content['can_request_session'] != true) {
throw ('Session key not requestable'); throw ('Session key not requestable');
} }
await room.requestSessionKey(content['session_id'], content['sender_key']);
final sessionId = content.tryGet<String>('session_id');
final senderKey = content.tryGet<String>('sender_key');
if (sessionId == null || senderKey == null) {
throw ('Unknown session_id or sender_key');
}
await room.requestSessionKey(sessionId, senderKey);
return; return;
} }
/// Gets the info map of file events, or a blank map if none present /// Gets the info map of file events, or a blank map if none present
Map get infoMap => Map get infoMap =>
content['info'] is Map ? content['info'] : <String, dynamic>{}; content.tryGetMap<String, Object?>('info') ?? <String, Object?>{};
/// Gets the thumbnail info map of file events, or a blank map if nonepresent /// Gets the thumbnail info map of file events, or a blank map if nonepresent
Map get thumbnailInfoMap => infoMap['thumbnail_info'] is Map Map get thumbnailInfoMap => infoMap['thumbnail_info'] is Map
@ -461,9 +466,10 @@ class Event extends MatrixEvent {
/// Gets the mimetype of the attachment of a file event, or a blank string if not present /// Gets the mimetype of the attachment of a file event, or a blank string if not present
String get attachmentMimetype => infoMap['mimetype'] is String String get attachmentMimetype => infoMap['mimetype'] is String
? infoMap['mimetype'].toLowerCase() ? infoMap['mimetype'].toLowerCase()
: (content['file'] is Map && content['file']['mimetype'] is String : (content
? content['file']['mimetype'] .tryGetMap<String, Object?>('file')
: ''); ?.tryGet<String>('mimetype') ??
'');
/// Gets the mimetype of the thumbnail of a file event, or a blank string if not present /// Gets the mimetype of the thumbnail of a file event, or a blank string if not present
String get thumbnailMimetype => thumbnailInfoMap['mimetype'] is String String get thumbnailMimetype => thumbnailInfoMap['mimetype'] is String
@ -475,7 +481,9 @@ class Event extends MatrixEvent {
/// Gets the underlying mxc url of an attachment of a file event, or null if not present /// Gets the underlying mxc url of an attachment of a file event, or null if not present
Uri? get attachmentMxcUrl { Uri? get attachmentMxcUrl {
final url = isAttachmentEncrypted ? content['file']['url'] : content['url']; final url = isAttachmentEncrypted
? (content.tryGetMap<String, Object?>('file')?['url'])
: content['url'];
return url is String ? Uri.tryParse(url) : null; return url is String ? Uri.tryParse(url) : null;
} }
@ -759,21 +767,17 @@ class Event extends MatrixEvent {
// if we need to strip the reply fallback. // if we need to strip the reply fallback.
var htmlMessage = content['format'] != 'org.matrix.custom.html'; var htmlMessage = content['format'] != 'org.matrix.custom.html';
// If we have an edit, we want to operate on the new content // If we have an edit, we want to operate on the new content
final newContent = content.tryGetMap<String, Object?>('m.new_content');
if (hideEdit && if (hideEdit &&
relationshipType == RelationshipTypes.edit && relationshipType == RelationshipTypes.edit &&
content.tryGet<Map<String, dynamic>>('m.new_content') != null) { newContent != null) {
if (plaintextBody && if (plaintextBody && newContent['format'] == 'org.matrix.custom.html') {
content['m.new_content']['format'] == 'org.matrix.custom.html') {
htmlMessage = true; htmlMessage = true;
body = HtmlToText.convert( body = HtmlToText.convert(
(content['m.new_content'] as Map<String, dynamic>) newContent.tryGet<String>('formatted_body') ?? formattedText);
.tryGet<String>('formatted_body') ??
formattedText);
} else { } else {
htmlMessage = false; htmlMessage = false;
body = (content['m.new_content'] as Map<String, dynamic>) body = newContent.tryGet<String>('body') ?? body;
.tryGet<String>('body') ??
body;
} }
} }
// Hide reply fallback // Hide reply fallback
@ -815,31 +819,27 @@ class Event extends MatrixEvent {
/// Get the relationship type of an event. `null` if there is none /// Get the relationship type of an event. `null` if there is none
String? get relationshipType { String? get relationshipType {
if (content.tryGet<Map<String, dynamic>>('m.relates_to') == null) { final mRelatesTo = content.tryGetMap<String, Object?>('m.relates_to');
if (mRelatesTo == null) {
return null; return null;
} }
if (content['m.relates_to'].containsKey('rel_type')) { final relType = mRelatesTo.tryGet<String>('rel_type');
if (content if (relType == RelationshipTypes.thread) {
.tryGet<Map<String, dynamic>>('m.relates_to') return RelationshipTypes.thread;
?.tryGet<String>('rel_type') ==
RelationshipTypes.thread) {
return RelationshipTypes.thread;
}
} }
if (content['m.relates_to'].containsKey('m.in_reply_to')) {
if (mRelatesTo.containsKey('m.in_reply_to')) {
return RelationshipTypes.reply; return RelationshipTypes.reply;
} }
return content return relType;
.tryGet<Map<String, dynamic>>('m.relates_to')
?.tryGet<String>('rel_type');
} }
/// Get the event ID that this relationship will reference. `null` if there is none /// Get the event ID that this relationship will reference. `null` if there is none
String? get relationshipEventId { String? get relationshipEventId {
final relatesToMap = content.tryGetMap<String, dynamic>('m.relates_to'); final relatesToMap = content.tryGetMap<String, Object?>('m.relates_to');
return relatesToMap?.tryGet<String>('event_id') ?? return relatesToMap?.tryGet<String>('event_id') ??
relatesToMap relatesToMap
?.tryGetMap<String, dynamic>('m.in_reply_to') ?.tryGetMap<String, Object?>('m.in_reply_to')
?.tryGet<String>('event_id'); ?.tryGet<String>('event_id');
} }

View File

@ -267,9 +267,10 @@ class Room {
} }
if (membership == Membership.leave) { if (membership == Membership.leave) {
final invitation = getState(EventTypes.RoomMember, client.userID!); final invitation = getState(EventTypes.RoomMember, client.userID!);
if (invitation != null && invitation.unsigned?['prev_sender'] != null) { if (invitation != null &&
invitation.unsigned?.tryGet<String>('prev_sender') != null) {
final name = unsafeGetUserFromMemoryOrFallback( final name = unsafeGetUserFromMemoryOrFallback(
invitation.unsigned?['prev_sender']) invitation.unsigned!.tryGet<String>('prev_sender')!)
.calcDisplayname(i18n: i18n); .calcDisplayname(i18n: i18n);
return i18n.wasDirectChatDisplayName(name); return i18n.wasDirectChatDisplayName(name);
} }
@ -1315,8 +1316,9 @@ class Room {
Future<void> removeFromDirectChat() async { Future<void> removeFromDirectChat() async {
final directChats = client.directChats.copy(); final directChats = client.directChats.copy();
for (final k in directChats.keys) { for (final k in directChats.keys) {
if (directChats[k] is List && directChats[k].contains(id)) { final directChat = directChats[k];
directChats[k].remove(id); if (directChat is List && directChat.contains(id)) {
directChat.remove(id);
} }
} }
@ -1747,7 +1749,7 @@ class Room {
return getState(EventTypes.RoomCreate)?.senderId == userId ? 100 : 0; return getState(EventTypes.RoomCreate)?.senderId == userId ? 100 : 0;
} }
return powerLevelMap return powerLevelMap
.tryGetMap<String, dynamic>('users') .tryGetMap<String, Object?>('users')
?.tryGet<int>(userId) ?? ?.tryGet<int>(userId) ??
powerLevelMap.tryGet<int>('users_default') ?? powerLevelMap.tryGet<int>('users_default') ??
0; 0;
@ -1802,7 +1804,7 @@ class Room {
final powerLevelMap = getState(EventTypes.RoomPowerLevels)?.content; final powerLevelMap = getState(EventTypes.RoomPowerLevels)?.content;
if (powerLevelMap == null) return 0; if (powerLevelMap == null) return 0;
return powerLevelMap return powerLevelMap
.tryGetMap<String, dynamic>('events') .tryGetMap<String, Object?>('events')
?.tryGet<int>(action) ?? ?.tryGet<int>(action) ??
powerLevelMap.tryGet<int>('state_default') ?? powerLevelMap.tryGet<int>('state_default') ??
50; 50;
@ -1833,7 +1835,8 @@ class Room {
final currentPowerLevelsMap = getState(EventTypes.RoomPowerLevels)?.content; final currentPowerLevelsMap = getState(EventTypes.RoomPowerLevels)?.content;
if (currentPowerLevelsMap != null) { if (currentPowerLevelsMap != null) {
final newPowerLevelMap = currentPowerLevelsMap; final newPowerLevelMap = currentPowerLevelsMap;
final eventsMap = newPowerLevelMap['events'] ?? {}; final eventsMap = newPowerLevelMap.tryGetMap<String, Object?>('events') ??
<String, Object?>{};
eventsMap.addAll({ eventsMap.addAll({
EventTypes.GroupCallPrefix: getDefaultPowerLevel(currentPowerLevelsMap), EventTypes.GroupCallPrefix: getDefaultPowerLevel(currentPowerLevelsMap),
EventTypes.GroupCallMemberPrefix: EventTypes.GroupCallMemberPrefix:
@ -1901,7 +1904,7 @@ class Room {
final powerLevelsMap = getState(EventTypes.RoomPowerLevels)?.content; final powerLevelsMap = getState(EventTypes.RoomPowerLevels)?.content;
if (powerLevelsMap == null) return 0 <= ownPowerLevel; if (powerLevelsMap == null) return 0 <= ownPowerLevel;
final pl = powerLevelsMap final pl = powerLevelsMap
.tryGetMap<String, dynamic>('events') .tryGetMap<String, Object?>('events')
?.tryGet<int>(eventType) ?? ?.tryGet<int>(eventType) ??
powerLevelsMap.tryGet<int>('events_default') ?? powerLevelsMap.tryGet<int>('events_default') ??
0; 0;
@ -1913,7 +1916,7 @@ class Room {
final userLevel = getPowerLevelByUserId(userid); final userLevel = getPowerLevelByUserId(userid);
final notificationLevel = getState(EventTypes.RoomPowerLevels) final notificationLevel = getState(EventTypes.RoomPowerLevels)
?.content ?.content
.tryGetMap<String, dynamic>('notifications') .tryGetMap<String, Object?>('notifications')
?.tryGet<int>(notificationType) ?? ?.tryGet<int>(notificationType) ??
50; 50;

View File

@ -347,16 +347,16 @@ class Timeline {
if (event.type == EventTypes.Encrypted && if (event.type == EventTypes.Encrypted &&
event.messageType == MessageTypes.BadEncrypted && event.messageType == MessageTypes.BadEncrypted &&
event.content['can_request_session'] == true) { event.content['can_request_session'] == true) {
try { final sessionId = event.content.tryGet<String>('session_id');
final senderKey = event.content.tryGet<String>('sender_key');
if (sessionId != null && senderKey != null) {
room.client.encryption?.keyManager.maybeAutoRequest( room.client.encryption?.keyManager.maybeAutoRequest(
room.id, room.id,
event.content['session_id'], sessionId,
event.content['sender_key'], senderKey,
tryOnlineBackup: tryOnlineBackup, tryOnlineBackup: tryOnlineBackup,
onlineKeyBackupOnly: onlineKeyBackupOnly, onlineKeyBackupOnly: onlineKeyBackupOnly,
); );
} catch (_) {
// dispose
} }
} }
} }
@ -387,7 +387,7 @@ class Timeline {
for (i = 0; i < events.length; i++) { for (i = 0; i < events.length; i++) {
final searchHaystack = <String>{events[i].eventId}; final searchHaystack = <String>{events[i].eventId};
final txnid = events[i].unsigned?['transaction_id']; final txnid = events[i].unsigned?.tryGet<String>('transaction_id');
if (txnid != null) { if (txnid != null) {
searchHaystack.add(txnid); searchHaystack.add(txnid);
} }
@ -401,8 +401,9 @@ class Timeline {
void _removeEventFromSet(Set<Event> eventSet, Event event) { void _removeEventFromSet(Set<Event> eventSet, Event event) {
eventSet.removeWhere((e) => eventSet.removeWhere((e) =>
e.matchesEventOrTransactionId(event.eventId) || e.matchesEventOrTransactionId(event.eventId) ||
(event.unsigned != null && event.unsigned != null &&
e.matchesEventOrTransactionId(event.unsigned?['transaction_id']))); e.matchesEventOrTransactionId(
event.unsigned?.tryGet<String>('transaction_id')));
} }
void addAggregatedEvent(Event event) { void addAggregatedEvent(Event event) {

View File

@ -275,7 +275,7 @@ abstract class SignableKey extends MatrixSignableKey {
var haveValidSignature = false; var haveValidSignature = false;
var gotSignatureFromCache = false; var gotSignatureFromCache = false;
final fullKeyIdBool = validSignatures final fullKeyIdBool = validSignatures
?.tryGetMap<String, dynamic>(otherUserId) ?.tryGetMap<String, Object?>(otherUserId)
?.tryGet<bool>(fullKeyId); ?.tryGet<bool>(fullKeyId);
if (fullKeyIdBool == true) { if (fullKeyIdBool == true) {
haveValidSignature = true; haveValidSignature = true;
@ -426,14 +426,15 @@ class DeviceKeys extends SignableKey {
late DateTime lastActive; late DateTime lastActive;
String? get curve25519Key => keys['curve25519:$deviceId']; String? get curve25519Key => keys['curve25519:$deviceId'];
String? get deviceDisplayName => unsigned?['device_display_name']; String? get deviceDisplayName =>
unsigned?.tryGet<String>('device_display_name');
bool? _validSelfSignature; bool? _validSelfSignature;
bool get selfSigned => bool get selfSigned =>
_validSelfSignature ?? _validSelfSignature ??
(_validSelfSignature = (deviceId != null && (_validSelfSignature = (deviceId != null &&
signatures signatures
?.tryGetMap<String, dynamic>(userId) ?.tryGetMap<String, Object?>(userId)
?.tryGet<String>('ed25519:$deviceId') == ?.tryGet<String>('ed25519:$deviceId') ==
null null
? false ? false

View File

@ -204,11 +204,11 @@ abstract class EventLocalizations {
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n)), event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n)),
EventTypes.RoomName: (event, i18n, body) => i18n.changedTheChatNameTo( EventTypes.RoomName: (event, i18n, body) => i18n.changedTheChatNameTo(
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n), event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
event.content['name']), event.content.tryGet<String>('name') ?? ''),
EventTypes.RoomTopic: (event, i18n, body) => EventTypes.RoomTopic: (event, i18n, body) =>
i18n.changedTheChatDescriptionTo( i18n.changedTheChatDescriptionTo(
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n), event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
event.content['topic']), event.content.tryGet<String>('topic') ?? ''),
EventTypes.RoomAvatar: (event, i18n, body) => i18n.changedTheChatAvatar( EventTypes.RoomAvatar: (event, i18n, body) => i18n.changedTheChatAvatar(
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n)), event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n)),
EventTypes.GuestAccess: (event, i18n, body) { EventTypes.GuestAccess: (event, i18n, body) {
@ -260,7 +260,7 @@ abstract class EventLocalizations {
EventTypes.Reaction: (event, i18n, body) => i18n.sentReaction( EventTypes.Reaction: (event, i18n, body) => i18n.sentReaction(
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n), event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
event.content event.content
.tryGetMap<String, dynamic>('m.relates_to') .tryGetMap<String, Object?>('m.relates_to')
?.tryGet<String>('key') ?? ?.tryGet<String>('key') ??
body, body,
), ),

View File

@ -60,12 +60,14 @@ extension ImagePackRoomExtension on Room {
addImagePack(client.accountData['im.ponies.user_emotes'], slug: 'user'); addImagePack(client.accountData['im.ponies.user_emotes'], slug: 'user');
// next we add all the external image packs // next we add all the external image packs
final packRooms = client.accountData['im.ponies.emote_rooms']; final packRooms = client.accountData['im.ponies.emote_rooms'];
if (packRooms != null && packRooms.content['rooms'] is Map) { final rooms = packRooms?.content.tryGetMap<String, Object?>('rooms');
for (final roomEntry in packRooms.content['rooms'].entries) { if (packRooms != null && rooms != null) {
for (final roomEntry in rooms.entries) {
final roomId = roomEntry.key; final roomId = roomEntry.key;
final room = client.getRoomById(roomId); final room = client.getRoomById(roomId);
if (room != null && roomEntry.value is Map) { final roomEntryValue = roomEntry.value;
for (final stateKeyEntry in roomEntry.value.entries) { if (room != null && roomEntryValue is Map<String, Object?>) {
for (final stateKeyEntry in roomEntryValue.entries) {
final stateKey = stateKeyEntry.key; final stateKey = stateKeyEntry.key;
final fallbackSlug = final fallbackSlug =
'${room.getLocalizedDisplayname()}-${stateKey.isNotEmpty ? '$stateKey-' : ''}${room.id}'; '${room.getLocalizedDisplayname()}-${stateKey.isNotEmpty ? '$stateKey-' : ''}${room.id}';

View File

@ -704,18 +704,20 @@ class VoIP {
final groupCallId = event.stateKey; final groupCallId = event.stateKey;
final callType = content['m.type']; final callType = content.tryGet<String>('m.type');
if (callType != GroupCallType.Video && callType != GroupCallType.Voice) { if (callType == null ||
callType != GroupCallType.Video && callType != GroupCallType.Voice) {
Logs().w('Received invalid group call type $callType for room $roomId.'); Logs().w('Received invalid group call type $callType for room $roomId.');
return null; return null;
} }
final callIntent = content['m.intent']; final callIntent = content.tryGet<String>('m.intent');
if (callIntent != GroupCallIntent.Prompt && if (callIntent == null ||
callIntent != GroupCallIntent.Room && callIntent != GroupCallIntent.Prompt &&
callIntent != GroupCallIntent.Ring) { callIntent != GroupCallIntent.Room &&
callIntent != GroupCallIntent.Ring) {
Logs() Logs()
.w('Received invalid group call intent $callType for room $roomId.'); .w('Received invalid group call intent $callType for room $roomId.');
return null; return null;

View File

@ -22,7 +22,7 @@ dependencies:
image: ^4.0.15 image: ^4.0.15
js: ^0.6.3 js: ^0.6.3
markdown: ^4.0.0 markdown: ^4.0.0
matrix_api_lite: ">=1.6.0 <1.7.0" # Pinned until compatible with 1.7.x matrix_api_lite: ^1.7.0
mime: ^1.0.0 mime: ^1.0.0
olm: ^2.0.2 olm: ^2.0.2
random_string: ^2.3.1 random_string: ^2.3.1
@ -38,3 +38,7 @@ dev_dependencies:
lints: ^2.0.0 lints: ^2.0.0
test: ^1.15.7 test: ^1.15.7
#flutter_test: {sdk: flutter} #flutter_test: {sdk: flutter}
#dependency_overrides:
# matrix_api_lite:
# path: ../matrix_api_lite

View File

@ -1042,8 +1042,10 @@ void main() {
test('pushRuleState', () async { test('pushRuleState', () async {
expect(room.pushRuleState, PushRuleState.mentionsOnly); expect(room.pushRuleState, PushRuleState.mentionsOnly);
matrix.accountData['m.push_rules']?.content['global']['override'].add( ((matrix.accountData['m.push_rules']?.content['global']
matrix.accountData['m.push_rules']?.content['global']['room'][0]); as Map<String, Object?>)['override'] as List)
.add(((matrix.accountData['m.push_rules']?.content['global']
as Map<String, Object?>)['room'] as List)[0]);
expect(room.pushRuleState, PushRuleState.dontNotify); expect(room.pushRuleState, PushRuleState.dontNotify);
}); });