Merge branch 'main' into huluwa-dev/fix-avatar-error-on-windows
This commit is contained in:
commit
d325e58a2c
|
|
@ -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.
|
||||||
|
|
@ -181,23 +181,24 @@ class CrossSigning {
|
||||||
}
|
}
|
||||||
if (signedKeys.isNotEmpty) {
|
if (signedKeys.isNotEmpty) {
|
||||||
// post our new keys!
|
// 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) {
|
for (final key in signedKeys) {
|
||||||
if (key.identifier == null ||
|
final signatures = key.signatures;
|
||||||
key.signatures == null ||
|
final identifier = key.identifier;
|
||||||
key.signatures?.isEmpty != false) {
|
if (identifier == null || signatures == null || signatures.isEmpty) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!payload.containsKey(key.userId)) {
|
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) {
|
if (payload[key.userId]?[key.identifier]?['signatures'] != null) {
|
||||||
// we need to merge signature objects
|
// we need to merge signature objects
|
||||||
payload[key.userId]![key.identifier]!['signatures']
|
payload[key.userId]![key.identifier]!
|
||||||
.addAll(key.signatures);
|
.tryGetMap<String, Map<String, String>>('signatures')!
|
||||||
|
.addAll(signatures);
|
||||||
} else {
|
} else {
|
||||||
// we can just add signatures
|
// we can just add signatures
|
||||||
payload[key.userId]![key.identifier!] = key.toJson();
|
payload[key.userId]![identifier] = key.toJson();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -102,28 +102,17 @@ class OlmManager {
|
||||||
|
|
||||||
/// Adds a signature to this json from this olm account and returns the signed
|
/// Adds a signature to this json from this olm account and returns the signed
|
||||||
/// json.
|
/// json.
|
||||||
Map<String, dynamic> signJson(Map<String, dynamic> payload) {
|
Map<String, Object?> signJson(Map<String, Object?> payload) {
|
||||||
if (!enabled) throw ('Encryption is disabled');
|
if (!enabled) throw ('Encryption is disabled');
|
||||||
final Map<String, dynamic>? unsigned = payload['unsigned'];
|
final signableJson = SignableJsonMap(payload);
|
||||||
final Map<String, dynamic>? signatures = payload['signatures'];
|
|
||||||
payload.remove('unsigned');
|
final canonical = canonicalJson.encode(signableJson.jsonMap);
|
||||||
payload.remove('signatures');
|
|
||||||
final canonical = canonicalJson.encode(payload);
|
|
||||||
final signature = _olmAccount!.sign(String.fromCharCodes(canonical));
|
final signature = _olmAccount!.sign(String.fromCharCodes(canonical));
|
||||||
if (signatures != null) {
|
|
||||||
payload['signatures'] = signatures;
|
final userSignatures = signableJson.signatures[client.userID!] ??= {};
|
||||||
} else {
|
userSignatures['ed25519:$ourDeviceId'] = signature.toBase64();
|
||||||
payload['signatures'] = <String, dynamic>{};
|
|
||||||
}
|
return signableJson.toJson();
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String signString(String s) {
|
String signString(String s) {
|
||||||
|
|
@ -811,3 +800,24 @@ class NoOlmSessionFoundException implements Exception {
|
||||||
String toString() =>
|
String toString() =>
|
||||||
'No olm session found for ${device.userId}:${device.deviceId}';
|
'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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,24 +41,25 @@ class OlmSession {
|
||||||
required this.lastReceived,
|
required this.lastReceived,
|
||||||
});
|
});
|
||||||
|
|
||||||
OlmSession.fromJson(Map<String, dynamic> dbEntry, this.key)
|
OlmSession.fromJson(Map<String, Object?> dbEntry, this.key)
|
||||||
: identityKey = dbEntry['identity_key'] ?? '' {
|
: identityKey = dbEntry.tryGet<String>('identity_key') ?? '' {
|
||||||
try {
|
try {
|
||||||
try {
|
try {
|
||||||
session = vod.Session.fromPickleEncrypted(
|
session = vod.Session.fromPickleEncrypted(
|
||||||
pickleKey: key.toPickleKey(),
|
pickleKey: key.toPickleKey(),
|
||||||
pickle: dbEntry['pickle'],
|
pickle: dbEntry['pickle'] as String,
|
||||||
);
|
);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
Logs().d('Unable to unpickle Olm session. Try LibOlm format.');
|
Logs().d('Unable to unpickle Olm session. Try LibOlm format.');
|
||||||
session = vod.Session.fromOlmPickleEncrypted(
|
session = vod.Session.fromOlmPickleEncrypted(
|
||||||
pickleKey: utf8.encode(key),
|
pickleKey: utf8.encode(key),
|
||||||
pickle: dbEntry['pickle'],
|
pickle: dbEntry['pickle'] as String,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
sessionId = dbEntry['session_id'];
|
sessionId = dbEntry['session_id'] as String;
|
||||||
lastReceived =
|
lastReceived = DateTime.fromMillisecondsSinceEpoch(
|
||||||
DateTime.fromMillisecondsSinceEpoch(dbEntry['last_received'] ?? 0);
|
dbEntry.tryGet<int>('last_received') ?? 0,
|
||||||
|
);
|
||||||
assert(sessionId == session!.sessionId);
|
assert(sessionId == session!.sessionId);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logs().e('[Vodozemac] Could not unpickle olm session', e, s);
|
Logs().e('[Vodozemac] Could not unpickle olm session', e, s);
|
||||||
|
|
|
||||||
|
|
@ -1790,12 +1790,19 @@ class Client extends MatrixApi {
|
||||||
if (eventId == null || roomId == null) return null;
|
if (eventId == null || roomId == null) return null;
|
||||||
|
|
||||||
// Create the room object:
|
// Create the room object:
|
||||||
final room = getRoomById(roomId) ??
|
var room =
|
||||||
await database.getSingleRoom(this, roomId) ??
|
getRoomById(roomId) ?? await database.getSingleRoom(this, roomId);
|
||||||
Room(
|
if (room == null) {
|
||||||
id: roomId,
|
await oneShotSync()
|
||||||
client: this,
|
.timeout(timeoutForServerRequests)
|
||||||
);
|
.catchError((_) => null);
|
||||||
|
room = getRoomById(roomId) ??
|
||||||
|
Room(
|
||||||
|
id: roomId,
|
||||||
|
client: this,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final roomName = notification.roomName;
|
final roomName = notification.roomName;
|
||||||
final roomAlias = notification.roomAlias;
|
final roomAlias = notification.roomAlias;
|
||||||
if (roomName != null) {
|
if (roomName != null) {
|
||||||
|
|
@ -1840,9 +1847,7 @@ class Client extends MatrixApi {
|
||||||
roomId: roomId,
|
roomId: roomId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
matrixEvent ??= await database
|
matrixEvent ??= await database.getEventById(eventId, room);
|
||||||
.getEventById(eventId, room)
|
|
||||||
.timeout(timeoutForServerRequests);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
matrixEvent ??= await getOneRoomEvent(roomId, eventId)
|
matrixEvent ??= await getOneRoomEvent(roomId, eventId)
|
||||||
|
|
@ -1869,9 +1874,8 @@ class Client extends MatrixApi {
|
||||||
if (room.fullyRead == matrixEvent.eventId) {
|
if (room.fullyRead == matrixEvent.eventId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
final readMarkerEvent = await database
|
final readMarkerEvent = await database.getEventById(room.fullyRead, room);
|
||||||
.getEventById(room.fullyRead, room)
|
|
||||||
.timeout(timeoutForServerRequests);
|
|
||||||
if (readMarkerEvent != null &&
|
if (readMarkerEvent != null &&
|
||||||
readMarkerEvent.originServerTs.isAfter(
|
readMarkerEvent.originServerTs.isAfter(
|
||||||
matrixEvent.originServerTs
|
matrixEvent.originServerTs
|
||||||
|
|
@ -1910,7 +1914,10 @@ class Client extends MatrixApi {
|
||||||
var decrypted = await encryption.decryptRoomEvent(event);
|
var decrypted = await encryption.decryptRoomEvent(event);
|
||||||
if (decrypted.messageType == MessageTypes.BadEncrypted &&
|
if (decrypted.messageType == MessageTypes.BadEncrypted &&
|
||||||
prevBatch != null) {
|
prevBatch != null) {
|
||||||
await oneShotSync();
|
await oneShotSync()
|
||||||
|
.timeout(timeoutForServerRequests)
|
||||||
|
.catchError((_) => null);
|
||||||
|
|
||||||
decrypted = await encryption.decryptRoomEvent(event);
|
decrypted = await encryption.decryptRoomEvent(event);
|
||||||
}
|
}
|
||||||
event = decrypted;
|
event = decrypted;
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ void main() {
|
||||||
group('client path', () {
|
group('client path', () {
|
||||||
late Client clientOnPath;
|
late Client clientOnPath;
|
||||||
|
|
||||||
final dbPath = join(Directory.current.path, 'test.sqlite');
|
final dbPath = join(Directory.current.path, 'client_path_test.sqlite');
|
||||||
|
|
||||||
setUp(() async {
|
setUp(() async {
|
||||||
expect(
|
expect(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue