diff --git a/lib/main.dart b/lib/main.dart index 7717a24..4fa6a1b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,7 +8,6 @@ import 'package:twonly/src/services/api/media_send.dart'; import 'package:twonly/src/services/api.service.dart'; import 'package:flutter/material.dart'; import 'package:twonly/src/providers/connection.provider.dart'; -import 'package:twonly/src/utils/hive.dart'; import 'package:twonly/src/providers/settings.provider.dart'; import 'package:twonly/src/services/fcm.service.dart'; import 'package:twonly/src/services/notification.service.dart'; @@ -33,9 +32,9 @@ void main() async { // Load the user's preferred theme while the splash screen is displayed. // This prevents a sudden theme change when the app is first displayed. await settingsController.loadSettings(); + await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); - await setupPushNotification(); - await initMediaStorage(); + setupPushNotification(); gCameras = await availableCameras(); @@ -48,8 +47,6 @@ void main() async { purgeReceivedMediaFiles(); purgeSendMediaFiles(); - await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); - runApp( MultiProvider( providers: [ diff --git a/lib/src/database/daos/message_retransmissions.dao.dart b/lib/src/database/daos/message_retransmissions.dao.dart new file mode 100644 index 0000000..f1f19a4 --- /dev/null +++ b/lib/src/database/daos/message_retransmissions.dao.dart @@ -0,0 +1,44 @@ +import 'package:drift/drift.dart'; +import 'package:twonly/src/database/tables/message_retransmissions.dart'; +import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/utils/log.dart'; + +part 'message_retransmissions.dao.g.dart'; + +@DriftAccessor(tables: [MessageRetransmissions]) +class MessageRetransmissionDao extends DatabaseAccessor + with _$MessageRetransmissionDaoMixin { + // this constructor is required so that the main database can create an instance + // of this object. + MessageRetransmissionDao(super.db); + + Future insertRetransmission( + MessageRetransmissionsCompanion message) async { + try { + return await into(messageRetransmissions).insert(message); + } catch (e) { + Log.error("Error while inserting message for retransmission: $e"); + return null; + } + } + + Future> getRetransmitAbleMessages() async { + return (await (select(messageRetransmissions) + ..where((t) => t.acknowledgeByServerAt.isNull())) + .get()) + .map((msg) => msg.retransmissionId) + .toList(); + } + + SingleOrNullSelectable getRetransmissionById( + int retransmissionId) { + return select(messageRetransmissions) + ..where((t) => t.retransmissionId.equals(retransmissionId)); + } + + Future deleteRetransmissionById(int retransmissionId) { + return (delete(messageRetransmissions) + ..where((t) => t.retransmissionId.equals(retransmissionId))) + .go(); + } +} diff --git a/lib/src/database/daos/message_retransmissions.dao.g.dart b/lib/src/database/daos/message_retransmissions.dao.g.dart new file mode 100644 index 0000000..cd7ca29 --- /dev/null +++ b/lib/src/database/daos/message_retransmissions.dao.g.dart @@ -0,0 +1,11 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'message_retransmissions.dao.dart'; + +// ignore_for_file: type=lint +mixin _$MessageRetransmissionDaoMixin on DatabaseAccessor { + $ContactsTable get contacts => attachedDatabase.contacts; + $MessagesTable get messages => attachedDatabase.messages; + $MessageRetransmissionsTable get messageRetransmissions => + attachedDatabase.messageRetransmissions; +} diff --git a/lib/src/database/tables/message_retransmissions.dart b/lib/src/database/tables/message_retransmissions.dart new file mode 100644 index 0000000..df745ad --- /dev/null +++ b/lib/src/database/tables/message_retransmissions.dart @@ -0,0 +1,19 @@ +import 'package:drift/drift.dart'; +import 'package:twonly/src/database/tables/contacts_table.dart'; +import 'package:twonly/src/database/tables/messages_table.dart'; + +@DataClassName('MessageRetransmission') +class MessageRetransmissions extends Table { + IntColumn get retransmissionId => integer().autoIncrement()(); + IntColumn get contactId => + integer().references(Contacts, #userId, onDelete: KeyAction.cascade)(); + + IntColumn get messageId => integer() + .nullable() + .references(Messages, #messageId, onDelete: KeyAction.cascade)(); + + BlobColumn get plaintextContent => blob()(); + BlobColumn get pushData => blob().nullable()(); + + DateTimeColumn get acknowledgeByServerAt => dateTime().nullable()(); +} diff --git a/lib/src/database/twonly_database.dart b/lib/src/database/twonly_database.dart index 48fe7bb..36a6759 100644 --- a/lib/src/database/twonly_database.dart +++ b/lib/src/database/twonly_database.dart @@ -5,11 +5,13 @@ import 'package:path_provider/path_provider.dart'; import 'package:twonly/src/database/daos/contacts_dao.dart'; import 'package:twonly/src/database/daos/media_downloads_dao.dart'; import 'package:twonly/src/database/daos/media_uploads_dao.dart'; +import 'package:twonly/src/database/daos/message_retransmissions.dao.dart'; import 'package:twonly/src/database/daos/messages_dao.dart'; import 'package:twonly/src/database/daos/signal_dao.dart'; import 'package:twonly/src/database/tables/contacts_table.dart'; import 'package:twonly/src/database/tables/media_download_table.dart'; import 'package:twonly/src/database/tables/media_uploads_table.dart'; +import 'package:twonly/src/database/tables/message_retransmissions.dart'; import 'package:twonly/src/database/tables/messages_table.dart'; import 'package:twonly/src/database/tables/signal_contact_prekey_table.dart'; import 'package:twonly/src/database/tables/signal_contact_signed_prekey_table.dart'; @@ -32,13 +34,15 @@ part 'twonly_database.g.dart'; SignalSenderKeyStores, SignalSessionStores, SignalContactPreKeys, - SignalContactSignedPreKeys + SignalContactSignedPreKeys, + MessageRetransmissions ], daos: [ MessagesDao, ContactsDao, MediaUploadsDao, MediaDownloadsDao, - SignalDao + SignalDao, + MessageRetransmissionDao ]) class TwonlyDatabase extends _$TwonlyDatabase { TwonlyDatabase([QueryExecutor? e]) diff --git a/lib/src/database/twonly_database.g.dart b/lib/src/database/twonly_database.g.dart index a46fa45..7a74aa7 100644 --- a/lib/src/database/twonly_database.g.dart +++ b/lib/src/database/twonly_database.g.dart @@ -4179,6 +4179,392 @@ class SignalContactSignedPreKeysCompanion } } +class $MessageRetransmissionsTable extends MessageRetransmissions + with TableInfo<$MessageRetransmissionsTable, MessageRetransmission> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $MessageRetransmissionsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _retransmissionIdMeta = + const VerificationMeta('retransmissionId'); + @override + late final GeneratedColumn retransmissionId = GeneratedColumn( + 'retransmission_id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + static const VerificationMeta _contactIdMeta = + const VerificationMeta('contactId'); + @override + late final GeneratedColumn contactId = GeneratedColumn( + 'contact_id', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES contacts (user_id) ON DELETE CASCADE')); + static const VerificationMeta _messageIdMeta = + const VerificationMeta('messageId'); + @override + late final GeneratedColumn messageId = GeneratedColumn( + 'message_id', aliasedName, true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES messages (message_id) ON DELETE CASCADE')); + static const VerificationMeta _plaintextContentMeta = + const VerificationMeta('plaintextContent'); + @override + late final GeneratedColumn plaintextContent = + GeneratedColumn('plaintext_content', aliasedName, false, + type: DriftSqlType.blob, requiredDuringInsert: true); + static const VerificationMeta _pushDataMeta = + const VerificationMeta('pushData'); + @override + late final GeneratedColumn pushData = GeneratedColumn( + 'push_data', aliasedName, true, + type: DriftSqlType.blob, requiredDuringInsert: false); + static const VerificationMeta _acknowledgeByServerAtMeta = + const VerificationMeta('acknowledgeByServerAt'); + @override + late final GeneratedColumn acknowledgeByServerAt = + GeneratedColumn('acknowledge_by_server_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + @override + List get $columns => [ + retransmissionId, + contactId, + messageId, + plaintextContent, + pushData, + acknowledgeByServerAt + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'message_retransmissions'; + @override + VerificationContext validateIntegrity( + Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('retransmission_id')) { + context.handle( + _retransmissionIdMeta, + retransmissionId.isAcceptableOrUnknown( + data['retransmission_id']!, _retransmissionIdMeta)); + } + if (data.containsKey('contact_id')) { + context.handle(_contactIdMeta, + contactId.isAcceptableOrUnknown(data['contact_id']!, _contactIdMeta)); + } else if (isInserting) { + context.missing(_contactIdMeta); + } + if (data.containsKey('message_id')) { + context.handle(_messageIdMeta, + messageId.isAcceptableOrUnknown(data['message_id']!, _messageIdMeta)); + } + if (data.containsKey('plaintext_content')) { + context.handle( + _plaintextContentMeta, + plaintextContent.isAcceptableOrUnknown( + data['plaintext_content']!, _plaintextContentMeta)); + } else if (isInserting) { + context.missing(_plaintextContentMeta); + } + if (data.containsKey('push_data')) { + context.handle(_pushDataMeta, + pushData.isAcceptableOrUnknown(data['push_data']!, _pushDataMeta)); + } + if (data.containsKey('acknowledge_by_server_at')) { + context.handle( + _acknowledgeByServerAtMeta, + acknowledgeByServerAt.isAcceptableOrUnknown( + data['acknowledge_by_server_at']!, _acknowledgeByServerAtMeta)); + } + return context; + } + + @override + Set get $primaryKey => {retransmissionId}; + @override + MessageRetransmission map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MessageRetransmission( + retransmissionId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}retransmission_id'])!, + contactId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}contact_id'])!, + messageId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}message_id']), + plaintextContent: attachedDatabase.typeMapping.read( + DriftSqlType.blob, data['${effectivePrefix}plaintext_content'])!, + pushData: attachedDatabase.typeMapping + .read(DriftSqlType.blob, data['${effectivePrefix}push_data']), + acknowledgeByServerAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}acknowledge_by_server_at']), + ); + } + + @override + $MessageRetransmissionsTable createAlias(String alias) { + return $MessageRetransmissionsTable(attachedDatabase, alias); + } +} + +class MessageRetransmission extends DataClass + implements Insertable { + final int retransmissionId; + final int contactId; + final int? messageId; + final Uint8List plaintextContent; + final Uint8List? pushData; + final DateTime? acknowledgeByServerAt; + const MessageRetransmission( + {required this.retransmissionId, + required this.contactId, + this.messageId, + required this.plaintextContent, + this.pushData, + this.acknowledgeByServerAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['retransmission_id'] = Variable(retransmissionId); + map['contact_id'] = Variable(contactId); + if (!nullToAbsent || messageId != null) { + map['message_id'] = Variable(messageId); + } + map['plaintext_content'] = Variable(plaintextContent); + if (!nullToAbsent || pushData != null) { + map['push_data'] = Variable(pushData); + } + if (!nullToAbsent || acknowledgeByServerAt != null) { + map['acknowledge_by_server_at'] = + Variable(acknowledgeByServerAt); + } + return map; + } + + MessageRetransmissionsCompanion toCompanion(bool nullToAbsent) { + return MessageRetransmissionsCompanion( + retransmissionId: Value(retransmissionId), + contactId: Value(contactId), + messageId: messageId == null && nullToAbsent + ? const Value.absent() + : Value(messageId), + plaintextContent: Value(plaintextContent), + pushData: pushData == null && nullToAbsent + ? const Value.absent() + : Value(pushData), + acknowledgeByServerAt: acknowledgeByServerAt == null && nullToAbsent + ? const Value.absent() + : Value(acknowledgeByServerAt), + ); + } + + factory MessageRetransmission.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MessageRetransmission( + retransmissionId: serializer.fromJson(json['retransmissionId']), + contactId: serializer.fromJson(json['contactId']), + messageId: serializer.fromJson(json['messageId']), + plaintextContent: + serializer.fromJson(json['plaintextContent']), + pushData: serializer.fromJson(json['pushData']), + acknowledgeByServerAt: + serializer.fromJson(json['acknowledgeByServerAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'retransmissionId': serializer.toJson(retransmissionId), + 'contactId': serializer.toJson(contactId), + 'messageId': serializer.toJson(messageId), + 'plaintextContent': serializer.toJson(plaintextContent), + 'pushData': serializer.toJson(pushData), + 'acknowledgeByServerAt': + serializer.toJson(acknowledgeByServerAt), + }; + } + + MessageRetransmission copyWith( + {int? retransmissionId, + int? contactId, + Value messageId = const Value.absent(), + Uint8List? plaintextContent, + Value pushData = const Value.absent(), + Value acknowledgeByServerAt = const Value.absent()}) => + MessageRetransmission( + retransmissionId: retransmissionId ?? this.retransmissionId, + contactId: contactId ?? this.contactId, + messageId: messageId.present ? messageId.value : this.messageId, + plaintextContent: plaintextContent ?? this.plaintextContent, + pushData: pushData.present ? pushData.value : this.pushData, + acknowledgeByServerAt: acknowledgeByServerAt.present + ? acknowledgeByServerAt.value + : this.acknowledgeByServerAt, + ); + MessageRetransmission copyWithCompanion( + MessageRetransmissionsCompanion data) { + return MessageRetransmission( + retransmissionId: data.retransmissionId.present + ? data.retransmissionId.value + : this.retransmissionId, + contactId: data.contactId.present ? data.contactId.value : this.contactId, + messageId: data.messageId.present ? data.messageId.value : this.messageId, + plaintextContent: data.plaintextContent.present + ? data.plaintextContent.value + : this.plaintextContent, + pushData: data.pushData.present ? data.pushData.value : this.pushData, + acknowledgeByServerAt: data.acknowledgeByServerAt.present + ? data.acknowledgeByServerAt.value + : this.acknowledgeByServerAt, + ); + } + + @override + String toString() { + return (StringBuffer('MessageRetransmission(') + ..write('retransmissionId: $retransmissionId, ') + ..write('contactId: $contactId, ') + ..write('messageId: $messageId, ') + ..write('plaintextContent: $plaintextContent, ') + ..write('pushData: $pushData, ') + ..write('acknowledgeByServerAt: $acknowledgeByServerAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + retransmissionId, + contactId, + messageId, + $driftBlobEquality.hash(plaintextContent), + $driftBlobEquality.hash(pushData), + acknowledgeByServerAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MessageRetransmission && + other.retransmissionId == this.retransmissionId && + other.contactId == this.contactId && + other.messageId == this.messageId && + $driftBlobEquality.equals( + other.plaintextContent, this.plaintextContent) && + $driftBlobEquality.equals(other.pushData, this.pushData) && + other.acknowledgeByServerAt == this.acknowledgeByServerAt); +} + +class MessageRetransmissionsCompanion + extends UpdateCompanion { + final Value retransmissionId; + final Value contactId; + final Value messageId; + final Value plaintextContent; + final Value pushData; + final Value acknowledgeByServerAt; + const MessageRetransmissionsCompanion({ + this.retransmissionId = const Value.absent(), + this.contactId = const Value.absent(), + this.messageId = const Value.absent(), + this.plaintextContent = const Value.absent(), + this.pushData = const Value.absent(), + this.acknowledgeByServerAt = const Value.absent(), + }); + MessageRetransmissionsCompanion.insert({ + this.retransmissionId = const Value.absent(), + required int contactId, + this.messageId = const Value.absent(), + required Uint8List plaintextContent, + this.pushData = const Value.absent(), + this.acknowledgeByServerAt = const Value.absent(), + }) : contactId = Value(contactId), + plaintextContent = Value(plaintextContent); + static Insertable custom({ + Expression? retransmissionId, + Expression? contactId, + Expression? messageId, + Expression? plaintextContent, + Expression? pushData, + Expression? acknowledgeByServerAt, + }) { + return RawValuesInsertable({ + if (retransmissionId != null) 'retransmission_id': retransmissionId, + if (contactId != null) 'contact_id': contactId, + if (messageId != null) 'message_id': messageId, + if (plaintextContent != null) 'plaintext_content': plaintextContent, + if (pushData != null) 'push_data': pushData, + if (acknowledgeByServerAt != null) + 'acknowledge_by_server_at': acknowledgeByServerAt, + }); + } + + MessageRetransmissionsCompanion copyWith( + {Value? retransmissionId, + Value? contactId, + Value? messageId, + Value? plaintextContent, + Value? pushData, + Value? acknowledgeByServerAt}) { + return MessageRetransmissionsCompanion( + retransmissionId: retransmissionId ?? this.retransmissionId, + contactId: contactId ?? this.contactId, + messageId: messageId ?? this.messageId, + plaintextContent: plaintextContent ?? this.plaintextContent, + pushData: pushData ?? this.pushData, + acknowledgeByServerAt: + acknowledgeByServerAt ?? this.acknowledgeByServerAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (retransmissionId.present) { + map['retransmission_id'] = Variable(retransmissionId.value); + } + if (contactId.present) { + map['contact_id'] = Variable(contactId.value); + } + if (messageId.present) { + map['message_id'] = Variable(messageId.value); + } + if (plaintextContent.present) { + map['plaintext_content'] = Variable(plaintextContent.value); + } + if (pushData.present) { + map['push_data'] = Variable(pushData.value); + } + if (acknowledgeByServerAt.present) { + map['acknowledge_by_server_at'] = + Variable(acknowledgeByServerAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MessageRetransmissionsCompanion(') + ..write('retransmissionId: $retransmissionId, ') + ..write('contactId: $contactId, ') + ..write('messageId: $messageId, ') + ..write('plaintextContent: $plaintextContent, ') + ..write('pushData: $pushData, ') + ..write('acknowledgeByServerAt: $acknowledgeByServerAt') + ..write(')')) + .toString(); + } +} + abstract class _$TwonlyDatabase extends GeneratedDatabase { _$TwonlyDatabase(QueryExecutor e) : super(e); $TwonlyDatabaseManager get managers => $TwonlyDatabaseManager(this); @@ -4198,6 +4584,8 @@ abstract class _$TwonlyDatabase extends GeneratedDatabase { $SignalContactPreKeysTable(this); late final $SignalContactSignedPreKeysTable signalContactSignedPreKeys = $SignalContactSignedPreKeysTable(this); + late final $MessageRetransmissionsTable messageRetransmissions = + $MessageRetransmissionsTable(this); late final MessagesDao messagesDao = MessagesDao(this as TwonlyDatabase); late final ContactsDao contactsDao = ContactsDao(this as TwonlyDatabase); late final MediaUploadsDao mediaUploadsDao = @@ -4205,6 +4593,8 @@ abstract class _$TwonlyDatabase extends GeneratedDatabase { late final MediaDownloadsDao mediaDownloadsDao = MediaDownloadsDao(this as TwonlyDatabase); late final SignalDao signalDao = SignalDao(this as TwonlyDatabase); + late final MessageRetransmissionDao messageRetransmissionDao = + MessageRetransmissionDao(this as TwonlyDatabase); @override Iterable> get allTables => allSchemaEntities.whereType>(); @@ -4219,8 +4609,28 @@ abstract class _$TwonlyDatabase extends GeneratedDatabase { signalSenderKeyStores, signalSessionStores, signalContactPreKeys, - signalContactSignedPreKeys + signalContactSignedPreKeys, + messageRetransmissions ]; + @override + StreamQueryUpdateRules get streamUpdateRules => const StreamQueryUpdateRules( + [ + WritePropagation( + on: TableUpdateQuery.onTableName('contacts', + limitUpdateKind: UpdateKind.delete), + result: [ + TableUpdate('message_retransmissions', kind: UpdateKind.delete), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName('messages', + limitUpdateKind: UpdateKind.delete), + result: [ + TableUpdate('message_retransmissions', kind: UpdateKind.delete), + ], + ), + ], + ); } typedef $$ContactsTableCreateCompanionBuilder = ContactsCompanion Function({ @@ -4292,6 +4702,26 @@ final class $$ContactsTableReferences return ProcessedTableManager( manager.$state.copyWith(prefetchedData: cache)); } + + static MultiTypedResultKey<$MessageRetransmissionsTable, + List> _messageRetransmissionsRefsTable( + _$TwonlyDatabase db) => + MultiTypedResultKey.fromTable(db.messageRetransmissions, + aliasName: $_aliasNameGenerator( + db.contacts.userId, db.messageRetransmissions.contactId)); + + $$MessageRetransmissionsTableProcessedTableManager + get messageRetransmissionsRefs { + final manager = $$MessageRetransmissionsTableTableManager( + $_db, $_db.messageRetransmissions) + .filter( + (f) => f.contactId.userId.sqlEquals($_itemColumn('user_id')!)); + + final cache = + $_typedResult.readTableOrNull(_messageRetransmissionsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache)); + } } class $$ContactsTableFilterComposer @@ -4400,6 +4830,29 @@ class $$ContactsTableFilterComposer )); return f(composer); } + + Expression messageRetransmissionsRefs( + Expression Function($$MessageRetransmissionsTableFilterComposer f) + f) { + final $$MessageRetransmissionsTableFilterComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: $db.messageRetransmissions, + getReferencedColumn: (t) => t.contactId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessageRetransmissionsTableFilterComposer( + $db: $db, + $table: $db.messageRetransmissions, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } } class $$ContactsTableOrderingComposer @@ -4589,6 +5042,29 @@ class $$ContactsTableAnnotationComposer )); return f(composer); } + + Expression messageRetransmissionsRefs( + Expression Function($$MessageRetransmissionsTableAnnotationComposer a) + f) { + final $$MessageRetransmissionsTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: $db.messageRetransmissions, + getReferencedColumn: (t) => t.contactId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessageRetransmissionsTableAnnotationComposer( + $db: $db, + $table: $db.messageRetransmissions, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } } class $$ContactsTableTableManager extends RootTableManager< @@ -4602,7 +5078,8 @@ class $$ContactsTableTableManager extends RootTableManager< $$ContactsTableUpdateCompanionBuilder, (Contact, $$ContactsTableReferences), Contact, - PrefetchHooks Function({bool messagesRefs})> { + PrefetchHooks Function( + {bool messagesRefs, bool messageRetransmissionsRefs})> { $$ContactsTableTableManager(_$TwonlyDatabase db, $ContactsTable table) : super(TableManagerState( db: db, @@ -4717,10 +5194,14 @@ class $$ContactsTableTableManager extends RootTableManager< .map((e) => (e.readTable(table), $$ContactsTableReferences(db, table, e))) .toList(), - prefetchHooksCallback: ({messagesRefs = false}) { + prefetchHooksCallback: ( + {messagesRefs = false, messageRetransmissionsRefs = false}) { return PrefetchHooks( db: db, - explicitlyWatchedTables: [if (messagesRefs) db.messages], + explicitlyWatchedTables: [ + if (messagesRefs) db.messages, + if (messageRetransmissionsRefs) db.messageRetransmissions + ], addJoins: null, getPrefetchedDataCallback: (items) async { return [ @@ -4735,6 +5216,19 @@ class $$ContactsTableTableManager extends RootTableManager< referencedItemsForCurrentItem: (item, referencedItems) => referencedItems .where((e) => e.contactId == item.userId), + typedResults: items), + if (messageRetransmissionsRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: $$ContactsTableReferences + ._messageRetransmissionsRefsTable(db), + managerFromTypedResult: (p0) => + $$ContactsTableReferences(db, table, p0) + .messageRetransmissionsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems + .where((e) => e.contactId == item.userId), typedResults: items) ]; }, @@ -4754,7 +5248,8 @@ typedef $$ContactsTableProcessedTableManager = ProcessedTableManager< $$ContactsTableUpdateCompanionBuilder, (Contact, $$ContactsTableReferences), Contact, - PrefetchHooks Function({bool messagesRefs})>; + PrefetchHooks Function( + {bool messagesRefs, bool messageRetransmissionsRefs})>; typedef $$MessagesTableCreateCompanionBuilder = MessagesCompanion Function({ required int contactId, Value messageId, @@ -4812,6 +5307,26 @@ final class $$MessagesTableReferences return ProcessedTableManager( manager.$state.copyWith(prefetchedData: [item])); } + + static MultiTypedResultKey<$MessageRetransmissionsTable, + List> _messageRetransmissionsRefsTable( + _$TwonlyDatabase db) => + MultiTypedResultKey.fromTable(db.messageRetransmissions, + aliasName: $_aliasNameGenerator( + db.messages.messageId, db.messageRetransmissions.messageId)); + + $$MessageRetransmissionsTableProcessedTableManager + get messageRetransmissionsRefs { + final manager = $$MessageRetransmissionsTableTableManager( + $_db, $_db.messageRetransmissions) + .filter((f) => + f.messageId.messageId.sqlEquals($_itemColumn('message_id')!)); + + final cache = + $_typedResult.readTableOrNull(_messageRetransmissionsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache)); + } } class $$MessagesTableFilterComposer @@ -4901,6 +5416,29 @@ class $$MessagesTableFilterComposer )); return composer; } + + Expression messageRetransmissionsRefs( + Expression Function($$MessageRetransmissionsTableFilterComposer f) + f) { + final $$MessageRetransmissionsTableFilterComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.messageRetransmissions, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessageRetransmissionsTableFilterComposer( + $db: $db, + $table: $db.messageRetransmissions, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } } class $$MessagesTableOrderingComposer @@ -5067,6 +5605,29 @@ class $$MessagesTableAnnotationComposer )); return composer; } + + Expression messageRetransmissionsRefs( + Expression Function($$MessageRetransmissionsTableAnnotationComposer a) + f) { + final $$MessageRetransmissionsTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.messageRetransmissions, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessageRetransmissionsTableAnnotationComposer( + $db: $db, + $table: $db.messageRetransmissions, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } } class $$MessagesTableTableManager extends RootTableManager< @@ -5080,7 +5641,7 @@ class $$MessagesTableTableManager extends RootTableManager< $$MessagesTableUpdateCompanionBuilder, (Message, $$MessagesTableReferences), Message, - PrefetchHooks Function({bool contactId})> { + PrefetchHooks Function({bool contactId, bool messageRetransmissionsRefs})> { $$MessagesTableTableManager(_$TwonlyDatabase db, $MessagesTable table) : super(TableManagerState( db: db, @@ -5171,10 +5732,13 @@ class $$MessagesTableTableManager extends RootTableManager< .map((e) => (e.readTable(table), $$MessagesTableReferences(db, table, e))) .toList(), - prefetchHooksCallback: ({contactId = false}) { + prefetchHooksCallback: ( + {contactId = false, messageRetransmissionsRefs = false}) { return PrefetchHooks( db: db, - explicitlyWatchedTables: [], + explicitlyWatchedTables: [ + if (messageRetransmissionsRefs) db.messageRetransmissions + ], addJoins: < T extends TableManagerState< dynamic, @@ -5202,7 +5766,21 @@ class $$MessagesTableTableManager extends RootTableManager< return state; }, getPrefetchedDataCallback: (items) async { - return []; + return [ + if (messageRetransmissionsRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: $$MessagesTableReferences + ._messageRetransmissionsRefsTable(db), + managerFromTypedResult: (p0) => + $$MessagesTableReferences(db, table, p0) + .messageRetransmissionsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems + .where((e) => e.messageId == item.messageId), + typedResults: items) + ]; }, ); }, @@ -5220,7 +5798,7 @@ typedef $$MessagesTableProcessedTableManager = ProcessedTableManager< $$MessagesTableUpdateCompanionBuilder, (Message, $$MessagesTableReferences), Message, - PrefetchHooks Function({bool contactId})>; + PrefetchHooks Function({bool contactId, bool messageRetransmissionsRefs})>; typedef $$MediaUploadsTableCreateCompanionBuilder = MediaUploadsCompanion Function({ Value mediaUploadId, @@ -6522,6 +7100,380 @@ typedef $$SignalContactSignedPreKeysTableProcessedTableManager ), SignalContactSignedPreKey, PrefetchHooks Function()>; +typedef $$MessageRetransmissionsTableCreateCompanionBuilder + = MessageRetransmissionsCompanion Function({ + Value retransmissionId, + required int contactId, + Value messageId, + required Uint8List plaintextContent, + Value pushData, + Value acknowledgeByServerAt, +}); +typedef $$MessageRetransmissionsTableUpdateCompanionBuilder + = MessageRetransmissionsCompanion Function({ + Value retransmissionId, + Value contactId, + Value messageId, + Value plaintextContent, + Value pushData, + Value acknowledgeByServerAt, +}); + +final class $$MessageRetransmissionsTableReferences extends BaseReferences< + _$TwonlyDatabase, $MessageRetransmissionsTable, MessageRetransmission> { + $$MessageRetransmissionsTableReferences( + super.$_db, super.$_table, super.$_typedResult); + + static $ContactsTable _contactIdTable(_$TwonlyDatabase db) => + db.contacts.createAlias($_aliasNameGenerator( + db.messageRetransmissions.contactId, db.contacts.userId)); + + $$ContactsTableProcessedTableManager get contactId { + final $_column = $_itemColumn('contact_id')!; + + final manager = $$ContactsTableTableManager($_db, $_db.contacts) + .filter((f) => f.userId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_contactIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } + + static $MessagesTable _messageIdTable(_$TwonlyDatabase db) => + db.messages.createAlias($_aliasNameGenerator( + db.messageRetransmissions.messageId, db.messages.messageId)); + + $$MessagesTableProcessedTableManager? get messageId { + final $_column = $_itemColumn('message_id'); + if ($_column == null) return null; + final manager = $$MessagesTableTableManager($_db, $_db.messages) + .filter((f) => f.messageId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_messageIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } +} + +class $$MessageRetransmissionsTableFilterComposer + extends Composer<_$TwonlyDatabase, $MessageRetransmissionsTable> { + $$MessageRetransmissionsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get retransmissionId => $composableBuilder( + column: $table.retransmissionId, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get plaintextContent => $composableBuilder( + column: $table.plaintextContent, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get pushData => $composableBuilder( + column: $table.pushData, builder: (column) => ColumnFilters(column)); + + ColumnFilters get acknowledgeByServerAt => $composableBuilder( + column: $table.acknowledgeByServerAt, + builder: (column) => ColumnFilters(column)); + + $$ContactsTableFilterComposer get contactId { + final $$ContactsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.contactId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableFilterComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + $$MessagesTableFilterComposer get messageId { + final $$MessagesTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessagesTableFilterComposer( + $db: $db, + $table: $db.messages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$MessageRetransmissionsTableOrderingComposer + extends Composer<_$TwonlyDatabase, $MessageRetransmissionsTable> { + $$MessageRetransmissionsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get retransmissionId => $composableBuilder( + column: $table.retransmissionId, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get plaintextContent => $composableBuilder( + column: $table.plaintextContent, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get pushData => $composableBuilder( + column: $table.pushData, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get acknowledgeByServerAt => $composableBuilder( + column: $table.acknowledgeByServerAt, + builder: (column) => ColumnOrderings(column)); + + $$ContactsTableOrderingComposer get contactId { + final $$ContactsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.contactId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableOrderingComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + $$MessagesTableOrderingComposer get messageId { + final $$MessagesTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessagesTableOrderingComposer( + $db: $db, + $table: $db.messages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$MessageRetransmissionsTableAnnotationComposer + extends Composer<_$TwonlyDatabase, $MessageRetransmissionsTable> { + $$MessageRetransmissionsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get retransmissionId => $composableBuilder( + column: $table.retransmissionId, builder: (column) => column); + + GeneratedColumn get plaintextContent => $composableBuilder( + column: $table.plaintextContent, builder: (column) => column); + + GeneratedColumn get pushData => + $composableBuilder(column: $table.pushData, builder: (column) => column); + + GeneratedColumn get acknowledgeByServerAt => $composableBuilder( + column: $table.acknowledgeByServerAt, builder: (column) => column); + + $$ContactsTableAnnotationComposer get contactId { + final $$ContactsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.contactId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableAnnotationComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + $$MessagesTableAnnotationComposer get messageId { + final $$MessagesTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessagesTableAnnotationComposer( + $db: $db, + $table: $db.messages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$MessageRetransmissionsTableTableManager extends RootTableManager< + _$TwonlyDatabase, + $MessageRetransmissionsTable, + MessageRetransmission, + $$MessageRetransmissionsTableFilterComposer, + $$MessageRetransmissionsTableOrderingComposer, + $$MessageRetransmissionsTableAnnotationComposer, + $$MessageRetransmissionsTableCreateCompanionBuilder, + $$MessageRetransmissionsTableUpdateCompanionBuilder, + (MessageRetransmission, $$MessageRetransmissionsTableReferences), + MessageRetransmission, + PrefetchHooks Function({bool contactId, bool messageId})> { + $$MessageRetransmissionsTableTableManager( + _$TwonlyDatabase db, $MessageRetransmissionsTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$MessageRetransmissionsTableFilterComposer( + $db: db, $table: table), + createOrderingComposer: () => + $$MessageRetransmissionsTableOrderingComposer( + $db: db, $table: table), + createComputedFieldComposer: () => + $$MessageRetransmissionsTableAnnotationComposer( + $db: db, $table: table), + updateCompanionCallback: ({ + Value retransmissionId = const Value.absent(), + Value contactId = const Value.absent(), + Value messageId = const Value.absent(), + Value plaintextContent = const Value.absent(), + Value pushData = const Value.absent(), + Value acknowledgeByServerAt = const Value.absent(), + }) => + MessageRetransmissionsCompanion( + retransmissionId: retransmissionId, + contactId: contactId, + messageId: messageId, + plaintextContent: plaintextContent, + pushData: pushData, + acknowledgeByServerAt: acknowledgeByServerAt, + ), + createCompanionCallback: ({ + Value retransmissionId = const Value.absent(), + required int contactId, + Value messageId = const Value.absent(), + required Uint8List plaintextContent, + Value pushData = const Value.absent(), + Value acknowledgeByServerAt = const Value.absent(), + }) => + MessageRetransmissionsCompanion.insert( + retransmissionId: retransmissionId, + contactId: contactId, + messageId: messageId, + plaintextContent: plaintextContent, + pushData: pushData, + acknowledgeByServerAt: acknowledgeByServerAt, + ), + withReferenceMapper: (p0) => p0 + .map((e) => ( + e.readTable(table), + $$MessageRetransmissionsTableReferences(db, table, e) + )) + .toList(), + prefetchHooksCallback: ({contactId = false, messageId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (contactId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.contactId, + referencedTable: $$MessageRetransmissionsTableReferences + ._contactIdTable(db), + referencedColumn: $$MessageRetransmissionsTableReferences + ._contactIdTable(db) + .userId, + ) as T; + } + if (messageId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.messageId, + referencedTable: $$MessageRetransmissionsTableReferences + ._messageIdTable(db), + referencedColumn: $$MessageRetransmissionsTableReferences + ._messageIdTable(db) + .messageId, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + )); +} + +typedef $$MessageRetransmissionsTableProcessedTableManager + = ProcessedTableManager< + _$TwonlyDatabase, + $MessageRetransmissionsTable, + MessageRetransmission, + $$MessageRetransmissionsTableFilterComposer, + $$MessageRetransmissionsTableOrderingComposer, + $$MessageRetransmissionsTableAnnotationComposer, + $$MessageRetransmissionsTableCreateCompanionBuilder, + $$MessageRetransmissionsTableUpdateCompanionBuilder, + (MessageRetransmission, $$MessageRetransmissionsTableReferences), + MessageRetransmission, + PrefetchHooks Function({bool contactId, bool messageId})>; class $TwonlyDatabaseManager { final _$TwonlyDatabase _db; @@ -6549,4 +7501,7 @@ class $TwonlyDatabaseManager { get signalContactSignedPreKeys => $$SignalContactSignedPreKeysTableTableManager( _db, _db.signalContactSignedPreKeys); + $$MessageRetransmissionsTableTableManager get messageRetransmissions => + $$MessageRetransmissionsTableTableManager( + _db, _db.messageRetransmissions); } diff --git a/lib/src/services/api.service.dart b/lib/src/services/api.service.dart index d85efdc..a48df1c 100644 --- a/lib/src/services/api.service.dart +++ b/lib/src/services/api.service.dart @@ -26,9 +26,9 @@ import 'package:twonly/src/services/api/server_messages.dart'; import 'package:twonly/src/services/signal/identity.signal.dart'; import 'package:twonly/src/services/signal/prekeys.signal.dart'; import 'package:twonly/src/services/signal/utils.signal.dart'; -import 'package:twonly/src/utils/hive.dart'; import 'package:twonly/src/services/fcm.service.dart'; import 'package:twonly/src/services/flame.service.dart'; +import 'package:twonly/src/utils/keyvalue.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; @@ -38,6 +38,7 @@ import 'package:libsignal_protocol_dart/src/ecc/ed25519.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; final lockConnecting = Mutex(); +final lockRetransStore = Mutex(); /// The ApiProvider is responsible for communicating with the server. /// It handles errors and does automatically tries to reconnect on @@ -203,39 +204,43 @@ class ApiService { } Future> getRetransmission() async { - final box = await getMediaStorage(); - try { - return box.get("rawbytes-to-retransmit"); - } catch (e) { - return {}; - } + return await KeyValueStore.get("rawbytes-to-retransmit") ?? {}; } Future retransmitRawBytes() async { - var retransmit = await getRetransmission(); - Log.info("retransmitting ${retransmit.keys.length} messages"); - for (final seq in retransmit.keys) { - try { - _channel!.sink.add(base64Decode(retransmit[seq])); - } catch (e) { - Log.error("$e"); + await lockRetransStore.protect(() async { + var retransmit = await getRetransmission(); + Log.info("retransmitting ${retransmit.keys.length} messages"); + bool gotError = false; + for (final seq in retransmit.keys) { + try { + _channel!.sink.add(base64Decode(retransmit[seq])); + } catch (e) { + gotError = true; + Log.error("$e"); + } } - } + if (!gotError) { + KeyValueStore.put("rawbytes-to-retransmit", {}); + } + }); } Future addToRetransmissionBuffer(Int64 seq, Uint8List bytes) async { - var retransmit = await getRetransmission(); - retransmit[seq.toString()] = base64Encode(bytes); - final box = await getMediaStorage(); - box.put("rawbytes-to-retransmit", retransmit); + await lockRetransStore.protect(() async { + var retransmit = await getRetransmission(); + retransmit[seq.toString()] = base64Encode(bytes); + KeyValueStore.put("rawbytes-to-retransmit", retransmit); + }); } Future removeFromRetransmissionBuffer(Int64 seq) async { - var retransmit = await getRetransmission(); - if (retransmit.isEmpty) return; - retransmit.remove(seq.toString()); - final box = await getMediaStorage(); - box.put("rawbytes-to-retransmit", retransmit); + await lockRetransStore.protect(() async { + var retransmit = await getRetransmission(); + if (retransmit.isEmpty) return; + retransmit.remove(seq.toString()); + KeyValueStore.put("rawbytes-to-retransmit", retransmit); + }); } Future sendRequestSync( diff --git a/lib/src/services/api/messages.dart b/lib/src/services/api/messages.dart index e089a4b..b2af649 100644 --- a/lib/src/services/api/messages.dart +++ b/lib/src/services/api/messages.dart @@ -1,8 +1,6 @@ import 'dart:convert'; -import 'dart:math'; +import 'dart:io'; import 'package:drift/drift.dart'; -import 'package:hive/hive.dart'; -import 'package:mutex/mutex.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/twonly_database.dart'; import 'package:twonly/src/database/tables/messages_table.dart'; @@ -11,192 +9,79 @@ import 'package:twonly/src/model/json/userdata.dart'; import 'package:twonly/src/model/protobuf/api/error.pb.dart'; import 'package:twonly/src/services/api/utils.dart'; import 'package:twonly/src/services/signal/encryption.signal.dart'; -import 'package:twonly/src/utils/hive.dart'; import 'package:twonly/src/services/notification.service.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/storage.dart'; -final lockSendingMessages = Mutex(); - Future tryTransmitMessages() async { - lockSendingMessages.protect(() async { - Map retransmit = await getAllMessagesForRetransmitting(); + final retransIds = + await twonlyDB.messageRetransmissionDao.getRetransmitAbleMessages(); - if (retransmit.isEmpty) return; - - Log.info("try sending messages: ${retransmit.length}"); - - Map failed = {}; - - List> sortedList = retransmit.entries.toList() - ..sort((a, b) => int.parse(a.key).compareTo(int.parse(b.key))); - - for (final element in sortedList) { - RetransmitMessage msg = - RetransmitMessage.fromJson(jsonDecode(element.value)); - - Result resp = await apiService.sendTextMessage( - msg.userId, - msg.bytes, - msg.pushData, - ); - - if (resp.isSuccess) { - if (msg.messageId != null) { - await twonlyDB.messagesDao.updateMessageByMessageId( - msg.messageId!, - MessagesCompanion( - acknowledgeByServer: Value(true), - ), - ); - } - } else { - failed[element.key] = element.value; - } - } - Box box = await getMediaStorage(); - box.put("messages-to-retransmit", jsonEncode(failed)); - }); -} - -class RetransmitMessage { - int? messageId; - int userId; - Uint8List bytes; - List? pushData; - RetransmitMessage({ - this.messageId, - required this.userId, - required this.bytes, - this.pushData, - }); - - // From JSON constructor - factory RetransmitMessage.fromJson(Map json) { - return RetransmitMessage( - messageId: json['messageId'], - userId: json['userId'], - bytes: base64Decode(json['bytes']), - pushData: json['pushData'] == null - ? null - : List.from(json['pushData'].map((item) => item as int)), - ); - } - - // To JSON method - Map toJson() { - return { - 'messageId': messageId, - 'userId': userId, - 'bytes': base64Encode(bytes), - 'pushData': pushData, - }; + for (final retransId in retransIds) { + sendRetransmitMessage(retransId); } } -Future> getAllMessagesForRetransmitting() async { - Box box = await getMediaStorage(); - String? retransmitJson = box.get("messages-to-retransmit"); - Map retransmit = {}; +Future sendRetransmitMessage(int retransId) async { + MessageRetransmission? retrans = await twonlyDB.messageRetransmissionDao + .getRetransmissionById(retransId) + .getSingleOrNull(); - if (retransmitJson != null) { - try { - retransmit = jsonDecode(retransmitJson); - } catch (e) { - Log.error("Could not decode the retransmit messages: $e"); - await box.delete("messages-to-retransmit"); - } + if (retrans == null) { + Log.error("$retransId not found in database"); + return; } - return retransmit; -} -Future sendRetransmitMessage( - String stateId, RetransmitMessage msg) async { - Log.info("Sending ${msg.messageId}"); - Result resp = - await apiService.sendTextMessage(msg.userId, msg.bytes, msg.pushData); + Uint8List? encryptedBytes = await signalEncryptMessage( + retrans.contactId, + retrans.plaintextContent, + ); + + if (encryptedBytes == null) { + Log.error("Could not encrypt the message. Aborting and trying again."); + return; + } + + Result resp = await apiService.sendTextMessage( + retrans.contactId, + encryptedBytes, + retrans.pushData, + ); bool retry = true; if (resp.isError) { if (resp.error == ErrorCode.UserIdNotFound) { retry = false; - if (msg.messageId != null) { + if (retrans.messageId != null) { await twonlyDB.messagesDao.updateMessageByMessageId( - msg.messageId!, + retrans.messageId!, MessagesCompanion(errorWhileSending: Value(true)), ); } + await twonlyDB.contactsDao.updateContact( + retrans.contactId, + ContactsCompanion(deleted: Value(true)), + ); } } if (resp.isSuccess) { retry = false; - if (msg.messageId != null) { + if (retrans.messageId != null) { await twonlyDB.messagesDao.updateMessageByMessageId( - msg.messageId!, - MessagesCompanion(acknowledgeByServer: Value(true)), + retrans.messageId!, + MessagesCompanion( + acknowledgeByServer: Value(true), + errorWhileSending: Value(false), + ), ); } } if (!retry) { - { - var retransmit = await getAllMessagesForRetransmitting(); - retransmit.remove(stateId); - Box box = await getMediaStorage(); - box.put("messages-to-retransmit", jsonEncode(retransmit)); - } + await twonlyDB.messageRetransmissionDao.deleteRetransmissionById(retransId); } - return resp; -} - -// this functions ensures that the message is received by the server and in case of errors will try again later -Future<(String, RetransmitMessage)?> encryptMessage( - int? messageId, int userId, MessageJson msg, - {PushKind? pushKind}) async { - return await lockSendingMessages - .protect<(String, RetransmitMessage)?>(() async { - Uint8List? bytes = await signalEncryptMessage(userId, msg); - - if (bytes == null) { - Log.error("Error encryption message!"); - return null; - } - - var retransmit = await getAllMessagesForRetransmitting(); - - int currentMaxStateId = messageId ?? 60000; - if (retransmit.isNotEmpty && messageId == null) { - currentMaxStateId = retransmit.keys.map((x) => int.parse(x)).reduce(max); - if (currentMaxStateId < 60000) { - currentMaxStateId = 60000; - } - } - - String stateId = (currentMaxStateId + 1).toString(); - - Box box = await getMediaStorage(); - - List? pushData; - if (pushKind != null) { - pushData = await getPushData(userId, pushKind); - } - - RetransmitMessage encryptedMessage = RetransmitMessage( - messageId: messageId, - userId: userId, - bytes: bytes, - pushData: pushData, - ); - - { - retransmit[stateId] = jsonEncode(encryptedMessage.toJson()); - box.put("messages-to-retransmit", jsonEncode(retransmit)); - } - - return (stateId, encryptedMessage); - }); } // encrypts and stores the message and then sends it in the background @@ -205,16 +90,38 @@ Future encryptAndSendMessageAsync(int? messageId, int userId, MessageJson msg, if (gIsDemoUser) { return; } - (String, RetransmitMessage)? stateData = - await encryptMessage(messageId, userId, msg, pushKind: pushKind); - if (stateData != null) { - final (stateId, message) = stateData; - sendRetransmitMessage(stateId, message); + + Uint8List? pushData; + if (pushKind != null) { + pushData = await getPushData(userId, pushKind); } + + Uint8List plaintextContent = + Uint8List.fromList(gzip.encode(utf8.encode(jsonEncode(msg.toJson())))); + + int? retransId = await twonlyDB.messageRetransmissionDao.insertRetransmission( + MessageRetransmissionsCompanion( + contactId: Value(userId), + messageId: Value(messageId), + plaintextContent: Value(plaintextContent), + pushData: Value(pushData), + ), + ); + + if (retransId == null) { + Log.error("Could not insert the message into the retransmission database"); + return; + } + + // this can now be done in the background... + sendRetransmitMessage(retransId); } Future sendTextMessage( - int target, TextMessageContent content, PushKind? pushKind) async { + int target, + TextMessageContent content, + PushKind? pushKind, +) async { DateTime messageSendAt = DateTime.now(); int? messageId = await twonlyDB.messagesDao.insertMessage( diff --git a/lib/src/services/api/server_messages.dart b/lib/src/services/api/server_messages.dart index cfaa7d9..31aa6e4 100644 --- a/lib/src/services/api/server_messages.dart +++ b/lib/src/services/api/server_messages.dart @@ -100,8 +100,9 @@ Future handleNewMessage(int fromUserId, Uint8List body) async { case MessageKind.opened: if (message.messageId != null) { final update = MessagesCompanion( - openedAt: Value(message.timestamp), - errorWhileSending: Value(false)); + openedAt: Value(message.timestamp), + errorWhileSending: Value(false), + ); await twonlyDB.messagesDao.updateMessageByOtherUser( fromUserId, message.messageId!, @@ -214,8 +215,11 @@ Future handleNewMessage(int fromUserId, Uint8List body) async { fromUserId, responseToMessageId, MessagesCompanion( - errorWhileSending: Value(false), - ), + errorWhileSending: Value(false), + openedAt: Value( + DateTime.now(), + ) // when a user reacted to the media file, it should be marked as opened + ), ); } @@ -259,16 +263,16 @@ Future handleNewMessage(int fromUserId, Uint8List body) async { } } - // await encryptAndSendMessageAsync( - // message.messageId!, - // fromUserId, - // MessageJson( - // kind: MessageKind.ack, - // messageId: message.messageId!, - // content: MessageContent(), - // timestamp: DateTime.now(), - // ), - // ); + await encryptAndSendMessageAsync( + message.messageId!, + fromUserId, + MessageJson( + kind: MessageKind.ack, + messageId: message.messageId!, + content: MessageContent(), + timestamp: DateTime.now(), + ), + ); // unarchive contact when receiving a new message await twonlyDB.contactsDao.updateContact( diff --git a/lib/src/services/notification.service.dart b/lib/src/services/notification.service.dart index cecbb8b..8cab7e4 100644 --- a/lib/src/services/notification.service.dart +++ b/lib/src/services/notification.service.dart @@ -238,7 +238,7 @@ class PushNotification { /// this will trigger a push notification /// push notification only containing the message kind and username -Future?> getPushData(int toUserId, PushKind kind) async { +Future getPushData(int toUserId, PushKind kind) async { final Map pushKeys = await getPushKeys("sendingPushKeys"); List key = "InsecureOnlyUsedForAddingContact".codeUnits; @@ -278,8 +278,7 @@ Future?> getPushData(int toUserId, PushKind kind) async { cipherText: secretBox.cipherText, mac: secretBox.mac.bytes, ); - - return jsonEncode(res.toJson()).codeUnits; + return Utf8Encoder().convert(jsonEncode(res.toJson())); } Future tryDecryptMessage( diff --git a/lib/src/services/signal/encryption.signal.dart b/lib/src/services/signal/encryption.signal.dart index a5238f0..8eb9c9a 100644 --- a/lib/src/services/signal/encryption.signal.dart +++ b/lib/src/services/signal/encryption.signal.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; +import 'package:mutex/mutex.dart'; import 'package:twonly/src/database/twonly_database.dart'; import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/database/signal/connect_signal_protocol_store.dart'; @@ -11,82 +12,87 @@ import 'package:twonly/src/services/signal/utils.signal.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; -Future signalEncryptMessage(int target, MessageJson msg) async { - try { - ConnectSignalProtocolStore signalStore = (await getSignalStore())!; - final address = SignalProtocolAddress(target.toString(), defaultDeviceId); +/// This caused some troubles, so protection the encryption... +final lockingSignalEncryption = Mutex(); - SessionCipher session = SessionCipher.fromStore(signalStore, address); +Future signalEncryptMessage( + int target, Uint8List plaintextContent) async { + return await lockingSignalEncryption.protect(() async { + try { + ConnectSignalProtocolStore signalStore = (await getSignalStore())!; + final address = SignalProtocolAddress(target.toString(), defaultDeviceId); - SignalContactPreKey? preKey = await getPreKeyByContactId(target); - SignalContactSignedPreKey? signedPreKey = await getSignedPreKeyByContactId( - target, - ); + SessionCipher session = SessionCipher.fromStore(signalStore, address); - if (signedPreKey != null) { - SessionBuilder sessionBuilder = SessionBuilder.fromSignalStore( - signalStore, - address, + SignalContactPreKey? preKey = await getPreKeyByContactId(target); + SignalContactSignedPreKey? signedPreKey = + await getSignedPreKeyByContactId( + target, ); - ECPublicKey? tempPrePublicKey; + if (signedPreKey != null) { + SessionBuilder sessionBuilder = SessionBuilder.fromSignalStore( + signalStore, + address, + ); - if (preKey != null) { - tempPrePublicKey = Curve.decodePoint( - DjbECPublicKey( - Uint8List.fromList(preKey.preKey), - ).serialize(), + ECPublicKey? tempPrePublicKey; + + if (preKey != null) { + tempPrePublicKey = Curve.decodePoint( + DjbECPublicKey( + Uint8List.fromList(preKey.preKey), + ).serialize(), + 1, + ); + } + + ECPublicKey? tempSignedPreKeyPublic = Curve.decodePoint( + DjbECPublicKey(Uint8List.fromList(signedPreKey.signedPreKey)) + .serialize(), 1, ); - } - ECPublicKey? tempSignedPreKeyPublic = Curve.decodePoint( - DjbECPublicKey(Uint8List.fromList(signedPreKey.signedPreKey)) - .serialize(), - 1, - ); - - Uint8List? tempSignedPreKeySignature = Uint8List.fromList( - signedPreKey.signedPreKeySignature, - ); - - final IdentityKey? tempIdentityKey = - await signalStore.getIdentity(address); - if (tempIdentityKey != null) { - PreKeyBundle preKeyBundle = PreKeyBundle( - target, - defaultDeviceId, - preKey?.preKeyId, - tempPrePublicKey, - signedPreKey.signedPreKeyId, - tempSignedPreKeyPublic, - tempSignedPreKeySignature, - tempIdentityKey, + Uint8List? tempSignedPreKeySignature = Uint8List.fromList( + signedPreKey.signedPreKeySignature, ); - try { - await sessionBuilder.processPreKeyBundle(preKeyBundle); - } catch (e) { - Log.error("could not process pre key bundle: $e"); + final IdentityKey? tempIdentityKey = + await signalStore.getIdentity(address); + if (tempIdentityKey != null) { + PreKeyBundle preKeyBundle = PreKeyBundle( + target, + defaultDeviceId, + preKey?.preKeyId, + tempPrePublicKey, + signedPreKey.signedPreKeyId, + tempSignedPreKeyPublic, + tempSignedPreKeySignature, + tempIdentityKey, + ); + + try { + await sessionBuilder.processPreKeyBundle(preKeyBundle); + } catch (e) { + Log.error("could not process pre key bundle: $e"); + } + } else { + Log.error("did not get the identity of the remote address"); } - } else { - Log.error("did not get the identity of the remote address"); } + + final ciphertext = await session.encrypt(plaintextContent); + + var b = BytesBuilder(); + b.add(ciphertext.serialize()); + b.add(intToBytes(ciphertext.getType())); + + return b.takeBytes(); + } catch (e) { + Log.error(e.toString()); + return null; } - - final ciphertext = await session.encrypt( - Uint8List.fromList(gzip.encode(utf8.encode(jsonEncode(msg.toJson())))), - ); - - var b = BytesBuilder(); - b.add(ciphertext.serialize()); - b.add(intToBytes(ciphertext.getType())); - - return b.takeBytes(); - } catch (e) { - Log.error(e.toString()); - return null; - } + }); } Future signalDecryptMessage(int source, Uint8List msg) async { @@ -115,7 +121,12 @@ Future signalDecryptMessage(int source, Uint8List msg) async { return null; } return MessageJson.fromJson( - jsonDecode(utf8.decode(gzip.decode(plaintext)))); + jsonDecode( + utf8.decode( + gzip.decode(plaintext), + ), + ), + ); } on InvalidKeyIdException catch (_) { return null; // got the same message again } on DuplicateMessageException catch (_) { diff --git a/lib/src/utils/hive.dart b/lib/src/utils/hive.dart deleted file mode 100644 index dcf9788..0000000 --- a/lib/src/utils/hive.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'dart:convert'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:hive/hive.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:twonly/src/services/notification.service.dart'; -import 'package:twonly/src/utils/log.dart'; - -Future initMediaStorage() async { - final storage = FlutterSecureStorage(); - var containsEncryptionKey = - await storage.containsKey(key: 'hive_encryption_key'); - if (!containsEncryptionKey) { - var key = Hive.generateSecureKey(); - await storage.write( - key: 'hive_encryption_key', - value: base64UrlEncode(key), - ); - } - final dir = await getApplicationSupportDirectory(); - Hive.init(dir.path); -} - -Future getMediaStorage() async { - try { - await initMediaStorage(); - final storage = FlutterSecureStorage(); - - var encryptionKey = - base64Url.decode((await storage.read(key: 'hive_encryption_key'))!); - - return await Hive.openBox( - 'media_storage', - encryptionCipher: HiveAesCipher(encryptionKey), - ); - } catch (e) { - await customLocalPushNotification("Secure Storage Error", - "Settings > Apps > twonly > Storage and Cache > Press clear on both"); - Log.error(e); - throw Exception(e); - } -} diff --git a/lib/src/utils/keyvalue.dart b/lib/src/utils/keyvalue.dart new file mode 100644 index 0000000..916ed0e --- /dev/null +++ b/lib/src/utils/keyvalue.dart @@ -0,0 +1,44 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:path_provider/path_provider.dart'; +import 'package:twonly/src/utils/log.dart'; + +class KeyValueStore { + static Future _getFilePath(String key) async { + final directory = await getApplicationSupportDirectory(); + return '${directory.path}/keyvalue/$key.json'; + } + + static Future?> get(String key) async { + try { + final filePath = await _getFilePath(key); + final file = File(filePath); + + // Check if the file exists + if (await file.exists()) { + final contents = await file.readAsString(); + return jsonDecode(contents); + } else { + return null; // File does not exist + } + } catch (e) { + Log.error('Error reading file: $e'); + return null; + } + } + + static Future put(String key, Map value) async { + try { + final filePath = await _getFilePath(key); + final file = File(filePath); + + // Create the directory if it doesn't exist + await file.parent.create(recursive: true); + + // Write the JSON data to the file + await file.writeAsString(jsonEncode(value)); + } catch (e) { + Log.error('Error writing file: $e'); + } + } +} diff --git a/lib/src/views/chats/chat_messages.view.dart b/lib/src/views/chats/chat_messages.view.dart index 543d2d6..6594925 100644 --- a/lib/src/views/chats/chat_messages.view.dart +++ b/lib/src/views/chats/chat_messages.view.dart @@ -146,8 +146,10 @@ class _ChatMessagesViewState extends State { } if (openedMessageOtherIds.isNotEmpty) { - notifyContactAboutOpeningMessage( - widget.contact.userId, openedMessageOtherIds); + await notifyContactAboutOpeningMessage( + widget.contact.userId, + openedMessageOtherIds, + ); } twonlyDB.messagesDao.openedAllNonMediaMessages(widget.contact.userId); diff --git a/lib/src/views/chats/media_viewer.view.dart b/lib/src/views/chats/media_viewer.view.dart index 5a1ef30..ceac069 100644 --- a/lib/src/views/chats/media_viewer.view.dart +++ b/lib/src/views/chats/media_viewer.view.dart @@ -210,7 +210,7 @@ class _MediaViewerViewState extends State { } } - notifyContactAboutOpeningMessage( + await notifyContactAboutOpeningMessage( current.contactId, [current.messageOtherId!], ); diff --git a/pubspec.lock b/pubspec.lock index 110f21f..ff29c60 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -779,14 +779,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" - hive: - dependency: "direct main" - description: - name: hive - sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" - url: "https://pub.dev" - source: hosted - version: "2.2.3" html: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 4d067ac..f5ed243 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,7 +30,6 @@ dependencies: font_awesome_flutter: ^10.8.0 gal: ^2.3.1 hand_signature: ^3.0.3 - hive: ^2.2.3 image: ^4.3.0 intl: ^0.20.2 introduction_screen: ^3.1.14