Add code formatting CI job

This commit is contained in:
Christian Pauly 2020-05-22 10:12:18 +00:00
parent f4a5ccdaa9
commit 27b4a620e5
16 changed files with 486 additions and 253 deletions

View File

@ -54,6 +54,7 @@ code_analyze:
image: cirrusci/flutter image: cirrusci/flutter
dependencies: [] dependencies: []
script: script:
- flutter format lib/ test/ test_driver/ --set-exit-if-changed
- flutter analyze - flutter analyze
build-api-doc: build-api-doc:

View File

@ -41,7 +41,10 @@ That means for example: "[users] add fetch users endpoint".
- Directories need to be lowercase. - Directories need to be lowercase.
## Code style: ## 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: ## Code quality:
- Don't repeat yourself! Use local variables, functions, classes. - Don't repeat yourself! Use local variables, functions, classes.

View File

@ -84,7 +84,8 @@ class Client {
/// debug: Print debug output? /// debug: Print debug output?
/// database: The database instance to use /// database: The database instance to use
/// enableE2eeRecovery: Enable additional logic to try to recover from bad e2ee sessions /// 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) { onLoginStateChanged.stream.listen((loginState) {
print('LoginState: ${loginState.toString()}'); print('LoginState: ${loginState.toString()}');
}); });
@ -1045,8 +1046,7 @@ class Client {
} }
if (sync['account_data'] is Map<String, dynamic> && if (sync['account_data'] is Map<String, dynamic> &&
sync['account_data']['events'] is List<dynamic>) { sync['account_data']['events'] is List<dynamic>) {
await _handleGlobalEvents( await _handleGlobalEvents(sync['account_data']['events'], 'account_data');
sync['account_data']['events'], 'account_data');
} }
if (sync['device_lists'] is Map<String, dynamic>) { if (sync['device_lists'] is Map<String, dynamic>) {
await _handleDeviceListsEvents(sync['device_lists']); 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) { if (deviceLists['changed'] is List) {
for (final userId in deviceLists['changed']) { for (final userId in deviceLists['changed']) {
if (_userDeviceKeys.containsKey(userId)) { 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) { for (final entry in rooms.entries) {
final id = entry.key; final id = entry.key;
final room = entry.value; final room = entry.value;
@ -1252,8 +1254,7 @@ class Client {
if (room['timeline'] is Map<String, dynamic> && if (room['timeline'] is Map<String, dynamic> &&
room['timeline']['events'] is List<dynamic> && room['timeline']['events'] is List<dynamic> &&
room['timeline']['events'].isNotEmpty) { room['timeline']['events'].isNotEmpty) {
await _handleRoomEvents( await _handleRoomEvents(id, room['timeline']['events'], 'timeline');
id, room['timeline']['events'], 'timeline');
handledEvents = true; 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++) { for (num i = 0; i < events.length; i++) {
await _handleEvent(events[i], chat_id, type); 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>) { if (event['type'] is String && event['content'] is Map<String, dynamic>) {
// The client must ignore any new m.room.encryption event to prevent // The client must ignore any new m.room.encryption event to prevent
// man-in-the-middle attacks! // man-in-the-middle attacks!
final room = getRoomById(roomID); final room = getRoomById(roomID);
if (room == null || if (room == null ||
(event['type'] == 'm.room.encryption' && room.encrypted && (event['type'] == 'm.room.encryption' &&
event['content']['algorithm'] != room.getState('m.room.encryption')?.content['algorithm'])) { room.encrypted &&
event['content']['algorithm'] !=
room.getState('m.room.encryption')?.content['algorithm'])) {
return; return;
} }
@ -1367,7 +1372,8 @@ class Client {
} }
if (update.eventType == 'm.room.encrypted' && database != null) { if (update.eventType == 'm.room.encrypted' && database != null) {
// the event is still encrytped....let's try fetching the keys from the database! // 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); update = update.decrypt(room);
} }
if (type != 'ephemeral' && database != null) { if (type != 'ephemeral' && database != null) {
@ -1659,10 +1665,9 @@ class Client {
if (entry.isValid) { if (entry.isValid) {
_userDeviceKeys[userId].deviceKeys[deviceId] = entry; _userDeviceKeys[userId].deviceKeys[deviceId] = entry;
if (deviceId == deviceID && if (deviceId == deviceID &&
entry.ed25519Key == entry.ed25519Key == fingerprintKey) {
fingerprintKey) { // Always trust the own device
// Always trust the own device entry.verified = true;
entry.verified = true;
} }
} }
if (database != null) { if (database != null) {
@ -1670,8 +1675,9 @@ class Client {
id, id,
userId, userId,
deviceId, deviceId,
json.encode( json.encode(_userDeviceKeys[userId]
_userDeviceKeys[userId].deviceKeys[deviceId].toJson()), .deviceKeys[deviceId]
.toJson()),
_userDeviceKeys[userId].deviceKeys[deviceId].verified, _userDeviceKeys[userId].deviceKeys[deviceId].verified,
_userDeviceKeys[userId].deviceKeys[deviceId].blocked, _userDeviceKeys[userId].deviceKeys[deviceId].blocked,
)); ));

View File

@ -19,31 +19,31 @@ class Database extends _$Database {
@override @override
MigrationStrategy get migration => MigrationStrategy( MigrationStrategy get migration => MigrationStrategy(
onCreate: (Migrator m) { onCreate: (Migrator m) {
return m.createAll(); return m.createAll();
}, },
onUpgrade: (Migrator m, int from, int to) async { onUpgrade: (Migrator m, int from, int to) async {
// this appears to be only called once, so multiple consecutive upgrades have to be handled appropriately in here // this appears to be only called once, so multiple consecutive upgrades have to be handled appropriately in here
if (from == 1) { if (from == 1) {
await m.createIndex(userDeviceKeysIndex); await m.createIndex(userDeviceKeysIndex);
await m.createIndex(userDeviceKeysKeyIndex); await m.createIndex(userDeviceKeysKeyIndex);
await m.createIndex(olmSessionsIndex); await m.createIndex(olmSessionsIndex);
await m.createIndex(outboundGroupSessionsIndex); await m.createIndex(outboundGroupSessionsIndex);
await m.createIndex(inboundGroupSessionsIndex); await m.createIndex(inboundGroupSessionsIndex);
await m.createIndex(roomsIndex); await m.createIndex(roomsIndex);
await m.createIndex(eventsIndex); await m.createIndex(eventsIndex);
await m.createIndex(roomStatesIndex); await m.createIndex(roomStatesIndex);
await m.createIndex(accountDataIndex); await m.createIndex(accountDataIndex);
await m.createIndex(roomAccountDataIndex); await m.createIndex(roomAccountDataIndex);
await m.createIndex(presencesIndex); await m.createIndex(presencesIndex);
from++; from++;
} }
if (from == 2) { if (from == 2) {
await m.deleteTable('outbound_group_sessions'); await m.deleteTable('outbound_group_sessions');
await m.createTable(outboundGroupSessions); await m.createTable(outboundGroupSessions);
} }
}, },
); );
Future<DbClient> getClient(String name) async { Future<DbClient> getClient(String name) async {
final res = await dbGetClient(name).get(); final res = await dbGetClient(name).get();
@ -51,7 +51,8 @@ class Database extends _$Database {
return res.first; return res.first;
} }
Future<Map<String, sdk.DeviceKeysList>> getUserDeviceKeys(int clientId) async { Future<Map<String, sdk.DeviceKeysList>> getUserDeviceKeys(
int clientId) async {
final deviceKeys = await getAllUserDeviceKeys(clientId).get(); final deviceKeys = await getAllUserDeviceKeys(clientId).get();
if (deviceKeys.isEmpty) { if (deviceKeys.isEmpty) {
return {}; return {};
@ -59,12 +60,14 @@ class Database extends _$Database {
final deviceKeysKeys = await getAllUserDeviceKeysKeys(clientId).get(); final deviceKeysKeys = await getAllUserDeviceKeysKeys(clientId).get();
final res = <String, sdk.DeviceKeysList>{}; final res = <String, sdk.DeviceKeysList>{};
for (final entry in deviceKeys) { for (final entry in deviceKeys) {
res[entry.userId] = sdk.DeviceKeysList.fromDb(entry, deviceKeysKeys.where((k) => k.userId == entry.userId).toList()); res[entry.userId] = sdk.DeviceKeysList.fromDb(entry,
deviceKeysKeys.where((k) => k.userId == entry.userId).toList());
} }
return res; 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(); final raw = await getAllOlmSessions(clientId).get();
if (raw.isEmpty) { if (raw.isEmpty) {
return {}; return {};
@ -85,7 +88,8 @@ class Database extends _$Database {
return res; 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(); final res = await dbGetOutboundGroupSession(clientId, roomId).get();
if (res.isEmpty) { if (res.isEmpty) {
return null; return null;
@ -93,22 +97,28 @@ class Database extends _$Database {
return res.first; 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(); return await dbGetInboundGroupSessionKeys(clientId, roomId).get();
} }
Future<DbInboundGroupSession> getDbInboundGroupSession(int clientId, String roomId, String sessionId) async { Future<DbInboundGroupSession> getDbInboundGroupSession(
final res = await dbGetInboundGroupSessionKey(clientId, roomId, sessionId).get(); int clientId, String roomId, String sessionId) async {
final res =
await dbGetInboundGroupSessionKey(clientId, roomId, sessionId).get();
if (res.isEmpty) { if (res.isEmpty) {
return null; return null;
} }
return res.first; return res.first;
} }
Future<List<sdk.Room>> getRoomList(sdk.Client client, {bool onlyLeft = false}) async { Future<List<sdk.Room>> getRoomList(sdk.Client client,
final res = await (select(rooms)..where((t) => onlyLeft {bool onlyLeft = false}) async {
? t.membership.equals('leave') final res = await (select(rooms)
: t.membership.equals('leave').not())).get(); ..where((t) => onlyLeft
? t.membership.equals('leave')
: t.membership.equals('leave').not()))
.get();
final resStates = await getAllRoomStates(client.id).get(); final resStates = await getAllRoomStates(client.id).get();
final resAccountData = await getAllRoomAccountData(client.id).get(); final resAccountData = await getAllRoomAccountData(client.id).get();
final roomList = <sdk.Room>[]; final roomList = <sdk.Room>[];
@ -145,11 +155,13 @@ class Database extends _$Database {
/// Stores a RoomUpdate object in the database. Must be called inside of /// Stores a RoomUpdate object in the database. Must be called inside of
/// [transaction]. /// [transaction].
final Set<String> _ensuredRooms = {}; 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}'; final setKey = '${clientId};${roomUpdate.id}';
if (roomUpdate.membership != sdk.Membership.leave) { if (roomUpdate.membership != sdk.Membership.leave) {
if (!_ensuredRooms.contains(setKey)) { 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); _ensuredRooms.add(setKey);
} }
} else { } else {
@ -161,24 +173,37 @@ class Database extends _$Database {
var doUpdate = oldRoom == null; var doUpdate = oldRoom == null;
if (!doUpdate) { if (!doUpdate) {
doUpdate = roomUpdate.highlight_count != oldRoom.highlightCount || doUpdate = roomUpdate.highlight_count != oldRoom.highlightCount ||
roomUpdate.notification_count != oldRoom.notificationCount || roomUpdate.notification_count != oldRoom.notificationCount ||
roomUpdate.membership.toString().split('.').last != oldRoom.membership.toString().split('.').last || roomUpdate.membership.toString().split('.').last !=
(roomUpdate.summary?.mJoinedMemberCount != null && oldRoom.membership.toString().split('.').last ||
roomUpdate.summary.mJoinedMemberCount != oldRoom.mInvitedMemberCount) || (roomUpdate.summary?.mJoinedMemberCount != null &&
(roomUpdate.summary?.mInvitedMemberCount != null && roomUpdate.summary.mJoinedMemberCount !=
roomUpdate.summary.mJoinedMemberCount != oldRoom.mJoinedMemberCount) || oldRoom.mInvitedMemberCount) ||
(roomUpdate.summary?.mHeroes != null && (roomUpdate.summary?.mInvitedMemberCount != null &&
roomUpdate.summary.mHeroes.join(',') != oldRoom.mHeroes.join(',')); roomUpdate.summary.mJoinedMemberCount !=
oldRoom.mJoinedMemberCount) ||
(roomUpdate.summary?.mHeroes != null &&
roomUpdate.summary.mHeroes.join(',') !=
oldRoom.mHeroes.join(','));
} }
if (doUpdate) { 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), highlightCount: Value(roomUpdate.highlight_count),
notificationCount: Value(roomUpdate.notification_count), notificationCount: Value(roomUpdate.notification_count),
membership: Value(roomUpdate.membership.toString().split('.').last), membership: Value(roomUpdate.membership.toString().split('.').last),
joinedMemberCount: roomUpdate.summary?.mJoinedMemberCount != null ? Value(roomUpdate.summary.mJoinedMemberCount) : Value.absent(), joinedMemberCount: roomUpdate.summary?.mJoinedMemberCount != null
invitedMemberCount: roomUpdate.summary?.mInvitedMemberCount != null ? Value(roomUpdate.summary.mInvitedMemberCount) : Value.absent(), ? Value(roomUpdate.summary.mJoinedMemberCount)
heroes: roomUpdate.summary?.mHeroes != null ? Value(roomUpdate.summary.mHeroes.join(',')) : Value.absent(), : 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(),
)); ));
} }
@ -193,17 +218,24 @@ class Database extends _$Database {
/// Stores an UserUpdate object in the database. Must be called inside of /// Stores an UserUpdate object in the database. Must be called inside of
/// [transaction]. /// [transaction].
Future<void> storeUserEventUpdate(int clientId, sdk.UserUpdate userUpdate) async { Future<void> storeUserEventUpdate(
int clientId, sdk.UserUpdate userUpdate) async {
if (userUpdate.type == 'account_data') { 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') { } 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 /// Stores an EventUpdate object in the database. Must be called inside of
/// [transaction]. /// [transaction].
Future<void> storeEventUpdate(int clientId, sdk.EventUpdate eventUpdate) async { Future<void> storeEventUpdate(
int clientId, sdk.EventUpdate eventUpdate) async {
if (eventUpdate.type == 'ephemeral') return; if (eventUpdate.type == 'ephemeral') return;
final eventContent = eventUpdate.content; final eventContent = eventUpdate.content;
final type = eventUpdate.type; final type = eventUpdate.type;
@ -227,11 +259,13 @@ class Database extends _$Database {
eventContent['unsigned'] is Map<String, dynamic> && eventContent['unsigned'] is Map<String, dynamic> &&
eventContent['unsigned']['transaction_id'] is String) { eventContent['unsigned']['transaction_id'] is String) {
// status changed and we have an old transaction id --> update event id and stuffs // 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 { } else {
DbEvent oldEvent; DbEvent oldEvent;
if (type == 'history') { 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) { if (allOldEvents.isNotEmpty) {
oldEvent = allOldEvents.first; oldEvent = allOldEvents.first;
} }
@ -241,7 +275,10 @@ class Database extends _$Database {
eventContent['event_id'], eventContent['event_id'],
chatId, chatId,
oldEvent?.sortOrder ?? eventUpdate.sortOrder, 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['sender'],
eventContent['type'], eventContent['type'],
json.encode(eventContent['unsigned'] ?? ''), json.encode(eventContent['unsigned'] ?? ''),
@ -256,7 +293,8 @@ class Database extends _$Database {
if (status != -1 && if (status != -1 &&
eventUpdate.content.containsKey('unsigned') && eventUpdate.content.containsKey('unsigned') &&
eventUpdate.content['unsigned']['transaction_id'] is String) { 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);
} }
} }
@ -269,7 +307,10 @@ class Database extends _$Database {
eventContent['event_id'] ?? now.millisecondsSinceEpoch.toString(), eventContent['event_id'] ?? now.millisecondsSinceEpoch.toString(),
chatId, chatId,
eventUpdate.sortOrder ?? 0.0, 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['sender'],
eventContent['type'], eventContent['type'],
json.encode(eventContent['unsigned'] ?? ''), json.encode(eventContent['unsigned'] ?? ''),
@ -287,7 +328,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(); final event = await getEvent(clientId, eventId, room.id).get();
if (event.isEmpty) { if (event.isEmpty) {
return null; return null;
@ -296,7 +338,9 @@ class Database extends _$Database {
} }
Future<bool> redactMessage(int clientId, sdk.EventUpdate eventUpdate) async { 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; var success = false;
for (final dbEvent in events) { for (final dbEvent in events) {
final event = sdk.Event.fromDb(dbEvent, null); final event = sdk.Event.fromDb(dbEvent, null);
@ -325,29 +369,44 @@ class Database extends _$Database {
Future<void> forgetRoom(int clientId, String roomId) async { Future<void> forgetRoom(int clientId, String roomId) async {
final setKey = '${clientId};${roomId}'; final setKey = '${clientId};${roomId}';
_ensuredRooms.remove(setKey); _ensuredRooms.remove(setKey);
await (delete(rooms)..where((r) => r.roomId.equals(roomId) & r.clientId.equals(clientId))).go(); await (delete(rooms)
await (delete(events)..where((r) => r.roomId.equals(roomId) & r.clientId.equals(clientId))).go(); ..where((r) => r.roomId.equals(roomId) & r.clientId.equals(clientId)))
await (delete(roomStates)..where((r) => r.roomId.equals(roomId) & r.clientId.equals(clientId))).go(); .go();
await (delete(roomAccountData)..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 { Future<void> clearCache(int clientId) async {
await (delete(presences)..where((r) => r.clientId.equals(clientId))).go(); 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(accountData)..where((r) => r.clientId.equals(clientId))).go();
await (delete(roomStates)..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(events)..where((r) => r.clientId.equals(clientId))).go();
await (delete(rooms)..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); await storePrevBatch(null, clientId);
} }
Future<void> clear(int clientId) async { Future<void> clear(int clientId) async {
await clearCache(clientId); 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(olmSessions)..where((r) => r.clientId.equals(clientId))).go();
await (delete(userDeviceKeysKey)..where((r) => r.clientId.equals(clientId))).go(); await (delete(userDeviceKeysKey)..where((r) => r.clientId.equals(clientId)))
await (delete(userDeviceKeys)..where((r) => r.clientId.equals(clientId))).go(); .go();
await (delete(userDeviceKeys)..where((r) => r.clientId.equals(clientId)))
.go();
await (delete(clients)..where((r) => r.clientId.equals(clientId))).go(); await (delete(clients)..where((r) => r.clientId.equals(clientId))).go();
} }

View File

@ -418,7 +418,7 @@ class Event {
} }
Future<void> loadSession() { Future<void> loadSession() {
return room.loadInboundGroupSessionKeyForEvent(this); return room.loadInboundGroupSessionKeyForEvent(this);
} }
/// Trys to decrypt this event. Returns a m.bad.encrypted event /// Trys to decrypt this event. Returns a m.bad.encrypted event

View File

@ -42,7 +42,15 @@ class Presence {
final String statusMsg; final String statusMsg;
final DateTime time; 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) Presence.fromJson(Map<String, dynamic> json)
: sender = json['sender'], : sender = json['sender'],
@ -66,15 +74,16 @@ class Presence {
return Presence( return Presence(
sender: dbEntry.sender, sender: dbEntry.sender,
displayname: content['displayname'], 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'], currentlyActive: content['currently_active'],
lastActiveAgo: content['last_active_ago'], lastActiveAgo: content['last_active_ago'],
time: DateTime.fromMillisecondsSinceEpoch( time: DateTime.fromMillisecondsSinceEpoch(
DateTime.now().millisecondsSinceEpoch - DateTime.now().millisecondsSinceEpoch -
(content['last_active_ago'] ?? 0)), (content['last_active_ago'] ?? 0)),
presence: PresenceType.values.firstWhere( presence: PresenceType.values.firstWhere(
(e) => (e) => e.toString() == "PresenceType.${content['presence']}",
e.toString() == "PresenceType.${content['presence']}",
orElse: () => null), orElse: () => null),
statusMsg: content['status_msg'], statusMsg: content['status_msg'],
); );

View File

@ -112,7 +112,8 @@ class Room {
} }
Future<void> updateSortOrder() async { 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 /// Clears the existing outboundGroupSession, tries to create a new one and
@ -165,10 +166,12 @@ class Room {
Future<void> _storeOutboundGroupSession() async { Future<void> _storeOutboundGroupSession() async {
if (_outboundGroupSession == null) return; if (_outboundGroupSession == null) return;
await client.database?.storeOutboundGroupSession( await client.database?.storeOutboundGroupSession(
client.id, id, _outboundGroupSession.pickle(client.userID), client.id,
json.encode(_outboundGroupSessionDevices), _outboundGroupSessionCreationTime, id,
_outboundGroupSessionSentMessages _outboundGroupSession.pickle(client.userID),
); json.encode(_outboundGroupSessionDevices),
_outboundGroupSessionCreationTime,
_outboundGroupSessionSentMessages);
return; return;
} }
@ -190,21 +193,27 @@ class Room {
} }
// next check if it needs to be rotated // next check if it needs to be rotated
final encryptionContent = getState('m.room.encryption')?.content; 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'] encryptionContent['rotation_period_msgs'] is int
: 100; ? encryptionContent['rotation_period_msgs']
final maxAge = encryptionContent != null && encryptionContent['rotation_period_ms'] is int : 100;
? encryptionContent['rotation_period_ms'] final maxAge = encryptionContent != null &&
: 604800000; // default of one week encryptionContent['rotation_period_ms'] is int
? encryptionContent['rotation_period_ms']
: 604800000; // default of one week
if (_outboundGroupSessionSentMessages >= maxMessages || if (_outboundGroupSessionSentMessages >= maxMessages ||
_outboundGroupSessionCreationTime.add(Duration(milliseconds: maxAge)).isBefore(DateTime.now())) { _outboundGroupSessionCreationTime
.add(Duration(milliseconds: maxAge))
.isBefore(DateTime.now())) {
wipe = true; wipe = true;
} }
if (!wipe) { if (!wipe) {
return false; 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 return true; // let's just short-circuit out of here, no need to do DB stuff
} }
_outboundGroupSessionDevices = null; _outboundGroupSessionDevices = null;
@ -250,8 +259,12 @@ class Room {
indexes: {}, indexes: {},
key: client.userID, key: client.userID,
); );
client.database?.storeInboundGroupSession(client.id, id, sessionId, client.database?.storeInboundGroupSession(
inboundGroupSession.pickle(client.userID), json.encode(content), client.id,
id,
sessionId,
inboundGroupSession.pickle(client.userID),
json.encode(content),
json.encode({}), json.encode({}),
); );
_tryAgainDecryptLastMessage(); _tryAgainDecryptLastMessage();
@ -398,8 +411,7 @@ class Room {
states.forEach((final String key, final entry) { states.forEach((final String key, final entry) {
if (!entry.containsKey('')) return; if (!entry.containsKey('')) return;
final Event state = entry['']; final Event state = entry[''];
if (state.sortOrder != null && if (state.sortOrder != null && state.sortOrder > lastSortOrder) {
state.sortOrder > lastSortOrder) {
lastSortOrder = state.sortOrder; lastSortOrder = state.sortOrder;
lastEvent = state; lastEvent = state;
} }
@ -436,7 +448,8 @@ class Room {
this.roomAccountData = const {}, this.roomAccountData = const {},
double newestSortOrder = 0.0, double newestSortOrder = 0.0,
double oldestSortOrder = 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 /// The default count of how much events should be requested when requesting the
/// history of this room. /// history of this room.
@ -524,7 +537,8 @@ class Room {
name = name.replaceAll(RegExp(r'[^\w-]'), ''); name = name.replaceAll(RegExp(r'[^\w-]'), '');
return name.toLowerCase(); return name.toLowerCase();
}; };
final addEmotePack = (String packName, Map<String, dynamic> content, [String packNameOverride]) { final addEmotePack = (String packName, Map<String, dynamic> content,
[String packNameOverride]) {
if (!(content['short'] is Map)) { if (!(content['short'] is Map)) {
return; return;
} }
@ -572,7 +586,10 @@ class Room {
final stateKey = stateKeyEntry.key; final stateKey = stateKeyEntry.key;
final event = room.getState('im.ponies.room_emotes', stateKey); final event = room.getState('im.ponies.room_emotes', stateKey);
if (event != null && stateKeyEntry.value is Map) { if (event != null && stateKeyEntry.value is Map) {
addEmotePack(room.canonicalAlias.isEmpty ? room.id : canonicalAlias, event.content, stateKeyEntry.value['name']); addEmotePack(
room.canonicalAlias.isEmpty ? room.id : canonicalAlias,
event.content,
stateKeyEntry.value['name']);
} }
} }
} }
@ -583,7 +600,11 @@ 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, {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>{ final event = <String, dynamic>{
'msgtype': 'm.text', 'msgtype': 'm.text',
'body': message, 'body': message,
@ -836,7 +857,11 @@ class Room {
final sortOrder = newSortOrder; final sortOrder = newSortOrder;
// Display a *sending* event and store it. // 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: { content: {
'type': type, 'type': type,
'event_id': messageID, 'event_id': messageID,
@ -994,7 +1019,8 @@ class Room {
final dbActions = <Future<dynamic> Function()>[]; final dbActions = <Future<dynamic> Function()>[];
if (client.database != null) { 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> && if (!(resp['chunk'] is List<dynamic> &&
@ -1012,7 +1038,8 @@ class Room {
).decrypt(this); ).decrypt(this);
client.onEvent.add(eventUpdate); client.onEvent.add(eventUpdate);
if (client.database != null) { if (client.database != null) {
dbActions.add(() => client.database.storeEventUpdate(client.id, eventUpdate)); dbActions.add(
() => client.database.storeEventUpdate(client.id, eventUpdate));
} }
} }
} }
@ -1028,11 +1055,13 @@ class Room {
).decrypt(this); ).decrypt(this);
client.onEvent.add(eventUpdate); client.onEvent.add(eventUpdate);
if (client.database != null) { if (client.database != null) {
dbActions.add(() => client.database.storeEventUpdate(client.id, eventUpdate)); dbActions.add(
() => client.database.storeEventUpdate(client.id, eventUpdate));
} }
} }
if (client.database != null) { 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 { await client.database?.transaction(() async {
for (final f in dbActions) { for (final f in dbActions) {
@ -1105,12 +1134,12 @@ class Room {
/// Returns a Room from a json String which comes normally from the store. If the /// Returns a Room from a json String which comes normally from the store. If the
/// state are also given, the method will await them. /// state are also given, the method will await them.
static Future<Room> getRoomFromTableRow( static Future<Room> getRoomFromTableRow(
DbRoom row, // either Map<String, dynamic> or DbRoom DbRoom row, // either Map<String, dynamic> or DbRoom
Client matrix, Client matrix, {
{ dynamic states, // DbRoomState, as iterator and optionally as future
dynamic states, // DbRoomState, as iterator and optionally as future dynamic
dynamic roomAccountData, // DbRoomAccountData, as iterator and optionally as future roomAccountData, // DbRoomAccountData, as iterator and optionally as future
}) async { }) async {
final newRoom = Room( final newRoom = Room(
id: row.roomId, id: row.roomId,
membership: Membership.values membership: Membership.values
@ -1136,7 +1165,8 @@ class Room {
rawStates = states; rawStates = states;
} }
for (final rawState in rawStates) { for (final rawState in rawStates) {
final newState = Event.fromDb(rawState, newRoom);; final newState = Event.fromDb(rawState, newRoom);
;
newRoom.setState(newState); newRoom.setState(newState);
} }
} }
@ -1179,7 +1209,8 @@ class Room {
await events[i].loadSession(); await events[i].loadSession();
events[i] = events[i].decrypted; events[i] = events[i].decrypted;
if (events[i].type != EventTypes.Encrypted) { if (events[i].type != EventTypes.Encrypted) {
await client.database.storeEventUpdate(client.id, await client.database.storeEventUpdate(
client.id,
EventUpdate( EventUpdate(
eventType: events[i].typeKey, eventType: events[i].typeKey,
content: events[i].toJson(), content: events[i].toJson(),
@ -1301,7 +1332,8 @@ class Room {
'content': resp, 'content': resp,
'state_key': mxID, 'state_key': mxID,
}; };
await client.database.storeEventUpdate(client.id, await client.database.storeEventUpdate(
client.id,
EventUpdate( EventUpdate(
content: content, content: content,
roomID: id, roomID: id,
@ -1748,28 +1780,29 @@ class Room {
var users = await requestParticipants(); var users = await requestParticipants();
for (final user in users) { for (final user in users) {
if (client.userDeviceKeys.containsKey(user.id)) { 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); deviceKeys.add(deviceKeyEntry);
} }
} }
} }
return deviceKeys; return deviceKeys;
} }
bool _restoredOutboundGroupSession = false; bool _restoredOutboundGroupSession = false;
Future<void> restoreOutboundGroupSession() async { Future<void> restoreOutboundGroupSession() async {
if (_restoredOutboundGroupSession || client.database == null) { if (_restoredOutboundGroupSession || client.database == null) {
return; return;
} }
final outboundSession = await client.database.getDbOutboundGroupSession(client.id, id); final outboundSession =
await client.database.getDbOutboundGroupSession(client.id, id);
if (outboundSession != null) { if (outboundSession != null) {
try { try {
_outboundGroupSession = olm.OutboundGroupSession(); _outboundGroupSession = olm.OutboundGroupSession();
_outboundGroupSession.unpickle( _outboundGroupSession.unpickle(client.userID, outboundSession.pickle);
client.userID, outboundSession.pickle);
_outboundGroupSessionDevices = _outboundGroupSessionDevices =
List<String>.from(json.decode(outboundSession.deviceIds)); List<String>.from(json.decode(outboundSession.deviceIds));
_outboundGroupSessionCreationTime = outboundSession.creationTime; _outboundGroupSessionCreationTime = outboundSession.creationTime;
_outboundGroupSessionSentMessages = outboundSession.sentMessages; _outboundGroupSessionSentMessages = outboundSession.sentMessages;
} catch (e) { } catch (e) {
@ -1825,46 +1858,53 @@ class Room {
Future<void> requestSessionKey(String sessionId, String senderKey) async { Future<void> requestSessionKey(String sessionId, String senderKey) async {
final users = await requestParticipants(); final users = await requestParticipants();
await client.sendToDevice( await client.sendToDevice(
[], [],
'm.room_key_request', 'm.room_key_request',
{ {
'action': 'request_cancellation', 'action': 'request_cancellation',
'request_id': base64.encode(utf8.encode(sessionId)), 'request_id': base64.encode(utf8.encode(sessionId)),
'requesting_device_id': client.deviceID, 'requesting_device_id': client.deviceID,
},
encrypted: false,
toUsers: users);
await client.sendToDevice(
[],
'm.room_key_request',
{
'action': 'request',
'body': {
'algorithm': 'm.megolm.v1.aes-sha2',
'room_id': id,
'sender_key': senderKey,
'session_id': sessionId,
}, },
'request_id': base64.encode(utf8.encode(sessionId)), encrypted: false,
'requesting_device_id': client.deviceID, toUsers: users);
}, await client.sendToDevice(
encrypted: false, [],
toUsers: users); 'm.room_key_request',
{
'action': 'request',
'body': {
'algorithm': 'm.megolm.v1.aes-sha2',
'room_id': id,
'sender_key': senderKey,
'session_id': sessionId,
},
'request_id': base64.encode(utf8.encode(sessionId)),
'requesting_device_id': client.deviceID,
},
encrypted: false,
toUsers: users);
} }
Future<void> loadInboundGroupSessionKey(String sessionId, [String senderKey]) async { Future<void> loadInboundGroupSessionKey(String sessionId,
if (sessionId == null || inboundGroupSessions.containsKey(sessionId)) return; // nothing to do [String senderKey]) async {
final session = await client.database.getDbInboundGroupSession(client.id, id, sessionId); if (sessionId == null || inboundGroupSessions.containsKey(sessionId)) {
return;
} // nothing to do
final session = await client.database
.getDbInboundGroupSession(client.id, id, sessionId);
if (session == null) { if (session == null) {
// no session found, let's request it! // 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)); unawaited(requestSessionKey(sessionId, senderKey));
_requestedSessionIds.add(sessionId); _requestedSessionIds.add(sessionId);
} }
return; return;
} }
try { try {
_inboundGroupSessions[sessionId] = SessionKey.fromDb(session, client.userID); _inboundGroupSessions[sessionId] =
SessionKey.fromDb(session, client.userID);
} catch (e) { } catch (e) {
print('[LibOlm] Could not unpickle inboundGroupSession: ' + e.toString()); print('[LibOlm] Could not unpickle inboundGroupSession: ' + e.toString());
} }
@ -1887,7 +1927,8 @@ class Room {
/// Returns a m.bad.encrypted event if it fails and does nothing if the event /// Returns a m.bad.encrypted event if it fails and does nothing if the event
/// was not encrypted. /// was not encrypted.
Event decryptGroupMessage(Event event) { 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; Map<String, dynamic> decryptedPayload;
try { try {
if (!client.encryptionEnabled) { if (!client.encryptionEnabled) {
@ -1905,7 +1946,9 @@ class Room {
.decrypt(event.content['ciphertext']); .decrypt(event.content['ciphertext']);
final messageIndexKey = final messageIndexKey =
event.eventId + event.time.millisecondsSinceEpoch.toString(); event.eventId + event.time.millisecondsSinceEpoch.toString();
if (inboundGroupSessions[sessionId].indexes.containsKey(messageIndexKey) && if (inboundGroupSessions[sessionId]
.indexes
.containsKey(messageIndexKey) &&
inboundGroupSessions[sessionId].indexes[messageIndexKey] != inboundGroupSessions[sessionId].indexes[messageIndexKey] !=
decryptResult.message_index) { decryptResult.message_index) {
if ((_outboundGroupSession?.session_id() ?? '') == sessionId) { if ((_outboundGroupSession?.session_id() ?? '') == sessionId) {
@ -1919,11 +1962,17 @@ class Room {
// the entry should always exist. In the case it doesn't, the following // 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 // 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 // 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); decryptedPayload = json.decode(decryptResult.plaintext);
} catch (exception) { } catch (exception) {
// alright, if this was actually by our own outbound group session, we might as well clear it // 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); clearOutboundGroupSession(wipe: true);
} }
if (exception.toString() == DecryptError.UNKNOWN_SESSION) { if (exception.toString() == DecryptError.UNKNOWN_SESSION) {

View File

@ -43,7 +43,8 @@ class EventUpdate {
// the order where to stort this event // the order where to stort this event
final double sortOrder; 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) { EventUpdate decrypt(Room room) {
if (eventType != 'm.room.encrypted') { if (eventType != 'm.room.encrypted') {

View File

@ -83,7 +83,9 @@ class Timeline {
// as r.limitedTimeline can be "null" sometimes, we need to check for == true // 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 // 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 // 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()); roomSub ??= room.client.onRoomUpdate.stream
.where((r) => r.id == room.id && r.limitedTimeline == true)
.listen((r) => events.clear());
sessionIdReceivedSub ??= sessionIdReceivedSub ??=
room.onSessionKeyReceived.stream.listen(_sessionKeyReceived); room.onSessionKeyReceived.stream.listen(_sessionKeyReceived);
} }
@ -129,8 +131,8 @@ class Timeline {
if (eventUpdate.eventType == 'm.room.redaction') { if (eventUpdate.eventType == 'm.room.redaction') {
final eventId = _findEvent(event_id: eventUpdate.content['redacts']); final eventId = _findEvent(event_id: eventUpdate.content['redacts']);
if (eventId != null) { if (eventId != null) {
events[eventId] events[eventId].setRedactionEvent(Event.fromJson(
.setRedactionEvent(Event.fromJson(eventUpdate.content, room, eventUpdate.sortOrder)); eventUpdate.content, room, eventUpdate.sortOrder));
} }
} else if (eventUpdate.content['status'] == -2) { } else if (eventUpdate.content['status'] == -2) {
var i = _findEvent(event_id: eventUpdate.content['event_id']); var i = _findEvent(event_id: eventUpdate.content['event_id']);
@ -146,18 +148,23 @@ class Timeline {
: null); : null);
if (i < events.length) { if (i < events.length) {
events[i] = Event.fromJson(eventUpdate.content, room, eventUpdate.sortOrder); events[i] = Event.fromJson(
eventUpdate.content, room, eventUpdate.sortOrder);
} }
} else { } else {
Event newEvent; Event newEvent;
var senderUser = room.getState('m.room.member', eventUpdate.content['sender'])?.asUser ?? await room.client.database var senderUser = room
?.getUser(room.client.id, eventUpdate.content['sender'], room); .getState('m.room.member', eventUpdate.content['sender'])
?.asUser ??
await room.client.database?.getUser(
room.client.id, eventUpdate.content['sender'], room);
if (senderUser != null) { if (senderUser != null) {
eventUpdate.content['displayname'] = senderUser.displayName; eventUpdate.content['displayname'] = senderUser.displayName;
eventUpdate.content['avatar_url'] = senderUser.avatarUrl.toString(); 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' && if (eventUpdate.type == 'history' &&
events.indexWhere( events.indexWhere(

View File

@ -10,7 +10,8 @@ class DeviceKeysList {
bool outdated = true; bool outdated = true;
Map<String, DeviceKeys> deviceKeys = {}; Map<String, DeviceKeys> deviceKeys = {};
DeviceKeysList.fromDb(DbUserDeviceKey dbEntry, List<DbUserDeviceKeysKey> childEntries) { DeviceKeysList.fromDb(
DbUserDeviceKey dbEntry, List<DbUserDeviceKeysKey> childEntries) {
userId = dbEntry.userId; userId = dbEntry.userId;
outdated = dbEntry.outdated; outdated = dbEntry.outdated;
deviceKeys = {}; deviceKeys = {};
@ -67,11 +68,16 @@ class DeviceKeys {
String get curve25519Key => keys['curve25519:$deviceId']; String get curve25519Key => keys['curve25519:$deviceId'];
String get ed25519Key => keys['ed25519:$deviceId']; String get ed25519Key => keys['ed25519:$deviceId'];
bool get isValid => userId != null && deviceId != null && curve25519Key != null && ed25519Key != null; bool get isValid =>
userId != null &&
deviceId != null &&
curve25519Key != null &&
ed25519Key != null;
Future<void> setVerified(bool newVerified, Client client) { Future<void> setVerified(bool newVerified, Client client) {
verified = newVerified; 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, Client client) { Future<void> setBlocked(bool newBlocked, Client client) {
@ -82,7 +88,8 @@ class DeviceKeys {
room.clearOutboundGroupSession(); room.clearOutboundGroupSession();
} }
} }
return client.database?.setBlockedUserDeviceKey(newBlocked, client.id, userId, deviceId); return client.database
?.setBlockedUserDeviceKey(newBlocked, client.id, userId, deviceId);
} }
DeviceKeys({ DeviceKeys({
@ -101,7 +108,9 @@ class DeviceKeys {
userId = dbEntry.userId; userId = dbEntry.userId;
deviceId = dbEntry.deviceId; deviceId = dbEntry.deviceId;
algorithms = content['algorithms'].cast<String>(); algorithms = content['algorithms'].cast<String>();
keys = content['keys'] != null ? Map<String, String>.from(content['keys']) : null; keys = content['keys'] != null
? Map<String, String>.from(content['keys'])
: null;
signatures = content['signatures'] != null signatures = content['signatures'] != null
? Map<String, dynamic>.from(content['signatures']) ? Map<String, dynamic>.from(content['signatures'])
: null; : null;
@ -147,7 +156,8 @@ class DeviceKeys {
} }
KeyVerification startVerification(Client client) { KeyVerification startVerification(Client client) {
final request = KeyVerification(client: client, userId: userId, deviceId: deviceId); final request =
KeyVerification(client: client, userId: userId, deviceId: deviceId);
request.start(); request.start();
client.addKeyVerificationRequest(request); client.addKeyVerificationRequest(request);
return request; return request;

View File

@ -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) { List<String> _intersect(List<String> a, List<dynamic> b) {
final res = <String>[]; final res = <String>[];
@ -76,7 +83,8 @@ List<int> _bytesToInt(Uint8List bytes, int totalBits) {
final VERIFICATION_METHODS = [_KeyVerificationMethodSas.type]; final VERIFICATION_METHODS = [_KeyVerificationMethodSas.type];
_KeyVerificationMethod _makeVerificationMethod(String type, KeyVerification request) { _KeyVerificationMethod _makeVerificationMethod(
String type, KeyVerification request) {
if (type == _KeyVerificationMethodSas.type) { if (type == _KeyVerificationMethodSas.type) {
return _KeyVerificationMethodSas(request: request); return _KeyVerificationMethodSas(request: request);
} }
@ -89,7 +97,7 @@ class KeyVerification {
final Room room; final Room room;
final String userId; final String userId;
void Function() onUpdate; void Function() onUpdate;
String get deviceId => _deviceId; String get deviceId => _deviceId;
String _deviceId; String _deviceId;
bool startedVerification = false; bool startedVerification = false;
_KeyVerificationMethod method; _KeyVerificationMethod method;
@ -104,7 +112,8 @@ class KeyVerification {
String canceledCode; String canceledCode;
String canceledReason; 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(); lastActivity = DateTime.now();
_deviceId ??= deviceId; _deviceId ??= deviceId;
} }
@ -115,9 +124,10 @@ class KeyVerification {
} }
static String getTransactionId(Map<String, dynamic> payload) { static String getTransactionId(Map<String, dynamic> payload) {
return payload['transaction_id'] ?? ( return payload['transaction_id'] ??
payload['m.relates_to'] is Map ? payload['m.relates_to']['event_id'] : null (payload['m.relates_to'] is Map
); ? payload['m.relates_to']['event_id']
: null);
} }
Future<void> start() async { Future<void> start() async {
@ -132,7 +142,8 @@ class KeyVerification {
setState(KeyVerificationState.waitingAccept); 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()); print('[Key Verification] Received type ${type}: ' + payload.toString());
try { try {
switch (type) { switch (type) {
@ -141,13 +152,16 @@ class KeyVerification {
transactionId ??= eventId ?? payload['transaction_id']; transactionId ??= eventId ?? payload['transaction_id'];
// verify the timestamp // verify the timestamp
final now = DateTime.now(); final now = DateTime.now();
final verifyTime = DateTime.fromMillisecondsSinceEpoch(payload['timestamp']); final verifyTime =
if (now.subtract(Duration(minutes: 10)).isAfter(verifyTime) || now.add(Duration(minutes: 5)).isBefore(verifyTime)) { DateTime.fromMillisecondsSinceEpoch(payload['timestamp']);
if (now.subtract(Duration(minutes: 10)).isAfter(verifyTime) ||
now.add(Duration(minutes: 5)).isBefore(verifyTime)) {
await cancel('m.timeout'); await cancel('m.timeout');
return; return;
} }
// verify it has a method we can use // verify it has a method we can use
possibleMethods = _intersect(VERIFICATION_METHODS, payload['methods']); possibleMethods =
_intersect(VERIFICATION_METHODS, payload['methods']);
if (possibleMethods.isEmpty) { if (possibleMethods.isEmpty) {
// reject it outright // reject it outright
await cancel('m.unknown_method'); await cancel('m.unknown_method');
@ -156,7 +170,8 @@ class KeyVerification {
setState(KeyVerificationState.askAccept); setState(KeyVerificationState.askAccept);
break; break;
case 'm.key.verification.ready': case 'm.key.verification.ready':
possibleMethods = _intersect(VERIFICATION_METHODS, payload['methods']); possibleMethods =
_intersect(VERIFICATION_METHODS, payload['methods']);
if (possibleMethods.isEmpty) { if (possibleMethods.isEmpty) {
// reject it outright // reject it outright
await cancel('m.unknown_method'); await cancel('m.unknown_method');
@ -214,7 +229,8 @@ class KeyVerification {
/// called when the user accepts an incoming verification /// called when the user accepts an incoming verification
Future<void> acceptVerification() async { 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; return;
} }
setState(KeyVerificationState.waitingAccept); setState(KeyVerificationState.waitingAccept);
@ -231,7 +247,8 @@ class KeyVerification {
/// called when the user rejects an incoming verification /// called when the user rejects an incoming verification
Future<void> rejectVerification() async { 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; return;
} }
await cancel('m.user'); await cancel('m.user');
@ -251,7 +268,9 @@ class KeyVerification {
List<int> get sasNumbers { List<int> get sasNumbers {
if (method is _KeyVerificationMethodSas) { 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 []; return [];
} }
@ -265,13 +284,15 @@ class KeyVerification {
List<KeyVerificationEmoji> get sasEmojis { List<KeyVerificationEmoji> get sasEmojis {
if (method is _KeyVerificationMethodSas) { 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 numbers.map((n) => KeyVerificationEmoji(n)).toList().sublist(0, 7);
} }
return []; return [];
} }
Future<void> verifyKeys(Map<String, String> keys, Future<bool> Function(String, DeviceKeys) verifier) async { Future<void> verifyKeys(Map<String, String> keys,
Future<bool> Function(String, DeviceKeys) verifier) async {
final verifiedDevices = <String>[]; final verifiedDevices = <String>[];
if (!client.userDeviceKeys.containsKey(userId)) { if (!client.userDeviceKeys.containsKey(userId)) {
@ -282,8 +303,10 @@ class KeyVerification {
final keyId = entry.key; final keyId = entry.key;
final verifyDeviceId = keyId.substring('ed25519:'.length); final verifyDeviceId = keyId.substring('ed25519:'.length);
final keyInfo = entry.value; final keyInfo = entry.value;
if (client.userDeviceKeys[userId].deviceKeys.containsKey(verifyDeviceId)) { if (client.userDeviceKeys[userId].deviceKeys
if (!(await verifier(keyInfo, client.userDeviceKeys[userId].deviceKeys[verifyDeviceId]))) { .containsKey(verifyDeviceId)) {
if (!(await verifier(keyInfo,
client.userDeviceKeys[userId].deviceKeys[verifyDeviceId]))) {
await cancel('m.key_mismatch'); await cancel('m.key_mismatch');
return; return;
} }
@ -295,12 +318,14 @@ class KeyVerification {
} }
// okay, we reached this far, so all the devices are verified! // okay, we reached this far, so all the devices are verified!
for (final verifyDeviceId in verifiedDevices) { for (final verifyDeviceId in verifiedDevices) {
await client.userDeviceKeys[userId].deviceKeys[verifyDeviceId].setVerified(true, client); await client.userDeviceKeys[userId].deviceKeys[verifyDeviceId]
.setVerified(true, client);
} }
} }
Future<bool> verifyActivity() async { 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(); lastActivity = DateTime.now();
return true; return true;
} }
@ -351,7 +376,8 @@ class KeyVerification {
if (['m.key.verification.request'].contains(type)) { if (['m.key.verification.request'].contains(type)) {
payload['msgtype'] = type; payload['msgtype'] = type;
payload['to'] = userId; 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'; type = 'm.room.message';
} }
final newTransactionId = await room.sendEvent(payload, type: type); final newTransactionId = await room.sendEvent(payload, type: type);
@ -360,7 +386,8 @@ class KeyVerification {
client.addKeyVerificationRequest(this); client.addKeyVerificationRequest(this);
} }
} else { } else {
await client.sendToDevice([client.userDeviceKeys[userId].deviceKeys[deviceId]], type, payload); await client.sendToDevice(
[client.userDeviceKeys[userId].deviceKeys[deviceId]], type, payload);
} }
} }
@ -385,6 +412,7 @@ abstract class _KeyVerificationMethod {
bool validateStart(Map<String, dynamic> payload) { bool validateStart(Map<String, dynamic> payload) {
return false; return false;
} }
Future<void> sendStart(); Future<void> sendStart();
void dispose() {} void dispose() {}
} }
@ -395,7 +423,8 @@ const KNOWN_MESSAGE_AUTHENTIFICATION_CODES = ['hkdf-hmac-sha256'];
const KNOWN_AUTHENTICATION_TYPES = ['emoji', 'decimal']; const KNOWN_AUTHENTICATION_TYPES = ['emoji', 'decimal'];
class _KeyVerificationMethodSas extends _KeyVerificationMethod { class _KeyVerificationMethodSas extends _KeyVerificationMethod {
_KeyVerificationMethodSas({KeyVerification request}) : super(request: request); _KeyVerificationMethodSas({KeyVerification request})
: super(request: request);
static String type = 'm.sas.v1'; static String type = 'm.sas.v1';
@ -419,7 +448,8 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
try { try {
switch (type) { switch (type) {
case 'm.key.verification.start': 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 return; // abort
} }
if (!validateStart(payload)) { if (!validateStart(payload)) {
@ -439,7 +469,8 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
await _sendKey(); await _sendKey();
break; break;
case 'm.key.verification.key': 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; return;
} }
_handleKey(payload); _handleKey(payload);
@ -506,7 +537,8 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
if (payload['method'] != type) { if (payload['method'] != type) {
return false; 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) { if (possibleKeyAgreementProtocols.isEmpty) {
return false; return false;
} }
@ -516,12 +548,15 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
return false; return false;
} }
hash = possibleHashes.first; 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) { if (possibleMessageAuthenticationCodes.isEmpty) {
return false; return false;
} }
messageAuthenticationCode = possibleMessageAuthenticationCodes.first; 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) { if (possibleAuthenticationTypes.isEmpty) {
return false; return false;
} }
@ -544,7 +579,8 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
} }
bool _handleAccept(Map<String, dynamic> payload) { 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; return false;
} }
keyAgreementProtocol = payload['key_agreement_protocol']; keyAgreementProtocol = payload['key_agreement_protocol'];
@ -552,11 +588,13 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
return false; return false;
} }
hash = payload['hash']; 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; return false;
} }
messageAuthenticationCode = payload['message_authentication_code']; 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) { if (possibleAuthenticationTypes.isEmpty) {
return false; return false;
} }
@ -585,13 +623,23 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
Uint8List makeSas(int bytes) { Uint8List makeSas(int bytes) {
var sasInfo = ''; var sasInfo = '';
if (keyAgreementProtocol == 'curve25519-hkdf-sha256') { if (keyAgreementProtocol == 'curve25519-hkdf-sha256') {
final ourInfo = '${client.userID}|${client.deviceID}|${sas.get_pubkey()}|'; final ourInfo =
final theirInfo = '${request.userId}|${request.deviceId}|${theirPublicKey}|'; '${client.userID}|${client.deviceID}|${sas.get_pubkey()}|';
sasInfo = 'MATRIX_KEY_VERIFICATION_SAS|' + (request.startedVerification ? ourInfo + theirInfo : theirInfo + ourInfo) + request.transactionId; final theirInfo =
'${request.userId}|${request.deviceId}|${theirPublicKey}|';
sasInfo = 'MATRIX_KEY_VERIFICATION_SAS|' +
(request.startedVerification
? ourInfo + theirInfo
: theirInfo + ourInfo) +
request.transactionId;
} else if (keyAgreementProtocol == 'curve25519') { } else if (keyAgreementProtocol == 'curve25519') {
final ourInfo = client.userID + client.deviceID; final ourInfo = client.userID + client.deviceID;
final theirInfo = request.userId + request.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 { } else {
throw 'Unknown key agreement protocol'; throw 'Unknown key agreement protocol';
} }
@ -600,9 +648,11 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
Future<void> _sendMac() async { Future<void> _sendMac() async {
final baseInfo = 'MATRIX_KEY_VERIFICATION_MAC' + final baseInfo = 'MATRIX_KEY_VERIFICATION_MAC' +
client.userID + client.deviceID + client.userID +
request.userId + request.deviceId + client.deviceID +
request.transactionId; request.userId +
request.deviceId +
request.transactionId;
final mac = <String, String>{}; final mac = <String, String>{};
final keyList = <String>[]; final keyList = <String>[];
@ -610,7 +660,8 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
// for now it is just our device key, once we have cross-signing // for now it is just our device key, once we have cross-signing
// we would also add the cross signing key here // we would also add the cross signing key here
final deviceKeyId = 'ed25519:${client.deviceID}'; final deviceKeyId = 'ed25519:${client.deviceID}';
mac[deviceKeyId] = _calculateMac(client.fingerprintKey, baseInfo + deviceKeyId); mac[deviceKeyId] =
_calculateMac(client.fingerprintKey, baseInfo + deviceKeyId);
keyList.add(deviceKeyId); keyList.add(deviceKeyId);
keyList.sort(); keyList.sort();
@ -624,13 +675,16 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
Future<void> _processMac() async { Future<void> _processMac() async {
final payload = macPayload; final payload = macPayload;
final baseInfo = 'MATRIX_KEY_VERIFICATION_MAC' + final baseInfo = 'MATRIX_KEY_VERIFICATION_MAC' +
request.userId + request.deviceId + request.userId +
client.userID + client.deviceID + request.deviceId +
request.transactionId; client.userID +
client.deviceID +
request.transactionId;
final keyList = payload['mac'].keys.toList(); final keyList = payload['mac'].keys.toList();
keyList.sort(); 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'); await request.cancel('m.key_mismatch');
return; return;
} }
@ -646,7 +700,9 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
} }
} }
await request.verifyKeys(mac, (String mac, DeviceKeys device) async { await request.verifyKeys(mac, (String mac, DeviceKeys device) async {
return mac == _calculateMac(device.ed25519Key, baseInfo + 'ed25519:' + device.deviceId); return mac ==
_calculateMac(
device.ed25519Key, baseInfo + 'ed25519:' + device.deviceId);
}); });
await request.send('m.key.verification.done', {}); await request.send('m.key.verification.done', {});
if (request.state != KeyVerificationState.error) { if (request.state != KeyVerificationState.error) {

View File

@ -13,11 +13,12 @@ class LinebreakSyntax extends InlineSyntax {
class SpoilerSyntax extends TagSyntax { class SpoilerSyntax extends TagSyntax {
Map<String, String> reasonMap = <String, String>{}; Map<String, String> reasonMap = <String, String>{};
SpoilerSyntax() : super( SpoilerSyntax()
r'\|\|(?:([^\|]+)\|(?!\|))?', : super(
requiresDelimiterRun: true, r'\|\|(?:([^\|]+)\|(?!\|))?',
end: r'\|\|', requiresDelimiterRun: true,
); end: r'\|\|',
);
@override @override
bool onMatch(InlineParser parser, Match match) { bool onMatch(InlineParser parser, Match match) {
@ -31,7 +32,8 @@ class SpoilerSyntax extends TagSyntax {
@override @override
bool onMatchEnd(InlineParser parser, Match match, TagState state) { bool onMatchEnd(InlineParser parser, Match match, TagState state) {
final element = Element('span', state.children); 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); parser.addNode(element);
return true; return true;
} }
@ -88,14 +90,33 @@ class PillSyntax extends InlineSyntax {
String markdown(String text, [Map<String, Map<String, String>> emotePacks]) { String markdown(String text, [Map<String, Map<String, String>> emotePacks]) {
emotePacks ??= <String, Map<String, String>>{}; emotePacks ??= <String, Map<String, String>>{};
var ret = markdownToHtml(text, var ret = markdownToHtml(
text,
extensionSet: ExtensionSet.commonMark, extensionSet: ExtensionSet.commonMark,
inlineSyntaxes: [StrikethroughSyntax(), LinebreakSyntax(), SpoilerSyntax(), EmoteSyntax(emotePacks), PillSyntax()], inlineSyntaxes: [
StrikethroughSyntax(),
LinebreakSyntax(),
SpoilerSyntax(),
EmoteSyntax(emotePacks),
PillSyntax()
],
); );
var stripPTags = '<p>'.allMatches(ret).length <= 1; var stripPTags = '<p>'.allMatches(ret).length <= 1;
if (stripPTags) { 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) { for (final tag in otherBlockTags) {
// we check for the close tag as the opening one might have attributes // we check for the close tag as the opening one might have attributes
if (ret.contains('</${tag}>')) { if (ret.contains('</${tag}>')) {

View File

@ -25,8 +25,9 @@ class RoomKeyRequest extends ToDeviceEvent {
await requestingDevice?.setVerified(true, client); await requestingDevice?.setVerified(true, client);
var message = session.content; var message = session.content;
message['forwarding_curve25519_key_chain'] = forwardedKeys; 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( await client.sendToDevice(
[requestingDevice], [requestingDevice],
'm.forwarded_room_key', 'm.forwarded_room_key',

View File

@ -20,9 +20,8 @@ class SessionKey {
SessionKey.fromDb(DbInboundGroupSession dbEntry, String key) : key = key { SessionKey.fromDb(DbInboundGroupSession dbEntry, String key) : key = key {
final parsedContent = Event.getMapFromPayload(dbEntry.content); final parsedContent = Event.getMapFromPayload(dbEntry.content);
final parsedIndexes = Event.getMapFromPayload(dbEntry.indexes); final parsedIndexes = Event.getMapFromPayload(dbEntry.indexes);
content = parsedContent != null content =
? Map<String, dynamic>.from(parsedContent) parsedContent != null ? Map<String, dynamic>.from(parsedContent) : null;
: null;
indexes = parsedIndexes != null indexes = parsedIndexes != null
? Map<String, int>.from(parsedIndexes) ? Map<String, int>.from(parsedIndexes)
: <String, int>{}; : <String, int>{};

View File

@ -15,12 +15,15 @@ void main() {
}, },
}; };
test('simple markdown', () { 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!'); expect(markdown('wha ~~strike~~ works!'), 'wha <del>strike</del> works!');
}); });
test('spoilers', () { test('spoilers', () {
expect(markdown('Snape killed ||Dumbledoor||'), 'Snape killed <span data-mx-spoiler="">Dumbledoor</span>'); expect(markdown('Snape killed ||Dumbledoor||'),
expect(markdown('Snape killed ||Story|Dumbledoor||'), 'Snape killed <span data-mx-spoiler="Story">Dumbledoor</span>'); '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', () { test('multiple paragraphs', () {
expect(markdown('Heya!\n\nBeep'), '<p>Heya!</p>\n<p>Beep</p>'); 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'); expect(markdown('foxies\ncute'), 'foxies<br />\ncute');
}); });
test('emotes', () { test('emotes', () {
expect(markdown(':fox:', emotePacks), '<img src="mxc:&#47;&#47;roomfox" alt=":fox:" title=":fox:" height="32" vertical-align="middle" />'); expect(markdown(':fox:', emotePacks),
expect(markdown(':user~fox:', emotePacks), '<img src="mxc:&#47;&#47;userfox" alt=":fox:" title=":fox:" height="32" vertical-align="middle" />'); '<img src="mxc:&#47;&#47;roomfox" alt=":fox:" title=":fox:" height="32" vertical-align="middle" />');
expect(markdown(':raccoon:', emotePacks), '<img src="mxc:&#47;&#47;raccoon" alt=":raccoon:" title=":raccoon:" height="32" vertical-align="middle" />'); expect(markdown(':user~fox:', emotePacks),
'<img src="mxc:&#47;&#47;userfox" alt=":fox:" title=":fox:" height="32" vertical-align="middle" />');
expect(markdown(':raccoon:', emotePacks),
'<img src="mxc:&#47;&#47;raccoon" alt=":raccoon:" title=":raccoon:" height="32" vertical-align="middle" />');
expect(markdown(':invalid:', emotePacks), ':invalid:'); expect(markdown(':invalid:', emotePacks), ':invalid:');
expect(markdown(':room~invalid:', emotePacks), ':room~invalid:'); expect(markdown(':room~invalid:', emotePacks), ':room~invalid:');
}); });
test('pills', () { test('pills', () {
expect(markdown('Hey @sorunome:sorunome.de!'), 'Hey <a href="https://matrix.to/#/@sorunome:sorunome.de">@sorunome:sorunome.de</a>!'); expect(markdown('Hey @sorunome:sorunome.de!'),
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'); 'Hey <a href="https://matrix.to/#/@sorunome:sorunome.de">@sorunome:sorunome.de</a>!');
expect(markdown('!blah:example.org'), '<a href="https://matrix.to/#/!blah:example.org">!blah:example.org</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>');
}); });
}); });
} }

View File

@ -128,7 +128,8 @@ void test() async {
await Future.delayed(Duration(seconds: 5)); await Future.delayed(Duration(seconds: 5));
assert(room.outboundGroupSession != null); assert(room.outboundGroupSession != null);
var currentSessionIdA = room.outboundGroupSession.session_id(); 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(testClientA.olmSessions[testClientB.identityKey].length == 1);
assert(testClientB.olmSessions[testClientA.identityKey].length == 1); assert(testClientB.olmSessions[testClientA.identityKey].length == 1);
assert(testClientA.olmSessions[testClientB.identityKey].first.session_id() == assert(testClientA.olmSessions[testClientB.identityKey].first.session_id() ==
@ -236,7 +237,8 @@ void test() async {
assert(restoredRoom.outboundGroupSession.session_id() == assert(restoredRoom.outboundGroupSession.session_id() ==
room.outboundGroupSession.session_id()); room.outboundGroupSession.session_id());
assert(restoredRoom.inboundGroupSessions.length == 4); 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++) { for (var i = 0; i < restoredRoom.inboundGroupSessions.length; i++) {
assert(restoredRoom.inboundGroupSessions.keys.toList()[i] == assert(restoredRoom.inboundGroupSessions.keys.toList()[i] ==
room.inboundGroupSessions.keys.toList()[i]); room.inboundGroupSessions.keys.toList()[i]);