Merge branch 'master' into soru/cross-signing
This commit is contained in:
commit
29721f00a8
|
@ -54,6 +54,7 @@ code_analyze:
|
|||
image: cirrusci/flutter
|
||||
dependencies: []
|
||||
script:
|
||||
- flutter format lib/ test/ test_driver/ --set-exit-if-changed
|
||||
- flutter analyze
|
||||
|
||||
build-api-doc:
|
||||
|
|
|
@ -41,7 +41,10 @@ That means for example: "[users] add fetch users endpoint".
|
|||
- Directories need to be lowercase.
|
||||
|
||||
## Code style:
|
||||
- We recommend using Android Studio for coding. We are using the Android Studio auto styling with ctrl+alt+shift+L.
|
||||
Please use code formatting. You can use VSCode or Android Studio. On other editors you need to run:
|
||||
```
|
||||
flutter format lib/**/*/*.dart
|
||||
```
|
||||
|
||||
## Code quality:
|
||||
- Don't repeat yourself! Use local variables, functions, classes.
|
||||
|
|
|
@ -84,7 +84,8 @@ class Client {
|
|||
/// debug: Print debug output?
|
||||
/// database: The database instance to use
|
||||
/// enableE2eeRecovery: Enable additional logic to try to recover from bad e2ee sessions
|
||||
Client(this.clientName, {this.debug = false, this.database, this.enableE2eeRecovery = false}) {
|
||||
Client(this.clientName,
|
||||
{this.debug = false, this.database, this.enableE2eeRecovery = false}) {
|
||||
onLoginStateChanged.stream.listen((loginState) {
|
||||
print('LoginState: ${loginState.toString()}');
|
||||
});
|
||||
|
@ -1045,8 +1046,7 @@ class Client {
|
|||
}
|
||||
if (sync['account_data'] is Map<String, dynamic> &&
|
||||
sync['account_data']['events'] is List<dynamic>) {
|
||||
await _handleGlobalEvents(
|
||||
sync['account_data']['events'], 'account_data');
|
||||
await _handleGlobalEvents(sync['account_data']['events'], 'account_data');
|
||||
}
|
||||
if (sync['device_lists'] is Map<String, dynamic>) {
|
||||
await _handleDeviceListsEvents(sync['device_lists']);
|
||||
|
@ -1077,7 +1077,8 @@ class Client {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _handleDeviceListsEvents(Map<String, dynamic> deviceLists) async {
|
||||
Future<void> _handleDeviceListsEvents(
|
||||
Map<String, dynamic> deviceLists) async {
|
||||
if (deviceLists['changed'] is List) {
|
||||
for (final userId in deviceLists['changed']) {
|
||||
if (_userDeviceKeys.containsKey(userId)) {
|
||||
|
@ -1179,7 +1180,8 @@ class Client {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _handleRooms(Map<String, dynamic> rooms, Membership membership) async {
|
||||
Future<void> _handleRooms(
|
||||
Map<String, dynamic> rooms, Membership membership) async {
|
||||
for (final entry in rooms.entries) {
|
||||
final id = entry.key;
|
||||
final room = entry.value;
|
||||
|
@ -1252,8 +1254,7 @@ class Client {
|
|||
if (room['timeline'] is Map<String, dynamic> &&
|
||||
room['timeline']['events'] is List<dynamic> &&
|
||||
room['timeline']['events'].isNotEmpty) {
|
||||
await _handleRoomEvents(
|
||||
id, room['timeline']['events'], 'timeline');
|
||||
await _handleRoomEvents(id, room['timeline']['events'], 'timeline');
|
||||
handledEvents = true;
|
||||
}
|
||||
|
||||
|
@ -1318,7 +1319,8 @@ class Client {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _handleRoomEvents(String chat_id, List<dynamic> events, String type) async {
|
||||
Future<void> _handleRoomEvents(
|
||||
String chat_id, List<dynamic> events, String type) async {
|
||||
for (num i = 0; i < events.length; i++) {
|
||||
await _handleEvent(events[i], chat_id, type);
|
||||
}
|
||||
|
@ -1341,14 +1343,17 @@ class Client {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _handleEvent(Map<String, dynamic> event, String roomID, String type) async {
|
||||
Future<void> _handleEvent(
|
||||
Map<String, dynamic> event, String roomID, String type) async {
|
||||
if (event['type'] is String && event['content'] is Map<String, dynamic>) {
|
||||
// The client must ignore any new m.room.encryption event to prevent
|
||||
// man-in-the-middle attacks!
|
||||
final room = getRoomById(roomID);
|
||||
if (room == null ||
|
||||
(event['type'] == 'm.room.encryption' && room.encrypted &&
|
||||
event['content']['algorithm'] != room.getState('m.room.encryption')?.content['algorithm'])) {
|
||||
(event['type'] == 'm.room.encryption' &&
|
||||
room.encrypted &&
|
||||
event['content']['algorithm'] !=
|
||||
room.getState('m.room.encryption')?.content['algorithm'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1367,7 +1372,8 @@ class Client {
|
|||
}
|
||||
if (update.eventType == 'm.room.encrypted' && database != null) {
|
||||
// the event is still encrytped....let's try fetching the keys from the database!
|
||||
await room.loadInboundGroupSessionKey(event['content']['session_id'], event['content']['sender_key']);
|
||||
await room.loadInboundGroupSessionKey(
|
||||
event['content']['session_id'], event['content']['sender_key']);
|
||||
update = update.decrypt(room);
|
||||
}
|
||||
if (type != 'ephemeral' && database != null) {
|
||||
|
@ -1667,8 +1673,7 @@ class Client {
|
|||
}
|
||||
_userDeviceKeys[userId].deviceKeys[deviceId] = entry;
|
||||
if (deviceId == deviceID &&
|
||||
entry.ed25519Key ==
|
||||
fingerprintKey) {
|
||||
entry.ed25519Key == fingerprintKey) {
|
||||
// Always trust the own device
|
||||
entry.setDirectVerified(true);
|
||||
}
|
||||
|
@ -1684,7 +1689,7 @@ class Client {
|
|||
userId,
|
||||
deviceId,
|
||||
json.encode(entry.toJson()),
|
||||
entry.verified,
|
||||
entry.directVerified,
|
||||
entry.blocked,
|
||||
));
|
||||
}
|
||||
|
@ -1745,7 +1750,7 @@ class Client {
|
|||
userId,
|
||||
publicKey,
|
||||
json.encode(entry.toJson()),
|
||||
entry.verified,
|
||||
entry.directVerified,
|
||||
entry.blocked,
|
||||
));
|
||||
}
|
||||
|
|
|
@ -76,7 +76,8 @@ class Database extends _$Database {
|
|||
return res;
|
||||
}
|
||||
|
||||
Future<Map<String, List<olm.Session>>> getOlmSessions(int clientId, String userId) async {
|
||||
Future<Map<String, List<olm.Session>>> getOlmSessions(
|
||||
int clientId, String userId) async {
|
||||
final raw = await getAllOlmSessions(clientId).get();
|
||||
if (raw.isEmpty) {
|
||||
return {};
|
||||
|
@ -97,7 +98,8 @@ class Database extends _$Database {
|
|||
return res;
|
||||
}
|
||||
|
||||
Future<DbOutboundGroupSession> getDbOutboundGroupSession(int clientId, String roomId) async {
|
||||
Future<DbOutboundGroupSession> getDbOutboundGroupSession(
|
||||
int clientId, String roomId) async {
|
||||
final res = await dbGetOutboundGroupSession(clientId, roomId).get();
|
||||
if (res.isEmpty) {
|
||||
return null;
|
||||
|
@ -105,22 +107,28 @@ class Database extends _$Database {
|
|||
return res.first;
|
||||
}
|
||||
|
||||
Future<List<DbInboundGroupSession>> getDbInboundGroupSessions(int clientId, String roomId) async {
|
||||
Future<List<DbInboundGroupSession>> getDbInboundGroupSessions(
|
||||
int clientId, String roomId) async {
|
||||
return await dbGetInboundGroupSessionKeys(clientId, roomId).get();
|
||||
}
|
||||
|
||||
Future<DbInboundGroupSession> getDbInboundGroupSession(int clientId, String roomId, String sessionId) async {
|
||||
final res = await dbGetInboundGroupSessionKey(clientId, roomId, sessionId).get();
|
||||
Future<DbInboundGroupSession> getDbInboundGroupSession(
|
||||
int clientId, String roomId, String sessionId) async {
|
||||
final res =
|
||||
await dbGetInboundGroupSessionKey(clientId, roomId, sessionId).get();
|
||||
if (res.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return res.first;
|
||||
}
|
||||
|
||||
Future<List<sdk.Room>> getRoomList(sdk.Client client, {bool onlyLeft = false}) async {
|
||||
final res = await (select(rooms)..where((t) => onlyLeft
|
||||
Future<List<sdk.Room>> getRoomList(sdk.Client client,
|
||||
{bool onlyLeft = false}) async {
|
||||
final res = await (select(rooms)
|
||||
..where((t) => onlyLeft
|
||||
? t.membership.equals('leave')
|
||||
: t.membership.equals('leave').not())).get();
|
||||
: t.membership.equals('leave').not()))
|
||||
.get();
|
||||
final resStates = await getAllRoomStates(client.id).get();
|
||||
final resAccountData = await getAllRoomAccountData(client.id).get();
|
||||
final roomList = <sdk.Room>[];
|
||||
|
@ -157,11 +165,13 @@ class Database extends _$Database {
|
|||
/// Stores a RoomUpdate object in the database. Must be called inside of
|
||||
/// [transaction].
|
||||
final Set<String> _ensuredRooms = {};
|
||||
Future<void> storeRoomUpdate(int clientId, sdk.RoomUpdate roomUpdate, [sdk.Room oldRoom]) async {
|
||||
Future<void> storeRoomUpdate(int clientId, sdk.RoomUpdate roomUpdate,
|
||||
[sdk.Room oldRoom]) async {
|
||||
final setKey = '${clientId};${roomUpdate.id}';
|
||||
if (roomUpdate.membership != sdk.Membership.leave) {
|
||||
if (!_ensuredRooms.contains(setKey)) {
|
||||
await ensureRoomExists(clientId, roomUpdate.id, roomUpdate.membership.toString().split('.').last);
|
||||
await ensureRoomExists(clientId, roomUpdate.id,
|
||||
roomUpdate.membership.toString().split('.').last);
|
||||
_ensuredRooms.add(setKey);
|
||||
}
|
||||
} else {
|
||||
|
@ -174,23 +184,36 @@ class Database extends _$Database {
|
|||
if (!doUpdate) {
|
||||
doUpdate = roomUpdate.highlight_count != oldRoom.highlightCount ||
|
||||
roomUpdate.notification_count != oldRoom.notificationCount ||
|
||||
roomUpdate.membership.toString().split('.').last != oldRoom.membership.toString().split('.').last ||
|
||||
roomUpdate.membership.toString().split('.').last !=
|
||||
oldRoom.membership.toString().split('.').last ||
|
||||
(roomUpdate.summary?.mJoinedMemberCount != null &&
|
||||
roomUpdate.summary.mJoinedMemberCount != oldRoom.mInvitedMemberCount) ||
|
||||
roomUpdate.summary.mJoinedMemberCount !=
|
||||
oldRoom.mInvitedMemberCount) ||
|
||||
(roomUpdate.summary?.mInvitedMemberCount != null &&
|
||||
roomUpdate.summary.mJoinedMemberCount != oldRoom.mJoinedMemberCount) ||
|
||||
roomUpdate.summary.mJoinedMemberCount !=
|
||||
oldRoom.mJoinedMemberCount) ||
|
||||
(roomUpdate.summary?.mHeroes != null &&
|
||||
roomUpdate.summary.mHeroes.join(',') != oldRoom.mHeroes.join(','));
|
||||
roomUpdate.summary.mHeroes.join(',') !=
|
||||
oldRoom.mHeroes.join(','));
|
||||
}
|
||||
|
||||
if (doUpdate) {
|
||||
await (update(rooms)..where((r) => r.roomId.equals(roomUpdate.id) & r.clientId.equals(clientId))).write(RoomsCompanion(
|
||||
await (update(rooms)
|
||||
..where((r) =>
|
||||
r.roomId.equals(roomUpdate.id) & r.clientId.equals(clientId)))
|
||||
.write(RoomsCompanion(
|
||||
highlightCount: Value(roomUpdate.highlight_count),
|
||||
notificationCount: Value(roomUpdate.notification_count),
|
||||
membership: Value(roomUpdate.membership.toString().split('.').last),
|
||||
joinedMemberCount: roomUpdate.summary?.mJoinedMemberCount != null ? Value(roomUpdate.summary.mJoinedMemberCount) : Value.absent(),
|
||||
invitedMemberCount: roomUpdate.summary?.mInvitedMemberCount != null ? Value(roomUpdate.summary.mInvitedMemberCount) : Value.absent(),
|
||||
heroes: roomUpdate.summary?.mHeroes != null ? Value(roomUpdate.summary.mHeroes.join(',')) : Value.absent(),
|
||||
joinedMemberCount: roomUpdate.summary?.mJoinedMemberCount != null
|
||||
? Value(roomUpdate.summary.mJoinedMemberCount)
|
||||
: Value.absent(),
|
||||
invitedMemberCount: roomUpdate.summary?.mInvitedMemberCount != null
|
||||
? Value(roomUpdate.summary.mInvitedMemberCount)
|
||||
: Value.absent(),
|
||||
heroes: roomUpdate.summary?.mHeroes != null
|
||||
? Value(roomUpdate.summary.mHeroes.join(','))
|
||||
: Value.absent(),
|
||||
));
|
||||
}
|
||||
|
||||
|
@ -205,17 +228,24 @@ class Database extends _$Database {
|
|||
|
||||
/// Stores an UserUpdate object in the database. Must be called inside of
|
||||
/// [transaction].
|
||||
Future<void> storeUserEventUpdate(int clientId, sdk.UserUpdate userUpdate) async {
|
||||
Future<void> storeUserEventUpdate(
|
||||
int clientId, sdk.UserUpdate userUpdate) async {
|
||||
if (userUpdate.type == 'account_data') {
|
||||
await storeAccountData(clientId, userUpdate.eventType, json.encode(userUpdate.content['content']));
|
||||
await storeAccountData(clientId, userUpdate.eventType,
|
||||
json.encode(userUpdate.content['content']));
|
||||
} else if (userUpdate.type == 'presence') {
|
||||
await storePresence(clientId, userUpdate.eventType, userUpdate.content['sender'], json.encode(userUpdate.content['content']));
|
||||
await storePresence(
|
||||
clientId,
|
||||
userUpdate.eventType,
|
||||
userUpdate.content['sender'],
|
||||
json.encode(userUpdate.content['content']));
|
||||
}
|
||||
}
|
||||
|
||||
/// Stores an EventUpdate object in the database. Must be called inside of
|
||||
/// [transaction].
|
||||
Future<void> storeEventUpdate(int clientId, sdk.EventUpdate eventUpdate) async {
|
||||
Future<void> storeEventUpdate(
|
||||
int clientId, sdk.EventUpdate eventUpdate) async {
|
||||
if (eventUpdate.type == 'ephemeral') return;
|
||||
final eventContent = eventUpdate.content;
|
||||
final type = eventUpdate.type;
|
||||
|
@ -239,11 +269,13 @@ class Database extends _$Database {
|
|||
eventContent['unsigned'] is Map<String, dynamic> &&
|
||||
eventContent['unsigned']['transaction_id'] is String) {
|
||||
// status changed and we have an old transaction id --> update event id and stuffs
|
||||
await updateEventStatus(status, eventContent['event_id'], clientId, eventContent['unsigned']['transaction_id'], chatId);
|
||||
await updateEventStatus(status, eventContent['event_id'], clientId,
|
||||
eventContent['unsigned']['transaction_id'], chatId);
|
||||
} else {
|
||||
DbEvent oldEvent;
|
||||
if (type == 'history') {
|
||||
final allOldEvents = await getEvent(clientId, eventContent['event_id'], chatId).get();
|
||||
final allOldEvents =
|
||||
await getEvent(clientId, eventContent['event_id'], chatId).get();
|
||||
if (allOldEvents.isNotEmpty) {
|
||||
oldEvent = allOldEvents.first;
|
||||
}
|
||||
|
@ -253,7 +285,10 @@ class Database extends _$Database {
|
|||
eventContent['event_id'],
|
||||
chatId,
|
||||
oldEvent?.sortOrder ?? eventUpdate.sortOrder,
|
||||
eventContent['origin_server_ts'] != null ? DateTime.fromMillisecondsSinceEpoch(eventContent['origin_server_ts']) : DateTime.now(),
|
||||
eventContent['origin_server_ts'] != null
|
||||
? DateTime.fromMillisecondsSinceEpoch(
|
||||
eventContent['origin_server_ts'])
|
||||
: DateTime.now(),
|
||||
eventContent['sender'],
|
||||
eventContent['type'],
|
||||
json.encode(eventContent['unsigned'] ?? ''),
|
||||
|
@ -268,7 +303,8 @@ class Database extends _$Database {
|
|||
if (status != -1 &&
|
||||
eventUpdate.content.containsKey('unsigned') &&
|
||||
eventUpdate.content['unsigned']['transaction_id'] is String) {
|
||||
await removeEvent(clientId, eventUpdate.content['unsigned']['transaction_id'], chatId);
|
||||
await removeEvent(clientId,
|
||||
eventUpdate.content['unsigned']['transaction_id'], chatId);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -281,7 +317,10 @@ class Database extends _$Database {
|
|||
eventContent['event_id'] ?? now.millisecondsSinceEpoch.toString(),
|
||||
chatId,
|
||||
eventUpdate.sortOrder ?? 0.0,
|
||||
eventContent['origin_server_ts'] != null ? DateTime.fromMillisecondsSinceEpoch(eventContent['origin_server_ts']) : now,
|
||||
eventContent['origin_server_ts'] != null
|
||||
? DateTime.fromMillisecondsSinceEpoch(
|
||||
eventContent['origin_server_ts'])
|
||||
: now,
|
||||
eventContent['sender'],
|
||||
eventContent['type'],
|
||||
json.encode(eventContent['unsigned'] ?? ''),
|
||||
|
@ -299,7 +338,8 @@ class Database extends _$Database {
|
|||
}
|
||||
}
|
||||
|
||||
Future<sdk.Event> getEventById(int clientId, String eventId, sdk.Room room) async {
|
||||
Future<sdk.Event> getEventById(
|
||||
int clientId, String eventId, sdk.Room room) async {
|
||||
final event = await getEvent(clientId, eventId, room.id).get();
|
||||
if (event.isEmpty) {
|
||||
return null;
|
||||
|
@ -308,7 +348,9 @@ class Database extends _$Database {
|
|||
}
|
||||
|
||||
Future<bool> redactMessage(int clientId, sdk.EventUpdate eventUpdate) async {
|
||||
final events = await getEvent(clientId, eventUpdate.content['redacts'], eventUpdate.roomID).get();
|
||||
final events = await getEvent(
|
||||
clientId, eventUpdate.content['redacts'], eventUpdate.roomID)
|
||||
.get();
|
||||
var success = false;
|
||||
for (final dbEvent in events) {
|
||||
final event = sdk.Event.fromDb(dbEvent, null);
|
||||
|
@ -337,29 +379,44 @@ class Database extends _$Database {
|
|||
Future<void> forgetRoom(int clientId, String roomId) async {
|
||||
final setKey = '${clientId};${roomId}';
|
||||
_ensuredRooms.remove(setKey);
|
||||
await (delete(rooms)..where((r) => r.roomId.equals(roomId) & r.clientId.equals(clientId))).go();
|
||||
await (delete(events)..where((r) => r.roomId.equals(roomId) & r.clientId.equals(clientId))).go();
|
||||
await (delete(roomStates)..where((r) => r.roomId.equals(roomId) & r.clientId.equals(clientId))).go();
|
||||
await (delete(roomAccountData)..where((r) => r.roomId.equals(roomId) & r.clientId.equals(clientId))).go();
|
||||
await (delete(rooms)
|
||||
..where((r) => r.roomId.equals(roomId) & r.clientId.equals(clientId)))
|
||||
.go();
|
||||
await (delete(events)
|
||||
..where((r) => r.roomId.equals(roomId) & r.clientId.equals(clientId)))
|
||||
.go();
|
||||
await (delete(roomStates)
|
||||
..where((r) => r.roomId.equals(roomId) & r.clientId.equals(clientId)))
|
||||
.go();
|
||||
await (delete(roomAccountData)
|
||||
..where((r) => r.roomId.equals(roomId) & r.clientId.equals(clientId)))
|
||||
.go();
|
||||
}
|
||||
|
||||
Future<void> clearCache(int clientId) async {
|
||||
await (delete(presences)..where((r) => r.clientId.equals(clientId))).go();
|
||||
await (delete(roomAccountData)..where((r) => r.clientId.equals(clientId))).go();
|
||||
await (delete(roomAccountData)..where((r) => r.clientId.equals(clientId)))
|
||||
.go();
|
||||
await (delete(accountData)..where((r) => r.clientId.equals(clientId))).go();
|
||||
await (delete(roomStates)..where((r) => r.clientId.equals(clientId))).go();
|
||||
await (delete(events)..where((r) => r.clientId.equals(clientId))).go();
|
||||
await (delete(rooms)..where((r) => r.clientId.equals(clientId))).go();
|
||||
await (delete(outboundGroupSessions)..where((r) => r.clientId.equals(clientId))).go();
|
||||
await (delete(outboundGroupSessions)
|
||||
..where((r) => r.clientId.equals(clientId)))
|
||||
.go();
|
||||
await storePrevBatch(null, clientId);
|
||||
}
|
||||
|
||||
Future<void> clear(int clientId) async {
|
||||
await clearCache(clientId);
|
||||
await (delete(inboundGroupSessions)..where((r) => r.clientId.equals(clientId))).go();
|
||||
await (delete(inboundGroupSessions)
|
||||
..where((r) => r.clientId.equals(clientId)))
|
||||
.go();
|
||||
await (delete(olmSessions)..where((r) => r.clientId.equals(clientId))).go();
|
||||
await (delete(userDeviceKeysKey)..where((r) => r.clientId.equals(clientId))).go();
|
||||
await (delete(userDeviceKeys)..where((r) => r.clientId.equals(clientId))).go();
|
||||
await (delete(userDeviceKeysKey)..where((r) => r.clientId.equals(clientId)))
|
||||
.go();
|
||||
await (delete(userDeviceKeys)..where((r) => r.clientId.equals(clientId)))
|
||||
.go();
|
||||
await (delete(clients)..where((r) => r.clientId.equals(clientId))).go();
|
||||
}
|
||||
|
||||
|
|
|
@ -42,7 +42,15 @@ class Presence {
|
|||
final String statusMsg;
|
||||
final DateTime time;
|
||||
|
||||
Presence({this.sender, this.displayname, this.avatarUrl, this.currentlyActive, this.lastActiveAgo, this.presence, this.statusMsg, this.time});
|
||||
Presence(
|
||||
{this.sender,
|
||||
this.displayname,
|
||||
this.avatarUrl,
|
||||
this.currentlyActive,
|
||||
this.lastActiveAgo,
|
||||
this.presence,
|
||||
this.statusMsg,
|
||||
this.time});
|
||||
|
||||
Presence.fromJson(Map<String, dynamic> json)
|
||||
: sender = json['sender'],
|
||||
|
@ -66,15 +74,16 @@ class Presence {
|
|||
return Presence(
|
||||
sender: dbEntry.sender,
|
||||
displayname: content['displayname'],
|
||||
avatarUrl: content['avatar_url'] != null ? Uri.parse(content['avatar_url']) : null,
|
||||
avatarUrl: content['avatar_url'] != null
|
||||
? Uri.parse(content['avatar_url'])
|
||||
: null,
|
||||
currentlyActive: content['currently_active'],
|
||||
lastActiveAgo: content['last_active_ago'],
|
||||
time: DateTime.fromMillisecondsSinceEpoch(
|
||||
DateTime.now().millisecondsSinceEpoch -
|
||||
(content['last_active_ago'] ?? 0)),
|
||||
presence: PresenceType.values.firstWhere(
|
||||
(e) =>
|
||||
e.toString() == "PresenceType.${content['presence']}",
|
||||
(e) => e.toString() == "PresenceType.${content['presence']}",
|
||||
orElse: () => null),
|
||||
statusMsg: content['status_msg'],
|
||||
);
|
||||
|
|
|
@ -112,7 +112,8 @@ class Room {
|
|||
}
|
||||
|
||||
Future<void> updateSortOrder() async {
|
||||
await client.database?.updateRoomSortOrder(_oldestSortOrder, _newestSortOrder, client.id, id);
|
||||
await client.database?.updateRoomSortOrder(
|
||||
_oldestSortOrder, _newestSortOrder, client.id, id);
|
||||
}
|
||||
|
||||
/// Clears the existing outboundGroupSession, tries to create a new one and
|
||||
|
@ -165,10 +166,12 @@ class Room {
|
|||
Future<void> _storeOutboundGroupSession() async {
|
||||
if (_outboundGroupSession == null) return;
|
||||
await client.database?.storeOutboundGroupSession(
|
||||
client.id, id, _outboundGroupSession.pickle(client.userID),
|
||||
json.encode(_outboundGroupSessionDevices), _outboundGroupSessionCreationTime,
|
||||
_outboundGroupSessionSentMessages
|
||||
);
|
||||
client.id,
|
||||
id,
|
||||
_outboundGroupSession.pickle(client.userID),
|
||||
json.encode(_outboundGroupSessionDevices),
|
||||
_outboundGroupSessionCreationTime,
|
||||
_outboundGroupSessionSentMessages);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -190,21 +193,27 @@ class Room {
|
|||
}
|
||||
// next check if it needs to be rotated
|
||||
final encryptionContent = getState('m.room.encryption')?.content;
|
||||
final maxMessages = encryptionContent != null && encryptionContent['rotation_period_msgs'] is int
|
||||
final maxMessages = encryptionContent != null &&
|
||||
encryptionContent['rotation_period_msgs'] is int
|
||||
? encryptionContent['rotation_period_msgs']
|
||||
: 100;
|
||||
final maxAge = encryptionContent != null && encryptionContent['rotation_period_ms'] is int
|
||||
final maxAge = encryptionContent != null &&
|
||||
encryptionContent['rotation_period_ms'] is int
|
||||
? encryptionContent['rotation_period_ms']
|
||||
: 604800000; // default of one week
|
||||
if (_outboundGroupSessionSentMessages >= maxMessages ||
|
||||
_outboundGroupSessionCreationTime.add(Duration(milliseconds: maxAge)).isBefore(DateTime.now())) {
|
||||
_outboundGroupSessionCreationTime
|
||||
.add(Duration(milliseconds: maxAge))
|
||||
.isBefore(DateTime.now())) {
|
||||
wipe = true;
|
||||
}
|
||||
if (!wipe) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!wipe && _outboundGroupSessionDevices == null && _outboundGroupSession == null) {
|
||||
if (!wipe &&
|
||||
_outboundGroupSessionDevices == null &&
|
||||
_outboundGroupSession == null) {
|
||||
return true; // let's just short-circuit out of here, no need to do DB stuff
|
||||
}
|
||||
_outboundGroupSessionDevices = null;
|
||||
|
@ -250,8 +259,12 @@ class Room {
|
|||
indexes: {},
|
||||
key: client.userID,
|
||||
);
|
||||
client.database?.storeInboundGroupSession(client.id, id, sessionId,
|
||||
inboundGroupSession.pickle(client.userID), json.encode(content),
|
||||
client.database?.storeInboundGroupSession(
|
||||
client.id,
|
||||
id,
|
||||
sessionId,
|
||||
inboundGroupSession.pickle(client.userID),
|
||||
json.encode(content),
|
||||
json.encode({}),
|
||||
);
|
||||
_tryAgainDecryptLastMessage();
|
||||
|
@ -398,8 +411,7 @@ class Room {
|
|||
states.forEach((final String key, final entry) {
|
||||
if (!entry.containsKey('')) return;
|
||||
final Event state = entry[''];
|
||||
if (state.sortOrder != null &&
|
||||
state.sortOrder > lastSortOrder) {
|
||||
if (state.sortOrder != null && state.sortOrder > lastSortOrder) {
|
||||
lastSortOrder = state.sortOrder;
|
||||
lastEvent = state;
|
||||
}
|
||||
|
@ -436,7 +448,8 @@ class Room {
|
|||
this.roomAccountData = const {},
|
||||
double newestSortOrder = 0.0,
|
||||
double oldestSortOrder = 0.0,
|
||||
}) : _newestSortOrder = newestSortOrder, _oldestSortOrder = oldestSortOrder;
|
||||
}) : _newestSortOrder = newestSortOrder,
|
||||
_oldestSortOrder = oldestSortOrder;
|
||||
|
||||
/// The default count of how much events should be requested when requesting the
|
||||
/// history of this room.
|
||||
|
@ -519,28 +532,79 @@ class Room {
|
|||
/// return all current emote packs for this room
|
||||
Map<String, Map<String, String>> get emotePacks {
|
||||
final packs = <String, Map<String, String>>{};
|
||||
final addEmotePack = (String packName, Map<String, dynamic> content) {
|
||||
final normalizeEmotePackName = (String name) {
|
||||
name = name.replaceAll(' ', '-');
|
||||
name = name.replaceAll(RegExp(r'[^\w-]'), '');
|
||||
return name.toLowerCase();
|
||||
};
|
||||
final addEmotePack = (String packName, Map<String, dynamic> content,
|
||||
[String packNameOverride]) {
|
||||
if (!(content['short'] is Map)) {
|
||||
return;
|
||||
}
|
||||
if (content['pack'] is Map && content['pack']['name'] is String) {
|
||||
packName = content['pack']['name'];
|
||||
}
|
||||
if (packNameOverride != null && packNameOverride.isNotEmpty) {
|
||||
packName = packNameOverride;
|
||||
}
|
||||
packName = normalizeEmotePackName(packName);
|
||||
if (!packs.containsKey(packName)) {
|
||||
packs[packName] = <String, String>{};
|
||||
content.forEach((key, value) {
|
||||
}
|
||||
content['short'].forEach((key, value) {
|
||||
if (key is String && value is String && value.startsWith('mxc://')) {
|
||||
packs[packName][key] = value;
|
||||
}
|
||||
});
|
||||
};
|
||||
final roomEmotes = getState('im.ponies.room_emotes');
|
||||
final userEmotes = client.accountData['im.ponies.user_emotes'];
|
||||
if (roomEmotes != null && roomEmotes.content['short'] is Map) {
|
||||
addEmotePack('room', roomEmotes.content['short']);
|
||||
// first add all the room emotes
|
||||
final allRoomEmotes = states.states['im.ponies.room_emotes'];
|
||||
if (allRoomEmotes != null) {
|
||||
for (final entry in allRoomEmotes.entries) {
|
||||
final stateKey = entry.key;
|
||||
final event = entry.value;
|
||||
addEmotePack(stateKey.isEmpty ? 'room' : stateKey, event.content);
|
||||
}
|
||||
}
|
||||
// next add all the user emotes
|
||||
final userEmotes = client.accountData['im.ponies.user_emotes'];
|
||||
if (userEmotes != null) {
|
||||
addEmotePack('user', userEmotes.content);
|
||||
}
|
||||
// finally add all the external emote rooms
|
||||
final emoteRooms = client.accountData['im.ponies.emote_rooms'];
|
||||
if (emoteRooms != null && emoteRooms.content['rooms'] is Map) {
|
||||
for (final roomEntry in emoteRooms.content['rooms'].entries) {
|
||||
final roomId = roomEntry.key;
|
||||
if (roomId == id) {
|
||||
continue;
|
||||
}
|
||||
final room = client.getRoomById(roomId);
|
||||
if (room != null && roomEntry.value is Map) {
|
||||
for (final stateKeyEntry in roomEntry.value.entries) {
|
||||
final stateKey = stateKeyEntry.key;
|
||||
final event = room.getState('im.ponies.room_emotes', stateKey);
|
||||
if (event != null && stateKeyEntry.value is Map) {
|
||||
addEmotePack(
|
||||
room.canonicalAlias.isEmpty ? room.id : canonicalAlias,
|
||||
event.content,
|
||||
stateKeyEntry.value['name']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (userEmotes != null && userEmotes.content['short'] is Map) {
|
||||
addEmotePack('user', userEmotes.content['short']);
|
||||
}
|
||||
return packs;
|
||||
}
|
||||
|
||||
/// Sends a normal text message to this room. Returns the event ID generated
|
||||
/// by the server for this message.
|
||||
Future<String> sendTextEvent(String message, {String txid, Event inReplyTo, bool parseMarkdown = true, Map<String, Map<String, String>> emotePacks}) {
|
||||
Future<String> sendTextEvent(String message,
|
||||
{String txid,
|
||||
Event inReplyTo,
|
||||
bool parseMarkdown = true,
|
||||
Map<String, Map<String, String>> emotePacks}) {
|
||||
final event = <String, dynamic>{
|
||||
'msgtype': 'm.text',
|
||||
'body': message,
|
||||
|
@ -793,7 +857,11 @@ class Room {
|
|||
|
||||
final sortOrder = newSortOrder;
|
||||
// Display a *sending* event and store it.
|
||||
var eventUpdate = EventUpdate(type: 'timeline', roomID: id, eventType: type, sortOrder: sortOrder,
|
||||
var eventUpdate = EventUpdate(
|
||||
type: 'timeline',
|
||||
roomID: id,
|
||||
eventType: type,
|
||||
sortOrder: sortOrder,
|
||||
content: {
|
||||
'type': type,
|
||||
'event_id': messageID,
|
||||
|
@ -951,7 +1019,8 @@ class Room {
|
|||
|
||||
final dbActions = <Future<dynamic> Function()>[];
|
||||
if (client.database != null) {
|
||||
dbActions.add(() => client.database.setRoomPrevBatch(prev_batch, client.id, id));
|
||||
dbActions.add(
|
||||
() => client.database.setRoomPrevBatch(prev_batch, client.id, id));
|
||||
}
|
||||
|
||||
if (!(resp['chunk'] is List<dynamic> &&
|
||||
|
@ -969,7 +1038,8 @@ class Room {
|
|||
).decrypt(this);
|
||||
client.onEvent.add(eventUpdate);
|
||||
if (client.database != null) {
|
||||
dbActions.add(() => client.database.storeEventUpdate(client.id, eventUpdate));
|
||||
dbActions.add(
|
||||
() => client.database.storeEventUpdate(client.id, eventUpdate));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -985,11 +1055,13 @@ class Room {
|
|||
).decrypt(this);
|
||||
client.onEvent.add(eventUpdate);
|
||||
if (client.database != null) {
|
||||
dbActions.add(() => client.database.storeEventUpdate(client.id, eventUpdate));
|
||||
dbActions.add(
|
||||
() => client.database.storeEventUpdate(client.id, eventUpdate));
|
||||
}
|
||||
}
|
||||
if (client.database != null) {
|
||||
dbActions.add(() => client.database.setRoomPrevBatch(resp['end'], client.id, id));
|
||||
dbActions.add(
|
||||
() => client.database.setRoomPrevBatch(resp['end'], client.id, id));
|
||||
}
|
||||
await client.database?.transaction(() async {
|
||||
for (final f in dbActions) {
|
||||
|
@ -1063,10 +1135,10 @@ class Room {
|
|||
/// state are also given, the method will await them.
|
||||
static Future<Room> getRoomFromTableRow(
|
||||
DbRoom row, // either Map<String, dynamic> or DbRoom
|
||||
Client matrix,
|
||||
{
|
||||
Client matrix, {
|
||||
dynamic states, // DbRoomState, as iterator and optionally as future
|
||||
dynamic roomAccountData, // DbRoomAccountData, as iterator and optionally as future
|
||||
dynamic
|
||||
roomAccountData, // DbRoomAccountData, as iterator and optionally as future
|
||||
}) async {
|
||||
final newRoom = Room(
|
||||
id: row.roomId,
|
||||
|
@ -1093,7 +1165,8 @@ class Room {
|
|||
rawStates = states;
|
||||
}
|
||||
for (final rawState in rawStates) {
|
||||
final newState = Event.fromDb(rawState, newRoom);;
|
||||
final newState = Event.fromDb(rawState, newRoom);
|
||||
;
|
||||
newRoom.setState(newState);
|
||||
}
|
||||
}
|
||||
|
@ -1136,7 +1209,8 @@ class Room {
|
|||
await events[i].loadSession();
|
||||
events[i] = events[i].decrypted;
|
||||
if (events[i].type != EventTypes.Encrypted) {
|
||||
await client.database.storeEventUpdate(client.id,
|
||||
await client.database.storeEventUpdate(
|
||||
client.id,
|
||||
EventUpdate(
|
||||
eventType: events[i].typeKey,
|
||||
content: events[i].toJson(),
|
||||
|
@ -1258,7 +1332,8 @@ class Room {
|
|||
'content': resp,
|
||||
'state_key': mxID,
|
||||
};
|
||||
await client.database.storeEventUpdate(client.id,
|
||||
await client.database.storeEventUpdate(
|
||||
client.id,
|
||||
EventUpdate(
|
||||
content: content,
|
||||
roomID: id,
|
||||
|
@ -1705,7 +1780,8 @@ class Room {
|
|||
var users = await requestParticipants();
|
||||
for (final user in users) {
|
||||
if (client.userDeviceKeys.containsKey(user.id)) {
|
||||
for (var deviceKeyEntry in client.userDeviceKeys[user.id].deviceKeys.values) {
|
||||
for (var deviceKeyEntry
|
||||
in client.userDeviceKeys[user.id].deviceKeys.values) {
|
||||
deviceKeys.add(deviceKeyEntry);
|
||||
}
|
||||
}
|
||||
|
@ -1719,12 +1795,12 @@ class Room {
|
|||
if (_restoredOutboundGroupSession || client.database == null) {
|
||||
return;
|
||||
}
|
||||
final outboundSession = await client.database.getDbOutboundGroupSession(client.id, id);
|
||||
final outboundSession =
|
||||
await client.database.getDbOutboundGroupSession(client.id, id);
|
||||
if (outboundSession != null) {
|
||||
try {
|
||||
_outboundGroupSession = olm.OutboundGroupSession();
|
||||
_outboundGroupSession.unpickle(
|
||||
client.userID, outboundSession.pickle);
|
||||
_outboundGroupSession.unpickle(client.userID, outboundSession.pickle);
|
||||
_outboundGroupSessionDevices =
|
||||
List<String>.from(json.decode(outboundSession.deviceIds));
|
||||
_outboundGroupSessionCreationTime = outboundSession.creationTime;
|
||||
|
@ -1809,19 +1885,26 @@ class Room {
|
|||
toUsers: users);
|
||||
}
|
||||
|
||||
Future<void> loadInboundGroupSessionKey(String sessionId, [String senderKey]) async {
|
||||
if (sessionId == null || inboundGroupSessions.containsKey(sessionId)) return; // nothing to do
|
||||
final session = await client.database.getDbInboundGroupSession(client.id, id, sessionId);
|
||||
Future<void> loadInboundGroupSessionKey(String sessionId,
|
||||
[String senderKey]) async {
|
||||
if (sessionId == null || inboundGroupSessions.containsKey(sessionId)) {
|
||||
return;
|
||||
} // nothing to do
|
||||
final session = await client.database
|
||||
.getDbInboundGroupSession(client.id, id, sessionId);
|
||||
if (session == null) {
|
||||
// no session found, let's request it!
|
||||
if (client.enableE2eeRecovery && !_requestedSessionIds.contains(sessionId) && senderKey != null) {
|
||||
if (client.enableE2eeRecovery &&
|
||||
!_requestedSessionIds.contains(sessionId) &&
|
||||
senderKey != null) {
|
||||
unawaited(requestSessionKey(sessionId, senderKey));
|
||||
_requestedSessionIds.add(sessionId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
_inboundGroupSessions[sessionId] = SessionKey.fromDb(session, client.userID);
|
||||
_inboundGroupSessions[sessionId] =
|
||||
SessionKey.fromDb(session, client.userID);
|
||||
} catch (e) {
|
||||
print('[LibOlm] Could not unpickle inboundGroupSession: ' + e.toString());
|
||||
}
|
||||
|
@ -1844,7 +1927,8 @@ class Room {
|
|||
/// Returns a m.bad.encrypted event if it fails and does nothing if the event
|
||||
/// was not encrypted.
|
||||
Event decryptGroupMessage(Event event) {
|
||||
if (event.type != EventTypes.Encrypted || event.content['ciphertext'] == null) return event;
|
||||
if (event.type != EventTypes.Encrypted ||
|
||||
event.content['ciphertext'] == null) return event;
|
||||
Map<String, dynamic> decryptedPayload;
|
||||
try {
|
||||
if (!client.encryptionEnabled) {
|
||||
|
@ -1862,7 +1946,9 @@ class Room {
|
|||
.decrypt(event.content['ciphertext']);
|
||||
final messageIndexKey =
|
||||
event.eventId + event.time.millisecondsSinceEpoch.toString();
|
||||
if (inboundGroupSessions[sessionId].indexes.containsKey(messageIndexKey) &&
|
||||
if (inboundGroupSessions[sessionId]
|
||||
.indexes
|
||||
.containsKey(messageIndexKey) &&
|
||||
inboundGroupSessions[sessionId].indexes[messageIndexKey] !=
|
||||
decryptResult.message_index) {
|
||||
if ((_outboundGroupSession?.session_id() ?? '') == sessionId) {
|
||||
|
@ -1876,11 +1962,17 @@ class Room {
|
|||
// the entry should always exist. In the case it doesn't, the following
|
||||
// line *could* throw an error. As that is a future, though, and we call
|
||||
// it un-awaited here, nothing happens, which is exactly the result we want
|
||||
client.database?.updateInboundGroupSessionIndexes(json.encode(inboundGroupSessions[sessionId].indexes), client.id, id, sessionId);
|
||||
client.database?.updateInboundGroupSessionIndexes(
|
||||
json.encode(inboundGroupSessions[sessionId].indexes),
|
||||
client.id,
|
||||
id,
|
||||
sessionId);
|
||||
decryptedPayload = json.decode(decryptResult.plaintext);
|
||||
} catch (exception) {
|
||||
// alright, if this was actually by our own outbound group session, we might as well clear it
|
||||
if (client.enableE2eeRecovery && (_outboundGroupSession?.session_id() ?? '') == event.content['session_id']) {
|
||||
if (client.enableE2eeRecovery &&
|
||||
(_outboundGroupSession?.session_id() ?? '') ==
|
||||
event.content['session_id']) {
|
||||
clearOutboundGroupSession(wipe: true);
|
||||
}
|
||||
if (exception.toString() == DecryptError.UNKNOWN_SESSION) {
|
||||
|
|
|
@ -43,7 +43,8 @@ class EventUpdate {
|
|||
// the order where to stort this event
|
||||
final double sortOrder;
|
||||
|
||||
EventUpdate({this.eventType, this.roomID, this.type, this.content, this.sortOrder});
|
||||
EventUpdate(
|
||||
{this.eventType, this.roomID, this.type, this.content, this.sortOrder});
|
||||
|
||||
EventUpdate decrypt(Room room) {
|
||||
if (eventType != 'm.room.encrypted') {
|
||||
|
|
|
@ -26,6 +26,7 @@ import 'dart:async';
|
|||
import 'event.dart';
|
||||
import 'room.dart';
|
||||
import 'sync/event_update.dart';
|
||||
import 'sync/room_update.dart';
|
||||
|
||||
typedef onTimelineUpdateCallback = void Function();
|
||||
typedef onTimelineInsertCallback = void Function(int insertID);
|
||||
|
@ -41,6 +42,7 @@ class Timeline {
|
|||
final onTimelineInsertCallback onInsert;
|
||||
|
||||
StreamSubscription<EventUpdate> sub;
|
||||
StreamSubscription<RoomUpdate> roomSub;
|
||||
StreamSubscription<String> sessionIdReceivedSub;
|
||||
bool _requestingHistoryLock = false;
|
||||
|
||||
|
@ -77,6 +79,13 @@ class Timeline {
|
|||
|
||||
Timeline({this.room, this.events, this.onUpdate, this.onInsert}) {
|
||||
sub ??= room.client.onEvent.stream.listen(_handleEventUpdate);
|
||||
// if the timeline is limited we want to clear our events cache
|
||||
// as r.limitedTimeline can be "null" sometimes, we need to check for == true
|
||||
// as after receiving a limited timeline room update new events are expected
|
||||
// to be received via the onEvent stream, it is unneeded to call sortAndUpdate
|
||||
roomSub ??= room.client.onRoomUpdate.stream
|
||||
.where((r) => r.id == room.id && r.limitedTimeline == true)
|
||||
.listen((r) => events.clear());
|
||||
sessionIdReceivedSub ??=
|
||||
room.onSessionKeyReceived.stream.listen(_sessionKeyReceived);
|
||||
}
|
||||
|
@ -84,6 +93,7 @@ class Timeline {
|
|||
/// Don't forget to call this before you dismiss this object!
|
||||
void cancelSubscriptions() {
|
||||
sub?.cancel();
|
||||
roomSub?.cancel();
|
||||
sessionIdReceivedSub?.cancel();
|
||||
}
|
||||
|
||||
|
@ -121,8 +131,8 @@ class Timeline {
|
|||
if (eventUpdate.eventType == 'm.room.redaction') {
|
||||
final eventId = _findEvent(event_id: eventUpdate.content['redacts']);
|
||||
if (eventId != null) {
|
||||
events[eventId]
|
||||
.setRedactionEvent(Event.fromJson(eventUpdate.content, room, eventUpdate.sortOrder));
|
||||
events[eventId].setRedactionEvent(Event.fromJson(
|
||||
eventUpdate.content, room, eventUpdate.sortOrder));
|
||||
}
|
||||
} else if (eventUpdate.content['status'] == -2) {
|
||||
var i = _findEvent(event_id: eventUpdate.content['event_id']);
|
||||
|
@ -138,18 +148,23 @@ class Timeline {
|
|||
: null);
|
||||
|
||||
if (i < events.length) {
|
||||
events[i] = Event.fromJson(eventUpdate.content, room, eventUpdate.sortOrder);
|
||||
events[i] = Event.fromJson(
|
||||
eventUpdate.content, room, eventUpdate.sortOrder);
|
||||
}
|
||||
} else {
|
||||
Event newEvent;
|
||||
var senderUser = room.getState('m.room.member', eventUpdate.content['sender'])?.asUser ?? await room.client.database
|
||||
?.getUser(room.client.id, eventUpdate.content['sender'], room);
|
||||
var senderUser = room
|
||||
.getState('m.room.member', eventUpdate.content['sender'])
|
||||
?.asUser ??
|
||||
await room.client.database?.getUser(
|
||||
room.client.id, eventUpdate.content['sender'], room);
|
||||
if (senderUser != null) {
|
||||
eventUpdate.content['displayname'] = senderUser.displayName;
|
||||
eventUpdate.content['avatar_url'] = senderUser.avatarUrl.toString();
|
||||
}
|
||||
|
||||
newEvent = Event.fromJson(eventUpdate.content, room, eventUpdate.sortOrder);
|
||||
newEvent =
|
||||
Event.fromJson(eventUpdate.content, room, eventUpdate.sortOrder);
|
||||
|
||||
if (eventUpdate.type == 'history' &&
|
||||
events.indexWhere(
|
||||
|
|
|
@ -258,11 +258,17 @@ class DeviceKeys extends _SignedKey {
|
|||
|
||||
String get curve25519Key => keys['curve25519:$deviceId'];
|
||||
|
||||
bool get isValid => userId != null && deviceId != null && keys != null && curve25519Key != null && ed25519Key != null;
|
||||
bool get isValid =>
|
||||
userId != null &&
|
||||
deviceId != null &&
|
||||
keys != null &&
|
||||
curve25519Key != null &&
|
||||
ed25519Key != null;
|
||||
|
||||
Future<void> setVerified(bool newVerified) {
|
||||
Future<void> setVerified(bool newVerified, Client client) {
|
||||
_verified = newVerified;
|
||||
return client.database?.setVerifiedUserDeviceKey(newVerified, client.id, userId, deviceId);
|
||||
return client.database
|
||||
?.setVerifiedUserDeviceKey(newVerified, client.id, userId, deviceId);
|
||||
}
|
||||
|
||||
Future<void> setBlocked(bool newBlocked) {
|
||||
|
@ -273,7 +279,8 @@ class DeviceKeys extends _SignedKey {
|
|||
room.clearOutboundGroupSession();
|
||||
}
|
||||
}
|
||||
return client.database?.setBlockedUserDeviceKey(newBlocked, client.id, userId, deviceId);
|
||||
return client.database
|
||||
?.setBlockedUserDeviceKey(newBlocked, client.id, userId, deviceId);
|
||||
}
|
||||
|
||||
DeviceKeys.fromDb(DbUserDeviceKeysKey dbEntry, Client cl) {
|
||||
|
@ -282,10 +289,12 @@ class DeviceKeys extends _SignedKey {
|
|||
content = Map<String, dynamic>.from(json);
|
||||
userId = dbEntry.userId;
|
||||
identifier = dbEntry.deviceId;
|
||||
algorithms = json['algorithms'].cast<String>();
|
||||
keys = json['keys'] != null ? Map<String, String>.from(json['keys']) : null;
|
||||
signatures = json['signatures'] != null
|
||||
? Map<String, dynamic>.from(json['signatures'])
|
||||
algorithms = content['algorithms'].cast<String>();
|
||||
keys = content['keys'] != null
|
||||
? Map<String, String>.from(content['keys'])
|
||||
: null;
|
||||
signatures = content['signatures'] != null
|
||||
? Map<String, dynamic>.from(content['signatures'])
|
||||
: null;
|
||||
unsigned = json['unsigned'] != null
|
||||
? Map<String, dynamic>.from(json['unsigned'])
|
||||
|
@ -311,8 +320,9 @@ class DeviceKeys extends _SignedKey {
|
|||
blocked = json['blocked'] ?? false;
|
||||
}
|
||||
|
||||
KeyVerification startVerification() {
|
||||
final request = KeyVerification(client: client, userId: userId, deviceId: deviceId);
|
||||
KeyVerification startVerification(Client client) {
|
||||
final request =
|
||||
KeyVerification(client: client, userId: userId, deviceId: deviceId);
|
||||
request.start();
|
||||
client.addKeyVerificationRequest(request);
|
||||
return request;
|
||||
|
|
|
@ -42,7 +42,14 @@ import '../room.dart';
|
|||
| |
|
||||
*/
|
||||
|
||||
enum KeyVerificationState { askAccept, waitingAccept, askSas, waitingSas, done, error }
|
||||
enum KeyVerificationState {
|
||||
askAccept,
|
||||
waitingAccept,
|
||||
askSas,
|
||||
waitingSas,
|
||||
done,
|
||||
error
|
||||
}
|
||||
|
||||
List<String> _intersect(List<String> a, List<dynamic> b) {
|
||||
final res = <String>[];
|
||||
|
@ -76,7 +83,8 @@ List<int> _bytesToInt(Uint8List bytes, int totalBits) {
|
|||
|
||||
final VERIFICATION_METHODS = [_KeyVerificationMethodSas.type];
|
||||
|
||||
_KeyVerificationMethod _makeVerificationMethod(String type, KeyVerification request) {
|
||||
_KeyVerificationMethod _makeVerificationMethod(
|
||||
String type, KeyVerification request) {
|
||||
if (type == _KeyVerificationMethodSas.type) {
|
||||
return _KeyVerificationMethodSas(request: request);
|
||||
}
|
||||
|
@ -104,7 +112,8 @@ class KeyVerification {
|
|||
String canceledCode;
|
||||
String canceledReason;
|
||||
|
||||
KeyVerification({this.client, this.room, this.userId, String deviceId, this.onUpdate}) {
|
||||
KeyVerification(
|
||||
{this.client, this.room, this.userId, String deviceId, this.onUpdate}) {
|
||||
lastActivity = DateTime.now();
|
||||
_deviceId ??= deviceId;
|
||||
}
|
||||
|
@ -115,9 +124,10 @@ class KeyVerification {
|
|||
}
|
||||
|
||||
static String getTransactionId(Map<String, dynamic> payload) {
|
||||
return payload['transaction_id'] ?? (
|
||||
payload['m.relates_to'] is Map ? payload['m.relates_to']['event_id'] : null
|
||||
);
|
||||
return payload['transaction_id'] ??
|
||||
(payload['m.relates_to'] is Map
|
||||
? payload['m.relates_to']['event_id']
|
||||
: null);
|
||||
}
|
||||
|
||||
Future<void> start() async {
|
||||
|
@ -132,7 +142,8 @@ class KeyVerification {
|
|||
setState(KeyVerificationState.waitingAccept);
|
||||
}
|
||||
|
||||
Future<void> handlePayload(String type, Map<String, dynamic> payload, [String eventId]) async {
|
||||
Future<void> handlePayload(String type, Map<String, dynamic> payload,
|
||||
[String eventId]) async {
|
||||
print('[Key Verification] Received type ${type}: ' + payload.toString());
|
||||
try {
|
||||
switch (type) {
|
||||
|
@ -141,13 +152,16 @@ class KeyVerification {
|
|||
transactionId ??= eventId ?? payload['transaction_id'];
|
||||
// verify the timestamp
|
||||
final now = DateTime.now();
|
||||
final verifyTime = DateTime.fromMillisecondsSinceEpoch(payload['timestamp']);
|
||||
if (now.subtract(Duration(minutes: 10)).isAfter(verifyTime) || now.add(Duration(minutes: 5)).isBefore(verifyTime)) {
|
||||
final verifyTime =
|
||||
DateTime.fromMillisecondsSinceEpoch(payload['timestamp']);
|
||||
if (now.subtract(Duration(minutes: 10)).isAfter(verifyTime) ||
|
||||
now.add(Duration(minutes: 5)).isBefore(verifyTime)) {
|
||||
await cancel('m.timeout');
|
||||
return;
|
||||
}
|
||||
// verify it has a method we can use
|
||||
possibleMethods = _intersect(VERIFICATION_METHODS, payload['methods']);
|
||||
possibleMethods =
|
||||
_intersect(VERIFICATION_METHODS, payload['methods']);
|
||||
if (possibleMethods.isEmpty) {
|
||||
// reject it outright
|
||||
await cancel('m.unknown_method');
|
||||
|
@ -156,7 +170,8 @@ class KeyVerification {
|
|||
setState(KeyVerificationState.askAccept);
|
||||
break;
|
||||
case 'm.key.verification.ready':
|
||||
possibleMethods = _intersect(VERIFICATION_METHODS, payload['methods']);
|
||||
possibleMethods =
|
||||
_intersect(VERIFICATION_METHODS, payload['methods']);
|
||||
if (possibleMethods.isEmpty) {
|
||||
// reject it outright
|
||||
await cancel('m.unknown_method');
|
||||
|
@ -214,7 +229,8 @@ class KeyVerification {
|
|||
|
||||
/// called when the user accepts an incoming verification
|
||||
Future<void> acceptVerification() async {
|
||||
if (!(await verifyLastStep(['m.key.verification.request', 'm.key.verification.start']))) {
|
||||
if (!(await verifyLastStep(
|
||||
['m.key.verification.request', 'm.key.verification.start']))) {
|
||||
return;
|
||||
}
|
||||
setState(KeyVerificationState.waitingAccept);
|
||||
|
@ -231,7 +247,8 @@ class KeyVerification {
|
|||
|
||||
/// called when the user rejects an incoming verification
|
||||
Future<void> rejectVerification() async {
|
||||
if (!(await verifyLastStep(['m.key.verification.request', 'm.key.verification.start']))) {
|
||||
if (!(await verifyLastStep(
|
||||
['m.key.verification.request', 'm.key.verification.start']))) {
|
||||
return;
|
||||
}
|
||||
await cancel('m.user');
|
||||
|
@ -251,7 +268,9 @@ class KeyVerification {
|
|||
|
||||
List<int> get sasNumbers {
|
||||
if (method is _KeyVerificationMethodSas) {
|
||||
return _bytesToInt((method as _KeyVerificationMethodSas).makeSas(5), 13).map((n) => n + 1000).toList();
|
||||
return _bytesToInt((method as _KeyVerificationMethodSas).makeSas(5), 13)
|
||||
.map((n) => n + 1000)
|
||||
.toList();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
@ -265,7 +284,8 @@ class KeyVerification {
|
|||
|
||||
List<KeyVerificationEmoji> get sasEmojis {
|
||||
if (method is _KeyVerificationMethodSas) {
|
||||
final numbers = _bytesToInt((method as _KeyVerificationMethodSas).makeSas(6), 6);
|
||||
final numbers =
|
||||
_bytesToInt((method as _KeyVerificationMethodSas).makeSas(6), 6);
|
||||
return numbers.map((n) => KeyVerificationEmoji(n)).toList().sublist(0, 7);
|
||||
}
|
||||
return [];
|
||||
|
@ -282,8 +302,10 @@ class KeyVerification {
|
|||
final keyId = entry.key;
|
||||
final verifyDeviceId = keyId.substring('ed25519:'.length);
|
||||
final keyInfo = entry.value;
|
||||
if (client.userDeviceKeys[userId].deviceKeys.containsKey(verifyDeviceId)) {
|
||||
if (!(await verifier(keyInfo, client.userDeviceKeys[userId].deviceKeys[verifyDeviceId]))) {
|
||||
if (client.userDeviceKeys[userId].deviceKeys
|
||||
.containsKey(verifyDeviceId)) {
|
||||
if (!(await verifier(keyInfo,
|
||||
client.userDeviceKeys[userId].deviceKeys[verifyDeviceId]))) {
|
||||
await cancel('m.key_mismatch');
|
||||
return;
|
||||
}
|
||||
|
@ -309,7 +331,8 @@ class KeyVerification {
|
|||
}
|
||||
|
||||
Future<bool> verifyActivity() async {
|
||||
if (lastActivity != null && lastActivity.add(Duration(minutes: 10)).isAfter(DateTime.now())) {
|
||||
if (lastActivity != null &&
|
||||
lastActivity.add(Duration(minutes: 10)).isAfter(DateTime.now())) {
|
||||
lastActivity = DateTime.now();
|
||||
return true;
|
||||
}
|
||||
|
@ -360,7 +383,8 @@ class KeyVerification {
|
|||
if (['m.key.verification.request'].contains(type)) {
|
||||
payload['msgtype'] = type;
|
||||
payload['to'] = userId;
|
||||
payload['body'] = 'Attempting verification request. (${type}) Apparently your client doesn\'t support this';
|
||||
payload['body'] =
|
||||
'Attempting verification request. (${type}) Apparently your client doesn\'t support this';
|
||||
type = 'm.room.message';
|
||||
}
|
||||
final newTransactionId = await room.sendEvent(payload, type: type);
|
||||
|
@ -369,7 +393,8 @@ class KeyVerification {
|
|||
client.addKeyVerificationRequest(this);
|
||||
}
|
||||
} else {
|
||||
await client.sendToDevice([client.userDeviceKeys[userId].deviceKeys[deviceId]], type, payload);
|
||||
await client.sendToDevice(
|
||||
[client.userDeviceKeys[userId].deviceKeys[deviceId]], type, payload);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -394,6 +419,7 @@ abstract class _KeyVerificationMethod {
|
|||
bool validateStart(Map<String, dynamic> payload) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<void> sendStart();
|
||||
void dispose() {}
|
||||
}
|
||||
|
@ -404,7 +430,8 @@ const KNOWN_MESSAGE_AUTHENTIFICATION_CODES = ['hkdf-hmac-sha256'];
|
|||
const KNOWN_AUTHENTICATION_TYPES = ['emoji', 'decimal'];
|
||||
|
||||
class _KeyVerificationMethodSas extends _KeyVerificationMethod {
|
||||
_KeyVerificationMethodSas({KeyVerification request}) : super(request: request);
|
||||
_KeyVerificationMethodSas({KeyVerification request})
|
||||
: super(request: request);
|
||||
|
||||
static String type = 'm.sas.v1';
|
||||
|
||||
|
@ -428,7 +455,8 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
|
|||
try {
|
||||
switch (type) {
|
||||
case 'm.key.verification.start':
|
||||
if (!(await request.verifyLastStep(['m.key.verification.request', 'm.key.verification.start']))) {
|
||||
if (!(await request.verifyLastStep(
|
||||
['m.key.verification.request', 'm.key.verification.start']))) {
|
||||
return; // abort
|
||||
}
|
||||
if (!validateStart(payload)) {
|
||||
|
@ -448,7 +476,8 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
|
|||
await _sendKey();
|
||||
break;
|
||||
case 'm.key.verification.key':
|
||||
if (!(await request.verifyLastStep(['m.key.verification.accept', 'm.key.verification.start']))) {
|
||||
if (!(await request.verifyLastStep(
|
||||
['m.key.verification.accept', 'm.key.verification.start']))) {
|
||||
return;
|
||||
}
|
||||
_handleKey(payload);
|
||||
|
@ -515,7 +544,8 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
|
|||
if (payload['method'] != type) {
|
||||
return false;
|
||||
}
|
||||
final possibleKeyAgreementProtocols = _intersect(KNOWN_KEY_AGREEMENT_PROTOCOLS, payload['key_agreement_protocols']);
|
||||
final possibleKeyAgreementProtocols = _intersect(
|
||||
KNOWN_KEY_AGREEMENT_PROTOCOLS, payload['key_agreement_protocols']);
|
||||
if (possibleKeyAgreementProtocols.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
|
@ -525,12 +555,15 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
|
|||
return false;
|
||||
}
|
||||
hash = possibleHashes.first;
|
||||
final possibleMessageAuthenticationCodes = _intersect(KNOWN_MESSAGE_AUTHENTIFICATION_CODES, payload['message_authentication_codes']);
|
||||
final possibleMessageAuthenticationCodes = _intersect(
|
||||
KNOWN_MESSAGE_AUTHENTIFICATION_CODES,
|
||||
payload['message_authentication_codes']);
|
||||
if (possibleMessageAuthenticationCodes.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
messageAuthenticationCode = possibleMessageAuthenticationCodes.first;
|
||||
final possibleAuthenticationTypes = _intersect(KNOWN_AUTHENTICATION_TYPES, payload['short_authentication_string']);
|
||||
final possibleAuthenticationTypes = _intersect(
|
||||
KNOWN_AUTHENTICATION_TYPES, payload['short_authentication_string']);
|
||||
if (possibleAuthenticationTypes.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
|
@ -553,7 +586,8 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
|
|||
}
|
||||
|
||||
bool _handleAccept(Map<String, dynamic> payload) {
|
||||
if (!KNOWN_KEY_AGREEMENT_PROTOCOLS.contains(payload['key_agreement_protocol'])) {
|
||||
if (!KNOWN_KEY_AGREEMENT_PROTOCOLS
|
||||
.contains(payload['key_agreement_protocol'])) {
|
||||
return false;
|
||||
}
|
||||
keyAgreementProtocol = payload['key_agreement_protocol'];
|
||||
|
@ -561,11 +595,13 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
|
|||
return false;
|
||||
}
|
||||
hash = payload['hash'];
|
||||
if (!KNOWN_MESSAGE_AUTHENTIFICATION_CODES.contains(payload['message_authentication_code'])) {
|
||||
if (!KNOWN_MESSAGE_AUTHENTIFICATION_CODES
|
||||
.contains(payload['message_authentication_code'])) {
|
||||
return false;
|
||||
}
|
||||
messageAuthenticationCode = payload['message_authentication_code'];
|
||||
final possibleAuthenticationTypes = _intersect(KNOWN_AUTHENTICATION_TYPES, payload['short_authentication_string']);
|
||||
final possibleAuthenticationTypes = _intersect(
|
||||
KNOWN_AUTHENTICATION_TYPES, payload['short_authentication_string']);
|
||||
if (possibleAuthenticationTypes.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
|
@ -594,13 +630,23 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
|
|||
Uint8List makeSas(int bytes) {
|
||||
var sasInfo = '';
|
||||
if (keyAgreementProtocol == 'curve25519-hkdf-sha256') {
|
||||
final ourInfo = '${client.userID}|${client.deviceID}|${sas.get_pubkey()}|';
|
||||
final theirInfo = '${request.userId}|${request.deviceId}|${theirPublicKey}|';
|
||||
sasInfo = 'MATRIX_KEY_VERIFICATION_SAS|' + (request.startedVerification ? ourInfo + theirInfo : theirInfo + ourInfo) + request.transactionId;
|
||||
final ourInfo =
|
||||
'${client.userID}|${client.deviceID}|${sas.get_pubkey()}|';
|
||||
final theirInfo =
|
||||
'${request.userId}|${request.deviceId}|${theirPublicKey}|';
|
||||
sasInfo = 'MATRIX_KEY_VERIFICATION_SAS|' +
|
||||
(request.startedVerification
|
||||
? ourInfo + theirInfo
|
||||
: theirInfo + ourInfo) +
|
||||
request.transactionId;
|
||||
} else if (keyAgreementProtocol == 'curve25519') {
|
||||
final ourInfo = client.userID + client.deviceID;
|
||||
final theirInfo = request.userId + request.deviceId;
|
||||
sasInfo = 'MATRIX_KEY_VERIFICATION_SAS' + (request.startedVerification ? ourInfo + theirInfo : theirInfo + ourInfo) + request.transactionId;
|
||||
sasInfo = 'MATRIX_KEY_VERIFICATION_SAS' +
|
||||
(request.startedVerification
|
||||
? ourInfo + theirInfo
|
||||
: theirInfo + ourInfo) +
|
||||
request.transactionId;
|
||||
} else {
|
||||
throw 'Unknown key agreement protocol';
|
||||
}
|
||||
|
@ -609,8 +655,10 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
|
|||
|
||||
Future<void> _sendMac() async {
|
||||
final baseInfo = 'MATRIX_KEY_VERIFICATION_MAC' +
|
||||
client.userID + client.deviceID +
|
||||
request.userId + request.deviceId +
|
||||
client.userID +
|
||||
client.deviceID +
|
||||
request.userId +
|
||||
request.deviceId +
|
||||
request.transactionId;
|
||||
final mac = <String, String>{};
|
||||
final keyList = <String>[];
|
||||
|
@ -619,7 +667,8 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
|
|||
// for now it is just our device key, once we have cross-signing
|
||||
// we would also add the cross signing key here
|
||||
final deviceKeyId = 'ed25519:${client.deviceID}';
|
||||
mac[deviceKeyId] = _calculateMac(client.fingerprintKey, baseInfo + deviceKeyId);
|
||||
mac[deviceKeyId] =
|
||||
_calculateMac(client.fingerprintKey, baseInfo + deviceKeyId);
|
||||
keyList.add(deviceKeyId);
|
||||
|
||||
keyList.sort();
|
||||
|
@ -633,13 +682,16 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
|
|||
Future<void> _processMac() async {
|
||||
final payload = macPayload;
|
||||
final baseInfo = 'MATRIX_KEY_VERIFICATION_MAC' +
|
||||
request.userId + request.deviceId +
|
||||
client.userID + client.deviceID +
|
||||
request.userId +
|
||||
request.deviceId +
|
||||
client.userID +
|
||||
client.deviceID +
|
||||
request.transactionId;
|
||||
|
||||
final keyList = payload['mac'].keys.toList();
|
||||
keyList.sort();
|
||||
if (payload['keys'] != _calculateMac(keyList.join(','), baseInfo + 'KEY_IDS')) {
|
||||
if (payload['keys'] !=
|
||||
_calculateMac(keyList.join(','), baseInfo + 'KEY_IDS')) {
|
||||
await request.cancel('m.key_mismatch');
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -13,7 +13,8 @@ class LinebreakSyntax extends InlineSyntax {
|
|||
|
||||
class SpoilerSyntax extends TagSyntax {
|
||||
Map<String, String> reasonMap = <String, String>{};
|
||||
SpoilerSyntax() : super(
|
||||
SpoilerSyntax()
|
||||
: super(
|
||||
r'\|\|(?:([^\|]+)\|(?!\|))?',
|
||||
requiresDelimiterRun: true,
|
||||
end: r'\|\|',
|
||||
|
@ -31,7 +32,8 @@ class SpoilerSyntax extends TagSyntax {
|
|||
@override
|
||||
bool onMatchEnd(InlineParser parser, Match match, TagState state) {
|
||||
final element = Element('span', state.children);
|
||||
element.attributes['data-mx-spoiler'] = htmlEscape.convert(reasonMap[match.input] ?? '');
|
||||
element.attributes['data-mx-spoiler'] =
|
||||
htmlEscape.convert(reasonMap[match.input] ?? '');
|
||||
parser.addNode(element);
|
||||
return true;
|
||||
}
|
||||
|
@ -88,14 +90,33 @@ class PillSyntax extends InlineSyntax {
|
|||
|
||||
String markdown(String text, [Map<String, Map<String, String>> emotePacks]) {
|
||||
emotePacks ??= <String, Map<String, String>>{};
|
||||
var ret = markdownToHtml(text,
|
||||
var ret = markdownToHtml(
|
||||
text,
|
||||
extensionSet: ExtensionSet.commonMark,
|
||||
inlineSyntaxes: [StrikethroughSyntax(), LinebreakSyntax(), SpoilerSyntax(), EmoteSyntax(emotePacks), PillSyntax()],
|
||||
inlineSyntaxes: [
|
||||
StrikethroughSyntax(),
|
||||
LinebreakSyntax(),
|
||||
SpoilerSyntax(),
|
||||
EmoteSyntax(emotePacks),
|
||||
PillSyntax()
|
||||
],
|
||||
);
|
||||
|
||||
var stripPTags = '<p>'.allMatches(ret).length <= 1;
|
||||
if (stripPTags) {
|
||||
final otherBlockTags = ['table', 'pre', 'ol', 'ul', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote'];
|
||||
final otherBlockTags = [
|
||||
'table',
|
||||
'pre',
|
||||
'ol',
|
||||
'ul',
|
||||
'h1',
|
||||
'h2',
|
||||
'h3',
|
||||
'h4',
|
||||
'h5',
|
||||
'h6',
|
||||
'blockquote'
|
||||
];
|
||||
for (final tag in otherBlockTags) {
|
||||
// we check for the close tag as the opening one might have attributes
|
||||
if (ret.contains('</${tag}>')) {
|
||||
|
|
|
@ -26,7 +26,8 @@ class RoomKeyRequest extends ToDeviceEvent {
|
|||
var message = session.content;
|
||||
message['forwarding_curve25519_key_chain'] = forwardedKeys;
|
||||
|
||||
message['session_key'] = session.inboundGroupSession.export_session(session.inboundGroupSession.first_known_index());
|
||||
message['session_key'] = session.inboundGroupSession
|
||||
.export_session(session.inboundGroupSession.first_known_index());
|
||||
await client.sendToDevice(
|
||||
[requestingDevice],
|
||||
'm.forwarded_room_key',
|
||||
|
|
|
@ -20,9 +20,8 @@ class SessionKey {
|
|||
SessionKey.fromDb(DbInboundGroupSession dbEntry, String key) : key = key {
|
||||
final parsedContent = Event.getMapFromPayload(dbEntry.content);
|
||||
final parsedIndexes = Event.getMapFromPayload(dbEntry.indexes);
|
||||
content = parsedContent != null
|
||||
? Map<String, dynamic>.from(parsedContent)
|
||||
: null;
|
||||
content =
|
||||
parsedContent != null ? Map<String, dynamic>.from(parsedContent) : null;
|
||||
indexes = parsedIndexes != null
|
||||
? Map<String, int>.from(parsedIndexes)
|
||||
: <String, int>{};
|
||||
|
|
|
@ -15,12 +15,15 @@ void main() {
|
|||
},
|
||||
};
|
||||
test('simple markdown', () {
|
||||
expect(markdown('hey *there* how are **you** doing?'), 'hey <em>there</em> how are <strong>you</strong> doing?');
|
||||
expect(markdown('hey *there* how are **you** doing?'),
|
||||
'hey <em>there</em> how are <strong>you</strong> doing?');
|
||||
expect(markdown('wha ~~strike~~ works!'), 'wha <del>strike</del> works!');
|
||||
});
|
||||
test('spoilers', () {
|
||||
expect(markdown('Snape killed ||Dumbledoor||'), 'Snape killed <span data-mx-spoiler="">Dumbledoor</span>');
|
||||
expect(markdown('Snape killed ||Story|Dumbledoor||'), 'Snape killed <span data-mx-spoiler="Story">Dumbledoor</span>');
|
||||
expect(markdown('Snape killed ||Dumbledoor||'),
|
||||
'Snape killed <span data-mx-spoiler="">Dumbledoor</span>');
|
||||
expect(markdown('Snape killed ||Story|Dumbledoor||'),
|
||||
'Snape killed <span data-mx-spoiler="Story">Dumbledoor</span>');
|
||||
});
|
||||
test('multiple paragraphs', () {
|
||||
expect(markdown('Heya!\n\nBeep'), '<p>Heya!</p>\n<p>Beep</p>');
|
||||
|
@ -32,16 +35,22 @@ void main() {
|
|||
expect(markdown('foxies\ncute'), 'foxies<br />\ncute');
|
||||
});
|
||||
test('emotes', () {
|
||||
expect(markdown(':fox:', emotePacks), '<img src="mxc://roomfox" alt=":fox:" title=":fox:" height="32" vertical-align="middle" />');
|
||||
expect(markdown(':user~fox:', emotePacks), '<img src="mxc://userfox" alt=":fox:" title=":fox:" height="32" vertical-align="middle" />');
|
||||
expect(markdown(':raccoon:', emotePacks), '<img src="mxc://raccoon" alt=":raccoon:" title=":raccoon:" height="32" vertical-align="middle" />');
|
||||
expect(markdown(':fox:', emotePacks),
|
||||
'<img src="mxc://roomfox" alt=":fox:" title=":fox:" height="32" vertical-align="middle" />');
|
||||
expect(markdown(':user~fox:', emotePacks),
|
||||
'<img src="mxc://userfox" alt=":fox:" title=":fox:" height="32" vertical-align="middle" />');
|
||||
expect(markdown(':raccoon:', emotePacks),
|
||||
'<img src="mxc://raccoon" alt=":raccoon:" title=":raccoon:" height="32" vertical-align="middle" />');
|
||||
expect(markdown(':invalid:', emotePacks), ':invalid:');
|
||||
expect(markdown(':room~invalid:', emotePacks), ':room~invalid:');
|
||||
});
|
||||
test('pills', () {
|
||||
expect(markdown('Hey @sorunome:sorunome.de!'), 'Hey <a href="https://matrix.to/#/@sorunome:sorunome.de">@sorunome:sorunome.de</a>!');
|
||||
expect(markdown('#fox:sorunome.de: you all are awesome'), '<a href="https://matrix.to/#/#fox:sorunome.de">#fox:sorunome.de</a>: you all are awesome');
|
||||
expect(markdown('!blah:example.org'), '<a href="https://matrix.to/#/!blah:example.org">!blah:example.org</a>');
|
||||
expect(markdown('Hey @sorunome:sorunome.de!'),
|
||||
'Hey <a href="https://matrix.to/#/@sorunome:sorunome.de">@sorunome:sorunome.de</a>!');
|
||||
expect(markdown('#fox:sorunome.de: you all are awesome'),
|
||||
'<a href="https://matrix.to/#/#fox:sorunome.de">#fox:sorunome.de</a>: you all are awesome');
|
||||
expect(markdown('!blah:example.org'),
|
||||
'<a href="https://matrix.to/#/!blah:example.org">!blah:example.org</a>');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -26,7 +26,9 @@ import 'package:test/test.dart';
|
|||
import 'package:famedlysdk/src/client.dart';
|
||||
import 'package:famedlysdk/src/room.dart';
|
||||
import 'package:famedlysdk/src/timeline.dart';
|
||||
import 'package:famedlysdk/src/user.dart';
|
||||
import 'package:famedlysdk/src/sync/event_update.dart';
|
||||
import 'package:famedlysdk/src/sync/room_update.dart';
|
||||
import 'fake_matrix_api.dart';
|
||||
|
||||
void main() {
|
||||
|
@ -240,5 +242,18 @@ void main() {
|
|||
expect(timeline.events[8].eventId, '1143273582443PhrSn:example.org');
|
||||
expect(room.prev_batch, 't47409-4357353_219380_26003_2265');
|
||||
});
|
||||
|
||||
test('Clear cache on limited timeline', () async {
|
||||
client.onRoomUpdate.add(RoomUpdate(
|
||||
id: roomID,
|
||||
membership: Membership.join,
|
||||
notification_count: 0,
|
||||
highlight_count: 0,
|
||||
limitedTimeline: true,
|
||||
prev_batch: 'blah',
|
||||
));
|
||||
await Future.delayed(Duration(milliseconds: 50));
|
||||
expect(timeline.events.isEmpty, true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -128,7 +128,8 @@ void test() async {
|
|||
await Future.delayed(Duration(seconds: 5));
|
||||
assert(room.outboundGroupSession != null);
|
||||
var currentSessionIdA = room.outboundGroupSession.session_id();
|
||||
assert(room.inboundGroupSessions.containsKey(room.outboundGroupSession.session_id()));
|
||||
assert(room.inboundGroupSessions
|
||||
.containsKey(room.outboundGroupSession.session_id()));
|
||||
assert(testClientA.olmSessions[testClientB.identityKey].length == 1);
|
||||
assert(testClientB.olmSessions[testClientA.identityKey].length == 1);
|
||||
assert(testClientA.olmSessions[testClientB.identityKey].first.session_id() ==
|
||||
|
@ -236,7 +237,8 @@ void test() async {
|
|||
assert(restoredRoom.outboundGroupSession.session_id() ==
|
||||
room.outboundGroupSession.session_id());
|
||||
assert(restoredRoom.inboundGroupSessions.length == 4);
|
||||
assert(restoredRoom.inboundGroupSessions.length == room.inboundGroupSessions.length);
|
||||
assert(restoredRoom.inboundGroupSessions.length ==
|
||||
room.inboundGroupSessions.length);
|
||||
for (var i = 0; i < restoredRoom.inboundGroupSessions.length; i++) {
|
||||
assert(restoredRoom.inboundGroupSessions.keys.toList()[i] ==
|
||||
room.inboundGroupSessions.keys.toList()[i]);
|
||||
|
|
Loading…
Reference in a new issue