Merge branch 'main' into huluwa-dev/fix-avatar-error-on-windows

This commit is contained in:
huluwa 2025-09-03 19:52:22 +09:30 committed by GitHub
commit d325e58a2c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 224 additions and 49 deletions

156
doc/commands.md Normal file
View File

@ -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 <message>
```
Sends a plain message to the current room (commands are not parsed).
```sh
/me <action>
```
Sends an emote message (e.g., `/me is happy` will display as "YourName is happy").
```sh
/dm <mxid> [--no-encryption]
```
Starts a direct chat with the given user. Optionally disables encryption.
```sh
/create [<group name>] [--no-encryption]
```
Creates a new group chat with the given name. Optionally disables encryption.
```sh
/plain <message>
```
Sends a plain text message (no markdown, no commands) to the current room.
```sh
/html <html>
```
Sends a message as raw HTML to the current room.
```sh
/react <emoji>
```
Reacts to the message you are replying to with the given emoji.
```sh
/join <room>
```
Joins the specified room.
```sh
/leave
```
Leaves the current room.
```sh
/op <mxid> [<power level>]
```
Sets the power level of a user in the current room (default: 50).
```sh
/kick <mxid>
```
Kicks a user from the current room.
```sh
/ban <mxid>
```
Bans a user from the current room.
```sh
/unban <mxid>
```
Unbans a user in the current room.
```sh
/invite <mxid>
```
Invites a user to the current room.
```sh
/myroomnick <displayname>
```
Sets your display name in the current room.
```sh
/myroomavatar <mxc-url>
```
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 <mxid>
```
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 <json>
```
Sends a raw event (as JSON) to the current room.
```sh
/ignore <mxid>
```
Ignores the given user (you will not see their messages).
```sh
/unignore <mxid>
```
Stops ignoring the given user.
```sh
/roomupgrade <version>
```
Upgrades the current room to a new version.
```sh
/logout
```
Logs out the current session.
```sh
/logoutAll
```
Logs out all sessions for the user.

View File

@ -181,23 +181,24 @@ class CrossSigning {
}
if (signedKeys.isNotEmpty) {
// post our new keys!
final payload = <String, Map<String, Map<String, dynamic>>>{};
final payload = <String, Map<String, Map<String, Object?>>>{};
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] = <String, Map<String, dynamic>>{};
payload[key.userId] = <String, Map<String, Object?>>{};
}
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<String, Map<String, String>>('signatures')!
.addAll(signatures);
} else {
// we can just add signatures
payload[key.userId]![key.identifier!] = key.toJson();
payload[key.userId]![identifier] = key.toJson();
}
}

View File

@ -102,28 +102,17 @@ class OlmManager {
/// Adds a signature to this json from this olm account and returns the signed
/// json.
Map<String, dynamic> signJson(Map<String, dynamic> payload) {
Map<String, Object?> signJson(Map<String, Object?> payload) {
if (!enabled) throw ('Encryption is disabled');
final Map<String, dynamic>? unsigned = payload['unsigned'];
final Map<String, dynamic>? 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'] = <String, dynamic>{};
}
if (!payload['signatures'].containsKey(client.userID)) {
payload['signatures'][client.userID] = <String, dynamic>{};
}
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<String, Object?> jsonMap;
final Map<String, Map<String, String>> signatures;
final Map<String, Object?>? unsigned;
SignableJsonMap(Map<String, Object?> json)
: jsonMap = json,
signatures =
json.tryGetMap<String, Map<String, String>>('signatures') ?? {},
unsigned = json.tryGetMap<String, Object?>('unsigned') {
jsonMap.remove('signatures');
jsonMap.remove('unsigned');
}
Map<String, Object?> toJson() => {
...jsonMap,
'signatures': signatures,
if (unsigned != null) 'unsigned': unsigned,
};
}

View File

@ -41,24 +41,25 @@ class OlmSession {
required this.lastReceived,
});
OlmSession.fromJson(Map<String, dynamic> dbEntry, this.key)
: identityKey = dbEntry['identity_key'] ?? '' {
OlmSession.fromJson(Map<String, Object?> dbEntry, this.key)
: identityKey = dbEntry.tryGet<String>('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<int>('last_received') ?? 0,
);
assert(sessionId == session!.sessionId);
} catch (e, s) {
Logs().e('[Vodozemac] Could not unpickle olm session', e, s);

View File

@ -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) ??
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;

View File

@ -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(