import 'package:moor/moor.dart'; import 'dart:convert'; import 'package:famedlysdk/famedlysdk.dart' as sdk; import 'package:olm/olm.dart' as olm; part 'database.g.dart'; @UseMoor( include: {'database.moor'}, ) class Database extends _$Database { Database(QueryExecutor e) : super(e); @override int get schemaVersion => 3; int get maxFileSize => 1 * 1024 * 1024; @override MigrationStrategy get migration => MigrationStrategy( onCreate: (Migrator m) { return m.createAll(); }, 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 if (from == 1) { await m.createIndex(userDeviceKeysIndex); await m.createIndex(userDeviceKeysKeyIndex); await m.createIndex(olmSessionsIndex); await m.createIndex(outboundGroupSessionsIndex); await m.createIndex(inboundGroupSessionsIndex); await m.createIndex(roomsIndex); await m.createIndex(eventsIndex); await m.createIndex(roomStatesIndex); await m.createIndex(accountDataIndex); await m.createIndex(roomAccountDataIndex); await m.createIndex(presencesIndex); from++; } if (from == 2) { await m.deleteTable('outbound_group_sessions'); await m.createTable(outboundGroupSessions); } }, ); Future getClient(String name) async { final res = await dbGetClient(name).get(); if (res.isEmpty) return null; return res.first; } Future> getUserDeviceKeys( int clientId) async { final deviceKeys = await getAllUserDeviceKeys(clientId).get(); if (deviceKeys.isEmpty) { return {}; } final deviceKeysKeys = await getAllUserDeviceKeysKeys(clientId).get(); final res = {}; for (final entry in deviceKeys) { res[entry.userId] = sdk.DeviceKeysList.fromDb(entry, deviceKeysKeys.where((k) => k.userId == entry.userId).toList()); } return res; } Future>> getOlmSessions( int clientId, String userId) async { final raw = await getAllOlmSessions(clientId).get(); if (raw.isEmpty) { return {}; } final res = >{}; for (final row in raw) { if (!res.containsKey(row.identityKey)) { res[row.identityKey] = []; } try { var session = olm.Session(); session.unpickle(userId, row.pickle); res[row.identityKey].add(session); } catch (e) { print('[LibOlm] Could not unpickle olm session: ' + e.toString()); } } return res; } Future getDbOutboundGroupSession( int clientId, String roomId) async { final res = await dbGetOutboundGroupSession(clientId, roomId).get(); if (res.isEmpty) { return null; } return res.first; } Future> getDbInboundGroupSessions( int clientId, String roomId) async { return await dbGetInboundGroupSessionKeys(clientId, roomId).get(); } Future 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> 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(); final resStates = await getAllRoomStates(client.id).get(); final resAccountData = await getAllRoomAccountData(client.id).get(); final roomList = []; for (final r in res) { final room = await sdk.Room.getRoomFromTableRow( r, client, states: resStates.where((rs) => rs.roomId == r.roomId), roomAccountData: resAccountData.where((rs) => rs.roomId == r.roomId), ); roomList.add(room); } return roomList; } Future> getAccountData(int clientId) async { final newAccountData = {}; final rawAccountData = await getAllAccountData(clientId).get(); for (final d in rawAccountData) { newAccountData[d.type] = sdk.AccountData.fromDb(d); } return newAccountData; } Future> getPresences(int clientId) async { final newPresences = {}; final rawPresences = await getAllPresences(clientId).get(); for (final d in rawPresences) { newPresences[d.sender] = sdk.Presence.fromDb(d); } return newPresences; } /// Stores a RoomUpdate object in the database. Must be called inside of /// [transaction]. final Set _ensuredRooms = {}; Future 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); _ensuredRooms.add(setKey); } } else { _ensuredRooms.remove(setKey); await removeRoom(clientId, roomUpdate.id); return; } var doUpdate = oldRoom == null; if (!doUpdate) { doUpdate = roomUpdate.highlight_count != oldRoom.highlightCount || roomUpdate.notification_count != oldRoom.notificationCount || roomUpdate.membership.toString().split('.').last != oldRoom.membership.toString().split('.').last || (roomUpdate.summary?.mJoinedMemberCount != null && roomUpdate.summary.mJoinedMemberCount != oldRoom.mInvitedMemberCount) || (roomUpdate.summary?.mInvitedMemberCount != null && roomUpdate.summary.mJoinedMemberCount != oldRoom.mJoinedMemberCount) || (roomUpdate.summary?.mHeroes != null && 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( 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(), )); } // Is the timeline limited? Then all previous messages should be // removed from the database! if (roomUpdate.limitedTimeline) { await removeRoomEvents(clientId, roomUpdate.id); await updateRoomSortOrder(0.0, 0.0, clientId, roomUpdate.id); await setRoomPrevBatch(roomUpdate.prev_batch, clientId, roomUpdate.id); } } /// Stores an UserUpdate object in the database. Must be called inside of /// [transaction]. Future storeUserEventUpdate( int clientId, sdk.UserUpdate userUpdate) async { if (userUpdate.type == 'account_data') { 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'])); } } /// Stores an EventUpdate object in the database. Must be called inside of /// [transaction]. Future storeEventUpdate( int clientId, sdk.EventUpdate eventUpdate) async { if (eventUpdate.type == 'ephemeral') return; final eventContent = eventUpdate.content; final type = eventUpdate.type; final chatId = eventUpdate.roomID; // Get the state_key for state events var stateKey = ''; if (eventContent['state_key'] is String) { stateKey = eventContent['state_key']; } if (eventUpdate.eventType == 'm.room.redaction') { await redactMessage(clientId, eventUpdate); } if (type == 'timeline' || type == 'history') { // calculate the status var status = 2; if (eventContent['status'] is num) status = eventContent['status']; if ((status == 1 || status == -1) && eventContent['unsigned'] is Map && 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); } else { DbEvent oldEvent; if (type == 'history') { final allOldEvents = await getEvent(clientId, eventContent['event_id'], chatId).get(); if (allOldEvents.isNotEmpty) { oldEvent = allOldEvents.first; } } await storeEvent( clientId, eventContent['event_id'], chatId, oldEvent?.sortOrder ?? eventUpdate.sortOrder, eventContent['origin_server_ts'] != null ? DateTime.fromMillisecondsSinceEpoch( eventContent['origin_server_ts']) : DateTime.now(), eventContent['sender'], eventContent['type'], json.encode(eventContent['unsigned'] ?? ''), json.encode(eventContent['content']), json.encode(eventContent['prevContent']), eventContent['state_key'], status, ); } // is there a transaction id? Then delete the event with this id. if (status != -1 && eventUpdate.content.containsKey('unsigned') && eventUpdate.content['unsigned']['transaction_id'] is String) { await removeEvent(clientId, eventUpdate.content['unsigned']['transaction_id'], chatId); } } if (type == 'history') return; if (type != 'account_data') { final now = DateTime.now(); await storeRoomState( clientId, eventContent['event_id'] ?? now.millisecondsSinceEpoch.toString(), chatId, eventUpdate.sortOrder ?? 0.0, eventContent['origin_server_ts'] != null ? DateTime.fromMillisecondsSinceEpoch( eventContent['origin_server_ts']) : now, eventContent['sender'], eventContent['type'], json.encode(eventContent['unsigned'] ?? ''), json.encode(eventContent['content']), json.encode(eventContent['prev_content'] ?? ''), stateKey, ); } else if (type == 'account_data') { await storeRoomAccountData( clientId, eventContent['type'], chatId, json.encode(eventContent['content']), ); } } Future getEventById( int clientId, String eventId, sdk.Room room) async { final event = await getEvent(clientId, eventId, room.id).get(); if (event.isEmpty) { return null; } return sdk.Event.fromDb(event.first, room); } Future redactMessage(int clientId, sdk.EventUpdate eventUpdate) async { 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); event.setRedactionEvent(sdk.Event.fromJson(eventUpdate.content, null)); final changes1 = await updateEvent( json.encode(event.unsigned ?? ''), json.encode(event.content ?? ''), json.encode(event.prevContent ?? ''), clientId, event.eventId, eventUpdate.roomID, ); final changes2 = await updateEvent( json.encode(event.unsigned ?? ''), json.encode(event.content ?? ''), json.encode(event.prevContent ?? ''), clientId, event.eventId, eventUpdate.roomID, ); if (changes1 == 1 && changes2 == 1) success = true; } return success; } Future 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(); } Future 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(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 storePrevBatch(null, clientId); } Future clear(int clientId) async { await clearCache(clientId); 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(clients)..where((r) => r.clientId.equals(clientId))).go(); } Future getUser(int clientId, String userId, sdk.Room room) async { final res = await dbGetUser(clientId, userId, room.id).get(); if (res.isEmpty) { return null; } return sdk.Event.fromDb(res.first, room).asUser; } Future> getEventList(int clientId, sdk.Room room) async { final res = await dbGetEventList(clientId, room.id).get(); final eventList = []; for (final r in res) { eventList.add(sdk.Event.fromDb(r, room)); } return eventList; } Future getFile(String mxcUri) async { final res = await dbGetFile(mxcUri).get(); if (res.isEmpty) return null; return res.first.bytes; } }