From b32f0f28bad6613cfb6c96cd8d92043586db1c39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Ku=C3=9Fowski?= Date: Mon, 18 Aug 2025 07:51:58 +0200 Subject: [PATCH 1/4] refactor: Sync for unknown room in push helper and catch timeout exceptions --- lib/src/client.dart | 33 ++++++++++++++++++++------------- test/client_test.dart | 2 +- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/lib/src/client.dart b/lib/src/client.dart index 3487a7d3..db490235 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -1790,12 +1790,19 @@ class Client extends MatrixApi { if (eventId == null || roomId == null) return null; // Create the room object: - final room = getRoomById(roomId) ?? - await database.getSingleRoom(this, roomId) ?? - Room( - id: roomId, - client: this, - ); + var room = + getRoomById(roomId) ?? await database.getSingleRoom(this, roomId); + if (room == null) { + await oneShotSync() + .timeout(timeoutForServerRequests) + .catchError((_) => null); + room = getRoomById(roomId) ?? + Room( + id: roomId, + client: this, + ); + } + final roomName = notification.roomName; final roomAlias = notification.roomAlias; if (roomName != null) { @@ -1840,9 +1847,7 @@ class Client extends MatrixApi { roomId: roomId, ); } - matrixEvent ??= await database - .getEventById(eventId, room) - .timeout(timeoutForServerRequests); + matrixEvent ??= await database.getEventById(eventId, room); try { matrixEvent ??= await getOneRoomEvent(roomId, eventId) @@ -1869,9 +1874,8 @@ class Client extends MatrixApi { if (room.fullyRead == matrixEvent.eventId) { return null; } - final readMarkerEvent = await database - .getEventById(room.fullyRead, room) - .timeout(timeoutForServerRequests); + final readMarkerEvent = await database.getEventById(room.fullyRead, room); + if (readMarkerEvent != null && readMarkerEvent.originServerTs.isAfter( matrixEvent.originServerTs @@ -1910,7 +1914,10 @@ class Client extends MatrixApi { var decrypted = await encryption.decryptRoomEvent(event); if (decrypted.messageType == MessageTypes.BadEncrypted && prevBatch != null) { - await oneShotSync(); + await oneShotSync() + .timeout(timeoutForServerRequests) + .catchError((_) => null); + decrypted = await encryption.decryptRoomEvent(event); } event = decrypted; diff --git a/test/client_test.dart b/test/client_test.dart index dfb28580..e38378a9 100644 --- a/test/client_test.dart +++ b/test/client_test.dart @@ -42,7 +42,7 @@ void main() { group('client path', () { late Client clientOnPath; - final dbPath = join(Directory.current.path, 'test.sqlite'); + final dbPath = join(Directory.current.path, 'client_path_test.sqlite'); setUp(() async { expect( From ffe1accdf68f05466f5100b63140faee9f0747ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Ku=C3=9Fowski?= Date: Thu, 21 Aug 2025 12:15:10 +0200 Subject: [PATCH 2/4] chore: Add documentation for commands --- doc/commands.md | 156 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 doc/commands.md diff --git a/doc/commands.md b/doc/commands.md new file mode 100644 index 00000000..8c8f6a27 --- /dev/null +++ b/doc/commands.md @@ -0,0 +1,156 @@ +## Available Chat Commands + +The Matrix Dart SDK supports out of the box chat commands. Just use `Room.sendTextEvent("/command");`. If you do not desire to get chat commands parsed, you can disable them like this: `Room.sendTextEvent("/command", parseCommands: false);` + +### Available Commands: + +## Available Chat Commands + +The Matrix Dart SDK supports out of the box chat commands. Just use `Room.sendTextEvent("/command");`. If you do not desire to get chat commands parsed, you can disable them like this: `Room.sendTextEvent("/command", parseCommands: false);` + +### Available Commands: + +```sh +/send +``` +Sends a plain message to the current room (commands are not parsed). + +```sh +/me +``` +Sends an emote message (e.g., `/me is happy` will display as "YourName is happy"). + +```sh +/dm [--no-encryption] +``` +Starts a direct chat with the given user. Optionally disables encryption. + +```sh +/create [] [--no-encryption] +``` +Creates a new group chat with the given name. Optionally disables encryption. + +```sh +/plain +``` +Sends a plain text message (no markdown, no commands) to the current room. + +```sh +/html +``` +Sends a message as raw HTML to the current room. + +```sh +/react +``` +Reacts to the message you are replying to with the given emoji. + +```sh +/join +``` +Joins the specified room. + +```sh +/leave +``` +Leaves the current room. + +```sh +/op [] +``` +Sets the power level of a user in the current room (default: 50). + +```sh +/kick +``` +Kicks a user from the current room. + +```sh +/ban +``` +Bans a user from the current room. + +```sh +/unban +``` +Unbans a user in the current room. + +```sh +/invite +``` +Invites a user to the current room. + +```sh +/myroomnick +``` +Sets your display name in the current room. + +```sh +/myroomavatar +``` +Sets your avatar in the current room. + +```sh +/discardsession +``` +Discards the outbound group session for the current room (forces new encryption session). + +```sh +/clearcache +``` +Clears the local cache. + +```sh +/markasdm +``` +Marks the current room as a direct chat with the given user. + +```sh +/markasgroup +``` +Removes the direct chat status from the current room. + +```sh +/hug +``` +Sends a "hug" event to the current room. + +```sh +/googly +``` +Sends a "googly eyes" event to the current room. + +```sh +/cuddle +``` +Sends a "cuddle" event to the current room. + +```sh +/sendRaw +``` +Sends a raw event (as JSON) to the current room. + +```sh +/ignore +``` +Ignores the given user (you will not see their messages). + +```sh +/unignore +``` +Stops ignoring the given user. + +```sh +/roomupgrade +``` +Upgrades the current room to a new version. + +```sh +/logout +``` +Logs out the current session. + +```sh +/logoutAll +``` +Logs out all sessions for the user. \ No newline at end of file From a6fc1a004d46a14cbbe3dfd086591f364073ce52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Ku=C3=9Fowski?= Date: Fri, 22 Aug 2025 13:32:49 +0200 Subject: [PATCH 3/4] refactor: Remove dynamic in cross signing code --- lib/encryption/cross_signing.dart | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/encryption/cross_signing.dart b/lib/encryption/cross_signing.dart index f725fff2..5f8b04c5 100644 --- a/lib/encryption/cross_signing.dart +++ b/lib/encryption/cross_signing.dart @@ -181,23 +181,24 @@ class CrossSigning { } if (signedKeys.isNotEmpty) { // post our new keys! - final payload = >>{}; + final payload = >>{}; for (final key in signedKeys) { - if (key.identifier == null || - key.signatures == null || - key.signatures?.isEmpty != false) { + final signatures = key.signatures; + final identifier = key.identifier; + if (identifier == null || signatures == null || signatures.isEmpty) { continue; } if (!payload.containsKey(key.userId)) { - payload[key.userId] = >{}; + payload[key.userId] = >{}; } if (payload[key.userId]?[key.identifier]?['signatures'] != null) { // we need to merge signature objects - payload[key.userId]![key.identifier]!['signatures'] - .addAll(key.signatures); + payload[key.userId]![key.identifier]! + .tryGetMap>('signatures')! + .addAll(signatures); } else { // we can just add signatures - payload[key.userId]![key.identifier!] = key.toJson(); + payload[key.userId]![identifier] = key.toJson(); } } From 7d0a4dc3bc13bc9b9421b0640f422b88941ddef8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Ku=C3=9Fowski?= Date: Fri, 22 Aug 2025 14:01:28 +0200 Subject: [PATCH 4/4] refactor: Make signableJson type safe with type safe class --- lib/encryption/olm_manager.dart | 50 ++++++++++++++++----------- lib/encryption/utils/olm_session.dart | 15 ++++---- 2 files changed, 38 insertions(+), 27 deletions(-) diff --git a/lib/encryption/olm_manager.dart b/lib/encryption/olm_manager.dart index 9b41d3c7..2176aee2 100644 --- a/lib/encryption/olm_manager.dart +++ b/lib/encryption/olm_manager.dart @@ -102,28 +102,17 @@ class OlmManager { /// Adds a signature to this json from this olm account and returns the signed /// json. - Map signJson(Map payload) { + Map signJson(Map payload) { if (!enabled) throw ('Encryption is disabled'); - final Map? unsigned = payload['unsigned']; - final Map? signatures = payload['signatures']; - payload.remove('unsigned'); - payload.remove('signatures'); - final canonical = canonicalJson.encode(payload); + final signableJson = SignableJsonMap(payload); + + final canonical = canonicalJson.encode(signableJson.jsonMap); final signature = _olmAccount!.sign(String.fromCharCodes(canonical)); - if (signatures != null) { - payload['signatures'] = signatures; - } else { - payload['signatures'] = {}; - } - if (!payload['signatures'].containsKey(client.userID)) { - payload['signatures'][client.userID] = {}; - } - payload['signatures'][client.userID]['ed25519:$ourDeviceId'] = - signature.toBase64(); - if (unsigned != null) { - payload['unsigned'] = unsigned; - } - return payload; + + final userSignatures = signableJson.signatures[client.userID!] ??= {}; + userSignatures['ed25519:$ourDeviceId'] = signature.toBase64(); + + return signableJson.toJson(); } String signString(String s) { @@ -811,3 +800,24 @@ class NoOlmSessionFoundException implements Exception { String toString() => 'No olm session found for ${device.userId}:${device.deviceId}'; } + +class SignableJsonMap { + final Map jsonMap; + final Map> signatures; + final Map? unsigned; + + SignableJsonMap(Map json) + : jsonMap = json, + signatures = + json.tryGetMap>('signatures') ?? {}, + unsigned = json.tryGetMap('unsigned') { + jsonMap.remove('signatures'); + jsonMap.remove('unsigned'); + } + + Map toJson() => { + ...jsonMap, + 'signatures': signatures, + if (unsigned != null) 'unsigned': unsigned, + }; +} diff --git a/lib/encryption/utils/olm_session.dart b/lib/encryption/utils/olm_session.dart index 6c5a385c..7be42ea2 100644 --- a/lib/encryption/utils/olm_session.dart +++ b/lib/encryption/utils/olm_session.dart @@ -41,24 +41,25 @@ class OlmSession { required this.lastReceived, }); - OlmSession.fromJson(Map dbEntry, this.key) - : identityKey = dbEntry['identity_key'] ?? '' { + OlmSession.fromJson(Map dbEntry, this.key) + : identityKey = dbEntry.tryGet('identity_key') ?? '' { try { try { session = vod.Session.fromPickleEncrypted( pickleKey: key.toPickleKey(), - pickle: dbEntry['pickle'], + pickle: dbEntry['pickle'] as String, ); } catch (_) { Logs().d('Unable to unpickle Olm session. Try LibOlm format.'); session = vod.Session.fromOlmPickleEncrypted( pickleKey: utf8.encode(key), - pickle: dbEntry['pickle'], + pickle: dbEntry['pickle'] as String, ); } - sessionId = dbEntry['session_id']; - lastReceived = - DateTime.fromMillisecondsSinceEpoch(dbEntry['last_received'] ?? 0); + sessionId = dbEntry['session_id'] as String; + lastReceived = DateTime.fromMillisecondsSinceEpoch( + dbEntry.tryGet('last_received') ?? 0, + ); assert(sessionId == session!.sessionId); } catch (e, s) { Logs().e('[Vodozemac] Could not unpickle olm session', e, s);