Soru/moor

This commit is contained in:
Sorunome 2020-05-13 13:58:59 +00:00 committed by Christian Pauly
commit f594c7005d
55 changed files with 1034 additions and 1133 deletions

View file

@ -1,13 +1,13 @@
extension BeautifyStringExtension on String {
String get beautified {
String beautifiedStr = "";
for (int i = 0; i < this.length; i++) {
beautifiedStr += this.substring(i, i + 1);
var beautifiedStr = '';
for (var i = 0; i < length; i++) {
beautifiedStr += substring(i, i + 1);
if (i % 4 == 3) {
beautifiedStr += " ";
beautifiedStr += ' ';
}
if (i % 16 == 15) {
beautifiedStr += "\n";
beautifiedStr += '\n';
}
}
return beautifiedStr;

View file

@ -0,0 +1,12 @@
import 'package:famedlysdk/famedlysdk.dart';
import 'package:encrypted_moor/encrypted_moor.dart';
import 'package:flutter/material.dart';
Database constructDb({bool logStatements = false, String filename = 'database.sqlite', String password = ''}) {
debugPrint('[Moor] using encrypted moor');
return Database(EncryptedExecutor(path: filename, password: password, logStatements: logStatements));
}
Future<String> getLocalstorage(String key) async {
return null;
}

View file

@ -0,0 +1,3 @@
export 'unsupported.dart'
if (dart.library.html) 'web.dart'
if (dart.library.io) 'mobile.dart';

View file

@ -0,0 +1,9 @@
import 'package:famedlysdk/famedlysdk.dart';
Database constructDb({bool logStatements = false, String filename = 'database.sqlite', String password = ''}) {
throw 'Platform not supported';
}
Future<String> getLocalstorage(String key) async {
return null;
}

View file

@ -0,0 +1,13 @@
import 'package:famedlysdk/famedlysdk.dart';
import 'package:moor/moor_web.dart';
import 'package:flutter/material.dart';
import 'dart:html';
Database constructDb({bool logStatements = false, String filename = 'database.sqlite', String password = ''}) {
debugPrint('[Moor] Using moor web');
return Database(WebDatabase.withStorage(MoorWebStorage.indexedDbIfSupported(filename), logStatements: logStatements));
}
Future<String> getLocalstorage(String key) async {
return await window.localStorage[key];
}

View file

@ -3,20 +3,20 @@ import 'package:flutter/material.dart';
/// Provides extra functionality for formatting the time.
extension DateTimeExtension on DateTime {
operator <(DateTime other) {
return this.millisecondsSinceEpoch < other.millisecondsSinceEpoch;
bool operator <(DateTime other) {
return millisecondsSinceEpoch < other.millisecondsSinceEpoch;
}
operator >(DateTime other) {
return this.millisecondsSinceEpoch > other.millisecondsSinceEpoch;
bool operator >(DateTime other) {
return millisecondsSinceEpoch > other.millisecondsSinceEpoch;
}
operator >=(DateTime other) {
return this.millisecondsSinceEpoch >= other.millisecondsSinceEpoch;
bool operator >=(DateTime other) {
return millisecondsSinceEpoch >= other.millisecondsSinceEpoch;
}
operator <=(DateTime other) {
return this.millisecondsSinceEpoch <= other.millisecondsSinceEpoch;
bool operator <=(DateTime other) {
return millisecondsSinceEpoch <= other.millisecondsSinceEpoch;
}
/// Two message events can belong to the same environment. That means that they
@ -34,28 +34,28 @@ extension DateTimeExtension on DateTime {
/// Returns a simple time String.
/// TODO: Add localization
String localizedTimeOfDay(BuildContext context) {
return L10n.of(context).timeOfDay(_z(this.hour % 12), _z(this.hour),
_z(this.minute), this.hour > 11 ? 'pm' : 'am');
return L10n.of(context).timeOfDay(
_z(hour % 12), _z(hour), _z(minute), hour > 11 ? 'pm' : 'am');
}
/// Returns [localizedTimeOfDay()] if the ChatTime is today, the name of the week
/// day if the ChatTime is this week and a date string else.
String localizedTimeShort(BuildContext context) {
DateTime now = DateTime.now();
var now = DateTime.now();
bool sameYear = now.year == this.year;
var sameYear = now.year == year;
bool sameDay = sameYear && now.month == this.month && now.day == this.day;
var sameDay = sameYear && now.month == month && now.day == day;
bool sameWeek = sameYear &&
var sameWeek = sameYear &&
!sameDay &&
now.millisecondsSinceEpoch - this.millisecondsSinceEpoch <
now.millisecondsSinceEpoch - millisecondsSinceEpoch <
1000 * 60 * 60 * 24 * 7;
if (sameDay) {
return localizedTimeOfDay(context);
} else if (sameWeek) {
switch (this.weekday) {
switch (weekday) {
case 1:
return L10n.of(context).monday;
case 2:
@ -73,29 +73,26 @@ extension DateTimeExtension on DateTime {
}
} else if (sameYear) {
return L10n.of(context).dateWithoutYear(
this.month.toString().padLeft(2, '0'),
this.day.toString().padLeft(2, '0'));
month.toString().padLeft(2, '0'), day.toString().padLeft(2, '0'));
}
return L10n.of(context).dateWithYear(
this.year.toString(),
this.month.toString().padLeft(2, '0'),
this.day.toString().padLeft(2, '0'));
return L10n.of(context).dateWithYear(year.toString(),
month.toString().padLeft(2, '0'), day.toString().padLeft(2, '0'));
}
/// If the DateTime is today, this returns [localizedTimeOfDay()], if not it also
/// shows the date.
/// TODO: Add localization
String localizedTime(BuildContext context) {
DateTime now = DateTime.now();
var now = DateTime.now();
bool sameYear = now.year == this.year;
var sameYear = now.year == year;
bool sameDay = sameYear && now.month == this.month && now.day == this.day;
var sameDay = sameYear && now.month == month && now.day == day;
if (sameDay) return localizedTimeOfDay(context);
return L10n.of(context).dateAndTimeOfDay(
localizedTimeShort(context), localizedTimeOfDay(context));
}
static String _z(int i) => i < 10 ? "0${i.toString()}" : i.toString();
static String _z(int i) => i < 10 ? '0${i.toString()}' : i.toString();
}

View file

@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
extension LocalizedBody on Event {
IconData get statusIcon {
switch (this.status) {
switch (status) {
case -1:
return Icons.error_outline;
case 0:
@ -22,21 +22,21 @@ extension LocalizedBody on Event {
[MessageTypes.Image, MessageTypes.Sticker].contains(messageType) &&
(kIsWeb ||
(content['info'] is Map &&
content['info']['size'] < room.client.store.maxFileSize));
content['info']['size'] < room.client.database.maxFileSize));
String get sizeString {
if (content["info"] is Map<String, dynamic> &&
content["info"].containsKey("size")) {
num size = content["info"]["size"];
if (content['info'] is Map<String, dynamic> &&
content['info'].containsKey('size')) {
num size = content['info']['size'];
if (size < 1000000) {
size = size / 1000;
return "${size.toString()}kb";
return '${size.toString()}kb';
} else if (size < 1000000000) {
size = size / 1000000;
return "${size.toString()}mb";
return '${size.toString()}mb';
} else {
size = size / 1000000000;
return "${size.toString()}gb";
return '${size.toString()}gb';
}
} else {
return null;

View file

@ -1,25 +1,186 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:famedlysdk/famedlysdk.dart';
import 'package:flutter/material.dart';
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';
import 'package:path/path.dart' as p;
import 'package:sqflite/sqflite.dart';
import './database/shared.dart';
import 'package:olm/olm.dart' as olm; // needed for migration
import 'package:random_string/random_string.dart';
class Store extends StoreAPI {
final Client client;
Future<Database> getDatabase(Client client, Store store) async {
var password = await store.getItem('database-password');
var needMigration = false;
if (password == null || password.isEmpty) {
needMigration = true;
password = randomString(255);
}
final db = constructDb(
logStatements: false,
filename: 'moor.sqlite',
password: password,
);
if (needMigration) {
await migrate(client.clientName, db, store);
await store.setItem('database-password', password);
}
return db;
}
Future<void> migrate(String clientName, Database db, Store store) async {
debugPrint('[Store] attempting old migration to moor...');
final oldKeys = await store.getAllItems();
if (oldKeys == null || oldKeys.isEmpty) {
debugPrint('[Store] empty store!');
return; // we are done!
}
final credentialsStr = oldKeys[clientName];
if (credentialsStr == null || credentialsStr.isEmpty) {
debugPrint('[Store] no credentials found!');
return; // no credentials
}
final Map<String, dynamic> credentials = json.decode(credentialsStr);
if (!credentials.containsKey('homeserver') ||
!credentials.containsKey('token') ||
!credentials.containsKey('userID')) {
debugPrint('[Store] invalid credentials!');
return; // invalid old store, we are done, too!
}
var clientId = 0;
final oldClient = await db.getClient(clientName);
if (oldClient == null) {
clientId = await db.insertClient(
clientName,
credentials['homeserver'],
credentials['token'],
credentials['userID'],
credentials['deviceID'],
credentials['deviceName'],
null,
credentials['olmAccount'],
);
} else {
clientId = oldClient.clientId;
await db.updateClient(
credentials['homeserver'],
credentials['token'],
credentials['userID'],
credentials['deviceID'],
credentials['deviceName'],
null,
credentials['olmAccount'],
clientId,
);
}
await db.clearCache(clientId);
debugPrint('[Store] Inserted/updated client, clientId = ${clientId}');
await db.transaction(() async {
// alright, we stored / updated the client and have the account ID, time to import everything else!
// user_device_keys and user_device_keys_key
debugPrint('[Store] Migrating user device keys...');
final deviceKeysListString = oldKeys['${clientName}.user_device_keys'];
if (deviceKeysListString != null && deviceKeysListString.isNotEmpty) {
Map<String, dynamic> rawUserDeviceKeys =
json.decode(deviceKeysListString);
for (final entry in rawUserDeviceKeys.entries) {
final map = entry.value;
await db.storeUserDeviceKeysInfo(
clientId, map['user_id'], map['outdated']);
for (final rawKey in map['device_keys'].entries) {
final jsonVaue = rawKey.value;
await db.storeUserDeviceKey(
clientId,
jsonVaue['user_id'],
jsonVaue['device_id'],
json.encode(jsonVaue),
jsonVaue['verified'],
jsonVaue['blocked']);
}
}
}
for (final entry in oldKeys.entries) {
final key = entry.key;
final value = entry.value;
if (value == null || value.isEmpty) {
continue;
}
// olm_sessions
final olmSessionsMatch =
RegExp(r'^\/clients\/([^\/]+)\/olm-sessions$').firstMatch(key);
if (olmSessionsMatch != null) {
if (olmSessionsMatch[1] != credentials['deviceID']) {
continue;
}
debugPrint('[Store] migrating olm sessions...');
final identityKey = json.decode(value);
for (final olmKey in identityKey.entries) {
final identKey = olmKey.key;
final sessions = olmKey.value;
for (final pickle in sessions) {
var sess = olm.Session();
sess.unpickle(credentials['userID'], pickle);
await db.storeOlmSession(
clientId, identKey, sess.session_id(), pickle);
sess?.free();
}
}
}
// outbound_group_sessions
final outboundGroupSessionsMatch = RegExp(
r'^\/clients\/([^\/]+)\/rooms\/([^\/]+)\/outbound_group_session$')
.firstMatch(key);
if (outboundGroupSessionsMatch != null) {
if (outboundGroupSessionsMatch[1] != credentials['deviceID']) {
continue;
}
final pickle = value;
final roomId = outboundGroupSessionsMatch[2];
debugPrint(
'[Store] Migrating outbound group sessions for room ${roomId}...');
final devicesString = oldKeys[
'/clients/${outboundGroupSessionsMatch[1]}/rooms/${roomId}/outbound_group_session_devices'];
var devices = <String>[];
if (devicesString != null) {
devices = List<String>.from(json.decode(devicesString));
}
await db.storeOutboundGroupSession(
clientId, roomId, pickle, json.encode(devices));
}
// session_keys
final sessionKeysMatch =
RegExp(r'^\/clients\/([^\/]+)\/rooms\/([^\/]+)\/session_keys$')
.firstMatch(key);
if (sessionKeysMatch != null) {
if (sessionKeysMatch[1] != credentials['deviceID']) {
continue;
}
final roomId = sessionKeysMatch[2];
debugPrint('[Store] Migrating session keys for room ${roomId}...');
final map = json.decode(value);
for (final entry in map.entries) {
await db.storeInboundGroupSession(
clientId,
roomId,
entry.key,
entry.value['inboundGroupSession'],
json.encode(entry.value['content']),
json.encode(entry.value['indexes']));
}
}
}
});
}
class Store {
final LocalStorage storage;
final FlutterSecureStorage secureStorage;
Store(this.client)
Store()
: storage = LocalStorage('LocalStorage'),
secureStorage = kIsWeb ? null : FlutterSecureStorage() {
_init();
}
secureStorage = kIsWeb ? null : FlutterSecureStorage();
Future<dynamic> getItem(String key) async {
if (kIsWeb) {
@ -49,587 +210,19 @@ class Store extends StoreAPI {
}
}
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);
Future<Map<String, dynamic>> getAllItems() async {
if (kIsWeb) {
try {
final rawStorage = await getLocalstorage('LocalStorage');
return json.decode(rawStorage);
} catch (_) {
return {};
}
}
return userDeviceKeys;
}
Future<void> storeUserDeviceKeys(
Map<String, DeviceKeysList> userDeviceKeys) async {
await setItem(_UserDeviceKeysKey, json.encode(userDeviceKeys));
}
String get _UserDeviceKeysKey => "${client.clientName}.user_device_keys";
_init() async {
final credentialsStr = await getItem(client.clientName);
if (credentialsStr == null || credentialsStr.isEmpty) {
client.onLoginStateChanged.add(LoginState.loggedOut);
return;
try {
return await secureStorage.readAll();
} catch (_) {
return {};
}
debugPrint("[Matrix] Restoring account credentials");
final Map<String, dynamic> credentials = json.decode(credentialsStr);
if (credentials["homeserver"] == null ||
credentials["token"] == null ||
credentials["userID"] == null) {
client.onLoginStateChanged.add(LoginState.loggedOut);
return;
}
client.connect(
newDeviceID: credentials["deviceID"],
newDeviceName: credentials["deviceName"],
newHomeserver: credentials["homeserver"],
newMatrixVersions: List<String>.from(credentials["matrixVersions"] ?? []),
newToken: credentials["token"],
newUserID: credentials["userID"],
newPrevBatch: kIsWeb
? null
: (credentials["prev_batch"]?.isEmpty ?? true)
? null
: credentials["prev_batch"],
newOlmAccount: credentials["olmAccount"],
);
}
Future<void> storeClient() async {
final Map<String, dynamic> credentials = {
"deviceID": client.deviceID,
"deviceName": client.deviceName,
"homeserver": client.homeserver,
"matrixVersions": client.matrixVersions,
"token": client.accessToken,
"userID": client.userID,
"olmAccount": client.pickledOlmAccount,
};
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 {
/// The maximum time that files are allowed to stay in the
/// store. By default this is are 30 days.
static const int MAX_FILE_STORING_TIME = 1 * 30 * 24 * 60 * 60 * 1000;
@override
final bool extended = true;
ExtendedStore(Client client) : super(client);
Database _db;
var txn;
/// 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;
@override
_init() async {
// Open the database and migrate if necessary.
var databasePath = await getDatabasesPath();
String path = p.join(databasePath, "FluffyMatrix.db");
_db = await openDatabase(path, version: 20,
onCreate: (Database db, int version) async {
await createTables(db);
}, onUpgrade: (Database db, int oldVersion, int newVersion) async {
debugPrint(
"[Store] Migrate database from version $oldVersion to $newVersion");
if (oldVersion >= 18 && newVersion <= 20) {
await createTables(db);
} else if (oldVersion != newVersion) {
// 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]);
} catch (_) {
list = [];
}
client.prevBatch = null;
await this.storePrevBatch(null);
schemes.forEach((String name, String scheme) async {
await db.execute("DROP TABLE IF EXISTS $name");
});
await createTables(db);
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"],
newMatrixVersions:
clientList["matrix_versions"].toString().split(","),
newPrevBatch: null,
);
await db.execute("DROP TABLE IF EXISTS Clients");
debugPrint(
"[Store] Restore client credentials from deprecated database of ${client.userID}");
}
} else {
client.onLoginStateChanged.add(LoginState.loggedOut);
}
return;
});
// Mark all pending events as failed.
await _db.rawUpdate("UPDATE Events SET status=-1 WHERE status=0");
// Delete all stored files which are older than [MAX_FILE_STORING_TIME]
final int currentDeadline = DateTime.now().millisecondsSinceEpoch -
ExtendedStore.MAX_FILE_STORING_TIME;
await _db.rawDelete(
"DELETE From Files WHERE saved_at<?",
[currentDeadline],
);
super._init();
}
Future<void> setRoomPrevBatch(String roomId, String prevBatch) async {
await txn.rawUpdate(
"UPDATE Rooms SET prev_batch=? WHERE room_id=?", [roomId, prevBatch]);
return;
}
Future<void> createTables(Database db) async {
schemes.forEach((String name, String scheme) async {
await db.execute(scheme);
});
}
/// Clears all tables from the database.
Future<void> clear() async {
schemes.forEach((String name, String scheme) async {
await _db.rawDelete("DELETE FROM $name");
});
await super.clear();
return;
}
Future<void> transaction(Function queries) async {
return _db.transaction((txnObj) async {
txn = txnObj.batch();
queries();
await txn.commit(noResult: true);
});
}
/// 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));
}
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
if (roomUpdate.membership != Membership.leave) {
txn.rawInsert(
"INSERT OR IGNORE INTO Rooms " + "VALUES(?, ?, 0, 0, '', 0, 0, '') ",
[roomUpdate.id, roomUpdate.membership.toString().split('.').last]);
} else {
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;
if (userUpdate.type == "account_data") {
txn.rawInsert("INSERT OR REPLACE INTO AccountData VALUES(?, ?)", [
userUpdate.eventType,
json.encode(userUpdate.content["content"]),
]);
} else if (userUpdate.type == "presence") {
txn.rawInsert("INSERT OR REPLACE INTO Presences VALUES(?, ?, ?)", [
userUpdate.eventType,
userUpdate.content["sender"],
json.encode(userUpdate.content["content"]),
]);
}
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) {
if (txn == null || eventUpdate.type == "ephemeral") return null;
Map<String, dynamic> eventContent = eventUpdate.content;
String type = eventUpdate.type;
String chatId = eventUpdate.roomID;
// Get the state_key for m.room.member events
String stateKey = "";
if (eventContent["state_key"] is String) {
stateKey = eventContent["state_key"];
}
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> &&
eventContent["unsigned"]["transaction_id"] is String) {
txn.rawUpdate(
"UPDATE Events SET status=?, event_id=? WHERE event_id=?", [
status,
eventContent["event_id"],
eventContent["unsigned"]["transaction_id"]
]);
} else {
txn.rawInsert(
"INSERT OR REPLACE INTO Events VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
[
eventContent["event_id"],
chatId,
eventContent["origin_server_ts"],
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) {
txn.rawDelete("DELETE FROM Events WHERE event_id=?",
[eventUpdate.content["unsigned"]["transaction_id"]]);
}
}
if (type == "history") return null;
if (type != "account_data") {
final String now = DateTime.now().millisecondsSinceEpoch.toString();
txn.rawInsert(
"INSERT OR REPLACE INTO RoomStates VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)",
[
eventContent["event_id"] ?? now,
chatId,
eventContent["origin_server_ts"] ?? now,
eventContent["sender"],
stateKey,
json.encode(eventContent["unsigned"] ?? ""),
json.encode(eventContent["prev_content"] ?? ""),
eventContent["type"],
json.encode(eventContent["content"]),
]);
} else if (type == "account_data") {
txn.rawInsert("INSERT OR REPLACE INTO RoomAccountData VALUES(?, ?, ?)", [
eventContent["type"],
chatId,
json.encode(eventContent["content"]),
]);
}
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;
return Event.fromJson(res[0], room).asUser;
}
/// 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 = [];
for (num i = 0; i < eventRes.length; i++) {
eventList.add(Event.fromJson(eventRes[i], room));
}
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<Map<String, dynamic>> resStates = await _db.rawQuery("SELECT * FROM RoomStates WHERE type IS NOT NULL");
List<Map<String, dynamic>> resAccountData = await _db.rawQuery("SELECT * FROM RoomAccountData");
List<Room> roomList = [];
for (num i = 0; i < res.length; i++) {
Room room = await Room.getRoomFromTableRow(
res[i],
client,
states: Future.value(resStates.where((r) => r["room_id"] == res[i]["room_id"]).toList()),
roomAccountData: Future.value(resAccountData.where((r) => r["room_id"] == res[i]["room_id"]).toList()),
);
roomList.add(room);
}
return roomList;
}
Future<List<Map<String, dynamic>>> getStatesFromRoomId(String id) async {
return _db.rawQuery(
"SELECT * FROM RoomStates WHERE room_id=? AND type IS NOT NULL", [id]);
}
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]);
await _db.rawDelete("DELETE FROM Events WHERE room_id=?", [roomID]);
await _db.rawDelete("DELETE FROM RoomStates WHERE room_id=?", [roomID]);
await _db
.rawDelete("DELETE FROM RoomAccountData 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]);
if (res.isEmpty) return null;
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");
for (int i = 0; i < rawAccountData.length; i++) {
newAccountData[rawAccountData[i]["type"]] =
AccountData.fromJson(rawAccountData[i]);
}
return newAccountData;
}
Future<Map<String, Presence>> getPresences() async {
Map<String, Presence> newPresences = {};
List<Map<String, dynamic>> rawPresences =
await _db.rawQuery("SELECT * FROM Presences");
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]["sender"]] = Presence.fromJson(rawPresence);
}
return newPresences;
}
Future removeEvent(String eventId) async {
assert(eventId != "");
await _db.rawDelete("DELETE FROM Events WHERE event_id=?", [eventId]);
return;
}
Future<void> storeFile(Uint8List bytes, String mxcUri) async {
await _db.rawInsert(
"INSERT OR REPLACE INTO Files VALUES(?, ?, ?)",
[mxcUri, bytes, DateTime.now().millisecondsSinceEpoch],
);
return;
}
Future<Uint8List> getFile(String mxcUri) async {
List<Map<String, dynamic>> res = await _db.rawQuery(
"SELECT * FROM Files WHERE mxc_uri=?",
[mxcUri],
);
if (res.isEmpty) return null;
return res.first["bytes"];
}
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(' +
'type TEXT, ' +
'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))',
/// The database scheme for room states.
'Files': 'CREATE TABLE IF NOT EXISTS Files(' +
'mxc_uri TEXT PRIMARY KEY, ' +
'bytes BLOB, ' +
'saved_at INTEGER, ' +
'UNIQUE(mxc_uri))',
};
@override
int get maxFileSize => 1 * 1024 * 1024;
}

View file

@ -15,9 +15,9 @@ import 'package:famedlysdk/famedlysdk.dart';
import 'famedlysdk_store.dart';
abstract class FirebaseController {
static FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
static FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
static final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
static final FlutterLocalNotificationsPlugin
_flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
static BuildContext context;
static const String CHANNEL_ID = 'fluffychat_push';
static const String CHANNEL_NAME = 'FluffyChat push channel';
@ -52,7 +52,7 @@ abstract class FirebaseController {
currentPushers.first.lang == 'en' &&
currentPushers.first.data.url == GATEWAY_URL &&
currentPushers.first.data.format == PUSHER_FORMAT) {
debugPrint("[Push] Pusher already set");
debugPrint('[Push] Pusher already set');
} else {
if (currentPushers.isNotEmpty) {
for (final currentPusher in currentPushers) {
@ -66,16 +66,16 @@ abstract class FirebaseController {
currentPusher.data.url,
append: true,
);
debugPrint("[Push] Remove legacy pusher for this device");
debugPrint('[Push] Remove legacy pusher for this device');
}
}
await client.setPushers(
token,
"http",
'http',
APP_ID,
clientName,
client.deviceName,
"en",
'en',
GATEWAY_URL,
append: false,
format: PUSHER_FORMAT,
@ -88,9 +88,9 @@ abstract class FirebaseController {
if (message is String) {
roomId = message;
} else if (message is Map) {
roomId = (message["data"] ?? message)["room_id"];
roomId = (message['data'] ?? message)['room_id'];
}
if (roomId?.isEmpty ?? true) throw ("Bad roomId");
if (roomId?.isEmpty ?? true) throw ('Bad roomId');
await Navigator.of(context).pushAndRemoveUntil(
AppRoute.defaultRoute(
context,
@ -98,7 +98,7 @@ abstract class FirebaseController {
),
(r) => r.isFirst);
} catch (_) {
BotToast.showText(text: "Failed to open chat...");
BotToast.showText(text: 'Failed to open chat...');
debugPrint(_);
}
};
@ -121,16 +121,16 @@ abstract class FirebaseController {
onResume: goToRoom,
onLaunch: goToRoom,
);
debugPrint("[Push] Firebase initialized");
debugPrint('[Push] Firebase initialized');
return;
}
static Future<dynamic> _onMessage(Map<String, dynamic> message) async {
try {
final data = message['data'] ?? message;
final String roomId = data["room_id"];
final String eventId = data["event_id"];
final int unread = json.decode(data["counts"])["unread"];
final String roomId = data['room_id'];
final String eventId = data['event_id'];
final int unread = json.decode(data['counts'])['unread'];
if ((roomId?.isEmpty ?? true) ||
(eventId?.isEmpty ?? true) ||
unread == 0) {
@ -148,10 +148,12 @@ abstract class FirebaseController {
if (context != null) {
client = Matrix.of(context).client;
} else {
final platform = kIsWeb ? "Web" : Platform.operatingSystem;
final clientName = "FluffyChat $platform";
final platform = kIsWeb ? 'Web' : Platform.operatingSystem;
final clientName = 'FluffyChat $platform';
client = Client(clientName, debug: false);
client.storeAPI = ExtendedStore(client);
final store = Store();
client.database = await getDatabase(client, store);
client.connect();
await client.onLoginStateChanged.stream
.firstWhere((l) => l == LoginState.logged)
.timeout(
@ -160,7 +162,7 @@ abstract class FirebaseController {
}
// Get the room
Room room = client.getRoomById(roomId);
var room = client.getRoomById(roomId);
if (room == null) {
await client.onRoomUpdate.stream
.where((u) => u.id == roomId)
@ -171,10 +173,10 @@ abstract class FirebaseController {
}
// Get the event
Event event = await client.store.getEventById(eventId, room);
var event = await client.database.getEventById(client.id, eventId, room);
if (event == null) {
final EventUpdate eventUpdate = await client.onEvent.stream
.where((u) => u.content["event_id"] == eventId)
final eventUpdate = await client.onEvent.stream
.where((u) => u.content['event_id'] == eventId)
.first
.timeout(Duration(seconds: 5));
event = Event.fromJson(eventUpdate.content, room);
@ -182,18 +184,18 @@ abstract class FirebaseController {
}
// Count all unread events
int unreadEvents = 0;
var unreadEvents = 0;
client.rooms
.forEach((Room room) => unreadEvents += room.notificationCount);
// Calculate title
final String title = unread > 1
final title = unread > 1
? i18n.unreadMessagesInChats(
unreadEvents.toString(), unread.toString())
: i18n.unreadMessages(unreadEvents.toString());
// Calculate the body
final String body = event.getLocalizedBody(
final body = event.getLocalizedBody(
i18n,
withSenderNamePrefix: true,
hideReply: true,
@ -238,7 +240,7 @@ abstract class FirebaseController {
0, room.getLocalizedDisplayname(i18n), body, platformChannelSpecifics,
payload: roomId);
} catch (exception) {
debugPrint("[Push] Error while processing notification: " +
debugPrint('[Push] Error while processing notification: ' +
exception.toString());
await _showDefaultNotification(message);
}
@ -248,8 +250,7 @@ abstract class FirebaseController {
static Future<dynamic> _showDefaultNotification(
Map<String, dynamic> message) async {
try {
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
var flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
// Init notifications framework
var initializationSettingsAndroid =
AndroidInitializationSettings('notifications_icon');
@ -261,10 +262,10 @@ abstract class FirebaseController {
// Notification data and matrix data
Map<dynamic, dynamic> data = message['data'] ?? message;
String eventID = data["event_id"];
String roomID = data["room_id"];
final int unread = data.containsKey("counts")
? json.decode(data["counts"])["unread"]
String eventID = data['event_id'];
String roomID = data['room_id'];
final int unread = data.containsKey('counts')
? json.decode(data['counts'])['unread']
: 1;
await flutterLocalNotificationsPlugin.cancelAll();
if (unread == 0 || roomID == null || eventID == null) {
@ -278,12 +279,12 @@ abstract class FirebaseController {
var iOSPlatformChannelSpecifics = IOSNotificationDetails();
var platformChannelSpecifics = NotificationDetails(
androidPlatformChannelSpecifics, iOSPlatformChannelSpecifics);
final String title = l10n.unreadChats(unread.toString());
final title = l10n.unreadChats(unread.toString());
await flutterLocalNotificationsPlugin.show(
1, title, l10n.openAppToReadMessages, platformChannelSpecifics,
payload: roomID);
} catch (exception) {
debugPrint("[Push] Error while processing background notification: " +
debugPrint('[Push] Error while processing background notification: ' +
exception.toString());
}
return Future<void>.value();
@ -291,10 +292,10 @@ abstract class FirebaseController {
static Future<String> downloadAndSaveAvatar(Uri content, Client client,
{int width, int height}) async {
final bool thumbnail = width == null && height == null ? false : true;
final String tempDirectory = (await getTemporaryDirectory()).path;
final String prefix = thumbnail ? "thumbnail" : "";
File file =
final thumbnail = width == null && height == null ? false : true;
final tempDirectory = (await getTemporaryDirectory()).path;
final prefix = thumbnail ? 'thumbnail' : '';
var file =
File('$tempDirectory/${prefix}_${content.toString().split("/").last}');
if (!file.existsSync()) {
@ -315,7 +316,7 @@ abstract class FirebaseController {
IosNotificationSettings(sound: true, badge: true, alert: true));
_firebaseMessaging.onIosSettingsRegistered
.listen((IosNotificationSettings settings) {
debugPrint("Settings registered: $settings");
debugPrint('Settings registered: $settings');
});
}
}

View file

@ -15,8 +15,8 @@ extension MatrixFileExtension on MatrixFile {
var element = html.document.createElement('a');
element.setAttribute(
'href', html.Url.createObjectUrlFromBlob(html.Blob([bytes])));
element.setAttribute('target', "_blank");
element.setAttribute('rel', "noopener");
element.setAttribute('target', '_blank');
element.setAttribute('rel', 'noopener');
element.setAttribute('download', fileName);
element.setAttribute('type', mimeType);
element.style.display = 'none';
@ -24,8 +24,8 @@ extension MatrixFileExtension on MatrixFile {
element.click();
element.remove();
} else {
Directory tempDir = await getTemporaryDirectory();
final file = File(tempDir.path + "/" + path.split("/").last);
var tempDir = await getTemporaryDirectory();
final file = File(tempDir.path + '/' + path.split('/').last);
file.writeAsBytesSync(bytes);
await OpenFile.open(file.path);
}

View file

@ -2,9 +2,9 @@ import 'package:flutter/material.dart';
extension StringColor on String {
Color get color {
double number = 0.0;
for (var i = 0; i < this.length; i++) {
number += this.codeUnitAt(i);
var number = 0.0;
for (var i = 0; i < length; i++) {
number += codeUnitAt(i);
}
number = (number % 10) * 25.5;
return HSLColor.fromAHSL(1, number, 1, 0.35).toColor();

View file

@ -12,7 +12,7 @@ class UrlLauncher {
const UrlLauncher(this.context, this.url);
void launchUrl() {
if (url.startsWith("https://matrix.to/#/")) {
if (url.startsWith('https://matrix.to/#/')) {
return openMatrixToUrl();
}
launch(url);
@ -20,8 +20,8 @@ class UrlLauncher {
void openMatrixToUrl() async {
final matrix = Matrix.of(context);
final String identifier = url.replaceAll("https://matrix.to/#/", "");
if (identifier.substring(0, 1) == "#") {
final identifier = url.replaceAll('https://matrix.to/#/', '');
if (identifier.substring(0, 1) == '#') {
final response = await SimpleDialogs(context).tryRequestWithLoadingDialog(
matrix.client.joinRoomById(
Uri.encodeComponent(identifier),
@ -30,13 +30,13 @@ class UrlLauncher {
if (response == false) return;
await Navigator.pushAndRemoveUntil(
context,
AppRoute.defaultRoute(context, ChatView(response["room_id"])),
AppRoute.defaultRoute(context, ChatView(response['room_id'])),
(r) => r.isFirst,
);
} else if (identifier.substring(0, 1) == "@") {
final User user = User(
} else if (identifier.substring(0, 1) == '@') {
final user = User(
identifier,
room: Room(id: "", client: matrix.client),
room: Room(id: '', client: matrix.client),
);
final String roomID = await SimpleDialogs(context)
.tryRequestWithLoadingDialog(user.startDirectChat());