Merge pull request #1952 from famedly/reza/require-trailing-comma
Add require trailing comma lint
This commit is contained in:
commit
52381cecfb
|
|
@ -36,6 +36,7 @@ linter:
|
||||||
prefer_single_quotes: true
|
prefer_single_quotes: true
|
||||||
sort_child_properties_last: true
|
sort_child_properties_last: true
|
||||||
sort_pub_dependencies: true
|
sort_pub_dependencies: true
|
||||||
|
require_trailing_commas: true
|
||||||
|
|
||||||
# Be nice to our users and allow them to configure what gets logged.
|
# Be nice to our users and allow them to configure what gets logged.
|
||||||
avoid_print: true
|
avoid_print: true
|
||||||
|
|
|
||||||
|
|
@ -72,11 +72,12 @@ class CrossSigning {
|
||||||
null;
|
null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> selfSign(
|
Future<void> selfSign({
|
||||||
{String? passphrase,
|
String? passphrase,
|
||||||
String? recoveryKey,
|
String? recoveryKey,
|
||||||
String? keyOrPassphrase,
|
String? keyOrPassphrase,
|
||||||
OpenSSSS? openSsss}) async {
|
OpenSSSS? openSsss,
|
||||||
|
}) async {
|
||||||
var handle = openSsss;
|
var handle = openSsss;
|
||||||
if (handle == null) {
|
if (handle == null) {
|
||||||
handle = encryption.ssss.open(EventTypes.CrossSigningMasterKey);
|
handle = encryption.ssss.open(EventTypes.CrossSigningMasterKey);
|
||||||
|
|
@ -89,7 +90,8 @@ class CrossSigning {
|
||||||
await handle.maybeCacheAll();
|
await handle.maybeCacheAll();
|
||||||
}
|
}
|
||||||
final masterPrivateKey = base64decodeUnpadded(
|
final masterPrivateKey = base64decodeUnpadded(
|
||||||
await handle.getStored(EventTypes.CrossSigningMasterKey));
|
await handle.getStored(EventTypes.CrossSigningMasterKey),
|
||||||
|
);
|
||||||
final keyObj = olm.PkSigning();
|
final keyObj = olm.PkSigning();
|
||||||
String? masterPubkey;
|
String? masterPubkey;
|
||||||
try {
|
try {
|
||||||
|
|
@ -117,11 +119,13 @@ class CrossSigning {
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool signable(List<SignableKey> keys) => keys.any((key) =>
|
bool signable(List<SignableKey> keys) => keys.any(
|
||||||
key is CrossSigningKey && key.usage.contains('master') ||
|
(key) =>
|
||||||
key is DeviceKeys &&
|
key is CrossSigningKey && key.usage.contains('master') ||
|
||||||
key.userId == client.userID &&
|
key is DeviceKeys &&
|
||||||
key.identifier != client.deviceID);
|
key.userId == client.userID &&
|
||||||
|
key.identifier != client.deviceID,
|
||||||
|
);
|
||||||
|
|
||||||
Future<void> sign(List<SignableKey> keys) async {
|
Future<void> sign(List<SignableKey> keys) async {
|
||||||
final signedKeys = <MatrixSignableKey>[];
|
final signedKeys = <MatrixSignableKey>[];
|
||||||
|
|
@ -133,7 +137,10 @@ class CrossSigning {
|
||||||
}
|
}
|
||||||
|
|
||||||
void addSignature(
|
void addSignature(
|
||||||
SignableKey key, SignableKey signedWith, String signature) {
|
SignableKey key,
|
||||||
|
SignableKey signedWith,
|
||||||
|
String signature,
|
||||||
|
) {
|
||||||
final signedKey = key.cloneForSigning();
|
final signedKey = key.cloneForSigning();
|
||||||
((signedKey.signatures ??=
|
((signedKey.signatures ??=
|
||||||
<String, Map<String, String>>{})[signedWith.userId] ??=
|
<String, Map<String, String>>{})[signedWith.userId] ??=
|
||||||
|
|
@ -154,9 +161,11 @@ class CrossSigning {
|
||||||
// we don't care about signing other cross-signing keys
|
// we don't care about signing other cross-signing keys
|
||||||
} else {
|
} else {
|
||||||
// okay, we'll sign a device key with our self signing key
|
// okay, we'll sign a device key with our self signing key
|
||||||
selfSigningKey ??= base64decodeUnpadded(await encryption.ssss
|
selfSigningKey ??= base64decodeUnpadded(
|
||||||
.getCached(EventTypes.CrossSigningSelfSigning) ??
|
await encryption.ssss
|
||||||
'');
|
.getCached(EventTypes.CrossSigningSelfSigning) ??
|
||||||
|
'',
|
||||||
|
);
|
||||||
if (selfSigningKey.isNotEmpty) {
|
if (selfSigningKey.isNotEmpty) {
|
||||||
final signature = _sign(key.signingContent, selfSigningKey);
|
final signature = _sign(key.signingContent, selfSigningKey);
|
||||||
addSignature(key, userKeys.selfSigningKey!, signature);
|
addSignature(key, userKeys.selfSigningKey!, signature);
|
||||||
|
|
@ -164,9 +173,10 @@ class CrossSigning {
|
||||||
}
|
}
|
||||||
} else if (key is CrossSigningKey && key.usage.contains('master')) {
|
} else if (key is CrossSigningKey && key.usage.contains('master')) {
|
||||||
// we are signing someone elses master key
|
// we are signing someone elses master key
|
||||||
userSigningKey ??= base64decodeUnpadded(await encryption.ssss
|
userSigningKey ??= base64decodeUnpadded(
|
||||||
.getCached(EventTypes.CrossSigningUserSigning) ??
|
await encryption.ssss.getCached(EventTypes.CrossSigningUserSigning) ??
|
||||||
'');
|
'',
|
||||||
|
);
|
||||||
if (userSigningKey.isNotEmpty) {
|
if (userSigningKey.isNotEmpty) {
|
||||||
final signature = _sign(key.signingContent, userSigningKey);
|
final signature = _sign(key.signingContent, userSigningKey);
|
||||||
addSignature(key, userKeys.userSigningKey!, signature);
|
addSignature(key, userKeys.userSigningKey!, signature);
|
||||||
|
|
|
||||||
|
|
@ -105,9 +105,15 @@ class Encryption {
|
||||||
);
|
);
|
||||||
|
|
||||||
void handleDeviceOneTimeKeysCount(
|
void handleDeviceOneTimeKeysCount(
|
||||||
Map<String, int>? countJson, List<String>? unusedFallbackKeyTypes) {
|
Map<String, int>? countJson,
|
||||||
runInRoot(() async => olmManager.handleDeviceOneTimeKeysCount(
|
List<String>? unusedFallbackKeyTypes,
|
||||||
countJson, unusedFallbackKeyTypes));
|
) {
|
||||||
|
runInRoot(
|
||||||
|
() async => olmManager.handleDeviceOneTimeKeysCount(
|
||||||
|
countJson,
|
||||||
|
unusedFallbackKeyTypes,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void onSync() {
|
void onSync() {
|
||||||
|
|
@ -173,9 +179,10 @@ class Encryption {
|
||||||
return await olmManager.decryptToDeviceEvent(event);
|
return await olmManager.decryptToDeviceEvent(event);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logs().w(
|
Logs().w(
|
||||||
'[LibOlm] Could not decrypt to device event from ${event.sender} with content: ${event.content}',
|
'[LibOlm] Could not decrypt to device event from ${event.sender} with content: ${event.content}',
|
||||||
e,
|
e,
|
||||||
s);
|
s,
|
||||||
|
);
|
||||||
client.onEncryptionError.add(
|
client.onEncryptionError.add(
|
||||||
SdkError(
|
SdkError(
|
||||||
exception: e is Exception ? e : Exception(e),
|
exception: e is Exception ? e : Exception(e),
|
||||||
|
|
@ -241,7 +248,10 @@ class Encryption {
|
||||||
client.database
|
client.database
|
||||||
// ignore: discarded_futures
|
// ignore: discarded_futures
|
||||||
?.updateInboundGroupSessionIndexes(
|
?.updateInboundGroupSessionIndexes(
|
||||||
json.encode(inboundGroupSession.indexes), roomId, sessionId)
|
json.encode(inboundGroupSession.indexes),
|
||||||
|
roomId,
|
||||||
|
sessionId,
|
||||||
|
)
|
||||||
// ignore: discarded_futures
|
// ignore: discarded_futures
|
||||||
.onError((e, _) => Logs().e('Ignoring error for updating indexes'));
|
.onError((e, _) => Logs().e('Ignoring error for updating indexes'));
|
||||||
}
|
}
|
||||||
|
|
@ -255,8 +265,10 @@ class Encryption {
|
||||||
?.session_id() ??
|
?.session_id() ??
|
||||||
'') ==
|
'') ==
|
||||||
content.sessionId) {
|
content.sessionId) {
|
||||||
runInRoot(() async =>
|
runInRoot(
|
||||||
keyManager.clearOrUseOutboundGroupSession(roomId, wipe: true));
|
() async =>
|
||||||
|
keyManager.clearOrUseOutboundGroupSession(roomId, wipe: true),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (canRequestSession) {
|
if (canRequestSession) {
|
||||||
decryptedPayload = {
|
decryptedPayload = {
|
||||||
|
|
@ -295,9 +307,12 @@ class Encryption {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Event> decryptRoomEvent(String roomId, Event event,
|
Future<Event> decryptRoomEvent(
|
||||||
{bool store = false,
|
String roomId,
|
||||||
EventUpdateType updateType = EventUpdateType.timeline}) async {
|
Event event, {
|
||||||
|
bool store = false,
|
||||||
|
EventUpdateType updateType = EventUpdateType.timeline,
|
||||||
|
}) async {
|
||||||
if (event.type != EventTypes.Encrypted || event.redacted) {
|
if (event.type != EventTypes.Encrypted || event.redacted) {
|
||||||
return event;
|
return event;
|
||||||
}
|
}
|
||||||
|
|
@ -351,8 +366,10 @@ class Encryption {
|
||||||
/// Encrypts the given json payload and creates a send-ready m.room.encrypted
|
/// Encrypts the given json payload and creates a send-ready m.room.encrypted
|
||||||
/// payload. This will create a new outgoingGroupSession if necessary.
|
/// payload. This will create a new outgoingGroupSession if necessary.
|
||||||
Future<Map<String, dynamic>> encryptGroupMessagePayload(
|
Future<Map<String, dynamic>> encryptGroupMessagePayload(
|
||||||
String roomId, Map<String, dynamic> payload,
|
String roomId,
|
||||||
{String type = EventTypes.Message}) async {
|
Map<String, dynamic> payload, {
|
||||||
|
String type = EventTypes.Message,
|
||||||
|
}) async {
|
||||||
payload = copyMap(payload);
|
payload = copyMap(payload);
|
||||||
final Map<String, dynamic>? mRelatesTo = payload.remove('m.relates_to');
|
final Map<String, dynamic>? mRelatesTo = payload.remove('m.relates_to');
|
||||||
|
|
||||||
|
|
@ -403,9 +420,10 @@ class Encryption {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, Map<String, Map<String, dynamic>>>> encryptToDeviceMessage(
|
Future<Map<String, Map<String, Map<String, dynamic>>>> encryptToDeviceMessage(
|
||||||
List<DeviceKeys> deviceKeys,
|
List<DeviceKeys> deviceKeys,
|
||||||
String type,
|
String type,
|
||||||
Map<String, dynamic> payload) async {
|
Map<String, dynamic> payload,
|
||||||
|
) async {
|
||||||
return await olmManager.encryptToDeviceMessage(deviceKeys, type, payload);
|
return await olmManager.encryptToDeviceMessage(deviceKeys, type, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -252,19 +252,23 @@ class KeyManager {
|
||||||
// do e2ee recovery
|
// do e2ee recovery
|
||||||
_requestedSessionIds.add(requestIdent);
|
_requestedSessionIds.add(requestIdent);
|
||||||
|
|
||||||
runInRoot(() async => request(
|
runInRoot(
|
||||||
room,
|
() async => request(
|
||||||
sessionId,
|
room,
|
||||||
senderKey,
|
sessionId,
|
||||||
tryOnlineBackup: tryOnlineBackup,
|
senderKey,
|
||||||
onlineKeyBackupOnly: onlineKeyBackupOnly,
|
tryOnlineBackup: tryOnlineBackup,
|
||||||
));
|
onlineKeyBackupOnly: onlineKeyBackupOnly,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Loads an inbound group session
|
/// Loads an inbound group session
|
||||||
Future<SessionKey?> loadInboundGroupSession(
|
Future<SessionKey?> loadInboundGroupSession(
|
||||||
String roomId, String sessionId) async {
|
String roomId,
|
||||||
|
String sessionId,
|
||||||
|
) async {
|
||||||
final sess = _inboundGroupSessions[roomId]?[sessionId];
|
final sess = _inboundGroupSessions[roomId]?[sessionId];
|
||||||
if (sess != null) {
|
if (sess != null) {
|
||||||
if (sess.sessionId != sessionId && sess.sessionId.isNotEmpty) {
|
if (sess.sessionId != sessionId && sess.sessionId.isNotEmpty) {
|
||||||
|
|
@ -290,7 +294,8 @@ class KeyManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, Map<String, bool>> _getDeviceKeyIdMap(
|
Map<String, Map<String, bool>> _getDeviceKeyIdMap(
|
||||||
List<DeviceKeys> deviceKeys) {
|
List<DeviceKeys> deviceKeys,
|
||||||
|
) {
|
||||||
final deviceKeyIds = <String, Map<String, bool>>{};
|
final deviceKeyIds = <String, Map<String, bool>>{};
|
||||||
for (final device in deviceKeys) {
|
for (final device in deviceKeys) {
|
||||||
final deviceId = device.deviceId;
|
final deviceId = device.deviceId;
|
||||||
|
|
@ -312,8 +317,11 @@ class KeyManager {
|
||||||
/// Clears the existing outboundGroupSession but first checks if the participating
|
/// Clears the existing outboundGroupSession but first checks if the participating
|
||||||
/// devices have been changed. Returns false if the session has not been cleared because
|
/// devices have been changed. Returns false if the session has not been cleared because
|
||||||
/// it wasn't necessary. Otherwise returns true.
|
/// it wasn't necessary. Otherwise returns true.
|
||||||
Future<bool> clearOrUseOutboundGroupSession(String roomId,
|
Future<bool> clearOrUseOutboundGroupSession(
|
||||||
{bool wipe = false, bool use = true}) async {
|
String roomId, {
|
||||||
|
bool wipe = false,
|
||||||
|
bool use = true,
|
||||||
|
}) async {
|
||||||
final room = client.getRoomById(roomId);
|
final room = client.getRoomById(roomId);
|
||||||
final sess = getOutboundGroupSession(roomId);
|
final sess = getOutboundGroupSession(roomId);
|
||||||
if (room == null || sess == null || sess.outboundGroupSession == null) {
|
if (room == null || sess == null || sess.outboundGroupSession == null) {
|
||||||
|
|
@ -336,7 +344,9 @@ class KeyManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
final inboundSess = await loadInboundGroupSession(
|
final inboundSess = await loadInboundGroupSession(
|
||||||
room.id, sess.outboundGroupSession!.session_id());
|
room.id,
|
||||||
|
sess.outboundGroupSession!.session_id(),
|
||||||
|
);
|
||||||
if (inboundSess == null) {
|
if (inboundSess == null) {
|
||||||
wipe = true;
|
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
|
// 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) {
|
for (final userId in oldUserIds) {
|
||||||
final oldBlockedDevices = sess.devices.containsKey(userId)
|
final oldBlockedDevices = sess.devices.containsKey(userId)
|
||||||
? Set.from(sess.devices[userId]!.entries
|
? Set.from(
|
||||||
.where((e) => e.value)
|
sess.devices[userId]!.entries
|
||||||
.map((e) => e.key))
|
.where((e) => e.value)
|
||||||
|
.map((e) => e.key),
|
||||||
|
)
|
||||||
: <String>{};
|
: <String>{};
|
||||||
final newBlockedDevices = newDeviceKeyIds.containsKey(userId)
|
final newBlockedDevices = newDeviceKeyIds.containsKey(userId)
|
||||||
? Set.from(newDeviceKeyIds[userId]!
|
? Set.from(
|
||||||
.entries
|
newDeviceKeyIds[userId]!
|
||||||
.where((e) => e.value)
|
.entries
|
||||||
.map((e) => e.key))
|
.where((e) => e.value)
|
||||||
|
.map((e) => e.key),
|
||||||
|
)
|
||||||
: <String>{};
|
: <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
|
// 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
|
// check if new devices got blocked
|
||||||
|
|
@ -383,15 +397,19 @@ class KeyManager {
|
||||||
}
|
}
|
||||||
// and now add all the new devices!
|
// and now add all the new devices!
|
||||||
final oldDeviceIds = sess.devices.containsKey(userId)
|
final oldDeviceIds = sess.devices.containsKey(userId)
|
||||||
? Set.from(sess.devices[userId]!.entries
|
? Set.from(
|
||||||
.where((e) => !e.value)
|
sess.devices[userId]!.entries
|
||||||
.map((e) => e.key))
|
.where((e) => !e.value)
|
||||||
|
.map((e) => e.key),
|
||||||
|
)
|
||||||
: <String>{};
|
: <String>{};
|
||||||
final newDeviceIds = newDeviceKeyIds.containsKey(userId)
|
final newDeviceIds = newDeviceKeyIds.containsKey(userId)
|
||||||
? Set.from(newDeviceKeyIds[userId]!
|
? Set.from(
|
||||||
.entries
|
newDeviceKeyIds[userId]!
|
||||||
.where((e) => !e.value)
|
.entries
|
||||||
.map((e) => e.key))
|
.where((e) => !e.value)
|
||||||
|
.map((e) => e.key),
|
||||||
|
)
|
||||||
: <String>{};
|
: <String>{};
|
||||||
|
|
||||||
// check if a device got removed
|
// check if a device got removed
|
||||||
|
|
@ -403,8 +421,11 @@ class KeyManager {
|
||||||
// check if any new devices need keys
|
// check if any new devices need keys
|
||||||
final newDevices = newDeviceIds.difference(oldDeviceIds);
|
final newDevices = newDeviceIds.difference(oldDeviceIds);
|
||||||
if (newDeviceIds.isNotEmpty) {
|
if (newDeviceIds.isNotEmpty) {
|
||||||
devicesToReceive.addAll(newDeviceKeys.where(
|
devicesToReceive.addAll(
|
||||||
(d) => d.userId == userId && newDevices.contains(d.deviceId)));
|
newDeviceKeys.where(
|
||||||
|
(d) => d.userId == userId && newDevices.contains(d.deviceId),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -438,18 +459,23 @@ class KeyManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await client.database?.updateInboundGroupSessionAllowedAtIndex(
|
await client.database?.updateInboundGroupSessionAllowedAtIndex(
|
||||||
json.encode(inboundSess!.allowedAtIndex),
|
json.encode(inboundSess!.allowedAtIndex),
|
||||||
room.id,
|
room.id,
|
||||||
sess.outboundGroupSession!.session_id());
|
sess.outboundGroupSession!.session_id(),
|
||||||
|
);
|
||||||
// send out the key
|
// send out the key
|
||||||
await client.sendToDeviceEncryptedChunked(
|
await client.sendToDeviceEncryptedChunked(
|
||||||
devicesToReceive, EventTypes.RoomKey, rawSession);
|
devicesToReceive,
|
||||||
|
EventTypes.RoomKey,
|
||||||
|
rawSession,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logs().e(
|
Logs().e(
|
||||||
'[LibOlm] Unable to re-send the session key at later index to new devices',
|
'[LibOlm] Unable to re-send the session key at later index to new devices',
|
||||||
e,
|
e,
|
||||||
s);
|
s,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -462,14 +488,17 @@ class KeyManager {
|
||||||
|
|
||||||
/// Store an outbound group session in the database
|
/// Store an outbound group session in the database
|
||||||
Future<void> storeOutboundGroupSession(
|
Future<void> storeOutboundGroupSession(
|
||||||
String roomId, OutboundGroupSession sess) async {
|
String roomId,
|
||||||
|
OutboundGroupSession sess,
|
||||||
|
) async {
|
||||||
final userID = client.userID;
|
final userID = client.userID;
|
||||||
if (userID == null) return;
|
if (userID == null) return;
|
||||||
await client.database?.storeOutboundGroupSession(
|
await client.database?.storeOutboundGroupSession(
|
||||||
roomId,
|
roomId,
|
||||||
sess.outboundGroupSession!.pickle(userID),
|
sess.outboundGroupSession!.pickle(userID),
|
||||||
json.encode(sess.devices),
|
json.encode(sess.devices),
|
||||||
sess.creationTime.millisecondsSinceEpoch);
|
sess.creationTime.millisecondsSinceEpoch,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final Map<String, Future<OutboundGroupSession>>
|
final Map<String, Future<OutboundGroupSession>>
|
||||||
|
|
@ -507,18 +536,21 @@ class KeyManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<OutboundGroupSession> _createOutboundGroupSession(
|
Future<OutboundGroupSession> _createOutboundGroupSession(
|
||||||
String roomId) async {
|
String roomId,
|
||||||
|
) async {
|
||||||
await clearOrUseOutboundGroupSession(roomId, wipe: true);
|
await clearOrUseOutboundGroupSession(roomId, wipe: true);
|
||||||
await client.firstSyncReceived;
|
await client.firstSyncReceived;
|
||||||
final room = client.getRoomById(roomId);
|
final room = client.getRoomById(roomId);
|
||||||
if (room == null) {
|
if (room == null) {
|
||||||
throw Exception(
|
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;
|
final userID = client.userID;
|
||||||
if (userID == null) {
|
if (userID == null) {
|
||||||
throw Exception(
|
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();
|
final deviceKeys = await room.getUserDeviceKeys();
|
||||||
|
|
@ -549,8 +581,12 @@ class KeyManager {
|
||||||
outboundGroupSession.message_index();
|
outboundGroupSession.message_index();
|
||||||
}
|
}
|
||||||
await setInboundGroupSession(
|
await setInboundGroupSession(
|
||||||
roomId, rawSession['session_id'], encryption.identityKey!, rawSession,
|
roomId,
|
||||||
allowedAtIndex: allowedAtIndex);
|
rawSession['session_id'],
|
||||||
|
encryption.identityKey!,
|
||||||
|
rawSession,
|
||||||
|
allowedAtIndex: allowedAtIndex,
|
||||||
|
);
|
||||||
final sess = OutboundGroupSession(
|
final sess = OutboundGroupSession(
|
||||||
devices: deviceKeyIds,
|
devices: deviceKeyIds,
|
||||||
creationTime: DateTime.now(),
|
creationTime: DateTime.now(),
|
||||||
|
|
@ -559,14 +595,18 @@ class KeyManager {
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
await client.sendToDeviceEncryptedChunked(
|
await client.sendToDeviceEncryptedChunked(
|
||||||
deviceKeys, EventTypes.RoomKey, rawSession);
|
deviceKeys,
|
||||||
|
EventTypes.RoomKey,
|
||||||
|
rawSession,
|
||||||
|
);
|
||||||
await storeOutboundGroupSession(roomId, sess);
|
await storeOutboundGroupSession(roomId, sess);
|
||||||
_outboundGroupSessions[roomId] = sess;
|
_outboundGroupSessions[roomId] = sess;
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logs().e(
|
Logs().e(
|
||||||
'[LibOlm] Unable to send the session key to the participating devices',
|
'[LibOlm] Unable to send the session key to the participating devices',
|
||||||
e,
|
e,
|
||||||
s);
|
s,
|
||||||
|
);
|
||||||
sess.dispose();
|
sess.dispose();
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
|
|
@ -611,8 +651,9 @@ class KeyManager {
|
||||||
GetRoomKeysVersionCurrentResponse? _roomKeysVersionCache;
|
GetRoomKeysVersionCurrentResponse? _roomKeysVersionCache;
|
||||||
DateTime? _roomKeysVersionCacheDate;
|
DateTime? _roomKeysVersionCacheDate;
|
||||||
|
|
||||||
Future<GetRoomKeysVersionCurrentResponse> getRoomKeysBackupInfo(
|
Future<GetRoomKeysVersionCurrentResponse> getRoomKeysBackupInfo([
|
||||||
[bool useCache = true]) async {
|
bool useCache = true,
|
||||||
|
]) async {
|
||||||
if (_roomKeysVersionCache != null &&
|
if (_roomKeysVersionCache != null &&
|
||||||
_roomKeysVersionCacheDate != null &&
|
_roomKeysVersionCacheDate != null &&
|
||||||
useCache &&
|
useCache &&
|
||||||
|
|
@ -650,10 +691,13 @@ class KeyManager {
|
||||||
final sessionData = session.sessionData;
|
final sessionData = session.sessionData;
|
||||||
Map<String, Object?>? decrypted;
|
Map<String, Object?>? decrypted;
|
||||||
try {
|
try {
|
||||||
decrypted = json.decode(decryption.decrypt(
|
decrypted = json.decode(
|
||||||
|
decryption.decrypt(
|
||||||
sessionData['ephemeral'] as String,
|
sessionData['ephemeral'] as String,
|
||||||
sessionData['mac'] as String,
|
sessionData['mac'] as String,
|
||||||
sessionData['ciphertext'] 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);
|
||||||
}
|
}
|
||||||
|
|
@ -662,12 +706,16 @@ class KeyManager {
|
||||||
decrypted['session_id'] = sessionId;
|
decrypted['session_id'] = sessionId;
|
||||||
decrypted['room_id'] = roomId;
|
decrypted['room_id'] = roomId;
|
||||||
await setInboundGroupSession(
|
await setInboundGroupSession(
|
||||||
roomId, sessionId, senderKey, decrypted,
|
roomId,
|
||||||
forwarded: true,
|
sessionId,
|
||||||
senderClaimedKeys: decrypted
|
senderKey,
|
||||||
.tryGetMap<String, String>('sender_claimed_keys') ??
|
decrypted,
|
||||||
<String, String>{},
|
forwarded: true,
|
||||||
uploaded: true);
|
senderClaimedKeys:
|
||||||
|
decrypted.tryGetMap<String, String>('sender_claimed_keys') ??
|
||||||
|
<String, String>{},
|
||||||
|
uploaded: true,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -733,10 +781,14 @@ class KeyManager {
|
||||||
} catch (err, stacktrace) {
|
} catch (err, stacktrace) {
|
||||||
if (err is MatrixException && err.errcode == 'M_NOT_FOUND') {
|
if (err is MatrixException && err.errcode == 'M_NOT_FOUND') {
|
||||||
Logs().i(
|
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 {
|
} else {
|
||||||
Logs().e('[KeyManager] Failed to access online key backup', err,
|
Logs().e(
|
||||||
stacktrace);
|
'[KeyManager] Failed to access online key backup',
|
||||||
|
err,
|
||||||
|
stacktrace,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// TODO: also don't request from others if we have an index of 0 now
|
// TODO: also don't request from others if we have an index of 0 now
|
||||||
|
|
@ -785,15 +837,17 @@ class KeyManager {
|
||||||
|
|
||||||
void startAutoUploadKeys() {
|
void startAutoUploadKeys() {
|
||||||
_uploadKeysOnSync = encryption.client.onSync.stream.listen(
|
_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
|
/// 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
|
/// 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`
|
/// skipped when an upload task is already in progress. Set `skipIfInProgress`
|
||||||
/// to `false` to await the pending upload task instead.
|
/// to `false` to await the pending upload task instead.
|
||||||
Future<void> uploadInboundGroupSessions(
|
Future<void> uploadInboundGroupSessions({
|
||||||
{bool skipIfInProgress = false}) async {
|
bool skipIfInProgress = false,
|
||||||
|
}) async {
|
||||||
final database = client.database;
|
final database = client.database;
|
||||||
final userID = client.userID;
|
final userID = client.userID;
|
||||||
if (database == null || userID == null) {
|
if (database == null || userID == null) {
|
||||||
|
|
@ -849,10 +903,12 @@ class KeyManager {
|
||||||
for (final dbSession in dbSessions) {
|
for (final dbSession in dbSessions) {
|
||||||
final device =
|
final device =
|
||||||
client.getUserDeviceKeysByCurve25519Key(dbSession.senderKey);
|
client.getUserDeviceKeysByCurve25519Key(dbSession.senderKey);
|
||||||
args.dbSessions.add(DbInboundGroupSessionBundle(
|
args.dbSessions.add(
|
||||||
dbSession: dbSession,
|
DbInboundGroupSessionBundle(
|
||||||
verified: device?.verified ?? false,
|
dbSession: dbSession,
|
||||||
));
|
verified: device?.verified ?? false,
|
||||||
|
),
|
||||||
|
);
|
||||||
i++;
|
i++;
|
||||||
if (i > 10) {
|
if (i > 10) {
|
||||||
await Future.delayed(Duration(milliseconds: 1));
|
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
|
// 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) {
|
for (final dbSession in dbSessions) {
|
||||||
await database.markInboundGroupSessionAsUploaded(
|
await database.markInboundGroupSessionAsUploaded(
|
||||||
dbSession.roomId, dbSession.sessionId);
|
dbSession.roomId,
|
||||||
|
dbSession.sessionId,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
decryption.free();
|
decryption.free();
|
||||||
|
|
@ -895,7 +953,8 @@ class KeyManager {
|
||||||
if (event.content['action'] == 'request') {
|
if (event.content['action'] == 'request') {
|
||||||
// we are *receiving* a request
|
// we are *receiving* a request
|
||||||
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().w('[KeyManager] No body, doing nothing');
|
Logs().w('[KeyManager] No body, doing nothing');
|
||||||
return; // no body
|
return; // no body
|
||||||
|
|
@ -908,7 +967,8 @@ class KeyManager {
|
||||||
final roomId = body.tryGet<String>('room_id');
|
final roomId = body.tryGet<String>('room_id');
|
||||||
if (roomId == null) {
|
if (roomId == null) {
|
||||||
Logs().w(
|
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
|
return; // wrong type for roomId or no roomId found
|
||||||
}
|
}
|
||||||
final device = client.userDeviceKeys[event.sender]
|
final device = client.userDeviceKeys[event.sender]
|
||||||
|
|
@ -930,7 +990,8 @@ class KeyManager {
|
||||||
final sessionId = body.tryGet<String>('session_id');
|
final sessionId = body.tryGet<String>('session_id');
|
||||||
if (sessionId == null) {
|
if (sessionId == null) {
|
||||||
Logs().w(
|
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
|
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
|
||||||
|
|
@ -941,7 +1002,8 @@ class KeyManager {
|
||||||
}
|
}
|
||||||
if (event.content['request_id'] is! String) {
|
if (event.content['request_id'] is! String) {
|
||||||
Logs().w(
|
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
|
return; // wrong type for request_id
|
||||||
}
|
}
|
||||||
final request = KeyManagerKeyShareRequest(
|
final request = KeyManagerKeyShareRequest(
|
||||||
|
|
@ -974,7 +1036,8 @@ class KeyManager {
|
||||||
final index =
|
final index =
|
||||||
session.allowedAtIndex[device.userId]![device.curve25519Key]!;
|
session.allowedAtIndex[device.userId]![device.curve25519Key]!;
|
||||||
Logs().i(
|
Logs().i(
|
||||||
'[KeyManager] Valid foreign request, forwarding key at index $index...');
|
'[KeyManager] Valid foreign request, forwarding key at index $index...',
|
||||||
|
);
|
||||||
await roomKeyRequest.forwardKey(index);
|
await roomKeyRequest.forwardKey(index);
|
||||||
} else {
|
} else {
|
||||||
Logs()
|
Logs()
|
||||||
|
|
@ -1002,15 +1065,19 @@ class KeyManager {
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final request = outgoingShareRequests.values.firstWhereOrNull((r) =>
|
final request = outgoingShareRequests.values.firstWhereOrNull(
|
||||||
r.room.id == event.content['room_id'] &&
|
(r) =>
|
||||||
r.sessionId == event.content['session_id']);
|
r.room.id == event.content['room_id'] &&
|
||||||
|
r.sessionId == event.content['session_id'],
|
||||||
|
);
|
||||||
if (request == null || request.canceled) {
|
if (request == null || request.canceled) {
|
||||||
return; // no associated request found or it got canceled
|
return; // no associated request found or it got canceled
|
||||||
}
|
}
|
||||||
final device = request.devices.firstWhereOrNull((d) =>
|
final device = request.devices.firstWhereOrNull(
|
||||||
d.userId == event.sender &&
|
(d) =>
|
||||||
d.curve25519Key == encryptedContent['sender_key']);
|
d.userId == event.sender &&
|
||||||
|
d.curve25519Key == encryptedContent['sender_key'],
|
||||||
|
);
|
||||||
if (device == null) {
|
if (device == null) {
|
||||||
return; // someone we didn't send our request to replied....better ignore this
|
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
|
// 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(
|
||||||
device.curve25519Key!, event.content,
|
request.room.id,
|
||||||
forwarded: true,
|
request.sessionId,
|
||||||
senderClaimedKeys: {
|
device.curve25519Key!,
|
||||||
'ed25519': event.content['sender_claimed_ed25519_key'] as String,
|
event.content,
|
||||||
});
|
forwarded: true,
|
||||||
|
senderClaimedKeys: {
|
||||||
|
'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,
|
||||||
|
);
|
||||||
outgoingShareRequests.remove(request.requestId);
|
outgoingShareRequests.remove(request.requestId);
|
||||||
// send cancel to all other devices
|
// send cancel to all other devices
|
||||||
if (request.devices.isEmpty) {
|
if (request.devices.isEmpty) {
|
||||||
|
|
@ -1057,7 +1129,8 @@ class KeyManager {
|
||||||
);
|
);
|
||||||
} else if (event.type == EventTypes.RoomKey) {
|
} else if (event.type == EventTypes.RoomKey) {
|
||||||
Logs().v(
|
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;
|
final encryptedContent = event.encryptedContent;
|
||||||
if (encryptedContent == null) {
|
if (encryptedContent == null) {
|
||||||
Logs().v('[KeyManager] not encrypted, ignoring...');
|
Logs().v('[KeyManager] not encrypted, ignoring...');
|
||||||
|
|
@ -1067,7 +1140,8 @@ class KeyManager {
|
||||||
final sessionId = event.content.tryGet<String>('session_id');
|
final sessionId = event.content.tryGet<String>('session_id');
|
||||||
if (roomId == null || sessionId == null) {
|
if (roomId == null || sessionId == null) {
|
||||||
Logs().w(
|
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;
|
return;
|
||||||
}
|
}
|
||||||
final sender_ed25519 = client.userDeviceKeys[event.sender]
|
final sender_ed25519 = client.userDeviceKeys[event.sender]
|
||||||
|
|
@ -1077,8 +1151,12 @@ class KeyManager {
|
||||||
}
|
}
|
||||||
Logs().v('[KeyManager] Keeping room key');
|
Logs().v('[KeyManager] Keeping room key');
|
||||||
await setInboundGroupSession(
|
await setInboundGroupSession(
|
||||||
roomId, sessionId, encryptedContent['sender_key'], event.content,
|
roomId,
|
||||||
forwarded: false);
|
sessionId,
|
||||||
|
encryptedContent['sender_key'],
|
||||||
|
event.content,
|
||||||
|
forwarded: false,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1105,13 +1183,13 @@ class KeyManagerKeyShareRequest {
|
||||||
final String sessionId;
|
final String sessionId;
|
||||||
bool canceled;
|
bool canceled;
|
||||||
|
|
||||||
KeyManagerKeyShareRequest(
|
KeyManagerKeyShareRequest({
|
||||||
{required this.requestId,
|
required this.requestId,
|
||||||
List<DeviceKeys>? devices,
|
List<DeviceKeys>? devices,
|
||||||
required this.room,
|
required this.room,
|
||||||
required this.sessionId,
|
required this.sessionId,
|
||||||
this.canceled = false})
|
this.canceled = false,
|
||||||
: devices = devices ?? [];
|
}) : devices = devices ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
class RoomKeyRequest extends ToDeviceEvent {
|
class RoomKeyRequest extends ToDeviceEvent {
|
||||||
|
|
@ -1119,11 +1197,14 @@ class RoomKeyRequest extends ToDeviceEvent {
|
||||||
KeyManagerKeyShareRequest request;
|
KeyManagerKeyShareRequest request;
|
||||||
|
|
||||||
RoomKeyRequest.fromToDeviceEvent(
|
RoomKeyRequest.fromToDeviceEvent(
|
||||||
ToDeviceEvent toDeviceEvent, this.keyManager, this.request)
|
ToDeviceEvent toDeviceEvent,
|
||||||
: super(
|
this.keyManager,
|
||||||
sender: toDeviceEvent.sender,
|
this.request,
|
||||||
content: toDeviceEvent.content,
|
) : super(
|
||||||
type: toDeviceEvent.type);
|
sender: toDeviceEvent.sender,
|
||||||
|
content: toDeviceEvent.content,
|
||||||
|
type: toDeviceEvent.type,
|
||||||
|
);
|
||||||
|
|
||||||
Room get room => request.room;
|
Room get room => request.room;
|
||||||
|
|
||||||
|
|
@ -1155,7 +1236,8 @@ class RoomKeyRequest extends ToDeviceEvent {
|
||||||
? keyManager.encryption.fingerprintKey
|
? keyManager.encryption.fingerprintKey
|
||||||
: null);
|
: null);
|
||||||
message['session_key'] = session.inboundGroupSession!.export_session(
|
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
|
// send the actual reply of the key back to the requester
|
||||||
await keyManager.client.sendToDeviceEncrypted(
|
await keyManager.client.sendToDeviceEncrypted(
|
||||||
[requestingDevice],
|
[requestingDevice],
|
||||||
|
|
@ -1217,8 +1299,10 @@ RoomKeys generateUploadKeysImplementation(GenerateUploadKeysArgs args) {
|
||||||
}
|
}
|
||||||
|
|
||||||
class DbInboundGroupSessionBundle {
|
class DbInboundGroupSessionBundle {
|
||||||
DbInboundGroupSessionBundle(
|
DbInboundGroupSessionBundle({
|
||||||
{required this.dbSession, required this.verified});
|
required this.dbSession,
|
||||||
|
required this.verified,
|
||||||
|
});
|
||||||
|
|
||||||
factory DbInboundGroupSessionBundle.fromJson(Map<dynamic, dynamic> json) =>
|
factory DbInboundGroupSessionBundle.fromJson(Map<dynamic, dynamic> json) =>
|
||||||
DbInboundGroupSessionBundle(
|
DbInboundGroupSessionBundle(
|
||||||
|
|
@ -1236,8 +1320,11 @@ class DbInboundGroupSessionBundle {
|
||||||
}
|
}
|
||||||
|
|
||||||
class GenerateUploadKeysArgs {
|
class GenerateUploadKeysArgs {
|
||||||
GenerateUploadKeysArgs(
|
GenerateUploadKeysArgs({
|
||||||
{required this.pubkey, required this.dbSessions, required this.userId});
|
required this.pubkey,
|
||||||
|
required this.dbSessions,
|
||||||
|
required this.userId,
|
||||||
|
});
|
||||||
|
|
||||||
factory GenerateUploadKeysArgs.fromJson(Map<dynamic, dynamic> json) =>
|
factory GenerateUploadKeysArgs.fromJson(Map<dynamic, dynamic> json) =>
|
||||||
GenerateUploadKeysArgs(
|
GenerateUploadKeysArgs(
|
||||||
|
|
|
||||||
|
|
@ -126,9 +126,15 @@ class KeyVerificationManager {
|
||||||
final room = client.getRoomById(update.roomID) ??
|
final room = client.getRoomById(update.roomID) ??
|
||||||
Room(id: update.roomID, client: client);
|
Room(id: update.roomID, client: client);
|
||||||
final newKeyRequest = KeyVerification(
|
final newKeyRequest = KeyVerification(
|
||||||
encryption: encryption, userId: event['sender'], room: room);
|
encryption: encryption,
|
||||||
|
userId: event['sender'],
|
||||||
|
room: room,
|
||||||
|
);
|
||||||
await newKeyRequest.handlePayload(
|
await newKeyRequest.handlePayload(
|
||||||
type, event['content'], event['event_id']);
|
type,
|
||||||
|
event['content'],
|
||||||
|
event['event_id'],
|
||||||
|
);
|
||||||
if (newKeyRequest.state != KeyVerificationState.askAccept) {
|
if (newKeyRequest.state != KeyVerificationState.askAccept) {
|
||||||
// something went wrong, let's just dispose the request
|
// something went wrong, let's just dispose the request
|
||||||
newKeyRequest.dispose();
|
newKeyRequest.dispose();
|
||||||
|
|
|
||||||
|
|
@ -193,7 +193,7 @@ class OlmManager {
|
||||||
'device_id': ourDeviceId,
|
'device_id': ourDeviceId,
|
||||||
'algorithms': [
|
'algorithms': [
|
||||||
AlgorithmTypes.olmV1Curve25519AesSha2,
|
AlgorithmTypes.olmV1Curve25519AesSha2,
|
||||||
AlgorithmTypes.megolmV1AesSha2
|
AlgorithmTypes.megolmV1AesSha2,
|
||||||
],
|
],
|
||||||
'keys': <String, dynamic>{},
|
'keys': <String, dynamic>{},
|
||||||
};
|
};
|
||||||
|
|
@ -247,7 +247,8 @@ class OlmManager {
|
||||||
if (dehydratedDeviceAlgorithm == null ||
|
if (dehydratedDeviceAlgorithm == null ||
|
||||||
dehydratedDevicePickleKey == null) {
|
dehydratedDevicePickleKey == null) {
|
||||||
throw Exception(
|
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(
|
await client.uploadDehydratedDevice(
|
||||||
|
|
@ -265,13 +266,14 @@ class OlmManager {
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
final currentUpload =
|
final currentUpload = this.currentUpload = CancelableOperation.fromFuture(
|
||||||
this.currentUpload = CancelableOperation.fromFuture(client.uploadKeys(
|
client.uploadKeys(
|
||||||
deviceKeys:
|
deviceKeys:
|
||||||
uploadDeviceKeys ? MatrixDeviceKeys.fromJson(deviceKeys) : null,
|
uploadDeviceKeys ? MatrixDeviceKeys.fromJson(deviceKeys) : null,
|
||||||
oneTimeKeys: signedOneTimeKeys,
|
oneTimeKeys: signedOneTimeKeys,
|
||||||
fallbackKeys: signedFallbackKeys,
|
fallbackKeys: signedFallbackKeys,
|
||||||
));
|
),
|
||||||
|
);
|
||||||
final response = await currentUpload.valueOrCancellation();
|
final response = await currentUpload.valueOrCancellation();
|
||||||
if (response == null) {
|
if (response == null) {
|
||||||
_uploadKeysLock = false;
|
_uploadKeysLock = false;
|
||||||
|
|
@ -314,11 +316,12 @@ class OlmManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
await uploadKeys(
|
await uploadKeys(
|
||||||
uploadDeviceKeys: uploadDeviceKeys,
|
uploadDeviceKeys: uploadDeviceKeys,
|
||||||
oldKeyCount: oldKeyCount,
|
oldKeyCount: oldKeyCount,
|
||||||
updateDatabase: updateDatabase,
|
updateDatabase: updateDatabase,
|
||||||
unusedFallbackKey: unusedFallbackKey,
|
unusedFallbackKey: unusedFallbackKey,
|
||||||
retry: retry - 1);
|
retry: retry - 1,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
_uploadKeysLock = false;
|
_uploadKeysLock = false;
|
||||||
|
|
@ -330,47 +333,50 @@ class OlmManager {
|
||||||
final _otkUpdateDedup = AsyncCache<void>.ephemeral();
|
final _otkUpdateDedup = AsyncCache<void>.ephemeral();
|
||||||
|
|
||||||
Future<void> handleDeviceOneTimeKeysCount(
|
Future<void> handleDeviceOneTimeKeysCount(
|
||||||
Map<String, int>? countJson, List<String>? unusedFallbackKeyTypes) async {
|
Map<String, int>? countJson,
|
||||||
|
List<String>? unusedFallbackKeyTypes,
|
||||||
|
) async {
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await _otkUpdateDedup.fetch(() =>
|
await _otkUpdateDedup.fetch(
|
||||||
runBenchmarked('handleOtkUpdate', () async {
|
() => runBenchmarked('handleOtkUpdate', () async {
|
||||||
final haveFallbackKeys = encryption.isMinOlmVersion(3, 2, 0);
|
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
|
// 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.
|
// and generate and upload more if not.
|
||||||
|
|
||||||
// If the server did not send us a count, assume it is 0
|
// If the server did not send us a count, assume it is 0
|
||||||
final keyCount = countJson?.tryGet<int>('signed_curve25519') ?? 0;
|
final keyCount = countJson?.tryGet<int>('signed_curve25519') ?? 0;
|
||||||
|
|
||||||
// If the server does not support fallback keys, it will not tell us about them.
|
// If the server does not support fallback keys, it will not tell us about them.
|
||||||
// If the server supports them but has no key, upload a new one.
|
// If the server supports them but has no key, upload a new one.
|
||||||
var unusedFallbackKey = true;
|
var unusedFallbackKey = true;
|
||||||
if (unusedFallbackKeyTypes?.contains('signed_curve25519') == false) {
|
if (unusedFallbackKeyTypes?.contains('signed_curve25519') == false) {
|
||||||
unusedFallbackKey = false;
|
unusedFallbackKey = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.
|
// 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()) {
|
if (keyCount > _olmAccount!.max_number_of_one_time_keys()) {
|
||||||
final requestingKeysFrom = {
|
final requestingKeysFrom = {
|
||||||
client.userID!: {ourDeviceId!: 'signed_curve25519'}
|
client.userID!: {ourDeviceId!: 'signed_curve25519'},
|
||||||
};
|
};
|
||||||
await client.claimKeys(requestingKeysFrom, timeout: 10000);
|
await client.claimKeys(requestingKeysFrom, timeout: 10000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only upload keys if they are less than half of the max or we have no unused fallback key
|
// Only upload keys if they are less than half of the max or we have no unused fallback key
|
||||||
if (keyCount < (_olmAccount!.max_number_of_one_time_keys() / 2) ||
|
if (keyCount < (_olmAccount!.max_number_of_one_time_keys() / 2) ||
|
||||||
!unusedFallbackKey) {
|
!unusedFallbackKey) {
|
||||||
await uploadKeys(
|
await uploadKeys(
|
||||||
oldKeyCount:
|
oldKeyCount:
|
||||||
keyCount < (_olmAccount!.max_number_of_one_time_keys() / 2)
|
keyCount < (_olmAccount!.max_number_of_one_time_keys() / 2)
|
||||||
? keyCount
|
? keyCount
|
||||||
: null,
|
: null,
|
||||||
unusedFallbackKey: haveFallbackKeys ? unusedFallbackKey : null,
|
unusedFallbackKey: haveFallbackKeys ? unusedFallbackKey : null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> storeOlmSession(OlmSession session) async {
|
Future<void> storeOlmSession(OlmSession session) async {
|
||||||
|
|
@ -389,11 +395,12 @@ class OlmManager {
|
||||||
_olmSessions[session.identityKey]![ix] = session;
|
_olmSessions[session.identityKey]![ix] = session;
|
||||||
}
|
}
|
||||||
await encryption.olmDatabase?.storeOlmSession(
|
await encryption.olmDatabase?.storeOlmSession(
|
||||||
session.identityKey,
|
session.identityKey,
|
||||||
session.sessionId!,
|
session.sessionId!,
|
||||||
session.pickledSession!,
|
session.pickledSession!,
|
||||||
session.lastReceived?.millisecondsSinceEpoch ??
|
session.lastReceived?.millisecondsSinceEpoch ??
|
||||||
DateTime.now().millisecondsSinceEpoch);
|
DateTime.now().millisecondsSinceEpoch,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<ToDeviceEvent> _decryptToDeviceEvent(ToDeviceEvent event) async {
|
Future<ToDeviceEvent> _decryptToDeviceEvent(ToDeviceEvent event) async {
|
||||||
|
|
@ -427,9 +434,10 @@ class OlmManager {
|
||||||
if (device != null) {
|
if (device != null) {
|
||||||
device.lastActive = DateTime.now();
|
device.lastActive = DateTime.now();
|
||||||
await encryption.olmDatabase?.setLastActiveUserDeviceKey(
|
await encryption.olmDatabase?.setLastActiveUserDeviceKey(
|
||||||
device.lastActive.millisecondsSinceEpoch,
|
device.lastActive.millisecondsSinceEpoch,
|
||||||
device.userId,
|
device.userId,
|
||||||
device.deviceId!);
|
device.deviceId!,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logs().e('Error while updating olm session timestamp', e, s);
|
Logs().e('Error while updating olm session timestamp', e, s);
|
||||||
|
|
@ -447,7 +455,9 @@ class OlmManager {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// The message was encrypted during this session, but is unable to decrypt
|
// The message was encrypted during this session, but is unable to decrypt
|
||||||
throw DecryptException(
|
throw DecryptException(
|
||||||
DecryptException.decryptionFailed, e.toString());
|
DecryptException.decryptionFailed,
|
||||||
|
e.toString(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
await updateSessionUsage(session);
|
await updateSessionUsage(session);
|
||||||
break;
|
break;
|
||||||
|
|
@ -475,13 +485,15 @@ class OlmManager {
|
||||||
|
|
||||||
plaintext = newSession.decrypt(type, body);
|
plaintext = newSession.decrypt(type, body);
|
||||||
|
|
||||||
await storeOlmSession(OlmSession(
|
await storeOlmSession(
|
||||||
key: client.userID!,
|
OlmSession(
|
||||||
identityKey: senderKey,
|
key: client.userID!,
|
||||||
sessionId: newSession.session_id(),
|
identityKey: senderKey,
|
||||||
session: newSession,
|
sessionId: newSession.session_id(),
|
||||||
lastReceived: DateTime.now(),
|
session: newSession,
|
||||||
));
|
lastReceived: DateTime.now(),
|
||||||
|
),
|
||||||
|
);
|
||||||
await updateSessionUsage();
|
await updateSessionUsage();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
newSession.free();
|
newSession.free();
|
||||||
|
|
@ -515,7 +527,8 @@ class OlmManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> getOlmSessionsForDevicesFromDatabase(
|
Future<void> getOlmSessionsForDevicesFromDatabase(
|
||||||
List<String> senderKeys) async {
|
List<String> senderKeys,
|
||||||
|
) async {
|
||||||
final rows = await encryption.olmDatabase?.getOlmSessionsForDevices(
|
final rows = await encryption.olmDatabase?.getOlmSessionsForDevices(
|
||||||
senderKeys,
|
senderKeys,
|
||||||
client.userID!,
|
client.userID!,
|
||||||
|
|
@ -532,8 +545,10 @@ class OlmManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<OlmSession>> getOlmSessions(String senderKey,
|
Future<List<OlmSession>> getOlmSessions(
|
||||||
{bool getFromDb = true}) async {
|
String senderKey, {
|
||||||
|
bool getFromDb = true,
|
||||||
|
}) async {
|
||||||
var sess = olmSessions[senderKey];
|
var sess = olmSessions[senderKey];
|
||||||
if ((getFromDb) && (sess == null || sess.isEmpty)) {
|
if ((getFromDb) && (sess == null || sess.isEmpty)) {
|
||||||
final sessions = await getOlmSessionsFromDatabase(senderKey);
|
final sessions = await getOlmSessionsFromDatabase(senderKey);
|
||||||
|
|
@ -545,10 +560,12 @@ class OlmManager {
|
||||||
if (sess == null) {
|
if (sess == null) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
sess.sort((a, b) => a.lastReceived == b.lastReceived
|
sess.sort(
|
||||||
? (a.sessionId ?? '').compareTo(b.sessionId ?? '')
|
(a, b) => a.lastReceived == b.lastReceived
|
||||||
: (b.lastReceived ?? DateTime(0))
|
? (a.sessionId ?? '').compareTo(b.sessionId ?? '')
|
||||||
.compareTo(a.lastReceived ?? DateTime(0)));
|
: (b.lastReceived ?? DateTime(0))
|
||||||
|
.compareTo(a.lastReceived ?? DateTime(0)),
|
||||||
|
);
|
||||||
return sess;
|
return sess;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -570,7 +587,8 @@ class OlmManager {
|
||||||
.subtract(Duration(hours: 1))
|
.subtract(Duration(hours: 1))
|
||||||
.isBefore(_restoredOlmSessionsTime[mapKey]!)) {
|
.isBefore(_restoredOlmSessionsTime[mapKey]!)) {
|
||||||
Logs().w(
|
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;
|
return;
|
||||||
}
|
}
|
||||||
_restoredOlmSessionsTime[mapKey] = DateTime.now();
|
_restoredOlmSessionsTime[mapKey] = DateTime.now();
|
||||||
|
|
@ -608,7 +626,8 @@ class OlmManager {
|
||||||
|
|
||||||
Future<void> startOutgoingOlmSessions(List<DeviceKeys> deviceKeys) async {
|
Future<void> startOutgoingOlmSessions(List<DeviceKeys> deviceKeys) async {
|
||||||
Logs().v(
|
Logs().v(
|
||||||
'[OlmManager] Starting session with ${deviceKeys.length} devices...');
|
'[OlmManager] Starting session with ${deviceKeys.length} devices...',
|
||||||
|
);
|
||||||
final requestingKeysFrom = <String, Map<String, String>>{};
|
final requestingKeysFrom = <String, Map<String, String>>{};
|
||||||
for (final device in deviceKeys) {
|
for (final device in deviceKeys) {
|
||||||
if (requestingKeysFrom[device.userId] == null) {
|
if (requestingKeysFrom[device.userId] == null) {
|
||||||
|
|
@ -644,15 +663,20 @@ class OlmManager {
|
||||||
final session = olm.Session();
|
final session = olm.Session();
|
||||||
try {
|
try {
|
||||||
session.create_outbound(
|
session.create_outbound(
|
||||||
_olmAccount!, identityKey, deviceKey.tryGet<String>('key')!);
|
_olmAccount!,
|
||||||
await storeOlmSession(OlmSession(
|
identityKey,
|
||||||
key: client.userID!,
|
deviceKey.tryGet<String>('key')!,
|
||||||
identityKey: identityKey,
|
);
|
||||||
sessionId: session.session_id(),
|
await storeOlmSession(
|
||||||
session: session,
|
OlmSession(
|
||||||
lastReceived:
|
key: client.userID!,
|
||||||
DateTime.now(), // we want to use a newly created session
|
identityKey: identityKey,
|
||||||
));
|
sessionId: session.session_id(),
|
||||||
|
session: session,
|
||||||
|
lastReceived:
|
||||||
|
DateTime.now(), // we want to use a newly created session
|
||||||
|
),
|
||||||
|
);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
session.free();
|
session.free();
|
||||||
Logs()
|
Logs()
|
||||||
|
|
@ -668,8 +692,11 @@ class OlmManager {
|
||||||
/// Throws `NoOlmSessionFoundException` if there is no olm session with this
|
/// Throws `NoOlmSessionFoundException` if there is no olm session with this
|
||||||
/// device and none could be created.
|
/// device and none could be created.
|
||||||
Future<Map<String, dynamic>> encryptToDeviceMessagePayload(
|
Future<Map<String, dynamic>> encryptToDeviceMessagePayload(
|
||||||
DeviceKeys device, String type, Map<String, dynamic> payload,
|
DeviceKeys device,
|
||||||
{bool getFromDb = true}) async {
|
String type,
|
||||||
|
Map<String, dynamic> payload, {
|
||||||
|
bool getFromDb = true,
|
||||||
|
}) async {
|
||||||
final sess =
|
final sess =
|
||||||
await getOlmSessions(device.curve25519Key!, getFromDb: getFromDb);
|
await getOlmSessions(device.curve25519Key!, getFromDb: getFromDb);
|
||||||
if (sess.isEmpty) {
|
if (sess.isEmpty) {
|
||||||
|
|
@ -688,12 +715,13 @@ class OlmManager {
|
||||||
if (encryption.olmDatabase != null) {
|
if (encryption.olmDatabase != null) {
|
||||||
try {
|
try {
|
||||||
await encryption.olmDatabase?.setLastSentMessageUserDeviceKey(
|
await encryption.olmDatabase?.setLastSentMessageUserDeviceKey(
|
||||||
json.encode({
|
json.encode({
|
||||||
'type': type,
|
'type': type,
|
||||||
'content': payload,
|
'content': payload,
|
||||||
}),
|
}),
|
||||||
device.userId,
|
device.userId,
|
||||||
device.deviceId!);
|
device.deviceId!,
|
||||||
|
);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
// we can ignore this error, since it would just make us use a different olm session possibly
|
// 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);
|
Logs().w('Error while updating olm usage timestamp', e, s);
|
||||||
|
|
@ -712,18 +740,22 @@ class OlmManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, Map<String, Map<String, dynamic>>>> encryptToDeviceMessage(
|
Future<Map<String, Map<String, Map<String, dynamic>>>> encryptToDeviceMessage(
|
||||||
List<DeviceKeys> deviceKeys,
|
List<DeviceKeys> deviceKeys,
|
||||||
String type,
|
String type,
|
||||||
Map<String, dynamic> payload) async {
|
Map<String, dynamic> payload,
|
||||||
|
) async {
|
||||||
final data = <String, Map<String, Map<String, dynamic>>>{};
|
final data = <String, Map<String, Map<String, dynamic>>>{};
|
||||||
// first check if any of our sessions we want to encrypt for are in the database
|
// first check if any of our sessions we want to encrypt for are in the database
|
||||||
if (encryption.olmDatabase != null) {
|
if (encryption.olmDatabase != null) {
|
||||||
await getOlmSessionsForDevicesFromDatabase(
|
await getOlmSessionsForDevicesFromDatabase(
|
||||||
deviceKeys.map((d) => d.curve25519Key!).toList());
|
deviceKeys.map((d) => d.curve25519Key!).toList(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
final deviceKeysWithoutSession = List<DeviceKeys>.from(deviceKeys);
|
final deviceKeysWithoutSession = List<DeviceKeys>.from(deviceKeys);
|
||||||
deviceKeysWithoutSession.removeWhere((DeviceKeys deviceKeys) =>
|
deviceKeysWithoutSession.removeWhere(
|
||||||
olmSessions[deviceKeys.curve25519Key]?.isNotEmpty ?? false);
|
(DeviceKeys deviceKeys) =>
|
||||||
|
olmSessions[deviceKeys.curve25519Key]?.isNotEmpty ?? false,
|
||||||
|
);
|
||||||
if (deviceKeysWithoutSession.isNotEmpty) {
|
if (deviceKeysWithoutSession.isNotEmpty) {
|
||||||
await startOutgoingOlmSessions(deviceKeysWithoutSession);
|
await startOutgoingOlmSessions(deviceKeysWithoutSession);
|
||||||
}
|
}
|
||||||
|
|
@ -731,8 +763,11 @@ class OlmManager {
|
||||||
final userData = data[device.userId] ??= {};
|
final userData = data[device.userId] ??= {};
|
||||||
try {
|
try {
|
||||||
userData[device.deviceId!] = await encryptToDeviceMessagePayload(
|
userData[device.deviceId!] = await encryptToDeviceMessagePayload(
|
||||||
device, type, payload,
|
device,
|
||||||
getFromDb: false);
|
type,
|
||||||
|
payload,
|
||||||
|
getFromDb: false,
|
||||||
|
);
|
||||||
} on NoOlmSessionFoundException catch (e) {
|
} on NoOlmSessionFoundException catch (e) {
|
||||||
Logs().d('[LibOlm] Error encrypting to-device event', e);
|
Logs().d('[LibOlm] Error encrypting to-device event', e);
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -753,12 +788,14 @@ class OlmManager {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final device = client.getUserDeviceKeysByCurve25519Key(
|
final device = client.getUserDeviceKeysByCurve25519Key(
|
||||||
encryptedContent.tryGet<String>('sender_key') ?? '');
|
encryptedContent.tryGet<String>('sender_key') ?? '',
|
||||||
|
);
|
||||||
if (device == null) {
|
if (device == null) {
|
||||||
return; // device not found
|
return; // device not found
|
||||||
}
|
}
|
||||||
Logs().v(
|
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
|
final lastSentMessageRes = await encryption.olmDatabase
|
||||||
?.getLastSentMessageUserDeviceKey(device.userId, device.deviceId!);
|
?.getLastSentMessageUserDeviceKey(device.userId, device.deviceId!);
|
||||||
if (lastSentMessageRes == null ||
|
if (lastSentMessageRes == null ||
|
||||||
|
|
@ -773,7 +810,10 @@ class OlmManager {
|
||||||
if (lastSentMessage['type'] != EventTypes.Dummy) {
|
if (lastSentMessage['type'] != EventTypes.Dummy) {
|
||||||
// okay, time to send the message!
|
// okay, time to send the message!
|
||||||
await client.sendToDeviceEncrypted(
|
await client.sendToDeviceEncrypted(
|
||||||
[device], lastSentMessage['type'], lastSentMessage['content']);
|
[device],
|
||||||
|
lastSentMessage['type'],
|
||||||
|
lastSentMessage['content'],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -82,13 +82,17 @@ class SSSS {
|
||||||
final hmacKey =
|
final hmacKey =
|
||||||
Hmac(sha256, prk.bytes).convert(aesKey.bytes + utf8.encode(name) + b);
|
Hmac(sha256, prk.bytes).convert(aesKey.bytes + utf8.encode(name) + b);
|
||||||
return DerivedKeys(
|
return DerivedKeys(
|
||||||
aesKey: Uint8List.fromList(aesKey.bytes),
|
aesKey: Uint8List.fromList(aesKey.bytes),
|
||||||
hmacKey: Uint8List.fromList(hmacKey.bytes));
|
hmacKey: Uint8List.fromList(hmacKey.bytes),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<EncryptedContent> encryptAes(
|
static Future<EncryptedContent> encryptAes(
|
||||||
String data, Uint8List key, String name,
|
String data,
|
||||||
[String? ivStr]) async {
|
Uint8List key,
|
||||||
|
String name, [
|
||||||
|
String? ivStr,
|
||||||
|
]) async {
|
||||||
Uint8List iv;
|
Uint8List iv;
|
||||||
if (ivStr != null) {
|
if (ivStr != null) {
|
||||||
iv = base64decodeUnpadded(ivStr);
|
iv = base64decodeUnpadded(ivStr);
|
||||||
|
|
@ -106,13 +110,17 @@ class SSSS {
|
||||||
final hmac = Hmac(sha256, keys.hmacKey).convert(ciphertext);
|
final hmac = Hmac(sha256, keys.hmacKey).convert(ciphertext);
|
||||||
|
|
||||||
return EncryptedContent(
|
return EncryptedContent(
|
||||||
iv: base64.encode(iv),
|
iv: base64.encode(iv),
|
||||||
ciphertext: base64.encode(ciphertext),
|
ciphertext: base64.encode(ciphertext),
|
||||||
mac: base64.encode(hmac.bytes));
|
mac: base64.encode(hmac.bytes),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<String> decryptAes(
|
static Future<String> decryptAes(
|
||||||
EncryptedContent data, Uint8List key, String name) async {
|
EncryptedContent data,
|
||||||
|
Uint8List key,
|
||||||
|
String name,
|
||||||
|
) async {
|
||||||
final keys = deriveKeys(key, name);
|
final keys = deriveKeys(key, name);
|
||||||
final cipher = base64decodeUnpadded(data.ciphertext);
|
final cipher = base64decodeUnpadded(data.ciphertext);
|
||||||
final hmac = base64
|
final hmac = base64
|
||||||
|
|
@ -144,8 +152,12 @@ class SSSS {
|
||||||
throw InvalidPassphraseException('Incorrect length');
|
throw InvalidPassphraseException('Incorrect length');
|
||||||
}
|
}
|
||||||
|
|
||||||
return Uint8List.fromList(result.sublist(olmRecoveryKeyPrefix.length,
|
return Uint8List.fromList(
|
||||||
olmRecoveryKeyPrefix.length + ssssKeyLength));
|
result.sublist(
|
||||||
|
olmRecoveryKeyPrefix.length,
|
||||||
|
olmRecoveryKeyPrefix.length + ssssKeyLength,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static String encodeRecoveryKey(Uint8List recoveryKey) {
|
static String encodeRecoveryKey(Uint8List recoveryKey) {
|
||||||
|
|
@ -160,7 +172,9 @@ class SSSS {
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<Uint8List> keyFromPassphrase(
|
static Future<Uint8List> keyFromPassphrase(
|
||||||
String passphrase, PassphraseInfo info) async {
|
String passphrase,
|
||||||
|
PassphraseInfo info,
|
||||||
|
) async {
|
||||||
if (info.algorithm != AlgorithmTypes.pbkdf2) {
|
if (info.algorithm != AlgorithmTypes.pbkdf2) {
|
||||||
throw InvalidPassphraseException('Unknown algorithm');
|
throw InvalidPassphraseException('Unknown algorithm');
|
||||||
}
|
}
|
||||||
|
|
@ -171,11 +185,12 @@ class SSSS {
|
||||||
throw InvalidPassphraseException('Passphrase info without salt');
|
throw InvalidPassphraseException('Passphrase info without salt');
|
||||||
}
|
}
|
||||||
return await uc.pbkdf2(
|
return await uc.pbkdf2(
|
||||||
Uint8List.fromList(utf8.encode(passphrase)),
|
Uint8List.fromList(utf8.encode(passphrase)),
|
||||||
Uint8List.fromList(utf8.encode(info.salt!)),
|
Uint8List.fromList(utf8.encode(info.salt!)),
|
||||||
uc.sha512,
|
uc.sha512,
|
||||||
info.iterations!,
|
info.iterations!,
|
||||||
info.bits ?? 256);
|
info.bits ?? 256,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void setValidator(String type, FutureOr<bool> Function(String) validator) {
|
void setValidator(String type, FutureOr<bool> Function(String) validator) {
|
||||||
|
|
@ -252,7 +267,10 @@ class SSSS {
|
||||||
// noooow we set the account data
|
// noooow we set the account data
|
||||||
|
|
||||||
await client.setAccountData(
|
await client.setAccountData(
|
||||||
client.userID!, accountDataTypeKeyId, content.toJson());
|
client.userID!,
|
||||||
|
accountDataTypeKeyId,
|
||||||
|
content.toJson(),
|
||||||
|
);
|
||||||
|
|
||||||
while (!client.accountData.containsKey(accountDataTypeKeyId)) {
|
while (!client.accountData.containsKey(accountDataTypeKeyId)) {
|
||||||
Logs().v('Waiting accountData to have $accountDataTypeKeyId');
|
Logs().v('Waiting accountData to have $accountDataTypeKeyId');
|
||||||
|
|
@ -354,8 +372,13 @@ class SSSS {
|
||||||
return decrypted;
|
return decrypted;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> store(String type, String secret, String keyId, Uint8List key,
|
Future<void> store(
|
||||||
{bool add = false}) async {
|
String type,
|
||||||
|
String secret,
|
||||||
|
String keyId,
|
||||||
|
Uint8List key, {
|
||||||
|
bool add = false,
|
||||||
|
}) async {
|
||||||
final encrypted = await encryptAes(secret, key, type);
|
final encrypted = await encryptAes(secret, key, type);
|
||||||
Map<String, dynamic>? content;
|
Map<String, dynamic>? content;
|
||||||
if (add && client.accountData[type] != null) {
|
if (add && client.accountData[type] != null) {
|
||||||
|
|
@ -386,7 +409,11 @@ class SSSS {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> validateAndStripOtherKeys(
|
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) {
|
if (await getStored(type, keyId, key) != secret) {
|
||||||
throw Exception('Secrets do not match up!');
|
throw Exception('Secrets do not match up!');
|
||||||
}
|
}
|
||||||
|
|
@ -457,11 +484,13 @@ class SSSS {
|
||||||
devices =
|
devices =
|
||||||
client.userDeviceKeys[client.userID]!.deviceKeys.values.toList();
|
client.userDeviceKeys[client.userID]!.deviceKeys.values.toList();
|
||||||
}
|
}
|
||||||
devices.removeWhere((DeviceKeys d) =>
|
devices.removeWhere(
|
||||||
d.userId != client.userID ||
|
(DeviceKeys d) =>
|
||||||
!d.verified ||
|
d.userId != client.userID ||
|
||||||
d.blocked ||
|
!d.verified ||
|
||||||
d.deviceId == client.deviceID);
|
d.blocked ||
|
||||||
|
d.deviceId == client.deviceID,
|
||||||
|
);
|
||||||
if (devices.isEmpty) {
|
if (devices.isEmpty) {
|
||||||
Logs().w('[SSSS] No devices');
|
Logs().w('[SSSS] No devices');
|
||||||
return;
|
return;
|
||||||
|
|
@ -555,9 +584,11 @@ class SSSS {
|
||||||
}
|
}
|
||||||
final request = pendingShareRequests[event.content['request_id']]!;
|
final request = pendingShareRequests[event.content['request_id']]!;
|
||||||
// alright, as we received a known request id, let's check if the sender is valid
|
// 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.userId == event.sender &&
|
(d) =>
|
||||||
d.curve25519Key == encryptedContent['sender_key']);
|
d.userId == event.sender &&
|
||||||
|
d.curve25519Key == encryptedContent['sender_key'],
|
||||||
|
);
|
||||||
if (device == null) {
|
if (device == null) {
|
||||||
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
|
||||||
|
|
@ -645,9 +676,11 @@ class _ShareRequest {
|
||||||
final List<DeviceKeys> devices;
|
final List<DeviceKeys> devices;
|
||||||
final DateTime start;
|
final DateTime start;
|
||||||
|
|
||||||
_ShareRequest(
|
_ShareRequest({
|
||||||
{required this.requestId, required this.type, required this.devices})
|
required this.requestId,
|
||||||
: start = DateTime.now();
|
required this.type,
|
||||||
|
required this.devices,
|
||||||
|
}) : start = DateTime.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
class EncryptedContent {
|
class EncryptedContent {
|
||||||
|
|
@ -655,8 +688,11 @@ class EncryptedContent {
|
||||||
final String ciphertext;
|
final String ciphertext;
|
||||||
final String mac;
|
final String mac;
|
||||||
|
|
||||||
EncryptedContent(
|
EncryptedContent({
|
||||||
{required this.iv, required this.ciphertext, required this.mac});
|
required this.iv,
|
||||||
|
required this.ciphertext,
|
||||||
|
required this.mac,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class DerivedKeys {
|
class DerivedKeys {
|
||||||
|
|
@ -682,11 +718,12 @@ class OpenSSSS {
|
||||||
String? get recoveryKey =>
|
String? get recoveryKey =>
|
||||||
isUnlocked ? SSSS.encodeRecoveryKey(privateKey!) : null;
|
isUnlocked ? SSSS.encodeRecoveryKey(privateKey!) : null;
|
||||||
|
|
||||||
Future<void> unlock(
|
Future<void> unlock({
|
||||||
{String? passphrase,
|
String? passphrase,
|
||||||
String? recoveryKey,
|
String? recoveryKey,
|
||||||
String? keyOrPassphrase,
|
String? keyOrPassphrase,
|
||||||
bool postUnlock = true}) async {
|
bool postUnlock = true,
|
||||||
|
}) async {
|
||||||
if (keyOrPassphrase != null) {
|
if (keyOrPassphrase != null) {
|
||||||
try {
|
try {
|
||||||
await unlock(recoveryKey: keyOrPassphrase, postUnlock: postUnlock);
|
await unlock(recoveryKey: keyOrPassphrase, postUnlock: postUnlock);
|
||||||
|
|
@ -701,7 +738,8 @@ class OpenSSSS {
|
||||||
} else if (passphrase != null) {
|
} else if (passphrase != null) {
|
||||||
if (!hasPassphrase) {
|
if (!hasPassphrase) {
|
||||||
throw InvalidPassphraseException(
|
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(
|
privateKey = await Future.value(
|
||||||
ssss.client.nativeImplementations.keyFromPassphrase(
|
ssss.client.nativeImplementations.keyFromPassphrase(
|
||||||
|
|
|
||||||
|
|
@ -164,7 +164,8 @@ class Bootstrap {
|
||||||
Set<String> allNeededKeys() {
|
Set<String> allNeededKeys() {
|
||||||
final secrets = analyzeSecrets();
|
final secrets = analyzeSecrets();
|
||||||
secrets.removeWhere(
|
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 keys = <String>{};
|
||||||
final defaultKeyId = encryption.ssss.defaultKeyId;
|
final defaultKeyId = encryption.ssss.defaultKeyId;
|
||||||
int removeKey(String key) {
|
int removeKey(String key) {
|
||||||
|
|
@ -297,7 +298,8 @@ class Bootstrap {
|
||||||
await encryption.ssss.setDefaultKeyId(newSsssKey!.keyId);
|
await encryption.ssss.setDefaultKeyId(newSsssKey!.keyId);
|
||||||
while (encryption.ssss.defaultKeyId != newSsssKey!.keyId) {
|
while (encryption.ssss.defaultKeyId != newSsssKey!.keyId) {
|
||||||
Logs().v(
|
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();
|
await client.oneShotSync();
|
||||||
}
|
}
|
||||||
if (oldSsssKeys != null) {
|
if (oldSsssKeys != null) {
|
||||||
|
|
@ -354,10 +356,11 @@ class Bootstrap {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> askSetupCrossSigning(
|
Future<void> askSetupCrossSigning({
|
||||||
{bool setupMasterKey = false,
|
bool setupMasterKey = false,
|
||||||
bool setupSelfSigningKey = false,
|
bool setupSelfSigningKey = false,
|
||||||
bool setupUserSigningKey = false}) async {
|
bool setupUserSigningKey = false,
|
||||||
|
}) async {
|
||||||
if (state != BootstrapState.askSetupCrossSigning) {
|
if (state != BootstrapState.askSetupCrossSigning) {
|
||||||
throw BootstrapBadStateException();
|
throw BootstrapBadStateException();
|
||||||
}
|
}
|
||||||
|
|
@ -395,8 +398,8 @@ class Bootstrap {
|
||||||
} else {
|
} else {
|
||||||
Logs().v('Get stored key...');
|
Logs().v('Get stored key...');
|
||||||
masterSigningKey = base64decodeUnpadded(
|
masterSigningKey = base64decodeUnpadded(
|
||||||
await newSsssKey?.getStored(EventTypes.CrossSigningMasterKey) ??
|
await newSsssKey?.getStored(EventTypes.CrossSigningMasterKey) ?? '',
|
||||||
'');
|
);
|
||||||
if (masterSigningKey.isEmpty) {
|
if (masterSigningKey.isEmpty) {
|
||||||
// no master signing key :(
|
// no master signing key :(
|
||||||
throw BootstrapBadStateException('No master key');
|
throw BootstrapBadStateException('No master key');
|
||||||
|
|
@ -473,12 +476,13 @@ class Bootstrap {
|
||||||
state = BootstrapState.loading;
|
state = BootstrapState.loading;
|
||||||
Logs().v('Upload device signing keys.');
|
Logs().v('Upload device signing keys.');
|
||||||
await client.uiaRequestBackground(
|
await client.uiaRequestBackground(
|
||||||
(AuthenticationData? auth) => client.uploadCrossSigningKeys(
|
(AuthenticationData? auth) => client.uploadCrossSigningKeys(
|
||||||
masterKey: masterKey,
|
masterKey: masterKey,
|
||||||
selfSigningKey: selfSigningKey,
|
selfSigningKey: selfSigningKey,
|
||||||
userSigningKey: userSigningKey,
|
userSigningKey: userSigningKey,
|
||||||
auth: auth,
|
auth: auth,
|
||||||
));
|
),
|
||||||
|
);
|
||||||
Logs().v('Device signing keys have been uploaded.');
|
Logs().v('Device signing keys have been uploaded.');
|
||||||
// aaaand set the SSSS secrets
|
// aaaand set the SSSS secrets
|
||||||
if (masterKey != null) {
|
if (masterKey != null) {
|
||||||
|
|
@ -503,7 +507,8 @@ class Bootstrap {
|
||||||
if (client.userDeviceKeys[client.userID]?.masterKey?.ed25519Key !=
|
if (client.userDeviceKeys[client.userID]?.masterKey?.ed25519Key !=
|
||||||
masterKey.publicKey) {
|
masterKey.publicKey) {
|
||||||
throw BootstrapBadStateException(
|
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...');
|
Logs().v('Set own master key to verified...');
|
||||||
await client.userDeviceKeys[client.userID]!.masterKey!
|
await client.userDeviceKeys[client.userID]!.masterKey!
|
||||||
|
|
@ -512,7 +517,8 @@ class Bootstrap {
|
||||||
}
|
}
|
||||||
if (selfSigningKey != null) {
|
if (selfSigningKey != null) {
|
||||||
keysToSign.add(
|
keysToSign.add(
|
||||||
client.userDeviceKeys[client.userID]!.deviceKeys[client.deviceID]!);
|
client.userDeviceKeys[client.userID]!.deviceKeys[client.deviceID]!,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Logs().v('Sign ourself...');
|
Logs().v('Sign ourself...');
|
||||||
await encryption.crossSigning.sign(keysToSign);
|
await encryption.crossSigning.sign(keysToSign);
|
||||||
|
|
@ -575,7 +581,8 @@ class Bootstrap {
|
||||||
await newSsssKey?.store(megolmKey, base64.encode(privKey));
|
await newSsssKey?.store(megolmKey, base64.encode(privKey));
|
||||||
|
|
||||||
Logs().v(
|
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();
|
await client.database?.markInboundGroupSessionsAsNeedingUpload();
|
||||||
Logs().v('And uploading keys...');
|
Logs().v('And uploading keys...');
|
||||||
await client.encryption?.keyManager.uploadInboundGroupSessions();
|
await client.encryption?.keyManager.uploadInboundGroupSessions();
|
||||||
|
|
|
||||||
|
|
@ -145,7 +145,9 @@ List<String> _intersect(List<String>? a, List<dynamic>? b) =>
|
||||||
(b == null || a == null) ? [] : a.where(b.contains).toList();
|
(b == null || a == null) ? [] : a.where(b.contains).toList();
|
||||||
|
|
||||||
List<String> _calculatePossibleMethods(
|
List<String> _calculatePossibleMethods(
|
||||||
List<String> knownMethods, List<dynamic> payloadMethods) {
|
List<String> knownMethods,
|
||||||
|
List<dynamic> payloadMethods,
|
||||||
|
) {
|
||||||
final output = <String>[];
|
final output = <String>[];
|
||||||
final copyKnownMethods = List<String>.from(knownMethods);
|
final copyKnownMethods = List<String>.from(knownMethods);
|
||||||
final copyPayloadMethods = List.from(payloadMethods);
|
final copyPayloadMethods = List.from(payloadMethods);
|
||||||
|
|
@ -193,7 +195,9 @@ List<int> _bytesToInt(Uint8List bytes, int totalBits) {
|
||||||
}
|
}
|
||||||
|
|
||||||
_KeyVerificationMethod _makeVerificationMethod(
|
_KeyVerificationMethod _makeVerificationMethod(
|
||||||
String type, KeyVerification request) {
|
String type,
|
||||||
|
KeyVerification request,
|
||||||
|
) {
|
||||||
if (type == EventTypes.Sas) {
|
if (type == EventTypes.Sas) {
|
||||||
return _KeyVerificationMethodSas(request: request);
|
return _KeyVerificationMethodSas(request: request);
|
||||||
}
|
}
|
||||||
|
|
@ -237,13 +241,13 @@ class KeyVerification {
|
||||||
QRCode? qrCode;
|
QRCode? qrCode;
|
||||||
String? randomSharedSecretForQRCode;
|
String? randomSharedSecretForQRCode;
|
||||||
SignableKey? keyToVerify;
|
SignableKey? keyToVerify;
|
||||||
KeyVerification(
|
KeyVerification({
|
||||||
{required this.encryption,
|
required this.encryption,
|
||||||
this.room,
|
this.room,
|
||||||
required this.userId,
|
required this.userId,
|
||||||
String? deviceId,
|
String? deviceId,
|
||||||
this.onUpdate})
|
this.onUpdate,
|
||||||
: _deviceId = deviceId,
|
}) : _deviceId = deviceId,
|
||||||
lastActivity = DateTime.now();
|
lastActivity = DateTime.now();
|
||||||
|
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
|
@ -287,8 +291,10 @@ class KeyVerification {
|
||||||
/// Once you get a ready event, i.e both sides are in a `askChoice` state,
|
/// 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
|
/// send either `m.reciprocate.v1` or `m.sas.v1` here. If you continue with
|
||||||
/// qr, send the qrData you just scanned
|
/// qr, send the qrData you just scanned
|
||||||
Future<void> continueVerification(String type,
|
Future<void> continueVerification(
|
||||||
{Uint8List? qrDataRawBytes}) async {
|
String type, {
|
||||||
|
Uint8List? qrDataRawBytes,
|
||||||
|
}) async {
|
||||||
bool qrChecksOut = false;
|
bool qrChecksOut = false;
|
||||||
if (possibleMethods.contains(type)) {
|
if (possibleMethods.contains(type)) {
|
||||||
if (qrDataRawBytes != null) {
|
if (qrDataRawBytes != null) {
|
||||||
|
|
@ -307,7 +313,8 @@ class KeyVerification {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Logs().e(
|
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');
|
await cancel('m.unknown_method');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -356,8 +363,11 @@ class KeyVerification {
|
||||||
return mode;
|
return mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> handlePayload(String type, Map<String, dynamic> payload,
|
Future<void> handlePayload(
|
||||||
[String? eventId]) async {
|
String type,
|
||||||
|
Map<String, dynamic> payload, [
|
||||||
|
String? eventId,
|
||||||
|
]) async {
|
||||||
if (isDone) {
|
if (isDone) {
|
||||||
return; // no need to do anything with already canceled requests
|
return; // no need to do anything with already canceled requests
|
||||||
}
|
}
|
||||||
|
|
@ -380,8 +390,10 @@ class KeyVerification {
|
||||||
now.add(Duration(minutes: 5)).isBefore(verifyTime)) {
|
now.add(Duration(minutes: 5)).isBefore(verifyTime)) {
|
||||||
// if the request is more than 20min in the past we just silently fail it
|
// if the request is more than 20min in the past we just silently fail it
|
||||||
// to not generate too many cancels
|
// to not generate too many cancels
|
||||||
await cancel('m.timeout',
|
await cancel(
|
||||||
now.subtract(Duration(minutes: 20)).isAfter(verifyTime));
|
'm.timeout',
|
||||||
|
now.subtract(Duration(minutes: 20)).isAfter(verifyTime),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -397,7 +409,9 @@ class KeyVerification {
|
||||||
oppositePossibleMethods = List<String>.from(payload['methods']);
|
oppositePossibleMethods = List<String>.from(payload['methods']);
|
||||||
// verify it has a method we can use
|
// verify it has a method we can use
|
||||||
possibleMethods = _calculatePossibleMethods(
|
possibleMethods = _calculatePossibleMethods(
|
||||||
knownVerificationMethods, payload['methods']);
|
knownVerificationMethods,
|
||||||
|
payload['methods'],
|
||||||
|
);
|
||||||
if (possibleMethods.isEmpty) {
|
if (possibleMethods.isEmpty) {
|
||||||
// reject it outright
|
// reject it outright
|
||||||
await cancel('m.unknown_method');
|
await cancel('m.unknown_method');
|
||||||
|
|
@ -412,17 +426,22 @@ class KeyVerification {
|
||||||
transactionId ??= eventId ?? payload['transaction_id'];
|
transactionId ??= eventId ?? payload['transaction_id'];
|
||||||
// and broadcast the cancel to the other devices
|
// and broadcast the cancel to the other devices
|
||||||
final devices = List<DeviceKeys>.from(
|
final devices = List<DeviceKeys>.from(
|
||||||
client.userDeviceKeys[userId]?.deviceKeys.values ??
|
client.userDeviceKeys[userId]?.deviceKeys.values ??
|
||||||
Iterable.empty());
|
Iterable.empty(),
|
||||||
|
);
|
||||||
devices.removeWhere(
|
devices.removeWhere(
|
||||||
(d) => {deviceId, client.deviceID}.contains(d.deviceId));
|
(d) => {deviceId, client.deviceID}.contains(d.deviceId),
|
||||||
|
);
|
||||||
final cancelPayload = <String, dynamic>{
|
final cancelPayload = <String, dynamic>{
|
||||||
'reason': 'Another device accepted the request',
|
'reason': 'Another device accepted the request',
|
||||||
'code': 'm.accepted',
|
'code': 'm.accepted',
|
||||||
};
|
};
|
||||||
makePayload(cancelPayload);
|
makePayload(cancelPayload);
|
||||||
await client.sendToDeviceEncrypted(
|
await client.sendToDeviceEncrypted(
|
||||||
devices, EventTypes.KeyVerificationCancel, cancelPayload);
|
devices,
|
||||||
|
EventTypes.KeyVerificationCancel,
|
||||||
|
cancelPayload,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
_deviceId ??= payload['from_device'];
|
_deviceId ??= payload['from_device'];
|
||||||
|
|
||||||
|
|
@ -437,7 +456,9 @@ class KeyVerification {
|
||||||
|
|
||||||
oppositePossibleMethods = List<String>.from(payload['methods']);
|
oppositePossibleMethods = List<String>.from(payload['methods']);
|
||||||
possibleMethods = _calculatePossibleMethods(
|
possibleMethods = _calculatePossibleMethods(
|
||||||
knownVerificationMethods, payload['methods']);
|
knownVerificationMethods,
|
||||||
|
payload['methods'],
|
||||||
|
);
|
||||||
if (possibleMethods.isEmpty) {
|
if (possibleMethods.isEmpty) {
|
||||||
// reject it outright
|
// reject it outright
|
||||||
await cancel('m.unknown_method');
|
await cancel('m.unknown_method');
|
||||||
|
|
@ -577,11 +598,12 @@ class KeyVerification {
|
||||||
setState(KeyVerificationState.error);
|
setState(KeyVerificationState.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> openSSSS(
|
Future<void> openSSSS({
|
||||||
{String? passphrase,
|
String? passphrase,
|
||||||
String? recoveryKey,
|
String? recoveryKey,
|
||||||
String? keyOrPassphrase,
|
String? keyOrPassphrase,
|
||||||
bool skip = false}) async {
|
bool skip = false,
|
||||||
|
}) async {
|
||||||
Future<void> next() async {
|
Future<void> next() async {
|
||||||
if (_nextAction == 'request') {
|
if (_nextAction == 'request') {
|
||||||
await sendRequest();
|
await sendRequest();
|
||||||
|
|
@ -600,9 +622,10 @@ class KeyVerification {
|
||||||
}
|
}
|
||||||
final handle = encryption.ssss.open(EventTypes.CrossSigningUserSigning);
|
final handle = encryption.ssss.open(EventTypes.CrossSigningUserSigning);
|
||||||
await handle.unlock(
|
await handle.unlock(
|
||||||
passphrase: passphrase,
|
passphrase: passphrase,
|
||||||
recoveryKey: recoveryKey,
|
recoveryKey: recoveryKey,
|
||||||
keyOrPassphrase: keyOrPassphrase);
|
keyOrPassphrase: keyOrPassphrase,
|
||||||
|
);
|
||||||
await handle.maybeCacheAll();
|
await handle.maybeCacheAll();
|
||||||
await next();
|
await next();
|
||||||
}
|
}
|
||||||
|
|
@ -611,7 +634,7 @@ class KeyVerification {
|
||||||
Future<void> acceptVerification() async {
|
Future<void> acceptVerification() async {
|
||||||
if (!(await verifyLastStep([
|
if (!(await verifyLastStep([
|
||||||
EventTypes.KeyVerificationRequest,
|
EventTypes.KeyVerificationRequest,
|
||||||
EventTypes.KeyVerificationStart
|
EventTypes.KeyVerificationStart,
|
||||||
]))) {
|
]))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -633,7 +656,9 @@ class KeyVerification {
|
||||||
// we are removing stuff only using the old possibleMethods should be ok here.
|
// we are removing stuff only using the old possibleMethods should be ok here.
|
||||||
final copyPossibleMethods = List<String>.from(possibleMethods);
|
final copyPossibleMethods = List<String>.from(possibleMethods);
|
||||||
possibleMethods = _calculatePossibleMethods(
|
possibleMethods = _calculatePossibleMethods(
|
||||||
copyKnownVerificationMethods, copyPossibleMethods);
|
copyKnownVerificationMethods,
|
||||||
|
copyPossibleMethods,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// we need to send a ready event
|
// we need to send a ready event
|
||||||
|
|
@ -657,7 +682,7 @@ class KeyVerification {
|
||||||
}
|
}
|
||||||
if (!(await verifyLastStep([
|
if (!(await verifyLastStep([
|
||||||
EventTypes.KeyVerificationRequest,
|
EventTypes.KeyVerificationRequest,
|
||||||
EventTypes.KeyVerificationStart
|
EventTypes.KeyVerificationStart,
|
||||||
]))) {
|
]))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -728,12 +753,16 @@ class KeyVerification {
|
||||||
if (requestInterval.length <= i) {
|
if (requestInterval.length <= i) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Timer(Duration(seconds: requestInterval[i]),
|
Timer(
|
||||||
() => maybeRequestSSSSSecrets(i + 1));
|
Duration(seconds: requestInterval[i]),
|
||||||
|
() => maybeRequestSSSSSecrets(i + 1),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> verifyKeysSAS(Map<String, String> keys,
|
Future<void> verifyKeysSAS(
|
||||||
Future<bool> Function(String, SignableKey) verifier) async {
|
Map<String, String> keys,
|
||||||
|
Future<bool> Function(String, SignableKey) verifier,
|
||||||
|
) async {
|
||||||
_verifiedDevices = <SignableKey>[];
|
_verifiedDevices = <SignableKey>[];
|
||||||
|
|
||||||
final userDeviceKey = client.userDeviceKeys[userId];
|
final userDeviceKey = client.userDeviceKeys[userId];
|
||||||
|
|
@ -759,7 +788,9 @@ class KeyVerification {
|
||||||
final wasUnknownSession = client.isUnknownSession;
|
final wasUnknownSession = client.isUnknownSession;
|
||||||
for (final key in _verifiedDevices) {
|
for (final key in _verifiedDevices) {
|
||||||
await key.setVerified(
|
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')) {
|
if (key is CrossSigningKey && key.usage.contains('master')) {
|
||||||
verifiedMasterKey = true;
|
verifiedMasterKey = true;
|
||||||
}
|
}
|
||||||
|
|
@ -859,7 +890,8 @@ class KeyVerification {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
Logs().e(
|
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');
|
await cancel('m.unexpected_message');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -917,9 +949,12 @@ class KeyVerification {
|
||||||
EventTypes.KeyVerificationRequest,
|
EventTypes.KeyVerificationRequest,
|
||||||
EventTypes.KeyVerificationCancel,
|
EventTypes.KeyVerificationCancel,
|
||||||
}.contains(type)) {
|
}.contains(type)) {
|
||||||
final deviceKeys = client.userDeviceKeys[userId]?.deviceKeys.values
|
final deviceKeys =
|
||||||
.where((deviceKey) => deviceKey.hasValidSignatureChain(
|
client.userDeviceKeys[userId]?.deviceKeys.values.where(
|
||||||
verifiedByTheirMasterKey: true));
|
(deviceKey) => deviceKey.hasValidSignatureChain(
|
||||||
|
verifiedByTheirMasterKey: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
if (deviceKeys != null) {
|
if (deviceKeys != null) {
|
||||||
await client.sendToDeviceEncrypted(
|
await client.sendToDeviceEncrypted(
|
||||||
|
|
@ -930,14 +965,16 @@ class KeyVerification {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Logs().e(
|
Logs().e(
|
||||||
'[Key Verification] Tried to broadcast and un-broadcastable type: $type');
|
'[Key Verification] Tried to broadcast and un-broadcastable type: $type',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (client.userDeviceKeys[userId]?.deviceKeys[deviceId] != null) {
|
if (client.userDeviceKeys[userId]?.deviceKeys[deviceId] != null) {
|
||||||
await client.sendToDeviceEncrypted(
|
await client.sendToDeviceEncrypted(
|
||||||
[client.userDeviceKeys[userId]!.deviceKeys[deviceId]!],
|
[client.userDeviceKeys[userId]!.deviceKeys[deviceId]!],
|
||||||
type,
|
type,
|
||||||
payload);
|
payload,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
Logs().e('[Key Verification] Unknown device');
|
Logs().e('[Key Verification] Unknown device');
|
||||||
}
|
}
|
||||||
|
|
@ -982,7 +1019,8 @@ class KeyVerification {
|
||||||
final otherUserMasterKey = otherUserKeys?.masterKey;
|
final otherUserMasterKey = otherUserKeys?.masterKey;
|
||||||
|
|
||||||
final secondKey = encodeBase64Unpadded(
|
final secondKey = encodeBase64Unpadded(
|
||||||
data.sublist(10 + encodedTxnLen + 32, 10 + encodedTxnLen + 32 + 32));
|
data.sublist(10 + encodedTxnLen + 32, 10 + encodedTxnLen + 32 + 32),
|
||||||
|
);
|
||||||
final randomSharedSecret =
|
final randomSharedSecret =
|
||||||
encodeBase64Unpadded(data.sublist(10 + encodedTxnLen + 32 + 32));
|
encodeBase64Unpadded(data.sublist(10 + encodedTxnLen + 32 + 32));
|
||||||
|
|
||||||
|
|
@ -991,7 +1029,8 @@ class KeyVerification {
|
||||||
.contains(remoteQrMode)) {
|
.contains(remoteQrMode)) {
|
||||||
if (!(ownMasterKey?.verified ?? false)) {
|
if (!(ownMasterKey?.verified ?? false)) {
|
||||||
Logs().e(
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1244,7 +1283,7 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
|
||||||
if (!(await request.verifyLastStep([
|
if (!(await request.verifyLastStep([
|
||||||
EventTypes.KeyVerificationReady,
|
EventTypes.KeyVerificationReady,
|
||||||
EventTypes.KeyVerificationRequest,
|
EventTypes.KeyVerificationRequest,
|
||||||
EventTypes.KeyVerificationStart
|
EventTypes.KeyVerificationStart,
|
||||||
]))) {
|
]))) {
|
||||||
return; // abort
|
return; // abort
|
||||||
}
|
}
|
||||||
|
|
@ -1257,7 +1296,7 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
|
||||||
case EventTypes.KeyVerificationAccept:
|
case EventTypes.KeyVerificationAccept:
|
||||||
if (!(await request.verifyLastStep([
|
if (!(await request.verifyLastStep([
|
||||||
EventTypes.KeyVerificationReady,
|
EventTypes.KeyVerificationReady,
|
||||||
EventTypes.KeyVerificationRequest
|
EventTypes.KeyVerificationRequest,
|
||||||
]))) {
|
]))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -1270,7 +1309,7 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
|
||||||
case 'm.key.verification.key':
|
case 'm.key.verification.key':
|
||||||
if (!(await request.verifyLastStep([
|
if (!(await request.verifyLastStep([
|
||||||
EventTypes.KeyVerificationAccept,
|
EventTypes.KeyVerificationAccept,
|
||||||
EventTypes.KeyVerificationStart
|
EventTypes.KeyVerificationStart,
|
||||||
]))) {
|
]))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -1338,7 +1377,9 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
final possibleKeyAgreementProtocols = _intersect(
|
final possibleKeyAgreementProtocols = _intersect(
|
||||||
knownKeyAgreementProtocols, payload['key_agreement_protocols']);
|
knownKeyAgreementProtocols,
|
||||||
|
payload['key_agreement_protocols'],
|
||||||
|
);
|
||||||
if (possibleKeyAgreementProtocols.isEmpty) {
|
if (possibleKeyAgreementProtocols.isEmpty) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -1349,14 +1390,17 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
|
||||||
}
|
}
|
||||||
hash = possibleHashes.first;
|
hash = possibleHashes.first;
|
||||||
final possibleMessageAuthenticationCodes = _intersect(
|
final possibleMessageAuthenticationCodes = _intersect(
|
||||||
knownHashesAuthentificationCodes,
|
knownHashesAuthentificationCodes,
|
||||||
payload['message_authentication_codes']);
|
payload['message_authentication_codes'],
|
||||||
|
);
|
||||||
if (possibleMessageAuthenticationCodes.isEmpty) {
|
if (possibleMessageAuthenticationCodes.isEmpty) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
messageAuthenticationCode = possibleMessageAuthenticationCodes.first;
|
messageAuthenticationCode = possibleMessageAuthenticationCodes.first;
|
||||||
final possibleAuthenticationTypes = _intersect(
|
final possibleAuthenticationTypes = _intersect(
|
||||||
knownAuthentificationTypes, payload['short_authentication_string']);
|
knownAuthentificationTypes,
|
||||||
|
payload['short_authentication_string'],
|
||||||
|
);
|
||||||
if (possibleAuthenticationTypes.isEmpty) {
|
if (possibleAuthenticationTypes.isEmpty) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -1394,7 +1438,9 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
|
||||||
}
|
}
|
||||||
messageAuthenticationCode = payload['message_authentication_code'];
|
messageAuthenticationCode = payload['message_authentication_code'];
|
||||||
final possibleAuthenticationTypes = _intersect(
|
final possibleAuthenticationTypes = _intersect(
|
||||||
knownAuthentificationTypes, payload['short_authentication_string']);
|
knownAuthentificationTypes,
|
||||||
|
payload['short_authentication_string'],
|
||||||
|
);
|
||||||
if (possibleAuthenticationTypes.isEmpty) {
|
if (possibleAuthenticationTypes.isEmpty) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -1497,7 +1543,9 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
|
||||||
await request.verifyKeysSAS(mac, (String mac, SignableKey key) async {
|
await request.verifyKeysSAS(mac, (String mac, SignableKey key) async {
|
||||||
return mac ==
|
return mac ==
|
||||||
_calculateMac(
|
_calculateMac(
|
||||||
key.ed25519Key!, '${baseInfo}ed25519:${key.identifier!}');
|
key.ed25519Key!,
|
||||||
|
'${baseInfo}ed25519:${key.identifier!}',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,11 +35,12 @@ class OutboundGroupSession {
|
||||||
bool get isValid => outboundGroupSession != null;
|
bool get isValid => outboundGroupSession != null;
|
||||||
final String key;
|
final String key;
|
||||||
|
|
||||||
OutboundGroupSession(
|
OutboundGroupSession({
|
||||||
{required this.devices,
|
required this.devices,
|
||||||
required this.creationTime,
|
required this.creationTime,
|
||||||
required this.outboundGroupSession,
|
required this.outboundGroupSession,
|
||||||
required this.key});
|
required this.key,
|
||||||
|
});
|
||||||
|
|
||||||
OutboundGroupSession.fromJson(Map<String, dynamic> dbEntry, this.key) {
|
OutboundGroupSession.fromJson(Map<String, dynamic> dbEntry, this.key) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -49,7 +50,8 @@ class OutboundGroupSession {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// devices is bad (old data), so just not use this session
|
// devices is bad (old data), so just not use this session
|
||||||
Logs().i(
|
Logs().i(
|
||||||
'[OutboundGroupSession] Session in database is old, not using it. $e');
|
'[OutboundGroupSession] Session in database is old, not using it. $e',
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
outboundGroupSession = olm.OutboundGroupSession();
|
outboundGroupSession = olm.OutboundGroupSession();
|
||||||
|
|
|
||||||
|
|
@ -60,17 +60,17 @@ class SessionKey {
|
||||||
/// Id of this session
|
/// Id of this session
|
||||||
String sessionId;
|
String sessionId;
|
||||||
|
|
||||||
SessionKey(
|
SessionKey({
|
||||||
{required this.content,
|
required this.content,
|
||||||
required this.inboundGroupSession,
|
required this.inboundGroupSession,
|
||||||
required this.key,
|
required this.key,
|
||||||
Map<String, String>? indexes,
|
Map<String, String>? indexes,
|
||||||
Map<String, Map<String, int>>? allowedAtIndex,
|
Map<String, Map<String, int>>? allowedAtIndex,
|
||||||
required this.roomId,
|
required this.roomId,
|
||||||
required this.sessionId,
|
required this.sessionId,
|
||||||
required this.senderKey,
|
required this.senderKey,
|
||||||
required this.senderClaimedKeys})
|
required this.senderClaimedKeys,
|
||||||
: indexes = indexes ?? <String, String>{},
|
}) : indexes = indexes ?? <String, String>{},
|
||||||
allowedAtIndex = allowedAtIndex ?? <String, Map<String, int>>{};
|
allowedAtIndex = allowedAtIndex ?? <String, Map<String, int>>{};
|
||||||
|
|
||||||
SessionKey.fromDb(StoredInboundGroupSession dbEntry, this.key)
|
SessionKey.fromDb(StoredInboundGroupSession dbEntry, this.key)
|
||||||
|
|
@ -94,7 +94,7 @@ class SessionKey {
|
||||||
?.catchMap((k, v) => MapEntry<String, String>(k, v)) ??
|
?.catchMap((k, v) => MapEntry<String, String>(k, v)) ??
|
||||||
(content['sender_claimed_ed25519_key'] is String
|
(content['sender_claimed_ed25519_key'] is String
|
||||||
? <String, String>{
|
? <String, String>{
|
||||||
'ed25519': content['sender_claimed_ed25519_key']
|
'ed25519': content['sender_claimed_ed25519_key'],
|
||||||
}
|
}
|
||||||
: <String, String>{}));
|
: <String, String>{}));
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -80,14 +80,18 @@ class DiscoveryInformation {
|
||||||
|
|
||||||
DiscoveryInformation.fromJson(Map<String, Object?> json)
|
DiscoveryInformation.fromJson(Map<String, Object?> json)
|
||||||
: mHomeserver = HomeserverInformation.fromJson(
|
: mHomeserver = HomeserverInformation.fromJson(
|
||||||
json['m.homeserver'] as Map<String, Object?>),
|
json['m.homeserver'] as Map<String, Object?>,
|
||||||
|
),
|
||||||
mIdentityServer = ((v) => v != null
|
mIdentityServer = ((v) => v != null
|
||||||
? IdentityServerInformation.fromJson(v as Map<String, Object?>)
|
? IdentityServerInformation.fromJson(v as Map<String, Object?>)
|
||||||
: null)(json['m.identity_server']),
|
: null)(json['m.identity_server']),
|
||||||
additionalProperties = Map.fromEntries(json.entries
|
additionalProperties = Map.fromEntries(
|
||||||
.where(
|
json.entries
|
||||||
(e) => !['m.homeserver', 'm.identity_server'].contains(e.key))
|
.where(
|
||||||
.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() {
|
Map<String, Object?> toJson() {
|
||||||
final mIdentityServer = this.mIdentityServer;
|
final mIdentityServer = this.mIdentityServer;
|
||||||
return {
|
return {
|
||||||
|
|
@ -462,8 +466,18 @@ class PublicRoomsChunk {
|
||||||
other.worldReadable == worldReadable);
|
other.worldReadable == worldReadable);
|
||||||
|
|
||||||
@dart.override
|
@dart.override
|
||||||
int get hashCode => Object.hash(avatarUrl, canonicalAlias, guestCanJoin,
|
int get hashCode => Object.hash(
|
||||||
joinRule, name, numJoinedMembers, roomId, roomType, topic, worldReadable);
|
avatarUrl,
|
||||||
|
canonicalAlias,
|
||||||
|
guestCanJoin,
|
||||||
|
joinRule,
|
||||||
|
name,
|
||||||
|
numJoinedMembers,
|
||||||
|
roomId,
|
||||||
|
roomType,
|
||||||
|
topic,
|
||||||
|
worldReadable,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
|
|
@ -633,17 +647,18 @@ class SpaceRoomsChunk implements PublicRoomsChunk, SpaceHierarchyRoomsChunk {
|
||||||
|
|
||||||
@dart.override
|
@dart.override
|
||||||
int get hashCode => Object.hash(
|
int get hashCode => Object.hash(
|
||||||
avatarUrl,
|
avatarUrl,
|
||||||
canonicalAlias,
|
canonicalAlias,
|
||||||
guestCanJoin,
|
guestCanJoin,
|
||||||
joinRule,
|
joinRule,
|
||||||
name,
|
name,
|
||||||
numJoinedMembers,
|
numJoinedMembers,
|
||||||
roomId,
|
roomId,
|
||||||
roomType,
|
roomType,
|
||||||
topic,
|
topic,
|
||||||
worldReadable,
|
worldReadable,
|
||||||
childrenState);
|
childrenState,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
|
|
@ -831,8 +846,8 @@ class GetRelatingEventsWithRelTypeAndEventTypeResponse {
|
||||||
});
|
});
|
||||||
|
|
||||||
GetRelatingEventsWithRelTypeAndEventTypeResponse.fromJson(
|
GetRelatingEventsWithRelTypeAndEventTypeResponse.fromJson(
|
||||||
Map<String, Object?> json)
|
Map<String, Object?> json,
|
||||||
: chunk = (json['chunk'] as List)
|
) : chunk = (json['chunk'] as List)
|
||||||
.map((v) => MatrixEvent.fromJson(v as Map<String, Object?>))
|
.map((v) => MatrixEvent.fromJson(v as Map<String, Object?>))
|
||||||
.toList(),
|
.toList(),
|
||||||
nextBatch = ((v) => v != null ? v as String : null)(json['next_batch']),
|
nextBatch = ((v) => v != null ? v as String : null)(json['next_batch']),
|
||||||
|
|
@ -1320,8 +1335,10 @@ class WhoIsInfo {
|
||||||
|
|
||||||
WhoIsInfo.fromJson(Map<String, Object?> json)
|
WhoIsInfo.fromJson(Map<String, Object?> json)
|
||||||
: devices = ((v) => v != null
|
: devices = ((v) => v != null
|
||||||
? (v as Map<String, Object?>).map((k, v) =>
|
? (v as Map<String, Object?>).map(
|
||||||
MapEntry(k, DeviceInfo.fromJson(v as Map<String, Object?>)))
|
(k, v) =>
|
||||||
|
MapEntry(k, DeviceInfo.fromJson(v as Map<String, Object?>)),
|
||||||
|
)
|
||||||
: null)(json['devices']),
|
: null)(json['devices']),
|
||||||
userId = ((v) => v != null ? v as String : null)(json['user_id']);
|
userId = ((v) => v != null ? v as String : null)(json['user_id']);
|
||||||
Map<String, Object?> toJson() {
|
Map<String, Object?> toJson() {
|
||||||
|
|
@ -1398,8 +1415,10 @@ class RoomVersionsCapability {
|
||||||
});
|
});
|
||||||
|
|
||||||
RoomVersionsCapability.fromJson(Map<String, Object?> json)
|
RoomVersionsCapability.fromJson(Map<String, Object?> json)
|
||||||
: available = (json['available'] as Map<String, Object?>).map((k, v) =>
|
: available = (json['available'] as Map<String, Object?>).map(
|
||||||
MapEntry(k, RoomVersionAvailable.values.fromString(v as String)!)),
|
(k, v) =>
|
||||||
|
MapEntry(k, RoomVersionAvailable.values.fromString(v as String)!),
|
||||||
|
),
|
||||||
default$ = json['default'] as String;
|
default$ = json['default'] as String;
|
||||||
Map<String, Object?> toJson() => {
|
Map<String, Object?> toJson() => {
|
||||||
'available': available.map((k, v) => MapEntry(k, v.name)),
|
'available': available.map((k, v) => MapEntry(k, v.name)),
|
||||||
|
|
@ -1456,16 +1475,20 @@ class Capabilities {
|
||||||
mSetDisplayname = ((v) => v != null
|
mSetDisplayname = ((v) => v != null
|
||||||
? BooleanCapability.fromJson(v as Map<String, Object?>)
|
? BooleanCapability.fromJson(v as Map<String, Object?>)
|
||||||
: null)(json['m.set_displayname']),
|
: null)(json['m.set_displayname']),
|
||||||
additionalProperties = Map.fromEntries(json.entries
|
additionalProperties = Map.fromEntries(
|
||||||
.where((e) => ![
|
json.entries
|
||||||
|
.where(
|
||||||
|
(e) => ![
|
||||||
'm.3pid_changes',
|
'm.3pid_changes',
|
||||||
'm.change_password',
|
'm.change_password',
|
||||||
'm.get_login_token',
|
'm.get_login_token',
|
||||||
'm.room_versions',
|
'm.room_versions',
|
||||||
'm.set_avatar_url',
|
'm.set_avatar_url',
|
||||||
'm.set_displayname'
|
'm.set_displayname',
|
||||||
].contains(e.key))
|
].contains(e.key),
|
||||||
.map((e) => MapEntry(e.key, e.value)));
|
)
|
||||||
|
.map((e) => MapEntry(e.key, e.value)),
|
||||||
|
);
|
||||||
Map<String, Object?> toJson() {
|
Map<String, Object?> toJson() {
|
||||||
final m3pidChanges = this.m3pidChanges;
|
final m3pidChanges = this.m3pidChanges;
|
||||||
final mChangePassword = this.mChangePassword;
|
final mChangePassword = this.mChangePassword;
|
||||||
|
|
@ -1519,8 +1542,14 @@ class Capabilities {
|
||||||
other.mSetDisplayname == mSetDisplayname);
|
other.mSetDisplayname == mSetDisplayname);
|
||||||
|
|
||||||
@dart.override
|
@dart.override
|
||||||
int get hashCode => Object.hash(m3pidChanges, mChangePassword, mGetLoginToken,
|
int get hashCode => Object.hash(
|
||||||
mRoomVersions, mSetAvatarUrl, mSetDisplayname);
|
m3pidChanges,
|
||||||
|
mChangePassword,
|
||||||
|
mGetLoginToken,
|
||||||
|
mRoomVersions,
|
||||||
|
mSetAvatarUrl,
|
||||||
|
mSetDisplayname,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
|
|
@ -1858,11 +1887,12 @@ class ThirdPartySigned {
|
||||||
ThirdPartySigned.fromJson(Map<String, Object?> json)
|
ThirdPartySigned.fromJson(Map<String, Object?> json)
|
||||||
: mxid = json['mxid'] as String,
|
: mxid = json['mxid'] as String,
|
||||||
sender = json['sender'] as String,
|
sender = json['sender'] as String,
|
||||||
signatures = (json['signatures'] as Map<String, Object?>).map((k, v) =>
|
signatures = (json['signatures'] as Map<String, Object?>).map(
|
||||||
MapEntry(
|
(k, v) => MapEntry(
|
||||||
k,
|
k,
|
||||||
(v as Map<String, Object?>)
|
(v as Map<String, Object?>).map((k, v) => MapEntry(k, v as String)),
|
||||||
.map((k, v) => MapEntry(k, v as String)))),
|
),
|
||||||
|
),
|
||||||
token = json['token'] as String;
|
token = json['token'] as String;
|
||||||
Map<String, Object?> toJson() => {
|
Map<String, Object?> toJson() => {
|
||||||
'mxid': mxid,
|
'mxid': mxid,
|
||||||
|
|
@ -1957,10 +1987,12 @@ class ClaimKeysResponse {
|
||||||
.map((k, v) => MapEntry(k, v as Map<String, Object?>))
|
.map((k, v) => MapEntry(k, v as Map<String, Object?>))
|
||||||
: null)(json['failures']),
|
: null)(json['failures']),
|
||||||
oneTimeKeys = (json['one_time_keys'] as Map<String, Object?>).map(
|
oneTimeKeys = (json['one_time_keys'] as Map<String, Object?>).map(
|
||||||
(k, v) => MapEntry(
|
(k, v) => MapEntry(
|
||||||
k,
|
k,
|
||||||
(v as Map<String, Object?>)
|
(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() {
|
Map<String, Object?> toJson() {
|
||||||
final failures = this.failures;
|
final failures = this.failures;
|
||||||
return {
|
return {
|
||||||
|
|
@ -2014,26 +2046,45 @@ class QueryKeysResponse {
|
||||||
|
|
||||||
QueryKeysResponse.fromJson(Map<String, Object?> json)
|
QueryKeysResponse.fromJson(Map<String, Object?> json)
|
||||||
: deviceKeys = ((v) => v != null
|
: deviceKeys = ((v) => v != null
|
||||||
? (v as Map<String, Object?>).map((k, v) => MapEntry(
|
? (v as Map<String, Object?>).map(
|
||||||
k,
|
(k, v) => MapEntry(
|
||||||
(v as Map<String, Object?>).map((k, v) => MapEntry(
|
k,
|
||||||
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']),
|
: null)(json['device_keys']),
|
||||||
failures = ((v) => v != null
|
failures = ((v) => v != null
|
||||||
? (v as Map<String, Object?>)
|
? (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?>))
|
||||||
: null)(json['failures']),
|
: null)(json['failures']),
|
||||||
masterKeys = ((v) => v != null
|
masterKeys = ((v) => v != null
|
||||||
? (v as Map<String, Object?>).map((k, v) => MapEntry(
|
? (v as Map<String, Object?>).map(
|
||||||
k, MatrixCrossSigningKey.fromJson(v as Map<String, Object?>)))
|
(k, v) => MapEntry(
|
||||||
|
k,
|
||||||
|
MatrixCrossSigningKey.fromJson(v as Map<String, Object?>),
|
||||||
|
),
|
||||||
|
)
|
||||||
: null)(json['master_keys']),
|
: null)(json['master_keys']),
|
||||||
selfSigningKeys = ((v) => v != null
|
selfSigningKeys = ((v) => v != null
|
||||||
? (v as Map<String, Object?>).map((k, v) => MapEntry(
|
? (v as Map<String, Object?>).map(
|
||||||
k, MatrixCrossSigningKey.fromJson(v as Map<String, Object?>)))
|
(k, v) => MapEntry(
|
||||||
|
k,
|
||||||
|
MatrixCrossSigningKey.fromJson(v as Map<String, Object?>),
|
||||||
|
),
|
||||||
|
)
|
||||||
: null)(json['self_signing_keys']),
|
: null)(json['self_signing_keys']),
|
||||||
userSigningKeys = ((v) => v != null
|
userSigningKeys = ((v) => v != null
|
||||||
? (v as Map<String, Object?>).map((k, v) => MapEntry(
|
? (v as Map<String, Object?>).map(
|
||||||
k, MatrixCrossSigningKey.fromJson(v as Map<String, Object?>)))
|
(k, v) => MapEntry(
|
||||||
|
k,
|
||||||
|
MatrixCrossSigningKey.fromJson(v as Map<String, Object?>),
|
||||||
|
),
|
||||||
|
)
|
||||||
: null)(json['user_signing_keys']);
|
: null)(json['user_signing_keys']);
|
||||||
Map<String, Object?> toJson() {
|
Map<String, Object?> toJson() {
|
||||||
final deviceKeys = this.deviceKeys;
|
final deviceKeys = this.deviceKeys;
|
||||||
|
|
@ -2044,7 +2095,8 @@ class QueryKeysResponse {
|
||||||
return {
|
return {
|
||||||
if (deviceKeys != null)
|
if (deviceKeys != null)
|
||||||
'device_keys': deviceKeys.map(
|
'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 (failures != null) 'failures': failures.map((k, v) => MapEntry(k, v)),
|
||||||
if (masterKeys != null)
|
if (masterKeys != null)
|
||||||
'master_keys': masterKeys.map((k, v) => MapEntry(k, v.toJson())),
|
'master_keys': masterKeys.map((k, v) => MapEntry(k, v.toJson())),
|
||||||
|
|
@ -2107,7 +2159,12 @@ class QueryKeysResponse {
|
||||||
|
|
||||||
@dart.override
|
@dart.override
|
||||||
int get hashCode => Object.hash(
|
int get hashCode => Object.hash(
|
||||||
deviceKeys, failures, masterKeys, selfSigningKeys, userSigningKeys);
|
deviceKeys,
|
||||||
|
failures,
|
||||||
|
masterKeys,
|
||||||
|
selfSigningKeys,
|
||||||
|
userSigningKeys,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
|
|
@ -2123,9 +2180,11 @@ class LoginFlow {
|
||||||
: getLoginToken =
|
: getLoginToken =
|
||||||
((v) => v != null ? v as bool : null)(json['get_login_token']),
|
((v) => v != null ? v as bool : null)(json['get_login_token']),
|
||||||
type = json['type'] as String,
|
type = json['type'] as String,
|
||||||
additionalProperties = Map.fromEntries(json.entries
|
additionalProperties = Map.fromEntries(
|
||||||
.where((e) => !['get_login_token', 'type'].contains(e.key))
|
json.entries
|
||||||
.map((e) => MapEntry(e.key, e.value)));
|
.where((e) => !['get_login_token', 'type'].contains(e.key))
|
||||||
|
.map((e) => MapEntry(e.key, e.value)),
|
||||||
|
);
|
||||||
Map<String, Object?> toJson() {
|
Map<String, Object?> toJson() {
|
||||||
final getLoginToken = this.getLoginToken;
|
final getLoginToken = this.getLoginToken;
|
||||||
return {
|
return {
|
||||||
|
|
@ -2255,8 +2314,15 @@ class LoginResponse {
|
||||||
other.wellKnown == wellKnown);
|
other.wellKnown == wellKnown);
|
||||||
|
|
||||||
@dart.override
|
@dart.override
|
||||||
int get hashCode => Object.hash(accessToken, deviceId, expiresInMs,
|
int get hashCode => Object.hash(
|
||||||
homeServer, refreshToken, userId, wellKnown);
|
accessToken,
|
||||||
|
deviceId,
|
||||||
|
expiresInMs,
|
||||||
|
homeServer,
|
||||||
|
refreshToken,
|
||||||
|
userId,
|
||||||
|
wellKnown,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
|
|
@ -2663,9 +2729,11 @@ class PusherData {
|
||||||
PusherData.fromJson(Map<String, Object?> json)
|
PusherData.fromJson(Map<String, Object?> json)
|
||||||
: format = ((v) => v != null ? v as String : null)(json['format']),
|
: format = ((v) => v != null ? v as String : null)(json['format']),
|
||||||
url = ((v) => v != null ? Uri.parse(v as String) : null)(json['url']),
|
url = ((v) => v != null ? Uri.parse(v as String) : null)(json['url']),
|
||||||
additionalProperties = Map.fromEntries(json.entries
|
additionalProperties = Map.fromEntries(
|
||||||
.where((e) => !['format', 'url'].contains(e.key))
|
json.entries
|
||||||
.map((e) => MapEntry(e.key, e.value)));
|
.where((e) => !['format', 'url'].contains(e.key))
|
||||||
|
.map((e) => MapEntry(e.key, e.value)),
|
||||||
|
);
|
||||||
Map<String, Object?> toJson() {
|
Map<String, Object?> toJson() {
|
||||||
final format = this.format;
|
final format = this.format;
|
||||||
final url = this.url;
|
final url = this.url;
|
||||||
|
|
@ -2824,8 +2892,16 @@ class Pusher implements PusherId {
|
||||||
other.profileTag == profileTag);
|
other.profileTag == profileTag);
|
||||||
|
|
||||||
@dart.override
|
@dart.override
|
||||||
int get hashCode => Object.hash(appId, pushkey, appDisplayName, data,
|
int get hashCode => Object.hash(
|
||||||
deviceDisplayName, kind, lang, profileTag);
|
appId,
|
||||||
|
pushkey,
|
||||||
|
appDisplayName,
|
||||||
|
data,
|
||||||
|
deviceDisplayName,
|
||||||
|
kind,
|
||||||
|
lang,
|
||||||
|
profileTag,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
|
|
@ -3316,7 +3392,13 @@ class RegisterResponse {
|
||||||
|
|
||||||
@dart.override
|
@dart.override
|
||||||
int get hashCode => Object.hash(
|
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)
|
RoomKeyBackup.fromJson(Map<String, Object?> json)
|
||||||
: sessions = (json['sessions'] as Map<String, Object?>).map((k, v) =>
|
: sessions = (json['sessions'] as Map<String, Object?>).map(
|
||||||
MapEntry(k, KeyBackupData.fromJson(v as Map<String, Object?>)));
|
(k, v) =>
|
||||||
|
MapEntry(k, KeyBackupData.fromJson(v as Map<String, Object?>)),
|
||||||
|
);
|
||||||
Map<String, Object?> toJson() => {
|
Map<String, Object?> toJson() => {
|
||||||
'sessions': sessions.map((k, v) => MapEntry(k, v.toJson())),
|
'sessions': sessions.map((k, v) => MapEntry(k, v.toJson())),
|
||||||
};
|
};
|
||||||
|
|
@ -3442,8 +3526,10 @@ class RoomKeys {
|
||||||
});
|
});
|
||||||
|
|
||||||
RoomKeys.fromJson(Map<String, Object?> json)
|
RoomKeys.fromJson(Map<String, Object?> json)
|
||||||
: rooms = (json['rooms'] as Map<String, Object?>).map((k, v) =>
|
: rooms = (json['rooms'] as Map<String, Object?>).map(
|
||||||
MapEntry(k, RoomKeyBackup.fromJson(v as Map<String, Object?>)));
|
(k, v) =>
|
||||||
|
MapEntry(k, RoomKeyBackup.fromJson(v as Map<String, Object?>)),
|
||||||
|
);
|
||||||
Map<String, Object?> toJson() => {
|
Map<String, Object?> toJson() => {
|
||||||
'rooms': rooms.map((k, v) => MapEntry(k, v.toJson())),
|
'rooms': rooms.map((k, v) => MapEntry(k, v.toJson())),
|
||||||
};
|
};
|
||||||
|
|
@ -4039,8 +4125,14 @@ class RoomEventFilter {
|
||||||
other.unreadThreadNotifications == unreadThreadNotifications);
|
other.unreadThreadNotifications == unreadThreadNotifications);
|
||||||
|
|
||||||
@dart.override
|
@dart.override
|
||||||
int get hashCode => Object.hash(containsUrl, includeRedundantMembers,
|
int get hashCode => Object.hash(
|
||||||
lazyLoadMembers, notRooms, rooms, unreadThreadNotifications);
|
containsUrl,
|
||||||
|
includeRedundantMembers,
|
||||||
|
lazyLoadMembers,
|
||||||
|
notRooms,
|
||||||
|
rooms,
|
||||||
|
unreadThreadNotifications,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
|
|
@ -4192,17 +4284,18 @@ class SearchFilter implements EventFilter, RoomEventFilter {
|
||||||
|
|
||||||
@dart.override
|
@dart.override
|
||||||
int get hashCode => Object.hash(
|
int get hashCode => Object.hash(
|
||||||
limit,
|
limit,
|
||||||
notSenders,
|
notSenders,
|
||||||
notTypes,
|
notTypes,
|
||||||
senders,
|
senders,
|
||||||
types,
|
types,
|
||||||
containsUrl,
|
containsUrl,
|
||||||
includeRedundantMembers,
|
includeRedundantMembers,
|
||||||
lazyLoadMembers,
|
lazyLoadMembers,
|
||||||
notRooms,
|
notRooms,
|
||||||
rooms,
|
rooms,
|
||||||
unreadThreadNotifications);
|
unreadThreadNotifications,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
|
|
@ -4393,7 +4486,14 @@ class RoomEventsCriteria {
|
||||||
|
|
||||||
@dart.override
|
@dart.override
|
||||||
int get hashCode => Object.hash(
|
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()
|
.toList()
|
||||||
: null)(json['events_before']),
|
: null)(json['events_before']),
|
||||||
profileInfo = ((v) => v != null
|
profileInfo = ((v) => v != null
|
||||||
? (v as Map<String, Object?>).map((k, v) =>
|
? (v as Map<String, Object?>).map(
|
||||||
MapEntry(k, UserProfile.fromJson(v as Map<String, Object?>)))
|
(k, v) => MapEntry(
|
||||||
|
k,
|
||||||
|
UserProfile.fromJson(v as Map<String, Object?>),
|
||||||
|
),
|
||||||
|
)
|
||||||
: null)(json['profile_info']),
|
: null)(json['profile_info']),
|
||||||
start = ((v) => v != null ? v as String : null)(json['start']);
|
start = ((v) => v != null ? v as String : null)(json['start']);
|
||||||
Map<String, Object?> toJson() {
|
Map<String, Object?> toJson() {
|
||||||
|
|
@ -4667,10 +4771,17 @@ class ResultRoomEvents {
|
||||||
ResultRoomEvents.fromJson(Map<String, Object?> json)
|
ResultRoomEvents.fromJson(Map<String, Object?> json)
|
||||||
: count = ((v) => v != null ? v as int : null)(json['count']),
|
: count = ((v) => v != null ? v as int : null)(json['count']),
|
||||||
groups = ((v) => v != null
|
groups = ((v) => v != null
|
||||||
? (v as Map<String, Object?>).map((k, v) => MapEntry(
|
? (v as Map<String, Object?>).map(
|
||||||
k,
|
(k, v) => MapEntry(
|
||||||
(v as Map<String, Object?>).map((k, v) => MapEntry(
|
k,
|
||||||
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']),
|
: null)(json['groups']),
|
||||||
highlights = ((v) => v != null
|
highlights = ((v) => v != null
|
||||||
? (v as List).map((v) => v as String).toList()
|
? (v as List).map((v) => v as String).toList()
|
||||||
|
|
@ -4682,11 +4793,16 @@ class ResultRoomEvents {
|
||||||
.toList()
|
.toList()
|
||||||
: null)(json['results']),
|
: null)(json['results']),
|
||||||
state = ((v) => v != null
|
state = ((v) => v != null
|
||||||
? (v as Map<String, Object?>).map((k, v) => MapEntry(
|
? (v as Map<String, Object?>).map(
|
||||||
k,
|
(k, v) => MapEntry(
|
||||||
(v as List)
|
k,
|
||||||
.map((v) => MatrixEvent.fromJson(v as Map<String, Object?>))
|
(v as List)
|
||||||
.toList()))
|
.map(
|
||||||
|
(v) => MatrixEvent.fromJson(v as Map<String, Object?>),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
)
|
||||||
: null)(json['state']);
|
: null)(json['state']);
|
||||||
Map<String, Object?> toJson() {
|
Map<String, Object?> toJson() {
|
||||||
final count = this.count;
|
final count = this.count;
|
||||||
|
|
@ -4699,7 +4815,8 @@ class ResultRoomEvents {
|
||||||
if (count != null) 'count': count,
|
if (count != null) 'count': count,
|
||||||
if (groups != null)
|
if (groups != null)
|
||||||
'groups': groups.map(
|
'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 (highlights != null) 'highlights': highlights.map((v) => v).toList(),
|
||||||
if (nextBatch != null) 'next_batch': nextBatch,
|
if (nextBatch != null) 'next_batch': nextBatch,
|
||||||
if (results != null) 'results': results.map((v) => v.toJson()).toList(),
|
if (results != null) 'results': results.map((v) => v.toJson()).toList(),
|
||||||
|
|
@ -4797,7 +4914,8 @@ class SearchResults {
|
||||||
|
|
||||||
SearchResults.fromJson(Map<String, Object?> json)
|
SearchResults.fromJson(Map<String, Object?> json)
|
||||||
: searchCategories = ResultCategories.fromJson(
|
: searchCategories = ResultCategories.fromJson(
|
||||||
json['search_categories'] as Map<String, Object?>);
|
json['search_categories'] as Map<String, Object?>,
|
||||||
|
);
|
||||||
Map<String, Object?> toJson() => {
|
Map<String, Object?> toJson() => {
|
||||||
'search_categories': searchCategories.toJson(),
|
'search_categories': searchCategories.toJson(),
|
||||||
};
|
};
|
||||||
|
|
@ -4957,8 +5075,9 @@ class Protocol {
|
||||||
});
|
});
|
||||||
|
|
||||||
Protocol.fromJson(Map<String, Object?> json)
|
Protocol.fromJson(Map<String, Object?> json)
|
||||||
: fieldTypes = (json['field_types'] as Map<String, Object?>).map((k, v) =>
|
: fieldTypes = (json['field_types'] as Map<String, Object?>).map(
|
||||||
MapEntry(k, FieldType.fromJson(v as Map<String, Object?>))),
|
(k, v) => MapEntry(k, FieldType.fromJson(v as Map<String, Object?>)),
|
||||||
|
),
|
||||||
icon = json['icon'] as String,
|
icon = json['icon'] as String,
|
||||||
instances = (json['instances'] as List)
|
instances = (json['instances'] as List)
|
||||||
.map((v) => ProtocolInstance.fromJson(v as Map<String, Object?>))
|
.map((v) => ProtocolInstance.fromJson(v as Map<String, Object?>))
|
||||||
|
|
@ -5218,17 +5337,18 @@ class StateFilter implements EventFilter, RoomEventFilter {
|
||||||
|
|
||||||
@dart.override
|
@dart.override
|
||||||
int get hashCode => Object.hash(
|
int get hashCode => Object.hash(
|
||||||
limit,
|
limit,
|
||||||
notSenders,
|
notSenders,
|
||||||
notTypes,
|
notTypes,
|
||||||
senders,
|
senders,
|
||||||
types,
|
types,
|
||||||
containsUrl,
|
containsUrl,
|
||||||
includeRedundantMembers,
|
includeRedundantMembers,
|
||||||
lazyLoadMembers,
|
lazyLoadMembers,
|
||||||
notRooms,
|
notRooms,
|
||||||
rooms,
|
rooms,
|
||||||
unreadThreadNotifications);
|
unreadThreadNotifications,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
|
|
@ -5320,7 +5440,14 @@ class RoomFilter {
|
||||||
|
|
||||||
@dart.override
|
@dart.override
|
||||||
int get hashCode => Object.hash(
|
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)
|
Tag.fromJson(Map<String, Object?> json)
|
||||||
: order =
|
: order =
|
||||||
((v) => v != null ? (v as num).toDouble() : null)(json['order']),
|
((v) => v != null ? (v as num).toDouble() : null)(json['order']),
|
||||||
additionalProperties = Map.fromEntries(json.entries
|
additionalProperties = Map.fromEntries(
|
||||||
.where((e) => !['order'].contains(e.key))
|
json.entries
|
||||||
.map((e) => MapEntry(e.key, e.value)));
|
.where((e) => !['order'].contains(e.key))
|
||||||
|
.map((e) => MapEntry(e.key, e.value)),
|
||||||
|
);
|
||||||
Map<String, Object?> toJson() {
|
Map<String, Object?> toJson() {
|
||||||
final order = this.order;
|
final order = this.order;
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -202,19 +202,29 @@ class MatrixApi extends Api {
|
||||||
|
|
||||||
@Deprecated('Use [deleteRoomKeyBySessionId] instead')
|
@Deprecated('Use [deleteRoomKeyBySessionId] instead')
|
||||||
Future<RoomKeysUpdateResponse> deleteRoomKeysBySessionId(
|
Future<RoomKeysUpdateResponse> deleteRoomKeysBySessionId(
|
||||||
String roomId, String sessionId, String version) async {
|
String roomId,
|
||||||
|
String sessionId,
|
||||||
|
String version,
|
||||||
|
) async {
|
||||||
return deleteRoomKeyBySessionId(roomId, sessionId, version);
|
return deleteRoomKeyBySessionId(roomId, sessionId, version);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deprecated('Use [deleteRoomKeyBySessionId] instead')
|
@Deprecated('Use [deleteRoomKeyBySessionId] instead')
|
||||||
Future<RoomKeysUpdateResponse> putRoomKeysBySessionId(String roomId,
|
Future<RoomKeysUpdateResponse> putRoomKeysBySessionId(
|
||||||
String sessionId, String version, KeyBackupData data) async {
|
String roomId,
|
||||||
|
String sessionId,
|
||||||
|
String version,
|
||||||
|
KeyBackupData data,
|
||||||
|
) async {
|
||||||
return putRoomKeyBySessionId(roomId, sessionId, version, data);
|
return putRoomKeyBySessionId(roomId, sessionId, version, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deprecated('Use [getRoomKeyBySessionId] instead')
|
@Deprecated('Use [getRoomKeyBySessionId] instead')
|
||||||
Future<KeyBackupData> getRoomKeysBySessionId(
|
Future<KeyBackupData> getRoomKeysBySessionId(
|
||||||
String roomId, String sessionId, String version) async {
|
String roomId,
|
||||||
|
String sessionId,
|
||||||
|
String version,
|
||||||
|
) async {
|
||||||
return getRoomKeyBySessionId(roomId, sessionId, version);
|
return getRoomKeyBySessionId(roomId, sessionId, version);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,16 +33,19 @@ class AuthenticationPassword extends AuthenticationData {
|
||||||
/// Identifier classes extending AuthenticationIdentifier.
|
/// Identifier classes extending AuthenticationIdentifier.
|
||||||
AuthenticationIdentifier identifier;
|
AuthenticationIdentifier identifier;
|
||||||
|
|
||||||
AuthenticationPassword(
|
AuthenticationPassword({
|
||||||
{super.session, required this.password, required this.identifier})
|
super.session,
|
||||||
: super(
|
required this.password,
|
||||||
|
required this.identifier,
|
||||||
|
}) : super(
|
||||||
type: AuthenticationTypes.password,
|
type: AuthenticationTypes.password,
|
||||||
);
|
);
|
||||||
|
|
||||||
AuthenticationPassword.fromJson(super.json)
|
AuthenticationPassword.fromJson(super.json)
|
||||||
: password = json['password'] as String,
|
: password = json['password'] as String,
|
||||||
identifier = AuthenticationIdentifier.subFromJson(
|
identifier = AuthenticationIdentifier.subFromJson(
|
||||||
json['identifier'] as Map<String, Object?>),
|
json['identifier'] as Map<String, Object?>,
|
||||||
|
),
|
||||||
super.fromJson();
|
super.fromJson();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -28,9 +28,10 @@ class AuthenticationThirdPartyIdentifier extends AuthenticationIdentifier {
|
||||||
String medium;
|
String medium;
|
||||||
String address;
|
String address;
|
||||||
|
|
||||||
AuthenticationThirdPartyIdentifier(
|
AuthenticationThirdPartyIdentifier({
|
||||||
{required this.medium, required this.address})
|
required this.medium,
|
||||||
: super(type: AuthenticationIdentifierTypes.thirdParty);
|
required this.address,
|
||||||
|
}) : super(type: AuthenticationIdentifierTypes.thirdParty);
|
||||||
|
|
||||||
AuthenticationThirdPartyIdentifier.fromJson(super.json)
|
AuthenticationThirdPartyIdentifier.fromJson(super.json)
|
||||||
: medium = json['medium'] as String,
|
: medium = json['medium'] as String,
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,11 @@ import 'package:matrix/matrix_api_lite/model/auth/authentication_data.dart';
|
||||||
class AuthenticationThreePidCreds extends AuthenticationData {
|
class AuthenticationThreePidCreds extends AuthenticationData {
|
||||||
late ThreepidCreds threepidCreds;
|
late ThreepidCreds threepidCreds;
|
||||||
|
|
||||||
AuthenticationThreePidCreds(
|
AuthenticationThreePidCreds({
|
||||||
{super.session, required String super.type, required this.threepidCreds});
|
super.session,
|
||||||
|
required String super.type,
|
||||||
|
required this.threepidCreds,
|
||||||
|
});
|
||||||
|
|
||||||
AuthenticationThreePidCreds.fromJson(Map<String, Object?> json)
|
AuthenticationThreePidCreds.fromJson(Map<String, Object?> json)
|
||||||
: super.fromJson(json) {
|
: super.fromJson(json) {
|
||||||
|
|
@ -55,11 +58,12 @@ class ThreepidCreds {
|
||||||
String? idServer;
|
String? idServer;
|
||||||
String? idAccessToken;
|
String? idAccessToken;
|
||||||
|
|
||||||
ThreepidCreds(
|
ThreepidCreds({
|
||||||
{required this.sid,
|
required this.sid,
|
||||||
required this.clientSecret,
|
required this.clientSecret,
|
||||||
this.idServer,
|
this.idServer,
|
||||||
this.idAccessToken});
|
this.idAccessToken,
|
||||||
|
});
|
||||||
|
|
||||||
ThreepidCreds.fromJson(Map<String, Object?> json)
|
ThreepidCreds.fromJson(Map<String, Object?> json)
|
||||||
: sid = json['sid'] as String,
|
: sid = json['sid'] as String,
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,11 @@ import 'package:matrix/matrix_api_lite/model/basic_event.dart';
|
||||||
class BasicEventWithSender extends BasicEvent {
|
class BasicEventWithSender extends BasicEvent {
|
||||||
String senderId;
|
String senderId;
|
||||||
|
|
||||||
BasicEventWithSender(
|
BasicEventWithSender({
|
||||||
{required super.type, required super.content, required this.senderId});
|
required super.type,
|
||||||
|
required super.content,
|
||||||
|
required this.senderId,
|
||||||
|
});
|
||||||
|
|
||||||
BasicEventWithSender.fromJson(super.json)
|
BasicEventWithSender.fromJson(super.json)
|
||||||
: senderId = json['sender'] as String,
|
: senderId = json['sender'] as String,
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,8 @@ class ChildrenState extends StrippedStateEvent {
|
||||||
|
|
||||||
ChildrenState.fromJson(super.json)
|
ChildrenState.fromJson(super.json)
|
||||||
: originServerTs = DateTime.fromMillisecondsSinceEpoch(
|
: originServerTs = DateTime.fromMillisecondsSinceEpoch(
|
||||||
json['origin_server_ts'] as int),
|
json['origin_server_ts'] as int,
|
||||||
|
),
|
||||||
super.fromJson();
|
super.fromJson();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,9 @@ class ForwardedRoomKeyContent extends RoomKeyContent {
|
||||||
senderClaimedEd25519Key =
|
senderClaimedEd25519Key =
|
||||||
json.tryGet('sender_claimed_ed25519_key', TryGet.required) ?? '',
|
json.tryGet('sender_claimed_ed25519_key', TryGet.required) ?? '',
|
||||||
forwardingCurve25519KeyChain = json.tryGetList(
|
forwardingCurve25519KeyChain = json.tryGetList(
|
||||||
'forwarding_curve25519_key_chain', TryGet.required) ??
|
'forwarding_curve25519_key_chain',
|
||||||
|
TryGet.required,
|
||||||
|
) ??
|
||||||
[],
|
[],
|
||||||
super.fromJson();
|
super.fromJson();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,21 +36,27 @@ enum ImagePackUsage {
|
||||||
}
|
}
|
||||||
|
|
||||||
List<ImagePackUsage>? imagePackUsageFromJson(List<String>? json) => json
|
List<ImagePackUsage>? imagePackUsageFromJson(List<String>? json) => json
|
||||||
?.map((v) => {
|
?.map(
|
||||||
'sticker': ImagePackUsage.sticker,
|
(v) => {
|
||||||
'emoticon': ImagePackUsage.emoticon,
|
'sticker': ImagePackUsage.sticker,
|
||||||
}[v])
|
'emoticon': ImagePackUsage.emoticon,
|
||||||
|
}[v],
|
||||||
|
)
|
||||||
.whereType<ImagePackUsage>()
|
.whereType<ImagePackUsage>()
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
List<String> imagePackUsageToJson(
|
List<String> imagePackUsageToJson(
|
||||||
List<ImagePackUsage>? usage, List<String>? prevUsage) {
|
List<ImagePackUsage>? usage,
|
||||||
|
List<String>? prevUsage,
|
||||||
|
) {
|
||||||
final knownUsages = <String>{'sticker', 'emoticon'};
|
final knownUsages = <String>{'sticker', 'emoticon'};
|
||||||
final usagesStr = usage
|
final usagesStr = usage
|
||||||
?.map((v) => {
|
?.map(
|
||||||
ImagePackUsage.sticker: 'sticker',
|
(v) => {
|
||||||
ImagePackUsage.emoticon: 'emoticon',
|
ImagePackUsage.sticker: 'sticker',
|
||||||
}[v])
|
ImagePackUsage.emoticon: 'emoticon',
|
||||||
|
}[v],
|
||||||
|
)
|
||||||
.whereType<String>()
|
.whereType<String>()
|
||||||
.toList() ??
|
.toList() ??
|
||||||
[];
|
[];
|
||||||
|
|
@ -74,30 +80,42 @@ class ImagePackContent {
|
||||||
ImagePackContent({required this.images, required this.pack}) : _json = {};
|
ImagePackContent({required this.images, required this.pack}) : _json = {};
|
||||||
|
|
||||||
ImagePackContent.fromJson(Map<String, Object?> json)
|
ImagePackContent.fromJson(Map<String, Object?> json)
|
||||||
: _json = Map.fromEntries(json.entries.where(
|
: _json = Map.fromEntries(
|
||||||
(e) => !['images', 'pack', 'emoticons', 'short'].contains(e.key))),
|
json.entries.where(
|
||||||
|
(e) => !['images', 'pack', 'emoticons', 'short'].contains(e.key),
|
||||||
|
),
|
||||||
|
),
|
||||||
pack = ImagePackPackContent.fromJson(
|
pack = ImagePackPackContent.fromJson(
|
||||||
json.tryGetMap<String, Object?>('pack') ?? {}),
|
json.tryGetMap<String, Object?>('pack') ?? {},
|
||||||
images = json.tryGetMap<String, Object?>('images')?.catchMap((k, v) =>
|
),
|
||||||
MapEntry(
|
images = json.tryGetMap<String, Object?>('images')?.catchMap(
|
||||||
|
(k, v) => MapEntry(
|
||||||
k,
|
k,
|
||||||
ImagePackImageContent.fromJson(
|
ImagePackImageContent.fromJson(
|
||||||
v as Map<String, Object?>))) ??
|
v as Map<String, Object?>,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
) ??
|
||||||
// the "emoticons" key needs a small migration on the key, ":string:" --> "string"
|
// the "emoticons" key needs a small migration on the key, ":string:" --> "string"
|
||||||
json.tryGetMap<String, Object?>('emoticons')?.catchMap((k, v) =>
|
json.tryGetMap<String, Object?>('emoticons')?.catchMap(
|
||||||
MapEntry(
|
(k, v) => MapEntry(
|
||||||
k.startsWith(':') && k.endsWith(':')
|
k.startsWith(':') && k.endsWith(':')
|
||||||
? k.substring(1, k.length - 1)
|
? k.substring(1, k.length - 1)
|
||||||
: k,
|
: k,
|
||||||
ImagePackImageContent.fromJson(
|
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
|
// the "short" key was still just a map from shortcode to mxc uri
|
||||||
json.tryGetMap<String, String>('short')?.catchMap((k, v) =>
|
json.tryGetMap<String, String>('short')?.catchMap(
|
||||||
MapEntry(
|
(k, v) => MapEntry(
|
||||||
k.startsWith(':') && k.endsWith(':')
|
k.startsWith(':') && k.endsWith(':')
|
||||||
? k.substring(1, k.length - 1)
|
? k.substring(1, k.length - 1)
|
||||||
: k,
|
: k,
|
||||||
ImagePackImageContent(url: Uri.parse(v)))) ??
|
ImagePackImageContent(url: Uri.parse(v)),
|
||||||
|
),
|
||||||
|
) ??
|
||||||
{};
|
{};
|
||||||
|
|
||||||
Map<String, Object?> toJson() => {
|
Map<String, Object?> toJson() => {
|
||||||
|
|
@ -120,8 +138,9 @@ class ImagePackImageContent {
|
||||||
: _json = {};
|
: _json = {};
|
||||||
|
|
||||||
ImagePackImageContent.fromJson(Map<String, Object?> json)
|
ImagePackImageContent.fromJson(Map<String, Object?> json)
|
||||||
: _json = Map.fromEntries(json.entries
|
: _json = Map.fromEntries(
|
||||||
.where((e) => !['url', 'body', 'info'].contains(e.key))),
|
json.entries.where((e) => !['url', 'body', 'info'].contains(e.key)),
|
||||||
|
),
|
||||||
url = Uri.parse(json['url'] as String),
|
url = Uri.parse(json['url'] as String),
|
||||||
body = json.tryGet('body'),
|
body = json.tryGet('body'),
|
||||||
info = json.tryGetMap<String, Object?>('info'),
|
info = json.tryGetMap<String, Object?>('info'),
|
||||||
|
|
@ -148,13 +167,20 @@ class ImagePackPackContent {
|
||||||
List<ImagePackUsage>? usage;
|
List<ImagePackUsage>? usage;
|
||||||
String? attribution;
|
String? attribution;
|
||||||
|
|
||||||
ImagePackPackContent(
|
ImagePackPackContent({
|
||||||
{this.displayName, this.avatarUrl, this.usage, this.attribution})
|
this.displayName,
|
||||||
: _json = {};
|
this.avatarUrl,
|
||||||
|
this.usage,
|
||||||
|
this.attribution,
|
||||||
|
}) : _json = {};
|
||||||
|
|
||||||
ImagePackPackContent.fromJson(Map<String, Object?> json)
|
ImagePackPackContent.fromJson(Map<String, Object?> json)
|
||||||
: _json = Map.fromEntries(json.entries.where((e) =>
|
: _json = Map.fromEntries(
|
||||||
!['display_name', 'avatar_url', 'attribution'].contains(e.key))),
|
json.entries.where(
|
||||||
|
(e) =>
|
||||||
|
!['display_name', 'avatar_url', 'attribution'].contains(e.key),
|
||||||
|
),
|
||||||
|
),
|
||||||
displayName = json.tryGet('display_name'),
|
displayName = json.tryGet('display_name'),
|
||||||
// we default to an invalid uri
|
// we default to an invalid uri
|
||||||
avatarUrl = Uri.tryParse(json.tryGet('avatar_url') ?? '.::'),
|
avatarUrl = Uri.tryParse(json.tryGet('avatar_url') ?? '.::'),
|
||||||
|
|
|
||||||
|
|
@ -45,8 +45,12 @@ class RoomEncryptedContent {
|
||||||
// filter out invalid/incomplete CiphertextInfos
|
// filter out invalid/incomplete CiphertextInfos
|
||||||
ciphertextOlm = json
|
ciphertextOlm = json
|
||||||
.tryGet<Map<String, Object?>>('ciphertext', TryGet.silent)
|
.tryGet<Map<String, Object?>>('ciphertext', TryGet.silent)
|
||||||
?.catchMap((k, v) => MapEntry(
|
?.catchMap(
|
||||||
k, CiphertextInfo.fromJson(v as Map<String, Object?>)));
|
(k, v) => MapEntry(
|
||||||
|
k,
|
||||||
|
CiphertextInfo.fromJson(v as Map<String, Object?>),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
Map<String, Object?> toJson() {
|
Map<String, Object?> toJson() {
|
||||||
final data = <String, Object?>{};
|
final data = <String, Object?>{};
|
||||||
|
|
@ -66,7 +70,8 @@ class RoomEncryptedContent {
|
||||||
ciphertextOlm!.map((k, v) => MapEntry(k, v.toJson()));
|
ciphertextOlm!.map((k, v) => MapEntry(k, v.toJson()));
|
||||||
if (ciphertextMegolm != null) {
|
if (ciphertextMegolm != null) {
|
||||||
Logs().wtf(
|
Logs().wtf(
|
||||||
'ciphertextOlm and ciphertextMegolm are both set, which should never happen!');
|
'ciphertextOlm and ciphertextMegolm are both set, which should never happen!',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
|
|
|
||||||
|
|
@ -34,11 +34,12 @@ class RoomKeyContent {
|
||||||
String sessionId;
|
String sessionId;
|
||||||
String sessionKey;
|
String sessionKey;
|
||||||
|
|
||||||
RoomKeyContent(
|
RoomKeyContent({
|
||||||
{required this.algorithm,
|
required this.algorithm,
|
||||||
required this.roomId,
|
required this.roomId,
|
||||||
required this.sessionId,
|
required this.sessionId,
|
||||||
required this.sessionKey});
|
required this.sessionKey,
|
||||||
|
});
|
||||||
|
|
||||||
RoomKeyContent.fromJson(Map<String, Object?> json)
|
RoomKeyContent.fromJson(Map<String, Object?> json)
|
||||||
: algorithm = json.tryGet('algorithm', TryGet.required) ?? '',
|
: algorithm = json.tryGet('algorithm', TryGet.required) ?? '',
|
||||||
|
|
|
||||||
|
|
@ -60,11 +60,12 @@ class RequestedKeyInfo {
|
||||||
String sessionId;
|
String sessionId;
|
||||||
String senderKey;
|
String senderKey;
|
||||||
|
|
||||||
RequestedKeyInfo(
|
RequestedKeyInfo({
|
||||||
{required this.algorithm,
|
required this.algorithm,
|
||||||
required this.roomId,
|
required this.roomId,
|
||||||
required this.sessionId,
|
required this.sessionId,
|
||||||
required this.senderKey});
|
required this.senderKey,
|
||||||
|
});
|
||||||
|
|
||||||
RequestedKeyInfo.fromJson(Map<String, Object?> json)
|
RequestedKeyInfo.fromJson(Map<String, Object?> json)
|
||||||
: algorithm = json.tryGet('algorithm', TryGet.required) ?? '',
|
: algorithm = json.tryGet('algorithm', TryGet.required) ?? '',
|
||||||
|
|
|
||||||
|
|
@ -63,11 +63,12 @@ class PassphraseInfo {
|
||||||
int? iterations;
|
int? iterations;
|
||||||
int? bits;
|
int? bits;
|
||||||
|
|
||||||
PassphraseInfo(
|
PassphraseInfo({
|
||||||
{required this.algorithm,
|
required this.algorithm,
|
||||||
required this.salt,
|
required this.salt,
|
||||||
required this.iterations,
|
required this.iterations,
|
||||||
this.bits});
|
this.bits,
|
||||||
|
});
|
||||||
|
|
||||||
PassphraseInfo.fromJson(Map<String, Object?> json)
|
PassphraseInfo.fromJson(Map<String, Object?> json)
|
||||||
: algorithm = json.tryGet('algorithm', TryGet.required),
|
: algorithm = json.tryGet('algorithm', TryGet.required),
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,8 @@ class MatrixEvent extends StrippedStateEvent {
|
||||||
: eventId = json['event_id'] as String,
|
: eventId = json['event_id'] as String,
|
||||||
roomId = json['room_id'] as String?,
|
roomId = json['room_id'] as String?,
|
||||||
originServerTs = DateTime.fromMillisecondsSinceEpoch(
|
originServerTs = DateTime.fromMillisecondsSinceEpoch(
|
||||||
json['origin_server_ts'] as int),
|
json['origin_server_ts'] as int,
|
||||||
|
),
|
||||||
unsigned = (json['unsigned'] as Map<String, Object?>?)?.copy(),
|
unsigned = (json['unsigned'] as Map<String, Object?>?)?.copy(),
|
||||||
prevContent = (json['prev_content'] as Map<String, Object?>?)?.copy(),
|
prevContent = (json['prev_content'] as Map<String, Object?>?)?.copy(),
|
||||||
redacts = json['redacts'] as String?,
|
redacts = json['redacts'] as String?,
|
||||||
|
|
|
||||||
|
|
@ -115,8 +115,10 @@ class MatrixException implements Exception {
|
||||||
?.whereType<Map<String, Object?>>()
|
?.whereType<Map<String, Object?>>()
|
||||||
.map((flow) => flow['stages'])
|
.map((flow) => flow['stages'])
|
||||||
.whereType<List<Object?>>()
|
.whereType<List<Object?>>()
|
||||||
.map((stages) =>
|
.map(
|
||||||
AuthenticationFlow(List<String>.from(stages.whereType<String>())))
|
(stages) =>
|
||||||
|
AuthenticationFlow(List<String>.from(stages.whereType<String>())),
|
||||||
|
)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
/// This section contains any information that the client will need to know in order to use a given type
|
/// This section contains any information that the client will need to know in order to use a given type
|
||||||
|
|
|
||||||
|
|
@ -31,8 +31,9 @@ class PresenceContent {
|
||||||
|
|
||||||
PresenceContent.fromJson(Map<String, Object?> json)
|
PresenceContent.fromJson(Map<String, Object?> json)
|
||||||
: presence = PresenceType.values.firstWhere(
|
: presence = PresenceType.values.firstWhere(
|
||||||
(p) => p.toString().split('.').last == json['presence'],
|
(p) => p.toString().split('.').last == json['presence'],
|
||||||
orElse: () => PresenceType.offline),
|
orElse: () => PresenceType.offline,
|
||||||
|
),
|
||||||
lastActiveAgo = json.tryGet<int>('last_active_ago'),
|
lastActiveAgo = json.tryGet<int>('last_active_ago'),
|
||||||
statusMsg = json.tryGet<String>('status_msg'),
|
statusMsg = json.tryGet<String>('status_msg'),
|
||||||
currentlyActive = json.tryGet<bool>('currently_active');
|
currentlyActive = json.tryGet<bool>('currently_active');
|
||||||
|
|
|
||||||
|
|
@ -29,11 +29,12 @@ class RoomKeysSingleKey {
|
||||||
bool isVerified;
|
bool isVerified;
|
||||||
Map<String, Object?> sessionData;
|
Map<String, Object?> sessionData;
|
||||||
|
|
||||||
RoomKeysSingleKey(
|
RoomKeysSingleKey({
|
||||||
{required this.firstMessageIndex,
|
required this.firstMessageIndex,
|
||||||
required this.forwardedCount,
|
required this.forwardedCount,
|
||||||
required this.isVerified,
|
required this.isVerified,
|
||||||
required this.sessionData});
|
required this.sessionData,
|
||||||
|
});
|
||||||
|
|
||||||
RoomKeysSingleKey.fromJson(Map<String, Object?> json)
|
RoomKeysSingleKey.fromJson(Map<String, Object?> json)
|
||||||
: firstMessageIndex = json['first_message_index'] as int,
|
: firstMessageIndex = json['first_message_index'] as int,
|
||||||
|
|
@ -57,8 +58,12 @@ class RoomKeysRoom {
|
||||||
RoomKeysRoom({required this.sessions});
|
RoomKeysRoom({required this.sessions});
|
||||||
|
|
||||||
RoomKeysRoom.fromJson(Map<String, Object?> json)
|
RoomKeysRoom.fromJson(Map<String, Object?> json)
|
||||||
: sessions = (json['sessions'] as Map<String, Object?>).map((k, v) =>
|
: sessions = (json['sessions'] as Map<String, Object?>).map(
|
||||||
MapEntry(k, RoomKeysSingleKey.fromJson(v as Map<String, Object?>)));
|
(k, v) => MapEntry(
|
||||||
|
k,
|
||||||
|
RoomKeysSingleKey.fromJson(v as Map<String, Object?>),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
Map<String, Object?> toJson() {
|
Map<String, Object?> toJson() {
|
||||||
final data = <String, Object?>{};
|
final data = <String, Object?>{};
|
||||||
|
|
|
||||||
|
|
@ -26,11 +26,12 @@ import 'package:matrix/matrix_api_lite.dart';
|
||||||
class StrippedStateEvent extends BasicEventWithSender {
|
class StrippedStateEvent extends BasicEventWithSender {
|
||||||
String? stateKey;
|
String? stateKey;
|
||||||
|
|
||||||
StrippedStateEvent(
|
StrippedStateEvent({
|
||||||
{required super.type,
|
required super.type,
|
||||||
required super.content,
|
required super.content,
|
||||||
required super.senderId,
|
required super.senderId,
|
||||||
this.stateKey});
|
this.stateKey,
|
||||||
|
});
|
||||||
|
|
||||||
StrippedStateEvent.fromJson(super.json)
|
StrippedStateEvent.fromJson(super.json)
|
||||||
: stateKey = json.tryGet<String>('state_key'),
|
: stateKey = json.tryGet<String>('state_key'),
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,8 @@ class SyncUpdate {
|
||||||
toDevice = json
|
toDevice = json
|
||||||
.tryGetMap<String, List<Object?>>('to_device')?['events']
|
.tryGetMap<String, List<Object?>>('to_device')?['events']
|
||||||
?.map(
|
?.map(
|
||||||
(i) => BasicEventWithSender.fromJson(i as Map<String, Object?>))
|
(i) => BasicEventWithSender.fromJson(i as Map<String, Object?>),
|
||||||
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
deviceLists = (() {
|
deviceLists = (() {
|
||||||
final temp = json.tryGetMap<String, Object?>('device_lists');
|
final temp = json.tryGetMap<String, Object?>('device_lists');
|
||||||
|
|
@ -72,7 +73,8 @@ class SyncUpdate {
|
||||||
deviceUnusedFallbackKeyTypes =
|
deviceUnusedFallbackKeyTypes =
|
||||||
json.tryGetList<String>('device_unused_fallback_key_types') ??
|
json.tryGetList<String>('device_unused_fallback_key_types') ??
|
||||||
json.tryGetList<String>(
|
json.tryGetList<String>(
|
||||||
'org.matrix.msc2732.device_unused_fallback_key_types');
|
'org.matrix.msc2732.device_unused_fallback_key_types',
|
||||||
|
);
|
||||||
|
|
||||||
Map<String, Object?> toJson() {
|
Map<String, Object?> toJson() {
|
||||||
final data = <String, Object?>{};
|
final data = <String, Object?>{};
|
||||||
|
|
@ -124,14 +126,24 @@ class RoomsUpdate {
|
||||||
});
|
});
|
||||||
|
|
||||||
RoomsUpdate.fromJson(Map<String, Object?> json) {
|
RoomsUpdate.fromJson(Map<String, Object?> json) {
|
||||||
join = json.tryGetMap<String, Object?>('join')?.catchMap((k, v) =>
|
join = json.tryGetMap<String, Object?>('join')?.catchMap(
|
||||||
MapEntry(k, JoinedRoomUpdate.fromJson(v as Map<String, Object?>)));
|
(k, v) =>
|
||||||
invite = json.tryGetMap<String, Object?>('invite')?.catchMap((k, v) =>
|
MapEntry(k, JoinedRoomUpdate.fromJson(v as Map<String, Object?>)),
|
||||||
MapEntry(k, InvitedRoomUpdate.fromJson(v as Map<String, Object?>)));
|
);
|
||||||
leave = json.tryGetMap<String, Object?>('leave')?.catchMap((k, v) =>
|
invite = json.tryGetMap<String, Object?>('invite')?.catchMap(
|
||||||
MapEntry(k, LeftRoomUpdate.fromJson(v as Map<String, Object?>)));
|
(k, v) => MapEntry(
|
||||||
knock = json.tryGetMap<String, Object?>('knock')?.catchMap((k, v) =>
|
k,
|
||||||
MapEntry(k, KnockRoomUpdate.fromJson(v as Map<String, Object?>)));
|
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() {
|
Map<String, Object?> toJson() {
|
||||||
|
|
@ -187,7 +199,9 @@ class JoinedRoomUpdate extends SyncRoomUpdate {
|
||||||
?.map((i) => BasicRoomEvent.fromJson(i as Map<String, Object?>))
|
?.map((i) => BasicRoomEvent.fromJson(i as Map<String, Object?>))
|
||||||
.toList(),
|
.toList(),
|
||||||
unreadNotifications = json.tryGetFromJson(
|
unreadNotifications = json.tryGetFromJson(
|
||||||
'unread_notifications', UnreadNotificationCounts.fromJson);
|
'unread_notifications',
|
||||||
|
UnreadNotificationCounts.fromJson,
|
||||||
|
);
|
||||||
|
|
||||||
Map<String, Object?> toJson() {
|
Map<String, Object?> toJson() {
|
||||||
final data = <String, Object?>{};
|
final data = <String, Object?>{};
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
extension FilterMap<K, V> on Map<K, V> {
|
extension FilterMap<K, V> on Map<K, V> {
|
||||||
Map<K2, V2> filterMap<K2, V2>(MapEntry<K2, V2>? Function(K, V) f) =>
|
Map<K2, V2> filterMap<K2, V2>(MapEntry<K2, V2>? Function(K, V) f) =>
|
||||||
Map.fromEntries(
|
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) =>
|
Map<K2, V2> catchMap<K2, V2>(MapEntry<K2, V2> Function(K, V) f) =>
|
||||||
filterMap((k, v) {
|
filterMap((k, v) {
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,8 @@ class _RequiredLog implements TryGet {
|
||||||
const _RequiredLog();
|
const _RequiredLog();
|
||||||
@override
|
@override
|
||||||
void call(String key, Type expected, Type actual) => Logs().w(
|
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 {
|
class _OptionalLog implements TryGet {
|
||||||
|
|
@ -48,7 +49,8 @@ class _OptionalLog implements TryGet {
|
||||||
void call(String key, Type expected, Type actual) {
|
void call(String key, Type expected, Type actual) {
|
||||||
if (actual != Null) {
|
if (actual != Null) {
|
||||||
Logs().w(
|
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();
|
return value.cast<T>().toList();
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
Logs().v(
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -96,13 +99,17 @@ extension TryGetMapExtension on Map<String, Object?> {
|
||||||
return Map.from(value.cast<A, B>());
|
return Map.from(value.cast<A, B>());
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
Logs().v(
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
A? tryGetFromJson<A>(String key, A Function(Map<String, Object?>) fromJson,
|
A? tryGetFromJson<A>(
|
||||||
[TryGet log = TryGet.optional]) {
|
String key,
|
||||||
|
A Function(Map<String, Object?>) fromJson, [
|
||||||
|
TryGet log = TryGet.optional,
|
||||||
|
]) {
|
||||||
final value = tryGetMap<String, Object?>(key, log);
|
final value = tryGetMap<String, Object?>(key, log);
|
||||||
|
|
||||||
return value != null ? fromJson(value) : null;
|
return value != null ? fromJson(value) : null;
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,9 @@ extension RecentEmojiExtension on Client {
|
||||||
if (userID == null) return;
|
if (userID == null) return;
|
||||||
final content = List.from(data.entries.map((e) => [e.key, e.value]));
|
final content = List.from(data.entries.map((e) => [e.key, e.value]));
|
||||||
return setAccountData(
|
return setAccountData(
|
||||||
userID!, 'io.element.recent_emoji', {'recent_emoji': content});
|
userID!,
|
||||||
|
'io.element.recent_emoji',
|
||||||
|
{'recent_emoji': content},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,8 +51,12 @@ class MatrixWidget {
|
||||||
);
|
);
|
||||||
|
|
||||||
/// creates an `m.jitsi` [MatrixWidget]
|
/// creates an `m.jitsi` [MatrixWidget]
|
||||||
factory MatrixWidget.jitsi(Room room, String name, Uri url,
|
factory MatrixWidget.jitsi(
|
||||||
{bool isAudioOnly = false}) =>
|
Room room,
|
||||||
|
String name,
|
||||||
|
Uri url, {
|
||||||
|
bool isAudioOnly = false,
|
||||||
|
}) =>
|
||||||
MatrixWidget(
|
MatrixWidget(
|
||||||
room: room,
|
room: room,
|
||||||
name: name,
|
name: name,
|
||||||
|
|
@ -104,8 +108,10 @@ class MatrixWidget {
|
||||||
// `[a-zA-Z0-9_-]` as well as non string values
|
// `[a-zA-Z0-9_-]` as well as non string values
|
||||||
if (data != null)
|
if (data != null)
|
||||||
...Map.from(data!)
|
...Map.from(data!)
|
||||||
..removeWhere((key, value) =>
|
..removeWhere(
|
||||||
!RegExp(r'^[\w-]+$').hasMatch(key) || !value is String)
|
(key, value) =>
|
||||||
|
!RegExp(r'^[\w-]+$').hasMatch(key) || !value is String,
|
||||||
|
)
|
||||||
..map((key, value) => MapEntry('\$key', value)),
|
..map((key, value) => MapEntry('\$key', value)),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,20 +29,22 @@ extension UiaLogin on Client {
|
||||||
final requestUri = Uri(path: '_matrix/client/$pathVersion/login');
|
final requestUri = Uri(path: '_matrix/client/$pathVersion/login');
|
||||||
final request = Request('POST', baseUri!.resolveUri(requestUri));
|
final request = Request('POST', baseUri!.resolveUri(requestUri));
|
||||||
request.headers['content-type'] = 'application/json';
|
request.headers['content-type'] = 'application/json';
|
||||||
request.bodyBytes = utf8.encode(jsonEncode({
|
request.bodyBytes = utf8.encode(
|
||||||
if (address != null) 'address': address,
|
jsonEncode({
|
||||||
if (deviceId != null) 'device_id': deviceId,
|
if (address != null) 'address': address,
|
||||||
if (identifier != null) 'identifier': identifier.toJson(),
|
if (deviceId != null) 'device_id': deviceId,
|
||||||
if (initialDeviceDisplayName != null)
|
if (identifier != null) 'identifier': identifier.toJson(),
|
||||||
'initial_device_display_name': initialDeviceDisplayName,
|
if (initialDeviceDisplayName != null)
|
||||||
if (medium != null) 'medium': medium,
|
'initial_device_display_name': initialDeviceDisplayName,
|
||||||
if (password != null) 'password': password,
|
if (medium != null) 'medium': medium,
|
||||||
if (token != null) 'token': token,
|
if (password != null) 'password': password,
|
||||||
'type': type,
|
if (token != null) 'token': token,
|
||||||
if (user != null) 'user': user,
|
'type': type,
|
||||||
if (auth != null) 'auth': auth.toJson(),
|
if (user != null) 'user': user,
|
||||||
if (refreshToken != null) 'refresh_token': refreshToken,
|
if (auth != null) 'auth': auth.toJson(),
|
||||||
}));
|
if (refreshToken != null) 'refresh_token': refreshToken,
|
||||||
|
}),
|
||||||
|
);
|
||||||
final response = await httpClient.send(request);
|
final response = await httpClient.send(request);
|
||||||
final responseBody = await response.stream.toBytes();
|
final responseBody = await response.stream.toBytes();
|
||||||
if (response.statusCode != 200) unexpectedResponse(response, responseBody);
|
if (response.statusCode != 200) unexpectedResponse(response, responseBody);
|
||||||
|
|
|
||||||
|
|
@ -68,16 +68,21 @@ extension DehydratedDeviceMatrixApi on MatrixApi {
|
||||||
|
|
||||||
/// fetch events sent to a dehydrated device.
|
/// fetch events sent to a dehydrated device.
|
||||||
/// https://github.com/matrix-org/matrix-spec-proposals/pull/3814
|
/// https://github.com/matrix-org/matrix-spec-proposals/pull/3814
|
||||||
Future<DehydratedDeviceEvents> getDehydratedDeviceEvents(String deviceId,
|
Future<DehydratedDeviceEvents> getDehydratedDeviceEvents(
|
||||||
{String? nextBatch, int limit = 100}) async {
|
String deviceId, {
|
||||||
final response = await request(RequestType.POST,
|
String? nextBatch,
|
||||||
'/client/unstable/org.matrix.msc3814.v1/dehydrated_device/$deviceId/events',
|
int limit = 100,
|
||||||
query: {
|
}) async {
|
||||||
'limit': limit.toString(),
|
final response = await request(
|
||||||
},
|
RequestType.POST,
|
||||||
data: {
|
'/client/unstable/org.matrix.msc3814.v1/dehydrated_device/$deviceId/events',
|
||||||
if (nextBatch != null) 'next_batch': nextBatch,
|
query: {
|
||||||
});
|
'limit': limit.toString(),
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
if (nextBatch != null) 'next_batch': nextBatch,
|
||||||
|
},
|
||||||
|
);
|
||||||
return DehydratedDeviceEvents.fromJson(response);
|
return DehydratedDeviceEvents.fromJson(response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,8 @@ extension DehydratedDeviceHandler on Client {
|
||||||
if (dehydratedDeviceIdentity == null ||
|
if (dehydratedDeviceIdentity == null ||
|
||||||
!dehydratedDeviceIdentity.hasValidSignatureChain()) {
|
!dehydratedDeviceIdentity.hasValidSignatureChain()) {
|
||||||
Logs().w(
|
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);
|
await _uploadNewDevice(secureStorage);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -101,8 +102,10 @@ extension DehydratedDeviceHandler on Client {
|
||||||
DehydratedDeviceEvents? events;
|
DehydratedDeviceEvents? events;
|
||||||
|
|
||||||
do {
|
do {
|
||||||
events = await getDehydratedDeviceEvents(device.deviceId,
|
events = await getDehydratedDeviceEvents(
|
||||||
nextBatch: events?.nextBatch);
|
device.deviceId,
|
||||||
|
nextBatch: events?.nextBatch,
|
||||||
|
);
|
||||||
|
|
||||||
for (final e in events.events ?? []) {
|
for (final e in events.events ?? []) {
|
||||||
// We are only interested in roomkeys, which ALWAYS need to be encrypted.
|
// 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.');
|
Logs().i('Dehydrated device key not found, creating new one.');
|
||||||
pickleDeviceKey = base64.encode(uc.secureRandomBytes(128));
|
pickleDeviceKey = base64.encode(uc.secureRandomBytes(128));
|
||||||
await secureStorage.store(
|
await secureStorage.store(
|
||||||
_ssssSecretNameForDehydratedDevice, pickleDeviceKey);
|
_ssssSecretNameForDehydratedDevice,
|
||||||
|
pickleDeviceKey,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const chars =
|
const chars =
|
||||||
'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890';
|
'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890';
|
||||||
final rnd = Random();
|
final rnd = Random();
|
||||||
|
|
||||||
final deviceIdSuffix = String.fromCharCodes(Iterable.generate(
|
final deviceIdSuffix = String.fromCharCodes(
|
||||||
10, (_) => chars.codeUnitAt(rnd.nextInt(chars.length))));
|
Iterable.generate(
|
||||||
|
10,
|
||||||
|
(_) => chars.codeUnitAt(rnd.nextInt(chars.length)),
|
||||||
|
),
|
||||||
|
);
|
||||||
final String device = 'FAM$deviceIdSuffix';
|
final String device = 'FAM$deviceIdSuffix';
|
||||||
|
|
||||||
// Generate a new olm account for the dehydrated device.
|
// Generate a new olm account for the dehydrated device.
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,12 @@ abstract class CuteEventContent {
|
||||||
static Map<String, dynamic> get googlyEyes => {
|
static Map<String, dynamic> get googlyEyes => {
|
||||||
'msgtype': CuteEventContent.eventType,
|
'msgtype': CuteEventContent.eventType,
|
||||||
'cute_type': 'googly_eyes',
|
'cute_type': 'googly_eyes',
|
||||||
'body': '👀'
|
'body': '👀',
|
||||||
};
|
};
|
||||||
static Map<String, dynamic> get cuddle => {
|
static Map<String, dynamic> get cuddle => {
|
||||||
'msgtype': CuteEventContent.eventType,
|
'msgtype': CuteEventContent.eventType,
|
||||||
'cute_type': 'cuddle',
|
'cute_type': 'cuddle',
|
||||||
'body': '😊'
|
'body': '😊',
|
||||||
};
|
};
|
||||||
static Map<String, dynamic> get hug => {
|
static Map<String, dynamic> get hug => {
|
||||||
'msgtype': CuteEventContent.eventType,
|
'msgtype': CuteEventContent.eventType,
|
||||||
|
|
|
||||||
|
|
@ -41,11 +41,13 @@ extension MscUnpublishedCustomRefreshTokenLifetime on MatrixApi {
|
||||||
final requestUri = Uri(path: '_matrix/client/v3/refresh');
|
final requestUri = Uri(path: '_matrix/client/v3/refresh');
|
||||||
final request = Request('POST', baseUri!.resolveUri(requestUri));
|
final request = Request('POST', baseUri!.resolveUri(requestUri));
|
||||||
request.headers['content-type'] = 'application/json';
|
request.headers['content-type'] = 'application/json';
|
||||||
request.bodyBytes = utf8.encode(jsonEncode({
|
request.bodyBytes = utf8.encode(
|
||||||
'refresh_token': refreshToken,
|
jsonEncode({
|
||||||
if (refreshTokenLifetimeMs != null)
|
'refresh_token': refreshToken,
|
||||||
customFieldKey: refreshTokenLifetimeMs,
|
if (refreshTokenLifetimeMs != null)
|
||||||
}));
|
customFieldKey: refreshTokenLifetimeMs,
|
||||||
|
}),
|
||||||
|
);
|
||||||
final response = await httpClient.send(request);
|
final response = await httpClient.send(request);
|
||||||
final responseBody = await response.stream.toBytes();
|
final responseBody = await response.stream.toBytes();
|
||||||
if (response.statusCode != 200) unexpectedResponse(response, responseBody);
|
if (response.statusCode != 200) unexpectedResponse(response, responseBody);
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -59,8 +59,11 @@ abstract class DatabaseApi {
|
||||||
|
|
||||||
Future<List<Room>> getRoomList(Client client);
|
Future<List<Room>> getRoomList(Client client);
|
||||||
|
|
||||||
Future<Room?> getSingleRoom(Client client, String roomId,
|
Future<Room?> getSingleRoom(
|
||||||
{bool loadImportantStates = true});
|
Client client,
|
||||||
|
String roomId, {
|
||||||
|
bool loadImportantStates = true,
|
||||||
|
});
|
||||||
|
|
||||||
Future<Map<String, BasicEvent>> getAccountData();
|
Future<Map<String, BasicEvent>> getAccountData();
|
||||||
|
|
||||||
|
|
@ -86,7 +89,9 @@ abstract class DatabaseApi {
|
||||||
Future<CachedProfileInformation?> getUserProfile(String userId);
|
Future<CachedProfileInformation?> getUserProfile(String userId);
|
||||||
|
|
||||||
Future<void> storeUserProfile(
|
Future<void> storeUserProfile(
|
||||||
String userId, CachedProfileInformation profile);
|
String userId,
|
||||||
|
CachedProfileInformation profile,
|
||||||
|
);
|
||||||
|
|
||||||
Future<void> markUserProfileAsOutdated(String userId);
|
Future<void> markUserProfileAsOutdated(String userId);
|
||||||
|
|
||||||
|
|
@ -316,7 +321,10 @@ abstract class DatabaseApi {
|
||||||
Future<List<StoredInboundGroupSession>> getInboundGroupSessionsToUpload();
|
Future<List<StoredInboundGroupSession>> getInboundGroupSessionsToUpload();
|
||||||
|
|
||||||
Future<void> addSeenDeviceId(
|
Future<void> addSeenDeviceId(
|
||||||
String userId, String deviceId, String publicKeys);
|
String userId,
|
||||||
|
String deviceId,
|
||||||
|
String publicKeys,
|
||||||
|
);
|
||||||
|
|
||||||
Future<void> addSeenPublicKey(String publicKey, String deviceId);
|
Future<void> addSeenPublicKey(String publicKey, String deviceId);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,8 @@ import 'package:matrix/src/utils/run_benchmarked.dart';
|
||||||
|
|
||||||
/// This database does not support file caching!
|
/// This database does not support file caching!
|
||||||
@Deprecated(
|
@Deprecated(
|
||||||
'Use [MatrixSdkDatabase] instead. Don\'t forget to properly migrate!')
|
'Use [MatrixSdkDatabase] instead. Don\'t forget to properly migrate!',
|
||||||
|
)
|
||||||
class HiveCollectionsDatabase extends DatabaseApi {
|
class HiveCollectionsDatabase extends DatabaseApi {
|
||||||
static const int version = 7;
|
static const int version = 7;
|
||||||
final String name;
|
final String name;
|
||||||
|
|
@ -374,8 +375,10 @@ class HiveCollectionsDatabase extends DatabaseApi {
|
||||||
.toList();
|
.toList();
|
||||||
final rawEvents = await _eventsBox.getAll(keys);
|
final rawEvents = await _eventsBox.getAll(keys);
|
||||||
return rawEvents
|
return rawEvents
|
||||||
.map((rawEvent) =>
|
.map(
|
||||||
rawEvent != null ? Event.fromJson(copyMap(rawEvent), room) : null)
|
(rawEvent) =>
|
||||||
|
rawEvent != null ? Event.fromJson(copyMap(rawEvent), room) : null,
|
||||||
|
)
|
||||||
.whereNotNull()
|
.whereNotNull()
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
@ -391,7 +394,8 @@ class HiveCollectionsDatabase extends DatabaseApi {
|
||||||
// Get the synced event IDs from the store
|
// Get the synced event IDs from the store
|
||||||
final timelineKey = TupleKey(room.id, '').toString();
|
final timelineKey = TupleKey(room.id, '').toString();
|
||||||
final timelineEventIds = List<String>.from(
|
final timelineEventIds = List<String>.from(
|
||||||
(await _timelineFragmentsBox.get(timelineKey)) ?? []);
|
(await _timelineFragmentsBox.get(timelineKey)) ?? [],
|
||||||
|
);
|
||||||
|
|
||||||
// Get the local stored SENDING events from the store
|
// Get the local stored SENDING events from the store
|
||||||
late final List<String> sendingEventIds;
|
late final List<String> sendingEventIds;
|
||||||
|
|
@ -400,17 +404,20 @@ class HiveCollectionsDatabase extends DatabaseApi {
|
||||||
} else {
|
} else {
|
||||||
final sendingTimelineKey = TupleKey(room.id, 'SENDING').toString();
|
final sendingTimelineKey = TupleKey(room.id, 'SENDING').toString();
|
||||||
sendingEventIds = List<String>.from(
|
sendingEventIds = List<String>.from(
|
||||||
(await _timelineFragmentsBox.get(sendingTimelineKey)) ?? []);
|
(await _timelineFragmentsBox.get(sendingTimelineKey)) ?? [],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Combine those two lists while respecting the start and limit parameters.
|
// Combine those two lists while respecting the start and limit parameters.
|
||||||
final end = min(timelineEventIds.length,
|
final end = min(
|
||||||
start + (limit ?? timelineEventIds.length));
|
timelineEventIds.length,
|
||||||
|
start + (limit ?? timelineEventIds.length),
|
||||||
|
);
|
||||||
final eventIds = List<String>.from([
|
final eventIds = List<String>.from([
|
||||||
...sendingEventIds,
|
...sendingEventIds,
|
||||||
...(start < timelineEventIds.length && !onlySending
|
...(start < timelineEventIds.length && !onlySending
|
||||||
? timelineEventIds.getRange(start, end).toList()
|
? timelineEventIds.getRange(start, end).toList()
|
||||||
: [])
|
: []),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return await _getEventsByIds(eventIds, room);
|
return await _getEventsByIds(eventIds, room);
|
||||||
|
|
@ -427,7 +434,8 @@ class HiveCollectionsDatabase extends DatabaseApi {
|
||||||
// Get the synced event IDs from the store
|
// Get the synced event IDs from the store
|
||||||
final timelineKey = TupleKey(room.id, '').toString();
|
final timelineKey = TupleKey(room.id, '').toString();
|
||||||
final timelineEventIds = List<String>.from(
|
final timelineEventIds = List<String>.from(
|
||||||
(await _timelineFragmentsBox.get(timelineKey)) ?? []);
|
(await _timelineFragmentsBox.get(timelineKey)) ?? [],
|
||||||
|
);
|
||||||
|
|
||||||
// Get the local stored SENDING events from the store
|
// Get the local stored SENDING events from the store
|
||||||
late final List<String> sendingEventIds;
|
late final List<String> sendingEventIds;
|
||||||
|
|
@ -436,7 +444,8 @@ class HiveCollectionsDatabase extends DatabaseApi {
|
||||||
} else {
|
} else {
|
||||||
final sendingTimelineKey = TupleKey(room.id, 'SENDING').toString();
|
final sendingTimelineKey = TupleKey(room.id, 'SENDING').toString();
|
||||||
sendingEventIds = List<String>.from(
|
sendingEventIds = List<String>.from(
|
||||||
(await _timelineFragmentsBox.get(sendingTimelineKey)) ?? []);
|
(await _timelineFragmentsBox.get(sendingTimelineKey)) ?? [],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Combine those two lists while respecting the start and limit parameters.
|
// Combine those two lists while respecting the start and limit parameters.
|
||||||
|
|
@ -481,7 +490,9 @@ class HiveCollectionsDatabase extends DatabaseApi {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<String>> getLastSentMessageUserDeviceKey(
|
Future<List<String>> getLastSentMessageUserDeviceKey(
|
||||||
String userId, String deviceId) async {
|
String userId,
|
||||||
|
String deviceId,
|
||||||
|
) async {
|
||||||
final raw =
|
final raw =
|
||||||
await _userDeviceKeysBox.get(TupleKey(userId, deviceId).toString());
|
await _userDeviceKeysBox.get(TupleKey(userId, deviceId).toString());
|
||||||
if (raw == null) return <String>[];
|
if (raw == null) return <String>[];
|
||||||
|
|
@ -489,8 +500,12 @@ class HiveCollectionsDatabase extends DatabaseApi {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> storeOlmSession(String identityKey, String sessionId,
|
Future<void> storeOlmSession(
|
||||||
String pickle, int lastReceived) async {
|
String identityKey,
|
||||||
|
String sessionId,
|
||||||
|
String pickle,
|
||||||
|
int lastReceived,
|
||||||
|
) async {
|
||||||
final rawSessions = (await _olmSessionsBox.get(identityKey)) ?? {};
|
final rawSessions = (await _olmSessionsBox.get(identityKey)) ?? {};
|
||||||
rawSessions[sessionId] = <String, dynamic>{
|
rawSessions[sessionId] = <String, dynamic>{
|
||||||
'identity_key': identityKey,
|
'identity_key': identityKey,
|
||||||
|
|
@ -504,7 +519,9 @@ class HiveCollectionsDatabase extends DatabaseApi {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<OlmSession>> getOlmSessions(
|
Future<List<OlmSession>> getOlmSessions(
|
||||||
String identityKey, String userId) async {
|
String identityKey,
|
||||||
|
String userId,
|
||||||
|
) async {
|
||||||
final rawSessions = await _olmSessionsBox.get(identityKey);
|
final rawSessions = await _olmSessionsBox.get(identityKey);
|
||||||
if (rawSessions == null || rawSessions.isEmpty) return <OlmSession>[];
|
if (rawSessions == null || rawSessions.isEmpty) return <OlmSession>[];
|
||||||
return rawSessions.values
|
return rawSessions.values
|
||||||
|
|
@ -518,23 +535,31 @@ class HiveCollectionsDatabase extends DatabaseApi {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<OlmSession>> getOlmSessionsForDevices(
|
Future<List<OlmSession>> getOlmSessionsForDevices(
|
||||||
List<String> identityKeys, String userId) async {
|
List<String> identityKeys,
|
||||||
|
String userId,
|
||||||
|
) async {
|
||||||
final sessions = await Future.wait(
|
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];
|
return <OlmSession>[for (final sublist in sessions) ...sublist];
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<OutboundGroupSession?> getOutboundGroupSession(
|
Future<OutboundGroupSession?> getOutboundGroupSession(
|
||||||
String roomId, String userId) async {
|
String roomId,
|
||||||
|
String userId,
|
||||||
|
) async {
|
||||||
final raw = await _outboundGroupSessionsBox.get(roomId);
|
final raw = await _outboundGroupSessionsBox.get(roomId);
|
||||||
if (raw == null) return null;
|
if (raw == null) return null;
|
||||||
return OutboundGroupSession.fromJson(copyMap(raw), userId);
|
return OutboundGroupSession.fromJson(copyMap(raw), userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Room?> getSingleRoom(Client client, String roomId,
|
Future<Room?> getSingleRoom(
|
||||||
{bool loadImportantStates = true}) async {
|
Client client,
|
||||||
|
String roomId, {
|
||||||
|
bool loadImportantStates = true,
|
||||||
|
}) async {
|
||||||
// Get raw room from database:
|
// Get raw room from database:
|
||||||
final roomData = await _roomsBox.get(roomId);
|
final roomData = await _roomsBox.get(roomId);
|
||||||
if (roomData == null) return null;
|
if (roomData == null) return null;
|
||||||
|
|
@ -589,9 +614,11 @@ class HiveCollectionsDatabase extends DatabaseApi {
|
||||||
for (final states in statesList) {
|
for (final states in statesList) {
|
||||||
if (states == null) continue;
|
if (states == null) continue;
|
||||||
final stateEvents = states.values
|
final stateEvents = states.values
|
||||||
.map((raw) => room.membership == Membership.invite
|
.map(
|
||||||
? StrippedStateEvent.fromJson(copyMap(raw))
|
(raw) => room.membership == Membership.invite
|
||||||
: Event.fromJson(copyMap(raw), room))
|
? StrippedStateEvent.fromJson(copyMap(raw))
|
||||||
|
: Event.fromJson(copyMap(raw), room),
|
||||||
|
)
|
||||||
.toList();
|
.toList();
|
||||||
for (final state in stateEvents) {
|
for (final state in stateEvents) {
|
||||||
room.setState(state);
|
room.setState(state);
|
||||||
|
|
@ -637,9 +664,11 @@ class HiveCollectionsDatabase extends DatabaseApi {
|
||||||
if (members != null) {
|
if (members != null) {
|
||||||
for (final member in members) {
|
for (final member in members) {
|
||||||
if (member == null) continue;
|
if (member == null) continue;
|
||||||
room.setState(room.membership == Membership.invite
|
room.setState(
|
||||||
? StrippedStateEvent.fromJson(copyMap(member))
|
room.membership == Membership.invite
|
||||||
: Event.fromJson(copyMap(member), room));
|
? StrippedStateEvent.fromJson(copyMap(member))
|
||||||
|
: Event.fromJson(copyMap(member), room),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -657,7 +686,8 @@ class HiveCollectionsDatabase extends DatabaseApi {
|
||||||
basicRoomEvent;
|
basicRoomEvent;
|
||||||
} else {
|
} else {
|
||||||
Logs().w(
|
Logs().w(
|
||||||
'Found account data for unknown room $roomId. Delete now...');
|
'Found account data for unknown room $roomId. Delete now...',
|
||||||
|
);
|
||||||
await _roomAccountDataBox
|
await _roomAccountDataBox
|
||||||
.delete(TupleKey(roomId, basicRoomEvent.type).toString());
|
.delete(TupleKey(roomId, basicRoomEvent.type).toString());
|
||||||
}
|
}
|
||||||
|
|
@ -687,7 +717,9 @@ class HiveCollectionsDatabase extends DatabaseApi {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<Event>> getUnimportantRoomEventStatesForRoom(
|
Future<List<Event>> getUnimportantRoomEventStatesForRoom(
|
||||||
List<String> events, Room room) async {
|
List<String> events,
|
||||||
|
Room room,
|
||||||
|
) async {
|
||||||
final keys = (await _roomStateBox.getAllKeys()).where((key) {
|
final keys = (await _roomStateBox.getAllKeys()).where((key) {
|
||||||
final tuple = TupleKey.fromString(key);
|
final tuple = TupleKey.fromString(key);
|
||||||
return tuple.parts.first == room.id && !events.contains(tuple.parts[1]);
|
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);
|
final states = await _roomStateBox.get(key);
|
||||||
if (states == null) continue;
|
if (states == null) continue;
|
||||||
unimportantEvents.addAll(
|
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();
|
return unimportantEvents.where((event) => event.stateKey != null).toList();
|
||||||
}
|
}
|
||||||
|
|
@ -753,20 +786,21 @@ class HiveCollectionsDatabase extends DatabaseApi {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
res[userId] = DeviceKeysList.fromDbJson(
|
res[userId] = DeviceKeysList.fromDbJson(
|
||||||
{
|
{
|
||||||
'client_id': client.id,
|
'client_id': client.id,
|
||||||
'user_id': userId,
|
'user_id': userId,
|
||||||
'outdated': await _userDeviceKeysOutdatedBox.get(userId),
|
'outdated': await _userDeviceKeysOutdatedBox.get(userId),
|
||||||
},
|
},
|
||||||
childEntries
|
childEntries
|
||||||
.where((c) => c != null)
|
.where((c) => c != null)
|
||||||
.toList()
|
.toList()
|
||||||
.cast<Map<String, dynamic>>(),
|
.cast<Map<String, dynamic>>(),
|
||||||
crossSigningEntries
|
crossSigningEntries
|
||||||
.where((c) => c != null)
|
.where((c) => c != null)
|
||||||
.toList()
|
.toList()
|
||||||
.cast<Map<String, dynamic>>(),
|
.cast<Map<String, dynamic>>(),
|
||||||
client);
|
client,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
});
|
});
|
||||||
|
|
@ -788,16 +822,17 @@ class HiveCollectionsDatabase extends DatabaseApi {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<int> insertClient(
|
Future<int> insertClient(
|
||||||
String name,
|
String name,
|
||||||
String homeserverUrl,
|
String homeserverUrl,
|
||||||
String token,
|
String token,
|
||||||
DateTime? tokenExpiresAt,
|
DateTime? tokenExpiresAt,
|
||||||
String? refreshToken,
|
String? refreshToken,
|
||||||
String userId,
|
String userId,
|
||||||
String? deviceId,
|
String? deviceId,
|
||||||
String? deviceName,
|
String? deviceName,
|
||||||
String? prevBatch,
|
String? prevBatch,
|
||||||
String? olmAccount) async {
|
String? olmAccount,
|
||||||
|
) async {
|
||||||
await transaction(() async {
|
await transaction(() async {
|
||||||
await _clientBox.put('homeserver_url', homeserverUrl);
|
await _clientBox.put('homeserver_url', homeserverUrl);
|
||||||
await _clientBox.put('token', token);
|
await _clientBox.put('token', token);
|
||||||
|
|
@ -842,7 +877,10 @@ class HiveCollectionsDatabase extends DatabaseApi {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<int> insertIntoToDeviceQueue(
|
Future<int> insertIntoToDeviceQueue(
|
||||||
String type, String txnId, String content) async {
|
String type,
|
||||||
|
String txnId,
|
||||||
|
String content,
|
||||||
|
) async {
|
||||||
final id = DateTime.now().millisecondsSinceEpoch;
|
final id = DateTime.now().millisecondsSinceEpoch;
|
||||||
await _toDeviceQueueBox.put(id.toString(), {
|
await _toDeviceQueueBox.put(id.toString(), {
|
||||||
'type': type,
|
'type': type,
|
||||||
|
|
@ -854,11 +892,14 @@ class HiveCollectionsDatabase extends DatabaseApi {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> markInboundGroupSessionAsUploaded(
|
Future<void> markInboundGroupSessionAsUploaded(
|
||||||
String roomId, String sessionId) async {
|
String roomId,
|
||||||
|
String sessionId,
|
||||||
|
) async {
|
||||||
final raw = await _inboundGroupSessionsBox.get(sessionId);
|
final raw = await _inboundGroupSessionsBox.get(sessionId);
|
||||||
if (raw == null) {
|
if (raw == null) {
|
||||||
Logs().w(
|
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;
|
return;
|
||||||
}
|
}
|
||||||
raw['uploaded'] = true;
|
raw['uploaded'] = true;
|
||||||
|
|
@ -903,7 +944,9 @@ class HiveCollectionsDatabase extends DatabaseApi {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> removeUserCrossSigningKey(
|
Future<void> removeUserCrossSigningKey(
|
||||||
String userId, String publicKey) async {
|
String userId,
|
||||||
|
String publicKey,
|
||||||
|
) async {
|
||||||
await _userCrossSigningKeysBox
|
await _userCrossSigningKeysBox
|
||||||
.delete(TupleKey(userId, publicKey).toString());
|
.delete(TupleKey(userId, publicKey).toString());
|
||||||
return;
|
return;
|
||||||
|
|
@ -917,7 +960,10 @@ class HiveCollectionsDatabase extends DatabaseApi {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> setBlockedUserCrossSigningKey(
|
Future<void> setBlockedUserCrossSigningKey(
|
||||||
bool blocked, String userId, String publicKey) async {
|
bool blocked,
|
||||||
|
String userId,
|
||||||
|
String publicKey,
|
||||||
|
) async {
|
||||||
final raw = await _userCrossSigningKeysBox
|
final raw = await _userCrossSigningKeysBox
|
||||||
.get(TupleKey(userId, publicKey).toString());
|
.get(TupleKey(userId, publicKey).toString());
|
||||||
raw!['blocked'] = blocked;
|
raw!['blocked'] = blocked;
|
||||||
|
|
@ -930,7 +976,10 @@ class HiveCollectionsDatabase extends DatabaseApi {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> setBlockedUserDeviceKey(
|
Future<void> setBlockedUserDeviceKey(
|
||||||
bool blocked, String userId, String deviceId) async {
|
bool blocked,
|
||||||
|
String userId,
|
||||||
|
String deviceId,
|
||||||
|
) async {
|
||||||
final raw =
|
final raw =
|
||||||
await _userDeviceKeysBox.get(TupleKey(userId, deviceId).toString());
|
await _userDeviceKeysBox.get(TupleKey(userId, deviceId).toString());
|
||||||
raw!['blocked'] = blocked;
|
raw!['blocked'] = blocked;
|
||||||
|
|
@ -943,7 +992,10 @@ class HiveCollectionsDatabase extends DatabaseApi {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> setLastActiveUserDeviceKey(
|
Future<void> setLastActiveUserDeviceKey(
|
||||||
int lastActive, String userId, String deviceId) async {
|
int lastActive,
|
||||||
|
String userId,
|
||||||
|
String deviceId,
|
||||||
|
) async {
|
||||||
final raw =
|
final raw =
|
||||||
await _userDeviceKeysBox.get(TupleKey(userId, deviceId).toString());
|
await _userDeviceKeysBox.get(TupleKey(userId, deviceId).toString());
|
||||||
raw!['last_active'] = lastActive;
|
raw!['last_active'] = lastActive;
|
||||||
|
|
@ -955,7 +1007,10 @@ class HiveCollectionsDatabase extends DatabaseApi {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> setLastSentMessageUserDeviceKey(
|
Future<void> setLastSentMessageUserDeviceKey(
|
||||||
String lastSentMessage, String userId, String deviceId) async {
|
String lastSentMessage,
|
||||||
|
String userId,
|
||||||
|
String deviceId,
|
||||||
|
) async {
|
||||||
final raw =
|
final raw =
|
||||||
await _userDeviceKeysBox.get(TupleKey(userId, deviceId).toString());
|
await _userDeviceKeysBox.get(TupleKey(userId, deviceId).toString());
|
||||||
raw!['last_sent_message'] = lastSentMessage;
|
raw!['last_sent_message'] = lastSentMessage;
|
||||||
|
|
@ -967,7 +1022,10 @@ class HiveCollectionsDatabase extends DatabaseApi {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> setRoomPrevBatch(
|
Future<void> setRoomPrevBatch(
|
||||||
String? prevBatch, String roomId, Client client) async {
|
String? prevBatch,
|
||||||
|
String roomId,
|
||||||
|
Client client,
|
||||||
|
) async {
|
||||||
final raw = await _roomsBox.get(roomId);
|
final raw = await _roomsBox.get(roomId);
|
||||||
if (raw == null) return;
|
if (raw == null) return;
|
||||||
final room = Room.fromJson(copyMap(raw), client);
|
final room = Room.fromJson(copyMap(raw), client);
|
||||||
|
|
@ -978,7 +1036,10 @@ class HiveCollectionsDatabase extends DatabaseApi {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> setVerifiedUserCrossSigningKey(
|
Future<void> setVerifiedUserCrossSigningKey(
|
||||||
bool verified, String userId, String publicKey) async {
|
bool verified,
|
||||||
|
String userId,
|
||||||
|
String publicKey,
|
||||||
|
) async {
|
||||||
final raw = (await _userCrossSigningKeysBox
|
final raw = (await _userCrossSigningKeysBox
|
||||||
.get(TupleKey(userId, publicKey).toString())) ??
|
.get(TupleKey(userId, publicKey).toString())) ??
|
||||||
{};
|
{};
|
||||||
|
|
@ -992,7 +1053,10 @@ class HiveCollectionsDatabase extends DatabaseApi {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> setVerifiedUserDeviceKey(
|
Future<void> setVerifiedUserDeviceKey(
|
||||||
bool verified, String userId, String deviceId) async {
|
bool verified,
|
||||||
|
String userId,
|
||||||
|
String deviceId,
|
||||||
|
) async {
|
||||||
final raw =
|
final raw =
|
||||||
await _userDeviceKeysBox.get(TupleKey(userId, deviceId).toString());
|
await _userDeviceKeysBox.get(TupleKey(userId, deviceId).toString());
|
||||||
raw!['verified'] = verified;
|
raw!['verified'] = verified;
|
||||||
|
|
@ -1025,8 +1089,9 @@ class HiveCollectionsDatabase extends DatabaseApi {
|
||||||
if (event != null) {
|
if (event != null) {
|
||||||
event.setRedactionEvent(Event.fromJson(eventUpdate.content, tmpRoom));
|
event.setRedactionEvent(Event.fromJson(eventUpdate.content, tmpRoom));
|
||||||
await _eventsBox.put(
|
await _eventsBox.put(
|
||||||
TupleKey(eventUpdate.roomID, event.eventId).toString(),
|
TupleKey(eventUpdate.roomID, event.eventId).toString(),
|
||||||
event.toJson());
|
event.toJson(),
|
||||||
|
);
|
||||||
|
|
||||||
if (tmpRoom.lastEvent?.eventId == event.eventId) {
|
if (tmpRoom.lastEvent?.eventId == event.eventId) {
|
||||||
await _roomStateBox.put(
|
await _roomStateBox.put(
|
||||||
|
|
@ -1041,7 +1106,7 @@ class HiveCollectionsDatabase extends DatabaseApi {
|
||||||
if ({
|
if ({
|
||||||
EventUpdateType.timeline,
|
EventUpdateType.timeline,
|
||||||
EventUpdateType.history,
|
EventUpdateType.history,
|
||||||
EventUpdateType.decryptedTimelineQueue
|
EventUpdateType.decryptedTimelineQueue,
|
||||||
}.contains(eventUpdate.type)) {
|
}.contains(eventUpdate.type)) {
|
||||||
final eventId = eventUpdate.content['event_id'];
|
final eventId = eventUpdate.content['event_id'];
|
||||||
// Is this ID already in the store?
|
// Is this ID already in the store?
|
||||||
|
|
@ -1089,8 +1154,10 @@ class HiveCollectionsDatabase extends DatabaseApi {
|
||||||
final transactionId = eventUpdate.content
|
final transactionId = eventUpdate.content
|
||||||
.tryGetMap<String, dynamic>('unsigned')
|
.tryGetMap<String, dynamic>('unsigned')
|
||||||
?.tryGet<String>('transaction_id');
|
?.tryGet<String>('transaction_id');
|
||||||
await _eventsBox.put(TupleKey(eventUpdate.roomID, eventId).toString(),
|
await _eventsBox.put(
|
||||||
eventUpdate.content);
|
TupleKey(eventUpdate.roomID, eventId).toString(),
|
||||||
|
eventUpdate.content,
|
||||||
|
);
|
||||||
|
|
||||||
// Update timeline fragments
|
// Update timeline fragments
|
||||||
final key = TupleKey(eventUpdate.roomID, status.isSent ? '' : 'SENDING')
|
final key = TupleKey(eventUpdate.roomID, status.isSent ? '' : 'SENDING')
|
||||||
|
|
@ -1141,11 +1208,12 @@ class HiveCollectionsDatabase extends DatabaseApi {
|
||||||
eventUpdate.type == EventUpdateType.inviteState)) {
|
eventUpdate.type == EventUpdateType.inviteState)) {
|
||||||
if (eventUpdate.content['type'] == EventTypes.RoomMember) {
|
if (eventUpdate.content['type'] == EventTypes.RoomMember) {
|
||||||
await _roomMembersBox.put(
|
await _roomMembersBox.put(
|
||||||
TupleKey(
|
TupleKey(
|
||||||
eventUpdate.roomID,
|
eventUpdate.roomID,
|
||||||
eventUpdate.content['state_key'],
|
eventUpdate.content['state_key'],
|
||||||
).toString(),
|
).toString(),
|
||||||
eventUpdate.content);
|
eventUpdate.content,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
final key = TupleKey(
|
final key = TupleKey(
|
||||||
eventUpdate.roomID,
|
eventUpdate.roomID,
|
||||||
|
|
@ -1177,33 +1245,39 @@ class HiveCollectionsDatabase extends DatabaseApi {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> storeInboundGroupSession(
|
Future<void> storeInboundGroupSession(
|
||||||
String roomId,
|
String roomId,
|
||||||
String sessionId,
|
String sessionId,
|
||||||
String pickle,
|
String pickle,
|
||||||
String content,
|
String content,
|
||||||
String indexes,
|
String indexes,
|
||||||
String allowedAtIndex,
|
String allowedAtIndex,
|
||||||
String senderKey,
|
String senderKey,
|
||||||
String senderClaimedKey) async {
|
String senderClaimedKey,
|
||||||
|
) async {
|
||||||
await _inboundGroupSessionsBox.put(
|
await _inboundGroupSessionsBox.put(
|
||||||
sessionId,
|
sessionId,
|
||||||
StoredInboundGroupSession(
|
StoredInboundGroupSession(
|
||||||
roomId: roomId,
|
roomId: roomId,
|
||||||
sessionId: sessionId,
|
sessionId: sessionId,
|
||||||
pickle: pickle,
|
pickle: pickle,
|
||||||
content: content,
|
content: content,
|
||||||
indexes: indexes,
|
indexes: indexes,
|
||||||
allowedAtIndex: allowedAtIndex,
|
allowedAtIndex: allowedAtIndex,
|
||||||
senderKey: senderKey,
|
senderKey: senderKey,
|
||||||
senderClaimedKeys: senderClaimedKey,
|
senderClaimedKeys: senderClaimedKey,
|
||||||
uploaded: false,
|
uploaded: false,
|
||||||
).toJson());
|
).toJson(),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> storeOutboundGroupSession(
|
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>{
|
await _outboundGroupSessionsBox.put(roomId, <String, dynamic>{
|
||||||
'room_id': roomId,
|
'room_id': roomId,
|
||||||
'pickle': pickle,
|
'pickle': pickle,
|
||||||
|
|
@ -1243,49 +1317,52 @@ class HiveCollectionsDatabase extends DatabaseApi {
|
||||||
final currentRawRoom = await _roomsBox.get(roomId);
|
final currentRawRoom = await _roomsBox.get(roomId);
|
||||||
if (currentRawRoom == null) {
|
if (currentRawRoom == null) {
|
||||||
await _roomsBox.put(
|
await _roomsBox.put(
|
||||||
roomId,
|
roomId,
|
||||||
roomUpdate is JoinedRoomUpdate
|
roomUpdate is JoinedRoomUpdate
|
||||||
? Room(
|
? Room(
|
||||||
client: client,
|
client: client,
|
||||||
id: roomId,
|
id: roomId,
|
||||||
membership: membership,
|
membership: membership,
|
||||||
highlightCount:
|
highlightCount:
|
||||||
roomUpdate.unreadNotifications?.highlightCount?.toInt() ??
|
roomUpdate.unreadNotifications?.highlightCount?.toInt() ??
|
||||||
0,
|
0,
|
||||||
notificationCount: roomUpdate
|
notificationCount: roomUpdate
|
||||||
.unreadNotifications?.notificationCount
|
.unreadNotifications?.notificationCount
|
||||||
?.toInt() ??
|
?.toInt() ??
|
||||||
0,
|
0,
|
||||||
prev_batch: roomUpdate.timeline?.prevBatch,
|
prev_batch: roomUpdate.timeline?.prevBatch,
|
||||||
summary: roomUpdate.summary,
|
summary: roomUpdate.summary,
|
||||||
lastEvent: lastEvent,
|
lastEvent: lastEvent,
|
||||||
).toJson()
|
).toJson()
|
||||||
: Room(
|
: Room(
|
||||||
client: client,
|
client: client,
|
||||||
id: roomId,
|
id: roomId,
|
||||||
membership: membership,
|
membership: membership,
|
||||||
lastEvent: lastEvent,
|
lastEvent: lastEvent,
|
||||||
).toJson());
|
).toJson(),
|
||||||
|
);
|
||||||
} else if (roomUpdate is JoinedRoomUpdate) {
|
} else if (roomUpdate is JoinedRoomUpdate) {
|
||||||
final currentRoom = Room.fromJson(copyMap(currentRawRoom), client);
|
final currentRoom = Room.fromJson(copyMap(currentRawRoom), client);
|
||||||
await _roomsBox.put(
|
await _roomsBox.put(
|
||||||
roomId,
|
roomId,
|
||||||
Room(
|
Room(
|
||||||
client: client,
|
client: client,
|
||||||
id: roomId,
|
id: roomId,
|
||||||
membership: membership,
|
membership: membership,
|
||||||
highlightCount:
|
highlightCount:
|
||||||
roomUpdate.unreadNotifications?.highlightCount?.toInt() ??
|
roomUpdate.unreadNotifications?.highlightCount?.toInt() ??
|
||||||
currentRoom.highlightCount,
|
currentRoom.highlightCount,
|
||||||
notificationCount:
|
notificationCount:
|
||||||
roomUpdate.unreadNotifications?.notificationCount?.toInt() ??
|
roomUpdate.unreadNotifications?.notificationCount?.toInt() ??
|
||||||
currentRoom.notificationCount,
|
currentRoom.notificationCount,
|
||||||
prev_batch:
|
prev_batch: roomUpdate.timeline?.prevBatch ?? currentRoom.prev_batch,
|
||||||
roomUpdate.timeline?.prevBatch ?? currentRoom.prev_batch,
|
summary: RoomSummary.fromJson(
|
||||||
summary: RoomSummary.fromJson(currentRoom.summary.toJson()
|
currentRoom.summary.toJson()
|
||||||
..addAll(roomUpdate.summary?.toJson() ?? {})),
|
..addAll(roomUpdate.summary?.toJson() ?? {}),
|
||||||
lastEvent: lastEvent,
|
),
|
||||||
).toJson());
|
lastEvent: lastEvent,
|
||||||
|
).toJson(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1295,15 +1372,20 @@ class HiveCollectionsDatabase extends DatabaseApi {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> storeSSSSCache(
|
Future<void> storeSSSSCache(
|
||||||
String type, String keyId, String ciphertext, String content) async {
|
String type,
|
||||||
|
String keyId,
|
||||||
|
String ciphertext,
|
||||||
|
String content,
|
||||||
|
) async {
|
||||||
await _ssssCacheBox.put(
|
await _ssssCacheBox.put(
|
||||||
type,
|
type,
|
||||||
SSSSCache(
|
SSSSCache(
|
||||||
type: type,
|
type: type,
|
||||||
keyId: keyId,
|
keyId: keyId,
|
||||||
ciphertext: ciphertext,
|
ciphertext: ciphertext,
|
||||||
content: content,
|
content: content,
|
||||||
).toJson());
|
).toJson(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -1314,8 +1396,13 @@ class HiveCollectionsDatabase extends DatabaseApi {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> storeUserCrossSigningKey(String userId, String publicKey,
|
Future<void> storeUserCrossSigningKey(
|
||||||
String content, bool verified, bool blocked) async {
|
String userId,
|
||||||
|
String publicKey,
|
||||||
|
String content,
|
||||||
|
bool verified,
|
||||||
|
bool blocked,
|
||||||
|
) async {
|
||||||
await _userCrossSigningKeysBox.put(
|
await _userCrossSigningKeysBox.put(
|
||||||
TupleKey(userId, publicKey).toString(),
|
TupleKey(userId, publicKey).toString(),
|
||||||
{
|
{
|
||||||
|
|
@ -1329,8 +1416,14 @@ class HiveCollectionsDatabase extends DatabaseApi {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> storeUserDeviceKey(String userId, String deviceId,
|
Future<void> storeUserDeviceKey(
|
||||||
String content, bool verified, bool blocked, int lastActive) async {
|
String userId,
|
||||||
|
String deviceId,
|
||||||
|
String content,
|
||||||
|
bool verified,
|
||||||
|
bool blocked,
|
||||||
|
int lastActive,
|
||||||
|
) async {
|
||||||
await _userDeviceKeysBox.put(TupleKey(userId, deviceId).toString(), {
|
await _userDeviceKeysBox.put(TupleKey(userId, deviceId).toString(), {
|
||||||
'user_id': userId,
|
'user_id': userId,
|
||||||
'device_id': deviceId,
|
'device_id': deviceId,
|
||||||
|
|
@ -1371,8 +1464,10 @@ class HiveCollectionsDatabase extends DatabaseApi {
|
||||||
if (tokenExpiresAt == null) {
|
if (tokenExpiresAt == null) {
|
||||||
await _clientBox.delete('token_expires_at');
|
await _clientBox.delete('token_expires_at');
|
||||||
} else {
|
} else {
|
||||||
await _clientBox.put('token_expires_at',
|
await _clientBox.put(
|
||||||
tokenExpiresAt.millisecondsSinceEpoch.toString());
|
'token_expires_at',
|
||||||
|
tokenExpiresAt.millisecondsSinceEpoch.toString(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (refreshToken == null) {
|
if (refreshToken == null) {
|
||||||
await _clientBox.delete('refresh_token');
|
await _clientBox.delete('refresh_token');
|
||||||
|
|
@ -1414,11 +1509,15 @@ class HiveCollectionsDatabase extends DatabaseApi {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> updateInboundGroupSessionAllowedAtIndex(
|
Future<void> updateInboundGroupSessionAllowedAtIndex(
|
||||||
String allowedAtIndex, String roomId, String sessionId) async {
|
String allowedAtIndex,
|
||||||
|
String roomId,
|
||||||
|
String sessionId,
|
||||||
|
) async {
|
||||||
final raw = await _inboundGroupSessionsBox.get(sessionId);
|
final raw = await _inboundGroupSessionsBox.get(sessionId);
|
||||||
if (raw == null) {
|
if (raw == null) {
|
||||||
Logs().w(
|
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;
|
return;
|
||||||
}
|
}
|
||||||
raw['allowed_at_index'] = allowedAtIndex;
|
raw['allowed_at_index'] = allowedAtIndex;
|
||||||
|
|
@ -1428,11 +1527,15 @@ class HiveCollectionsDatabase extends DatabaseApi {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> updateInboundGroupSessionIndexes(
|
Future<void> updateInboundGroupSessionIndexes(
|
||||||
String indexes, String roomId, String sessionId) async {
|
String indexes,
|
||||||
|
String roomId,
|
||||||
|
String sessionId,
|
||||||
|
) async {
|
||||||
final raw = await _inboundGroupSessionsBox.get(sessionId);
|
final raw = await _inboundGroupSessionsBox.get(sessionId);
|
||||||
if (raw == null) {
|
if (raw == null) {
|
||||||
Logs().w(
|
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;
|
return;
|
||||||
}
|
}
|
||||||
final json = copyMap(raw);
|
final json = copyMap(raw);
|
||||||
|
|
@ -1552,11 +1655,15 @@ class HiveCollectionsDatabase extends DatabaseApi {
|
||||||
}
|
}
|
||||||
for (final key in json[_inboundGroupSessionsBoxName]!.keys) {
|
for (final key in json[_inboundGroupSessionsBoxName]!.keys) {
|
||||||
await _inboundGroupSessionsBox.put(
|
await _inboundGroupSessionsBox.put(
|
||||||
key, json[_inboundGroupSessionsBoxName]![key]);
|
key,
|
||||||
|
json[_inboundGroupSessionsBoxName]![key],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
for (final key in json[_outboundGroupSessionsBoxName]!.keys) {
|
for (final key in json[_outboundGroupSessionsBoxName]!.keys) {
|
||||||
await _outboundGroupSessionsBox.put(
|
await _outboundGroupSessionsBox.put(
|
||||||
key, json[_outboundGroupSessionsBoxName]![key]);
|
key,
|
||||||
|
json[_outboundGroupSessionsBoxName]![key],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
for (final key in json[_olmSessionsBoxName]!.keys) {
|
for (final key in json[_olmSessionsBoxName]!.keys) {
|
||||||
await _olmSessionsBox.put(key, json[_olmSessionsBoxName]![key]);
|
await _olmSessionsBox.put(key, json[_olmSessionsBoxName]![key]);
|
||||||
|
|
@ -1566,11 +1673,15 @@ class HiveCollectionsDatabase extends DatabaseApi {
|
||||||
}
|
}
|
||||||
for (final key in json[_userDeviceKeysOutdatedBoxName]!.keys) {
|
for (final key in json[_userDeviceKeysOutdatedBoxName]!.keys) {
|
||||||
await _userDeviceKeysOutdatedBox.put(
|
await _userDeviceKeysOutdatedBox.put(
|
||||||
key, json[_userDeviceKeysOutdatedBoxName]![key]);
|
key,
|
||||||
|
json[_userDeviceKeysOutdatedBoxName]![key],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
for (final key in json[_userCrossSigningKeysBoxName]!.keys) {
|
for (final key in json[_userCrossSigningKeysBoxName]!.keys) {
|
||||||
await _userCrossSigningKeysBox.put(
|
await _userCrossSigningKeysBox.put(
|
||||||
key, json[_userCrossSigningKeysBoxName]![key]);
|
key,
|
||||||
|
json[_userCrossSigningKeysBoxName]![key],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
for (final key in json[_ssssCacheBoxName]!.keys) {
|
for (final key in json[_ssssCacheBoxName]!.keys) {
|
||||||
await _ssssCacheBox.put(key, json[_ssssCacheBoxName]![key]);
|
await _ssssCacheBox.put(key, json[_ssssCacheBoxName]![key]);
|
||||||
|
|
@ -1580,7 +1691,9 @@ class HiveCollectionsDatabase extends DatabaseApi {
|
||||||
}
|
}
|
||||||
for (final key in json[_timelineFragmentsBoxName]!.keys) {
|
for (final key in json[_timelineFragmentsBoxName]!.keys) {
|
||||||
await _timelineFragmentsBox.put(
|
await _timelineFragmentsBox.put(
|
||||||
key, json[_timelineFragmentsBoxName]![key]);
|
key,
|
||||||
|
json[_timelineFragmentsBoxName]![key],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
for (final key in json[_seenDeviceIdsBoxName]!.keys) {
|
for (final key in json[_seenDeviceIdsBoxName]!.keys) {
|
||||||
await _seenDeviceIdsBox.put(key, json[_seenDeviceIdsBoxName]![key]);
|
await _seenDeviceIdsBox.put(key, json[_seenDeviceIdsBoxName]![key]);
|
||||||
|
|
@ -1629,7 +1742,9 @@ class HiveCollectionsDatabase extends DatabaseApi {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> storeUserProfile(
|
Future<void> storeUserProfile(
|
||||||
String userId, CachedProfileInformation profile) async {
|
String userId,
|
||||||
|
CachedProfileInformation profile,
|
||||||
|
) async {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,8 @@ import 'package:matrix/src/utils/run_benchmarked.dart';
|
||||||
///
|
///
|
||||||
/// This database does not support file caching!
|
/// This database does not support file caching!
|
||||||
@Deprecated(
|
@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 {
|
class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
|
||||||
static const int version = 6;
|
static const int version = 6;
|
||||||
final String name;
|
final String name;
|
||||||
|
|
@ -254,11 +255,16 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
|
||||||
convertToJson(raw),
|
convertToJson(raw),
|
||||||
Client(''),
|
Client(''),
|
||||||
);
|
);
|
||||||
await addSeenDeviceId(deviceKeys.userId, deviceKeys.deviceId!,
|
await addSeenDeviceId(
|
||||||
deviceKeys.curve25519Key! + deviceKeys.ed25519Key!);
|
deviceKeys.userId,
|
||||||
|
deviceKeys.deviceId!,
|
||||||
|
deviceKeys.curve25519Key! + deviceKeys.ed25519Key!,
|
||||||
|
);
|
||||||
await addSeenPublicKey(deviceKeys.ed25519Key!, deviceKeys.deviceId!);
|
await addSeenPublicKey(deviceKeys.ed25519Key!, deviceKeys.deviceId!);
|
||||||
await addSeenPublicKey(
|
await addSeenPublicKey(
|
||||||
deviceKeys.curve25519Key!, deviceKeys.deviceId!);
|
deviceKeys.curve25519Key!,
|
||||||
|
deviceKeys.deviceId!,
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Logs().w('Can not migrate device $key', e);
|
Logs().w('Can not migrate device $key', e);
|
||||||
}
|
}
|
||||||
|
|
@ -344,18 +350,21 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map<String, BasicEvent>> getAccountData() =>
|
Future<Map<String, BasicEvent>> getAccountData() =>
|
||||||
runBenchmarked<Map<String, BasicEvent>>('Get all account data from Hive',
|
runBenchmarked<Map<String, BasicEvent>>(
|
||||||
() async {
|
'Get all account data from Hive',
|
||||||
final accountData = <String, BasicEvent>{};
|
() async {
|
||||||
for (final key in _accountDataBox.keys) {
|
final accountData = <String, BasicEvent>{};
|
||||||
final raw = await _accountDataBox.get(key);
|
for (final key in _accountDataBox.keys) {
|
||||||
accountData[key.toString().fromHiveKey] = BasicEvent(
|
final raw = await _accountDataBox.get(key);
|
||||||
type: key.toString().fromHiveKey,
|
accountData[key.toString().fromHiveKey] = BasicEvent(
|
||||||
content: convertToJson(raw),
|
type: key.toString().fromHiveKey,
|
||||||
);
|
content: convertToJson(raw),
|
||||||
}
|
);
|
||||||
return accountData;
|
}
|
||||||
}, _accountDataBox.keys.length);
|
return accountData;
|
||||||
|
},
|
||||||
|
_accountDataBox.keys.length,
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map<String, dynamic>?> getClient(String name) =>
|
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
|
/// 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 {
|
Future<List<Event>> _getEventsByIds(List<String> eventIds, Room room) async {
|
||||||
final events = await Future.wait(eventIds.map((String eventId) async {
|
final events = await Future.wait(
|
||||||
final entry = await _eventsBox.get(MultiKey(room.id, eventId).toString());
|
eventIds.map((String eventId) async {
|
||||||
return entry is Map ? Event.fromJson(convertToJson(entry), room) : null;
|
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();
|
return events.whereType<Event>().toList();
|
||||||
}
|
}
|
||||||
|
|
@ -397,7 +409,8 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
|
||||||
// Get the synced event IDs from the store
|
// Get the synced event IDs from the store
|
||||||
final timelineKey = MultiKey(room.id, '').toString();
|
final timelineKey = MultiKey(room.id, '').toString();
|
||||||
final timelineEventIds = List<String>.from(
|
final timelineEventIds = List<String>.from(
|
||||||
(await _timelineFragmentsBox.get(timelineKey)) ?? []);
|
(await _timelineFragmentsBox.get(timelineKey)) ?? [],
|
||||||
|
);
|
||||||
// Get the local stored SENDING events from the store
|
// Get the local stored SENDING events from the store
|
||||||
late final List sendingEventIds;
|
late final List sendingEventIds;
|
||||||
if (start != 0) {
|
if (start != 0) {
|
||||||
|
|
@ -405,18 +418,21 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
|
||||||
} else {
|
} else {
|
||||||
final sendingTimelineKey = MultiKey(room.id, 'SENDING').toString();
|
final sendingTimelineKey = MultiKey(room.id, 'SENDING').toString();
|
||||||
sendingEventIds = List<String>.from(
|
sendingEventIds = List<String>.from(
|
||||||
(await _timelineFragmentsBox.get(sendingTimelineKey)) ?? []);
|
(await _timelineFragmentsBox.get(sendingTimelineKey)) ?? [],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Combine those two lists while respecting the start and limit parameters.
|
// Combine those two lists while respecting the start and limit parameters.
|
||||||
final end = min(timelineEventIds.length,
|
final end = min(
|
||||||
start + (limit ?? timelineEventIds.length));
|
timelineEventIds.length,
|
||||||
|
start + (limit ?? timelineEventIds.length),
|
||||||
|
);
|
||||||
final eventIds = List<String>.from(
|
final eventIds = List<String>.from(
|
||||||
[
|
[
|
||||||
...sendingEventIds,
|
...sendingEventIds,
|
||||||
...(start < timelineEventIds.length && !onlySending
|
...(start < timelineEventIds.length && !onlySending
|
||||||
? timelineEventIds.getRange(start, end).toList()
|
? timelineEventIds.getRange(start, end).toList()
|
||||||
: [])
|
: []),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -435,7 +451,8 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
|
||||||
final timelineKey = MultiKey(room.id, '').toString();
|
final timelineKey = MultiKey(room.id, '').toString();
|
||||||
|
|
||||||
final timelineEventIds = List<String>.from(
|
final timelineEventIds = List<String>.from(
|
||||||
(await _timelineFragmentsBox.get(timelineKey)) ?? []);
|
(await _timelineFragmentsBox.get(timelineKey)) ?? [],
|
||||||
|
);
|
||||||
|
|
||||||
// Get the local stored SENDING events from the store
|
// Get the local stored SENDING events from the store
|
||||||
late final List<String> sendingEventIds;
|
late final List<String> sendingEventIds;
|
||||||
|
|
@ -444,7 +461,8 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
|
||||||
} else {
|
} else {
|
||||||
final sendingTimelineKey = MultiKey(room.id, 'SENDING').toString();
|
final sendingTimelineKey = MultiKey(room.id, 'SENDING').toString();
|
||||||
sendingEventIds = List<String>.from(
|
sendingEventIds = List<String>.from(
|
||||||
(await _timelineFragmentsBox.get(sendingTimelineKey)) ?? []);
|
(await _timelineFragmentsBox.get(sendingTimelineKey)) ?? [],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Combine those two lists while respecting the start and limit parameters.
|
// Combine those two lists while respecting the start and limit parameters.
|
||||||
|
|
@ -474,9 +492,11 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
|
||||||
@override
|
@override
|
||||||
Future<List<StoredInboundGroupSession>>
|
Future<List<StoredInboundGroupSession>>
|
||||||
getInboundGroupSessionsToUpload() async {
|
getInboundGroupSessionsToUpload() async {
|
||||||
final sessions = (await Future.wait(_inboundGroupSessionsBox.keys.map(
|
final sessions = (await Future.wait(
|
||||||
(sessionId) async =>
|
_inboundGroupSessionsBox.keys.map(
|
||||||
await _inboundGroupSessionsBox.get(sessionId))))
|
(sessionId) async => await _inboundGroupSessionsBox.get(sessionId),
|
||||||
|
),
|
||||||
|
))
|
||||||
.where((rawSession) => rawSession['uploaded'] == false)
|
.where((rawSession) => rawSession['uploaded'] == false)
|
||||||
.take(500)
|
.take(500)
|
||||||
.map(
|
.map(
|
||||||
|
|
@ -490,7 +510,9 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<String>> getLastSentMessageUserDeviceKey(
|
Future<List<String>> getLastSentMessageUserDeviceKey(
|
||||||
String userId, String deviceId) async {
|
String userId,
|
||||||
|
String deviceId,
|
||||||
|
) async {
|
||||||
final raw =
|
final raw =
|
||||||
await _userDeviceKeysBox.get(MultiKey(userId, deviceId).toString());
|
await _userDeviceKeysBox.get(MultiKey(userId, deviceId).toString());
|
||||||
if (raw == null) return <String>[];
|
if (raw == null) return <String>[];
|
||||||
|
|
@ -498,8 +520,12 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> storeOlmSession(String identityKey, String sessionId,
|
Future<void> storeOlmSession(
|
||||||
String pickle, int lastReceived) async {
|
String identityKey,
|
||||||
|
String sessionId,
|
||||||
|
String pickle,
|
||||||
|
int lastReceived,
|
||||||
|
) async {
|
||||||
final rawSessions =
|
final rawSessions =
|
||||||
(await _olmSessionsBox.get(identityKey.toHiveKey) as Map?) ?? {};
|
(await _olmSessionsBox.get(identityKey.toHiveKey) as Map?) ?? {};
|
||||||
rawSessions[sessionId] = <String, dynamic>{
|
rawSessions[sessionId] = <String, dynamic>{
|
||||||
|
|
@ -514,7 +540,9 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<OlmSession>> getOlmSessions(
|
Future<List<OlmSession>> getOlmSessions(
|
||||||
String identityKey, String userId) async {
|
String identityKey,
|
||||||
|
String userId,
|
||||||
|
) async {
|
||||||
final rawSessions =
|
final rawSessions =
|
||||||
await _olmSessionsBox.get(identityKey.toHiveKey) as Map? ?? {};
|
await _olmSessionsBox.get(identityKey.toHiveKey) as Map? ?? {};
|
||||||
|
|
||||||
|
|
@ -540,23 +568,31 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<OlmSession>> getOlmSessionsForDevices(
|
Future<List<OlmSession>> getOlmSessionsForDevices(
|
||||||
List<String> identityKeys, String userId) async {
|
List<String> identityKeys,
|
||||||
|
String userId,
|
||||||
|
) async {
|
||||||
final sessions = await Future.wait(
|
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];
|
return <OlmSession>[for (final sublist in sessions) ...sublist];
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<OutboundGroupSession?> getOutboundGroupSession(
|
Future<OutboundGroupSession?> getOutboundGroupSession(
|
||||||
String roomId, String userId) async {
|
String roomId,
|
||||||
|
String userId,
|
||||||
|
) async {
|
||||||
final raw = await _outboundGroupSessionsBox.get(roomId.toHiveKey);
|
final raw = await _outboundGroupSessionsBox.get(roomId.toHiveKey);
|
||||||
if (raw == null) return null;
|
if (raw == null) return null;
|
||||||
return OutboundGroupSession.fromJson(convertToJson(raw), userId);
|
return OutboundGroupSession.fromJson(convertToJson(raw), userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Room?> getSingleRoom(Client client, String roomId,
|
Future<Room?> getSingleRoom(
|
||||||
{bool loadImportantStates = true}) async {
|
Client client,
|
||||||
|
String roomId, {
|
||||||
|
bool loadImportantStates = true,
|
||||||
|
}) async {
|
||||||
// Get raw room from database:
|
// Get raw room from database:
|
||||||
final roomData = await _roomsBox.get(roomId);
|
final roomData = await _roomsBox.get(roomId);
|
||||||
if (roomData == null) return null;
|
if (roomData == null) return null;
|
||||||
|
|
@ -580,88 +616,96 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<Room>> getRoomList(Client client) =>
|
Future<List<Room>> getRoomList(Client client) => runBenchmarked<List<Room>>(
|
||||||
runBenchmarked<List<Room>>('Get room list from hive', () async {
|
'Get room list from hive',
|
||||||
final rooms = <String, Room>{};
|
() async {
|
||||||
final userID = client.userID;
|
final rooms = <String, Room>{};
|
||||||
final importantRoomStates = client.importantStateEvents;
|
final userID = client.userID;
|
||||||
for (final key in _roomsBox.keys) {
|
final importantRoomStates = client.importantStateEvents;
|
||||||
// Get the room
|
for (final key in _roomsBox.keys) {
|
||||||
final raw = await _roomsBox.get(key);
|
// Get the room
|
||||||
final room = Room.fromJson(convertToJson(raw), client);
|
final raw = await _roomsBox.get(key);
|
||||||
|
final room = Room.fromJson(convertToJson(raw), client);
|
||||||
|
|
||||||
// let's see if we need any m.room.member events
|
// let's see if we need any m.room.member events
|
||||||
// We always need the member event for ourself
|
// We always need the member event for ourself
|
||||||
final membersToPostload = <String>{if (userID != null) userID};
|
final membersToPostload = <String>{if (userID != null) userID};
|
||||||
// If the room is a direct chat, those IDs should be there too
|
// If the room is a direct chat, those IDs should be there too
|
||||||
if (room.isDirectChat) {
|
if (room.isDirectChat) {
|
||||||
membersToPostload.add(room.directChatMatrixID!);
|
membersToPostload.add(room.directChatMatrixID!);
|
||||||
}
|
|
||||||
// the lastEvent message preview might have an author we need to fetch, if it is a group chat
|
|
||||||
final lastEvent = room.getState(EventTypes.Message);
|
|
||||||
if (lastEvent != null && !room.isDirectChat) {
|
|
||||||
membersToPostload.add(lastEvent.senderId);
|
|
||||||
}
|
|
||||||
// if the room has no name and no canonical alias, its name is calculated
|
|
||||||
// based on the heroes of the room
|
|
||||||
if (room.getState(EventTypes.RoomName) == null &&
|
|
||||||
room.getState(EventTypes.RoomCanonicalAlias) == null) {
|
|
||||||
// we don't have a name and no canonical alias, so we'll need to
|
|
||||||
// post-load the heroes
|
|
||||||
membersToPostload.addAll(room.summary.mHeroes ?? []);
|
|
||||||
}
|
|
||||||
// Load members
|
|
||||||
for (final userId in membersToPostload) {
|
|
||||||
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
|
// the lastEvent message preview might have an author we need to fetch, if it is a group chat
|
||||||
? StrippedStateEvent.fromJson(copyMap(raw))
|
final lastEvent = room.getState(EventTypes.Message);
|
||||||
: Event.fromJson(convertToJson(state), room));
|
if (lastEvent != null && !room.isDirectChat) {
|
||||||
}
|
membersToPostload.add(lastEvent.senderId);
|
||||||
|
}
|
||||||
// Get the "important" room states. All other states will be loaded once
|
// if the room has no name and no canonical alias, its name is calculated
|
||||||
// `getUnimportantRoomStates()` is called.
|
// based on the heroes of the room
|
||||||
for (final type in importantRoomStates) {
|
if (room.getState(EventTypes.RoomName) == null &&
|
||||||
final states = await _roomStateBox
|
room.getState(EventTypes.RoomCanonicalAlias) == null) {
|
||||||
.get(MultiKey(room.id, type).toString()) as Map?;
|
// we don't have a name and no canonical alias, so we'll need to
|
||||||
if (states == null) continue;
|
// post-load the heroes
|
||||||
final stateEvents = states.values
|
membersToPostload.addAll(room.summary.mHeroes ?? []);
|
||||||
.map((raw) => room.membership == Membership.invite
|
}
|
||||||
|
// Load members
|
||||||
|
for (final userId in membersToPostload) {
|
||||||
|
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
|
||||||
? StrippedStateEvent.fromJson(copyMap(raw))
|
? StrippedStateEvent.fromJson(copyMap(raw))
|
||||||
: Event.fromJson(convertToJson(raw), room))
|
: Event.fromJson(convertToJson(state), room),
|
||||||
.toList();
|
);
|
||||||
for (final state in stateEvents) {
|
}
|
||||||
room.setState(state);
|
|
||||||
|
// Get the "important" room states. All other states will be loaded once
|
||||||
|
// `getUnimportantRoomStates()` is called.
|
||||||
|
for (final type in importantRoomStates) {
|
||||||
|
final states = await _roomStateBox
|
||||||
|
.get(MultiKey(room.id, type).toString()) as Map?;
|
||||||
|
if (states == null) continue;
|
||||||
|
final stateEvents = states.values
|
||||||
|
.map(
|
||||||
|
(raw) => room.membership == Membership.invite
|
||||||
|
? StrippedStateEvent.fromJson(copyMap(raw))
|
||||||
|
: Event.fromJson(convertToJson(raw), room),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
for (final state in stateEvents) {
|
||||||
|
room.setState(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to the list and continue.
|
||||||
|
rooms[room.id] = room;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the room account data
|
||||||
|
for (final key in _roomAccountDataBox.keys) {
|
||||||
|
final roomId = MultiKey.fromString(key).parts.first;
|
||||||
|
if (rooms.containsKey(roomId)) {
|
||||||
|
final raw = await _roomAccountDataBox.get(key);
|
||||||
|
final basicRoomEvent = BasicRoomEvent.fromJson(
|
||||||
|
convertToJson(raw),
|
||||||
|
);
|
||||||
|
rooms[roomId]!.roomAccountData[basicRoomEvent.type] =
|
||||||
|
basicRoomEvent;
|
||||||
|
} else {
|
||||||
|
Logs().w(
|
||||||
|
'Found account data for unknown room $roomId. Delete now...',
|
||||||
|
);
|
||||||
|
await _roomAccountDataBox.delete(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add to the list and continue.
|
return rooms.values.toList();
|
||||||
rooms[room.id] = room;
|
},
|
||||||
}
|
_roomsBox.keys.length,
|
||||||
|
);
|
||||||
// Get the room account data
|
|
||||||
for (final key in _roomAccountDataBox.keys) {
|
|
||||||
final roomId = MultiKey.fromString(key).parts.first;
|
|
||||||
if (rooms.containsKey(roomId)) {
|
|
||||||
final raw = await _roomAccountDataBox.get(key);
|
|
||||||
final basicRoomEvent = BasicRoomEvent.fromJson(
|
|
||||||
convertToJson(raw),
|
|
||||||
);
|
|
||||||
rooms[roomId]!.roomAccountData[basicRoomEvent.type] =
|
|
||||||
basicRoomEvent;
|
|
||||||
} else {
|
|
||||||
Logs().w(
|
|
||||||
'Found account data for unknown room $roomId. Delete now...');
|
|
||||||
await _roomAccountDataBox.delete(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return rooms.values.toList();
|
|
||||||
}, _roomsBox.keys.length);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<SSSSCache?> getSSSSCache(String type) async {
|
Future<SSSSCache?> getSSSSCache(String type) async {
|
||||||
|
|
@ -672,15 +716,19 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<QueuedToDeviceEvent>> getToDeviceEventQueue() async =>
|
Future<List<QueuedToDeviceEvent>> getToDeviceEventQueue() async =>
|
||||||
await Future.wait(_toDeviceQueueBox.keys.map((i) async {
|
await Future.wait(
|
||||||
final raw = await _toDeviceQueueBox.get(i);
|
_toDeviceQueueBox.keys.map((i) async {
|
||||||
raw['id'] = i;
|
final raw = await _toDeviceQueueBox.get(i);
|
||||||
return QueuedToDeviceEvent.fromJson(convertToJson(raw));
|
raw['id'] = i;
|
||||||
}).toList());
|
return QueuedToDeviceEvent.fromJson(convertToJson(raw));
|
||||||
|
}).toList(),
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<Event>> getUnimportantRoomEventStatesForRoom(
|
Future<List<Event>> getUnimportantRoomEventStatesForRoom(
|
||||||
List<String> events, Room room) async {
|
List<String> events,
|
||||||
|
Room room,
|
||||||
|
) async {
|
||||||
final keys = _roomStateBox.keys.where((key) {
|
final keys = _roomStateBox.keys.where((key) {
|
||||||
final tuple = MultiKey.fromString(key);
|
final tuple = MultiKey.fromString(key);
|
||||||
return tuple.parts.first == room.id && !events.contains(tuple.parts[1]);
|
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) {
|
for (final key in keys) {
|
||||||
final Map states = await _roomStateBox.get(key);
|
final Map states = await _roomStateBox.get(key);
|
||||||
unimportantEvents.addAll(
|
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();
|
return unimportantEvents.where((event) => event.stateKey != null).toList();
|
||||||
}
|
}
|
||||||
|
|
@ -706,36 +755,48 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
|
||||||
@override
|
@override
|
||||||
Future<Map<String, DeviceKeysList>> getUserDeviceKeys(Client client) =>
|
Future<Map<String, DeviceKeysList>> getUserDeviceKeys(Client client) =>
|
||||||
runBenchmarked<Map<String, DeviceKeysList>>(
|
runBenchmarked<Map<String, DeviceKeysList>>(
|
||||||
'Get all user device keys from Hive', () async {
|
'Get all user device keys from Hive',
|
||||||
final deviceKeysOutdated = _userDeviceKeysOutdatedBox.keys;
|
() async {
|
||||||
if (deviceKeysOutdated.isEmpty) {
|
final deviceKeysOutdated = _userDeviceKeysOutdatedBox.keys;
|
||||||
return {};
|
if (deviceKeysOutdated.isEmpty) {
|
||||||
}
|
return {};
|
||||||
final res = <String, DeviceKeysList>{};
|
}
|
||||||
for (final userId in deviceKeysOutdated) {
|
final res = <String, DeviceKeysList>{};
|
||||||
final deviceKeysBoxKeys = _userDeviceKeysBox.keys.where((tuple) {
|
for (final userId in deviceKeysOutdated) {
|
||||||
final tupleKey = MultiKey.fromString(tuple);
|
final deviceKeysBoxKeys = _userDeviceKeysBox.keys.where((tuple) {
|
||||||
return tupleKey.parts.first == userId;
|
final tupleKey = MultiKey.fromString(tuple);
|
||||||
});
|
return tupleKey.parts.first == userId;
|
||||||
final crossSigningKeysBoxKeys =
|
});
|
||||||
_userCrossSigningKeysBox.keys.where((tuple) {
|
final crossSigningKeysBoxKeys =
|
||||||
final tupleKey = MultiKey.fromString(tuple);
|
_userCrossSigningKeysBox.keys.where((tuple) {
|
||||||
return tupleKey.parts.first == userId;
|
final tupleKey = MultiKey.fromString(tuple);
|
||||||
});
|
return tupleKey.parts.first == userId;
|
||||||
res[userId] = DeviceKeysList.fromDbJson(
|
});
|
||||||
|
res[userId] = DeviceKeysList.fromDbJson(
|
||||||
{
|
{
|
||||||
'client_id': client.id,
|
'client_id': client.id,
|
||||||
'user_id': userId,
|
'user_id': userId,
|
||||||
'outdated': await _userDeviceKeysOutdatedBox.get(userId),
|
'outdated': await _userDeviceKeysOutdatedBox.get(userId),
|
||||||
},
|
},
|
||||||
await Future.wait(deviceKeysBoxKeys.map((key) async =>
|
await Future.wait(
|
||||||
convertToJson(await _userDeviceKeysBox.get(key)))),
|
deviceKeysBoxKeys.map(
|
||||||
await Future.wait(crossSigningKeysBoxKeys.map((key) async =>
|
(key) async =>
|
||||||
convertToJson(await _userCrossSigningKeysBox.get(key)))),
|
convertToJson(await _userDeviceKeysBox.get(key)),
|
||||||
client);
|
),
|
||||||
}
|
),
|
||||||
return res;
|
await Future.wait(
|
||||||
}, _userDeviceKeysBox.keys.length);
|
crossSigningKeysBoxKeys.map(
|
||||||
|
(key) async =>
|
||||||
|
convertToJson(await _userCrossSigningKeysBox.get(key)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
client,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
_userDeviceKeysBox.keys.length,
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<User>> getUsers(Room room) async {
|
Future<List<User>> getUsers(Room room) async {
|
||||||
|
|
@ -751,20 +812,23 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> insertClient(
|
Future<void> insertClient(
|
||||||
String name,
|
String name,
|
||||||
String homeserverUrl,
|
String homeserverUrl,
|
||||||
String token,
|
String token,
|
||||||
DateTime? tokenExpiresAt,
|
DateTime? tokenExpiresAt,
|
||||||
String? refreshToken,
|
String? refreshToken,
|
||||||
String userId,
|
String userId,
|
||||||
String? deviceId,
|
String? deviceId,
|
||||||
String? deviceName,
|
String? deviceName,
|
||||||
String? prevBatch,
|
String? prevBatch,
|
||||||
String? olmAccount) async {
|
String? olmAccount,
|
||||||
|
) async {
|
||||||
await _clientBox.put('homeserver_url', homeserverUrl);
|
await _clientBox.put('homeserver_url', homeserverUrl);
|
||||||
await _clientBox.put('token', token);
|
await _clientBox.put('token', token);
|
||||||
await _clientBox.put(
|
await _clientBox.put(
|
||||||
'token_expires_at', tokenExpiresAt?.millisecondsSinceEpoch.toString());
|
'token_expires_at',
|
||||||
|
tokenExpiresAt?.millisecondsSinceEpoch.toString(),
|
||||||
|
);
|
||||||
await _clientBox.put('refresh_token', refreshToken);
|
await _clientBox.put('refresh_token', refreshToken);
|
||||||
await _clientBox.put('user_id', userId);
|
await _clientBox.put('user_id', userId);
|
||||||
await _clientBox.put('device_id', deviceId);
|
await _clientBox.put('device_id', deviceId);
|
||||||
|
|
@ -777,7 +841,10 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<int> insertIntoToDeviceQueue(
|
Future<int> insertIntoToDeviceQueue(
|
||||||
String type, String txnId, String content) async {
|
String type,
|
||||||
|
String txnId,
|
||||||
|
String content,
|
||||||
|
) async {
|
||||||
return await _toDeviceQueueBox.add(<String, dynamic>{
|
return await _toDeviceQueueBox.add(<String, dynamic>{
|
||||||
'type': type,
|
'type': type,
|
||||||
'txn_id': txnId,
|
'txn_id': txnId,
|
||||||
|
|
@ -787,11 +854,14 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> markInboundGroupSessionAsUploaded(
|
Future<void> markInboundGroupSessionAsUploaded(
|
||||||
String roomId, String sessionId) async {
|
String roomId,
|
||||||
|
String sessionId,
|
||||||
|
) async {
|
||||||
final raw = await _inboundGroupSessionsBox.get(sessionId.toHiveKey);
|
final raw = await _inboundGroupSessionsBox.get(sessionId.toHiveKey);
|
||||||
if (raw == null) {
|
if (raw == null) {
|
||||||
Logs().w(
|
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;
|
return;
|
||||||
}
|
}
|
||||||
raw['uploaded'] = true;
|
raw['uploaded'] = true;
|
||||||
|
|
@ -833,7 +903,9 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> removeUserCrossSigningKey(
|
Future<void> removeUserCrossSigningKey(
|
||||||
String userId, String publicKey) async {
|
String userId,
|
||||||
|
String publicKey,
|
||||||
|
) async {
|
||||||
await _userCrossSigningKeysBox
|
await _userCrossSigningKeysBox
|
||||||
.delete(MultiKey(userId, publicKey).toString());
|
.delete(MultiKey(userId, publicKey).toString());
|
||||||
return;
|
return;
|
||||||
|
|
@ -847,7 +919,10 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> setBlockedUserCrossSigningKey(
|
Future<void> setBlockedUserCrossSigningKey(
|
||||||
bool blocked, String userId, String publicKey) async {
|
bool blocked,
|
||||||
|
String userId,
|
||||||
|
String publicKey,
|
||||||
|
) async {
|
||||||
final raw = await _userCrossSigningKeysBox
|
final raw = await _userCrossSigningKeysBox
|
||||||
.get(MultiKey(userId, publicKey).toString());
|
.get(MultiKey(userId, publicKey).toString());
|
||||||
raw['blocked'] = blocked;
|
raw['blocked'] = blocked;
|
||||||
|
|
@ -860,7 +935,10 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> setBlockedUserDeviceKey(
|
Future<void> setBlockedUserDeviceKey(
|
||||||
bool blocked, String userId, String deviceId) async {
|
bool blocked,
|
||||||
|
String userId,
|
||||||
|
String deviceId,
|
||||||
|
) async {
|
||||||
final raw =
|
final raw =
|
||||||
await _userDeviceKeysBox.get(MultiKey(userId, deviceId).toString());
|
await _userDeviceKeysBox.get(MultiKey(userId, deviceId).toString());
|
||||||
raw['blocked'] = blocked;
|
raw['blocked'] = blocked;
|
||||||
|
|
@ -873,7 +951,10 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> setLastActiveUserDeviceKey(
|
Future<void> setLastActiveUserDeviceKey(
|
||||||
int lastActive, String userId, String deviceId) async {
|
int lastActive,
|
||||||
|
String userId,
|
||||||
|
String deviceId,
|
||||||
|
) async {
|
||||||
final raw =
|
final raw =
|
||||||
await _userDeviceKeysBox.get(MultiKey(userId, deviceId).toString());
|
await _userDeviceKeysBox.get(MultiKey(userId, deviceId).toString());
|
||||||
raw['last_active'] = lastActive;
|
raw['last_active'] = lastActive;
|
||||||
|
|
@ -885,7 +966,10 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> setLastSentMessageUserDeviceKey(
|
Future<void> setLastSentMessageUserDeviceKey(
|
||||||
String lastSentMessage, String userId, String deviceId) async {
|
String lastSentMessage,
|
||||||
|
String userId,
|
||||||
|
String deviceId,
|
||||||
|
) async {
|
||||||
final raw =
|
final raw =
|
||||||
await _userDeviceKeysBox.get(MultiKey(userId, deviceId).toString());
|
await _userDeviceKeysBox.get(MultiKey(userId, deviceId).toString());
|
||||||
raw['last_sent_message'] = lastSentMessage;
|
raw['last_sent_message'] = lastSentMessage;
|
||||||
|
|
@ -897,7 +981,10 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> setRoomPrevBatch(
|
Future<void> setRoomPrevBatch(
|
||||||
String? prevBatch, String roomId, Client client) async {
|
String? prevBatch,
|
||||||
|
String roomId,
|
||||||
|
Client client,
|
||||||
|
) async {
|
||||||
final raw = await _roomsBox.get(roomId.toHiveKey);
|
final raw = await _roomsBox.get(roomId.toHiveKey);
|
||||||
if (raw == null) return;
|
if (raw == null) return;
|
||||||
final room = Room.fromJson(convertToJson(raw), client);
|
final room = Room.fromJson(convertToJson(raw), client);
|
||||||
|
|
@ -908,7 +995,10 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> setVerifiedUserCrossSigningKey(
|
Future<void> setVerifiedUserCrossSigningKey(
|
||||||
bool verified, String userId, String publicKey) async {
|
bool verified,
|
||||||
|
String userId,
|
||||||
|
String publicKey,
|
||||||
|
) async {
|
||||||
final raw = (await _userCrossSigningKeysBox
|
final raw = (await _userCrossSigningKeysBox
|
||||||
.get(MultiKey(userId, publicKey).toString()) as Map?) ??
|
.get(MultiKey(userId, publicKey).toString()) as Map?) ??
|
||||||
{};
|
{};
|
||||||
|
|
@ -922,7 +1012,10 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> setVerifiedUserDeviceKey(
|
Future<void> setVerifiedUserDeviceKey(
|
||||||
bool verified, String userId, String deviceId) async {
|
bool verified,
|
||||||
|
String userId,
|
||||||
|
String deviceId,
|
||||||
|
) async {
|
||||||
final raw =
|
final raw =
|
||||||
await _userDeviceKeysBox.get(MultiKey(userId, deviceId).toString());
|
await _userDeviceKeysBox.get(MultiKey(userId, deviceId).toString());
|
||||||
raw['verified'] = verified;
|
raw['verified'] = verified;
|
||||||
|
|
@ -936,7 +1029,9 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
|
||||||
@override
|
@override
|
||||||
Future<void> storeAccountData(String type, String content) async {
|
Future<void> storeAccountData(String type, String content) async {
|
||||||
await _accountDataBox.put(
|
await _accountDataBox.put(
|
||||||
type.toHiveKey, convertToJson(jsonDecode(content)));
|
type.toHiveKey,
|
||||||
|
convertToJson(jsonDecode(content)),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -955,8 +1050,9 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
|
||||||
if (event != null) {
|
if (event != null) {
|
||||||
event.setRedactionEvent(Event.fromJson(eventUpdate.content, tmpRoom));
|
event.setRedactionEvent(Event.fromJson(eventUpdate.content, tmpRoom));
|
||||||
await _eventsBox.put(
|
await _eventsBox.put(
|
||||||
MultiKey(eventUpdate.roomID, event.eventId).toString(),
|
MultiKey(eventUpdate.roomID, event.eventId).toString(),
|
||||||
event.toJson());
|
event.toJson(),
|
||||||
|
);
|
||||||
|
|
||||||
if (tmpRoom.lastEvent?.eventId == event.eventId) {
|
if (tmpRoom.lastEvent?.eventId == event.eventId) {
|
||||||
await _roomStateBox.put(
|
await _roomStateBox.put(
|
||||||
|
|
@ -971,7 +1067,7 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
|
||||||
if ({
|
if ({
|
||||||
EventUpdateType.timeline,
|
EventUpdateType.timeline,
|
||||||
EventUpdateType.history,
|
EventUpdateType.history,
|
||||||
EventUpdateType.decryptedTimelineQueue
|
EventUpdateType.decryptedTimelineQueue,
|
||||||
}.contains(eventUpdate.type)) {
|
}.contains(eventUpdate.type)) {
|
||||||
final eventId = eventUpdate.content['event_id'];
|
final eventId = eventUpdate.content['event_id'];
|
||||||
// Is this ID already in the store?
|
// Is this ID already in the store?
|
||||||
|
|
@ -1020,8 +1116,10 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
|
||||||
.tryGetMap<String, dynamic>('unsigned')
|
.tryGetMap<String, dynamic>('unsigned')
|
||||||
?.tryGet<String>('transaction_id');
|
?.tryGet<String>('transaction_id');
|
||||||
|
|
||||||
await _eventsBox.put(MultiKey(eventUpdate.roomID, eventId).toString(),
|
await _eventsBox.put(
|
||||||
eventUpdate.content);
|
MultiKey(eventUpdate.roomID, eventId).toString(),
|
||||||
|
eventUpdate.content,
|
||||||
|
);
|
||||||
|
|
||||||
// Update timeline fragments
|
// Update timeline fragments
|
||||||
final key = MultiKey(eventUpdate.roomID, status.isSent ? '' : 'SENDING')
|
final key = MultiKey(eventUpdate.roomID, status.isSent ? '' : 'SENDING')
|
||||||
|
|
@ -1070,11 +1168,12 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
|
||||||
eventUpdate.type == EventUpdateType.inviteState)) {
|
eventUpdate.type == EventUpdateType.inviteState)) {
|
||||||
if (eventUpdate.content['type'] == EventTypes.RoomMember) {
|
if (eventUpdate.content['type'] == EventTypes.RoomMember) {
|
||||||
await _roomMembersBox.put(
|
await _roomMembersBox.put(
|
||||||
MultiKey(
|
MultiKey(
|
||||||
eventUpdate.roomID,
|
eventUpdate.roomID,
|
||||||
eventUpdate.content['state_key'],
|
eventUpdate.content['state_key'],
|
||||||
).toString(),
|
).toString(),
|
||||||
eventUpdate.content);
|
eventUpdate.content,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
final key = MultiKey(
|
final key = MultiKey(
|
||||||
eventUpdate.roomID,
|
eventUpdate.roomID,
|
||||||
|
|
@ -1106,33 +1205,39 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> storeInboundGroupSession(
|
Future<void> storeInboundGroupSession(
|
||||||
String roomId,
|
String roomId,
|
||||||
String sessionId,
|
String sessionId,
|
||||||
String pickle,
|
String pickle,
|
||||||
String content,
|
String content,
|
||||||
String indexes,
|
String indexes,
|
||||||
String allowedAtIndex,
|
String allowedAtIndex,
|
||||||
String senderKey,
|
String senderKey,
|
||||||
String senderClaimedKey) async {
|
String senderClaimedKey,
|
||||||
|
) async {
|
||||||
await _inboundGroupSessionsBox.put(
|
await _inboundGroupSessionsBox.put(
|
||||||
sessionId.toHiveKey,
|
sessionId.toHiveKey,
|
||||||
StoredInboundGroupSession(
|
StoredInboundGroupSession(
|
||||||
roomId: roomId,
|
roomId: roomId,
|
||||||
sessionId: sessionId,
|
sessionId: sessionId,
|
||||||
pickle: pickle,
|
pickle: pickle,
|
||||||
content: content,
|
content: content,
|
||||||
indexes: indexes,
|
indexes: indexes,
|
||||||
allowedAtIndex: allowedAtIndex,
|
allowedAtIndex: allowedAtIndex,
|
||||||
senderKey: senderKey,
|
senderKey: senderKey,
|
||||||
senderClaimedKeys: senderClaimedKey,
|
senderClaimedKeys: senderClaimedKey,
|
||||||
uploaded: false,
|
uploaded: false,
|
||||||
).toJson());
|
).toJson(),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> storeOutboundGroupSession(
|
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>{
|
await _outboundGroupSessionsBox.put(roomId.toHiveKey, <String, dynamic>{
|
||||||
'room_id': roomId,
|
'room_id': roomId,
|
||||||
'pickle': pickle,
|
'pickle': pickle,
|
||||||
|
|
@ -1152,8 +1257,12 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> storeRoomUpdate(String roomId, SyncRoomUpdate roomUpdate,
|
Future<void> storeRoomUpdate(
|
||||||
Event? lastEvent, Client client) async {
|
String roomId,
|
||||||
|
SyncRoomUpdate roomUpdate,
|
||||||
|
Event? lastEvent,
|
||||||
|
Client client,
|
||||||
|
) async {
|
||||||
// Leave room if membership is leave
|
// Leave room if membership is leave
|
||||||
if (roomUpdate is LeftRoomUpdate) {
|
if (roomUpdate is LeftRoomUpdate) {
|
||||||
await forgetRoom(roomId);
|
await forgetRoom(roomId);
|
||||||
|
|
@ -1167,49 +1276,52 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
|
||||||
// Make sure room exists
|
// Make sure room exists
|
||||||
if (!_roomsBox.containsKey(roomId.toHiveKey)) {
|
if (!_roomsBox.containsKey(roomId.toHiveKey)) {
|
||||||
await _roomsBox.put(
|
await _roomsBox.put(
|
||||||
roomId.toHiveKey,
|
roomId.toHiveKey,
|
||||||
roomUpdate is JoinedRoomUpdate
|
roomUpdate is JoinedRoomUpdate
|
||||||
? Room(
|
? Room(
|
||||||
client: client,
|
client: client,
|
||||||
id: roomId,
|
id: roomId,
|
||||||
membership: membership,
|
membership: membership,
|
||||||
highlightCount:
|
highlightCount:
|
||||||
roomUpdate.unreadNotifications?.highlightCount?.toInt() ??
|
roomUpdate.unreadNotifications?.highlightCount?.toInt() ??
|
||||||
0,
|
0,
|
||||||
notificationCount: roomUpdate
|
notificationCount: roomUpdate
|
||||||
.unreadNotifications?.notificationCount
|
.unreadNotifications?.notificationCount
|
||||||
?.toInt() ??
|
?.toInt() ??
|
||||||
0,
|
0,
|
||||||
prev_batch: roomUpdate.timeline?.prevBatch,
|
prev_batch: roomUpdate.timeline?.prevBatch,
|
||||||
summary: roomUpdate.summary,
|
summary: roomUpdate.summary,
|
||||||
lastEvent: lastEvent,
|
lastEvent: lastEvent,
|
||||||
).toJson()
|
).toJson()
|
||||||
: Room(
|
: Room(
|
||||||
client: client,
|
client: client,
|
||||||
id: roomId,
|
id: roomId,
|
||||||
membership: membership,
|
membership: membership,
|
||||||
lastEvent: lastEvent,
|
lastEvent: lastEvent,
|
||||||
).toJson());
|
).toJson(),
|
||||||
|
);
|
||||||
} else if (roomUpdate is JoinedRoomUpdate) {
|
} else if (roomUpdate is JoinedRoomUpdate) {
|
||||||
final currentRawRoom = await _roomsBox.get(roomId.toHiveKey);
|
final currentRawRoom = await _roomsBox.get(roomId.toHiveKey);
|
||||||
final currentRoom = Room.fromJson(convertToJson(currentRawRoom), client);
|
final currentRoom = Room.fromJson(convertToJson(currentRawRoom), client);
|
||||||
await _roomsBox.put(
|
await _roomsBox.put(
|
||||||
roomId.toHiveKey,
|
roomId.toHiveKey,
|
||||||
Room(
|
Room(
|
||||||
client: client,
|
client: client,
|
||||||
id: roomId,
|
id: roomId,
|
||||||
membership: membership,
|
membership: membership,
|
||||||
highlightCount:
|
highlightCount:
|
||||||
roomUpdate.unreadNotifications?.highlightCount?.toInt() ??
|
roomUpdate.unreadNotifications?.highlightCount?.toInt() ??
|
||||||
currentRoom.highlightCount,
|
currentRoom.highlightCount,
|
||||||
notificationCount:
|
notificationCount:
|
||||||
roomUpdate.unreadNotifications?.notificationCount?.toInt() ??
|
roomUpdate.unreadNotifications?.notificationCount?.toInt() ??
|
||||||
currentRoom.notificationCount,
|
currentRoom.notificationCount,
|
||||||
prev_batch:
|
prev_batch: roomUpdate.timeline?.prevBatch ?? currentRoom.prev_batch,
|
||||||
roomUpdate.timeline?.prevBatch ?? currentRoom.prev_batch,
|
summary: RoomSummary.fromJson(
|
||||||
summary: RoomSummary.fromJson(currentRoom.summary.toJson()
|
currentRoom.summary.toJson()
|
||||||
..addAll(roomUpdate.summary?.toJson() ?? {})),
|
..addAll(roomUpdate.summary?.toJson() ?? {}),
|
||||||
).toJson());
|
),
|
||||||
|
).toJson(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1219,15 +1331,20 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> storeSSSSCache(
|
Future<void> storeSSSSCache(
|
||||||
String type, String keyId, String ciphertext, String content) async {
|
String type,
|
||||||
|
String keyId,
|
||||||
|
String ciphertext,
|
||||||
|
String content,
|
||||||
|
) async {
|
||||||
await _ssssCacheBox.put(
|
await _ssssCacheBox.put(
|
||||||
type,
|
type,
|
||||||
SSSSCache(
|
SSSSCache(
|
||||||
type: type,
|
type: type,
|
||||||
keyId: keyId,
|
keyId: keyId,
|
||||||
ciphertext: ciphertext,
|
ciphertext: ciphertext,
|
||||||
content: content,
|
content: content,
|
||||||
).toJson());
|
).toJson(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -1238,8 +1355,13 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> storeUserCrossSigningKey(String userId, String publicKey,
|
Future<void> storeUserCrossSigningKey(
|
||||||
String content, bool verified, bool blocked) async {
|
String userId,
|
||||||
|
String publicKey,
|
||||||
|
String content,
|
||||||
|
bool verified,
|
||||||
|
bool blocked,
|
||||||
|
) async {
|
||||||
await _userCrossSigningKeysBox.put(
|
await _userCrossSigningKeysBox.put(
|
||||||
MultiKey(userId, publicKey).toString(),
|
MultiKey(userId, publicKey).toString(),
|
||||||
{
|
{
|
||||||
|
|
@ -1253,8 +1375,14 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> storeUserDeviceKey(String userId, String deviceId,
|
Future<void> storeUserDeviceKey(
|
||||||
String content, bool verified, bool blocked, int lastActive) async {
|
String userId,
|
||||||
|
String deviceId,
|
||||||
|
String content,
|
||||||
|
bool verified,
|
||||||
|
bool blocked,
|
||||||
|
int lastActive,
|
||||||
|
) async {
|
||||||
await _userDeviceKeysBox.put(MultiKey(userId, deviceId).toString(), {
|
await _userDeviceKeysBox.put(MultiKey(userId, deviceId).toString(), {
|
||||||
'user_id': userId,
|
'user_id': userId,
|
||||||
'device_id': deviceId,
|
'device_id': deviceId,
|
||||||
|
|
@ -1292,7 +1420,9 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
|
||||||
await _clientBox.put('homeserver_url', homeserverUrl);
|
await _clientBox.put('homeserver_url', homeserverUrl);
|
||||||
await _clientBox.put('token', token);
|
await _clientBox.put('token', token);
|
||||||
await _clientBox.put(
|
await _clientBox.put(
|
||||||
'token_expires_at', tokenExpiresAt?.millisecondsSinceEpoch.toString());
|
'token_expires_at',
|
||||||
|
tokenExpiresAt?.millisecondsSinceEpoch.toString(),
|
||||||
|
);
|
||||||
await _clientBox.put('refresh_token', refreshToken);
|
await _clientBox.put('refresh_token', refreshToken);
|
||||||
await _clientBox.put('user_id', userId);
|
await _clientBox.put('user_id', userId);
|
||||||
await _clientBox.put('device_id', deviceId);
|
await _clientBox.put('device_id', deviceId);
|
||||||
|
|
@ -1312,11 +1442,15 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> updateInboundGroupSessionAllowedAtIndex(
|
Future<void> updateInboundGroupSessionAllowedAtIndex(
|
||||||
String allowedAtIndex, String roomId, String sessionId) async {
|
String allowedAtIndex,
|
||||||
|
String roomId,
|
||||||
|
String sessionId,
|
||||||
|
) async {
|
||||||
final raw = await _inboundGroupSessionsBox.get(sessionId.toHiveKey);
|
final raw = await _inboundGroupSessionsBox.get(sessionId.toHiveKey);
|
||||||
if (raw == null) {
|
if (raw == null) {
|
||||||
Logs().w(
|
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;
|
return;
|
||||||
}
|
}
|
||||||
raw['allowed_at_index'] = allowedAtIndex;
|
raw['allowed_at_index'] = allowedAtIndex;
|
||||||
|
|
@ -1326,11 +1460,15 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> updateInboundGroupSessionIndexes(
|
Future<void> updateInboundGroupSessionIndexes(
|
||||||
String indexes, String roomId, String sessionId) async {
|
String indexes,
|
||||||
|
String roomId,
|
||||||
|
String sessionId,
|
||||||
|
) async {
|
||||||
final raw = await _inboundGroupSessionsBox.get(sessionId.toHiveKey);
|
final raw = await _inboundGroupSessionsBox.get(sessionId.toHiveKey);
|
||||||
if (raw == null) {
|
if (raw == null) {
|
||||||
Logs().w(
|
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;
|
return;
|
||||||
}
|
}
|
||||||
raw['indexes'] = indexes;
|
raw['indexes'] = indexes;
|
||||||
|
|
@ -1340,8 +1478,10 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<StoredInboundGroupSession>> getAllInboundGroupSessions() async {
|
Future<List<StoredInboundGroupSession>> getAllInboundGroupSessions() async {
|
||||||
final rawSessions = await Future.wait(_inboundGroupSessionsBox.keys
|
final rawSessions = await Future.wait(
|
||||||
.map((key) => _inboundGroupSessionsBox.get(key)));
|
_inboundGroupSessionsBox.keys
|
||||||
|
.map((key) => _inboundGroupSessionsBox.get(key)),
|
||||||
|
);
|
||||||
return rawSessions
|
return rawSessions
|
||||||
.map((raw) => StoredInboundGroupSession.fromJson(convertToJson(raw)))
|
.map((raw) => StoredInboundGroupSession.fromJson(convertToJson(raw)))
|
||||||
.toList();
|
.toList();
|
||||||
|
|
@ -1435,7 +1575,9 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> storeUserProfile(
|
Future<void> storeUserProfile(
|
||||||
String userId, CachedProfileInformation profile) async {
|
String userId,
|
||||||
|
CachedProfileInformation profile,
|
||||||
|
) async {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,15 +22,18 @@ class BoxCollection with ZoneTransactionMixin {
|
||||||
int version = 1,
|
int version = 1,
|
||||||
}) async {
|
}) async {
|
||||||
idbFactory ??= window.indexedDB!;
|
idbFactory ??= window.indexedDB!;
|
||||||
final db = await idbFactory.open(name, version: version,
|
final db = await idbFactory.open(
|
||||||
onUpgradeNeeded: (VersionChangeEvent event) {
|
name,
|
||||||
final db = event.target.result;
|
version: version,
|
||||||
for (final name in boxNames) {
|
onUpgradeNeeded: (VersionChangeEvent event) {
|
||||||
if (db.objectStoreNames.contains(name)) continue;
|
final db = event.target.result;
|
||||||
|
for (final name in boxNames) {
|
||||||
|
if (db.objectStoreNames.contains(name)) continue;
|
||||||
|
|
||||||
db.createObjectStore(name, autoIncrement: true);
|
db.createObjectStore(name, autoIncrement: true);
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
return BoxCollection(db, boxNames, name);
|
return BoxCollection(db, boxNames, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -147,7 +150,8 @@ class Box<V> {
|
||||||
txn ??= boxCollection._db.transaction(name, 'readonly');
|
txn ??= boxCollection._db.transaction(name, 'readonly');
|
||||||
final store = txn.objectStore(name);
|
final store = txn.objectStore(name);
|
||||||
final list = await Future.wait(
|
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++) {
|
for (var i = 0; i < keys.length; i++) {
|
||||||
_cache[keys[i]] = list[i];
|
_cache[keys[i]] = list[i];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,8 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
|
||||||
// there was a field of type `dart:io:Directory` here. This one broke the
|
// 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.
|
// dart js standalone compiler. Migration via URI as file system identifier.
|
||||||
@Deprecated(
|
@Deprecated(
|
||||||
'Breaks support for web standalone. Use [fileStorageLocation] instead.')
|
'Breaks support for web standalone. Use [fileStorageLocation] instead.',
|
||||||
|
)
|
||||||
Object? get fileStoragePath => fileStorageLocation?.toFilePath();
|
Object? get fileStoragePath => fileStorageLocation?.toFilePath();
|
||||||
|
|
||||||
static const String _clientBoxName = 'box_client';
|
static const String _clientBoxName = 'box_client';
|
||||||
|
|
@ -183,7 +184,8 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
|
||||||
this.maxFileSize = 0,
|
this.maxFileSize = 0,
|
||||||
// TODO : remove deprecated member migration on next major release
|
// TODO : remove deprecated member migration on next major release
|
||||||
@Deprecated(
|
@Deprecated(
|
||||||
'Breaks support for web standalone. Use [fileStorageLocation] instead.')
|
'Breaks support for web standalone. Use [fileStorageLocation] instead.',
|
||||||
|
)
|
||||||
dynamic fileStoragePath,
|
dynamic fileStoragePath,
|
||||||
Uri? fileStorageLocation,
|
Uri? fileStorageLocation,
|
||||||
Duration? deleteFilesAfterDuration,
|
Duration? deleteFilesAfterDuration,
|
||||||
|
|
@ -315,7 +317,8 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
|
||||||
.where((session) => session.uploaded == false)
|
.where((session) => session.uploaded == false)
|
||||||
.toList();
|
.toList();
|
||||||
Logs().i(
|
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 {
|
await transaction(() async {
|
||||||
for (final session in sessionsToUpload) {
|
for (final session in sessionsToUpload) {
|
||||||
await _inboundGroupSessionsUploadQueueBox.put(
|
await _inboundGroupSessionsUploadQueueBox.put(
|
||||||
|
|
@ -476,8 +479,10 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Combine those two lists while respecting the start and limit parameters.
|
// Combine those two lists while respecting the start and limit parameters.
|
||||||
final end = min(timelineEventIds.length,
|
final end = min(
|
||||||
start + (limit ?? timelineEventIds.length));
|
timelineEventIds.length,
|
||||||
|
start + (limit ?? timelineEventIds.length),
|
||||||
|
);
|
||||||
final eventIds = [
|
final eventIds = [
|
||||||
...sendingEventIds,
|
...sendingEventIds,
|
||||||
if (!onlySending && start < timelineEventIds.length)
|
if (!onlySending && start < timelineEventIds.length)
|
||||||
|
|
@ -511,7 +516,9 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<String>> getLastSentMessageUserDeviceKey(
|
Future<List<String>> getLastSentMessageUserDeviceKey(
|
||||||
String userId, String deviceId) async {
|
String userId,
|
||||||
|
String deviceId,
|
||||||
|
) async {
|
||||||
final raw =
|
final raw =
|
||||||
await _userDeviceKeysBox.get(TupleKey(userId, deviceId).toString());
|
await _userDeviceKeysBox.get(TupleKey(userId, deviceId).toString());
|
||||||
if (raw == null) return <String>[];
|
if (raw == null) return <String>[];
|
||||||
|
|
@ -519,8 +526,12 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> storeOlmSession(String identityKey, String sessionId,
|
Future<void> storeOlmSession(
|
||||||
String pickle, int lastReceived) async {
|
String identityKey,
|
||||||
|
String sessionId,
|
||||||
|
String pickle,
|
||||||
|
int lastReceived,
|
||||||
|
) async {
|
||||||
final rawSessions = copyMap((await _olmSessionsBox.get(identityKey)) ?? {});
|
final rawSessions = copyMap((await _olmSessionsBox.get(identityKey)) ?? {});
|
||||||
rawSessions[sessionId] = {
|
rawSessions[sessionId] = {
|
||||||
'identity_key': identityKey,
|
'identity_key': identityKey,
|
||||||
|
|
@ -534,7 +545,9 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<OlmSession>> getOlmSessions(
|
Future<List<OlmSession>> getOlmSessions(
|
||||||
String identityKey, String userId) async {
|
String identityKey,
|
||||||
|
String userId,
|
||||||
|
) async {
|
||||||
final rawSessions = await _olmSessionsBox.get(identityKey);
|
final rawSessions = await _olmSessionsBox.get(identityKey);
|
||||||
if (rawSessions == null || rawSessions.isEmpty) return <OlmSession>[];
|
if (rawSessions == null || rawSessions.isEmpty) return <OlmSession>[];
|
||||||
return rawSessions.values
|
return rawSessions.values
|
||||||
|
|
@ -548,23 +561,31 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<OlmSession>> getOlmSessionsForDevices(
|
Future<List<OlmSession>> getOlmSessionsForDevices(
|
||||||
List<String> identityKeys, String userId) async {
|
List<String> identityKeys,
|
||||||
|
String userId,
|
||||||
|
) async {
|
||||||
final sessions = await Future.wait(
|
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];
|
return <OlmSession>[for (final sublist in sessions) ...sublist];
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<OutboundGroupSession?> getOutboundGroupSession(
|
Future<OutboundGroupSession?> getOutboundGroupSession(
|
||||||
String roomId, String userId) async {
|
String roomId,
|
||||||
|
String userId,
|
||||||
|
) async {
|
||||||
final raw = await _outboundGroupSessionsBox.get(roomId);
|
final raw = await _outboundGroupSessionsBox.get(roomId);
|
||||||
if (raw == null) return null;
|
if (raw == null) return null;
|
||||||
return OutboundGroupSession.fromJson(copyMap(raw), userId);
|
return OutboundGroupSession.fromJson(copyMap(raw), userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Room?> getSingleRoom(Client client, String roomId,
|
Future<Room?> getSingleRoom(
|
||||||
{bool loadImportantStates = true}) async {
|
Client client,
|
||||||
|
String roomId, {
|
||||||
|
bool loadImportantStates = true,
|
||||||
|
}) async {
|
||||||
// Get raw room from database:
|
// Get raw room from database:
|
||||||
final roomData = await _roomsBox.get(roomId);
|
final roomData = await _roomsBox.get(roomId);
|
||||||
if (roomData == null) return null;
|
if (roomData == null) return null;
|
||||||
|
|
@ -611,9 +632,11 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
|
||||||
}
|
}
|
||||||
final states = entry.value;
|
final states = entry.value;
|
||||||
final stateEvents = states.values
|
final stateEvents = states.values
|
||||||
.map((raw) => room.membership == Membership.invite
|
.map(
|
||||||
? StrippedStateEvent.fromJson(copyMap(raw))
|
(raw) => room.membership == Membership.invite
|
||||||
: Event.fromJson(copyMap(raw), room))
|
? StrippedStateEvent.fromJson(copyMap(raw))
|
||||||
|
: Event.fromJson(copyMap(raw), room),
|
||||||
|
)
|
||||||
.toList();
|
.toList();
|
||||||
for (final state in stateEvents) {
|
for (final state in stateEvents) {
|
||||||
room.setState(state);
|
room.setState(state);
|
||||||
|
|
@ -633,7 +656,8 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
|
||||||
basicRoomEvent;
|
basicRoomEvent;
|
||||||
} else {
|
} else {
|
||||||
Logs().w(
|
Logs().w(
|
||||||
'Found account data for unknown room $roomId. Delete now...');
|
'Found account data for unknown room $roomId. Delete now...',
|
||||||
|
);
|
||||||
await _roomAccountDataBox
|
await _roomAccountDataBox
|
||||||
.delete(TupleKey(roomId, basicRoomEvent.type).toString());
|
.delete(TupleKey(roomId, basicRoomEvent.type).toString());
|
||||||
}
|
}
|
||||||
|
|
@ -663,7 +687,9 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<Event>> getUnimportantRoomEventStatesForRoom(
|
Future<List<Event>> getUnimportantRoomEventStatesForRoom(
|
||||||
List<String> events, Room room) async {
|
List<String> events,
|
||||||
|
Room room,
|
||||||
|
) async {
|
||||||
final keys = (await _nonPreloadRoomStateBox.getAllKeys()).where((key) {
|
final keys = (await _nonPreloadRoomStateBox.getAllKeys()).where((key) {
|
||||||
final tuple = TupleKey.fromString(key);
|
final tuple = TupleKey.fromString(key);
|
||||||
return tuple.parts.first == room.id && !events.contains(tuple.parts[1]);
|
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);
|
final states = await _nonPreloadRoomStateBox.get(key);
|
||||||
if (states == null) continue;
|
if (states == null) continue;
|
||||||
unimportantEvents.addAll(
|
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();
|
return unimportantEvents.where((event) => event.stateKey != null).toList();
|
||||||
|
|
@ -726,20 +753,21 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
res[userId] = DeviceKeysList.fromDbJson(
|
res[userId] = DeviceKeysList.fromDbJson(
|
||||||
{
|
{
|
||||||
'client_id': client.id,
|
'client_id': client.id,
|
||||||
'user_id': userId,
|
'user_id': userId,
|
||||||
'outdated': deviceKeysOutdated[userId],
|
'outdated': deviceKeysOutdated[userId],
|
||||||
},
|
},
|
||||||
childEntries
|
childEntries
|
||||||
.where((c) => c != null)
|
.where((c) => c != null)
|
||||||
.toList()
|
.toList()
|
||||||
.cast<Map<String, dynamic>>(),
|
.cast<Map<String, dynamic>>(),
|
||||||
crossSigningEntries
|
crossSigningEntries
|
||||||
.where((c) => c != null)
|
.where((c) => c != null)
|
||||||
.toList()
|
.toList()
|
||||||
.cast<Map<String, dynamic>>(),
|
.cast<Map<String, dynamic>>(),
|
||||||
client);
|
client,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
});
|
});
|
||||||
|
|
@ -761,24 +789,27 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<int> insertClient(
|
Future<int> insertClient(
|
||||||
String name,
|
String name,
|
||||||
String homeserverUrl,
|
String homeserverUrl,
|
||||||
String token,
|
String token,
|
||||||
DateTime? tokenExpiresAt,
|
DateTime? tokenExpiresAt,
|
||||||
String? refreshToken,
|
String? refreshToken,
|
||||||
String userId,
|
String userId,
|
||||||
String? deviceId,
|
String? deviceId,
|
||||||
String? deviceName,
|
String? deviceName,
|
||||||
String? prevBatch,
|
String? prevBatch,
|
||||||
String? olmAccount) async {
|
String? olmAccount,
|
||||||
|
) async {
|
||||||
await transaction(() async {
|
await transaction(() async {
|
||||||
await _clientBox.put('homeserver_url', homeserverUrl);
|
await _clientBox.put('homeserver_url', homeserverUrl);
|
||||||
await _clientBox.put('token', token);
|
await _clientBox.put('token', token);
|
||||||
if (tokenExpiresAt == null) {
|
if (tokenExpiresAt == null) {
|
||||||
await _clientBox.delete('token_expires_at');
|
await _clientBox.delete('token_expires_at');
|
||||||
} else {
|
} else {
|
||||||
await _clientBox.put('token_expires_at',
|
await _clientBox.put(
|
||||||
tokenExpiresAt.millisecondsSinceEpoch.toString());
|
'token_expires_at',
|
||||||
|
tokenExpiresAt.millisecondsSinceEpoch.toString(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (refreshToken == null) {
|
if (refreshToken == null) {
|
||||||
await _clientBox.delete('refresh_token');
|
await _clientBox.delete('refresh_token');
|
||||||
|
|
@ -813,7 +844,10 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<int> insertIntoToDeviceQueue(
|
Future<int> insertIntoToDeviceQueue(
|
||||||
String type, String txnId, String content) async {
|
String type,
|
||||||
|
String txnId,
|
||||||
|
String content,
|
||||||
|
) async {
|
||||||
final id = DateTime.now().millisecondsSinceEpoch;
|
final id = DateTime.now().millisecondsSinceEpoch;
|
||||||
await _toDeviceQueueBox.put(id.toString(), {
|
await _toDeviceQueueBox.put(id.toString(), {
|
||||||
'type': type,
|
'type': type,
|
||||||
|
|
@ -825,7 +859,9 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> markInboundGroupSessionAsUploaded(
|
Future<void> markInboundGroupSessionAsUploaded(
|
||||||
String roomId, String sessionId) async {
|
String roomId,
|
||||||
|
String sessionId,
|
||||||
|
) async {
|
||||||
await _inboundGroupSessionsUploadQueueBox.delete(sessionId);
|
await _inboundGroupSessionsUploadQueueBox.delete(sessionId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -871,7 +907,9 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> removeUserCrossSigningKey(
|
Future<void> removeUserCrossSigningKey(
|
||||||
String userId, String publicKey) async {
|
String userId,
|
||||||
|
String publicKey,
|
||||||
|
) async {
|
||||||
await _userCrossSigningKeysBox
|
await _userCrossSigningKeysBox
|
||||||
.delete(TupleKey(userId, publicKey).toString());
|
.delete(TupleKey(userId, publicKey).toString());
|
||||||
return;
|
return;
|
||||||
|
|
@ -885,7 +923,10 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> setBlockedUserCrossSigningKey(
|
Future<void> setBlockedUserCrossSigningKey(
|
||||||
bool blocked, String userId, String publicKey) async {
|
bool blocked,
|
||||||
|
String userId,
|
||||||
|
String publicKey,
|
||||||
|
) async {
|
||||||
final raw = copyMap(
|
final raw = copyMap(
|
||||||
await _userCrossSigningKeysBox
|
await _userCrossSigningKeysBox
|
||||||
.get(TupleKey(userId, publicKey).toString()) ??
|
.get(TupleKey(userId, publicKey).toString()) ??
|
||||||
|
|
@ -901,7 +942,10 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> setBlockedUserDeviceKey(
|
Future<void> setBlockedUserDeviceKey(
|
||||||
bool blocked, String userId, String deviceId) async {
|
bool blocked,
|
||||||
|
String userId,
|
||||||
|
String deviceId,
|
||||||
|
) async {
|
||||||
final raw = copyMap(
|
final raw = copyMap(
|
||||||
await _userDeviceKeysBox.get(TupleKey(userId, deviceId).toString()) ?? {},
|
await _userDeviceKeysBox.get(TupleKey(userId, deviceId).toString()) ?? {},
|
||||||
);
|
);
|
||||||
|
|
@ -915,7 +959,10 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> setLastActiveUserDeviceKey(
|
Future<void> setLastActiveUserDeviceKey(
|
||||||
int lastActive, String userId, String deviceId) async {
|
int lastActive,
|
||||||
|
String userId,
|
||||||
|
String deviceId,
|
||||||
|
) async {
|
||||||
final raw = copyMap(
|
final raw = copyMap(
|
||||||
await _userDeviceKeysBox.get(TupleKey(userId, deviceId).toString()) ?? {},
|
await _userDeviceKeysBox.get(TupleKey(userId, deviceId).toString()) ?? {},
|
||||||
);
|
);
|
||||||
|
|
@ -929,7 +976,10 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> setLastSentMessageUserDeviceKey(
|
Future<void> setLastSentMessageUserDeviceKey(
|
||||||
String lastSentMessage, String userId, String deviceId) async {
|
String lastSentMessage,
|
||||||
|
String userId,
|
||||||
|
String deviceId,
|
||||||
|
) async {
|
||||||
final raw = copyMap(
|
final raw = copyMap(
|
||||||
await _userDeviceKeysBox.get(TupleKey(userId, deviceId).toString()) ?? {},
|
await _userDeviceKeysBox.get(TupleKey(userId, deviceId).toString()) ?? {},
|
||||||
);
|
);
|
||||||
|
|
@ -942,7 +992,10 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> setRoomPrevBatch(
|
Future<void> setRoomPrevBatch(
|
||||||
String? prevBatch, String roomId, Client client) async {
|
String? prevBatch,
|
||||||
|
String roomId,
|
||||||
|
Client client,
|
||||||
|
) async {
|
||||||
final raw = await _roomsBox.get(roomId);
|
final raw = await _roomsBox.get(roomId);
|
||||||
if (raw == null) return;
|
if (raw == null) return;
|
||||||
final room = Room.fromJson(copyMap(raw), client);
|
final room = Room.fromJson(copyMap(raw), client);
|
||||||
|
|
@ -953,7 +1006,10 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> setVerifiedUserCrossSigningKey(
|
Future<void> setVerifiedUserCrossSigningKey(
|
||||||
bool verified, String userId, String publicKey) async {
|
bool verified,
|
||||||
|
String userId,
|
||||||
|
String publicKey,
|
||||||
|
) async {
|
||||||
final raw = copyMap(
|
final raw = copyMap(
|
||||||
(await _userCrossSigningKeysBox
|
(await _userCrossSigningKeysBox
|
||||||
.get(TupleKey(userId, publicKey).toString())) ??
|
.get(TupleKey(userId, publicKey).toString())) ??
|
||||||
|
|
@ -969,7 +1025,10 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> setVerifiedUserDeviceKey(
|
Future<void> setVerifiedUserDeviceKey(
|
||||||
bool verified, String userId, String deviceId) async {
|
bool verified,
|
||||||
|
String userId,
|
||||||
|
String deviceId,
|
||||||
|
) async {
|
||||||
final raw = copyMap(
|
final raw = copyMap(
|
||||||
await _userDeviceKeysBox.get(TupleKey(userId, deviceId).toString()) ?? {},
|
await _userDeviceKeysBox.get(TupleKey(userId, deviceId).toString()) ?? {},
|
||||||
);
|
);
|
||||||
|
|
@ -1002,8 +1061,9 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
|
||||||
if (event != null) {
|
if (event != null) {
|
||||||
event.setRedactionEvent(Event.fromJson(eventUpdate.content, tmpRoom));
|
event.setRedactionEvent(Event.fromJson(eventUpdate.content, tmpRoom));
|
||||||
await _eventsBox.put(
|
await _eventsBox.put(
|
||||||
TupleKey(eventUpdate.roomID, event.eventId).toString(),
|
TupleKey(eventUpdate.roomID, event.eventId).toString(),
|
||||||
event.toJson());
|
event.toJson(),
|
||||||
|
);
|
||||||
|
|
||||||
if (tmpRoom.lastEvent?.eventId == event.eventId) {
|
if (tmpRoom.lastEvent?.eventId == event.eventId) {
|
||||||
if (client.importantStateEvents.contains(event.type)) {
|
if (client.importantStateEvents.contains(event.type)) {
|
||||||
|
|
@ -1070,8 +1130,10 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
|
||||||
final transactionId = eventUpdate.content
|
final transactionId = eventUpdate.content
|
||||||
.tryGetMap<String, dynamic>('unsigned')
|
.tryGetMap<String, dynamic>('unsigned')
|
||||||
?.tryGet<String>('transaction_id');
|
?.tryGet<String>('transaction_id');
|
||||||
await _eventsBox.put(TupleKey(eventUpdate.roomID, eventId).toString(),
|
await _eventsBox.put(
|
||||||
eventUpdate.content);
|
TupleKey(eventUpdate.roomID, eventId).toString(),
|
||||||
|
eventUpdate.content,
|
||||||
|
);
|
||||||
|
|
||||||
// Update timeline fragments
|
// Update timeline fragments
|
||||||
final key = TupleKey(eventUpdate.roomID, status.isSent ? '' : 'SENDING')
|
final key = TupleKey(eventUpdate.roomID, status.isSent ? '' : 'SENDING')
|
||||||
|
|
@ -1122,11 +1184,12 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
|
||||||
eventUpdate.type == EventUpdateType.inviteState)) {
|
eventUpdate.type == EventUpdateType.inviteState)) {
|
||||||
if (eventUpdate.content['type'] == EventTypes.RoomMember) {
|
if (eventUpdate.content['type'] == EventTypes.RoomMember) {
|
||||||
await _roomMembersBox.put(
|
await _roomMembersBox.put(
|
||||||
TupleKey(
|
TupleKey(
|
||||||
eventUpdate.roomID,
|
eventUpdate.roomID,
|
||||||
eventUpdate.content['state_key'],
|
eventUpdate.content['state_key'],
|
||||||
).toString(),
|
).toString(),
|
||||||
eventUpdate.content);
|
eventUpdate.content,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
final type = eventUpdate.content['type'] as String;
|
final type = eventUpdate.content['type'] as String;
|
||||||
final roomStateBox = client.importantStateEvents.contains(type)
|
final roomStateBox = client.importantStateEvents.contains(type)
|
||||||
|
|
@ -1157,14 +1220,15 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> storeInboundGroupSession(
|
Future<void> storeInboundGroupSession(
|
||||||
String roomId,
|
String roomId,
|
||||||
String sessionId,
|
String sessionId,
|
||||||
String pickle,
|
String pickle,
|
||||||
String content,
|
String content,
|
||||||
String indexes,
|
String indexes,
|
||||||
String allowedAtIndex,
|
String allowedAtIndex,
|
||||||
String senderKey,
|
String senderKey,
|
||||||
String senderClaimedKey) async {
|
String senderClaimedKey,
|
||||||
|
) async {
|
||||||
final json = StoredInboundGroupSession(
|
final json = StoredInboundGroupSession(
|
||||||
roomId: roomId,
|
roomId: roomId,
|
||||||
sessionId: sessionId,
|
sessionId: sessionId,
|
||||||
|
|
@ -1186,7 +1250,11 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> storeOutboundGroupSession(
|
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>{
|
await _outboundGroupSessionsBox.put(roomId, <String, dynamic>{
|
||||||
'room_id': roomId,
|
'room_id': roomId,
|
||||||
'pickle': pickle,
|
'pickle': pickle,
|
||||||
|
|
@ -1206,8 +1274,12 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> storeRoomUpdate(String roomId, SyncRoomUpdate roomUpdate,
|
Future<void> storeRoomUpdate(
|
||||||
Event? lastEvent, Client client) async {
|
String roomId,
|
||||||
|
SyncRoomUpdate roomUpdate,
|
||||||
|
Event? lastEvent,
|
||||||
|
Client client,
|
||||||
|
) async {
|
||||||
// Leave room if membership is leave
|
// Leave room if membership is leave
|
||||||
if (roomUpdate is LeftRoomUpdate) {
|
if (roomUpdate is LeftRoomUpdate) {
|
||||||
await forgetRoom(roomId);
|
await forgetRoom(roomId);
|
||||||
|
|
@ -1222,49 +1294,52 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
|
||||||
final currentRawRoom = await _roomsBox.get(roomId);
|
final currentRawRoom = await _roomsBox.get(roomId);
|
||||||
if (currentRawRoom == null) {
|
if (currentRawRoom == null) {
|
||||||
await _roomsBox.put(
|
await _roomsBox.put(
|
||||||
roomId,
|
roomId,
|
||||||
roomUpdate is JoinedRoomUpdate
|
roomUpdate is JoinedRoomUpdate
|
||||||
? Room(
|
? Room(
|
||||||
client: client,
|
client: client,
|
||||||
id: roomId,
|
id: roomId,
|
||||||
membership: membership,
|
membership: membership,
|
||||||
highlightCount:
|
highlightCount:
|
||||||
roomUpdate.unreadNotifications?.highlightCount?.toInt() ??
|
roomUpdate.unreadNotifications?.highlightCount?.toInt() ??
|
||||||
0,
|
0,
|
||||||
notificationCount: roomUpdate
|
notificationCount: roomUpdate
|
||||||
.unreadNotifications?.notificationCount
|
.unreadNotifications?.notificationCount
|
||||||
?.toInt() ??
|
?.toInt() ??
|
||||||
0,
|
0,
|
||||||
prev_batch: roomUpdate.timeline?.prevBatch,
|
prev_batch: roomUpdate.timeline?.prevBatch,
|
||||||
summary: roomUpdate.summary,
|
summary: roomUpdate.summary,
|
||||||
lastEvent: lastEvent,
|
lastEvent: lastEvent,
|
||||||
).toJson()
|
).toJson()
|
||||||
: Room(
|
: Room(
|
||||||
client: client,
|
client: client,
|
||||||
id: roomId,
|
id: roomId,
|
||||||
membership: membership,
|
membership: membership,
|
||||||
lastEvent: lastEvent,
|
lastEvent: lastEvent,
|
||||||
).toJson());
|
).toJson(),
|
||||||
|
);
|
||||||
} else if (roomUpdate is JoinedRoomUpdate) {
|
} else if (roomUpdate is JoinedRoomUpdate) {
|
||||||
final currentRoom = Room.fromJson(copyMap(currentRawRoom), client);
|
final currentRoom = Room.fromJson(copyMap(currentRawRoom), client);
|
||||||
await _roomsBox.put(
|
await _roomsBox.put(
|
||||||
roomId,
|
roomId,
|
||||||
Room(
|
Room(
|
||||||
client: client,
|
client: client,
|
||||||
id: roomId,
|
id: roomId,
|
||||||
membership: membership,
|
membership: membership,
|
||||||
highlightCount:
|
highlightCount:
|
||||||
roomUpdate.unreadNotifications?.highlightCount?.toInt() ??
|
roomUpdate.unreadNotifications?.highlightCount?.toInt() ??
|
||||||
currentRoom.highlightCount,
|
currentRoom.highlightCount,
|
||||||
notificationCount:
|
notificationCount:
|
||||||
roomUpdate.unreadNotifications?.notificationCount?.toInt() ??
|
roomUpdate.unreadNotifications?.notificationCount?.toInt() ??
|
||||||
currentRoom.notificationCount,
|
currentRoom.notificationCount,
|
||||||
prev_batch:
|
prev_batch: roomUpdate.timeline?.prevBatch ?? currentRoom.prev_batch,
|
||||||
roomUpdate.timeline?.prevBatch ?? currentRoom.prev_batch,
|
summary: RoomSummary.fromJson(
|
||||||
summary: RoomSummary.fromJson(currentRoom.summary.toJson()
|
currentRoom.summary.toJson()
|
||||||
..addAll(roomUpdate.summary?.toJson() ?? {})),
|
..addAll(roomUpdate.summary?.toJson() ?? {}),
|
||||||
lastEvent: lastEvent,
|
),
|
||||||
).toJson());
|
lastEvent: lastEvent,
|
||||||
|
).toJson(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1274,15 +1349,20 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> storeSSSSCache(
|
Future<void> storeSSSSCache(
|
||||||
String type, String keyId, String ciphertext, String content) async {
|
String type,
|
||||||
|
String keyId,
|
||||||
|
String ciphertext,
|
||||||
|
String content,
|
||||||
|
) async {
|
||||||
await _ssssCacheBox.put(
|
await _ssssCacheBox.put(
|
||||||
type,
|
type,
|
||||||
SSSSCache(
|
SSSSCache(
|
||||||
type: type,
|
type: type,
|
||||||
keyId: keyId,
|
keyId: keyId,
|
||||||
ciphertext: ciphertext,
|
ciphertext: ciphertext,
|
||||||
content: content,
|
content: content,
|
||||||
).toJson());
|
).toJson(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -1293,8 +1373,13 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> storeUserCrossSigningKey(String userId, String publicKey,
|
Future<void> storeUserCrossSigningKey(
|
||||||
String content, bool verified, bool blocked) async {
|
String userId,
|
||||||
|
String publicKey,
|
||||||
|
String content,
|
||||||
|
bool verified,
|
||||||
|
bool blocked,
|
||||||
|
) async {
|
||||||
await _userCrossSigningKeysBox.put(
|
await _userCrossSigningKeysBox.put(
|
||||||
TupleKey(userId, publicKey).toString(),
|
TupleKey(userId, publicKey).toString(),
|
||||||
{
|
{
|
||||||
|
|
@ -1308,8 +1393,14 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> storeUserDeviceKey(String userId, String deviceId,
|
Future<void> storeUserDeviceKey(
|
||||||
String content, bool verified, bool blocked, int lastActive) async {
|
String userId,
|
||||||
|
String deviceId,
|
||||||
|
String content,
|
||||||
|
bool verified,
|
||||||
|
bool blocked,
|
||||||
|
int lastActive,
|
||||||
|
) async {
|
||||||
await _userDeviceKeysBox.put(TupleKey(userId, deviceId).toString(), {
|
await _userDeviceKeysBox.put(TupleKey(userId, deviceId).toString(), {
|
||||||
'user_id': userId,
|
'user_id': userId,
|
||||||
'device_id': deviceId,
|
'device_id': deviceId,
|
||||||
|
|
@ -1350,8 +1441,10 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
|
||||||
if (tokenExpiresAt == null) {
|
if (tokenExpiresAt == null) {
|
||||||
await _clientBox.delete('token_expires_at');
|
await _clientBox.delete('token_expires_at');
|
||||||
} else {
|
} else {
|
||||||
await _clientBox.put('token_expires_at',
|
await _clientBox.put(
|
||||||
tokenExpiresAt.millisecondsSinceEpoch.toString());
|
'token_expires_at',
|
||||||
|
tokenExpiresAt.millisecondsSinceEpoch.toString(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (refreshToken == null) {
|
if (refreshToken == null) {
|
||||||
await _clientBox.delete('refresh_token');
|
await _clientBox.delete('refresh_token');
|
||||||
|
|
@ -1393,11 +1486,15 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> updateInboundGroupSessionAllowedAtIndex(
|
Future<void> updateInboundGroupSessionAllowedAtIndex(
|
||||||
String allowedAtIndex, String roomId, String sessionId) async {
|
String allowedAtIndex,
|
||||||
|
String roomId,
|
||||||
|
String sessionId,
|
||||||
|
) async {
|
||||||
final raw = await _inboundGroupSessionsBox.get(sessionId);
|
final raw = await _inboundGroupSessionsBox.get(sessionId);
|
||||||
if (raw == null) {
|
if (raw == null) {
|
||||||
Logs().w(
|
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;
|
return;
|
||||||
}
|
}
|
||||||
raw['allowed_at_index'] = allowedAtIndex;
|
raw['allowed_at_index'] = allowedAtIndex;
|
||||||
|
|
@ -1407,11 +1504,15 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> updateInboundGroupSessionIndexes(
|
Future<void> updateInboundGroupSessionIndexes(
|
||||||
String indexes, String roomId, String sessionId) async {
|
String indexes,
|
||||||
|
String roomId,
|
||||||
|
String sessionId,
|
||||||
|
) async {
|
||||||
final raw = await _inboundGroupSessionsBox.get(sessionId);
|
final raw = await _inboundGroupSessionsBox.get(sessionId);
|
||||||
if (raw == null) {
|
if (raw == null) {
|
||||||
Logs().w(
|
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;
|
return;
|
||||||
}
|
}
|
||||||
final json = copyMap(raw);
|
final json = copyMap(raw);
|
||||||
|
|
@ -1510,11 +1611,15 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
|
||||||
}
|
}
|
||||||
for (final key in json[_preloadRoomStateBoxName]!.keys) {
|
for (final key in json[_preloadRoomStateBoxName]!.keys) {
|
||||||
await _preloadRoomStateBox.put(
|
await _preloadRoomStateBox.put(
|
||||||
key, json[_preloadRoomStateBoxName]![key]);
|
key,
|
||||||
|
json[_preloadRoomStateBoxName]![key],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
for (final key in json[_nonPreloadRoomStateBoxName]!.keys) {
|
for (final key in json[_nonPreloadRoomStateBoxName]!.keys) {
|
||||||
await _nonPreloadRoomStateBox.put(
|
await _nonPreloadRoomStateBox.put(
|
||||||
key, json[_nonPreloadRoomStateBoxName]![key]);
|
key,
|
||||||
|
json[_nonPreloadRoomStateBoxName]![key],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
for (final key in json[_roomMembersBoxName]!.keys) {
|
for (final key in json[_roomMembersBoxName]!.keys) {
|
||||||
await _roomMembersBox.put(key, json[_roomMembersBoxName]![key]);
|
await _roomMembersBox.put(key, json[_roomMembersBoxName]![key]);
|
||||||
|
|
@ -1527,15 +1632,21 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
|
||||||
}
|
}
|
||||||
for (final key in json[_inboundGroupSessionsBoxName]!.keys) {
|
for (final key in json[_inboundGroupSessionsBoxName]!.keys) {
|
||||||
await _inboundGroupSessionsBox.put(
|
await _inboundGroupSessionsBox.put(
|
||||||
key, json[_inboundGroupSessionsBoxName]![key]);
|
key,
|
||||||
|
json[_inboundGroupSessionsBoxName]![key],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
for (final key in json[_inboundGroupSessionsUploadQueueBoxName]!.keys) {
|
for (final key in json[_inboundGroupSessionsUploadQueueBoxName]!.keys) {
|
||||||
await _inboundGroupSessionsUploadQueueBox.put(
|
await _inboundGroupSessionsUploadQueueBox.put(
|
||||||
key, json[_inboundGroupSessionsUploadQueueBoxName]![key]);
|
key,
|
||||||
|
json[_inboundGroupSessionsUploadQueueBoxName]![key],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
for (final key in json[_outboundGroupSessionsBoxName]!.keys) {
|
for (final key in json[_outboundGroupSessionsBoxName]!.keys) {
|
||||||
await _outboundGroupSessionsBox.put(
|
await _outboundGroupSessionsBox.put(
|
||||||
key, json[_outboundGroupSessionsBoxName]![key]);
|
key,
|
||||||
|
json[_outboundGroupSessionsBoxName]![key],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
for (final key in json[_olmSessionsBoxName]!.keys) {
|
for (final key in json[_olmSessionsBoxName]!.keys) {
|
||||||
await _olmSessionsBox.put(key, json[_olmSessionsBoxName]![key]);
|
await _olmSessionsBox.put(key, json[_olmSessionsBoxName]![key]);
|
||||||
|
|
@ -1545,11 +1656,15 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
|
||||||
}
|
}
|
||||||
for (final key in json[_userDeviceKeysOutdatedBoxName]!.keys) {
|
for (final key in json[_userDeviceKeysOutdatedBoxName]!.keys) {
|
||||||
await _userDeviceKeysOutdatedBox.put(
|
await _userDeviceKeysOutdatedBox.put(
|
||||||
key, json[_userDeviceKeysOutdatedBoxName]![key]);
|
key,
|
||||||
|
json[_userDeviceKeysOutdatedBoxName]![key],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
for (final key in json[_userCrossSigningKeysBoxName]!.keys) {
|
for (final key in json[_userCrossSigningKeysBoxName]!.keys) {
|
||||||
await _userCrossSigningKeysBox.put(
|
await _userCrossSigningKeysBox.put(
|
||||||
key, json[_userCrossSigningKeysBoxName]![key]);
|
key,
|
||||||
|
json[_userCrossSigningKeysBoxName]![key],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
for (final key in json[_ssssCacheBoxName]!.keys) {
|
for (final key in json[_ssssCacheBoxName]!.keys) {
|
||||||
await _ssssCacheBox.put(key, json[_ssssCacheBoxName]![key]);
|
await _ssssCacheBox.put(key, json[_ssssCacheBoxName]![key]);
|
||||||
|
|
@ -1559,7 +1674,9 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
|
||||||
}
|
}
|
||||||
for (final key in json[_timelineFragmentsBoxName]!.keys) {
|
for (final key in json[_timelineFragmentsBoxName]!.keys) {
|
||||||
await _timelineFragmentsBox.put(
|
await _timelineFragmentsBox.put(
|
||||||
key, json[_timelineFragmentsBoxName]![key]);
|
key,
|
||||||
|
json[_timelineFragmentsBoxName]![key],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
for (final key in json[_seenDeviceIdsBoxName]!.keys) {
|
for (final key in json[_seenDeviceIdsBoxName]!.keys) {
|
||||||
await _seenDeviceIdsBox.put(key, json[_seenDeviceIdsBoxName]![key]);
|
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
|
// Get the synced event IDs from the store
|
||||||
final timelineKey = TupleKey(room.id, '').toString();
|
final timelineKey = TupleKey(room.id, '').toString();
|
||||||
final timelineEventIds = List<String>.from(
|
final timelineEventIds = List<String>.from(
|
||||||
(await _timelineFragmentsBox.get(timelineKey)) ?? []);
|
(await _timelineFragmentsBox.get(timelineKey)) ?? [],
|
||||||
|
);
|
||||||
|
|
||||||
// Get the local stored SENDING events from the store
|
// Get the local stored SENDING events from the store
|
||||||
late final List<String> sendingEventIds;
|
late final List<String> sendingEventIds;
|
||||||
|
|
@ -1594,7 +1712,8 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
|
||||||
} else {
|
} else {
|
||||||
final sendingTimelineKey = TupleKey(room.id, 'SENDING').toString();
|
final sendingTimelineKey = TupleKey(room.id, 'SENDING').toString();
|
||||||
sendingEventIds = List<String>.from(
|
sendingEventIds = List<String>.from(
|
||||||
(await _timelineFragmentsBox.get(sendingTimelineKey)) ?? []);
|
(await _timelineFragmentsBox.get(sendingTimelineKey)) ?? [],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Combine those two lists while respecting the start and limit parameters.
|
// Combine those two lists while respecting the start and limit parameters.
|
||||||
|
|
@ -1667,13 +1786,17 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<CachedProfileInformation?> getUserProfile(String userId) =>
|
Future<CachedProfileInformation?> getUserProfile(String userId) =>
|
||||||
_userProfilesBox.get(userId).then((json) => json == null
|
_userProfilesBox.get(userId).then(
|
||||||
? null
|
(json) => json == null
|
||||||
: CachedProfileInformation.fromJson(copyMap(json)));
|
? null
|
||||||
|
: CachedProfileInformation.fromJson(copyMap(json)),
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> storeUserProfile(
|
Future<void> storeUserProfile(
|
||||||
String userId, CachedProfileInformation profile) =>
|
String userId,
|
||||||
|
CachedProfileInformation profile,
|
||||||
|
) =>
|
||||||
_userProfilesBox.put(
|
_userProfilesBox.put(
|
||||||
userId,
|
userId,
|
||||||
profile.toJson(),
|
profile.toJson(),
|
||||||
|
|
|
||||||
|
|
@ -127,7 +127,8 @@ class Box<V> {
|
||||||
if (value == null) return null;
|
if (value == null) return null;
|
||||||
if (value is! String) {
|
if (value is! String) {
|
||||||
throw Exception(
|
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) {
|
switch (V) {
|
||||||
case const (int):
|
case const (int):
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,8 @@ class SQfLiteEncryptionHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
Logs().d(
|
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.
|
// hell, it's unencrypted. This should not happen. Time to encrypt it.
|
||||||
final plainDb = await factory.openDatabase(path);
|
final plainDb = await factory.openDatabase(path);
|
||||||
|
|
@ -119,7 +120,8 @@ class SQfLiteEncryptionHelper {
|
||||||
final encryptedPath = '$path.encrypted';
|
final encryptedPath = '$path.encrypted';
|
||||||
|
|
||||||
await plainDb.execute(
|
await plainDb.execute(
|
||||||
"ATTACH DATABASE '$encryptedPath' AS encrypted KEY '$cipher';");
|
"ATTACH DATABASE '$encryptedPath' AS encrypted KEY '$cipher';",
|
||||||
|
);
|
||||||
await plainDb.execute("SELECT sqlcipher_export('encrypted');");
|
await plainDb.execute("SELECT sqlcipher_export('encrypted');");
|
||||||
// ignore: prefer_single_quotes
|
// ignore: prefer_single_quotes
|
||||||
await plainDb.execute("DETACH DATABASE encrypted;");
|
await plainDb.execute("DETACH DATABASE encrypted;");
|
||||||
|
|
@ -164,7 +166,8 @@ class SQfLiteEncryptionHelper {
|
||||||
} else {
|
} else {
|
||||||
final version = cipherVersion.singleOrNull?['cipher_version'];
|
final version = cipherVersion.singleOrNull?['cipher_version'];
|
||||||
Logs().d(
|
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';");
|
final result = await database.rawQuery("PRAGMA KEY='$cipher';");
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,8 @@ class Event extends MatrixEvent {
|
||||||
);
|
);
|
||||||
|
|
||||||
@Deprecated(
|
@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 sender => senderFromMemoryOrFallback;
|
||||||
|
|
||||||
User get senderFromMemoryOrFallback =>
|
User get senderFromMemoryOrFallback =>
|
||||||
|
|
@ -138,7 +139,7 @@ class Event extends MatrixEvent {
|
||||||
timeline: TimelineUpdate(
|
timeline: TimelineUpdate(
|
||||||
events: [MatrixEvent.fromJson(json)],
|
events: [MatrixEvent.fromJson(json)],
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -188,22 +189,25 @@ class Event extends MatrixEvent {
|
||||||
final originalSource =
|
final originalSource =
|
||||||
Event.getMapFromPayload(jsonPayload['original_source']);
|
Event.getMapFromPayload(jsonPayload['original_source']);
|
||||||
return Event(
|
return Event(
|
||||||
status: eventStatusFromInt(jsonPayload['status'] ??
|
status: eventStatusFromInt(
|
||||||
|
jsonPayload['status'] ??
|
||||||
unsigned[messageSendingStatusKey] ??
|
unsigned[messageSendingStatusKey] ??
|
||||||
defaultStatus.intValue),
|
defaultStatus.intValue,
|
||||||
stateKey: jsonPayload['state_key'],
|
),
|
||||||
prevContent: prevContent,
|
stateKey: jsonPayload['state_key'],
|
||||||
content: content,
|
prevContent: prevContent,
|
||||||
type: jsonPayload['type'],
|
content: content,
|
||||||
eventId: jsonPayload['event_id'] ?? '',
|
type: jsonPayload['type'],
|
||||||
senderId: jsonPayload['sender'],
|
eventId: jsonPayload['event_id'] ?? '',
|
||||||
originServerTs: DateTime.fromMillisecondsSinceEpoch(
|
senderId: jsonPayload['sender'],
|
||||||
jsonPayload['origin_server_ts'] ?? 0),
|
originServerTs: DateTime.fromMillisecondsSinceEpoch(
|
||||||
unsigned: unsigned,
|
jsonPayload['origin_server_ts'] ?? 0,
|
||||||
room: room,
|
),
|
||||||
originalSource: originalSource.isEmpty
|
unsigned: unsigned,
|
||||||
? null
|
room: room,
|
||||||
: MatrixEvent.fromJson(originalSource));
|
originalSource:
|
||||||
|
originalSource.isEmpty ? null : MatrixEvent.fromJson(originalSource),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -311,9 +315,12 @@ class Event extends MatrixEvent {
|
||||||
final receipts = room.receiptState;
|
final receipts = room.receiptState;
|
||||||
final receiptsList = receipts.global.otherUsers.entries
|
final receiptsList = receipts.global.otherUsers.entries
|
||||||
.where((entry) => entry.value.eventId == eventId)
|
.where((entry) => entry.value.eventId == eventId)
|
||||||
.map((entry) => Receipt(
|
.map(
|
||||||
|
(entry) => Receipt(
|
||||||
room.unsafeGetUserFromMemoryOrFallback(entry.key),
|
room.unsafeGetUserFromMemoryOrFallback(entry.key),
|
||||||
entry.value.timestamp))
|
entry.value.timestamp,
|
||||||
|
),
|
||||||
|
)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
// add your own only once
|
// add your own only once
|
||||||
|
|
@ -321,21 +328,31 @@ class Event extends MatrixEvent {
|
||||||
receipts.mainThread?.latestOwnReceipt;
|
receipts.mainThread?.latestOwnReceipt;
|
||||||
if (own != null && own.eventId == eventId) {
|
if (own != null && own.eventId == eventId) {
|
||||||
receiptsList.add(
|
receiptsList.add(
|
||||||
Receipt(room.unsafeGetUserFromMemoryOrFallback(room.client.userID!),
|
Receipt(
|
||||||
own.timestamp),
|
room.unsafeGetUserFromMemoryOrFallback(room.client.userID!),
|
||||||
|
own.timestamp,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// also add main thread. https://github.com/famedly/product-management/issues/1020
|
// also add main thread. https://github.com/famedly/product-management/issues/1020
|
||||||
// also deduplicate.
|
// also deduplicate.
|
||||||
receiptsList.addAll(receipts.mainThread?.otherUsers.entries
|
receiptsList.addAll(
|
||||||
.where((entry) =>
|
receipts.mainThread?.otherUsers.entries
|
||||||
entry.value.eventId == eventId &&
|
.where(
|
||||||
receiptsList.every((element) => element.user.id != entry.key))
|
(entry) =>
|
||||||
.map((entry) => Receipt(
|
entry.value.eventId == eventId &&
|
||||||
room.unsafeGetUserFromMemoryOrFallback(entry.key),
|
receiptsList
|
||||||
entry.value.timestamp)) ??
|
.every((element) => element.user.id != entry.key),
|
||||||
[]);
|
)
|
||||||
|
.map(
|
||||||
|
(entry) => Receipt(
|
||||||
|
room.unsafeGetUserFromMemoryOrFallback(entry.key),
|
||||||
|
entry.value.timestamp,
|
||||||
|
),
|
||||||
|
) ??
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
return receiptsList;
|
return receiptsList;
|
||||||
}
|
}
|
||||||
|
|
@ -383,7 +400,7 @@ class Event extends MatrixEvent {
|
||||||
timeline: TimelineUpdate(
|
timeline: TimelineUpdate(
|
||||||
events: [redactedBecause],
|
events: [redactedBecause],
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -553,14 +570,15 @@ class Event extends MatrixEvent {
|
||||||
///
|
///
|
||||||
/// Important! To use this link you have to set a http header like this:
|
/// Important! To use this link you have to set a http header like this:
|
||||||
/// `headers: {"authorization": "Bearer ${client.accessToken}"}`
|
/// `headers: {"authorization": "Bearer ${client.accessToken}"}`
|
||||||
Future<Uri?> getAttachmentUri(
|
Future<Uri?> getAttachmentUri({
|
||||||
{bool getThumbnail = false,
|
bool getThumbnail = false,
|
||||||
bool useThumbnailMxcUrl = false,
|
bool useThumbnailMxcUrl = false,
|
||||||
double width = 800.0,
|
double width = 800.0,
|
||||||
double height = 800.0,
|
double height = 800.0,
|
||||||
ThumbnailMethod method = ThumbnailMethod.scale,
|
ThumbnailMethod method = ThumbnailMethod.scale,
|
||||||
int minNoThumbSize = _minNoThumbSize,
|
int minNoThumbSize = _minNoThumbSize,
|
||||||
bool animated = false}) async {
|
bool animated = false,
|
||||||
|
}) async {
|
||||||
if (![EventTypes.Message, EventTypes.Sticker].contains(type) ||
|
if (![EventTypes.Message, EventTypes.Sticker].contains(type) ||
|
||||||
!hasAttachment ||
|
!hasAttachment ||
|
||||||
isAttachmentEncrypted) {
|
isAttachmentEncrypted) {
|
||||||
|
|
@ -607,14 +625,15 @@ class Event extends MatrixEvent {
|
||||||
/// Important! To use this link you have to set a http header like this:
|
/// Important! To use this link you have to set a http header like this:
|
||||||
/// `headers: {"authorization": "Bearer ${client.accessToken}"}`
|
/// `headers: {"authorization": "Bearer ${client.accessToken}"}`
|
||||||
@Deprecated('Use getAttachmentUri() instead')
|
@Deprecated('Use getAttachmentUri() instead')
|
||||||
Uri? getAttachmentUrl(
|
Uri? getAttachmentUrl({
|
||||||
{bool getThumbnail = false,
|
bool getThumbnail = false,
|
||||||
bool useThumbnailMxcUrl = false,
|
bool useThumbnailMxcUrl = false,
|
||||||
double width = 800.0,
|
double width = 800.0,
|
||||||
double height = 800.0,
|
double height = 800.0,
|
||||||
ThumbnailMethod method = ThumbnailMethod.scale,
|
ThumbnailMethod method = ThumbnailMethod.scale,
|
||||||
int minNoThumbSize = _minNoThumbSize,
|
int minNoThumbSize = _minNoThumbSize,
|
||||||
bool animated = false}) {
|
bool animated = false,
|
||||||
|
}) {
|
||||||
if (![EventTypes.Message, EventTypes.Sticker].contains(type) ||
|
if (![EventTypes.Message, EventTypes.Sticker].contains(type) ||
|
||||||
!hasAttachment ||
|
!hasAttachment ||
|
||||||
isAttachmentEncrypted) {
|
isAttachmentEncrypted) {
|
||||||
|
|
@ -680,10 +699,11 @@ class Event extends MatrixEvent {
|
||||||
/// true to download the thumbnail instead. Set [fromLocalStoreOnly] to true
|
/// true to download the thumbnail instead. Set [fromLocalStoreOnly] to true
|
||||||
/// if you want to retrieve the attachment from the local store only without
|
/// if you want to retrieve the attachment from the local store only without
|
||||||
/// making http request.
|
/// making http request.
|
||||||
Future<MatrixFile> downloadAndDecryptAttachment(
|
Future<MatrixFile> downloadAndDecryptAttachment({
|
||||||
{bool getThumbnail = false,
|
bool getThumbnail = false,
|
||||||
Future<Uint8List> Function(Uri)? downloadCallback,
|
Future<Uint8List> Function(Uri)? downloadCallback,
|
||||||
bool fromLocalStoreOnly = false}) async {
|
bool fromLocalStoreOnly = false,
|
||||||
|
}) async {
|
||||||
if (![EventTypes.Message, EventTypes.Sticker].contains(type)) {
|
if (![EventTypes.Message, EventTypes.Sticker].contains(type)) {
|
||||||
throw ("This event has the type '$type' and so it can't contain an attachment.");
|
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;
|
uint8list.lengthInBytes < database.maxFileSize;
|
||||||
if (storeable) {
|
if (storeable) {
|
||||||
await database.storeFile(
|
await database.storeFile(
|
||||||
mxcUrl, uint8list, DateTime.now().millisecondsSinceEpoch);
|
mxcUrl,
|
||||||
|
uint8list,
|
||||||
|
DateTime.now().millisecondsSinceEpoch,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else if (uint8list == null) {
|
} else if (uint8list == null) {
|
||||||
throw ('Unable to download file from local store.');
|
throw ('Unable to download file from local store.');
|
||||||
|
|
@ -771,12 +794,14 @@ class Event extends MatrixEvent {
|
||||||
/// it to plain text.
|
/// it to plain text.
|
||||||
/// [removeMarkdown] allow to remove the markdown formating from the event body.
|
/// [removeMarkdown] allow to remove the markdown formating from the event body.
|
||||||
/// Usefull form message preview or notifications text.
|
/// Usefull form message preview or notifications text.
|
||||||
Future<String> calcLocalizedBody(MatrixLocalizations i18n,
|
Future<String> calcLocalizedBody(
|
||||||
{bool withSenderNamePrefix = false,
|
MatrixLocalizations i18n, {
|
||||||
bool hideReply = false,
|
bool withSenderNamePrefix = false,
|
||||||
bool hideEdit = false,
|
bool hideReply = false,
|
||||||
bool plaintextBody = false,
|
bool hideEdit = false,
|
||||||
bool removeMarkdown = false}) async {
|
bool plaintextBody = false,
|
||||||
|
bool removeMarkdown = false,
|
||||||
|
}) async {
|
||||||
if (redacted) {
|
if (redacted) {
|
||||||
await redactedBecause?.fetchSenderUser();
|
await redactedBecause?.fetchSenderUser();
|
||||||
}
|
}
|
||||||
|
|
@ -799,30 +824,36 @@ class Event extends MatrixEvent {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deprecated('Use calcLocalizedBody or calcLocalizedBodyFallback')
|
@Deprecated('Use calcLocalizedBody or calcLocalizedBodyFallback')
|
||||||
String getLocalizedBody(MatrixLocalizations i18n,
|
String getLocalizedBody(
|
||||||
{bool withSenderNamePrefix = false,
|
MatrixLocalizations i18n, {
|
||||||
bool hideReply = false,
|
bool withSenderNamePrefix = false,
|
||||||
bool hideEdit = false,
|
bool hideReply = false,
|
||||||
bool plaintextBody = false,
|
bool hideEdit = false,
|
||||||
bool removeMarkdown = false}) =>
|
bool plaintextBody = false,
|
||||||
calcLocalizedBodyFallback(i18n,
|
bool removeMarkdown = false,
|
||||||
withSenderNamePrefix: withSenderNamePrefix,
|
}) =>
|
||||||
hideReply: hideReply,
|
calcLocalizedBodyFallback(
|
||||||
hideEdit: hideEdit,
|
i18n,
|
||||||
plaintextBody: plaintextBody,
|
withSenderNamePrefix: withSenderNamePrefix,
|
||||||
removeMarkdown: removeMarkdown);
|
hideReply: hideReply,
|
||||||
|
hideEdit: hideEdit,
|
||||||
|
plaintextBody: plaintextBody,
|
||||||
|
removeMarkdown: removeMarkdown,
|
||||||
|
);
|
||||||
|
|
||||||
/// Works similar to `calcLocalizedBody()` but does not wait for the sender
|
/// 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
|
/// 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
|
/// fallback and display the localpart of the MXID according to the
|
||||||
/// values of `formatLocalpart` and `mxidLocalPartFallback` in the `Client`
|
/// values of `formatLocalpart` and `mxidLocalPartFallback` in the `Client`
|
||||||
/// class.
|
/// class.
|
||||||
String calcLocalizedBodyFallback(MatrixLocalizations i18n,
|
String calcLocalizedBodyFallback(
|
||||||
{bool withSenderNamePrefix = false,
|
MatrixLocalizations i18n, {
|
||||||
bool hideReply = false,
|
bool withSenderNamePrefix = false,
|
||||||
bool hideEdit = false,
|
bool hideReply = false,
|
||||||
bool plaintextBody = false,
|
bool hideEdit = false,
|
||||||
bool removeMarkdown = false}) {
|
bool plaintextBody = false,
|
||||||
|
bool removeMarkdown = false,
|
||||||
|
}) {
|
||||||
if (redacted) {
|
if (redacted) {
|
||||||
if (status.intValue < EventStatus.synced.intValue) {
|
if (status.intValue < EventStatus.synced.intValue) {
|
||||||
return i18n.cancelledSend;
|
return i18n.cancelledSend;
|
||||||
|
|
@ -896,7 +927,9 @@ class Event extends MatrixEvent {
|
||||||
// if the message is formatted
|
// if the message is formatted
|
||||||
if (hideReply && mayHaveReplyFallback) {
|
if (hideReply && mayHaveReplyFallback) {
|
||||||
body = body.replaceFirst(
|
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
|
// 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
|
// we need to check again if it isn't empty, as we potentially removed all
|
||||||
// aggregated edits
|
// aggregated edits
|
||||||
if (allEditEvents.isNotEmpty) {
|
if (allEditEvents.isNotEmpty) {
|
||||||
allEditEvents.sort((a, b) => a.originServerTs.millisecondsSinceEpoch -
|
allEditEvents.sort(
|
||||||
b.originServerTs.millisecondsSinceEpoch >
|
(a, b) => a.originServerTs.millisecondsSinceEpoch -
|
||||||
0
|
b.originServerTs.millisecondsSinceEpoch >
|
||||||
? 1
|
0
|
||||||
: -1);
|
? 1
|
||||||
|
: -1,
|
||||||
|
);
|
||||||
final rawEvent = allEditEvents.last.toJson();
|
final rawEvent = allEditEvents.last.toJson();
|
||||||
// update the content of the new event to render
|
// update the content of the new event to render
|
||||||
if (rawEvent['content']['m.new_content'] is Map) {
|
if (rawEvent['content']['m.new_content'] is Map) {
|
||||||
|
|
@ -1051,9 +1086,14 @@ class Event extends MatrixEvent {
|
||||||
if (isRichMessage) {
|
if (isRichMessage) {
|
||||||
// calcUnlocalizedBody strips out the <img /> tags in favor of a :placeholder:
|
// calcUnlocalizedBody strips out the <img /> tags in favor of a :placeholder:
|
||||||
final formattedTextStripped = formattedText.replaceAll(
|
final formattedTextStripped = formattedText.replaceAll(
|
||||||
RegExp('<mx-reply>.*</mx-reply>',
|
RegExp(
|
||||||
caseSensitive: false, multiLine: false, dotAll: true),
|
'<mx-reply>.*</mx-reply>',
|
||||||
'');
|
caseSensitive: false,
|
||||||
|
multiLine: false,
|
||||||
|
dotAll: true,
|
||||||
|
),
|
||||||
|
'',
|
||||||
|
);
|
||||||
return _onlyEmojiEmoteRegex.hasMatch(formattedTextStripped);
|
return _onlyEmojiEmoteRegex.hasMatch(formattedTextStripped);
|
||||||
} else {
|
} else {
|
||||||
return _onlyEmojiRegex.hasMatch(plaintextBody);
|
return _onlyEmojiRegex.hasMatch(plaintextBody);
|
||||||
|
|
@ -1068,9 +1108,14 @@ class Event extends MatrixEvent {
|
||||||
if (isRichMessage) {
|
if (isRichMessage) {
|
||||||
// calcUnlocalizedBody strips out the <img /> tags in favor of a :placeholder:
|
// calcUnlocalizedBody strips out the <img /> tags in favor of a :placeholder:
|
||||||
final formattedTextStripped = formattedText.replaceAll(
|
final formattedTextStripped = formattedText.replaceAll(
|
||||||
RegExp('<mx-reply>.*</mx-reply>',
|
RegExp(
|
||||||
caseSensitive: false, multiLine: false, dotAll: true),
|
'<mx-reply>.*</mx-reply>',
|
||||||
'');
|
caseSensitive: false,
|
||||||
|
multiLine: false,
|
||||||
|
dotAll: true,
|
||||||
|
),
|
||||||
|
'',
|
||||||
|
);
|
||||||
return _countEmojiEmoteRegex.allMatches(formattedTextStripped).length;
|
return _countEmojiEmoteRegex.allMatches(formattedTextStripped).length;
|
||||||
} else {
|
} else {
|
||||||
return _countEmojiRegex.allMatches(plaintextBody).length;
|
return _countEmojiRegex.allMatches(plaintextBody).length;
|
||||||
|
|
@ -1083,7 +1128,8 @@ class Event extends MatrixEvent {
|
||||||
final status = unsigned?.tryGet<String>(fileSendingStatusKey);
|
final status = unsigned?.tryGet<String>(fileSendingStatusKey);
|
||||||
if (status == null) return null;
|
if (status == null) return null;
|
||||||
return FileSendingStatus.values.singleWhereOrNull(
|
return FileSendingStatus.values.singleWhereOrNull(
|
||||||
(fileSendingStatus) => fileSendingStatus.name == status);
|
(fileSendingStatus) => fileSendingStatus.name == status,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ extension EventStatusExtension on EventStatus {
|
||||||
bool get isSent => [
|
bool get isSent => [
|
||||||
EventStatus.sent,
|
EventStatus.sent,
|
||||||
EventStatus.synced,
|
EventStatus.synced,
|
||||||
EventStatus.roomState
|
EventStatus.roomState,
|
||||||
].contains(this);
|
].contains(this);
|
||||||
|
|
||||||
/// Returns `true` if the status is `synced` or `roomState`:
|
/// Returns `true` if the status is `synced` or `roomState`:
|
||||||
|
|
|
||||||
|
|
@ -167,10 +167,11 @@ class LatestReceiptStateForTimeline {
|
||||||
|
|
||||||
factory LatestReceiptStateForTimeline.empty() =>
|
factory LatestReceiptStateForTimeline.empty() =>
|
||||||
LatestReceiptStateForTimeline(
|
LatestReceiptStateForTimeline(
|
||||||
ownPrivate: null,
|
ownPrivate: null,
|
||||||
ownPublic: null,
|
ownPublic: null,
|
||||||
latestOwnReceipt: null,
|
latestOwnReceipt: null,
|
||||||
otherUsers: {});
|
otherUsers: {},
|
||||||
|
);
|
||||||
|
|
||||||
factory LatestReceiptStateForTimeline.fromJson(Map<String, dynamic> json) {
|
factory LatestReceiptStateForTimeline.fromJson(Map<String, dynamic> json) {
|
||||||
final private = json['private'];
|
final private = json['private'];
|
||||||
|
|
@ -229,7 +230,8 @@ class LatestReceiptState {
|
||||||
mainThread:
|
mainThread:
|
||||||
main.isNotEmpty ? LatestReceiptStateForTimeline.fromJson(main) : null,
|
main.isNotEmpty ? LatestReceiptStateForTimeline.fromJson(main) : null,
|
||||||
byThread: byThread.map(
|
byThread: byThread.map(
|
||||||
(k, v) => MapEntry(k, LatestReceiptStateForTimeline.fromJson(v))),
|
(k, v) => MapEntry(k, LatestReceiptStateForTimeline.fromJson(v)),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,9 @@ class TimelineChunk {
|
||||||
String nextBatch;
|
String nextBatch;
|
||||||
|
|
||||||
List<Event> events;
|
List<Event> events;
|
||||||
TimelineChunk(
|
TimelineChunk({
|
||||||
{required this.events, this.prevBatch = '', this.nextBatch = ''});
|
required this.events,
|
||||||
|
this.prevBatch = '',
|
||||||
|
this.nextBatch = '',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,8 @@ class CachedPresence {
|
||||||
.singleWhere((type) => type.name == json['presence']),
|
.singleWhere((type) => type.name == json['presence']),
|
||||||
lastActiveTimestamp: json['last_active_timestamp'] != null
|
lastActiveTimestamp: json['last_active_timestamp'] != null
|
||||||
? DateTime.fromMillisecondsSinceEpoch(
|
? DateTime.fromMillisecondsSinceEpoch(
|
||||||
json['last_active_timestamp'] as int)
|
json['last_active_timestamp'] as int,
|
||||||
|
)
|
||||||
: null,
|
: null,
|
||||||
statusMsg: json['status_msg'] as String?,
|
statusMsg: json['status_msg'] as String?,
|
||||||
currentlyActive: json['currently_active'] as bool?,
|
currentlyActive: json['currently_active'] as bool?,
|
||||||
|
|
@ -55,8 +56,13 @@ class CachedPresence {
|
||||||
this.currentlyActive,
|
this.currentlyActive,
|
||||||
});
|
});
|
||||||
|
|
||||||
CachedPresence(this.presence, int? lastActiveAgo, this.statusMsg,
|
CachedPresence(
|
||||||
this.currentlyActive, this.userid) {
|
this.presence,
|
||||||
|
int? lastActiveAgo,
|
||||||
|
this.statusMsg,
|
||||||
|
this.currentlyActive,
|
||||||
|
this.userid,
|
||||||
|
) {
|
||||||
if (lastActiveAgo != null) {
|
if (lastActiveAgo != null) {
|
||||||
lastActiveTimestamp =
|
lastActiveTimestamp =
|
||||||
DateTime.now().subtract(Duration(milliseconds: lastActiveAgo));
|
DateTime.now().subtract(Duration(milliseconds: lastActiveAgo));
|
||||||
|
|
@ -65,15 +71,21 @@ class CachedPresence {
|
||||||
|
|
||||||
CachedPresence.fromMatrixEvent(Presence event)
|
CachedPresence.fromMatrixEvent(Presence event)
|
||||||
: this(
|
: this(
|
||||||
event.presence.presence,
|
event.presence.presence,
|
||||||
event.presence.lastActiveAgo,
|
event.presence.lastActiveAgo,
|
||||||
event.presence.statusMsg,
|
event.presence.statusMsg,
|
||||||
event.presence.currentlyActive,
|
event.presence.currentlyActive,
|
||||||
event.senderId);
|
event.senderId,
|
||||||
|
);
|
||||||
|
|
||||||
CachedPresence.fromPresenceResponse(GetPresenceResponse event, String userid)
|
CachedPresence.fromPresenceResponse(GetPresenceResponse event, String userid)
|
||||||
: this(event.presence, event.lastActiveAgo, event.statusMsg,
|
: this(
|
||||||
event.currentlyActive, userid);
|
event.presence,
|
||||||
|
event.lastActiveAgo,
|
||||||
|
event.statusMsg,
|
||||||
|
event.currentlyActive,
|
||||||
|
userid,
|
||||||
|
);
|
||||||
|
|
||||||
CachedPresence.neverSeen(this.userid) : presence = PresenceType.offline;
|
CachedPresence.neverSeen(this.userid) : presence = PresenceType.offline;
|
||||||
|
|
||||||
|
|
@ -91,7 +103,7 @@ class CachedPresence {
|
||||||
final json = {
|
final json = {
|
||||||
'content': content,
|
'content': content,
|
||||||
'sender': '@example:localhost',
|
'sender': '@example:localhost',
|
||||||
'type': 'm.presence'
|
'type': 'm.presence',
|
||||||
};
|
};
|
||||||
|
|
||||||
return Presence.fromJson(json);
|
return Presence.fromJson(json);
|
||||||
|
|
|
||||||
|
|
@ -115,9 +115,11 @@ class Room {
|
||||||
if (!partial) {
|
if (!partial) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final allStates = await client.database
|
final allStates =
|
||||||
?.getUnimportantRoomEventStatesForRoom(
|
await client.database?.getUnimportantRoomEventStatesForRoom(
|
||||||
client.importantStateEvents.toList(), this);
|
client.importantStateEvents.toList(),
|
||||||
|
this,
|
||||||
|
);
|
||||||
|
|
||||||
if (allStates != null) {
|
if (allStates != null) {
|
||||||
for (final state in allStates) {
|
for (final state in allStates) {
|
||||||
|
|
@ -216,12 +218,16 @@ class Room {
|
||||||
|
|
||||||
if (heroes == null) return [];
|
if (heroes == null) return [];
|
||||||
|
|
||||||
return await Future.wait(heroes.map((hero) async =>
|
return await Future.wait(
|
||||||
(await requestUser(
|
heroes.map(
|
||||||
hero,
|
(hero) async =>
|
||||||
ignoreErrors: true,
|
(await requestUser(
|
||||||
)) ??
|
hero,
|
||||||
User(hero, room: this)));
|
ignoreErrors: true,
|
||||||
|
)) ??
|
||||||
|
User(hero, room: this),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a localized displayname for this server. If the room is a groupchat
|
/// 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
|
// removing oneself from the hero list
|
||||||
(hero) => hero.isNotEmpty && hero != client.userID,
|
(hero) => hero.isNotEmpty && hero != client.userID,
|
||||||
)
|
)
|
||||||
.map((hero) => unsafeGetUserFromMemoryOrFallback(hero)
|
.map(
|
||||||
.calcDisplayname(i18n: i18n))
|
(hero) => unsafeGetUserFromMemoryOrFallback(hero)
|
||||||
|
.calcDisplayname(i18n: i18n),
|
||||||
|
)
|
||||||
.join(', ');
|
.join(', ');
|
||||||
if (isAbandonedDMRoom) {
|
if (isAbandonedDMRoom) {
|
||||||
return i18n.wasDirectChatDisplayName(result);
|
return i18n.wasDirectChatDisplayName(result);
|
||||||
|
|
@ -273,8 +281,9 @@ class Room {
|
||||||
if (membership == Membership.leave) {
|
if (membership == Membership.leave) {
|
||||||
if (directChatMatrixID != null) {
|
if (directChatMatrixID != null) {
|
||||||
return i18n.wasDirectChatDisplayName(
|
return i18n.wasDirectChatDisplayName(
|
||||||
unsafeGetUserFromMemoryOrFallback(directChatMatrixID)
|
unsafeGetUserFromMemoryOrFallback(directChatMatrixID)
|
||||||
.calcDisplayname(i18n: i18n));
|
.calcDisplayname(i18n: i18n),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return i18n.emptyChat;
|
return i18n.emptyChat;
|
||||||
|
|
@ -450,12 +459,13 @@ class Room {
|
||||||
|
|
||||||
/// Add a tag to the room.
|
/// Add a tag to the room.
|
||||||
Future<void> addTag(String tag, {double? order}) => client.setRoomTag(
|
Future<void> addTag(String tag, {double? order}) => client.setRoomTag(
|
||||||
client.userID!,
|
client.userID!,
|
||||||
id,
|
id,
|
||||||
tag,
|
tag,
|
||||||
Tag(
|
Tag(
|
||||||
order: order,
|
order: order,
|
||||||
));
|
),
|
||||||
|
);
|
||||||
|
|
||||||
/// Removes a tag from the room.
|
/// Removes a tag from the room.
|
||||||
Future<void> removeTag(String tag) => client.deleteRoomTag(
|
Future<void> removeTag(String tag) => client.deleteRoomTag(
|
||||||
|
|
@ -493,10 +503,10 @@ class Room {
|
||||||
|
|
||||||
bool get markedUnread {
|
bool get markedUnread {
|
||||||
return MarkedUnread.fromJson(
|
return MarkedUnread.fromJson(
|
||||||
roomAccountData[EventType.markedUnread]?.content ??
|
roomAccountData[EventType.markedUnread]?.content ??
|
||||||
roomAccountData[EventType.oldMarkedUnread]?.content ??
|
roomAccountData[EventType.oldMarkedUnread]?.content ??
|
||||||
{})
|
{},
|
||||||
.unread;
|
).unread;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks if the last event has a read marker of the user.
|
/// Checks if the last event has a read marker of the user.
|
||||||
|
|
@ -525,8 +535,9 @@ class Room {
|
||||||
}
|
}
|
||||||
|
|
||||||
LatestReceiptState get receiptState => LatestReceiptState.fromJson(
|
LatestReceiptState get receiptState => LatestReceiptState.fromJson(
|
||||||
roomAccountData[LatestReceiptState.eventType]?.content ??
|
roomAccountData[LatestReceiptState.eventType]?.content ??
|
||||||
<String, dynamic>{});
|
<String, dynamic>{},
|
||||||
|
);
|
||||||
|
|
||||||
/// Returns true if this room is unread. To check if there are new messages
|
/// Returns true if this room is unread. To check if there are new messages
|
||||||
/// in muted rooms, use [hasNewMessages].
|
/// in muted rooms, use [hasNewMessages].
|
||||||
|
|
@ -564,7 +575,7 @@ class Room {
|
||||||
type: EventType.markedUnread,
|
type: EventType.markedUnread,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -600,31 +611,38 @@ class Room {
|
||||||
|
|
||||||
/// Sends a normal text message to this room. Returns the event ID generated
|
/// Sends a normal text message to this room. Returns the event ID generated
|
||||||
/// by the server for this message.
|
/// by the server for this message.
|
||||||
Future<String?> sendTextEvent(String message,
|
Future<String?> sendTextEvent(
|
||||||
{String? txid,
|
String message, {
|
||||||
Event? inReplyTo,
|
String? txid,
|
||||||
String? editEventId,
|
Event? inReplyTo,
|
||||||
bool parseMarkdown = true,
|
String? editEventId,
|
||||||
bool parseCommands = true,
|
bool parseMarkdown = true,
|
||||||
String msgtype = MessageTypes.Text,
|
bool parseCommands = true,
|
||||||
String? threadRootEventId,
|
String msgtype = MessageTypes.Text,
|
||||||
String? threadLastEventId}) {
|
String? threadRootEventId,
|
||||||
|
String? threadLastEventId,
|
||||||
|
}) {
|
||||||
if (parseCommands) {
|
if (parseCommands) {
|
||||||
return client.parseAndRunCommand(this, message,
|
return client.parseAndRunCommand(
|
||||||
inReplyTo: inReplyTo,
|
this,
|
||||||
editEventId: editEventId,
|
message,
|
||||||
txid: txid,
|
inReplyTo: inReplyTo,
|
||||||
threadRootEventId: threadRootEventId,
|
editEventId: editEventId,
|
||||||
threadLastEventId: threadLastEventId);
|
txid: txid,
|
||||||
|
threadRootEventId: threadRootEventId,
|
||||||
|
threadLastEventId: threadLastEventId,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
final event = <String, dynamic>{
|
final event = <String, dynamic>{
|
||||||
'msgtype': msgtype,
|
'msgtype': msgtype,
|
||||||
'body': message,
|
'body': message,
|
||||||
};
|
};
|
||||||
if (parseMarkdown) {
|
if (parseMarkdown) {
|
||||||
final html = markdown(event['body'],
|
final html = markdown(
|
||||||
getEmotePacks: () => getImagePacksFlat(ImagePackUsage.emoticon),
|
event['body'],
|
||||||
getMention: getMention);
|
getEmotePacks: () => getImagePacksFlat(ImagePackUsage.emoticon),
|
||||||
|
getMention: getMention,
|
||||||
|
);
|
||||||
// if the decoded html is the same as the body, there is no need in sending a formatted message
|
// 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')) !=
|
if (HtmlUnescape().convert(html.replaceAll(RegExp(r'<br />\n?'), '\n')) !=
|
||||||
event['body']) {
|
event['body']) {
|
||||||
|
|
@ -645,13 +663,17 @@ class Room {
|
||||||
/// Sends a reaction to an event with an [eventId] and the content [key] into a 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.
|
/// Returns the event ID generated by the server for this reaction.
|
||||||
Future<String?> sendReaction(String eventId, String key, {String? txid}) {
|
Future<String?> sendReaction(String eventId, String key, {String? txid}) {
|
||||||
return sendEvent({
|
return sendEvent(
|
||||||
'm.relates_to': {
|
{
|
||||||
'rel_type': RelationshipTypes.reaction,
|
'm.relates_to': {
|
||||||
'event_id': eventId,
|
'rel_type': RelationshipTypes.reaction,
|
||||||
'key': key,
|
'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.
|
/// Sends the location with description [body] and geo URI [geoUri] into a room.
|
||||||
|
|
@ -845,10 +867,10 @@ class Room {
|
||||||
'ext': true,
|
'ext': true,
|
||||||
'k': encryptedFile.k,
|
'k': encryptedFile.k,
|
||||||
'key_ops': ['encrypt', 'decrypt'],
|
'key_ops': ['encrypt', 'decrypt'],
|
||||||
'kty': 'oct'
|
'kty': 'oct',
|
||||||
},
|
},
|
||||||
'iv': encryptedFile.iv,
|
'iv': encryptedFile.iv,
|
||||||
'hashes': {'sha256': encryptedFile.sha256}
|
'hashes': {'sha256': encryptedFile.sha256},
|
||||||
},
|
},
|
||||||
'info': {
|
'info': {
|
||||||
...file.info,
|
...file.info,
|
||||||
|
|
@ -864,16 +886,16 @@ class Room {
|
||||||
'ext': true,
|
'ext': true,
|
||||||
'k': encryptedThumbnail.k,
|
'k': encryptedThumbnail.k,
|
||||||
'key_ops': ['encrypt', 'decrypt'],
|
'key_ops': ['encrypt', 'decrypt'],
|
||||||
'kty': 'oct'
|
'kty': 'oct',
|
||||||
},
|
},
|
||||||
'iv': encryptedThumbnail.iv,
|
'iv': encryptedThumbnail.iv,
|
||||||
'hashes': {'sha256': encryptedThumbnail.sha256}
|
'hashes': {'sha256': encryptedThumbnail.sha256},
|
||||||
},
|
},
|
||||||
if (thumbnail != null) 'thumbnail_info': thumbnail.info,
|
if (thumbnail != null) 'thumbnail_info': thumbnail.info,
|
||||||
if (thumbnail?.blurhash != null &&
|
if (thumbnail?.blurhash != null &&
|
||||||
file is MatrixImageFile &&
|
file is MatrixImageFile &&
|
||||||
file.blurhash == null)
|
file.blurhash == null)
|
||||||
'xyz.amorgan.blurhash': thumbnail!.blurhash
|
'xyz.amorgan.blurhash': thumbnail!.blurhash,
|
||||||
},
|
},
|
||||||
if (extraContent != null) ...extraContent,
|
if (extraContent != null) ...extraContent,
|
||||||
};
|
};
|
||||||
|
|
@ -898,12 +920,16 @@ class Room {
|
||||||
/// encryption security level.
|
/// encryption security level.
|
||||||
Future<EncryptionHealthState> calcEncryptionHealthState() async {
|
Future<EncryptionHealthState> calcEncryptionHealthState() async {
|
||||||
final users = await requestParticipants();
|
final users = await requestParticipants();
|
||||||
users.removeWhere((u) =>
|
users.removeWhere(
|
||||||
!{Membership.invite, Membership.join}.contains(u.membership) ||
|
(u) =>
|
||||||
!client.userDeviceKeys.containsKey(u.id));
|
!{Membership.invite, Membership.join}.contains(u.membership) ||
|
||||||
|
!client.userDeviceKeys.containsKey(u.id),
|
||||||
|
);
|
||||||
|
|
||||||
if (users.any((u) =>
|
if (users.any(
|
||||||
client.userDeviceKeys[u.id]!.verified != UserVerifiedStatus.verified)) {
|
(u) =>
|
||||||
|
client.userDeviceKeys[u.id]!.verified != UserVerifiedStatus.verified,
|
||||||
|
)) {
|
||||||
return EncryptionHealthState.unverifiedDevices;
|
return EncryptionHealthState.unverifiedDevices;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -983,9 +1009,14 @@ class Room {
|
||||||
? inReplyTo.formattedText
|
? inReplyTo.formattedText
|
||||||
: htmlEscape.convert(inReplyTo.body).replaceAll('\n', '<br>'))
|
: htmlEscape.convert(inReplyTo.body).replaceAll('\n', '<br>'))
|
||||||
.replaceAll(
|
.replaceAll(
|
||||||
RegExp(r'<mx-reply>.*</mx-reply>',
|
RegExp(
|
||||||
caseSensitive: false, multiLine: false, dotAll: true),
|
r'<mx-reply>.*</mx-reply>',
|
||||||
'');
|
caseSensitive: false,
|
||||||
|
multiLine: false,
|
||||||
|
dotAll: true,
|
||||||
|
),
|
||||||
|
'',
|
||||||
|
);
|
||||||
final repliedHtml = content.tryGet<String>('formatted_body') ??
|
final repliedHtml = content.tryGet<String>('formatted_body') ??
|
||||||
htmlEscape
|
htmlEscape
|
||||||
.convert(content.tryGet<String>('body') ?? '')
|
.convert(content.tryGet<String>('body') ?? '')
|
||||||
|
|
@ -1017,7 +1048,7 @@ class Room {
|
||||||
'm.in_reply_to': {
|
'm.in_reply_to': {
|
||||||
'event_id': threadLastEventId,
|
'event_id': threadLastEventId,
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1085,7 +1116,8 @@ class Room {
|
||||||
.add(Duration(milliseconds: e.retryAfterMs!))
|
.add(Duration(milliseconds: e.retryAfterMs!))
|
||||||
.isAfter(timeoutDate)) {
|
.isAfter(timeoutDate)) {
|
||||||
Logs().w(
|
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!));
|
await Future.delayed(Duration(milliseconds: e.retryAfterMs!));
|
||||||
} else if (e is MatrixException ||
|
} else if (e is MatrixException ||
|
||||||
e is EventTooLarge ||
|
e is EventTooLarge ||
|
||||||
|
|
@ -1202,7 +1234,8 @@ class Room {
|
||||||
if (users is! Map<String, Object?>) {
|
if (users is! Map<String, Object?>) {
|
||||||
if (users != null) {
|
if (users != null) {
|
||||||
Logs().v(
|
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?>{};
|
users = powerLevelMapCopy['users'] = <String, Object?>{};
|
||||||
}
|
}
|
||||||
|
|
@ -1232,10 +1265,11 @@ class Room {
|
||||||
/// be received maximum. When the request is answered, [onHistoryReceived] will be triggered **before**
|
/// be received maximum. When the request is answered, [onHistoryReceived] will be triggered **before**
|
||||||
/// the historical events will be published in the onEvent stream.
|
/// the historical events will be published in the onEvent stream.
|
||||||
/// Returns the actual count of received timeline events.
|
/// Returns the actual count of received timeline events.
|
||||||
Future<int> requestHistory(
|
Future<int> requestHistory({
|
||||||
{int historyCount = defaultHistoryCount,
|
int historyCount = defaultHistoryCount,
|
||||||
void Function()? onHistoryReceived,
|
void Function()? onHistoryReceived,
|
||||||
direction = Direction.b}) async {
|
direction = Direction.b,
|
||||||
|
}) async {
|
||||||
final prev_batch = this.prev_batch;
|
final prev_batch = this.prev_batch;
|
||||||
|
|
||||||
final storeInDatabase = !isArchived;
|
final storeInDatabase = !isArchived;
|
||||||
|
|
@ -1258,43 +1292,43 @@ class Room {
|
||||||
if (!((resp.chunk.isNotEmpty) && resp.end != null)) return;
|
if (!((resp.chunk.isNotEmpty) && resp.end != null)) return;
|
||||||
|
|
||||||
await client.handleSync(
|
await client.handleSync(
|
||||||
SyncUpdate(
|
SyncUpdate(
|
||||||
nextBatch: '',
|
nextBatch: '',
|
||||||
rooms: RoomsUpdate(
|
rooms: RoomsUpdate(
|
||||||
join: membership == Membership.join
|
join: membership == Membership.join
|
||||||
? {
|
? {
|
||||||
id: JoinedRoomUpdate(
|
id: JoinedRoomUpdate(
|
||||||
state: resp.state,
|
state: resp.state,
|
||||||
timeline: TimelineUpdate(
|
timeline: TimelineUpdate(
|
||||||
limited: false,
|
limited: false,
|
||||||
events: direction == Direction.b
|
events: direction == Direction.b
|
||||||
? resp.chunk
|
? resp.chunk
|
||||||
: resp.chunk.reversed.toList(),
|
: resp.chunk.reversed.toList(),
|
||||||
prevBatch: direction == Direction.b
|
prevBatch:
|
||||||
? resp.end
|
direction == Direction.b ? resp.end : resp.start,
|
||||||
: resp.start,
|
),
|
||||||
),
|
),
|
||||||
)
|
}
|
||||||
}
|
: null,
|
||||||
: null,
|
leave: membership != Membership.join
|
||||||
leave: membership != Membership.join
|
? {
|
||||||
? {
|
id: LeftRoomUpdate(
|
||||||
id: LeftRoomUpdate(
|
state: resp.state,
|
||||||
state: resp.state,
|
timeline: TimelineUpdate(
|
||||||
timeline: TimelineUpdate(
|
limited: false,
|
||||||
limited: false,
|
events: direction == Direction.b
|
||||||
events: direction == Direction.b
|
? resp.chunk
|
||||||
? resp.chunk
|
: resp.chunk.reversed.toList(),
|
||||||
: resp.chunk.reversed.toList(),
|
prevBatch:
|
||||||
prevBatch: direction == Direction.b
|
direction == Direction.b ? resp.end : resp.start,
|
||||||
? resp.end
|
),
|
||||||
: resp.start,
|
),
|
||||||
),
|
}
|
||||||
),
|
: null,
|
||||||
}
|
|
||||||
: null),
|
|
||||||
),
|
),
|
||||||
direction: Direction.b);
|
),
|
||||||
|
direction: Direction.b,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (client.database != null) {
|
if (client.database != null) {
|
||||||
|
|
@ -1368,8 +1402,11 @@ class Room {
|
||||||
/// read receipt's location.
|
/// 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.
|
/// 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.
|
/// 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,
|
Future<void> setReadMarker(
|
||||||
{String? mRead, bool? public}) async {
|
String? eventId, {
|
||||||
|
String? mRead,
|
||||||
|
bool? public,
|
||||||
|
}) async {
|
||||||
await client.setReadMarker(
|
await client.setReadMarker(
|
||||||
id,
|
id,
|
||||||
mFullyRead: eventId,
|
mFullyRead: eventId,
|
||||||
|
|
@ -1381,15 +1418,16 @@ class Room {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<TimelineChunk?> getEventContext(String eventId) async {
|
Future<TimelineChunk?> getEventContext(String eventId) async {
|
||||||
final resp = await client.getEventContext(id, eventId,
|
final resp = await client.getEventContext(
|
||||||
limit: Room.defaultHistoryCount
|
id, eventId,
|
||||||
// filter: jsonEncode(StateFilter(lazyLoadMembers: true).toJson()),
|
limit: Room.defaultHistoryCount,
|
||||||
);
|
// filter: jsonEncode(StateFilter(lazyLoadMembers: true).toJson()),
|
||||||
|
);
|
||||||
|
|
||||||
final events = [
|
final events = [
|
||||||
if (resp.eventsAfter != null) ...resp.eventsAfter!.reversed,
|
if (resp.eventsAfter != null) ...resp.eventsAfter!.reversed,
|
||||||
if (resp.event != null) resp.event!,
|
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();
|
].map((e) => Event.fromMatrixEvent(e, this)).toList();
|
||||||
|
|
||||||
// Try again to decrypt encrypted events but don't update the database.
|
// Try again to decrypt encrypted events but don't update the database.
|
||||||
|
|
@ -1406,7 +1444,10 @@ class Room {
|
||||||
}
|
}
|
||||||
|
|
||||||
final chunk = TimelineChunk(
|
final chunk = TimelineChunk(
|
||||||
nextBatch: resp.end ?? '', prevBatch: resp.start ?? '', events: events);
|
nextBatch: resp.end ?? '',
|
||||||
|
prevBatch: resp.start ?? '',
|
||||||
|
events: events,
|
||||||
|
);
|
||||||
|
|
||||||
return chunk;
|
return chunk;
|
||||||
}
|
}
|
||||||
|
|
@ -1415,9 +1456,12 @@ class Room {
|
||||||
/// specified. In general you want to use `setReadMarker` instead to set private
|
/// specified. In general you want to use `setReadMarker` instead to set private
|
||||||
/// and public receipt as well as the marker at the same time.
|
/// and public receipt as well as the marker at the same time.
|
||||||
@Deprecated(
|
@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.')
|
'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 {
|
Future<void> postReceipt(
|
||||||
|
String eventId, {
|
||||||
|
ReceiptType type = ReceiptType.mRead,
|
||||||
|
}) async {
|
||||||
await client.postReceipt(
|
await client.postReceipt(
|
||||||
id,
|
id,
|
||||||
ReceiptType.mRead,
|
ReceiptType.mRead,
|
||||||
|
|
@ -1435,13 +1479,14 @@ class Room {
|
||||||
/// [onChange], [onRemove], [onInsert] and the [onHistoryReceived] callbacks.
|
/// [onChange], [onRemove], [onInsert] and the [onHistoryReceived] callbacks.
|
||||||
/// This method can also retrieve the timeline at a specific point by setting
|
/// This method can also retrieve the timeline at a specific point by setting
|
||||||
/// the [eventContextId]
|
/// the [eventContextId]
|
||||||
Future<Timeline> getTimeline(
|
Future<Timeline> getTimeline({
|
||||||
{void Function(int index)? onChange,
|
void Function(int index)? onChange,
|
||||||
void Function(int index)? onRemove,
|
void Function(int index)? onRemove,
|
||||||
void Function(int insertID)? onInsert,
|
void Function(int insertID)? onInsert,
|
||||||
void Function()? onNewEvent,
|
void Function()? onNewEvent,
|
||||||
void Function()? onUpdate,
|
void Function()? onUpdate,
|
||||||
String? eventContextId}) async {
|
String? eventContextId,
|
||||||
|
}) async {
|
||||||
await postLoad();
|
await postLoad();
|
||||||
|
|
||||||
List<Event> events;
|
List<Event> events;
|
||||||
|
|
@ -1478,13 +1523,14 @@ class Room {
|
||||||
}
|
}
|
||||||
|
|
||||||
final timeline = Timeline(
|
final timeline = Timeline(
|
||||||
room: this,
|
room: this,
|
||||||
chunk: chunk,
|
chunk: chunk,
|
||||||
onChange: onChange,
|
onChange: onChange,
|
||||||
onRemove: onRemove,
|
onRemove: onRemove,
|
||||||
onInsert: onInsert,
|
onInsert: onInsert,
|
||||||
onNewEvent: onNewEvent,
|
onNewEvent: onNewEvent,
|
||||||
onUpdate: onUpdate);
|
onUpdate: onUpdate,
|
||||||
|
);
|
||||||
|
|
||||||
// Fetch all users from database we have got here.
|
// Fetch all users from database we have got here.
|
||||||
if (eventContextId == null) {
|
if (eventContextId == null) {
|
||||||
|
|
@ -1536,12 +1582,13 @@ class Room {
|
||||||
/// List `membershipFilter` defines with what membership do you want the
|
/// List `membershipFilter` defines with what membership do you want the
|
||||||
/// participants, default set to
|
/// participants, default set to
|
||||||
/// [[Membership.join, Membership.invite, Membership.knock]]
|
/// [[Membership.join, Membership.invite, Membership.knock]]
|
||||||
List<User> getParticipants(
|
List<User> getParticipants([
|
||||||
[List<Membership> membershipFilter = const [
|
List<Membership> membershipFilter = const [
|
||||||
Membership.join,
|
Membership.join,
|
||||||
Membership.invite,
|
Membership.invite,
|
||||||
Membership.knock,
|
Membership.knock,
|
||||||
]]) {
|
],
|
||||||
|
]) {
|
||||||
final members = states[EventTypes.RoomMember];
|
final members = states[EventTypes.RoomMember];
|
||||||
if (members != null) {
|
if (members != null) {
|
||||||
return members.entries
|
return members.entries
|
||||||
|
|
@ -1560,14 +1607,15 @@ class Room {
|
||||||
/// [[Membership.join, Membership.invite, Membership.knock]]
|
/// [[Membership.join, Membership.invite, Membership.knock]]
|
||||||
/// Set [cache] to `false` if you do not want to cache the users in memory
|
/// 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.
|
/// for this session which is highly recommended for large public rooms.
|
||||||
Future<List<User>> requestParticipants(
|
Future<List<User>> requestParticipants([
|
||||||
[List<Membership> membershipFilter = const [
|
List<Membership> membershipFilter = const [
|
||||||
Membership.join,
|
Membership.join,
|
||||||
Membership.invite,
|
Membership.invite,
|
||||||
Membership.knock,
|
Membership.knock,
|
||||||
],
|
],
|
||||||
bool suppressWarning = false,
|
bool suppressWarning = false,
|
||||||
bool cache = true]) async {
|
bool cache = true,
|
||||||
|
]) async {
|
||||||
if (!participantListComplete || partial) {
|
if (!participantListComplete || partial) {
|
||||||
// we aren't fully loaded, maybe the users are in the database
|
// 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
|
// We always need to check the database in the partial case, since state
|
||||||
|
|
@ -1624,7 +1672,8 @@ class Room {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deprecated(
|
@Deprecated(
|
||||||
'The method was renamed unsafeGetUserFromMemoryOrFallback. Please prefer requestParticipants.')
|
'The method was renamed unsafeGetUserFromMemoryOrFallback. Please prefer requestParticipants.',
|
||||||
|
)
|
||||||
User getUserByMXIDSync(String mxID) {
|
User getUserByMXIDSync(String mxID) {
|
||||||
return unsafeGetUserFromMemoryOrFallback(mxID);
|
return unsafeGetUserFromMemoryOrFallback(mxID);
|
||||||
}
|
}
|
||||||
|
|
@ -1801,12 +1850,14 @@ class Room {
|
||||||
final cache = _inflightUserRequests[parameters] ??= AsyncCache.ephemeral();
|
final cache = _inflightUserRequests[parameters] ??= AsyncCache.ephemeral();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final user = await cache.fetch(() => _requestUser(
|
final user = await cache.fetch(
|
||||||
mxID,
|
() => _requestUser(
|
||||||
ignoreErrors: ignoreErrors,
|
mxID,
|
||||||
requestState: requestState,
|
ignoreErrors: ignoreErrors,
|
||||||
requestProfile: requestProfile,
|
requestState: requestState,
|
||||||
));
|
requestProfile: requestProfile,
|
||||||
|
),
|
||||||
|
);
|
||||||
_inflightUserRequests.remove(parameters);
|
_inflightUserRequests.remove(parameters);
|
||||||
return user;
|
return user;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
|
|
@ -1937,7 +1988,7 @@ class Room {
|
||||||
final eventsMap = newPowerLevelMap.tryGetMap<String, Object?>('events') ??
|
final eventsMap = newPowerLevelMap.tryGetMap<String, Object?>('events') ??
|
||||||
<String, Object?>{};
|
<String, Object?>{};
|
||||||
eventsMap.addAll({
|
eventsMap.addAll({
|
||||||
EventTypes.GroupCallMember: getDefaultPowerLevel(currentPowerLevelsMap)
|
EventTypes.GroupCallMember: getDefaultPowerLevel(currentPowerLevelsMap),
|
||||||
});
|
});
|
||||||
newPowerLevelMap.addAll({'events': eventsMap});
|
newPowerLevelMap.addAll({'events': eventsMap});
|
||||||
await client.setRoomStateWithKey(
|
await client.setRoomStateWithKey(
|
||||||
|
|
@ -2099,7 +2150,7 @@ class Room {
|
||||||
id,
|
id,
|
||||||
[PushRuleAction.dontNotify],
|
[PushRuleAction.dontNotify],
|
||||||
conditions: [
|
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.
|
/// Redacts this event. Throws `ErrorResponse` on error.
|
||||||
Future<String?> redactEvent(String eventId,
|
Future<String?> redactEvent(
|
||||||
{String? reason, String? txid}) async {
|
String eventId, {
|
||||||
|
String? reason,
|
||||||
|
String? txid,
|
||||||
|
}) async {
|
||||||
// Create new transaction id
|
// Create new transaction id
|
||||||
String messageID;
|
String messageID;
|
||||||
final now = DateTime.now().millisecondsSinceEpoch;
|
final now = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
|
@ -2167,7 +2221,8 @@ class Room {
|
||||||
?.content
|
?.content
|
||||||
.tryGet<String>('guest_access');
|
.tryGet<String>('guest_access');
|
||||||
return GuestAccess.values.singleWhereOrNull(
|
return GuestAccess.values.singleWhereOrNull(
|
||||||
(element) => element.text == guestAccessString) ??
|
(element) => element.text == guestAccessString,
|
||||||
|
) ??
|
||||||
GuestAccess.forbidden;
|
GuestAccess.forbidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2193,7 +2248,8 @@ class Room {
|
||||||
?.content
|
?.content
|
||||||
.tryGet<String>('history_visibility');
|
.tryGet<String>('history_visibility');
|
||||||
return HistoryVisibility.values.singleWhereOrNull(
|
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.
|
/// 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);
|
await client.encryption?.keyManager.request(this, sessionId, senderKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _handleFakeSync(SyncUpdate syncUpdate,
|
Future<void> _handleFakeSync(
|
||||||
{Direction? direction}) async {
|
SyncUpdate syncUpdate, {
|
||||||
|
Direction? direction,
|
||||||
|
}) async {
|
||||||
if (client.database != null) {
|
if (client.database != null) {
|
||||||
await client.database?.transaction(() async {
|
await client.database?.transaction(() async {
|
||||||
await client.handleSync(syncUpdate, direction: direction);
|
await client.handleSync(syncUpdate, direction: direction);
|
||||||
|
|
@ -2309,9 +2367,11 @@ class Room {
|
||||||
.where((child) => child.via.isNotEmpty)
|
.where((child) => child.via.isNotEmpty)
|
||||||
.toList() ??
|
.toList() ??
|
||||||
[])
|
[])
|
||||||
..sort((a, b) => a.order.isEmpty || b.order.isEmpty
|
..sort(
|
||||||
? b.order.compareTo(a.order)
|
(a, b) => a.order.isEmpty || b.order.isEmpty
|
||||||
: a.order.compareTo(b.order));
|
? b.order.compareTo(a.order)
|
||||||
|
: a.order.compareTo(b.order),
|
||||||
|
);
|
||||||
|
|
||||||
/// Adds or edits a child of this space.
|
/// Adds or edits a child of this space.
|
||||||
Future<void> setSpaceChild(
|
Future<void> setSpaceChild(
|
||||||
|
|
@ -2337,7 +2397,8 @@ class Room {
|
||||||
Future<Uri> matrixToInviteLink() async {
|
Future<Uri> matrixToInviteLink() async {
|
||||||
if (canonicalAlias.isNotEmpty) {
|
if (canonicalAlias.isNotEmpty) {
|
||||||
return Uri.parse(
|
return Uri.parse(
|
||||||
'https://matrix.to/#/${Uri.encodeComponent(canonicalAlias)}');
|
'https://matrix.to/#/${Uri.encodeComponent(canonicalAlias)}',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
final List queryParameters = [];
|
final List queryParameters = [];
|
||||||
final users = await requestParticipants([Membership.join]);
|
final users = await requestParticipants([Membership.join]);
|
||||||
|
|
@ -2347,8 +2408,9 @@ class Room {
|
||||||
temp.removeWhere((user) => user.powerLevel < 50);
|
temp.removeWhere((user) => user.powerLevel < 50);
|
||||||
if (currentPowerLevelsMap != null) {
|
if (currentPowerLevelsMap != null) {
|
||||||
// just for weird rooms
|
// just for weird rooms
|
||||||
temp.removeWhere((user) =>
|
temp.removeWhere(
|
||||||
user.powerLevel < getDefaultPowerLevel(currentPowerLevelsMap));
|
(user) => user.powerLevel < getDefaultPowerLevel(currentPowerLevelsMap),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (temp.isNotEmpty) {
|
if (temp.isNotEmpty) {
|
||||||
|
|
@ -2368,10 +2430,9 @@ class Room {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final sortedServers = Map.fromEntries(servers.entries.toList()
|
final sortedServers = Map.fromEntries(
|
||||||
..sort((e1, e2) => e2.value.compareTo(e1.value)))
|
servers.entries.toList()..sort((e1, e2) => e2.value.compareTo(e1.value)),
|
||||||
.keys
|
).keys.take(3);
|
||||||
.take(3);
|
|
||||||
for (final server in sortedServers) {
|
for (final server in sortedServers) {
|
||||||
if (!queryParameters.contains(server)) {
|
if (!queryParameters.contains(server)) {
|
||||||
queryParameters.add(server);
|
queryParameters.add(server);
|
||||||
|
|
@ -2386,7 +2447,8 @@ class Room {
|
||||||
queryString += 'via=${queryParameters[i]}';
|
queryString += 'via=${queryParameters[i]}';
|
||||||
}
|
}
|
||||||
return Uri.parse(
|
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.
|
/// Remove a child from this space by setting the `via` to an empty list.
|
||||||
|
|
|
||||||
|
|
@ -84,8 +84,9 @@ class Timeline {
|
||||||
(room.prev_batch != null && events.last.type != EventTypes.RoomCreate);
|
(room.prev_batch != null && events.last.type != EventTypes.RoomCreate);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> requestHistory(
|
Future<void> requestHistory({
|
||||||
{int historyCount = Room.defaultHistoryCount}) async {
|
int historyCount = Room.defaultHistoryCount,
|
||||||
|
}) async {
|
||||||
if (isRequestingHistory) {
|
if (isRequestingHistory) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -97,8 +98,9 @@ class Timeline {
|
||||||
|
|
||||||
bool get canRequestFuture => !allowNewEvent;
|
bool get canRequestFuture => !allowNewEvent;
|
||||||
|
|
||||||
Future<void> requestFuture(
|
Future<void> requestFuture({
|
||||||
{int historyCount = Room.defaultHistoryCount}) async {
|
int historyCount = Room.defaultHistoryCount,
|
||||||
|
}) async {
|
||||||
if (allowNewEvent) {
|
if (allowNewEvent) {
|
||||||
return; // we shouldn't force to add new events if they will autatically be added
|
return; // we shouldn't force to add new events if they will autatically be added
|
||||||
}
|
}
|
||||||
|
|
@ -109,9 +111,10 @@ class Timeline {
|
||||||
isRequestingFuture = false;
|
isRequestingFuture = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _requestEvents(
|
Future<void> _requestEvents({
|
||||||
{int historyCount = Room.defaultHistoryCount,
|
int historyCount = Room.defaultHistoryCount,
|
||||||
required Direction direction}) async {
|
required Direction direction,
|
||||||
|
}) async {
|
||||||
onUpdate?.call();
|
onUpdate?.call();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -184,9 +187,10 @@ class Timeline {
|
||||||
/// be received maximum. When the request is answered, [onHistoryReceived] will be triggered **before**
|
/// be received maximum. When the request is answered, [onHistoryReceived] will be triggered **before**
|
||||||
/// the historical events will be published in the onEvent stream.
|
/// the historical events will be published in the onEvent stream.
|
||||||
/// Returns the actual count of received timeline events.
|
/// Returns the actual count of received timeline events.
|
||||||
Future<int> getRoomEvents(
|
Future<int> getRoomEvents({
|
||||||
{int historyCount = Room.defaultHistoryCount,
|
int historyCount = Room.defaultHistoryCount,
|
||||||
direction = Direction.b}) async {
|
direction = Direction.b,
|
||||||
|
}) async {
|
||||||
final resp = await room.client.getRoomEvents(
|
final resp = await room.client.getRoomEvents(
|
||||||
room.id,
|
room.id,
|
||||||
direction,
|
direction,
|
||||||
|
|
@ -212,10 +216,12 @@ class Timeline {
|
||||||
newNextBatch != null) {
|
newNextBatch != null) {
|
||||||
if (type == EventUpdateType.history) {
|
if (type == EventUpdateType.history) {
|
||||||
Logs().w(
|
Logs().w(
|
||||||
'[nav] we can still request history prevBatch: $type $newPrevBatch');
|
'[nav] we can still request history prevBatch: $type $newPrevBatch',
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
Logs().w(
|
Logs().w(
|
||||||
'[nav] we can still request timeline nextBatch: $type $newNextBatch');
|
'[nav] we can still request timeline nextBatch: $type $newNextBatch',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -229,8 +235,9 @@ class Timeline {
|
||||||
if (allowNewEvent) {
|
if (allowNewEvent) {
|
||||||
Logs().d('We now allow sync update into the timeline.');
|
Logs().d('We now allow sync update into the timeline.');
|
||||||
newEvents.addAll(
|
newEvents.addAll(
|
||||||
await room.client.database?.getEventList(room, onlySending: true) ??
|
await room.client.database?.getEventList(room, onlySending: true) ??
|
||||||
[]);
|
[],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -272,14 +279,15 @@ class Timeline {
|
||||||
return resp.chunk.length;
|
return resp.chunk.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
Timeline(
|
Timeline({
|
||||||
{required this.room,
|
required this.room,
|
||||||
this.onUpdate,
|
this.onUpdate,
|
||||||
this.onChange,
|
this.onChange,
|
||||||
this.onInsert,
|
this.onInsert,
|
||||||
this.onRemove,
|
this.onRemove,
|
||||||
this.onNewEvent,
|
this.onNewEvent,
|
||||||
required this.chunk}) {
|
required this.chunk,
|
||||||
|
}) {
|
||||||
sub = room.client.onEvent.stream.listen(_handleEventUpdate);
|
sub = room.client.onEvent.stream.listen(_handleEventUpdate);
|
||||||
|
|
||||||
// If the timeline is limited we want to clear our events cache
|
// 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) {
|
void _removeEventFromSet(Set<Event> eventSet, Event event) {
|
||||||
eventSet.removeWhere((e) =>
|
eventSet.removeWhere(
|
||||||
e.matchesEventOrTransactionId(event.eventId) ||
|
(e) =>
|
||||||
event.unsigned != null &&
|
e.matchesEventOrTransactionId(event.eventId) ||
|
||||||
e.matchesEventOrTransactionId(
|
event.unsigned != null &&
|
||||||
event.unsigned?.tryGet<String>('transaction_id')));
|
e.matchesEventOrTransactionId(
|
||||||
|
event.unsigned?.tryGet<String>('transaction_id'),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void addAggregatedEvent(Event event) {
|
void addAggregatedEvent(Event event) {
|
||||||
|
|
@ -483,17 +494,20 @@ class Timeline {
|
||||||
|
|
||||||
if (!allowNewEvent) return;
|
if (!allowNewEvent) return;
|
||||||
|
|
||||||
final status = eventStatusFromInt(eventUpdate.content['status'] ??
|
final status = eventStatusFromInt(
|
||||||
(eventUpdate.content['unsigned'] is Map<String, dynamic>
|
eventUpdate.content['status'] ??
|
||||||
? eventUpdate.content['unsigned'][messageSendingStatusKey]
|
(eventUpdate.content['unsigned'] is Map<String, dynamic>
|
||||||
: null) ??
|
? eventUpdate.content['unsigned'][messageSendingStatusKey]
|
||||||
EventStatus.synced.intValue);
|
: null) ??
|
||||||
|
EventStatus.synced.intValue,
|
||||||
|
);
|
||||||
|
|
||||||
final i = _findEvent(
|
final i = _findEvent(
|
||||||
event_id: eventUpdate.content['event_id'],
|
event_id: eventUpdate.content['event_id'],
|
||||||
unsigned_txid: eventUpdate.content['unsigned'] is Map
|
unsigned_txid: eventUpdate.content['unsigned'] is Map
|
||||||
? eventUpdate.content['unsigned']['transaction_id']
|
? eventUpdate.content['unsigned']['transaction_id']
|
||||||
: null);
|
: null,
|
||||||
|
);
|
||||||
|
|
||||||
if (i < events.length) {
|
if (i < events.length) {
|
||||||
// if the old status is larger than the new one, we also want to preserve the old status
|
// 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 &&
|
if (eventUpdate.type == EventUpdateType.history &&
|
||||||
events.indexWhere(
|
events.indexWhere(
|
||||||
(e) => e.eventId == eventUpdate.content['event_id']) !=
|
(e) => e.eventId == eventUpdate.content['event_id'],
|
||||||
|
) !=
|
||||||
-1) return;
|
-1) return;
|
||||||
var index = events.length;
|
var index = events.length;
|
||||||
if (eventUpdate.type == EventUpdateType.history) {
|
if (eventUpdate.type == EventUpdateType.history) {
|
||||||
|
|
@ -548,10 +563,12 @@ class Timeline {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
events[index].setRedactionEvent(Event.fromJson(
|
events[index].setRedactionEvent(
|
||||||
eventUpdate.content,
|
Event.fromJson(
|
||||||
room,
|
eventUpdate.content,
|
||||||
));
|
room,
|
||||||
|
),
|
||||||
|
);
|
||||||
onChange?.call(index);
|
onChange?.call(index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -72,12 +72,15 @@ class User extends StrippedStateEvent {
|
||||||
/// invite
|
/// invite
|
||||||
/// leave
|
/// leave
|
||||||
/// ban
|
/// ban
|
||||||
Membership get membership => Membership.values.firstWhere((e) {
|
Membership get membership => Membership.values.firstWhere(
|
||||||
if (content['membership'] != null) {
|
(e) {
|
||||||
return e.toString() == 'Membership.${content['membership']}';
|
if (content['membership'] != null) {
|
||||||
}
|
return e.toString() == 'Membership.${content['membership']}';
|
||||||
return false;
|
}
|
||||||
}, orElse: () => Membership.join);
|
return false;
|
||||||
|
},
|
||||||
|
orElse: () => Membership.join,
|
||||||
|
);
|
||||||
|
|
||||||
/// The avatar if the user has one.
|
/// The avatar if the user has one.
|
||||||
Uri? get avatarUrl {
|
Uri? get avatarUrl {
|
||||||
|
|
@ -94,10 +97,11 @@ class User extends StrippedStateEvent {
|
||||||
/// the first character of each word becomes uppercase.
|
/// the first character of each word becomes uppercase.
|
||||||
/// If [mxidLocalPartFallback] is true, then the local part of the mxid will be shown
|
/// 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".
|
/// if there is no other displayname available. If not then this will return "Unknown user".
|
||||||
String calcDisplayname(
|
String calcDisplayname({
|
||||||
{bool? formatLocalpart,
|
bool? formatLocalpart,
|
||||||
bool? mxidLocalPartFallback,
|
bool? mxidLocalPartFallback,
|
||||||
MatrixLocalizations i18n = const MatrixDefaultLocalizations()}) {
|
MatrixLocalizations i18n = const MatrixDefaultLocalizations(),
|
||||||
|
}) {
|
||||||
formatLocalpart ??= room.client.formatLocalpart;
|
formatLocalpart ??= room.client.formatLocalpart;
|
||||||
mxidLocalPartFallback ??= room.client.mxidLocalPartFallback;
|
mxidLocalPartFallback ??= room.client.mxidLocalPartFallback;
|
||||||
final displayName = this.displayName;
|
final displayName = this.displayName;
|
||||||
|
|
@ -203,10 +207,12 @@ class User extends StrippedStateEvent {
|
||||||
|
|
||||||
// get all the users with the same display name
|
// get all the users with the same display name
|
||||||
final allUsersWithSameDisplayname = room.getParticipants();
|
final allUsersWithSameDisplayname = room.getParticipants();
|
||||||
allUsersWithSameDisplayname.removeWhere((user) =>
|
allUsersWithSameDisplayname.removeWhere(
|
||||||
user.id == id ||
|
(user) =>
|
||||||
(user.displayName?.isEmpty ?? true) ||
|
user.id == id ||
|
||||||
user.displayName != displayName);
|
(user.displayName?.isEmpty ?? true) ||
|
||||||
|
user.displayName != displayName,
|
||||||
|
);
|
||||||
if (allUsersWithSameDisplayname.isEmpty) {
|
if (allUsersWithSameDisplayname.isEmpty) {
|
||||||
return identifier;
|
return identifier;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,9 @@ extension CommandsClientExtension on Client {
|
||||||
/// Add a command to the command handler. `command` is its name, and `callback` is the
|
/// Add a command to the command handler. `command` is its name, and `callback` is the
|
||||||
/// callback to invoke
|
/// callback to invoke
|
||||||
void addCommand(
|
void addCommand(
|
||||||
String command, FutureOr<String?> Function(CommandArgs) callback) {
|
String command,
|
||||||
|
FutureOr<String?> Function(CommandArgs) callback,
|
||||||
|
) {
|
||||||
commands[command.toLowerCase()] = callback;
|
commands[command.toLowerCase()] = callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -313,12 +315,13 @@ class CommandArgs {
|
||||||
String? threadRootEventId;
|
String? threadRootEventId;
|
||||||
String? threadLastEventId;
|
String? threadLastEventId;
|
||||||
|
|
||||||
CommandArgs(
|
CommandArgs({
|
||||||
{required this.msg,
|
required this.msg,
|
||||||
this.editEventId,
|
this.editEventId,
|
||||||
this.inReplyTo,
|
this.inReplyTo,
|
||||||
required this.room,
|
required this.room,
|
||||||
this.txid,
|
this.txid,
|
||||||
this.threadRootEventId,
|
this.threadRootEventId,
|
||||||
this.threadLastEventId});
|
this.threadLastEventId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,22 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
typedef ComputeCallback = Future<R> Function<Q, R>(
|
typedef ComputeCallback = Future<R> Function<Q, R>(
|
||||||
FutureOr<R> Function(Q message) callback, Q message,
|
FutureOr<R> Function(Q message) callback,
|
||||||
{String? debugLabel});
|
Q message, {
|
||||||
|
String? debugLabel,
|
||||||
|
});
|
||||||
|
|
||||||
// keep types in sync with [computeCallbackFromRunInBackground]
|
// keep types in sync with [computeCallbackFromRunInBackground]
|
||||||
typedef ComputeRunner = Future<T> Function<T, U>(
|
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) {
|
ComputeCallback computeCallbackFromRunInBackground(ComputeRunner runner) {
|
||||||
return <U, T>(FutureOr<T> Function(U arg) callback, U arg,
|
return <U, T>(
|
||||||
{String? debugLabel}) =>
|
FutureOr<T> Function(U arg) callback,
|
||||||
|
U arg, {
|
||||||
|
String? debugLabel,
|
||||||
|
}) =>
|
||||||
runner.call(callback, arg);
|
runner.call(callback, arg);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,23 +43,25 @@ final libcrypto = () {
|
||||||
|
|
||||||
final PKCS5_PBKDF2_HMAC = libcrypto.lookupFunction<
|
final PKCS5_PBKDF2_HMAC = libcrypto.lookupFunction<
|
||||||
IntPtr Function(
|
IntPtr Function(
|
||||||
Pointer<Uint8> pass,
|
Pointer<Uint8> pass,
|
||||||
IntPtr passlen,
|
IntPtr passlen,
|
||||||
Pointer<Uint8> salt,
|
Pointer<Uint8> salt,
|
||||||
IntPtr saltlen,
|
IntPtr saltlen,
|
||||||
IntPtr iter,
|
IntPtr iter,
|
||||||
Pointer<NativeType> digest,
|
Pointer<NativeType> digest,
|
||||||
IntPtr keylen,
|
IntPtr keylen,
|
||||||
Pointer<Uint8> out),
|
Pointer<Uint8> out,
|
||||||
|
),
|
||||||
int Function(
|
int Function(
|
||||||
Pointer<Uint8> pass,
|
Pointer<Uint8> pass,
|
||||||
int passlen,
|
int passlen,
|
||||||
Pointer<Uint8> salt,
|
Pointer<Uint8> salt,
|
||||||
int saltlen,
|
int saltlen,
|
||||||
int iter,
|
int iter,
|
||||||
Pointer<NativeType> digest,
|
Pointer<NativeType> digest,
|
||||||
int keylen,
|
int keylen,
|
||||||
Pointer<Uint8> out)>('PKCS5_PBKDF2_HMAC');
|
Pointer<Uint8> out,
|
||||||
|
)>('PKCS5_PBKDF2_HMAC');
|
||||||
|
|
||||||
final EVP_sha1 = libcrypto.lookupFunction<Pointer<NativeType> Function(),
|
final EVP_sha1 = libcrypto.lookupFunction<Pointer<NativeType> Function(),
|
||||||
Pointer<NativeType> Function()>('EVP_sha1');
|
Pointer<NativeType> Function()>('EVP_sha1');
|
||||||
|
|
@ -82,54 +84,71 @@ final EVP_CIPHER_CTX_new = libcrypto.lookupFunction<
|
||||||
|
|
||||||
final EVP_EncryptInit_ex = libcrypto.lookupFunction<
|
final EVP_EncryptInit_ex = libcrypto.lookupFunction<
|
||||||
Pointer<NativeType> Function(
|
Pointer<NativeType> Function(
|
||||||
Pointer<NativeType> ctx,
|
Pointer<NativeType> ctx,
|
||||||
Pointer<NativeType> alg,
|
Pointer<NativeType> alg,
|
||||||
Pointer<NativeType> some,
|
Pointer<NativeType> some,
|
||||||
Pointer<Uint8> key,
|
Pointer<Uint8> key,
|
||||||
Pointer<Uint8> iv),
|
Pointer<Uint8> iv,
|
||||||
|
),
|
||||||
Pointer<NativeType> Function(
|
Pointer<NativeType> Function(
|
||||||
Pointer<NativeType> ctx,
|
Pointer<NativeType> ctx,
|
||||||
Pointer<NativeType> alg,
|
Pointer<NativeType> alg,
|
||||||
Pointer<NativeType> some,
|
Pointer<NativeType> some,
|
||||||
Pointer<Uint8> key,
|
Pointer<Uint8> key,
|
||||||
Pointer<Uint8> iv)>('EVP_EncryptInit_ex');
|
Pointer<Uint8> iv,
|
||||||
|
)>('EVP_EncryptInit_ex');
|
||||||
|
|
||||||
final EVP_EncryptUpdate = libcrypto.lookupFunction<
|
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> Function(
|
||||||
Pointer<NativeType> ctx,
|
Pointer<NativeType> ctx,
|
||||||
Pointer<Uint8> output,
|
Pointer<Uint8> output,
|
||||||
Pointer<IntPtr> outputLen,
|
Pointer<IntPtr> outputLen,
|
||||||
Pointer<Uint8> input,
|
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<
|
final EVP_EncryptFinal_ex = libcrypto.lookupFunction<
|
||||||
Pointer<NativeType> Function(
|
Pointer<NativeType> Function(
|
||||||
Pointer<NativeType> ctx, Pointer<Uint8> data, Pointer<IntPtr> len),
|
Pointer<NativeType> ctx,
|
||||||
Pointer<NativeType> Function(Pointer<NativeType> ctx, Pointer<Uint8> data,
|
Pointer<Uint8> data,
|
||||||
Pointer<IntPtr> len)>('EVP_EncryptFinal_ex');
|
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<
|
final EVP_CIPHER_CTX_free = libcrypto.lookupFunction<
|
||||||
Pointer<NativeType> Function(Pointer<NativeType> ctx),
|
Pointer<NativeType> Function(Pointer<NativeType> ctx),
|
||||||
Pointer<NativeType> Function(
|
Pointer<NativeType> Function(
|
||||||
Pointer<NativeType> ctx)>('EVP_CIPHER_CTX_free');
|
Pointer<NativeType> ctx,
|
||||||
|
)>('EVP_CIPHER_CTX_free');
|
||||||
|
|
||||||
final EVP_Digest = libcrypto.lookupFunction<
|
final EVP_Digest = libcrypto.lookupFunction<
|
||||||
IntPtr Function(
|
IntPtr Function(
|
||||||
Pointer<Uint8> data,
|
Pointer<Uint8> data,
|
||||||
IntPtr len,
|
IntPtr len,
|
||||||
Pointer<Uint8> hash,
|
Pointer<Uint8> hash,
|
||||||
Pointer<IntPtr> hsize,
|
Pointer<IntPtr> hsize,
|
||||||
Pointer<NativeType> alg,
|
Pointer<NativeType> alg,
|
||||||
Pointer<NativeType> engine),
|
Pointer<NativeType> engine,
|
||||||
|
),
|
||||||
int Function(
|
int Function(
|
||||||
Pointer<Uint8> data,
|
Pointer<Uint8> data,
|
||||||
int len,
|
int len,
|
||||||
Pointer<Uint8> hash,
|
Pointer<Uint8> hash,
|
||||||
Pointer<IntPtr> hsize,
|
Pointer<IntPtr> hsize,
|
||||||
Pointer<NativeType> alg,
|
Pointer<NativeType> alg,
|
||||||
Pointer<NativeType> engine)>('EVP_Digest');
|
Pointer<NativeType> engine,
|
||||||
|
)>('EVP_Digest');
|
||||||
|
|
||||||
final EVP_MD_size = () {
|
final EVP_MD_size = () {
|
||||||
// EVP_MD_size was renamed to EVP_MD_get_size in Openssl3.0.
|
// EVP_MD_size was renamed to EVP_MD_get_size in Openssl3.0.
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,10 @@ abstract class Cipher {
|
||||||
String name;
|
String name;
|
||||||
Object params(Uint8List iv);
|
Object params(Uint8List iv);
|
||||||
Future<Uint8List> encrypt(
|
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']);
|
final subtleKey = await importKey('raw', key, name, false, ['encrypt']);
|
||||||
return (await subtle.encrypt(params(iv), subtleKey, input)).asUint8List();
|
return (await subtle.encrypt(params(iv), subtleKey, input)).asUint8List();
|
||||||
}
|
}
|
||||||
|
|
@ -51,14 +54,24 @@ class _AesCtr extends Cipher {
|
||||||
AesCtrParams(name: name, counter: iv, length: 64);
|
AesCtrParams(name: name, counter: iv, length: 64);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Uint8List> pbkdf2(Uint8List passphrase, Uint8List salt, Hash hash,
|
Future<Uint8List> pbkdf2(
|
||||||
int iterations, int bits) async {
|
Uint8List passphrase,
|
||||||
|
Uint8List salt,
|
||||||
|
Hash hash,
|
||||||
|
int iterations,
|
||||||
|
int bits,
|
||||||
|
) async {
|
||||||
final raw =
|
final raw =
|
||||||
await importKey('raw', passphrase, 'PBKDF2', false, ['deriveBits']);
|
await importKey('raw', passphrase, 'PBKDF2', false, ['deriveBits']);
|
||||||
final res = await deriveBits(
|
final res = await deriveBits(
|
||||||
Pbkdf2Params(
|
Pbkdf2Params(
|
||||||
name: 'PBKDF2', hash: hash.name, salt: salt, iterations: iterations),
|
name: 'PBKDF2',
|
||||||
raw,
|
hash: hash.name,
|
||||||
bits);
|
salt: salt,
|
||||||
|
iterations: iterations,
|
||||||
|
),
|
||||||
|
raw,
|
||||||
|
bits,
|
||||||
|
);
|
||||||
return Uint8List.view(res);
|
return Uint8List.view(res);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,12 @@ class _AesCtr extends Cipher {
|
||||||
}
|
}
|
||||||
|
|
||||||
FutureOr<Uint8List> pbkdf2(
|
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 outLen = bits ~/ 8;
|
||||||
final mem = malloc.call<Uint8>(passphrase.length + salt.length + outLen);
|
final mem = malloc.call<Uint8>(passphrase.length + salt.length + outLen);
|
||||||
final saltMem = mem.elementAt(passphrase.length);
|
final saltMem = mem.elementAt(passphrase.length);
|
||||||
|
|
@ -98,8 +103,16 @@ FutureOr<Uint8List> pbkdf2(
|
||||||
try {
|
try {
|
||||||
mem.asTypedList(passphrase.length).setAll(0, passphrase);
|
mem.asTypedList(passphrase.length).setAll(0, passphrase);
|
||||||
saltMem.asTypedList(salt.length).setAll(0, salt);
|
saltMem.asTypedList(salt.length).setAll(0, salt);
|
||||||
PKCS5_PBKDF2_HMAC(mem, passphrase.length, saltMem, salt.length, iterations,
|
PKCS5_PBKDF2_HMAC(
|
||||||
hash.ptr, outLen, outMem);
|
mem,
|
||||||
|
passphrase.length,
|
||||||
|
saltMem,
|
||||||
|
salt.length,
|
||||||
|
iterations,
|
||||||
|
hash.ptr,
|
||||||
|
outLen,
|
||||||
|
outMem,
|
||||||
|
);
|
||||||
return Uint8List.fromList(outMem.asTypedList(outLen));
|
return Uint8List.fromList(outMem.asTypedList(outLen));
|
||||||
} finally {
|
} finally {
|
||||||
malloc.free(mem);
|
malloc.free(mem);
|
||||||
|
|
|
||||||
|
|
@ -50,13 +50,24 @@ Future<ByteBuffer> decrypt(dynamic algorithm, dynamic key, Uint8List data) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@JS('crypto.subtle.importKey')
|
@JS('crypto.subtle.importKey')
|
||||||
external dynamic _importKey(String format, dynamic keyData, dynamic algorithm,
|
external dynamic _importKey(
|
||||||
bool extractable, List<String> keyUsages);
|
String format,
|
||||||
|
dynamic keyData,
|
||||||
|
dynamic algorithm,
|
||||||
|
bool extractable,
|
||||||
|
List<String> keyUsages,
|
||||||
|
);
|
||||||
|
|
||||||
Future<dynamic> importKey(String format, dynamic keyData, dynamic algorithm,
|
Future<dynamic> importKey(
|
||||||
bool extractable, List<String> keyUsages) {
|
String format,
|
||||||
|
dynamic keyData,
|
||||||
|
dynamic algorithm,
|
||||||
|
bool extractable,
|
||||||
|
List<String> keyUsages,
|
||||||
|
) {
|
||||||
return promiseToFuture(
|
return promiseToFuture(
|
||||||
_importKey(format, keyData, algorithm, extractable, keyUsages));
|
_importKey(format, keyData, algorithm, extractable, keyUsages),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@JS('crypto.subtle.exportKey')
|
@JS('crypto.subtle.exportKey')
|
||||||
|
|
@ -67,13 +78,30 @@ Future<dynamic> exportKey(String algorithm, dynamic key) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@JS('crypto.subtle.deriveKey')
|
@JS('crypto.subtle.deriveKey')
|
||||||
external dynamic _deriveKey(dynamic algorithm, dynamic baseKey,
|
external dynamic _deriveKey(
|
||||||
dynamic derivedKeyAlgorithm, bool extractable, List<String> keyUsages);
|
dynamic algorithm,
|
||||||
|
dynamic baseKey,
|
||||||
|
dynamic derivedKeyAlgorithm,
|
||||||
|
bool extractable,
|
||||||
|
List<String> keyUsages,
|
||||||
|
);
|
||||||
|
|
||||||
Future<ByteBuffer> deriveKey(dynamic algorithm, dynamic baseKey,
|
Future<ByteBuffer> deriveKey(
|
||||||
dynamic derivedKeyAlgorithm, bool extractable, List<String> keyUsages) {
|
dynamic algorithm,
|
||||||
return promiseToFuture(_deriveKey(
|
dynamic baseKey,
|
||||||
algorithm, baseKey, derivedKeyAlgorithm, extractable, keyUsages));
|
dynamic derivedKeyAlgorithm,
|
||||||
|
bool extractable,
|
||||||
|
List<String> keyUsages,
|
||||||
|
) {
|
||||||
|
return promiseToFuture(
|
||||||
|
_deriveKey(
|
||||||
|
algorithm,
|
||||||
|
baseKey,
|
||||||
|
derivedKeyAlgorithm,
|
||||||
|
extractable,
|
||||||
|
keyUsages,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@JS('crypto.subtle.deriveBits')
|
@JS('crypto.subtle.deriveBits')
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,10 @@ class DeviceKeysList {
|
||||||
} else {
|
} else {
|
||||||
// start verification with verified devices
|
// start verification with verified devices
|
||||||
final request = KeyVerification(
|
final request = KeyVerification(
|
||||||
encryption: encryption, userId: userId, deviceId: '*');
|
encryption: encryption,
|
||||||
|
userId: userId,
|
||||||
|
deviceId: '*',
|
||||||
|
);
|
||||||
await request.start();
|
await request.start();
|
||||||
encryption.keyVerificationManager.addRequest(request);
|
encryption.keyVerificationManager.addRequest(request);
|
||||||
return request;
|
return request;
|
||||||
|
|
@ -103,11 +106,11 @@ class DeviceKeysList {
|
||||||
}
|
}
|
||||||
|
|
||||||
DeviceKeysList.fromDbJson(
|
DeviceKeysList.fromDbJson(
|
||||||
Map<String, dynamic> dbEntry,
|
Map<String, dynamic> dbEntry,
|
||||||
List<Map<String, dynamic>> childEntries,
|
List<Map<String, dynamic>> childEntries,
|
||||||
List<Map<String, dynamic>> crossSigningEntries,
|
List<Map<String, dynamic>> crossSigningEntries,
|
||||||
this.client)
|
this.client,
|
||||||
: userId = dbEntry['user_id'] ?? '' {
|
) : userId = dbEntry['user_id'] ?? '' {
|
||||||
outdated = dbEntry['outdated'];
|
outdated = dbEntry['outdated'];
|
||||||
deviceKeys = {};
|
deviceKeys = {};
|
||||||
for (final childEntry in childEntries) {
|
for (final childEntry in childEntries) {
|
||||||
|
|
@ -195,8 +198,11 @@ abstract class SignableKey extends MatrixSignableKey {
|
||||||
return String.fromCharCodes(canonicalJson.encode(data));
|
return String.fromCharCodes(canonicalJson.encode(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _verifySignature(String pubKey, String signature,
|
bool _verifySignature(
|
||||||
{bool isSignatureWithoutLibolmValid = false}) {
|
String pubKey,
|
||||||
|
String signature, {
|
||||||
|
bool isSignatureWithoutLibolmValid = false,
|
||||||
|
}) {
|
||||||
olm.Utility olmutil;
|
olm.Utility olmutil;
|
||||||
try {
|
try {
|
||||||
olmutil = olm.Utility();
|
olmutil = olm.Utility();
|
||||||
|
|
@ -313,10 +319,11 @@ abstract class SignableKey extends MatrixSignableKey {
|
||||||
}
|
}
|
||||||
// or else we just recurse into that key and check if it works out
|
// or else we just recurse into that key and check if it works out
|
||||||
final haveChain = key.hasValidSignatureChain(
|
final haveChain = key.hasValidSignatureChain(
|
||||||
verifiedOnly: verifiedOnly,
|
verifiedOnly: verifiedOnly,
|
||||||
visited: visited_,
|
visited: visited_,
|
||||||
onlyValidateUserIds: onlyValidateUserIds,
|
onlyValidateUserIds: onlyValidateUserIds,
|
||||||
verifiedByTheirMasterKey: verifiedByTheirMasterKey);
|
verifiedByTheirMasterKey: verifiedByTheirMasterKey,
|
||||||
|
);
|
||||||
if (haveChain) {
|
if (haveChain) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -396,8 +403,9 @@ class CrossSigningKey extends SignableKey {
|
||||||
}
|
}
|
||||||
|
|
||||||
CrossSigningKey.fromMatrixCrossSigningKey(
|
CrossSigningKey.fromMatrixCrossSigningKey(
|
||||||
MatrixCrossSigningKey key, Client client)
|
MatrixCrossSigningKey key,
|
||||||
: super.fromJson(key.toJson().copy(), client) {
|
Client client,
|
||||||
|
) : super.fromJson(key.toJson().copy(), client) {
|
||||||
final json = toJson();
|
final json = toJson();
|
||||||
identifier = key.publicKey;
|
identifier = key.publicKey;
|
||||||
usage = json['usage'].cast<String>();
|
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
|
// without libolm we still want to be able to add devices. In that case we ofc just can't
|
||||||
// verify the signature
|
// verify the signature
|
||||||
_verifySignature(
|
_verifySignature(
|
||||||
ed25519Key!, signatures![userId]!['ed25519:$deviceId']!,
|
ed25519Key!,
|
||||||
isSignatureWithoutLibolmValid: true));
|
signatures![userId]!['ed25519:$deviceId']!,
|
||||||
|
isSignatureWithoutLibolmValid: true,
|
||||||
|
));
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get blocked => super.blocked || !selfSigned;
|
bool get blocked => super.blocked || !selfSigned;
|
||||||
|
|
@ -480,9 +490,11 @@ class DeviceKeys extends SignableKey {
|
||||||
?.setBlockedUserDeviceKey(newBlocked, userId, deviceId!);
|
?.setBlockedUserDeviceKey(newBlocked, userId, deviceId!);
|
||||||
}
|
}
|
||||||
|
|
||||||
DeviceKeys.fromMatrixDeviceKeys(MatrixDeviceKeys keys, Client client,
|
DeviceKeys.fromMatrixDeviceKeys(
|
||||||
[DateTime? lastActiveTs])
|
MatrixDeviceKeys keys,
|
||||||
: super.fromJson(keys.toJson().copy(), client) {
|
Client client, [
|
||||||
|
DateTime? lastActiveTs,
|
||||||
|
]) : super.fromJson(keys.toJson().copy(), client) {
|
||||||
final json = toJson();
|
final json = toJson();
|
||||||
identifier = keys.deviceId;
|
identifier = keys.deviceId;
|
||||||
algorithms = json['algorithms'].cast<String>();
|
algorithms = json['algorithms'].cast<String>();
|
||||||
|
|
@ -518,7 +530,10 @@ class DeviceKeys extends SignableKey {
|
||||||
}
|
}
|
||||||
|
|
||||||
final request = KeyVerification(
|
final request = KeyVerification(
|
||||||
encryption: encryption, userId: userId, deviceId: deviceId!);
|
encryption: encryption,
|
||||||
|
userId: userId,
|
||||||
|
deviceId: deviceId!,
|
||||||
|
);
|
||||||
|
|
||||||
await request.start();
|
await request.start();
|
||||||
encryption.keyVerificationManager.addRequest(request);
|
encryption.keyVerificationManager.addRequest(request);
|
||||||
|
|
|
||||||
|
|
@ -28,46 +28,61 @@ abstract class EventLocalizations {
|
||||||
// Thus, it seems easier to offload that logic into `Event.getLocalizedBody()` and pass the
|
// Thus, it seems easier to offload that logic into `Event.getLocalizedBody()` and pass the
|
||||||
// `body` variable around here.
|
// `body` variable around here.
|
||||||
static String _localizedBodyNormalMessage(
|
static String _localizedBodyNormalMessage(
|
||||||
Event event, MatrixLocalizations i18n, String body) {
|
Event event,
|
||||||
|
MatrixLocalizations i18n,
|
||||||
|
String body,
|
||||||
|
) {
|
||||||
switch (event.messageType) {
|
switch (event.messageType) {
|
||||||
case MessageTypes.Image:
|
case MessageTypes.Image:
|
||||||
return i18n.sentAPicture(
|
return i18n.sentAPicture(
|
||||||
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n));
|
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
|
||||||
|
);
|
||||||
case MessageTypes.File:
|
case MessageTypes.File:
|
||||||
return i18n.sentAFile(
|
return i18n.sentAFile(
|
||||||
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n));
|
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
|
||||||
|
);
|
||||||
case MessageTypes.Audio:
|
case MessageTypes.Audio:
|
||||||
return i18n.sentAnAudio(
|
return i18n.sentAnAudio(
|
||||||
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n));
|
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
|
||||||
|
);
|
||||||
case MessageTypes.Video:
|
case MessageTypes.Video:
|
||||||
return i18n.sentAVideo(
|
return i18n.sentAVideo(
|
||||||
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n));
|
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
|
||||||
|
);
|
||||||
case MessageTypes.Location:
|
case MessageTypes.Location:
|
||||||
return i18n.sharedTheLocation(
|
return i18n.sharedTheLocation(
|
||||||
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n));
|
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
|
||||||
|
);
|
||||||
case MessageTypes.Sticker:
|
case MessageTypes.Sticker:
|
||||||
return i18n.sentASticker(
|
return i18n.sentASticker(
|
||||||
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n));
|
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
|
||||||
|
);
|
||||||
case MessageTypes.Emote:
|
case MessageTypes.Emote:
|
||||||
return '* $body';
|
return '* $body';
|
||||||
case EventTypes.KeyVerificationRequest:
|
case EventTypes.KeyVerificationRequest:
|
||||||
return i18n.requestedKeyVerification(
|
return i18n.requestedKeyVerification(
|
||||||
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n));
|
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
|
||||||
|
);
|
||||||
case EventTypes.KeyVerificationCancel:
|
case EventTypes.KeyVerificationCancel:
|
||||||
return i18n.canceledKeyVerification(
|
return i18n.canceledKeyVerification(
|
||||||
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n));
|
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
|
||||||
|
);
|
||||||
case EventTypes.KeyVerificationDone:
|
case EventTypes.KeyVerificationDone:
|
||||||
return i18n.completedKeyVerification(
|
return i18n.completedKeyVerification(
|
||||||
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n));
|
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
|
||||||
|
);
|
||||||
case EventTypes.KeyVerificationReady:
|
case EventTypes.KeyVerificationReady:
|
||||||
return i18n.isReadyForKeyVerification(
|
return i18n.isReadyForKeyVerification(
|
||||||
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n));
|
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
|
||||||
|
);
|
||||||
case EventTypes.KeyVerificationAccept:
|
case EventTypes.KeyVerificationAccept:
|
||||||
return i18n.acceptedKeyVerification(
|
return i18n.acceptedKeyVerification(
|
||||||
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n));
|
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
|
||||||
|
);
|
||||||
case EventTypes.KeyVerificationStart:
|
case EventTypes.KeyVerificationStart:
|
||||||
return i18n.startedKeyVerification(
|
return i18n.startedKeyVerification(
|
||||||
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n));
|
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
|
||||||
|
);
|
||||||
case MessageTypes.BadEncrypted:
|
case MessageTypes.BadEncrypted:
|
||||||
String errorText;
|
String errorText;
|
||||||
switch (event.body) {
|
switch (event.body) {
|
||||||
|
|
@ -102,27 +117,35 @@ abstract class EventLocalizations {
|
||||||
String Function(Event event, MatrixLocalizations i18n, String body)?>
|
String Function(Event event, MatrixLocalizations i18n, String body)?>
|
||||||
localizationsMap = {
|
localizationsMap = {
|
||||||
EventTypes.Sticker: (event, i18n, body) => i18n.sentASticker(
|
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.Redaction: (event, i18n, body) => i18n.redactedAnEvent(event),
|
||||||
EventTypes.RoomAliases: (event, i18n, body) => i18n.changedTheRoomAliases(
|
EventTypes.RoomAliases: (event, i18n, body) => i18n.changedTheRoomAliases(
|
||||||
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n)),
|
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
|
||||||
|
),
|
||||||
EventTypes.RoomCanonicalAlias: (event, i18n, body) =>
|
EventTypes.RoomCanonicalAlias: (event, i18n, body) =>
|
||||||
i18n.changedTheRoomInvitationLink(
|
i18n.changedTheRoomInvitationLink(
|
||||||
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n)),
|
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
|
||||||
|
),
|
||||||
EventTypes.RoomCreate: (event, i18n, body) => i18n.createdTheChat(
|
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.RoomTombstone: (event, i18n, body) => i18n.roomHasBeenUpgraded,
|
||||||
EventTypes.RoomJoinRules: (event, i18n, body) {
|
EventTypes.RoomJoinRules: (event, i18n, body) {
|
||||||
final joinRules = JoinRules.values.firstWhereOrNull((r) =>
|
final joinRules = JoinRules.values.firstWhereOrNull(
|
||||||
r.toString().replaceAll('JoinRules.', '') ==
|
(r) =>
|
||||||
event.content['join_rule']);
|
r.toString().replaceAll('JoinRules.', '') ==
|
||||||
|
event.content['join_rule'],
|
||||||
|
);
|
||||||
if (joinRules == null) {
|
if (joinRules == null) {
|
||||||
return i18n.changedTheJoinRules(
|
return i18n.changedTheJoinRules(
|
||||||
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n));
|
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return i18n.changedTheJoinRulesTo(
|
return i18n.changedTheJoinRulesTo(
|
||||||
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
|
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
|
||||||
joinRules.getLocalizedString(i18n));
|
joinRules.getLocalizedString(i18n),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
EventTypes.RoomMember: (event, i18n, body) {
|
EventTypes.RoomMember: (event, i18n, body) {
|
||||||
|
|
@ -188,58 +211,75 @@ abstract class EventLocalizations {
|
||||||
},
|
},
|
||||||
EventTypes.RoomPowerLevels: (event, i18n, body) =>
|
EventTypes.RoomPowerLevels: (event, i18n, body) =>
|
||||||
i18n.changedTheChatPermissions(
|
i18n.changedTheChatPermissions(
|
||||||
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.tryGet<String>('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.tryGet<String>('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) {
|
||||||
final guestAccess = GuestAccess.values.firstWhereOrNull((r) =>
|
final guestAccess = GuestAccess.values.firstWhereOrNull(
|
||||||
r.toString().replaceAll('GuestAccess.', '') ==
|
(r) =>
|
||||||
event.content['guest_access']);
|
r.toString().replaceAll('GuestAccess.', '') ==
|
||||||
|
event.content['guest_access'],
|
||||||
|
);
|
||||||
if (guestAccess == null) {
|
if (guestAccess == null) {
|
||||||
return i18n.changedTheGuestAccessRules(
|
return i18n.changedTheGuestAccessRules(
|
||||||
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n));
|
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return i18n.changedTheGuestAccessRulesTo(
|
return i18n.changedTheGuestAccessRulesTo(
|
||||||
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
|
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
|
||||||
guestAccess.getLocalizedString(i18n));
|
guestAccess.getLocalizedString(i18n),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
EventTypes.HistoryVisibility: (event, i18n, body) {
|
EventTypes.HistoryVisibility: (event, i18n, body) {
|
||||||
final historyVisibility = HistoryVisibility.values.firstWhereOrNull((r) =>
|
final historyVisibility = HistoryVisibility.values.firstWhereOrNull(
|
||||||
r.toString().replaceAll('HistoryVisibility.', '') ==
|
(r) =>
|
||||||
event.content['history_visibility']);
|
r.toString().replaceAll('HistoryVisibility.', '') ==
|
||||||
|
event.content['history_visibility'],
|
||||||
|
);
|
||||||
if (historyVisibility == null) {
|
if (historyVisibility == null) {
|
||||||
return i18n.changedTheHistoryVisibility(
|
return i18n.changedTheHistoryVisibility(
|
||||||
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n));
|
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return i18n.changedTheHistoryVisibilityTo(
|
return i18n.changedTheHistoryVisibilityTo(
|
||||||
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
|
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
|
||||||
historyVisibility.getLocalizedString(i18n));
|
historyVisibility.getLocalizedString(i18n),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
EventTypes.Encryption: (event, i18n, body) {
|
EventTypes.Encryption: (event, i18n, body) {
|
||||||
var localizedBody = i18n.activatedEndToEndEncryption(
|
var localizedBody = i18n.activatedEndToEndEncryption(
|
||||||
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n));
|
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
|
||||||
|
);
|
||||||
if (event.room.client.encryptionEnabled == false) {
|
if (event.room.client.encryptionEnabled == false) {
|
||||||
localizedBody += '. ${i18n.needPantalaimonWarning}';
|
localizedBody += '. ${i18n.needPantalaimonWarning}';
|
||||||
}
|
}
|
||||||
return localizedBody;
|
return localizedBody;
|
||||||
},
|
},
|
||||||
EventTypes.CallAnswer: (event, i18n, body) => i18n.answeredTheCall(
|
EventTypes.CallAnswer: (event, i18n, body) => i18n.answeredTheCall(
|
||||||
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n)),
|
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
|
||||||
|
),
|
||||||
EventTypes.CallHangup: (event, i18n, body) => i18n.endedTheCall(
|
EventTypes.CallHangup: (event, i18n, body) => i18n.endedTheCall(
|
||||||
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n)),
|
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
|
||||||
|
),
|
||||||
EventTypes.CallInvite: (event, i18n, body) => i18n.startedACall(
|
EventTypes.CallInvite: (event, i18n, body) => i18n.startedACall(
|
||||||
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n)),
|
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
|
||||||
|
),
|
||||||
EventTypes.CallCandidates: (event, i18n, body) => i18n.sentCallInformations(
|
EventTypes.CallCandidates: (event, i18n, body) => i18n.sentCallInformations(
|
||||||
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n)),
|
event.senderFromMemoryOrFallback.calcDisplayname(i18n: i18n),
|
||||||
|
),
|
||||||
EventTypes.Encrypted: (event, i18n, body) =>
|
EventTypes.Encrypted: (event, i18n, body) =>
|
||||||
_localizedBodyNormalMessage(event, i18n, body),
|
_localizedBodyNormalMessage(event, i18n, body),
|
||||||
EventTypes.Message: (event, i18n, body) =>
|
EventTypes.Message: (event, i18n, body) =>
|
||||||
|
|
|
||||||
|
|
@ -53,8 +53,11 @@ class EventUpdate {
|
||||||
// The json payload of the content of this event.
|
// The json payload of the content of this event.
|
||||||
final Map<String, dynamic> content;
|
final Map<String, dynamic> content;
|
||||||
|
|
||||||
EventUpdate(
|
EventUpdate({
|
||||||
{required this.roomID, required this.type, required this.content});
|
required this.roomID,
|
||||||
|
required this.type,
|
||||||
|
required this.content,
|
||||||
|
});
|
||||||
|
|
||||||
Future<EventUpdate> decrypt(Room room, {bool store = false}) async {
|
Future<EventUpdate> decrypt(Room room, {bool store = false}) async {
|
||||||
final encryption = room.client.encryption;
|
final encryption = room.client.encryption;
|
||||||
|
|
@ -65,10 +68,16 @@ class EventUpdate {
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
final decrpytedEvent = await encryption.decryptRoomEvent(
|
final decrpytedEvent = await encryption.decryptRoomEvent(
|
||||||
room.id, Event.fromJson(content, room),
|
room.id,
|
||||||
store: store, updateType: type);
|
Event.fromJson(content, room),
|
||||||
|
store: store,
|
||||||
|
updateType: type,
|
||||||
|
);
|
||||||
return EventUpdate(
|
return EventUpdate(
|
||||||
roomID: roomID, type: type, content: decrpytedEvent.toJson());
|
roomID: roomID,
|
||||||
|
type: type,
|
||||||
|
content: decrpytedEvent.toJson(),
|
||||||
|
);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logs().e('[LibOlm] Could not decrypt megolm event', e, s);
|
Logs().e('[LibOlm] Could not decrypt megolm event', e, s);
|
||||||
return this;
|
return this;
|
||||||
|
|
|
||||||
|
|
@ -32,9 +32,14 @@ class HtmlToText {
|
||||||
// miss-matching tags, and this way we actually correctly identify what we want to strip and, well,
|
// miss-matching tags, and this way we actually correctly identify what we want to strip and, well,
|
||||||
// strip it.
|
// strip it.
|
||||||
final renderHtml = html.replaceAll(
|
final renderHtml = html.replaceAll(
|
||||||
RegExp('<mx-reply>.*</mx-reply>',
|
RegExp(
|
||||||
caseSensitive: false, multiLine: false, dotAll: true),
|
'<mx-reply>.*</mx-reply>',
|
||||||
'');
|
caseSensitive: false,
|
||||||
|
multiLine: false,
|
||||||
|
dotAll: true,
|
||||||
|
),
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
|
||||||
final opts = _ConvertOpts();
|
final opts = _ConvertOpts();
|
||||||
var reply = _walkNode(opts, parseFragment(renderHtml));
|
var reply = _walkNode(opts, parseFragment(renderHtml));
|
||||||
|
|
@ -63,7 +68,9 @@ class HtmlToText {
|
||||||
text = text.substring(match.end);
|
text = text.substring(match.end);
|
||||||
// remove the </code> closing tag
|
// remove the </code> closing tag
|
||||||
text = text.replaceAll(
|
text = text.replaceAll(
|
||||||
RegExp(r'</code>$', multiLine: false, caseSensitive: false), '');
|
RegExp(r'</code>$', multiLine: false, caseSensitive: false),
|
||||||
|
'',
|
||||||
|
);
|
||||||
text = HtmlUnescape().convert(text);
|
text = HtmlUnescape().convert(text);
|
||||||
if (text.isNotEmpty) {
|
if (text.isNotEmpty) {
|
||||||
if (text[0] != '\n') {
|
if (text[0] != '\n') {
|
||||||
|
|
@ -108,8 +115,10 @@ class HtmlToText {
|
||||||
_listBulletPoints[opts.listDepth % _listBulletPoints.length];
|
_listBulletPoints[opts.listDepth % _listBulletPoints.length];
|
||||||
|
|
||||||
return entries
|
return entries
|
||||||
.map((s) =>
|
.map(
|
||||||
'${' ' * opts.listDepth}$bulletPoint ${s.replaceAll('\n', '\n${' ' * opts.listDepth} ')}')
|
(s) =>
|
||||||
|
'${' ' * opts.listDepth}$bulletPoint ${s.replaceAll('\n', '\n${' ' * opts.listDepth} ')}',
|
||||||
|
)
|
||||||
.join('\n');
|
.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -124,15 +133,20 @@ class HtmlToText {
|
||||||
: 1;
|
: 1;
|
||||||
|
|
||||||
return entries
|
return entries
|
||||||
.mapIndexed((index, s) =>
|
.mapIndexed(
|
||||||
'${' ' * opts.listDepth}${start + index}. ${s.replaceAll('\n', '\n${' ' * opts.listDepth} ')}')
|
(index, s) =>
|
||||||
|
'${' ' * opts.listDepth}${start + index}. ${s.replaceAll('\n', '\n${' ' * opts.listDepth} ')}',
|
||||||
|
)
|
||||||
.join('\n');
|
.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
static const _listBulletPoints = <String>['●', '○', '■', '‣'];
|
static const _listBulletPoints = <String>['●', '○', '■', '‣'];
|
||||||
|
|
||||||
static List<String> _listChildNodes(_ConvertOpts opts, Element node,
|
static List<String> _listChildNodes(
|
||||||
[Iterable<String>? types]) {
|
_ConvertOpts opts,
|
||||||
|
Element node, [
|
||||||
|
Iterable<String>? types,
|
||||||
|
]) {
|
||||||
final replies = <String>[];
|
final replies = <String>[];
|
||||||
for (final child in node.nodes) {
|
for (final child in node.nodes) {
|
||||||
if (types != null &&
|
if (types != null &&
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,9 @@ import 'dart:async';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
http.StreamedResponse replaceStream(
|
http.StreamedResponse replaceStream(
|
||||||
http.StreamedResponse base, Stream<List<int>> stream) =>
|
http.StreamedResponse base,
|
||||||
|
Stream<List<int>> stream,
|
||||||
|
) =>
|
||||||
http.StreamedResponse(
|
http.StreamedResponse(
|
||||||
http.ByteStream(stream),
|
http.ByteStream(stream),
|
||||||
base.statusCode,
|
base.statusCode,
|
||||||
|
|
|
||||||
|
|
@ -44,13 +44,14 @@ extension ImagePackRoomExtension on Room {
|
||||||
}
|
}
|
||||||
packs
|
packs
|
||||||
.putIfAbsent(
|
.putIfAbsent(
|
||||||
finalSlug,
|
finalSlug,
|
||||||
() => ImagePackContent.fromJson({})
|
() => ImagePackContent.fromJson({})
|
||||||
..pack.displayName = imagePack.pack.displayName ??
|
..pack.displayName = imagePack.pack.displayName ??
|
||||||
room?.getLocalizedDisplayname() ??
|
room?.getLocalizedDisplayname() ??
|
||||||
finalSlug
|
finalSlug
|
||||||
..pack.avatarUrl = imagePack.pack.avatarUrl ?? room?.avatar
|
..pack.avatarUrl = imagePack.pack.avatarUrl ?? room?.avatar
|
||||||
..pack.attribution = imagePack.pack.attribution)
|
..pack.attribution = imagePack.pack.attribution,
|
||||||
|
)
|
||||||
.images[entry.key] = image;
|
.images[entry.key] = image;
|
||||||
allMxcs.add(image.url);
|
allMxcs.add(image.url);
|
||||||
}
|
}
|
||||||
|
|
@ -71,8 +72,11 @@ extension ImagePackRoomExtension on Room {
|
||||||
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}';
|
||||||
addImagePack(room.getState('im.ponies.room_emotes', stateKey),
|
addImagePack(
|
||||||
room: room, slug: fallbackSlug);
|
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'];
|
final allRoomEmotes = states['im.ponies.room_emotes'];
|
||||||
if (allRoomEmotes != null) {
|
if (allRoomEmotes != null) {
|
||||||
for (final entry in allRoomEmotes.entries) {
|
for (final entry in allRoomEmotes.entries) {
|
||||||
addImagePack(entry.value,
|
addImagePack(
|
||||||
room: this,
|
entry.value,
|
||||||
slug: (entry.value.stateKey?.isNotEmpty == true)
|
room: this,
|
||||||
? entry.value.stateKey
|
slug: (entry.value.stateKey?.isNotEmpty == true)
|
||||||
: 'room');
|
? entry.value.stateKey
|
||||||
|
: 'room',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return packs;
|
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
|
/// 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
|
/// slugs to a map of the image code to their mxc url
|
||||||
Map<String, Map<String, String>> getImagePacksFlat([ImagePackUsage? usage]) =>
|
Map<String, Map<String, String>> getImagePacksFlat([ImagePackUsage? usage]) =>
|
||||||
getImagePacks(usage).map((k, v) =>
|
getImagePacks(usage).map(
|
||||||
MapEntry(k, v.images.map((k, v) => MapEntry(k, v.url.toString()))));
|
(k, v) =>
|
||||||
|
MapEntry(k, v.images.map((k, v) => MapEntry(k, v.url.toString()))),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -155,7 +155,7 @@ class BlockLatexSyntax extends BlockSyntax {
|
||||||
// we use .substring(2) as childLines will *always* contain the first two '$$'
|
// we use .substring(2) as childLines will *always* contain the first two '$$'
|
||||||
final latex = childLines.join('\n').trim().substring(2).trim();
|
final latex = childLines.join('\n').trim().substring(2).trim();
|
||||||
final element = Element('div', [
|
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);
|
element.attributes['data-mx-maths'] = htmlAttrEscape.convert(latex);
|
||||||
return element;
|
return element;
|
||||||
|
|
@ -165,7 +165,8 @@ class BlockLatexSyntax extends BlockSyntax {
|
||||||
class PillSyntax extends InlineSyntax {
|
class PillSyntax extends InlineSyntax {
|
||||||
PillSyntax()
|
PillSyntax()
|
||||||
: super(
|
: super(
|
||||||
r'([@#!][^\s:]*:(?:[^\s]+\.\w+|[\d\.]+|\[[a-fA-F0-9:]+\])(?::\d+)?)');
|
r'([@#!][^\s:]*:(?:[^\s]+\.\w+|[\d\.]+|\[[a-fA-F0-9:]+\])(?::\d+)?)',
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool onMatch(InlineParser parser, Match match) {
|
bool onMatch(InlineParser parser, Match match) {
|
||||||
|
|
@ -269,8 +270,11 @@ String markdown(
|
||||||
}
|
}
|
||||||
|
|
||||||
extension on String {
|
extension on String {
|
||||||
String convertLinebreaksToBr(String tagName,
|
String convertLinebreaksToBr(
|
||||||
{bool exclude = false, String replaceWith = '<br/>'}) {
|
String tagName, {
|
||||||
|
bool exclude = false,
|
||||||
|
String replaceWith = '<br/>',
|
||||||
|
}) {
|
||||||
final parts = split('$tagName>');
|
final parts = split('$tagName>');
|
||||||
var convertLinebreaks = exclude;
|
var convertLinebreaks = exclude;
|
||||||
for (var i = 0; i < parts.length; i++) {
|
for (var i = 0; i < parts.length; i++) {
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,9 @@ class MatrixDefaultLocalizations extends MatrixLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String changedTheGuestAccessRulesTo(
|
String changedTheGuestAccessRulesTo(
|
||||||
String senderName, String localizedString) =>
|
String senderName,
|
||||||
|
String localizedString,
|
||||||
|
) =>
|
||||||
'$senderName changed the guest access rules to $localizedString';
|
'$senderName changed the guest access rules to $localizedString';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -70,7 +72,9 @@ class MatrixDefaultLocalizations extends MatrixLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String changedTheHistoryVisibilityTo(
|
String changedTheHistoryVisibilityTo(
|
||||||
String senderName, String localizedString) =>
|
String senderName,
|
||||||
|
String localizedString,
|
||||||
|
) =>
|
||||||
'$senderName changed the history visibility to $localizedString';
|
'$senderName changed the history visibility to $localizedString';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -48,11 +48,16 @@ class MatrixFile {
|
||||||
|
|
||||||
/// derivatives the MIME type from the [bytes] and correspondingly creates a
|
/// derivatives the MIME type from the [bytes] and correspondingly creates a
|
||||||
/// [MatrixFile], [MatrixImageFile], [MatrixAudioFile] or a [MatrixVideoFile]
|
/// [MatrixFile], [MatrixImageFile], [MatrixAudioFile] or a [MatrixVideoFile]
|
||||||
factory MatrixFile.fromMimeType(
|
factory MatrixFile.fromMimeType({
|
||||||
{required Uint8List bytes, required String name, String? mimeType}) {
|
required Uint8List bytes,
|
||||||
final msgType = msgTypeFromMime(mimeType ??
|
required String name,
|
||||||
lookupMimeType(name, headerBytes: bytes) ??
|
String? mimeType,
|
||||||
'application/octet-stream');
|
}) {
|
||||||
|
final msgType = msgTypeFromMime(
|
||||||
|
mimeType ??
|
||||||
|
lookupMimeType(name, headerBytes: bytes) ??
|
||||||
|
'application/octet-stream',
|
||||||
|
);
|
||||||
if (msgType == MessageTypes.Image) {
|
if (msgType == MessageTypes.Image) {
|
||||||
return MatrixImageFile(bytes: bytes, name: name, mimeType: mimeType);
|
return MatrixImageFile(bytes: bytes, name: name, mimeType: mimeType);
|
||||||
}
|
}
|
||||||
|
|
@ -134,8 +139,8 @@ class MatrixImageFile extends MatrixFile {
|
||||||
int maxDimension = 1600,
|
int maxDimension = 1600,
|
||||||
String? mimeType,
|
String? mimeType,
|
||||||
Future<MatrixImageFileResizedResponse?> Function(
|
Future<MatrixImageFileResizedResponse?> Function(
|
||||||
MatrixImageFileResizeArguments)?
|
MatrixImageFileResizeArguments,
|
||||||
customImageResizer,
|
)? customImageResizer,
|
||||||
@Deprecated('Use [nativeImplementations] instead') ComputeRunner? compute,
|
@Deprecated('Use [nativeImplementations] instead') ComputeRunner? compute,
|
||||||
NativeImplementations nativeImplementations = NativeImplementations.dummy,
|
NativeImplementations nativeImplementations = NativeImplementations.dummy,
|
||||||
}) async {
|
}) async {
|
||||||
|
|
@ -146,9 +151,10 @@ class MatrixImageFile extends MatrixFile {
|
||||||
final image = MatrixImageFile(name: name, mimeType: mimeType, bytes: bytes);
|
final image = MatrixImageFile(name: name, mimeType: mimeType, bytes: bytes);
|
||||||
|
|
||||||
return await image.generateThumbnail(
|
return await image.generateThumbnail(
|
||||||
dimension: maxDimension,
|
dimension: maxDimension,
|
||||||
customImageResizer: customImageResizer,
|
customImageResizer: customImageResizer,
|
||||||
nativeImplementations: nativeImplementations) ??
|
nativeImplementations: nativeImplementations,
|
||||||
|
) ??
|
||||||
image;
|
image;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -187,8 +193,8 @@ class MatrixImageFile extends MatrixFile {
|
||||||
Future<MatrixImageFile?> generateThumbnail({
|
Future<MatrixImageFile?> generateThumbnail({
|
||||||
int dimension = Client.defaultThumbnailSize,
|
int dimension = Client.defaultThumbnailSize,
|
||||||
Future<MatrixImageFileResizedResponse?> Function(
|
Future<MatrixImageFileResizedResponse?> Function(
|
||||||
MatrixImageFileResizeArguments)?
|
MatrixImageFileResizeArguments,
|
||||||
customImageResizer,
|
)? customImageResizer,
|
||||||
@Deprecated('Use [nativeImplementations] instead') ComputeRunner? compute,
|
@Deprecated('Use [nativeImplementations] instead') ComputeRunner? compute,
|
||||||
NativeImplementations nativeImplementations = NativeImplementations.dummy,
|
NativeImplementations nativeImplementations = NativeImplementations.dummy,
|
||||||
}) async {
|
}) async {
|
||||||
|
|
@ -212,7 +218,9 @@ class MatrixImageFile extends MatrixFile {
|
||||||
|
|
||||||
// we should take the opportunity to update the image dimension
|
// we should take the opportunity to update the image dimension
|
||||||
setImageSizeIfNull(
|
setImageSizeIfNull(
|
||||||
width: resizedData.originalWidth, height: resizedData.originalHeight);
|
width: resizedData.originalWidth,
|
||||||
|
height: resizedData.originalHeight,
|
||||||
|
);
|
||||||
|
|
||||||
// the thumbnail should rather return null than the enshrined image
|
// the thumbnail should rather return null than the enshrined image
|
||||||
if (resizedData.width > dimension || resizedData.height > dimension) {
|
if (resizedData.width > dimension || resizedData.height > dimension) {
|
||||||
|
|
@ -233,7 +241,8 @@ class MatrixImageFile extends MatrixFile {
|
||||||
/// you would likely want to use [NativeImplementations] and
|
/// you would likely want to use [NativeImplementations] and
|
||||||
/// [Client.nativeImplementations] instead
|
/// [Client.nativeImplementations] instead
|
||||||
static MatrixImageFileResizedResponse? calcMetadataImplementation(
|
static MatrixImageFileResizedResponse? calcMetadataImplementation(
|
||||||
Uint8List bytes) {
|
Uint8List bytes,
|
||||||
|
) {
|
||||||
final image = decodeImage(bytes);
|
final image = decodeImage(bytes);
|
||||||
if (image == null) return null;
|
if (image == null) return null;
|
||||||
|
|
||||||
|
|
@ -252,12 +261,15 @@ class MatrixImageFile extends MatrixFile {
|
||||||
/// you would likely want to use [NativeImplementations] and
|
/// you would likely want to use [NativeImplementations] and
|
||||||
/// [Client.nativeImplementations] instead
|
/// [Client.nativeImplementations] instead
|
||||||
static MatrixImageFileResizedResponse? resizeImplementation(
|
static MatrixImageFileResizedResponse? resizeImplementation(
|
||||||
MatrixImageFileResizeArguments arguments) {
|
MatrixImageFileResizeArguments arguments,
|
||||||
|
) {
|
||||||
final image = decodeImage(arguments.bytes);
|
final image = decodeImage(arguments.bytes);
|
||||||
|
|
||||||
final resized = copyResize(image!,
|
final resized = copyResize(
|
||||||
height: image.height > image.width ? arguments.maxDimension : null,
|
image!,
|
||||||
width: image.width >= image.height ? arguments.maxDimension : null);
|
height: image.height > image.width ? arguments.maxDimension : null,
|
||||||
|
width: image.width >= image.height ? arguments.maxDimension : null,
|
||||||
|
);
|
||||||
|
|
||||||
final encoded = encodeNamedImage(arguments.fileName, resized);
|
final encoded = encodeNamedImage(arguments.fileName, resized);
|
||||||
if (encoded == null) return null;
|
if (encoded == null) return null;
|
||||||
|
|
@ -302,7 +314,8 @@ class MatrixImageFileResizedResponse {
|
||||||
) =>
|
) =>
|
||||||
MatrixImageFileResizedResponse(
|
MatrixImageFileResizedResponse(
|
||||||
bytes: Uint8List.fromList(
|
bytes: Uint8List.fromList(
|
||||||
(json['bytes'] as Iterable<dynamic>).whereType<int>().toList()),
|
(json['bytes'] as Iterable<dynamic>).whereType<int>().toList(),
|
||||||
|
),
|
||||||
width: json['width'],
|
width: json['width'],
|
||||||
height: json['height'],
|
height: json['height'],
|
||||||
originalHeight: json['originalHeight'],
|
originalHeight: json['originalHeight'],
|
||||||
|
|
@ -354,13 +367,14 @@ class MatrixVideoFile extends MatrixFile {
|
||||||
final int? height;
|
final int? height;
|
||||||
final int? duration;
|
final int? duration;
|
||||||
|
|
||||||
MatrixVideoFile(
|
MatrixVideoFile({
|
||||||
{required super.bytes,
|
required super.bytes,
|
||||||
required super.name,
|
required super.name,
|
||||||
super.mimeType,
|
super.mimeType,
|
||||||
this.width,
|
this.width,
|
||||||
this.height,
|
this.height,
|
||||||
this.duration});
|
this.duration,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get msgType => 'm.video';
|
String get msgType => 'm.video';
|
||||||
|
|
@ -377,11 +391,12 @@ class MatrixVideoFile extends MatrixFile {
|
||||||
class MatrixAudioFile extends MatrixFile {
|
class MatrixAudioFile extends MatrixFile {
|
||||||
final int? duration;
|
final int? duration;
|
||||||
|
|
||||||
MatrixAudioFile(
|
MatrixAudioFile({
|
||||||
{required super.bytes,
|
required super.bytes,
|
||||||
required super.name,
|
required super.name,
|
||||||
super.mimeType,
|
super.mimeType,
|
||||||
this.duration});
|
this.duration,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get msgType => 'm.audio';
|
String get msgType => 'm.audio';
|
||||||
|
|
|
||||||
|
|
@ -82,14 +82,16 @@ extension MatrixIdExtension on String {
|
||||||
return uri.replace(pathSegments: identifiers);
|
return uri.replace(pathSegments: identifiers);
|
||||||
} else if (toLowerCase().startsWith(matrixToPrefix)) {
|
} else if (toLowerCase().startsWith(matrixToPrefix)) {
|
||||||
return Uri.tryParse(
|
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 {
|
} else {
|
||||||
return Uri(
|
return Uri(
|
||||||
pathSegments: RegExp(r'/((?:[#!@+][^:]*:)?[^/?]*)(?:\?.*$)?')
|
pathSegments: RegExp(r'/((?:[#!@+][^:]*:)?[^/?]*)(?:\?.*$)?')
|
||||||
.allMatches('/$this')
|
.allMatches('/$this')
|
||||||
.map((m) => m[1]!),
|
.map((m) => m[1]!),
|
||||||
query: RegExp(r'(?:/(?:[#!@+][^:]*:)?[^/?]*)*\?(.*$)')
|
query: RegExp(r'(?:/(?:[#!@+][^:]*:)?[^/?]*)*\?(.*$)')
|
||||||
.firstMatch('/$this')?[1]);
|
.firstMatch('/$this')?[1],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -121,10 +123,11 @@ class MatrixIdentifierStringExtensionResults {
|
||||||
final Set<String> via;
|
final Set<String> via;
|
||||||
final String? action;
|
final String? action;
|
||||||
|
|
||||||
MatrixIdentifierStringExtensionResults(
|
MatrixIdentifierStringExtensionResults({
|
||||||
{required this.primaryIdentifier,
|
required this.primaryIdentifier,
|
||||||
this.secondaryIdentifier,
|
this.secondaryIdentifier,
|
||||||
this.queryString,
|
this.queryString,
|
||||||
this.via = const {},
|
this.via = const {},
|
||||||
this.action});
|
this.action,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -131,12 +131,16 @@ abstract class MatrixLocalizations {
|
||||||
String changedTheGuestAccessRules(String senderName);
|
String changedTheGuestAccessRules(String senderName);
|
||||||
|
|
||||||
String changedTheGuestAccessRulesTo(
|
String changedTheGuestAccessRulesTo(
|
||||||
String senderName, String localizedString);
|
String senderName,
|
||||||
|
String localizedString,
|
||||||
|
);
|
||||||
|
|
||||||
String changedTheHistoryVisibility(String senderName);
|
String changedTheHistoryVisibility(String senderName);
|
||||||
|
|
||||||
String changedTheHistoryVisibilityTo(
|
String changedTheHistoryVisibilityTo(
|
||||||
String senderName, String localizedString);
|
String senderName,
|
||||||
|
String localizedString,
|
||||||
|
);
|
||||||
|
|
||||||
String activatedEndToEndEncryption(String senderName);
|
String activatedEndToEndEncryption(String senderName);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -140,14 +140,17 @@ class NativeImplementationsIsolate extends NativeImplementations {
|
||||||
// ignore: deprecated_member_use_from_same_package
|
// ignore: deprecated_member_use_from_same_package
|
||||||
/// known from [Client.runInBackground]
|
/// known from [Client.runInBackground]
|
||||||
factory NativeImplementationsIsolate.fromRunInBackground(
|
factory NativeImplementationsIsolate.fromRunInBackground(
|
||||||
ComputeRunner runInBackground) {
|
ComputeRunner runInBackground,
|
||||||
|
) {
|
||||||
return NativeImplementationsIsolate(
|
return NativeImplementationsIsolate(
|
||||||
computeCallbackFromRunInBackground(runInBackground),
|
computeCallbackFromRunInBackground(runInBackground),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<T> runInBackground<T, U>(
|
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;
|
final compute = this.compute;
|
||||||
return await compute(function, arg);
|
return await compute(function, arg);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -193,8 +193,12 @@ class _OptimizedRules {
|
||||||
actions = EvaluatedPushRuleAction.fromActions(rule.actions);
|
actions = EvaluatedPushRuleAction.fromActions(rule.actions);
|
||||||
}
|
}
|
||||||
|
|
||||||
EvaluatedPushRuleAction? match(Map<String, String> event, String? displayName,
|
EvaluatedPushRuleAction? match(
|
||||||
int memberCount, Room room) {
|
Map<String, String> event,
|
||||||
|
String? displayName,
|
||||||
|
int memberCount,
|
||||||
|
Room room,
|
||||||
|
) {
|
||||||
if (patterns.any((pat) => !pat.match(event))) {
|
if (patterns.any((pat) => !pat.match(event))) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -207,8 +211,10 @@ class _OptimizedRules {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final regex = RegExp('(^|\\W)${RegExp.escape(displayName)}(\$|\\W)',
|
final regex = RegExp(
|
||||||
caseSensitive: false);
|
'(^|\\W)${RegExp.escape(displayName)}(\$|\\W)',
|
||||||
|
caseSensitive: false,
|
||||||
|
);
|
||||||
if (!regex.hasMatch(body)) {
|
if (!regex.hasMatch(body)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -217,9 +223,12 @@ class _OptimizedRules {
|
||||||
if (notificationPermissions.isNotEmpty) {
|
if (notificationPermissions.isNotEmpty) {
|
||||||
final sender = event.tryGet<String>('sender');
|
final sender = event.tryGet<String>('sender');
|
||||||
if (sender == null ||
|
if (sender == null ||
|
||||||
notificationPermissions.any((notificationType) =>
|
notificationPermissions.any(
|
||||||
!room.canSendNotification(sender,
|
(notificationType) => !room.canSendNotification(
|
||||||
notificationType: notificationType))) {
|
sender,
|
||||||
|
notificationType: notificationType,
|
||||||
|
),
|
||||||
|
)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -258,7 +267,10 @@ class PushruleEvaluator {
|
||||||
actions: c.actions,
|
actions: c.actions,
|
||||||
conditions: [
|
conditions: [
|
||||||
PushCondition(
|
PushCondition(
|
||||||
kind: 'event_match', key: 'content.body', pattern: c.pattern)
|
kind: 'event_match',
|
||||||
|
key: 'content.body',
|
||||||
|
pattern: c.pattern,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
ruleId: c.ruleId,
|
ruleId: c.ruleId,
|
||||||
default$: c.default$,
|
default$: c.default$,
|
||||||
|
|
@ -284,7 +296,10 @@ class PushruleEvaluator {
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, String> _flattenJson(
|
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) {
|
for (final entry in obj.entries) {
|
||||||
final key = prefix == '' ? entry.key : '$prefix.${entry.key}';
|
final key = prefix == '' ? entry.key : '$prefix.${entry.key}';
|
||||||
final value = entry.value;
|
final value = entry.value;
|
||||||
|
|
|
||||||
|
|
@ -16,26 +16,30 @@ extension SyncUpdateItemCount on SyncUpdate {
|
||||||
|
|
||||||
int get _joinRoomsItemCount =>
|
int get _joinRoomsItemCount =>
|
||||||
rooms?.join?.values.fold<int>(
|
rooms?.join?.values.fold<int>(
|
||||||
0,
|
0,
|
||||||
(prev, room) =>
|
(prev, room) =>
|
||||||
prev +
|
prev +
|
||||||
(room.accountData?.length ?? 0) +
|
(room.accountData?.length ?? 0) +
|
||||||
(room.state?.length ?? 0) +
|
(room.state?.length ?? 0) +
|
||||||
(room.timeline?.events?.length ?? 0)) ??
|
(room.timeline?.events?.length ?? 0),
|
||||||
|
) ??
|
||||||
0;
|
0;
|
||||||
|
|
||||||
int get _inviteRoomsItemCount =>
|
int get _inviteRoomsItemCount =>
|
||||||
rooms?.invite?.values.fold<int>(
|
rooms?.invite?.values.fold<int>(
|
||||||
0, (prev, room) => prev + (room.inviteState?.length ?? 0)) ??
|
0,
|
||||||
|
(prev, room) => prev + (room.inviteState?.length ?? 0),
|
||||||
|
) ??
|
||||||
0;
|
0;
|
||||||
|
|
||||||
int get _leaveRoomsItemCount =>
|
int get _leaveRoomsItemCount =>
|
||||||
rooms?.leave?.values.fold<int>(
|
rooms?.leave?.values.fold<int>(
|
||||||
0,
|
0,
|
||||||
(prev, room) =>
|
(prev, room) =>
|
||||||
prev +
|
prev +
|
||||||
(room.accountData?.length ?? 0) +
|
(room.accountData?.length ?? 0) +
|
||||||
(room.state?.length ?? 0) +
|
(room.state?.length ?? 0) +
|
||||||
(room.timeline?.events?.length ?? 0)) ??
|
(room.timeline?.events?.length ?? 0),
|
||||||
|
) ??
|
||||||
0;
|
0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,10 @@ class ToDeviceEvent extends BasicEventWithSender {
|
||||||
factory ToDeviceEvent.fromJson(Map<String, dynamic> json) {
|
factory ToDeviceEvent.fromJson(Map<String, dynamic> json) {
|
||||||
final event = BasicEventWithSender.fromJson(json);
|
final event = BasicEventWithSender.fromJson(json);
|
||||||
return ToDeviceEvent(
|
return ToDeviceEvent(
|
||||||
sender: event.senderId, type: event.type, content: event.content);
|
sender: event.senderId,
|
||||||
|
type: event.type,
|
||||||
|
content: event.content,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,9 @@ class UiaRequest<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Set<String> getNextStages(
|
Set<String> getNextStages(
|
||||||
List<AuthenticationFlow> flows, List<String> completed) {
|
List<AuthenticationFlow> flows,
|
||||||
|
List<String> completed,
|
||||||
|
) {
|
||||||
final nextStages = <String>{};
|
final nextStages = <String>{};
|
||||||
for (final flow in flows) {
|
for (final flow in flows) {
|
||||||
// check the flow starts with the completed stages
|
// check the flow starts with the completed stages
|
||||||
|
|
|
||||||
|
|
@ -59,11 +59,13 @@ extension MxcUriExtension on Uri {
|
||||||
///
|
///
|
||||||
/// Important! To use this link you have to set a http header like this:
|
/// Important! To use this link you have to set a http header like this:
|
||||||
/// `headers: {"authorization": "Bearer ${client.accessToken}"}`
|
/// `headers: {"authorization": "Bearer ${client.accessToken}"}`
|
||||||
Future<Uri> getThumbnailUri(Client client,
|
Future<Uri> getThumbnailUri(
|
||||||
{num? width,
|
Client client, {
|
||||||
num? height,
|
num? width,
|
||||||
ThumbnailMethod? method = ThumbnailMethod.crop,
|
num? height,
|
||||||
bool? animated = false}) async {
|
ThumbnailMethod? method = ThumbnailMethod.crop,
|
||||||
|
bool? animated = false,
|
||||||
|
}) async {
|
||||||
if (!isScheme('mxc')) return Uri();
|
if (!isScheme('mxc')) return Uri();
|
||||||
final homeserver = client.homeserver;
|
final homeserver = client.homeserver;
|
||||||
if (homeserver == null) {
|
if (homeserver == null) {
|
||||||
|
|
@ -97,7 +99,8 @@ extension MxcUriExtension on Uri {
|
||||||
Uri getDownloadLink(Client matrix) => isScheme('mxc')
|
Uri getDownloadLink(Client matrix) => isScheme('mxc')
|
||||||
? matrix.homeserver != null
|
? matrix.homeserver != null
|
||||||
? matrix.homeserver?.resolve(
|
? matrix.homeserver?.resolve(
|
||||||
'_matrix/media/v3/download/$host${hasPort ? ':$port' : ''}$path') ??
|
'_matrix/media/v3/download/$host${hasPort ? ':$port' : ''}$path',
|
||||||
|
) ??
|
||||||
Uri()
|
Uri()
|
||||||
: Uri()
|
: Uri()
|
||||||
: Uri();
|
: Uri();
|
||||||
|
|
@ -108,11 +111,13 @@ extension MxcUriExtension on Uri {
|
||||||
/// If `animated` (default false) is set to true, an animated thumbnail is requested
|
/// 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.
|
/// as per MSC2705. Thumbnails only animate if the media repository supports that.
|
||||||
@Deprecated('Use `getThumbnailUri()` instead')
|
@Deprecated('Use `getThumbnailUri()` instead')
|
||||||
Uri getThumbnail(Client matrix,
|
Uri getThumbnail(
|
||||||
{num? width,
|
Client matrix, {
|
||||||
num? height,
|
num? width,
|
||||||
ThumbnailMethod? method = ThumbnailMethod.crop,
|
num? height,
|
||||||
bool? animated = false}) {
|
ThumbnailMethod? method = ThumbnailMethod.crop,
|
||||||
|
bool? animated = false,
|
||||||
|
}) {
|
||||||
if (!isScheme('mxc')) return Uri();
|
if (!isScheme('mxc')) return Uri();
|
||||||
final homeserver = matrix.homeserver;
|
final homeserver = matrix.homeserver;
|
||||||
if (homeserver == null) {
|
if (homeserver == null) {
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,10 @@ class NativeImplementationsWebWorker extends NativeImplementations {
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
if (!retryInDummy) {
|
if (!retryInDummy) {
|
||||||
Logs().e(
|
Logs().e(
|
||||||
'Web worker computation error. Ignoring and returning null', e, s);
|
'Web worker computation error. Ignoring and returning null',
|
||||||
|
e,
|
||||||
|
s,
|
||||||
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
Logs().e('Web worker computation error. Fallback to main thread', e, s);
|
Logs().e('Web worker computation error. Fallback to main thread', e, s);
|
||||||
|
|
@ -94,7 +97,10 @@ class NativeImplementationsWebWorker extends NativeImplementations {
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
if (!retryInDummy) {
|
if (!retryInDummy) {
|
||||||
Logs().e(
|
Logs().e(
|
||||||
'Web worker computation error. Ignoring and returning null', e, s);
|
'Web worker computation error. Ignoring and returning null',
|
||||||
|
e,
|
||||||
|
s,
|
||||||
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
Logs().e('Web worker computation error. Fallback to main thread', e, s);
|
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]
|
/// converts a stringifyed, obfuscated [StackTrace] into a [StackTrace]
|
||||||
typedef WebWorkerStackTraceCallback = FutureOr<StackTrace> Function(
|
typedef WebWorkerStackTraceCallback = FutureOr<StackTrace> Function(
|
||||||
String obfuscatedStackTrace);
|
String obfuscatedStackTrace,
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -33,4 +33,5 @@ class WebWorkerError extends Error {
|
||||||
|
|
||||||
/// converts a stringifyed, obfuscated [StackTrace] into a [StackTrace]
|
/// converts a stringifyed, obfuscated [StackTrace] into a [StackTrace]
|
||||||
typedef WebWorkerStackTraceCallback = FutureOr<StackTrace> Function(
|
typedef WebWorkerStackTraceCallback = FutureOr<StackTrace> Function(
|
||||||
String obfuscatedStackTrace);
|
String obfuscatedStackTrace,
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -46,14 +46,18 @@ Future<void> startWebWorker() async {
|
||||||
switch (operation.name) {
|
switch (operation.name) {
|
||||||
case WebWorkerOperations.shrinkImage:
|
case WebWorkerOperations.shrinkImage:
|
||||||
final result = MatrixImageFile.resizeImplementation(
|
final result = MatrixImageFile.resizeImplementation(
|
||||||
MatrixImageFileResizeArguments.fromJson(
|
MatrixImageFileResizeArguments.fromJson(
|
||||||
Map.from(operation.data as Map)));
|
Map.from(operation.data as Map),
|
||||||
|
),
|
||||||
|
);
|
||||||
sendResponse(operation.label as double, result?.toJson());
|
sendResponse(operation.label as double, result?.toJson());
|
||||||
break;
|
break;
|
||||||
case WebWorkerOperations.calcImageMetadata:
|
case WebWorkerOperations.calcImageMetadata:
|
||||||
final result = MatrixImageFile.calcMetadataImplementation(
|
final result = MatrixImageFile.calcMetadataImplementation(
|
||||||
Uint8List.fromList(
|
Uint8List.fromList(
|
||||||
(operation.data as JsArray).whereType<int>().toList()));
|
(operation.data as JsArray).whereType<int>().toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
sendResponse(operation.label as double, result?.toJson());
|
sendResponse(operation.label as double, result?.toJson());
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,8 @@ abstract class CallBackend {
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
throw MatrixSDKVoipException(
|
throw MatrixSDKVoipException(
|
||||||
'Invalid type: $type in CallBackend.fromJson');
|
'Invalid type: $type in CallBackend.fromJson',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,9 @@ class LiveKitBackend extends CallBackend {
|
||||||
///
|
///
|
||||||
/// also does the sending for you
|
/// also does the sending for you
|
||||||
Future<void> _makeNewSenderKey(
|
Future<void> _makeNewSenderKey(
|
||||||
GroupCallSession groupCall, bool delayBeforeUsingKeyOurself) async {
|
GroupCallSession groupCall,
|
||||||
|
bool delayBeforeUsingKeyOurself,
|
||||||
|
) async {
|
||||||
final key = secureRandomBytes(32);
|
final key = secureRandomBytes(32);
|
||||||
final keyIndex = _getNewEncryptionKeyIndex();
|
final keyIndex = _getNewEncryptionKeyIndex();
|
||||||
Logs().i('[VOIP E2EE] Generated new key $key at index $keyIndex');
|
Logs().i('[VOIP E2EE] Generated new key $key at index $keyIndex');
|
||||||
|
|
@ -99,7 +101,8 @@ class LiveKitBackend extends CallBackend {
|
||||||
|
|
||||||
if (keyProvider == null) {
|
if (keyProvider == null) {
|
||||||
throw MatrixSDKVoipException(
|
throw MatrixSDKVoipException(
|
||||||
'_ratchetKey called but KeyProvider was null');
|
'_ratchetKey called but KeyProvider was null',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final myKeys = _encryptionKeysMap[groupCall.localParticipant];
|
final myKeys = _encryptionKeysMap[groupCall.localParticipant];
|
||||||
|
|
@ -120,7 +123,8 @@ class LiveKitBackend extends CallBackend {
|
||||||
}
|
}
|
||||||
|
|
||||||
Logs().i(
|
Logs().i(
|
||||||
'[VOIP E2EE] Ratched latest key to $ratchetedKey at idx $latestLocalKeyIndex');
|
'[VOIP E2EE] Ratched latest key to $ratchetedKey at idx $latestLocalKeyIndex',
|
||||||
|
);
|
||||||
|
|
||||||
await _setEncryptionKey(
|
await _setEncryptionKey(
|
||||||
groupCall,
|
groupCall,
|
||||||
|
|
@ -179,9 +183,13 @@ class LiveKitBackend extends CallBackend {
|
||||||
// stil decrypt everything
|
// stil decrypt everything
|
||||||
final useKeyTimeout = Future.delayed(useKeyDelay, () async {
|
final useKeyTimeout = Future.delayed(useKeyDelay, () async {
|
||||||
Logs().i(
|
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(
|
await groupCall.voip.delegate.keyProvider?.onSetEncryptionKey(
|
||||||
participant, encryptionKeyBin, encryptionKeyIndex);
|
participant,
|
||||||
|
encryptionKeyBin,
|
||||||
|
encryptionKeyIndex,
|
||||||
|
);
|
||||||
if (participant.isLocal) {
|
if (participant.isLocal) {
|
||||||
_currentLocalKeyIndex = encryptionKeyIndex;
|
_currentLocalKeyIndex = encryptionKeyIndex;
|
||||||
}
|
}
|
||||||
|
|
@ -189,9 +197,13 @@ class LiveKitBackend extends CallBackend {
|
||||||
_setNewKeyTimeouts.add(useKeyTimeout);
|
_setNewKeyTimeouts.add(useKeyTimeout);
|
||||||
} else {
|
} else {
|
||||||
Logs().i(
|
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(
|
await groupCall.voip.delegate.keyProvider?.onSetEncryptionKey(
|
||||||
participant, encryptionKeyBin, encryptionKeyIndex);
|
participant,
|
||||||
|
encryptionKeyBin,
|
||||||
|
encryptionKeyIndex,
|
||||||
|
);
|
||||||
if (participant.isLocal) {
|
if (participant.isLocal) {
|
||||||
_currentLocalKeyIndex = encryptionKeyIndex;
|
_currentLocalKeyIndex = encryptionKeyIndex;
|
||||||
}
|
}
|
||||||
|
|
@ -214,7 +226,8 @@ class LiveKitBackend extends CallBackend {
|
||||||
|
|
||||||
if (myKeys == null || myLatestKey == null) {
|
if (myKeys == null || myLatestKey == null) {
|
||||||
Logs().w(
|
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 _makeNewSenderKey(groupCall, false);
|
||||||
await _sendEncryptionKeysEvent(
|
await _sendEncryptionKeysEvent(
|
||||||
groupCall,
|
groupCall,
|
||||||
|
|
@ -261,7 +274,8 @@ class LiveKitBackend extends CallBackend {
|
||||||
) async {
|
) async {
|
||||||
if (remoteParticipants.isEmpty) return;
|
if (remoteParticipants.isEmpty) return;
|
||||||
Logs().v(
|
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 =
|
final txid =
|
||||||
VoIP.customTxid ?? groupCall.client.generateUniqueTransactionId();
|
VoIP.customTxid ?? groupCall.client.generateUniqueTransactionId();
|
||||||
final mustEncrypt =
|
final mustEncrypt =
|
||||||
|
|
@ -284,7 +298,7 @@ class LiveKitBackend extends CallBackend {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
unencryptedDataToSend.addAll({
|
unencryptedDataToSend.addAll({
|
||||||
participant.userId: {participant.deviceId!: data}
|
participant.userId: {participant.deviceId!: data},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -352,11 +366,13 @@ class LiveKitBackend extends CallBackend {
|
||||||
|
|
||||||
if (keyContent.keys.isEmpty) {
|
if (keyContent.keys.isEmpty) {
|
||||||
Logs().w(
|
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;
|
return;
|
||||||
} else {
|
} else {
|
||||||
Logs().i(
|
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) {
|
for (final key in keyContent.keys) {
|
||||||
|
|
@ -400,7 +416,8 @@ class LiveKitBackend extends CallBackend {
|
||||||
)
|
)
|
||||||
.isNotEmpty) {
|
.isNotEmpty) {
|
||||||
Logs().d(
|
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(
|
await _sendEncryptionKeysEvent(
|
||||||
groupCall,
|
groupCall,
|
||||||
_latestLocalKeyIndex,
|
_latestLocalKeyIndex,
|
||||||
|
|
@ -409,7 +426,7 @@ class LiveKitBackend extends CallBackend {
|
||||||
groupCall.voip,
|
groupCall.voip,
|
||||||
userId: userId,
|
userId: userId,
|
||||||
deviceId: deviceId,
|
deviceId: deviceId,
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -470,8 +487,10 @@ class LiveKitBackend extends CallBackend {
|
||||||
|
|
||||||
/// get everything else from your livekit sdk in your client
|
/// get everything else from your livekit sdk in your client
|
||||||
@override
|
@override
|
||||||
Future<WrappedMediaStream?> initLocalStream(GroupCallSession groupCall,
|
Future<WrappedMediaStream?> initLocalStream(
|
||||||
{WrappedMediaStream? stream}) async {
|
GroupCallSession groupCall, {
|
||||||
|
WrappedMediaStream? stream,
|
||||||
|
}) async {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -506,25 +525,35 @@ class LiveKitBackend extends CallBackend {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> setDeviceMuted(
|
Future<void> setDeviceMuted(
|
||||||
GroupCallSession groupCall, bool muted, MediaInputKind kind) async {
|
GroupCallSession groupCall,
|
||||||
|
bool muted,
|
||||||
|
MediaInputKind kind,
|
||||||
|
) async {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> setScreensharingEnabled(GroupCallSession groupCall, bool enabled,
|
Future<void> setScreensharingEnabled(
|
||||||
String desktopCapturerSourceId) async {
|
GroupCallSession groupCall,
|
||||||
|
bool enabled,
|
||||||
|
String desktopCapturerSourceId,
|
||||||
|
) async {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> setupP2PCallWithNewMember(GroupCallSession groupCall,
|
Future<void> setupP2PCallWithNewMember(
|
||||||
CallParticipant rp, CallMembership mem) async {
|
GroupCallSession groupCall,
|
||||||
|
CallParticipant rp,
|
||||||
|
CallMembership mem,
|
||||||
|
) async {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> setupP2PCallsWithExistingMembers(
|
Future<void> setupP2PCallsWithExistingMembers(
|
||||||
GroupCallSession groupCall) async {
|
GroupCallSession groupCall,
|
||||||
|
) async {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,9 @@ class MeshBackend extends CallBackend {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<MediaStream> _getUserMedia(
|
Future<MediaStream> _getUserMedia(
|
||||||
GroupCallSession groupCall, CallType type) async {
|
GroupCallSession groupCall,
|
||||||
|
CallType type,
|
||||||
|
) async {
|
||||||
final mediaConstraints = {
|
final mediaConstraints = {
|
||||||
'audio': UserMediaConstraints.micMediaConstraints,
|
'audio': UserMediaConstraints.micMediaConstraints,
|
||||||
'video': type == CallType.kVideo
|
'video': type == CallType.kVideo
|
||||||
|
|
@ -92,15 +94,19 @@ class MeshBackend extends CallBackend {
|
||||||
}
|
}
|
||||||
|
|
||||||
CallSession? _getCallForParticipant(
|
CallSession? _getCallForParticipant(
|
||||||
GroupCallSession groupCall, CallParticipant participant) {
|
GroupCallSession groupCall,
|
||||||
return _callSessions.singleWhereOrNull((call) =>
|
CallParticipant participant,
|
||||||
call.groupCallId == groupCall.groupCallId &&
|
) {
|
||||||
CallParticipant(
|
return _callSessions.singleWhereOrNull(
|
||||||
groupCall.voip,
|
(call) =>
|
||||||
userId: call.remoteUserId!,
|
call.groupCallId == groupCall.groupCallId &&
|
||||||
deviceId: call.remoteDeviceId,
|
CallParticipant(
|
||||||
) ==
|
groupCall.voip,
|
||||||
participant);
|
userId: call.remoteUserId!,
|
||||||
|
deviceId: call.remoteDeviceId,
|
||||||
|
) ==
|
||||||
|
participant,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _addCall(GroupCallSession groupCall, CallSession call) async {
|
Future<void> _addCall(GroupCallSession groupCall, CallSession call) async {
|
||||||
|
|
@ -113,12 +119,15 @@ class MeshBackend extends CallBackend {
|
||||||
Future<void> _initCall(GroupCallSession groupCall, CallSession call) async {
|
Future<void> _initCall(GroupCallSession groupCall, CallSession call) async {
|
||||||
if (call.remoteUserId == null) {
|
if (call.remoteUserId == null) {
|
||||||
throw MatrixSDKVoipException(
|
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(
|
||||||
await _onCallStateChanged(call, event);
|
((event) async {
|
||||||
}));
|
await _onCallStateChanged(call, event);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
call.onCallReplaced.stream.listen((CallSession newCall) async {
|
call.onCallReplaced.stream.listen((CallSession newCall) async {
|
||||||
await _replaceCall(groupCall, call, newCall);
|
await _replaceCall(groupCall, call, newCall);
|
||||||
|
|
@ -168,8 +177,11 @@ class MeshBackend extends CallBackend {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Removes a peer call from group calls.
|
/// Removes a peer call from group calls.
|
||||||
Future<void> _removeCall(GroupCallSession groupCall, CallSession call,
|
Future<void> _removeCall(
|
||||||
CallErrorCode hangupReason) async {
|
GroupCallSession groupCall,
|
||||||
|
CallSession call,
|
||||||
|
CallErrorCode hangupReason,
|
||||||
|
) async {
|
||||||
await _disposeCall(groupCall, call, hangupReason);
|
await _disposeCall(groupCall, call, hangupReason);
|
||||||
|
|
||||||
_callSessions.removeWhere((element) => call.callId == element.callId);
|
_callSessions.removeWhere((element) => call.callId == element.callId);
|
||||||
|
|
@ -177,11 +189,15 @@ class MeshBackend extends CallBackend {
|
||||||
groupCall.onGroupCallEvent.add(GroupCallStateChange.callsChanged);
|
groupCall.onGroupCallEvent.add(GroupCallStateChange.callsChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _disposeCall(GroupCallSession groupCall, CallSession call,
|
Future<void> _disposeCall(
|
||||||
CallErrorCode hangupReason) async {
|
GroupCallSession groupCall,
|
||||||
|
CallSession call,
|
||||||
|
CallErrorCode hangupReason,
|
||||||
|
) async {
|
||||||
if (call.remoteUserId == null) {
|
if (call.remoteUserId == null) {
|
||||||
throw MatrixSDKVoipException(
|
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) {
|
if (call.hangupReason == CallErrorCode.replaced) {
|
||||||
|
|
@ -220,10 +236,13 @@ class MeshBackend extends CallBackend {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onStreamsChanged(
|
Future<void> _onStreamsChanged(
|
||||||
GroupCallSession groupCall, CallSession call) async {
|
GroupCallSession groupCall,
|
||||||
|
CallSession call,
|
||||||
|
) async {
|
||||||
if (call.remoteUserId == null) {
|
if (call.remoteUserId == null) {
|
||||||
throw MatrixSDKVoipException(
|
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(
|
final currentUserMediaStream = _getUserMediaStreamByParticipantId(
|
||||||
|
|
@ -243,19 +262,23 @@ class MeshBackend extends CallBackend {
|
||||||
} else if (currentUserMediaStream != null &&
|
} else if (currentUserMediaStream != null &&
|
||||||
remoteUsermediaStream != null) {
|
remoteUsermediaStream != null) {
|
||||||
await _replaceUserMediaStream(
|
await _replaceUserMediaStream(
|
||||||
groupCall, currentUserMediaStream, remoteUsermediaStream);
|
groupCall,
|
||||||
|
currentUserMediaStream,
|
||||||
|
remoteUsermediaStream,
|
||||||
|
);
|
||||||
} else if (currentUserMediaStream != null &&
|
} else if (currentUserMediaStream != null &&
|
||||||
remoteUsermediaStream == null) {
|
remoteUsermediaStream == null) {
|
||||||
await _removeUserMediaStream(groupCall, currentUserMediaStream);
|
await _removeUserMediaStream(groupCall, currentUserMediaStream);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final currentScreenshareStream =
|
final currentScreenshareStream = _getScreenshareStreamByParticipantId(
|
||||||
_getScreenshareStreamByParticipantId(CallParticipant(
|
CallParticipant(
|
||||||
groupCall.voip,
|
groupCall.voip,
|
||||||
userId: call.remoteUserId!,
|
userId: call.remoteUserId!,
|
||||||
deviceId: call.remoteDeviceId,
|
deviceId: call.remoteDeviceId,
|
||||||
).id);
|
).id,
|
||||||
|
);
|
||||||
final remoteScreensharingStream = call.remoteScreenSharingStream;
|
final remoteScreensharingStream = call.remoteScreenSharingStream;
|
||||||
final remoteScreenshareStreamChanged =
|
final remoteScreenshareStreamChanged =
|
||||||
remoteScreensharingStream != currentScreenshareStream;
|
remoteScreensharingStream != currentScreenshareStream;
|
||||||
|
|
@ -267,7 +290,10 @@ class MeshBackend extends CallBackend {
|
||||||
} else if (currentScreenshareStream != null &&
|
} else if (currentScreenshareStream != null &&
|
||||||
remoteScreensharingStream != null) {
|
remoteScreensharingStream != null) {
|
||||||
await _replaceScreenshareStream(
|
await _replaceScreenshareStream(
|
||||||
groupCall, currentScreenshareStream, remoteScreensharingStream);
|
groupCall,
|
||||||
|
currentScreenshareStream,
|
||||||
|
remoteScreensharingStream,
|
||||||
|
);
|
||||||
} else if (currentScreenshareStream != null &&
|
} else if (currentScreenshareStream != null &&
|
||||||
remoteScreensharingStream == null) {
|
remoteScreensharingStream == null) {
|
||||||
await _removeScreenshareStream(groupCall, currentScreenshareStream);
|
await _removeScreenshareStream(groupCall, currentScreenshareStream);
|
||||||
|
|
@ -302,9 +328,11 @@ class MeshBackend extends CallBackend {
|
||||||
|
|
||||||
// https://www.w3.org/TR/webrtc-stats/#summary
|
// https://www.w3.org/TR/webrtc-stats/#summary
|
||||||
final otherPartyAudioLevel = statsReport
|
final otherPartyAudioLevel = statsReport
|
||||||
.singleWhereOrNull((element) =>
|
.singleWhereOrNull(
|
||||||
element.type == 'inbound-rtp' &&
|
(element) =>
|
||||||
element.values['kind'] == 'audio')
|
element.type == 'inbound-rtp' &&
|
||||||
|
element.values['kind'] == 'audio',
|
||||||
|
)
|
||||||
?.values['audioLevel'];
|
?.values['audioLevel'];
|
||||||
if (otherPartyAudioLevel != null) {
|
if (otherPartyAudioLevel != null) {
|
||||||
_audioLevelsMap[stream.participant] = otherPartyAudioLevel;
|
_audioLevelsMap[stream.participant] = otherPartyAudioLevel;
|
||||||
|
|
@ -313,9 +341,11 @@ class MeshBackend extends CallBackend {
|
||||||
// https://www.w3.org/TR/webrtc-stats/#dom-rtcstatstype-media-source
|
// https://www.w3.org/TR/webrtc-stats/#dom-rtcstatstype-media-source
|
||||||
// firefox does not seem to have this though. Works on chrome and android
|
// firefox does not seem to have this though. Works on chrome and android
|
||||||
final ownAudioLevel = statsReport
|
final ownAudioLevel = statsReport
|
||||||
.singleWhereOrNull((element) =>
|
.singleWhereOrNull(
|
||||||
element.type == 'media-source' &&
|
(element) =>
|
||||||
element.values['kind'] == 'audio')
|
element.type == 'media-source' &&
|
||||||
|
element.values['kind'] == 'audio',
|
||||||
|
)
|
||||||
?.values['audioLevel'];
|
?.values['audioLevel'];
|
||||||
if (groupCall.localParticipant != null &&
|
if (groupCall.localParticipant != null &&
|
||||||
ownAudioLevel != null &&
|
ownAudioLevel != null &&
|
||||||
|
|
@ -345,7 +375,8 @@ class MeshBackend extends CallBackend {
|
||||||
}
|
}
|
||||||
|
|
||||||
WrappedMediaStream? _getScreenshareStreamByParticipantId(
|
WrappedMediaStream? _getScreenshareStreamByParticipantId(
|
||||||
String participantId) {
|
String participantId,
|
||||||
|
) {
|
||||||
final stream = _screenshareStreams
|
final stream = _screenshareStreams
|
||||||
.where((stream) => stream.participant.id == participantId);
|
.where((stream) => stream.participant.id == participantId);
|
||||||
if (stream.isNotEmpty) {
|
if (stream.isNotEmpty) {
|
||||||
|
|
@ -355,7 +386,9 @@ class MeshBackend extends CallBackend {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _addScreenshareStream(
|
void _addScreenshareStream(
|
||||||
GroupCallSession groupCall, WrappedMediaStream stream) {
|
GroupCallSession groupCall,
|
||||||
|
WrappedMediaStream stream,
|
||||||
|
) {
|
||||||
_screenshareStreams.add(stream);
|
_screenshareStreams.add(stream);
|
||||||
onStreamAdd.add(stream);
|
onStreamAdd.add(stream);
|
||||||
groupCall.onGroupCallEvent
|
groupCall.onGroupCallEvent
|
||||||
|
|
@ -368,11 +401,13 @@ class MeshBackend extends CallBackend {
|
||||||
WrappedMediaStream replacementStream,
|
WrappedMediaStream replacementStream,
|
||||||
) async {
|
) async {
|
||||||
final streamIndex = _screenshareStreams.indexWhere(
|
final streamIndex = _screenshareStreams.indexWhere(
|
||||||
(stream) => stream.participant.id == existingStream.participant.id);
|
(stream) => stream.participant.id == existingStream.participant.id,
|
||||||
|
);
|
||||||
|
|
||||||
if (streamIndex == -1) {
|
if (streamIndex == -1) {
|
||||||
throw MatrixSDKVoipException(
|
throw MatrixSDKVoipException(
|
||||||
'Couldn\'t find screenshare stream to replace');
|
'Couldn\'t find screenshare stream to replace',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_screenshareStreams.replaceRange(streamIndex, 1, [replacementStream]);
|
_screenshareStreams.replaceRange(streamIndex, 1, [replacementStream]);
|
||||||
|
|
@ -391,11 +426,13 @@ class MeshBackend extends CallBackend {
|
||||||
|
|
||||||
if (streamIndex == -1) {
|
if (streamIndex == -1) {
|
||||||
throw MatrixSDKVoipException(
|
throw MatrixSDKVoipException(
|
||||||
'Couldn\'t find screenshare stream to remove');
|
'Couldn\'t find screenshare stream to remove',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_screenshareStreams.removeWhere(
|
_screenshareStreams.removeWhere(
|
||||||
(element) => element.participant.id == stream.participant.id);
|
(element) => element.participant.id == stream.participant.id,
|
||||||
|
);
|
||||||
|
|
||||||
onStreamRemoved.add(stream);
|
onStreamRemoved.add(stream);
|
||||||
|
|
||||||
|
|
@ -449,11 +486,13 @@ class MeshBackend extends CallBackend {
|
||||||
WrappedMediaStream replacementStream,
|
WrappedMediaStream replacementStream,
|
||||||
) async {
|
) async {
|
||||||
final streamIndex = _userMediaStreams.indexWhere(
|
final streamIndex = _userMediaStreams.indexWhere(
|
||||||
(stream) => stream.participant.id == existingStream.participant.id);
|
(stream) => stream.participant.id == existingStream.participant.id,
|
||||||
|
);
|
||||||
|
|
||||||
if (streamIndex == -1) {
|
if (streamIndex == -1) {
|
||||||
throw MatrixSDKVoipException(
|
throw MatrixSDKVoipException(
|
||||||
'Couldn\'t find user media stream to replace');
|
'Couldn\'t find user media stream to replace',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_userMediaStreams.replaceRange(streamIndex, 1, [replacementStream]);
|
_userMediaStreams.replaceRange(streamIndex, 1, [replacementStream]);
|
||||||
|
|
@ -468,15 +507,18 @@ class MeshBackend extends CallBackend {
|
||||||
WrappedMediaStream stream,
|
WrappedMediaStream stream,
|
||||||
) async {
|
) async {
|
||||||
final streamIndex = _userMediaStreams.indexWhere(
|
final streamIndex = _userMediaStreams.indexWhere(
|
||||||
(element) => element.participant.id == stream.participant.id);
|
(element) => element.participant.id == stream.participant.id,
|
||||||
|
);
|
||||||
|
|
||||||
if (streamIndex == -1) {
|
if (streamIndex == -1) {
|
||||||
throw MatrixSDKVoipException(
|
throw MatrixSDKVoipException(
|
||||||
'Couldn\'t find user media stream to remove');
|
'Couldn\'t find user media stream to remove',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_userMediaStreams.removeWhere(
|
_userMediaStreams.removeWhere(
|
||||||
(element) => element.participant.id == stream.participant.id);
|
(element) => element.participant.id == stream.participant.id,
|
||||||
|
);
|
||||||
_audioLevelsMap.remove(stream.participant);
|
_audioLevelsMap.remove(stream.participant);
|
||||||
onStreamRemoved.add(stream);
|
onStreamRemoved.add(stream);
|
||||||
|
|
||||||
|
|
@ -527,11 +569,14 @@ class MeshBackend extends CallBackend {
|
||||||
/// This allows you to configure the camera before joining the call without
|
/// This allows you to configure the camera before joining the call without
|
||||||
/// having to reopen the stream and possibly losing settings.
|
/// having to reopen the stream and possibly losing settings.
|
||||||
@override
|
@override
|
||||||
Future<WrappedMediaStream?> initLocalStream(GroupCallSession groupCall,
|
Future<WrappedMediaStream?> initLocalStream(
|
||||||
{WrappedMediaStream? stream}) async {
|
GroupCallSession groupCall, {
|
||||||
|
WrappedMediaStream? stream,
|
||||||
|
}) async {
|
||||||
if (groupCall.state != GroupCallState.localCallFeedUninitialized) {
|
if (groupCall.state != GroupCallState.localCallFeedUninitialized) {
|
||||||
throw MatrixSDKVoipException(
|
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);
|
groupCall.setState(GroupCallState.initializingLocalCallFeed);
|
||||||
|
|
@ -575,7 +620,10 @@ class MeshBackend extends CallBackend {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> setDeviceMuted(
|
Future<void> setDeviceMuted(
|
||||||
GroupCallSession groupCall, bool muted, MediaInputKind kind) async {
|
GroupCallSession groupCall,
|
||||||
|
bool muted,
|
||||||
|
MediaInputKind kind,
|
||||||
|
) async {
|
||||||
if (!await hasMediaDevice(groupCall.voip.delegate, kind)) {
|
if (!await hasMediaDevice(groupCall.voip.delegate, kind)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -585,7 +633,9 @@ class MeshBackend extends CallBackend {
|
||||||
case MediaInputKind.audioinput:
|
case MediaInputKind.audioinput:
|
||||||
localUserMediaStream!.setAudioMuted(muted);
|
localUserMediaStream!.setAudioMuted(muted);
|
||||||
setTracksEnabled(
|
setTracksEnabled(
|
||||||
localUserMediaStream!.stream!.getAudioTracks(), !muted);
|
localUserMediaStream!.stream!.getAudioTracks(),
|
||||||
|
!muted,
|
||||||
|
);
|
||||||
for (final call in _callSessions) {
|
for (final call in _callSessions) {
|
||||||
await call.setMicrophoneMuted(muted);
|
await call.setMicrophoneMuted(muted);
|
||||||
}
|
}
|
||||||
|
|
@ -593,7 +643,9 @@ class MeshBackend extends CallBackend {
|
||||||
case MediaInputKind.videoinput:
|
case MediaInputKind.videoinput:
|
||||||
localUserMediaStream!.setVideoMuted(muted);
|
localUserMediaStream!.setVideoMuted(muted);
|
||||||
setTracksEnabled(
|
setTracksEnabled(
|
||||||
localUserMediaStream!.stream!.getVideoTracks(), !muted);
|
localUserMediaStream!.stream!.getVideoTracks(),
|
||||||
|
!muted,
|
||||||
|
);
|
||||||
for (final call in _callSessions) {
|
for (final call in _callSessions) {
|
||||||
await call.setLocalVideoMuted(muted);
|
await call.setLocalVideoMuted(muted);
|
||||||
}
|
}
|
||||||
|
|
@ -607,7 +659,9 @@ class MeshBackend extends CallBackend {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onIncomingCall(
|
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.
|
// The incoming calls may be for another room, which we will ignore.
|
||||||
if (newCall.room.id != groupCall.room.id) {
|
if (newCall.room.id != groupCall.room.id) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -621,7 +675,8 @@ class MeshBackend extends CallBackend {
|
||||||
if (newCall.groupCallId == null ||
|
if (newCall.groupCallId == null ||
|
||||||
newCall.groupCallId != groupCall.groupCallId) {
|
newCall.groupCallId != groupCall.groupCallId) {
|
||||||
Logs().v(
|
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();
|
await newCall.reject();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -640,7 +695,8 @@ class MeshBackend extends CallBackend {
|
||||||
}
|
}
|
||||||
|
|
||||||
Logs().v(
|
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.
|
// Check if the user calling has an existing call and use this call instead.
|
||||||
if (existingCall != null) {
|
if (existingCall != null) {
|
||||||
|
|
@ -674,7 +730,8 @@ class MeshBackend extends CallBackend {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
Logs().v(
|
Logs().v(
|
||||||
'Screensharing permissions granted. Setting screensharing enabled on all calls');
|
'Screensharing permissions granted. Setting screensharing enabled on all calls',
|
||||||
|
);
|
||||||
_localScreenshareStream = WrappedMediaStream(
|
_localScreenshareStream = WrappedMediaStream(
|
||||||
stream: stream,
|
stream: stream,
|
||||||
participant: groupCall.localParticipant!,
|
participant: groupCall.localParticipant!,
|
||||||
|
|
@ -693,8 +750,9 @@ class MeshBackend extends CallBackend {
|
||||||
.add(GroupCallStateChange.localScreenshareStateChanged);
|
.add(GroupCallStateChange.localScreenshareStateChanged);
|
||||||
for (final call in _callSessions) {
|
for (final call in _callSessions) {
|
||||||
await call.addLocalStream(
|
await call.addLocalStream(
|
||||||
await localScreenshareStream!.stream!.clone(),
|
await localScreenshareStream!.stream!.clone(),
|
||||||
localScreenshareStream!.purpose);
|
localScreenshareStream!.purpose,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await groupCall.sendMemberStateEvent();
|
await groupCall.sendMemberStateEvent();
|
||||||
|
|
@ -766,7 +824,8 @@ class MeshBackend extends CallBackend {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> setupP2PCallsWithExistingMembers(
|
Future<void> setupP2PCallsWithExistingMembers(
|
||||||
GroupCallSession groupCall) async {
|
GroupCallSession groupCall,
|
||||||
|
) async {
|
||||||
for (final call in _callSessions) {
|
for (final call in _callSessions) {
|
||||||
await _onIncomingCall(groupCall, call);
|
await _onIncomingCall(groupCall, call);
|
||||||
}
|
}
|
||||||
|
|
@ -790,7 +849,8 @@ class MeshBackend extends CallBackend {
|
||||||
await existingCall.hangup(reason: CallErrorCode.unknownError);
|
await existingCall.hangup(reason: CallErrorCode.unknownError);
|
||||||
} else {
|
} else {
|
||||||
Logs().e(
|
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;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -824,10 +884,14 @@ class MeshBackend extends CallBackend {
|
||||||
// party id set to when answered
|
// party id set to when answered
|
||||||
newCall.remoteSessionId = mem.membershipId;
|
newCall.remoteSessionId = mem.membershipId;
|
||||||
|
|
||||||
await newCall.placeCallWithStreams(_getLocalStreams(),
|
await newCall.placeCallWithStreams(
|
||||||
requestScreenSharing: mem.feeds?.any((element) =>
|
_getLocalStreams(),
|
||||||
element['purpose'] == SDPStreamMetadataPurpose.Screenshare) ??
|
requestScreenSharing: mem.feeds?.any(
|
||||||
false);
|
(element) =>
|
||||||
|
element['purpose'] == SDPStreamMetadataPurpose.Screenshare,
|
||||||
|
) ??
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
await _addCall(groupCall, newCall);
|
await _addCall(groupCall, newCall);
|
||||||
}
|
}
|
||||||
|
|
@ -835,9 +899,11 @@ class MeshBackend extends CallBackend {
|
||||||
@override
|
@override
|
||||||
List<Map<String, String>>? getCurrentFeeds() {
|
List<Map<String, String>>? getCurrentFeeds() {
|
||||||
return _getLocalStreams()
|
return _getLocalStreams()
|
||||||
.map((feed) => ({
|
.map(
|
||||||
'purpose': feed.purpose,
|
(feed) => ({
|
||||||
}))
|
'purpose': feed.purpose,
|
||||||
|
}),
|
||||||
|
)
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -849,32 +915,46 @@ class MeshBackend extends CallBackend {
|
||||||
|
|
||||||
/// get everything is livekit specific mesh calls shouldn't be affected by these
|
/// get everything is livekit specific mesh calls shouldn't be affected by these
|
||||||
@override
|
@override
|
||||||
Future<void> onCallEncryption(GroupCallSession groupCall, String userId,
|
Future<void> onCallEncryption(
|
||||||
String deviceId, Map<String, dynamic> content) async {
|
GroupCallSession groupCall,
|
||||||
|
String userId,
|
||||||
|
String deviceId,
|
||||||
|
Map<String, dynamic> content,
|
||||||
|
) async {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> onCallEncryptionKeyRequest(GroupCallSession groupCall,
|
Future<void> onCallEncryptionKeyRequest(
|
||||||
String userId, String deviceId, Map<String, dynamic> content) async {
|
GroupCallSession groupCall,
|
||||||
|
String userId,
|
||||||
|
String deviceId,
|
||||||
|
Map<String, dynamic> content,
|
||||||
|
) async {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> onLeftParticipant(
|
Future<void> onLeftParticipant(
|
||||||
GroupCallSession groupCall, List<CallParticipant> anyLeft) async {
|
GroupCallSession groupCall,
|
||||||
|
List<CallParticipant> anyLeft,
|
||||||
|
) async {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> onNewParticipant(
|
Future<void> onNewParticipant(
|
||||||
GroupCallSession groupCall, List<CallParticipant> anyJoined) async {
|
GroupCallSession groupCall,
|
||||||
|
List<CallParticipant> anyJoined,
|
||||||
|
) async {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> requestEncrytionKey(GroupCallSession groupCall,
|
Future<void> requestEncrytionKey(
|
||||||
List<CallParticipant> remoteParticipants) async {
|
GroupCallSession groupCall,
|
||||||
|
List<CallParticipant> remoteParticipants,
|
||||||
|
) async {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -147,7 +147,8 @@ class CallSession {
|
||||||
|
|
||||||
WrappedMediaStream? get localUserMediaStream {
|
WrappedMediaStream? get localUserMediaStream {
|
||||||
final stream = getLocalStreams.where(
|
final stream = getLocalStreams.where(
|
||||||
(element) => element.purpose == SDPStreamMetadataPurpose.Usermedia);
|
(element) => element.purpose == SDPStreamMetadataPurpose.Usermedia,
|
||||||
|
);
|
||||||
if (stream.isNotEmpty) {
|
if (stream.isNotEmpty) {
|
||||||
return stream.first;
|
return stream.first;
|
||||||
}
|
}
|
||||||
|
|
@ -156,7 +157,8 @@ class CallSession {
|
||||||
|
|
||||||
WrappedMediaStream? get localScreenSharingStream {
|
WrappedMediaStream? get localScreenSharingStream {
|
||||||
final stream = getLocalStreams.where(
|
final stream = getLocalStreams.where(
|
||||||
(element) => element.purpose == SDPStreamMetadataPurpose.Screenshare);
|
(element) => element.purpose == SDPStreamMetadataPurpose.Screenshare,
|
||||||
|
);
|
||||||
if (stream.isNotEmpty) {
|
if (stream.isNotEmpty) {
|
||||||
return stream.first;
|
return stream.first;
|
||||||
}
|
}
|
||||||
|
|
@ -165,7 +167,8 @@ class CallSession {
|
||||||
|
|
||||||
WrappedMediaStream? get remoteUserMediaStream {
|
WrappedMediaStream? get remoteUserMediaStream {
|
||||||
final stream = getRemoteStreams.where(
|
final stream = getRemoteStreams.where(
|
||||||
(element) => element.purpose == SDPStreamMetadataPurpose.Usermedia);
|
(element) => element.purpose == SDPStreamMetadataPurpose.Usermedia,
|
||||||
|
);
|
||||||
if (stream.isNotEmpty) {
|
if (stream.isNotEmpty) {
|
||||||
return stream.first;
|
return stream.first;
|
||||||
}
|
}
|
||||||
|
|
@ -174,7 +177,8 @@ class CallSession {
|
||||||
|
|
||||||
WrappedMediaStream? get remoteScreenSharingStream {
|
WrappedMediaStream? get remoteScreenSharingStream {
|
||||||
final stream = getRemoteStreams.where(
|
final stream = getRemoteStreams.where(
|
||||||
(element) => element.purpose == SDPStreamMetadataPurpose.Screenshare);
|
(element) => element.purpose == SDPStreamMetadataPurpose.Screenshare,
|
||||||
|
);
|
||||||
if (stream.isNotEmpty) {
|
if (stream.isNotEmpty) {
|
||||||
return stream.first;
|
return stream.first;
|
||||||
}
|
}
|
||||||
|
|
@ -190,8 +194,10 @@ class CallSession {
|
||||||
|
|
||||||
// check if we have a video track locally and have transceivers setup correctly.
|
// check if we have a video track locally and have transceivers setup correctly.
|
||||||
return localUserMediaVideoTrack != null &&
|
return localUserMediaVideoTrack != null &&
|
||||||
transceivers.singleWhereOrNull((transceiver) =>
|
transceivers.singleWhereOrNull(
|
||||||
transceiver.sender.track?.id == localUserMediaVideoTrack.id) !=
|
(transceiver) =>
|
||||||
|
transceiver.sender.track?.id == localUserMediaVideoTrack.id,
|
||||||
|
) !=
|
||||||
null;
|
null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -209,8 +215,13 @@ class CallSession {
|
||||||
}
|
}
|
||||||
|
|
||||||
// incoming call
|
// incoming call
|
||||||
Future<void> initWithInvite(CallType type, RTCSessionDescription offer,
|
Future<void> initWithInvite(
|
||||||
SDPStreamMetadata? metadata, int lifetime, bool isGroupCall) async {
|
CallType type,
|
||||||
|
RTCSessionDescription offer,
|
||||||
|
SDPStreamMetadata? metadata,
|
||||||
|
int lifetime,
|
||||||
|
bool isGroupCall,
|
||||||
|
) async {
|
||||||
if (!isGroupCall) {
|
if (!isGroupCall) {
|
||||||
// glare fixes
|
// glare fixes
|
||||||
final prevCallId = voip.incomingCallRoomId[room.id];
|
final prevCallId = voip.incomingCallRoomId[room.id];
|
||||||
|
|
@ -223,13 +234,15 @@ class CallSession {
|
||||||
Logs().d('[glare] invite or answer sent, lex compare now');
|
Logs().d('[glare] invite or answer sent, lex compare now');
|
||||||
if (callId.compareTo(prevCall.callId) > 0) {
|
if (callId.compareTo(prevCall.callId) > 0) {
|
||||||
Logs().d(
|
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);
|
await hangup(reason: CallErrorCode.unknownError);
|
||||||
voip.currentCID =
|
voip.currentCID =
|
||||||
VoipId(roomId: room.id, callId: prevCall.callId);
|
VoipId(roomId: room.id, callId: prevCall.callId);
|
||||||
} else {
|
} else {
|
||||||
Logs().d(
|
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
|
/// These fixes do not work all the time because sometimes the code
|
||||||
/// is at an unrecoverable stage (invite already sent when we were
|
/// is at an unrecoverable stage (invite already sent when we were
|
||||||
|
|
@ -240,7 +253,8 @@ class CallSession {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Logs().d(
|
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);
|
await prevCall.hangup(reason: CallErrorCode.unknownError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -308,13 +322,21 @@ class CallSession {
|
||||||
|
|
||||||
final metadata = SDPStreamMetadata({
|
final metadata = SDPStreamMetadata({
|
||||||
localUserMediaStream!.stream!.id: SDPStreamPurpose(
|
localUserMediaStream!.stream!.id: SDPStreamPurpose(
|
||||||
purpose: SDPStreamMetadataPurpose.Usermedia,
|
purpose: SDPStreamMetadataPurpose.Usermedia,
|
||||||
audio_muted: localUserMediaStream!.stream!.getAudioTracks().isEmpty,
|
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,
|
final res = await sendAnswerCall(
|
||||||
type: answer.type!, capabilities: callCapabilities, metadata: metadata);
|
room,
|
||||||
|
callId,
|
||||||
|
answer.sdp!,
|
||||||
|
localPartyId,
|
||||||
|
type: answer.type!,
|
||||||
|
capabilities: callCapabilities,
|
||||||
|
metadata: metadata,
|
||||||
|
);
|
||||||
Logs().v('[VOIP] answer res => $res');
|
Logs().v('[VOIP] answer res => $res');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -360,9 +382,9 @@ class CallSession {
|
||||||
|
|
||||||
if (requestScreenSharing) {
|
if (requestScreenSharing) {
|
||||||
await pc!.addTransceiver(
|
await pc!.addTransceiver(
|
||||||
kind: RTCRtpMediaType.RTCRtpMediaTypeVideo,
|
kind: RTCRtpMediaType.RTCRtpMediaTypeVideo,
|
||||||
init:
|
init: RTCRtpTransceiverInit(direction: TransceiverDirection.RecvOnly),
|
||||||
RTCRtpTransceiverInit(direction: TransceiverDirection.RecvOnly));
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
setCallState(CallState.kCreateOffer);
|
setCallState(CallState.kCreateOffer);
|
||||||
|
|
@ -372,7 +394,9 @@ class CallSession {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> onAnswerReceived(
|
Future<void> onAnswerReceived(
|
||||||
RTCSessionDescription answer, SDPStreamMetadata? metadata) async {
|
RTCSessionDescription answer,
|
||||||
|
SDPStreamMetadata? metadata,
|
||||||
|
) async {
|
||||||
if (metadata != null) {
|
if (metadata != null) {
|
||||||
_updateRemoteSDPStreamMetadata(metadata);
|
_updateRemoteSDPStreamMetadata(metadata);
|
||||||
}
|
}
|
||||||
|
|
@ -387,12 +411,18 @@ class CallSession {
|
||||||
if (remotePartyId != null) {
|
if (remotePartyId != null) {
|
||||||
/// Send select_answer event.
|
/// Send select_answer event.
|
||||||
await sendSelectCallAnswer(
|
await sendSelectCallAnswer(
|
||||||
opts.room, callId, localPartyId, remotePartyId!);
|
opts.room,
|
||||||
|
callId,
|
||||||
|
localPartyId,
|
||||||
|
remotePartyId!,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> onNegotiateReceived(
|
Future<void> onNegotiateReceived(
|
||||||
SDPStreamMetadata? metadata, RTCSessionDescription description) async {
|
SDPStreamMetadata? metadata,
|
||||||
|
RTCSessionDescription description,
|
||||||
|
) async {
|
||||||
final polite = direction == CallDirection.kIncoming;
|
final polite = direction == CallDirection.kIncoming;
|
||||||
|
|
||||||
// Here we follow the perfect negotiation logic from
|
// Here we follow the perfect negotiation logic from
|
||||||
|
|
@ -425,12 +455,13 @@ class CallSession {
|
||||||
}
|
}
|
||||||
|
|
||||||
await sendCallNegotiate(
|
await sendCallNegotiate(
|
||||||
room,
|
room,
|
||||||
callId,
|
callId,
|
||||||
CallTimeouts.defaultCallEventLifetime.inMilliseconds,
|
CallTimeouts.defaultCallEventLifetime.inMilliseconds,
|
||||||
localPartyId,
|
localPartyId,
|
||||||
answer.sdp!,
|
answer.sdp!,
|
||||||
type: answer.type!);
|
type: answer.type!,
|
||||||
|
);
|
||||||
await pc!.setLocalDescription(answer);
|
await pc!.setLocalDescription(answer);
|
||||||
}
|
}
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
|
|
@ -464,7 +495,8 @@ class CallSession {
|
||||||
_remoteSDPStreamMetadata?.sdpStreamMetadatas
|
_remoteSDPStreamMetadata?.sdpStreamMetadatas
|
||||||
.forEach((streamId, sdpStreamMetadata) {
|
.forEach((streamId, sdpStreamMetadata) {
|
||||||
Logs().i(
|
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) {
|
for (final wpstream in getRemoteStreams) {
|
||||||
final streamId = wpstream.stream!.id;
|
final streamId = wpstream.stream!.id;
|
||||||
|
|
@ -498,7 +530,8 @@ class CallSession {
|
||||||
|
|
||||||
if (!candidate.isValid) {
|
if (!candidate.isValid) {
|
||||||
Logs().w(
|
Logs().w(
|
||||||
'[VOIP] onCandidatesReceived => skip invalid candidate ${candidate.toMap()}');
|
'[VOIP] onCandidatesReceived => skip invalid candidate ${candidate.toMap()}',
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -530,11 +563,13 @@ class CallSession {
|
||||||
// Skip if there is nothing to do
|
// Skip if there is nothing to do
|
||||||
if (enabled && localScreenSharingStream != null) {
|
if (enabled && localScreenSharingStream != null) {
|
||||||
Logs().w(
|
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;
|
return true;
|
||||||
} else if (!enabled && localScreenSharingStream == null) {
|
} else if (!enabled && localScreenSharingStream == null) {
|
||||||
Logs().w(
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -635,7 +670,8 @@ class CallSession {
|
||||||
final metadata = _remoteSDPStreamMetadata?.sdpStreamMetadatas[stream.id];
|
final metadata = _remoteSDPStreamMetadata?.sdpStreamMetadatas[stream.id];
|
||||||
if (metadata == null) {
|
if (metadata == null) {
|
||||||
Logs().i(
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -762,24 +798,28 @@ class CallSession {
|
||||||
// remove any screen sharing or remote transceivers, these don't need
|
// remove any screen sharing or remote transceivers, these don't need
|
||||||
// to be replaced anyway.
|
// to be replaced anyway.
|
||||||
final transceivers = await pc!.getTransceivers();
|
final transceivers = await pc!.getTransceivers();
|
||||||
transceivers.removeWhere((transceiver) =>
|
transceivers.removeWhere(
|
||||||
transceiver.sender.track == null ||
|
(transceiver) =>
|
||||||
(localScreenSharingStream != null &&
|
transceiver.sender.track == null ||
|
||||||
localScreenSharingStream!.stream != null &&
|
(localScreenSharingStream != null &&
|
||||||
localScreenSharingStream!.stream!
|
localScreenSharingStream!.stream != null &&
|
||||||
.getTracks()
|
localScreenSharingStream!.stream!
|
||||||
.map((e) => e.id)
|
.getTracks()
|
||||||
.contains(transceiver.sender.track?.id)));
|
.map((e) => e.id)
|
||||||
|
.contains(transceiver.sender.track?.id)),
|
||||||
|
);
|
||||||
|
|
||||||
// in an ideal case the following should happen
|
// in an ideal case the following should happen
|
||||||
// - audio track gets replaced
|
// - audio track gets replaced
|
||||||
// - new video track gets added
|
// - new video track gets added
|
||||||
for (final newTrack in streamTracks) {
|
for (final newTrack in streamTracks) {
|
||||||
final transceiver = transceivers.singleWhereOrNull(
|
final transceiver = transceivers.singleWhereOrNull(
|
||||||
(transceiver) => transceiver.sender.track!.kind == newTrack.kind);
|
(transceiver) => transceiver.sender.track!.kind == newTrack.kind,
|
||||||
|
);
|
||||||
if (transceiver != null) {
|
if (transceiver != null) {
|
||||||
Logs().d(
|
Logs().d(
|
||||||
'[VOIP] replacing ${transceiver.sender.track} in transceiver');
|
'[VOIP] replacing ${transceiver.sender.track} in transceiver',
|
||||||
|
);
|
||||||
final oldSender = transceiver.sender;
|
final oldSender = transceiver.sender;
|
||||||
await oldSender.replaceTrack(newTrack);
|
await oldSender.replaceTrack(newTrack);
|
||||||
await transceiver.setDirection(
|
await transceiver.setDirection(
|
||||||
|
|
@ -811,9 +851,9 @@ class CallSession {
|
||||||
_remoteOnHold = onHold;
|
_remoteOnHold = onHold;
|
||||||
final transceivers = await pc!.getTransceivers();
|
final transceivers = await pc!.getTransceivers();
|
||||||
for (final transceiver in transceivers) {
|
for (final transceiver in transceivers) {
|
||||||
await transceiver.setDirection(onHold
|
await transceiver.setDirection(
|
||||||
? TransceiverDirection.SendOnly
|
onHold ? TransceiverDirection.SendOnly : TransceiverDirection.SendRecv,
|
||||||
: TransceiverDirection.SendRecv);
|
);
|
||||||
}
|
}
|
||||||
await updateMuteStatus();
|
await updateMuteStatus();
|
||||||
fireCallEvent(CallStateChange.kRemoteHoldUnhold);
|
fireCallEvent(CallStateChange.kRemoteHoldUnhold);
|
||||||
|
|
@ -858,14 +898,16 @@ class CallSession {
|
||||||
final metadata = SDPStreamMetadata({
|
final metadata = SDPStreamMetadata({
|
||||||
if (localUserMediaStream != null)
|
if (localUserMediaStream != null)
|
||||||
localUserMediaStream!.stream!.id: SDPStreamPurpose(
|
localUserMediaStream!.stream!.id: SDPStreamPurpose(
|
||||||
purpose: SDPStreamMetadataPurpose.Usermedia,
|
purpose: SDPStreamMetadataPurpose.Usermedia,
|
||||||
audio_muted: localUserMediaStream!.audioMuted,
|
audio_muted: localUserMediaStream!.audioMuted,
|
||||||
video_muted: localUserMediaStream!.videoMuted),
|
video_muted: localUserMediaStream!.videoMuted,
|
||||||
|
),
|
||||||
if (localScreenSharingStream != null)
|
if (localScreenSharingStream != null)
|
||||||
localScreenSharingStream!.stream!.id: SDPStreamPurpose(
|
localScreenSharingStream!.stream!.id: SDPStreamPurpose(
|
||||||
purpose: SDPStreamMetadataPurpose.Screenshare,
|
purpose: SDPStreamMetadataPurpose.Screenshare,
|
||||||
audio_muted: localScreenSharingStream!.audioMuted,
|
audio_muted: localScreenSharingStream!.audioMuted,
|
||||||
video_muted: localScreenSharingStream!.videoMuted),
|
video_muted: localScreenSharingStream!.videoMuted,
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
await pc!.setLocalDescription(answer);
|
await pc!.setLocalDescription(answer);
|
||||||
|
|
@ -898,7 +940,8 @@ class CallSession {
|
||||||
setCallState(CallState.kEnding);
|
setCallState(CallState.kEnding);
|
||||||
if (state != CallState.kRinging && state != CallState.kFledgling) {
|
if (state != CallState.kRinging && state != CallState.kFledgling) {
|
||||||
Logs().e(
|
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);
|
await hangup(reason: CallErrorCode.userHangup, shouldEmit: shouldEmit);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -909,8 +952,10 @@ class CallSession {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> hangup(
|
Future<void> hangup({
|
||||||
{required CallErrorCode reason, bool shouldEmit = true}) async {
|
required CallErrorCode reason,
|
||||||
|
bool shouldEmit = true,
|
||||||
|
}) async {
|
||||||
setCallState(CallState.kEnding);
|
setCallState(CallState.kEnding);
|
||||||
await terminate(CallParty.kLocal, reason, shouldEmit);
|
await terminate(CallParty.kLocal, reason, shouldEmit);
|
||||||
try {
|
try {
|
||||||
|
|
@ -1002,7 +1047,10 @@ class CallSession {
|
||||||
|
|
||||||
if (shouldTerminate) {
|
if (shouldTerminate) {
|
||||||
await terminate(
|
await terminate(
|
||||||
CallParty.kRemote, reason ?? CallErrorCode.userHangup, true);
|
CallParty.kRemote,
|
||||||
|
reason ?? CallErrorCode.userHangup,
|
||||||
|
true,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
Logs().e('[VOIP] Call is in state: ${state.toString()}: ignoring reject');
|
Logs().e('[VOIP] Call is in state: ${state.toString()}: ignoring reject');
|
||||||
}
|
}
|
||||||
|
|
@ -1011,7 +1059,8 @@ class CallSession {
|
||||||
Future<void> _gotLocalOffer(RTCSessionDescription offer) async {
|
Future<void> _gotLocalOffer(RTCSessionDescription offer) async {
|
||||||
if (callHasEnded) {
|
if (callHasEnded) {
|
||||||
Logs().d(
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1020,7 +1069,10 @@ class CallSession {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Logs().d('Error setting local description! ${err.toString()}');
|
Logs().d('Error setting local description! ${err.toString()}');
|
||||||
await terminate(
|
await terminate(
|
||||||
CallParty.kLocal, CallErrorCode.setLocalDescription, true);
|
CallParty.kLocal,
|
||||||
|
CallErrorCode.setLocalDescription,
|
||||||
|
true,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1038,13 +1090,14 @@ class CallSession {
|
||||||
final metadata = _getLocalSDPStreamMetadata();
|
final metadata = _getLocalSDPStreamMetadata();
|
||||||
if (state == CallState.kCreateOffer) {
|
if (state == CallState.kCreateOffer) {
|
||||||
await sendInviteToCall(
|
await sendInviteToCall(
|
||||||
room,
|
room,
|
||||||
callId,
|
callId,
|
||||||
CallTimeouts.callInviteLifetime.inMilliseconds,
|
CallTimeouts.callInviteLifetime.inMilliseconds,
|
||||||
localPartyId,
|
localPartyId,
|
||||||
offer.sdp!,
|
offer.sdp!,
|
||||||
capabilities: callCapabilities,
|
capabilities: callCapabilities,
|
||||||
metadata: metadata);
|
metadata: metadata,
|
||||||
|
);
|
||||||
// just incase we ended the call but already sent the invite
|
// just incase we ended the call but already sent the invite
|
||||||
// raraley happens during glares
|
// raraley happens during glares
|
||||||
if (state == CallState.kEnded) {
|
if (state == CallState.kEnded) {
|
||||||
|
|
@ -1069,14 +1122,15 @@ class CallSession {
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await sendCallNegotiate(
|
await sendCallNegotiate(
|
||||||
room,
|
room,
|
||||||
callId,
|
callId,
|
||||||
CallTimeouts.defaultCallEventLifetime.inMilliseconds,
|
CallTimeouts.defaultCallEventLifetime.inMilliseconds,
|
||||||
localPartyId,
|
localPartyId,
|
||||||
offer.sdp!,
|
offer.sdp!,
|
||||||
type: offer.type!,
|
type: offer.type!,
|
||||||
capabilities: callCapabilities,
|
capabilities: callCapabilities,
|
||||||
metadata: metadata);
|
metadata: metadata,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1151,7 +1205,7 @@ class CallSession {
|
||||||
_missedCall = false;
|
_missedCall = false;
|
||||||
} else if ({
|
} else if ({
|
||||||
RTCIceConnectionState.RTCIceConnectionStateFailed,
|
RTCIceConnectionState.RTCIceConnectionStateFailed,
|
||||||
RTCIceConnectionState.RTCIceConnectionStateDisconnected
|
RTCIceConnectionState.RTCIceConnectionStateDisconnected,
|
||||||
}.contains(state)) {
|
}.contains(state)) {
|
||||||
if (iceRestartedCount < 3) {
|
if (iceRestartedCount < 3) {
|
||||||
await restartIce();
|
await restartIce();
|
||||||
|
|
@ -1199,10 +1253,14 @@ class CallSession {
|
||||||
localUserMediaStream!.isVideoMuted()) ||
|
localUserMediaStream!.isVideoMuted()) ||
|
||||||
_remoteOnHold;
|
_remoteOnHold;
|
||||||
|
|
||||||
_setTracksEnabled(localUserMediaStream?.stream?.getAudioTracks() ?? [],
|
_setTracksEnabled(
|
||||||
!micShouldBeMuted);
|
localUserMediaStream?.stream?.getAudioTracks() ?? [],
|
||||||
_setTracksEnabled(localUserMediaStream?.stream?.getVideoTracks() ?? [],
|
!micShouldBeMuted,
|
||||||
!vidShouldBeMuted);
|
);
|
||||||
|
_setTracksEnabled(
|
||||||
|
localUserMediaStream?.stream?.getVideoTracks() ?? [],
|
||||||
|
!vidShouldBeMuted,
|
||||||
|
);
|
||||||
|
|
||||||
await sendSDPStreamMetadataChanged(
|
await sendSDPStreamMetadataChanged(
|
||||||
room,
|
room,
|
||||||
|
|
@ -1270,7 +1328,7 @@ class CallSession {
|
||||||
Future<RTCPeerConnection> _createPeerConnection() async {
|
Future<RTCPeerConnection> _createPeerConnection() async {
|
||||||
final configuration = <String, dynamic>{
|
final configuration = <String, dynamic>{
|
||||||
'iceServers': opts.iceServers,
|
'iceServers': opts.iceServers,
|
||||||
'sdpSemantics': 'unified-plan'
|
'sdpSemantics': 'unified-plan',
|
||||||
};
|
};
|
||||||
final pc = await voip.delegate.createPeerConnection(configuration);
|
final pc = await voip.delegate.createPeerConnection(configuration);
|
||||||
pc.onTrack = (RTCTrackEvent event) async {
|
pc.onTrack = (RTCTrackEvent event) async {
|
||||||
|
|
@ -1290,7 +1348,9 @@ class CallSession {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> createDataChannel(
|
Future<void> createDataChannel(
|
||||||
String label, RTCDataChannelInit dataChannelDict) async {
|
String label,
|
||||||
|
RTCDataChannelInit dataChannelDict,
|
||||||
|
) async {
|
||||||
await pc?.createDataChannel(label, dataChannelDict);
|
await pc?.createDataChannel(label, dataChannelDict);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1339,7 +1399,11 @@ class CallSession {
|
||||||
}
|
}
|
||||||
_localCandidates.clear();
|
_localCandidates.clear();
|
||||||
final res = await sendCallCandidates(
|
final res = await sendCallCandidates(
|
||||||
opts.room, callId, localPartyId, candidates);
|
opts.room,
|
||||||
|
callId,
|
||||||
|
localPartyId,
|
||||||
|
candidates,
|
||||||
|
);
|
||||||
Logs().v('[VOIP] sendCallCandidates res => $res');
|
Logs().v('[VOIP] sendCallCandidates res => $res');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -1350,7 +1414,8 @@ class CallSession {
|
||||||
|
|
||||||
if (_candidateSendTries > 5) {
|
if (_candidateSendTries > 5) {
|
||||||
Logs().d(
|
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);
|
await hangup(reason: CallErrorCode.iceTimeout);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -1407,13 +1472,15 @@ class CallSession {
|
||||||
}
|
}
|
||||||
if (selectedPartyId == null) {
|
if (selectedPartyId == null) {
|
||||||
Logs().w(
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedPartyId != localPartyId) {
|
if (selectedPartyId != localPartyId) {
|
||||||
Logs().w(
|
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
|
// The other party has picked somebody else's answer
|
||||||
await terminate(CallParty.kRemote, CallErrorCode.answeredElsewhere, true);
|
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.
|
/// 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.
|
/// [party_id] The party ID for call, Can be set to client.deviceId.
|
||||||
Future<String?> sendInviteToCall(
|
Future<String?> sendInviteToCall(
|
||||||
Room room, String callId, int lifetime, String party_id, String sdp,
|
Room room,
|
||||||
{String type = 'offer',
|
String callId,
|
||||||
String version = voipProtoVersion,
|
int lifetime,
|
||||||
String? txid,
|
String party_id,
|
||||||
CallCapabilities? capabilities,
|
String sdp, {
|
||||||
SDPStreamMetadata? metadata}) async {
|
String type = 'offer',
|
||||||
|
String version = voipProtoVersion,
|
||||||
|
String? txid,
|
||||||
|
CallCapabilities? capabilities,
|
||||||
|
SDPStreamMetadata? metadata,
|
||||||
|
}) async {
|
||||||
final content = {
|
final content = {
|
||||||
'call_id': callId,
|
'call_id': callId,
|
||||||
'party_id': party_id,
|
'party_id': party_id,
|
||||||
|
|
@ -1471,8 +1543,13 @@ class CallSession {
|
||||||
/// [party_id] The party ID for call, Can be set to client.deviceId.
|
/// [party_id] The party ID for call, Can be set to client.deviceId.
|
||||||
/// [selected_party_id] The party ID for the selected answer.
|
/// [selected_party_id] The party ID for the selected answer.
|
||||||
Future<String?> sendSelectCallAnswer(
|
Future<String?> sendSelectCallAnswer(
|
||||||
Room room, String callId, String party_id, String selected_party_id,
|
Room room,
|
||||||
{String version = voipProtoVersion, String? txid}) async {
|
String callId,
|
||||||
|
String party_id,
|
||||||
|
String selected_party_id, {
|
||||||
|
String version = voipProtoVersion,
|
||||||
|
String? txid,
|
||||||
|
}) async {
|
||||||
final content = {
|
final content = {
|
||||||
'call_id': callId,
|
'call_id': callId,
|
||||||
'party_id': party_id,
|
'party_id': party_id,
|
||||||
|
|
@ -1495,8 +1572,13 @@ class CallSession {
|
||||||
/// [callId] is a unique identifier for the call.
|
/// [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.
|
/// [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.
|
/// [party_id] The party ID for call, Can be set to client.deviceId.
|
||||||
Future<String?> sendCallReject(Room room, String callId, String party_id,
|
Future<String?> sendCallReject(
|
||||||
{String version = voipProtoVersion, String? txid}) async {
|
Room room,
|
||||||
|
String callId,
|
||||||
|
String party_id, {
|
||||||
|
String version = voipProtoVersion,
|
||||||
|
String? txid,
|
||||||
|
}) async {
|
||||||
final content = {
|
final content = {
|
||||||
'call_id': callId,
|
'call_id': callId,
|
||||||
'party_id': party_id,
|
'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.
|
/// [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.
|
/// [party_id] The party ID for call, Can be set to client.deviceId.
|
||||||
Future<String?> sendCallNegotiate(
|
Future<String?> sendCallNegotiate(
|
||||||
Room room, String callId, int lifetime, String party_id, String sdp,
|
Room room,
|
||||||
{String type = 'offer',
|
String callId,
|
||||||
String version = voipProtoVersion,
|
int lifetime,
|
||||||
String? txid,
|
String party_id,
|
||||||
CallCapabilities? capabilities,
|
String sdp, {
|
||||||
SDPStreamMetadata? metadata}) async {
|
String type = 'offer',
|
||||||
|
String version = voipProtoVersion,
|
||||||
|
String? txid,
|
||||||
|
CallCapabilities? capabilities,
|
||||||
|
SDPStreamMetadata? metadata,
|
||||||
|
}) async {
|
||||||
final content = {
|
final content = {
|
||||||
'call_id': callId,
|
'call_id': callId,
|
||||||
'party_id': party_id,
|
'party_id': party_id,
|
||||||
|
|
@ -1596,12 +1683,16 @@ class CallSession {
|
||||||
/// [sdp] The SDP text of the session description.
|
/// [sdp] The SDP text of the session description.
|
||||||
/// [party_id] The party ID for call, Can be set to client.deviceId.
|
/// [party_id] The party ID for call, Can be set to client.deviceId.
|
||||||
Future<String?> sendAnswerCall(
|
Future<String?> sendAnswerCall(
|
||||||
Room room, String callId, String sdp, String party_id,
|
Room room,
|
||||||
{String type = 'answer',
|
String callId,
|
||||||
String version = voipProtoVersion,
|
String sdp,
|
||||||
String? txid,
|
String party_id, {
|
||||||
CallCapabilities? capabilities,
|
String type = 'answer',
|
||||||
SDPStreamMetadata? metadata}) async {
|
String version = voipProtoVersion,
|
||||||
|
String? txid,
|
||||||
|
CallCapabilities? capabilities,
|
||||||
|
SDPStreamMetadata? metadata,
|
||||||
|
}) async {
|
||||||
final content = {
|
final content = {
|
||||||
'call_id': callId,
|
'call_id': callId,
|
||||||
'party_id': party_id,
|
'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.
|
/// [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.
|
/// [party_id] The party ID for call, Can be set to client.deviceId.
|
||||||
Future<String?> sendHangupCall(
|
Future<String?> sendHangupCall(
|
||||||
Room room, String callId, String party_id, String? hangupCause,
|
Room room,
|
||||||
{String version = voipProtoVersion, String? txid}) async {
|
String callId,
|
||||||
|
String party_id,
|
||||||
|
String? hangupCause, {
|
||||||
|
String version = voipProtoVersion,
|
||||||
|
String? txid,
|
||||||
|
}) async {
|
||||||
final content = {
|
final content = {
|
||||||
'call_id': callId,
|
'call_id': callId,
|
||||||
'party_id': party_id,
|
'party_id': party_id,
|
||||||
|
|
@ -1656,8 +1752,13 @@ class CallSession {
|
||||||
/// [party_id] The party ID for call, Can be set to client.deviceId.
|
/// [party_id] The party ID for call, Can be set to client.deviceId.
|
||||||
/// [metadata] The sdp_stream_metadata object.
|
/// [metadata] The sdp_stream_metadata object.
|
||||||
Future<String?> sendSDPStreamMetadataChanged(
|
Future<String?> sendSDPStreamMetadataChanged(
|
||||||
Room room, String callId, String party_id, SDPStreamMetadata metadata,
|
Room room,
|
||||||
{String version = voipProtoVersion, String? txid}) async {
|
String callId,
|
||||||
|
String party_id,
|
||||||
|
SDPStreamMetadata metadata, {
|
||||||
|
String version = voipProtoVersion,
|
||||||
|
String? txid,
|
||||||
|
}) async {
|
||||||
final content = {
|
final content = {
|
||||||
'call_id': callId,
|
'call_id': callId,
|
||||||
'party_id': party_id,
|
'party_id': party_id,
|
||||||
|
|
@ -1682,8 +1783,13 @@ class CallSession {
|
||||||
/// [party_id] The party ID for call, Can be set to client.deviceId.
|
/// [party_id] The party ID for call, Can be set to client.deviceId.
|
||||||
/// [callReplaces] transfer info
|
/// [callReplaces] transfer info
|
||||||
Future<String?> sendCallReplaces(
|
Future<String?> sendCallReplaces(
|
||||||
Room room, String callId, String party_id, CallReplaces callReplaces,
|
Room room,
|
||||||
{String version = voipProtoVersion, String? txid}) async {
|
String callId,
|
||||||
|
String party_id,
|
||||||
|
CallReplaces callReplaces, {
|
||||||
|
String version = voipProtoVersion,
|
||||||
|
String? txid,
|
||||||
|
}) async {
|
||||||
final content = {
|
final content = {
|
||||||
'call_id': callId,
|
'call_id': callId,
|
||||||
'party_id': party_id,
|
'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.
|
/// [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.
|
/// [party_id] The party ID for call, Can be set to client.deviceId.
|
||||||
/// [assertedIdentity] the asserted identity
|
/// [assertedIdentity] the asserted identity
|
||||||
Future<String?> sendAssertedIdentity(Room room, String callId,
|
Future<String?> sendAssertedIdentity(
|
||||||
String party_id, AssertedIdentity assertedIdentity,
|
Room room,
|
||||||
{String version = voipProtoVersion, String? txid}) async {
|
String callId,
|
||||||
|
String party_id,
|
||||||
|
AssertedIdentity assertedIdentity, {
|
||||||
|
String version = voipProtoVersion,
|
||||||
|
String? txid,
|
||||||
|
}) async {
|
||||||
final content = {
|
final content = {
|
||||||
'call_id': callId,
|
'call_id': callId,
|
||||||
'party_id': party_id,
|
'party_id': party_id,
|
||||||
|
|
@ -1753,19 +1864,24 @@ class CallSession {
|
||||||
await client.userDeviceKeysLoading;
|
await client.userDeviceKeysLoading;
|
||||||
if (client.userDeviceKeys[remoteUserId]?.deviceKeys[remoteDeviceId] !=
|
if (client.userDeviceKeys[remoteUserId]?.deviceKeys[remoteDeviceId] !=
|
||||||
null) {
|
null) {
|
||||||
await client.sendToDeviceEncrypted([
|
await client.sendToDeviceEncrypted(
|
||||||
client.userDeviceKeys[remoteUserId]!.deviceKeys[remoteDeviceId]!
|
[
|
||||||
], type, data);
|
client.userDeviceKeys[remoteUserId]!.deviceKeys[remoteDeviceId]!,
|
||||||
|
],
|
||||||
|
type,
|
||||||
|
data,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
Logs().w(
|
Logs().w(
|
||||||
'[VOIP] _sendCallContent missing device keys for $remoteUserId');
|
'[VOIP] _sendCallContent missing device keys for $remoteUserId',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await client.sendToDevice(
|
await client.sendToDevice(
|
||||||
type,
|
type,
|
||||||
txid,
|
txid,
|
||||||
{
|
{
|
||||||
remoteUserId!: {remoteDeviceId!: data}
|
remoteUserId!: {remoteDeviceId!: data},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -173,17 +173,20 @@ class GroupCallSession {
|
||||||
_resendMemberStateEventTimer!.cancel();
|
_resendMemberStateEventTimer!.cancel();
|
||||||
}
|
}
|
||||||
_resendMemberStateEventTimer = Timer.periodic(
|
_resendMemberStateEventTimer = Timer.periodic(
|
||||||
CallTimeouts.updateExpireTsTimerDuration, ((timer) async {
|
CallTimeouts.updateExpireTsTimerDuration,
|
||||||
Logs().d('sendMemberStateEvent updating member event with timer');
|
((timer) async {
|
||||||
if (state != GroupCallState.ended ||
|
Logs().d('sendMemberStateEvent updating member event with timer');
|
||||||
state != GroupCallState.localCallFeedUninitialized) {
|
if (state != GroupCallState.ended ||
|
||||||
await sendMemberStateEvent();
|
state != GroupCallState.localCallFeedUninitialized) {
|
||||||
} else {
|
await sendMemberStateEvent();
|
||||||
Logs().d(
|
} else {
|
||||||
'[VOIP] deteceted groupCall in state $state, removing state event');
|
Logs().d(
|
||||||
await removeMemberStateEvent();
|
'[VOIP] deteceted groupCall in state $state, removing state event',
|
||||||
}
|
);
|
||||||
}));
|
await removeMemberStateEvent();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> removeMemberStateEvent() {
|
Future<void> removeMemberStateEvent() {
|
||||||
|
|
@ -218,7 +221,8 @@ class GroupCallSession {
|
||||||
|
|
||||||
for (final mem in ignoredMems) {
|
for (final mem in ignoredMems) {
|
||||||
Logs().v(
|
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 = {};
|
final Set<CallParticipant> newP = {};
|
||||||
|
|
@ -236,7 +240,8 @@ class GroupCallSession {
|
||||||
|
|
||||||
if (state != GroupCallState.entered) {
|
if (state != GroupCallState.entered) {
|
||||||
Logs().w(
|
Logs().w(
|
||||||
'[VOIP] onMemberStateChanged groupCall state is currently $state, skipping member update');
|
'[VOIP] onMemberStateChanged groupCall state is currently $state, skipping member update',
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -253,7 +258,8 @@ class GroupCallSession {
|
||||||
..remove(localParticipant);
|
..remove(localParticipant);
|
||||||
if (nonLocalAnyJoined.isNotEmpty && state == GroupCallState.entered) {
|
if (nonLocalAnyJoined.isNotEmpty && state == GroupCallState.entered) {
|
||||||
Logs().v(
|
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());
|
await backend.onNewParticipant(this, nonLocalAnyJoined.toList());
|
||||||
}
|
}
|
||||||
_participants.addAll(anyJoined);
|
_participants.addAll(anyJoined);
|
||||||
|
|
@ -265,7 +271,8 @@ class GroupCallSession {
|
||||||
..remove(localParticipant);
|
..remove(localParticipant);
|
||||||
if (nonLocalAnyLeft.isNotEmpty && state == GroupCallState.entered) {
|
if (nonLocalAnyLeft.isNotEmpty && state == GroupCallState.entered) {
|
||||||
Logs().v(
|
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());
|
await backend.onLeftParticipant(this, nonLocalAnyLeft.toList());
|
||||||
}
|
}
|
||||||
_participants.removeAll(anyLeft);
|
_participants.removeAll(anyLeft);
|
||||||
|
|
@ -275,7 +282,8 @@ class GroupCallSession {
|
||||||
|
|
||||||
onGroupCallEvent.add(GroupCallStateChange.participantsChanged);
|
onGroupCallEvent.add(GroupCallStateChange.participantsChanged);
|
||||||
Logs().d(
|
Logs().d(
|
||||||
'[VOIP] onMemberStateChanged current list: ${_participants.map((e) => e.id).toString()}');
|
'[VOIP] onMemberStateChanged current list: ${_participants.map((e) => e.id).toString()}',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -105,10 +105,11 @@ class SDPStreamPurpose {
|
||||||
bool audio_muted;
|
bool audio_muted;
|
||||||
bool video_muted;
|
bool video_muted;
|
||||||
|
|
||||||
SDPStreamPurpose(
|
SDPStreamPurpose({
|
||||||
{required this.purpose,
|
required this.purpose,
|
||||||
this.audio_muted = false,
|
this.audio_muted = false,
|
||||||
this.video_muted = false});
|
this.video_muted = false,
|
||||||
|
});
|
||||||
factory SDPStreamPurpose.fromJson(Map<String, dynamic> json) =>
|
factory SDPStreamPurpose.fromJson(Map<String, dynamic> json) =>
|
||||||
SDPStreamPurpose(
|
SDPStreamPurpose(
|
||||||
audio_muted: json['audio_muted'] as bool? ?? false,
|
audio_muted: json['audio_muted'] as bool? ?? false,
|
||||||
|
|
@ -133,8 +134,11 @@ class SDPStreamMetadata {
|
||||||
SDPStreamMetadata(this.sdpStreamMetadatas);
|
SDPStreamMetadata(this.sdpStreamMetadatas);
|
||||||
|
|
||||||
factory SDPStreamMetadata.fromJson(Map<String, dynamic> json) =>
|
factory SDPStreamMetadata.fromJson(Map<String, dynamic> json) =>
|
||||||
SDPStreamMetadata(json.map(
|
SDPStreamMetadata(
|
||||||
(key, value) => MapEntry(key, SDPStreamPurpose.fromJson(value))));
|
json.map(
|
||||||
|
(key, value) => MapEntry(key, SDPStreamPurpose.fromJson(value)),
|
||||||
|
),
|
||||||
|
);
|
||||||
Map<String, dynamic> toJson() =>
|
Map<String, dynamic> toJson() =>
|
||||||
sdpStreamMetadatas.map((key, value) => MapEntry(key, value.toJson()));
|
sdpStreamMetadatas.map((key, value) => MapEntry(key, value.toJson()));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,10 @@ enum E2EEKeyMode {
|
||||||
|
|
||||||
abstract class EncryptionKeyProvider {
|
abstract class EncryptionKeyProvider {
|
||||||
Future<void> onSetEncryptionKey(
|
Future<void> onSetEncryptionKey(
|
||||||
CallParticipant participant, Uint8List key, int index);
|
CallParticipant participant,
|
||||||
|
Uint8List key,
|
||||||
|
int index,
|
||||||
|
);
|
||||||
|
|
||||||
Future<Uint8List> onRatchetKey(CallParticipant participant, int index);
|
Future<Uint8List> onRatchetKey(CallParticipant participant, int index);
|
||||||
|
|
||||||
|
|
@ -42,11 +45,13 @@ class EncryptionKeysEventContent {
|
||||||
|
|
||||||
factory EncryptionKeysEventContent.fromJson(Map<String, dynamic> json) =>
|
factory EncryptionKeysEventContent.fromJson(Map<String, dynamic> json) =>
|
||||||
EncryptionKeysEventContent(
|
EncryptionKeysEventContent(
|
||||||
(json['keys'] as List<dynamic>)
|
(json['keys'] as List<dynamic>)
|
||||||
.map(
|
.map(
|
||||||
(e) => EncryptionKeyEntry.fromJson(e as Map<String, dynamic>))
|
(e) => EncryptionKeyEntry.fromJson(e as Map<String, dynamic>),
|
||||||
.toList(),
|
)
|
||||||
json['call_id'] as String);
|
.toList(),
|
||||||
|
json['call_id'] as String,
|
||||||
|
);
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
'keys': keys.map((e) => e.toJson()).toList(),
|
'keys': keys.map((e) => e.toJson()).toList(),
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,9 @@ import 'package:matrix/matrix.dart';
|
||||||
abstract class WebRTCDelegate {
|
abstract class WebRTCDelegate {
|
||||||
MediaDevices get mediaDevices;
|
MediaDevices get mediaDevices;
|
||||||
Future<RTCPeerConnection> createPeerConnection(
|
Future<RTCPeerConnection> createPeerConnection(
|
||||||
Map<String, dynamic> configuration,
|
Map<String, dynamic> configuration, [
|
||||||
[Map<String, dynamic> constraints = const {}]);
|
Map<String, dynamic> constraints = const {},
|
||||||
|
]);
|
||||||
Future<void> playRingtone();
|
Future<void> playRingtone();
|
||||||
Future<void> stopRingtone();
|
Future<void> stopRingtone();
|
||||||
Future<void> handleNewCall(CallSession session);
|
Future<void> handleNewCall(CallSession session);
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ class ConnectionTester {
|
||||||
'iceServers': iceServers,
|
'iceServers': iceServers,
|
||||||
'sdpSemantics': 'unified-plan',
|
'sdpSemantics': 'unified-plan',
|
||||||
'iceCandidatePoolSize': 1,
|
'iceCandidatePoolSize': 1,
|
||||||
'iceTransportPolicy': 'relay'
|
'iceTransportPolicy': 'relay',
|
||||||
};
|
};
|
||||||
pc1 = await delegate.createPeerConnection(configuration);
|
pc1 = await delegate.createPeerConnection(configuration);
|
||||||
pc2 = await delegate.createPeerConnection(configuration);
|
pc2 = await delegate.createPeerConnection(configuration);
|
||||||
|
|
@ -78,9 +78,11 @@ class ConnectionTester {
|
||||||
return connected;
|
return connected;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> waitUntilAsync(Future<bool> Function() test,
|
Future<int> waitUntilAsync(
|
||||||
{final int maxIterations = 1000,
|
Future<bool> Function() test, {
|
||||||
final Duration step = const Duration(milliseconds: 10)}) async {
|
final int maxIterations = 1000,
|
||||||
|
final Duration step = const Duration(milliseconds: 10),
|
||||||
|
}) async {
|
||||||
int iterations = 0;
|
int iterations = 0;
|
||||||
for (; iterations < maxIterations; iterations++) {
|
for (; iterations < maxIterations; iterations++) {
|
||||||
await Future.delayed(step);
|
await Future.delayed(step);
|
||||||
|
|
@ -90,7 +92,8 @@ class ConnectionTester {
|
||||||
}
|
}
|
||||||
if (iterations >= maxIterations) {
|
if (iterations >= maxIterations) {
|
||||||
throw TimeoutException(
|
throw TimeoutException(
|
||||||
'Condition not reached within ${iterations * step.inMilliseconds}ms');
|
'Condition not reached within ${iterations * step.inMilliseconds}ms',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return iterations;
|
return iterations;
|
||||||
}
|
}
|
||||||
|
|
@ -112,7 +115,7 @@ class ConnectionTester {
|
||||||
{
|
{
|
||||||
'username': _turnServerCredentials!.username,
|
'username': _turnServerCredentials!.username,
|
||||||
'credential': _turnServerCredentials!.password,
|
'credential': _turnServerCredentials!.password,
|
||||||
'url': _turnServerCredentials!.uris[0]
|
'url': _turnServerCredentials!.uris[0],
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,8 @@ extension FamedlyCallMemberEventsExtension on Room {
|
||||||
|
|
||||||
/// passing no `CallMembership` removes it from the state event.
|
/// passing no `CallMembership` removes it from the state event.
|
||||||
Future<void> updateFamedlyCallMemberStateEvent(
|
Future<void> updateFamedlyCallMemberStateEvent(
|
||||||
CallMembership callMembership) async {
|
CallMembership callMembership,
|
||||||
|
) async {
|
||||||
final ownMemberships = getCallMembershipsForUser(client.userID!);
|
final ownMemberships = getCallMembershipsForUser(client.userID!);
|
||||||
|
|
||||||
// do not bother removing other deviceId expired events because we have no
|
// do not bother removing other deviceId expired events because we have no
|
||||||
|
|
@ -93,7 +94,7 @@ extension FamedlyCallMemberEventsExtension on Room {
|
||||||
ownMemberships.add(callMembership);
|
ownMemberships.add(callMembership);
|
||||||
|
|
||||||
final newContent = {
|
final newContent = {
|
||||||
'memberships': List.from(ownMemberships.map((e) => e.toJson()))
|
'memberships': List.from(ownMemberships.map((e) => e.toJson())),
|
||||||
};
|
};
|
||||||
|
|
||||||
await setFamedlyCallMemberEvent(newContent);
|
await setFamedlyCallMemberEvent(newContent);
|
||||||
|
|
@ -107,14 +108,16 @@ extension FamedlyCallMemberEventsExtension on Room {
|
||||||
}) async {
|
}) async {
|
||||||
final ownMemberships = getCallMembershipsForUser(client.userID!);
|
final ownMemberships = getCallMembershipsForUser(client.userID!);
|
||||||
|
|
||||||
ownMemberships.removeWhere((mem) =>
|
ownMemberships.removeWhere(
|
||||||
mem.callId == groupCallId &&
|
(mem) =>
|
||||||
mem.deviceId == deviceId &&
|
mem.callId == groupCallId &&
|
||||||
mem.application == application &&
|
mem.deviceId == deviceId &&
|
||||||
mem.scope == scope);
|
mem.application == application &&
|
||||||
|
mem.scope == scope,
|
||||||
|
);
|
||||||
|
|
||||||
final newContent = {
|
final newContent = {
|
||||||
'memberships': List.from(ownMemberships.map((e) => e.toJson()))
|
'memberships': List.from(ownMemberships.map((e) => e.toJson())),
|
||||||
};
|
};
|
||||||
await setFamedlyCallMemberEvent(newContent);
|
await setFamedlyCallMemberEvent(newContent);
|
||||||
}
|
}
|
||||||
|
|
@ -145,12 +148,18 @@ extension FamedlyCallMemberEventsExtension on Room {
|
||||||
List<CallMembership> getCallMembershipsFromEvent(MatrixEvent event) {
|
List<CallMembership> getCallMembershipsFromEvent(MatrixEvent event) {
|
||||||
if (event.roomId != id) return [];
|
if (event.roomId != id) return [];
|
||||||
return getCallMembershipsFromEventContent(
|
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
|
/// returns a list of memberships from a famedly call matrix event
|
||||||
List<CallMembership> getCallMembershipsFromEventContent(
|
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 mems = content.tryGetList<Map>('memberships');
|
||||||
final callMems = <CallMembership>[];
|
final callMems = <CallMembership>[];
|
||||||
for (final m in mems ?? []) {
|
for (final m in mems ?? []) {
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,9 @@ void setTracksEnabled(List<MediaStreamTrack> tracks, bool enabled) {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> hasMediaDevice(
|
Future<bool> hasMediaDevice(
|
||||||
WebRTCDelegate delegate, MediaInputKind mediaInputKind) async {
|
WebRTCDelegate delegate,
|
||||||
|
MediaInputKind mediaInputKind,
|
||||||
|
) async {
|
||||||
final devices = await delegate.mediaDevices.enumerateDevices();
|
final devices = await delegate.mediaDevices.enumerateDevices();
|
||||||
return devices
|
return devices
|
||||||
.where((device) => device.kind == mediaInputKind.name)
|
.where((device) => device.kind == mediaInputKind.name)
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ class UserMediaConstraints {
|
||||||
static const Map<String, Object> micMediaConstraints = {
|
static const Map<String, Object> micMediaConstraints = {
|
||||||
'echoCancellation': true,
|
'echoCancellation': true,
|
||||||
'noiseSuppression': true,
|
'noiseSuppression': true,
|
||||||
'autoGainControl': false
|
'autoGainControl': false,
|
||||||
};
|
};
|
||||||
|
|
||||||
static const Map<String, Object> camMediaConstraints = {
|
static const Map<String, Object> camMediaConstraints = {
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,8 @@ class CallTimeouts {
|
||||||
|
|
||||||
class CallConstants {
|
class CallConstants {
|
||||||
static final callEventsRegxp = RegExp(
|
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 = {
|
static const callEndedEventTypes = {
|
||||||
EventTypes.CallAnswer,
|
EventTypes.CallAnswer,
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,8 @@ class VoIP {
|
||||||
if (CallConstants.omitWhenCallEndedTypes.contains(event.type) &&
|
if (CallConstants.omitWhenCallEndedTypes.contains(event.type) &&
|
||||||
event.content.tryGet<String>('call_id') == callId) {
|
event.content.tryGet<String>('call_id') == callId) {
|
||||||
Logs().v(
|
Logs().v(
|
||||||
'Ommit "${event.type}" event for an already terminated call');
|
'Ommit "${event.type}" event for an already terminated call',
|
||||||
|
);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -146,7 +147,8 @@ class VoIP {
|
||||||
(callEvent.content.tryGet<int>('lifetime') ??
|
(callEvent.content.tryGet<int>('lifetime') ??
|
||||||
CallTimeouts.callInviteLifetime.inMilliseconds)) {
|
CallTimeouts.callInviteLifetime.inMilliseconds)) {
|
||||||
Logs().w(
|
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 true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -186,7 +188,8 @@ class VoIP {
|
||||||
groupCallSession = groupCalls[VoipId(roomId: roomId, callId: confId)];
|
groupCallSession = groupCalls[VoipId(roomId: roomId, callId: confId)];
|
||||||
} else {
|
} else {
|
||||||
Logs().w(
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -195,17 +198,20 @@ class VoIP {
|
||||||
final destSessionId = event.content.tryGet<String>('dest_session_id');
|
final destSessionId = event.content.tryGet<String>('dest_session_id');
|
||||||
if (destSessionId != currentSessionId) {
|
if (destSessionId != currentSessionId) {
|
||||||
Logs().w(
|
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;
|
return;
|
||||||
}
|
}
|
||||||
} else if (groupCallSession == null || remoteDeviceId == null) {
|
} else if (groupCallSession == null || remoteDeviceId == null) {
|
||||||
Logs().w(
|
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;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Logs().w(
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -213,14 +219,16 @@ class VoIP {
|
||||||
|
|
||||||
if (room == null) {
|
if (room == null) {
|
||||||
Logs().w(
|
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;
|
return;
|
||||||
} else if (client.userID != null &&
|
} else if (client.userID != null &&
|
||||||
client.deviceID != null &&
|
client.deviceID != null &&
|
||||||
remoteUserId == client.userID &&
|
remoteUserId == client.userID &&
|
||||||
remoteDeviceId == client.deviceID) {
|
remoteDeviceId == client.deviceID) {
|
||||||
Logs().v(
|
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;
|
return;
|
||||||
} else if (!event.type
|
} else if (!event.type
|
||||||
.startsWith(EventTypes.GroupCallMemberEncryptionKeys)) {
|
.startsWith(EventTypes.GroupCallMemberEncryptionKeys)) {
|
||||||
|
|
@ -237,37 +245,43 @@ class VoIP {
|
||||||
!{EventTypes.CallInvite, EventTypes.GroupCallMemberInvite}
|
!{EventTypes.CallInvite, EventTypes.GroupCallMemberInvite}
|
||||||
.contains(event.type)) {
|
.contains(event.type)) {
|
||||||
Logs().w(
|
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;
|
return;
|
||||||
} else if (call != null) {
|
} else if (call != null) {
|
||||||
// multiple checks to make sure the events sent are from the the
|
// multiple checks to make sure the events sent are from the the
|
||||||
// expected party
|
// expected party
|
||||||
if (call.room.id != room.id) {
|
if (call.room.id != room.id) {
|
||||||
Logs().w(
|
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;
|
return;
|
||||||
}
|
}
|
||||||
if (call.remoteUserId != null && call.remoteUserId != remoteUserId) {
|
if (call.remoteUserId != null && call.remoteUserId != remoteUserId) {
|
||||||
Logs().d(
|
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;
|
return;
|
||||||
}
|
}
|
||||||
if (call.remotePartyId != null && call.remotePartyId != partyId) {
|
if (call.remotePartyId != null && call.remotePartyId != partyId) {
|
||||||
Logs().w(
|
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;
|
return;
|
||||||
}
|
}
|
||||||
if ((call.remotePartyId != null &&
|
if ((call.remotePartyId != null &&
|
||||||
call.remotePartyId == localPartyId)) {
|
call.remotePartyId == localPartyId)) {
|
||||||
Logs().v(
|
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;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Logs().v(
|
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) {
|
switch (event.type) {
|
||||||
case EventTypes.CallInvite:
|
case EventTypes.CallInvite:
|
||||||
|
|
@ -313,11 +327,19 @@ class VoIP {
|
||||||
break;
|
break;
|
||||||
case EventTypes.GroupCallMemberEncryptionKeys:
|
case EventTypes.GroupCallMemberEncryptionKeys:
|
||||||
await groupCallSession!.backend.onCallEncryption(
|
await groupCallSession!.backend.onCallEncryption(
|
||||||
groupCallSession, remoteUserId, remoteDeviceId!, content);
|
groupCallSession,
|
||||||
|
remoteUserId,
|
||||||
|
remoteDeviceId!,
|
||||||
|
content,
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case EventTypes.GroupCallMemberEncryptionKeysRequest:
|
case EventTypes.GroupCallMemberEncryptionKeysRequest:
|
||||||
await groupCallSession!.backend.onCallEncryptionKeyRequest(
|
await groupCallSession!.backend.onCallEncryptionKeyRequest(
|
||||||
groupCallSession, remoteUserId, remoteDeviceId!, content);
|
groupCallSession,
|
||||||
|
remoteUserId,
|
||||||
|
remoteDeviceId!,
|
||||||
|
content,
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -336,10 +358,15 @@ class VoIP {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> onCallInvite(Room room, String remoteUserId,
|
Future<void> onCallInvite(
|
||||||
String? remoteDeviceId, Map<String, dynamic> content) async {
|
Room room,
|
||||||
|
String remoteUserId,
|
||||||
|
String? remoteDeviceId,
|
||||||
|
Map<String, dynamic> content,
|
||||||
|
) async {
|
||||||
Logs().v(
|
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 String callId = content['call_id'];
|
||||||
final int lifetime = content['lifetime'];
|
final int lifetime = content['lifetime'];
|
||||||
|
|
@ -348,7 +375,8 @@ class VoIP {
|
||||||
final call = calls[VoipId(roomId: room.id, callId: callId)];
|
final call = calls[VoipId(roomId: room.id, callId: callId)];
|
||||||
|
|
||||||
Logs().d(
|
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) {
|
if (call != null && call.state == CallState.kEnded) {
|
||||||
// Session already exist.
|
// Session already exist.
|
||||||
|
|
@ -371,7 +399,8 @@ class VoIP {
|
||||||
if (content['capabilities'] != null) {
|
if (content['capabilities'] != null) {
|
||||||
final capabilities = CallCapabilities.fromJson(content['capabilities']);
|
final capabilities = CallCapabilities.fromJson(content['capabilities']);
|
||||||
Logs().v(
|
Logs().v(
|
||||||
'[VOIP] CallCapabilities: dtmf => ${capabilities.dtmf}, transferee => ${capabilities.transferee}');
|
'[VOIP] CallCapabilities: dtmf => ${capabilities.dtmf}, transferee => ${capabilities.transferee}',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
var callType = CallType.kVoice;
|
var callType = CallType.kVoice;
|
||||||
|
|
@ -382,7 +411,8 @@ class VoIP {
|
||||||
sdpStreamMetadata.sdpStreamMetadatas
|
sdpStreamMetadata.sdpStreamMetadatas
|
||||||
.forEach((streamId, SDPStreamPurpose purpose) {
|
.forEach((streamId, SDPStreamPurpose purpose) {
|
||||||
Logs().v(
|
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) {
|
if (!purpose.video_muted) {
|
||||||
callType = CallType.kVideo;
|
callType = CallType.kVideo;
|
||||||
|
|
@ -419,7 +449,8 @@ class VoIP {
|
||||||
(confId == null ||
|
(confId == null ||
|
||||||
currentGroupCID != VoipId(roomId: room.id, callId: confId))) {
|
currentGroupCID != VoipId(roomId: room.id, callId: confId))) {
|
||||||
Logs().v(
|
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
|
// no need to emit here because handleNewCall was never triggered yet
|
||||||
await newCall.reject(reason: CallErrorCode.userBusy, shouldEmit: false);
|
await newCall.reject(reason: CallErrorCode.userBusy, shouldEmit: false);
|
||||||
await delegate.handleMissedCall(newCall);
|
await delegate.handleMissedCall(newCall);
|
||||||
|
|
@ -449,7 +480,12 @@ class VoIP {
|
||||||
currentCID = VoipId(roomId: room.id, callId: callId);
|
currentCID = VoipId(roomId: room.id, callId: callId);
|
||||||
|
|
||||||
await newCall.initWithInvite(
|
await newCall.initWithInvite(
|
||||||
callType, offer, sdpStreamMetadata, lifetime, confId != null);
|
callType,
|
||||||
|
offer,
|
||||||
|
sdpStreamMetadata,
|
||||||
|
lifetime,
|
||||||
|
confId != null,
|
||||||
|
);
|
||||||
|
|
||||||
// Popup CallingPage for incoming call.
|
// Popup CallingPage for incoming call.
|
||||||
if (confId == null && !newCall.callHasEnded) {
|
if (confId == null && !newCall.callHasEnded) {
|
||||||
|
|
@ -462,8 +498,12 @@ class VoIP {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> onCallAnswer(Room room, String remoteUserId,
|
Future<void> onCallAnswer(
|
||||||
String? remoteDeviceId, Map<String, dynamic> content) async {
|
Room room,
|
||||||
|
String remoteUserId,
|
||||||
|
String? remoteDeviceId,
|
||||||
|
Map<String, dynamic> content,
|
||||||
|
) async {
|
||||||
Logs().v('[VOIP] onCallAnswer => ${content.toString()}');
|
Logs().v('[VOIP] onCallAnswer => ${content.toString()}');
|
||||||
final String callId = content['call_id'];
|
final String callId = content['call_id'];
|
||||||
|
|
||||||
|
|
@ -478,31 +518,37 @@ class VoIP {
|
||||||
|
|
||||||
if (call.room.id != room.id) {
|
if (call.room.id != room.id) {
|
||||||
Logs().w(
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (call.remoteUserId == null) {
|
if (call.remoteUserId == null) {
|
||||||
Logs().i(
|
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;
|
call.remoteUserId = remoteUserId;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (call.remoteDeviceId == null) {
|
if (call.remoteDeviceId == null) {
|
||||||
Logs().i(
|
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;
|
call.remoteDeviceId = remoteDeviceId;
|
||||||
}
|
}
|
||||||
if (call.remotePartyId != null) {
|
if (call.remotePartyId != null) {
|
||||||
Logs().d(
|
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;
|
return;
|
||||||
} else {
|
} else {
|
||||||
call.remotePartyId = content['party_id'];
|
call.remotePartyId = content['party_id'];
|
||||||
}
|
}
|
||||||
|
|
||||||
final answer = RTCSessionDescription(
|
final answer = RTCSessionDescription(
|
||||||
content['answer']['sdp'], content['answer']['type']);
|
content['answer']['sdp'],
|
||||||
|
content['answer']['type'],
|
||||||
|
);
|
||||||
|
|
||||||
SDPStreamMetadata? metadata;
|
SDPStreamMetadata? metadata;
|
||||||
if (content[sdpStreamMetadataKey] != null) {
|
if (content[sdpStreamMetadataKey] != null) {
|
||||||
|
|
@ -535,11 +581,13 @@ class VoIP {
|
||||||
if (call != null) {
|
if (call != null) {
|
||||||
// hangup in any case, either if the other party hung up or we did on another device
|
// hangup in any case, either if the other party hung up or we did on another device
|
||||||
await call.terminate(
|
await call.terminate(
|
||||||
CallParty.kRemote,
|
CallParty.kRemote,
|
||||||
CallErrorCode.values.firstWhereOrNull(
|
CallErrorCode.values.firstWhereOrNull(
|
||||||
(element) => element.reason == content['reason']) ??
|
(element) => element.reason == content['reason'],
|
||||||
CallErrorCode.userHangup,
|
) ??
|
||||||
true);
|
CallErrorCode.userHangup,
|
||||||
|
true,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
Logs().v('[VOIP] onCallHangup: Session [$callId] not found!');
|
Logs().v('[VOIP] onCallHangup: Session [$callId] not found!');
|
||||||
}
|
}
|
||||||
|
|
@ -556,7 +604,8 @@ class VoIP {
|
||||||
if (call != null) {
|
if (call != null) {
|
||||||
await call.onRejectReceived(
|
await call.onRejectReceived(
|
||||||
CallErrorCode.values.firstWhereOrNull(
|
CallErrorCode.values.firstWhereOrNull(
|
||||||
(element) => element.reason == content['reason']) ??
|
(element) => element.reason == content['reason'],
|
||||||
|
) ??
|
||||||
CallErrorCode.userHangup,
|
CallErrorCode.userHangup,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -565,7 +614,9 @@ class VoIP {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> onCallSelectAnswer(
|
Future<void> onCallSelectAnswer(
|
||||||
Room room, Map<String, dynamic> content) async {
|
Room room,
|
||||||
|
Map<String, dynamic> content,
|
||||||
|
) async {
|
||||||
final String callId = content['call_id'];
|
final String callId = content['call_id'];
|
||||||
Logs().d('SelectAnswer received for call ID $callId');
|
Logs().d('SelectAnswer received for call ID $callId');
|
||||||
final String selectedPartyId = content['selected_party_id'];
|
final String selectedPartyId = content['selected_party_id'];
|
||||||
|
|
@ -574,7 +625,8 @@ class VoIP {
|
||||||
if (call != null) {
|
if (call != null) {
|
||||||
if (call.room.id != room.id) {
|
if (call.room.id != room.id) {
|
||||||
Logs().w(
|
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;
|
return;
|
||||||
}
|
}
|
||||||
await call.onSelectAnswerReceived(selectedPartyId);
|
await call.onSelectAnswerReceived(selectedPartyId);
|
||||||
|
|
@ -582,7 +634,9 @@ class VoIP {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> onSDPStreamMetadataChangedReceived(
|
Future<void> onSDPStreamMetadataChangedReceived(
|
||||||
Room room, Map<String, dynamic> content) async {
|
Room room,
|
||||||
|
Map<String, dynamic> content,
|
||||||
|
) async {
|
||||||
final String callId = content['call_id'];
|
final String callId = content['call_id'];
|
||||||
Logs().d('SDP Stream metadata received for call ID $callId');
|
Logs().d('SDP Stream metadata received for call ID $callId');
|
||||||
|
|
||||||
|
|
@ -593,12 +647,15 @@ class VoIP {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await call.onSDPStreamMetadataReceived(
|
await call.onSDPStreamMetadataReceived(
|
||||||
SDPStreamMetadata.fromJson(content[sdpStreamMetadataKey]));
|
SDPStreamMetadata.fromJson(content[sdpStreamMetadataKey]),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> onAssertedIdentityReceived(
|
Future<void> onAssertedIdentityReceived(
|
||||||
Room room, Map<String, dynamic> content) async {
|
Room room,
|
||||||
|
Map<String, dynamic> content,
|
||||||
|
) async {
|
||||||
final String callId = content['call_id'];
|
final String callId = content['call_id'];
|
||||||
Logs().d('Asserted identity received for call ID $callId');
|
Logs().d('Asserted identity received for call ID $callId');
|
||||||
|
|
||||||
|
|
@ -609,7 +666,8 @@ class VoIP {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
call.onAssertedIdentityReceived(
|
call.onAssertedIdentityReceived(
|
||||||
AssertedIdentity.fromJson(content['asserted_identity']));
|
AssertedIdentity.fromJson(content['asserted_identity']),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -630,8 +688,10 @@ class VoIP {
|
||||||
if (content[sdpStreamMetadataKey] != null) {
|
if (content[sdpStreamMetadataKey] != null) {
|
||||||
metadata = SDPStreamMetadata.fromJson(content[sdpStreamMetadataKey]);
|
metadata = SDPStreamMetadata.fromJson(content[sdpStreamMetadataKey]);
|
||||||
}
|
}
|
||||||
await call.onNegotiateReceived(metadata,
|
await call.onNegotiateReceived(
|
||||||
RTCSessionDescription(description['sdp'], description['type']));
|
metadata,
|
||||||
|
RTCSessionDescription(description['sdp'], description['type']),
|
||||||
|
);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logs().e('[VOIP] Failed to complete negotiation', e, s);
|
Logs().e('[VOIP] Failed to complete negotiation', e, s);
|
||||||
}
|
}
|
||||||
|
|
@ -668,7 +728,7 @@ class VoIP {
|
||||||
{
|
{
|
||||||
'username': _turnServerCredentials!.username,
|
'username': _turnServerCredentials!.username,
|
||||||
'credential': _turnServerCredentials!.password,
|
'credential': _turnServerCredentials!.password,
|
||||||
'urls': _turnServerCredentials!.uris
|
'urls': _turnServerCredentials!.uris,
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
@ -832,7 +892,8 @@ class VoIP {
|
||||||
}) async {
|
}) async {
|
||||||
if (membership.isExpired) {
|
if (membership.isExpired) {
|
||||||
Logs().d(
|
Logs().d(
|
||||||
'Ignoring expired membership in passive groupCall creator. ${membership.toJson()}');
|
'Ignoring expired membership in passive groupCall creator. ${membership.toJson()}',
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -859,7 +920,8 @@ class VoIP {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (groupCalls.containsKey(
|
if (groupCalls.containsKey(
|
||||||
VoipId(roomId: membership.roomId, callId: membership.callId))) {
|
VoipId(roomId: membership.roomId, callId: membership.callId),
|
||||||
|
)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -38,17 +38,19 @@ void main() {
|
||||||
'display_name': 'John Doe',
|
'display_name': 'John Doe',
|
||||||
'three_pids': [
|
'three_pids': [
|
||||||
{'medium': 'email', 'address': 'john.doe@example.org'},
|
{'medium': 'email', 'address': 'john.doe@example.org'},
|
||||||
{'medium': 'msisdn', 'address': '123456789'}
|
{'medium': 'msisdn', 'address': '123456789'},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
'{"a":null}': {'a': null},
|
'{"a":null}': {'a': null},
|
||||||
};
|
};
|
||||||
for (final entry in textMap.entries) {
|
for (final entry in textMap.entries) {
|
||||||
test(entry.key, () async {
|
test(entry.key, () async {
|
||||||
expect(
|
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
Loading…
Reference in New Issue