FurryChat/lib/utils/famedlysdk_store.dart

571 lines
19 KiB
Dart
Raw Normal View History

2020-01-01 18:10:13 +00:00
import 'dart:convert';
import 'package:famedlysdk/famedlysdk.dart';
2020-01-26 11:17:54 +00:00
import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:localstorage/localstorage.dart';
import 'dart:async';
import 'dart:core';
2020-01-01 18:10:13 +00:00
import 'package:path/path.dart' as p;
import 'package:sqflite/sqflite.dart';
class Store extends StoreAPI {
final Client client;
2020-01-26 11:17:54 +00:00
final LocalStorage storage;
final FlutterSecureStorage secureStorage;
2020-01-01 18:10:13 +00:00
2020-01-26 11:17:54 +00:00
Store(this.client)
2020-01-30 13:45:35 +00:00
: storage = LocalStorage('LocalStorage'),
2020-01-26 11:17:54 +00:00
secureStorage = kIsWeb ? null : FlutterSecureStorage() {
2020-01-01 18:10:13 +00:00
_init();
}
2020-01-26 11:17:54 +00:00
Future<dynamic> getItem(String key) async {
if (kIsWeb) {
await storage.ready;
return await storage.getItem(key);
}
return await secureStorage.read(key: key);
}
Future<void> setItem(String key, String value) async {
if (kIsWeb) {
await storage.ready;
return await storage.setItem(key, value);
}
return await secureStorage.write(key: key, value: value);
}
2020-02-04 13:42:35 +00:00
Future<Map<String, DeviceKeysList>> getUserDeviceKeys() async {
final deviceKeysListString = await getItem(_UserDeviceKeysKey);
if (deviceKeysListString == null) return {};
Map<String, dynamic> rawUserDeviceKeys = json.decode(deviceKeysListString);
Map<String, DeviceKeysList> userDeviceKeys = {};
for (final entry in rawUserDeviceKeys.entries) {
userDeviceKeys[entry.key] = DeviceKeysList.fromJson(entry.value);
}
return userDeviceKeys;
}
Future<void> storeUserDeviceKeys(
Map<String, DeviceKeysList> userDeviceKeys) async {
await setItem(_UserDeviceKeysKey, json.encode(userDeviceKeys));
}
String get _UserDeviceKeysKey => "${client.clientName}.user_device_keys";
2020-01-26 11:17:54 +00:00
_init() async {
final credentialsStr = await getItem(client.clientName);
if (credentialsStr == null || credentialsStr.isEmpty) {
client.onLoginStateChanged.add(LoginState.loggedOut);
return;
}
debugPrint("[Matrix] Restoring account credentials");
final Map<String, dynamic> credentials = json.decode(credentialsStr);
client.connect(
newDeviceID: credentials["deviceID"],
newDeviceName: credentials["deviceName"],
newHomeserver: credentials["homeserver"],
newLazyLoadMembers: credentials["lazyLoadMembers"],
newMatrixVersions: List<String>.from(credentials["matrixVersions"]),
newToken: credentials["token"],
newUserID: credentials["userID"],
newPrevBatch: kIsWeb
? null
: (credentials["prev_batch"]?.isEmpty ?? true)
? null
: credentials["prev_batch"],
2020-02-15 09:14:45 +00:00
newOlmAccount: credentials["olmAccount"],
2020-01-26 11:17:54 +00:00
);
}
Future<void> storeClient() async {
final Map<String, dynamic> credentials = {
"deviceID": client.deviceID,
"deviceName": client.deviceName,
"homeserver": client.homeserver,
"lazyLoadMembers": client.lazyLoadMembers,
"matrixVersions": client.matrixVersions,
"token": client.accessToken,
"userID": client.userID,
2020-02-15 09:14:45 +00:00
"olmAccount": client.pickledOlmAccount,
2020-01-26 11:17:54 +00:00
};
await setItem(client.clientName, json.encode(credentials));
return;
}
Future<void> clear() => kIsWeb ? storage.clear() : secureStorage.deleteAll();
}
/// Responsible to store all data persistent and to query objects from the
/// database.
class ExtendedStore extends Store implements ExtendedStoreAPI {
@override
final bool extended = true;
ExtendedStore(Client client) : super(client);
2020-01-01 18:10:13 +00:00
Database _db;
2020-01-26 11:17:54 +00:00
var txn;
2020-01-01 18:10:13 +00:00
/// SQLite database for all persistent data. It is recommended to extend this
/// SDK instead of writing direct queries to the database.
//Database get db => _db;
2020-01-26 11:17:54 +00:00
@override
2020-01-01 18:10:13 +00:00
_init() async {
2020-01-26 11:17:54 +00:00
// Open the database and migrate if necessary.
2020-01-01 18:10:13 +00:00
var databasePath = await getDatabasesPath();
String path = p.join(databasePath, "FluffyMatrix.db");
2020-01-26 11:17:54 +00:00
_db = await openDatabase(path, version: 16,
2020-01-01 18:10:13 +00:00
onCreate: (Database db, int version) async {
await createTables(db);
}, onUpgrade: (Database db, int oldVersion, int newVersion) async {
2020-01-26 11:17:54 +00:00
debugPrint(
"[Store] Migrate database from version $oldVersion to $newVersion");
2020-01-01 18:10:13 +00:00
if (oldVersion != newVersion) {
2020-01-26 11:17:54 +00:00
// Look for an old entry in an old clients library
List<Map> list = [];
try {
list = await db.rawQuery(
"SELECT * FROM Clients WHERE client=?", [client.clientName]);
} on DatabaseException catch (_) {} catch (_) {
rethrow;
}
if (list.length == 1) {
debugPrint("[Store] Found old client from deprecated store");
var clientList = list[0];
_db = db;
client.connect(
newToken: clientList["token"],
newHomeserver: clientList["homeserver"],
newUserID: clientList["matrix_id"],
newDeviceID: clientList["device_id"],
newDeviceName: clientList["device_name"],
newLazyLoadMembers: clientList["lazy_load_members"] == 1,
newMatrixVersions:
clientList["matrix_versions"].toString().split(","),
newPrevBatch: null,
);
await db.execute("DROP TABLE IF EXISTS Clients");
if (client.debug) {
debugPrint(
"[Store] Restore client credentials from deprecated database of ${client.userID}");
}
schemes.forEach((String name, String scheme) async {
await db.execute("DROP TABLE IF EXISTS $name");
});
await createTables(db);
}
} else {
client.onLoginStateChanged.add(LoginState.loggedOut);
2020-01-01 18:10:13 +00:00
}
});
2020-01-26 11:17:54 +00:00
// Mark all pending events as failed.
2020-01-01 18:10:13 +00:00
await _db.rawUpdate("UPDATE Events SET status=-1 WHERE status=0");
2020-01-26 11:17:54 +00:00
super._init();
}
2020-01-01 18:10:13 +00:00
2020-01-26 11:17:54 +00:00
Future<void> setRoomPrevBatch(String roomId, String prevBatch) async {
await txn.rawUpdate(
"UPDATE Rooms SET prev_batch=? WHERE room_id=?", [roomId, prevBatch]);
return;
2020-01-01 18:10:13 +00:00
}
Future<void> createTables(Database db) async {
2020-01-02 11:27:02 +00:00
schemes.forEach((String name, String scheme) async {
2020-01-01 18:10:13 +00:00
await db.execute(scheme);
});
}
/// Clears all tables from the database.
Future<void> clear() async {
2020-01-02 11:27:02 +00:00
schemes.forEach((String name, String scheme) async {
2020-01-26 11:17:54 +00:00
await _db.rawDelete("DELETE FROM $name");
2020-01-01 18:10:13 +00:00
});
2020-01-26 11:17:54 +00:00
await super.clear();
2020-01-01 18:10:13 +00:00
return;
}
2020-01-06 20:22:50 +00:00
Future<void> transaction(Function queries) async {
2020-01-01 18:10:13 +00:00
return _db.transaction((txnObj) async {
2020-01-06 20:22:50 +00:00
txn = txnObj.batch();
queries();
await txn.commit(noResult: true);
2020-01-01 18:10:13 +00:00
});
}
2020-01-26 11:17:54 +00:00
/// Will be automatically called on every synchronisation.
Future<void> storePrevBatch(String prevBatch) async {
final credentialsStr = await getItem(client.clientName);
if (credentialsStr == null) return;
final Map<String, dynamic> credentials = json.decode(credentialsStr);
credentials["prev_batch"] = prevBatch;
await setItem(client.clientName, json.encode(credentials));
2020-01-01 18:10:13 +00:00
}
Future<void> storeRoomPrevBatch(Room room) async {
await _db.rawUpdate("UPDATE Rooms SET prev_batch=? WHERE room_id=?",
[room.prev_batch, room.id]);
return null;
}
/// Stores a RoomUpdate object in the database. Must be called inside of
/// [transaction].
Future<void> storeRoomUpdate(RoomUpdate roomUpdate) {
if (txn == null) return null;
// Insert the chat into the database if not exists
2020-01-02 21:31:39 +00:00
if (roomUpdate.membership != Membership.leave) {
2020-01-01 18:10:13 +00:00
txn.rawInsert(
"INSERT OR IGNORE INTO Rooms " + "VALUES(?, ?, 0, 0, '', 0, 0, '') ",
[roomUpdate.id, roomUpdate.membership.toString().split('.').last]);
2020-01-02 21:31:39 +00:00
} else {
2020-01-01 18:10:13 +00:00
txn.rawDelete("DELETE FROM Rooms WHERE room_id=? ", [roomUpdate.id]);
return null;
}
// Update the notification counts and the limited timeline boolean and the summary
String updateQuery =
"UPDATE Rooms SET highlight_count=?, notification_count=?, membership=?";
List<dynamic> updateArgs = [
roomUpdate.highlight_count,
roomUpdate.notification_count,
roomUpdate.membership.toString().split('.').last
];
if (roomUpdate.summary?.mJoinedMemberCount != null) {
updateQuery += ", joined_member_count=?";
updateArgs.add(roomUpdate.summary.mJoinedMemberCount);
}
if (roomUpdate.summary?.mInvitedMemberCount != null) {
updateQuery += ", invited_member_count=?";
updateArgs.add(roomUpdate.summary.mInvitedMemberCount);
}
if (roomUpdate.summary?.mHeroes != null) {
updateQuery += ", heroes=?";
updateArgs.add(roomUpdate.summary.mHeroes.join(","));
}
updateQuery += " WHERE room_id=?";
updateArgs.add(roomUpdate.id);
txn.rawUpdate(updateQuery, updateArgs);
// Is the timeline limited? Then all previous messages should be
// removed from the database!
if (roomUpdate.limitedTimeline) {
txn.rawDelete("DELETE FROM Events WHERE room_id=?", [roomUpdate.id]);
txn.rawUpdate("UPDATE Rooms SET prev_batch=? WHERE room_id=?",
[roomUpdate.prev_batch, roomUpdate.id]);
}
return null;
}
/// Stores an UserUpdate object in the database. Must be called inside of
/// [transaction].
Future<void> storeUserEventUpdate(UserUpdate userUpdate) {
if (txn == null) return null;
2020-01-02 21:31:39 +00:00
if (userUpdate.type == "account_data") {
2020-01-01 18:10:13 +00:00
txn.rawInsert("INSERT OR REPLACE INTO AccountData VALUES(?, ?)", [
userUpdate.eventType,
json.encode(userUpdate.content["content"]),
]);
2020-01-02 21:31:39 +00:00
} else if (userUpdate.type == "presence") {
2020-01-01 18:10:13 +00:00
txn.rawInsert("INSERT OR REPLACE INTO Presences VALUES(?, ?, ?)", [
userUpdate.eventType,
userUpdate.content["sender"],
json.encode(userUpdate.content["content"]),
]);
2020-01-02 21:31:39 +00:00
}
2020-01-01 18:10:13 +00:00
return null;
}
Future<dynamic> redactMessage(EventUpdate eventUpdate) async {
List<Map<String, dynamic>> res = await _db.rawQuery(
"SELECT * FROM Events WHERE event_id=?",
[eventUpdate.content["redacts"]]);
if (res.length == 1) {
Event event = Event.fromJson(res[0], null);
event.setRedactionEvent(Event.fromJson(eventUpdate.content, null));
final int changes1 = await _db.rawUpdate(
"UPDATE Events SET unsigned=?, content=?, prev_content=? WHERE event_id=?",
[
json.encode(event.unsigned ?? ""),
json.encode(event.content ?? ""),
json.encode(event.prevContent ?? ""),
event.eventId,
],
);
final int changes2 = await _db.rawUpdate(
"UPDATE RoomStates SET unsigned=?, content=?, prev_content=? WHERE event_id=?",
[
json.encode(event.unsigned ?? ""),
json.encode(event.content ?? ""),
json.encode(event.prevContent ?? ""),
event.eventId,
],
);
if (changes1 == 1 && changes2 == 1) return true;
}
return false;
}
/// Stores an EventUpdate object in the database. Must be called inside of
/// [transaction].
Future<void> storeEventUpdate(EventUpdate eventUpdate) {
2020-01-04 16:10:59 +00:00
if (txn == null || eventUpdate.type == "ephemeral") return null;
2020-01-01 18:10:13 +00:00
Map<String, dynamic> eventContent = eventUpdate.content;
String type = eventUpdate.type;
2020-01-02 11:27:02 +00:00
String chatId = eventUpdate.roomID;
2020-01-01 18:10:13 +00:00
// Get the state_key for m.room.member events
2020-01-02 11:27:02 +00:00
String stateKey = "";
2020-01-01 18:10:13 +00:00
if (eventContent["state_key"] is String) {
2020-01-02 11:27:02 +00:00
stateKey = eventContent["state_key"];
2020-01-01 18:10:13 +00:00
}
if (eventUpdate.eventType == "m.room.redaction") {
redactMessage(eventUpdate);
}
if (type == "timeline" || type == "history") {
// calculate the status
num status = 2;
if (eventContent["status"] is num) status = eventContent["status"];
// Save the event in the database
if ((status == 1 || status == -1) &&
eventContent["unsigned"] is Map<String, dynamic> &&
2020-01-02 21:31:39 +00:00
eventContent["unsigned"]["transaction_id"] is String) {
2020-01-01 18:10:13 +00:00
txn.rawUpdate(
"UPDATE Events SET status=?, event_id=? WHERE event_id=?", [
status,
eventContent["event_id"],
eventContent["unsigned"]["transaction_id"]
]);
2020-01-02 21:31:39 +00:00
} else {
2020-01-01 18:10:13 +00:00
txn.rawInsert(
"INSERT OR REPLACE INTO Events VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
[
eventContent["event_id"],
2020-01-02 11:27:02 +00:00
chatId,
2020-01-01 18:10:13 +00:00
eventContent["origin_server_ts"],
eventContent["sender"],
eventContent["type"],
json.encode(eventContent["unsigned"] ?? ""),
json.encode(eventContent["content"]),
json.encode(eventContent["prevContent"]),
eventContent["state_key"],
status
]);
2020-01-02 21:31:39 +00:00
}
2020-01-01 18:10:13 +00:00
// Is there a transaction id? Then delete the event with this id.
if (status != -1 &&
eventUpdate.content.containsKey("unsigned") &&
2020-01-02 21:31:39 +00:00
eventUpdate.content["unsigned"]["transaction_id"] is String) {
2020-01-01 18:10:13 +00:00
txn.rawDelete("DELETE FROM Events WHERE event_id=?",
[eventUpdate.content["unsigned"]["transaction_id"]]);
2020-01-02 21:31:39 +00:00
}
2020-01-01 18:10:13 +00:00
}
if (type == "history") return null;
2020-02-09 10:50:25 +00:00
if (type != "account_data") {
2020-01-01 18:10:13 +00:00
final String now = DateTime.now().millisecondsSinceEpoch.toString();
txn.rawInsert(
"INSERT OR REPLACE INTO RoomStates VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)",
[
eventContent["event_id"] ?? now,
2020-01-02 11:27:02 +00:00
chatId,
2020-01-01 18:10:13 +00:00
eventContent["origin_server_ts"] ?? now,
eventContent["sender"],
2020-01-02 11:27:02 +00:00
stateKey,
2020-01-01 18:10:13 +00:00
json.encode(eventContent["unsigned"] ?? ""),
json.encode(eventContent["prev_content"] ?? ""),
eventContent["type"],
json.encode(eventContent["content"]),
]);
2020-01-04 16:10:59 +00:00
} else if (type == "account_data") {
2020-01-01 18:10:13 +00:00
txn.rawInsert("INSERT OR REPLACE INTO RoomAccountData VALUES(?, ?, ?)", [
eventContent["type"],
2020-01-02 11:27:02 +00:00
chatId,
2020-01-01 18:10:13 +00:00
json.encode(eventContent["content"]),
]);
2020-01-02 21:31:39 +00:00
}
2020-01-01 18:10:13 +00:00
return null;
}
/// Returns a User object by a given Matrix ID and a Room.
Future<User> getUser({String matrixID, Room room}) async {
List<Map<String, dynamic>> res = await _db.rawQuery(
"SELECT * FROM RoomStates WHERE state_key=? AND room_id=?",
[matrixID, room.id]);
if (res.length != 1) return null;
2020-01-02 14:10:21 +00:00
return Event.fromJson(res[0], room).asUser;
2020-01-01 18:10:13 +00:00
}
/// Returns a list of events for the given room and sets all participants.
Future<List<Event>> getEventList(Room room) async {
List<Map<String, dynamic>> eventRes = await _db.rawQuery(
"SELECT * " +
" FROM Events " +
" WHERE room_id=?" +
" GROUP BY event_id " +
" ORDER BY origin_server_ts DESC",
[room.id]);
List<Event> eventList = [];
2020-01-02 21:31:39 +00:00
for (num i = 0; i < eventRes.length; i++) {
2020-01-01 18:10:13 +00:00
eventList.add(Event.fromJson(eventRes[i], room));
2020-01-02 21:31:39 +00:00
}
2020-01-01 18:10:13 +00:00
return eventList;
}
/// Returns all rooms, the client is participating. Excludes left rooms.
Future<List<Room>> getRoomList({bool onlyLeft = false}) async {
List<Map<String, dynamic>> res = await _db.rawQuery("SELECT * " +
" FROM Rooms" +
" WHERE membership" +
(onlyLeft ? "=" : "!=") +
"'leave' " +
" GROUP BY room_id ");
List<Room> roomList = [];
for (num i = 0; i < res.length; i++) {
Room room = await Room.getRoomFromTableRow(
res[i],
client,
states: getStatesFromRoomId(res[i]["room_id"]),
roomAccountData: getAccountDataFromRoomId(res[i]["room_id"]),
2020-01-01 18:10:13 +00:00
);
2020-01-04 16:12:24 +00:00
roomList.add(room);
2020-01-01 18:10:13 +00:00
}
return roomList;
}
Future<List<Map<String, dynamic>>> getStatesFromRoomId(String id) async {
2020-02-14 14:26:39 +00:00
return _db.rawQuery(
"SELECT * FROM RoomStates WHERE room_id=? AND type IS NOT NULL", [id]);
2020-01-01 18:10:13 +00:00
}
Future<List<Map<String, dynamic>>> getAccountDataFromRoomId(String id) async {
return _db.rawQuery("SELECT * FROM RoomAccountData WHERE room_id=?", [id]);
}
Future<void> resetNotificationCount(String roomID) async {
await _db.rawDelete(
"UPDATE Rooms SET notification_count=0, highlight_count=0 WHERE room_id=?",
[roomID]);
return;
}
Future<void> forgetRoom(String roomID) async {
await _db.rawDelete("DELETE FROM Rooms WHERE room_id=?", [roomID]);
return;
}
/// Searches for the event in the store.
Future<Event> getEventById(String eventID, Room room) async {
List<Map<String, dynamic>> res = await _db.rawQuery(
"SELECT * FROM Events WHERE event_id=? AND room_id=?",
[eventID, room.id]);
2020-01-02 21:31:39 +00:00
if (res.isEmpty) return null;
2020-01-01 18:10:13 +00:00
return Event.fromJson(res[0], room);
}
Future<Map<String, AccountData>> getAccountData() async {
Map<String, AccountData> newAccountData = {};
List<Map<String, dynamic>> rawAccountData =
await _db.rawQuery("SELECT * FROM AccountData");
2020-01-02 21:31:39 +00:00
for (int i = 0; i < rawAccountData.length; i++) {
2020-01-01 18:10:13 +00:00
newAccountData[rawAccountData[i]["type"]] =
AccountData.fromJson(rawAccountData[i]);
2020-01-02 21:31:39 +00:00
}
2020-01-01 18:10:13 +00:00
return newAccountData;
}
Future<Map<String, Presence>> getPresences() async {
Map<String, Presence> newPresences = {};
2020-01-02 11:27:02 +00:00
List<Map<String, dynamic>> rawPresences =
2020-01-01 18:10:13 +00:00
await _db.rawQuery("SELECT * FROM Presences");
2020-01-02 11:27:02 +00:00
for (int i = 0; i < rawPresences.length; i++) {
Map<String, dynamic> rawPresence = {
"sender": rawPresences[i]["sender"],
"content": json.decode(rawPresences[i]["content"]),
};
newPresences[rawPresences[i]["type"]] = Presence.fromJson(rawPresence);
}
2020-01-01 18:10:13 +00:00
return newPresences;
}
Future removeEvent(String eventId) async {
assert(eventId != "");
await _db.rawDelete("DELETE FROM Events WHERE event_id=?", [eventId]);
return;
}
static final Map<String, String> schemes = {
/// The database scheme for the Room class.
'Rooms': 'CREATE TABLE IF NOT EXISTS Rooms(' +
'room_id TEXT PRIMARY KEY, ' +
'membership TEXT, ' +
'highlight_count INTEGER, ' +
'notification_count INTEGER, ' +
'prev_batch TEXT, ' +
'joined_member_count INTEGER, ' +
'invited_member_count INTEGER, ' +
'heroes TEXT, ' +
'UNIQUE(room_id))',
/// The database scheme for the TimelineEvent class.
'Events': 'CREATE TABLE IF NOT EXISTS Events(' +
'event_id TEXT PRIMARY KEY, ' +
'room_id TEXT, ' +
'origin_server_ts INTEGER, ' +
'sender TEXT, ' +
'type TEXT, ' +
'unsigned TEXT, ' +
'content TEXT, ' +
'prev_content TEXT, ' +
'state_key TEXT, ' +
"status INTEGER, " +
'UNIQUE(event_id))',
/// The database scheme for room states.
'RoomStates': 'CREATE TABLE IF NOT EXISTS RoomStates(' +
'event_id TEXT PRIMARY KEY, ' +
'room_id TEXT, ' +
'origin_server_ts INTEGER, ' +
'sender TEXT, ' +
'state_key TEXT, ' +
'unsigned TEXT, ' +
'prev_content TEXT, ' +
'type TEXT, ' +
'content TEXT, ' +
'UNIQUE(room_id,state_key,type))',
/// The database scheme for room states.
'AccountData': 'CREATE TABLE IF NOT EXISTS AccountData(' +
'type TEXT PRIMARY KEY, ' +
'content TEXT, ' +
'UNIQUE(type))',
/// The database scheme for room states.
'RoomAccountData': 'CREATE TABLE IF NOT EXISTS RoomAccountData(' +
2020-01-05 11:27:03 +00:00
'type TEXT, ' +
2020-01-01 18:10:13 +00:00
'room_id TEXT, ' +
'content TEXT, ' +
'UNIQUE(type,room_id))',
/// The database scheme for room states.
'Presences': 'CREATE TABLE IF NOT EXISTS Presences(' +
'type TEXT PRIMARY KEY, ' +
'sender TEXT, ' +
'content TEXT, ' +
'UNIQUE(sender))',
};
}