This commit is contained in:
otsmr 2025-06-19 16:10:20 +02:00
parent 4108d9a798
commit 80746b5139
29 changed files with 4852 additions and 390 deletions

271
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,271 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "twonly-app",
"request": "launch",
"type": "dart"
},
{
"name": "twonly-app (profile mode)",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "twonly-app (release mode)",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "flutter_secure_storage",
"cwd": "dependencies/flutter_secure_storage",
"request": "launch",
"type": "dart"
},
{
"name": "flutter_zxing",
"cwd": "dependencies/flutter_zxing",
"request": "launch",
"type": "dart"
},
{
"name": "flutter_zxing (profile mode)",
"cwd": "dependencies/flutter_zxing",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "flutter_zxing (release mode)",
"cwd": "dependencies/flutter_zxing",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "flutter_secure_storage",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage",
"request": "launch",
"type": "dart"
},
{
"name": "flutter_secure_storage (profile mode)",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "flutter_secure_storage (release mode)",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "flutter_secure_storage_darwin",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_darwin",
"request": "launch",
"type": "dart"
},
{
"name": "flutter_secure_storage_darwin (profile mode)",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_darwin",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "flutter_secure_storage_darwin (release mode)",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_darwin",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "flutter_secure_storage_linux",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_linux",
"request": "launch",
"type": "dart"
},
{
"name": "flutter_secure_storage_linux (profile mode)",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_linux",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "flutter_secure_storage_linux (release mode)",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_linux",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "flutter_secure_storage_platform_interface",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_platform_interface",
"request": "launch",
"type": "dart"
},
{
"name": "flutter_secure_storage_platform_interface (profile mode)",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_platform_interface",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "flutter_secure_storage_platform_interface (release mode)",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_platform_interface",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "flutter_secure_storage_web",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_web",
"request": "launch",
"type": "dart"
},
{
"name": "flutter_secure_storage_web (profile mode)",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_web",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "flutter_secure_storage_web (release mode)",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_web",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "flutter_secure_storage_windows",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_windows",
"request": "launch",
"type": "dart"
},
{
"name": "flutter_secure_storage_windows (profile mode)",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_windows",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "flutter_secure_storage_windows (release mode)",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_windows",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "example",
"cwd": "dependencies/flutter_zxing/example",
"request": "launch",
"type": "dart"
},
{
"name": "example (profile mode)",
"cwd": "dependencies/flutter_zxing/example",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "example (release mode)",
"cwd": "dependencies/flutter_zxing/example",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "zxscanner",
"cwd": "dependencies/flutter_zxing/zxscanner",
"request": "launch",
"type": "dart"
},
{
"name": "zxscanner (profile mode)",
"cwd": "dependencies/flutter_zxing/zxscanner",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "zxscanner (release mode)",
"cwd": "dependencies/flutter_zxing/zxscanner",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "flutter_secure_storage_macos",
"cwd": "dependencies/flutter_secure_storage/archived_packages/flutter_secure_storage_macos",
"request": "launch",
"type": "dart"
},
{
"name": "flutter_secure_storage_macos (profile mode)",
"cwd": "dependencies/flutter_secure_storage/archived_packages/flutter_secure_storage_macos",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "flutter_secure_storage_macos (release mode)",
"cwd": "dependencies/flutter_secure_storage/archived_packages/flutter_secure_storage_macos",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "example",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage/example",
"request": "launch",
"type": "dart"
},
{
"name": "example (profile mode)",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage/example",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "example (release mode)",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage/example",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "example",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_windows/example",
"request": "launch",
"type": "dart"
},
{
"name": "example (profile mode)",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_windows/example",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "example (release mode)",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_windows/example",
"request": "launch",
"type": "dart",
"flutterMode": "release"
}
]
}

File diff suppressed because one or more lines are too long

View file

@ -44,6 +44,7 @@ void main() async {
twonlyDB = TwonlyDatabase();
await twonlyDB.messagesDao.resetPendingDownloadState();
await twonlyDB.messagesDao.handleMediaFilesOlderThan7Days();
await twonlyDB.signalDao.purgeOutDatedPreKeys();
// purge media files in the background
purgeReceivedMediaFiles();

View file

@ -36,9 +36,34 @@ class MessageRetransmissionDao extends DatabaseAccessor<TwonlyDatabase>
..where((t) => t.retransmissionId.equals(retransmissionId));
}
Future updateRetransmission(
int retransmissionId,
MessageRetransmissionsCompanion updatedValues,
) {
return (update(messageRetransmissions)
..where((c) => c.retransmissionId.equals(retransmissionId)))
.write(updatedValues);
}
Future resetAckStatusForAllMessages() {
return ((update(messageRetransmissions))
..where((m) => m.willNotGetACKByUser.equals(false)))
.write(
MessageRetransmissionsCompanion(
acknowledgeByServerAt: Value(null),
),
);
}
Future deleteRetransmissionById(int retransmissionId) {
return (delete(messageRetransmissions)
..where((t) => t.retransmissionId.equals(retransmissionId)))
.go();
}
Future deleteRetransmissionByMessageId(int messageId) {
return (delete(messageRetransmissions)
..where((t) => t.messageId.equals(messageId)))
.go();
}
}

View file

@ -1,11 +1,16 @@
import 'package:drift/drift.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/tables/signal_contact_prekey_table.dart';
import 'package:twonly/src/database/tables/signal_contact_signed_prekey_table.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/utils/log.dart';
part 'signal_dao.g.dart';
@DriftAccessor(tables: [SignalContactPreKeys, SignalContactSignedPreKeys])
@DriftAccessor(tables: [
SignalContactPreKeys,
SignalContactSignedPreKeys,
])
class SignalDao extends DatabaseAccessor<TwonlyDatabase> with _$SignalDaoMixin {
// this constructor is required so that the main database can create an instance
// of this object.
@ -19,6 +24,12 @@ class SignalDao extends DatabaseAccessor<TwonlyDatabase> with _$SignalDaoMixin {
.go();
}
Future deleteAllPreKeysByContactId(int contactId) async {
await (delete(signalContactPreKeys)
..where((t) => t.contactId.equals(contactId)))
.go();
}
// 1: Count the number of pre-keys by contact ID
Future<int> countPreKeysByContactId(int contactId) {
return (select(signalContactPreKeys)
@ -49,9 +60,13 @@ class SignalDao extends DatabaseAccessor<TwonlyDatabase> with _$SignalDaoMixin {
// 3: Insert multiple pre-keys
Future<void> insertPreKeys(
List<SignalContactPreKeysCompanion> preKeys) async {
await batch((batch) {
batch.insertAll(signalContactPreKeys, preKeys);
});
for (final preKey in preKeys) {
try {
into(signalContactPreKeys).insert(preKey);
} catch (e) {
Log.error("$e");
}
}
}
// 4: Get signed pre-key by contact ID
@ -64,12 +79,28 @@ class SignalDao extends DatabaseAccessor<TwonlyDatabase> with _$SignalDaoMixin {
// 5: Insert or update signed pre-key by contact ID
Future<void> insertOrUpdateSignedPreKeyByContactId(
SignalContactSignedPreKeysCompanion signedPreKey) async {
final existingKey =
await getSignedPreKeyByContactId(signedPreKey.contactId.value);
if (existingKey != null) {
await update(signalContactSignedPreKeys).replace(signedPreKey);
} else {
await (delete(signalContactSignedPreKeys)
..where((t) => t.contactId.equals(signedPreKey.contactId.value)))
.go();
await into(signalContactSignedPreKeys).insert(signedPreKey);
}
Future<void> purgeOutDatedPreKeys() async {
// other pre keys are valid 25 days
await (delete(signalContactSignedPreKeys)
..where((t) => (t.createdAt.isSmallerThanValue(
DateTime.now().subtract(
Duration(days: 25),
),
))))
.go();
// own pre keys are valid for 40 days
await (delete(twonlyDB.signalPreKeyStores)
..where((t) => (t.createdAt.isSmallerThanValue(
DateTime.now().subtract(
Duration(days: 40),
),
))))
.go();
}
}

View file

@ -32,25 +32,6 @@ class ConnectPreKeyStore extends PreKeyStore {
.go();
}
static Future<int?> getNextPreKeyId() async {
try {
String tableName = twonlyDB.signalPreKeyStores.actualTableName;
String columnName = twonlyDB.signalPreKeyStores.preKeyId.name;
final result = await twonlyDB
.customSelect('SELECT MAX($columnName) AS max_id FROM $tableName')
.get();
int? count = result.first.read<int?>('max_id');
if (count == null) {
return 0;
}
return count + 1;
} catch (e) {
Log.error("$e");
}
return null;
}
@override
Future<void> storePreKey(int preKeyId, PreKeyRecord record) async {
final preKeyCompanion = SignalPreKeyStoresCompanion(

View file

@ -22,24 +22,6 @@ class ConnectSignedPreKeyStore extends SignedPreKeyStore {
return store;
}
Future<int> getNextKeyId() async {
final storage = FlutterSecureStorage();
final storeSerialized = await storage.read(
key: SecureStorageKeys.signalSignedPreKey,
);
if (storeSerialized == null) {
return 0;
}
final storeHashMap = json.decode(storeSerialized);
var maxKeyId = 0;
for (final item in storeHashMap) {
if (maxKeyId < item[0]) {
maxKeyId = item[0];
}
}
return maxKeyId + 1;
}
Future safeStore(HashMap<int, Uint8List> store) async {
final storage = FlutterSecureStorage();
var storeHashMap = [];

View file

@ -15,5 +15,8 @@ class MessageRetransmissions extends Table {
BlobColumn get plaintextContent => blob()();
BlobColumn get pushData => blob().nullable()();
BoolColumn get willNotGetACKByUser =>
boolean().withDefault(Constant(false))();
DateTimeColumn get acknowledgeByServerAt => dateTime().nullable()();
}

View file

@ -14,6 +14,7 @@ enum MessageKind {
opened,
ack,
pushKey,
requestPushKey,
receiveMediaError,
signalDecryptError
}

View file

@ -53,7 +53,7 @@ class TwonlyDatabase extends _$TwonlyDatabase {
TwonlyDatabase.forTesting(DatabaseConnection super.connection);
@override
int get schemaVersion => 11;
int get schemaVersion => 12;
static QueryExecutor _openConnection() {
return driftDatabase(
@ -121,6 +121,10 @@ class TwonlyDatabase extends _$TwonlyDatabase {
from10To11: (m, schema) async {
m.createTable(messageRetransmissions);
},
from11To12: (m, schema) async {
m.addColumn(schema.messageRetransmissions,
schema.messageRetransmissions.willNotGetACKByUser);
},
),
);
}
@ -135,6 +139,12 @@ class TwonlyDatabase extends _$TwonlyDatabase {
await delete(messageRetransmissions).go();
await delete(mediaDownloads).go();
await delete(mediaUploads).go();
await update(contacts).write(
ContactsCompanion(
avatarSvg: Value(null),
myAvatarCounter: Value(0),
),
);
await delete(signalContactPreKeys).go();
await delete(signalContactSignedPreKeys).go();
}

View file

@ -4225,6 +4225,16 @@ class $MessageRetransmissionsTable extends MessageRetransmissions
late final GeneratedColumn<Uint8List> pushData = GeneratedColumn<Uint8List>(
'push_data', aliasedName, true,
type: DriftSqlType.blob, requiredDuringInsert: false);
static const VerificationMeta _willNotGetACKByUserMeta =
const VerificationMeta('willNotGetACKByUser');
@override
late final GeneratedColumn<bool> willNotGetACKByUser = GeneratedColumn<bool>(
'will_not_get_a_c_k_by_user', aliasedName, false,
type: DriftSqlType.bool,
requiredDuringInsert: false,
defaultConstraints: GeneratedColumn.constraintIsAlways(
'CHECK ("will_not_get_a_c_k_by_user" IN (0, 1))'),
defaultValue: Constant(false));
static const VerificationMeta _acknowledgeByServerAtMeta =
const VerificationMeta('acknowledgeByServerAt');
@override
@ -4238,6 +4248,7 @@ class $MessageRetransmissionsTable extends MessageRetransmissions
messageId,
plaintextContent,
pushData,
willNotGetACKByUser,
acknowledgeByServerAt
];
@override
@ -4279,6 +4290,12 @@ class $MessageRetransmissionsTable extends MessageRetransmissions
context.handle(_pushDataMeta,
pushData.isAcceptableOrUnknown(data['push_data']!, _pushDataMeta));
}
if (data.containsKey('will_not_get_a_c_k_by_user')) {
context.handle(
_willNotGetACKByUserMeta,
willNotGetACKByUser.isAcceptableOrUnknown(
data['will_not_get_a_c_k_by_user']!, _willNotGetACKByUserMeta));
}
if (data.containsKey('acknowledge_by_server_at')) {
context.handle(
_acknowledgeByServerAtMeta,
@ -4304,6 +4321,8 @@ class $MessageRetransmissionsTable extends MessageRetransmissions
DriftSqlType.blob, data['${effectivePrefix}plaintext_content'])!,
pushData: attachedDatabase.typeMapping
.read(DriftSqlType.blob, data['${effectivePrefix}push_data']),
willNotGetACKByUser: attachedDatabase.typeMapping.read(DriftSqlType.bool,
data['${effectivePrefix}will_not_get_a_c_k_by_user'])!,
acknowledgeByServerAt: attachedDatabase.typeMapping.read(
DriftSqlType.dateTime,
data['${effectivePrefix}acknowledge_by_server_at']),
@ -4323,6 +4342,7 @@ class MessageRetransmission extends DataClass
final int? messageId;
final Uint8List plaintextContent;
final Uint8List? pushData;
final bool willNotGetACKByUser;
final DateTime? acknowledgeByServerAt;
const MessageRetransmission(
{required this.retransmissionId,
@ -4330,6 +4350,7 @@ class MessageRetransmission extends DataClass
this.messageId,
required this.plaintextContent,
this.pushData,
required this.willNotGetACKByUser,
this.acknowledgeByServerAt});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
@ -4343,6 +4364,7 @@ class MessageRetransmission extends DataClass
if (!nullToAbsent || pushData != null) {
map['push_data'] = Variable<Uint8List>(pushData);
}
map['will_not_get_a_c_k_by_user'] = Variable<bool>(willNotGetACKByUser);
if (!nullToAbsent || acknowledgeByServerAt != null) {
map['acknowledge_by_server_at'] =
Variable<DateTime>(acknowledgeByServerAt);
@ -4361,6 +4383,7 @@ class MessageRetransmission extends DataClass
pushData: pushData == null && nullToAbsent
? const Value.absent()
: Value(pushData),
willNotGetACKByUser: Value(willNotGetACKByUser),
acknowledgeByServerAt: acknowledgeByServerAt == null && nullToAbsent
? const Value.absent()
: Value(acknowledgeByServerAt),
@ -4377,6 +4400,8 @@ class MessageRetransmission extends DataClass
plaintextContent:
serializer.fromJson<Uint8List>(json['plaintextContent']),
pushData: serializer.fromJson<Uint8List?>(json['pushData']),
willNotGetACKByUser:
serializer.fromJson<bool>(json['willNotGetACKByUser']),
acknowledgeByServerAt:
serializer.fromJson<DateTime?>(json['acknowledgeByServerAt']),
);
@ -4390,6 +4415,7 @@ class MessageRetransmission extends DataClass
'messageId': serializer.toJson<int?>(messageId),
'plaintextContent': serializer.toJson<Uint8List>(plaintextContent),
'pushData': serializer.toJson<Uint8List?>(pushData),
'willNotGetACKByUser': serializer.toJson<bool>(willNotGetACKByUser),
'acknowledgeByServerAt':
serializer.toJson<DateTime?>(acknowledgeByServerAt),
};
@ -4401,6 +4427,7 @@ class MessageRetransmission extends DataClass
Value<int?> messageId = const Value.absent(),
Uint8List? plaintextContent,
Value<Uint8List?> pushData = const Value.absent(),
bool? willNotGetACKByUser,
Value<DateTime?> acknowledgeByServerAt = const Value.absent()}) =>
MessageRetransmission(
retransmissionId: retransmissionId ?? this.retransmissionId,
@ -4408,6 +4435,7 @@ class MessageRetransmission extends DataClass
messageId: messageId.present ? messageId.value : this.messageId,
plaintextContent: plaintextContent ?? this.plaintextContent,
pushData: pushData.present ? pushData.value : this.pushData,
willNotGetACKByUser: willNotGetACKByUser ?? this.willNotGetACKByUser,
acknowledgeByServerAt: acknowledgeByServerAt.present
? acknowledgeByServerAt.value
: this.acknowledgeByServerAt,
@ -4424,6 +4452,9 @@ class MessageRetransmission extends DataClass
? data.plaintextContent.value
: this.plaintextContent,
pushData: data.pushData.present ? data.pushData.value : this.pushData,
willNotGetACKByUser: data.willNotGetACKByUser.present
? data.willNotGetACKByUser.value
: this.willNotGetACKByUser,
acknowledgeByServerAt: data.acknowledgeByServerAt.present
? data.acknowledgeByServerAt.value
: this.acknowledgeByServerAt,
@ -4438,6 +4469,7 @@ class MessageRetransmission extends DataClass
..write('messageId: $messageId, ')
..write('plaintextContent: $plaintextContent, ')
..write('pushData: $pushData, ')
..write('willNotGetACKByUser: $willNotGetACKByUser, ')
..write('acknowledgeByServerAt: $acknowledgeByServerAt')
..write(')'))
.toString();
@ -4450,6 +4482,7 @@ class MessageRetransmission extends DataClass
messageId,
$driftBlobEquality.hash(plaintextContent),
$driftBlobEquality.hash(pushData),
willNotGetACKByUser,
acknowledgeByServerAt);
@override
bool operator ==(Object other) =>
@ -4461,6 +4494,7 @@ class MessageRetransmission extends DataClass
$driftBlobEquality.equals(
other.plaintextContent, this.plaintextContent) &&
$driftBlobEquality.equals(other.pushData, this.pushData) &&
other.willNotGetACKByUser == this.willNotGetACKByUser &&
other.acknowledgeByServerAt == this.acknowledgeByServerAt);
}
@ -4471,6 +4505,7 @@ class MessageRetransmissionsCompanion
final Value<int?> messageId;
final Value<Uint8List> plaintextContent;
final Value<Uint8List?> pushData;
final Value<bool> willNotGetACKByUser;
final Value<DateTime?> acknowledgeByServerAt;
const MessageRetransmissionsCompanion({
this.retransmissionId = const Value.absent(),
@ -4478,6 +4513,7 @@ class MessageRetransmissionsCompanion
this.messageId = const Value.absent(),
this.plaintextContent = const Value.absent(),
this.pushData = const Value.absent(),
this.willNotGetACKByUser = const Value.absent(),
this.acknowledgeByServerAt = const Value.absent(),
});
MessageRetransmissionsCompanion.insert({
@ -4486,6 +4522,7 @@ class MessageRetransmissionsCompanion
this.messageId = const Value.absent(),
required Uint8List plaintextContent,
this.pushData = const Value.absent(),
this.willNotGetACKByUser = const Value.absent(),
this.acknowledgeByServerAt = const Value.absent(),
}) : contactId = Value(contactId),
plaintextContent = Value(plaintextContent);
@ -4495,6 +4532,7 @@ class MessageRetransmissionsCompanion
Expression<int>? messageId,
Expression<Uint8List>? plaintextContent,
Expression<Uint8List>? pushData,
Expression<bool>? willNotGetACKByUser,
Expression<DateTime>? acknowledgeByServerAt,
}) {
return RawValuesInsertable({
@ -4503,6 +4541,8 @@ class MessageRetransmissionsCompanion
if (messageId != null) 'message_id': messageId,
if (plaintextContent != null) 'plaintext_content': plaintextContent,
if (pushData != null) 'push_data': pushData,
if (willNotGetACKByUser != null)
'will_not_get_a_c_k_by_user': willNotGetACKByUser,
if (acknowledgeByServerAt != null)
'acknowledge_by_server_at': acknowledgeByServerAt,
});
@ -4514,6 +4554,7 @@ class MessageRetransmissionsCompanion
Value<int?>? messageId,
Value<Uint8List>? plaintextContent,
Value<Uint8List?>? pushData,
Value<bool>? willNotGetACKByUser,
Value<DateTime?>? acknowledgeByServerAt}) {
return MessageRetransmissionsCompanion(
retransmissionId: retransmissionId ?? this.retransmissionId,
@ -4521,6 +4562,7 @@ class MessageRetransmissionsCompanion
messageId: messageId ?? this.messageId,
plaintextContent: plaintextContent ?? this.plaintextContent,
pushData: pushData ?? this.pushData,
willNotGetACKByUser: willNotGetACKByUser ?? this.willNotGetACKByUser,
acknowledgeByServerAt:
acknowledgeByServerAt ?? this.acknowledgeByServerAt,
);
@ -4544,6 +4586,10 @@ class MessageRetransmissionsCompanion
if (pushData.present) {
map['push_data'] = Variable<Uint8List>(pushData.value);
}
if (willNotGetACKByUser.present) {
map['will_not_get_a_c_k_by_user'] =
Variable<bool>(willNotGetACKByUser.value);
}
if (acknowledgeByServerAt.present) {
map['acknowledge_by_server_at'] =
Variable<DateTime>(acknowledgeByServerAt.value);
@ -4559,6 +4605,7 @@ class MessageRetransmissionsCompanion
..write('messageId: $messageId, ')
..write('plaintextContent: $plaintextContent, ')
..write('pushData: $pushData, ')
..write('willNotGetACKByUser: $willNotGetACKByUser, ')
..write('acknowledgeByServerAt: $acknowledgeByServerAt')
..write(')'))
.toString();
@ -7107,6 +7154,7 @@ typedef $$MessageRetransmissionsTableCreateCompanionBuilder
Value<int?> messageId,
required Uint8List plaintextContent,
Value<Uint8List?> pushData,
Value<bool> willNotGetACKByUser,
Value<DateTime?> acknowledgeByServerAt,
});
typedef $$MessageRetransmissionsTableUpdateCompanionBuilder
@ -7116,6 +7164,7 @@ typedef $$MessageRetransmissionsTableUpdateCompanionBuilder
Value<int?> messageId,
Value<Uint8List> plaintextContent,
Value<Uint8List?> pushData,
Value<bool> willNotGetACKByUser,
Value<DateTime?> acknowledgeByServerAt,
});
@ -7175,6 +7224,10 @@ class $$MessageRetransmissionsTableFilterComposer
ColumnFilters<Uint8List> get pushData => $composableBuilder(
column: $table.pushData, builder: (column) => ColumnFilters(column));
ColumnFilters<bool> get willNotGetACKByUser => $composableBuilder(
column: $table.willNotGetACKByUser,
builder: (column) => ColumnFilters(column));
ColumnFilters<DateTime> get acknowledgeByServerAt => $composableBuilder(
column: $table.acknowledgeByServerAt,
builder: (column) => ColumnFilters(column));
@ -7240,6 +7293,10 @@ class $$MessageRetransmissionsTableOrderingComposer
ColumnOrderings<Uint8List> get pushData => $composableBuilder(
column: $table.pushData, builder: (column) => ColumnOrderings(column));
ColumnOrderings<bool> get willNotGetACKByUser => $composableBuilder(
column: $table.willNotGetACKByUser,
builder: (column) => ColumnOrderings(column));
ColumnOrderings<DateTime> get acknowledgeByServerAt => $composableBuilder(
column: $table.acknowledgeByServerAt,
builder: (column) => ColumnOrderings(column));
@ -7303,6 +7360,9 @@ class $$MessageRetransmissionsTableAnnotationComposer
GeneratedColumn<Uint8List> get pushData =>
$composableBuilder(column: $table.pushData, builder: (column) => column);
GeneratedColumn<bool> get willNotGetACKByUser => $composableBuilder(
column: $table.willNotGetACKByUser, builder: (column) => column);
GeneratedColumn<DateTime> get acknowledgeByServerAt => $composableBuilder(
column: $table.acknowledgeByServerAt, builder: (column) => column);
@ -7379,6 +7439,7 @@ class $$MessageRetransmissionsTableTableManager extends RootTableManager<
Value<int?> messageId = const Value.absent(),
Value<Uint8List> plaintextContent = const Value.absent(),
Value<Uint8List?> pushData = const Value.absent(),
Value<bool> willNotGetACKByUser = const Value.absent(),
Value<DateTime?> acknowledgeByServerAt = const Value.absent(),
}) =>
MessageRetransmissionsCompanion(
@ -7387,6 +7448,7 @@ class $$MessageRetransmissionsTableTableManager extends RootTableManager<
messageId: messageId,
plaintextContent: plaintextContent,
pushData: pushData,
willNotGetACKByUser: willNotGetACKByUser,
acknowledgeByServerAt: acknowledgeByServerAt,
),
createCompanionCallback: ({
@ -7395,6 +7457,7 @@ class $$MessageRetransmissionsTableTableManager extends RootTableManager<
Value<int?> messageId = const Value.absent(),
required Uint8List plaintextContent,
Value<Uint8List?> pushData = const Value.absent(),
Value<bool> willNotGetACKByUser = const Value.absent(),
Value<DateTime?> acknowledgeByServerAt = const Value.absent(),
}) =>
MessageRetransmissionsCompanion.insert(
@ -7403,6 +7466,7 @@ class $$MessageRetransmissionsTableTableManager extends RootTableManager<
messageId: messageId,
plaintextContent: plaintextContent,
pushData: pushData,
willNotGetACKByUser: willNotGetACKByUser,
acknowledgeByServerAt: acknowledgeByServerAt,
),
withReferenceMapper: (p0) => p0

View file

@ -2465,6 +2465,263 @@ i1.GeneratedColumn<i2.Uint8List> _column_66(String aliasedName) =>
i1.GeneratedColumn<DateTime> _column_67(String aliasedName) =>
i1.GeneratedColumn<DateTime>('acknowledge_by_server_at', aliasedName, true,
type: i1.DriftSqlType.dateTime);
final class Schema12 extends i0.VersionedSchema {
Schema12({required super.database}) : super(version: 12);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
contacts,
messages,
mediaUploads,
mediaDownloads,
signalIdentityKeyStores,
signalPreKeyStores,
signalSenderKeyStores,
signalSessionStores,
signalContactPreKeys,
signalContactSignedPreKeys,
messageRetransmissions,
];
late final Shape13 contacts = Shape13(
source: i0.VersionedTable(
entityName: 'contacts',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(user_id)',
],
columns: [
_column_0,
_column_1,
_column_2,
_column_3,
_column_4,
_column_5,
_column_6,
_column_7,
_column_8,
_column_9,
_column_39,
_column_53,
_column_57,
_column_54,
_column_40,
_column_10,
_column_11,
_column_12,
_column_13,
_column_14,
_column_55,
_column_15,
_column_16,
],
attachedDatabase: database,
),
alias: null);
late final Shape10 messages = Shape10(
source: i0.VersionedTable(
entityName: 'messages',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_17,
_column_18,
_column_19,
_column_48,
_column_49,
_column_20,
_column_21,
_column_22,
_column_52,
_column_23,
_column_24,
_column_25,
_column_26,
_column_27,
_column_28,
_column_29,
_column_30,
],
attachedDatabase: database,
),
alias: null);
late final Shape7 mediaUploads = Shape7(
source: i0.VersionedTable(
entityName: 'media_uploads',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_41,
_column_42,
_column_56,
_column_44,
_column_45,
_column_46,
_column_47,
],
attachedDatabase: database,
),
alias: null);
late final Shape9 mediaDownloads = Shape9(
source: i0.VersionedTable(
entityName: 'media_downloads',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_50,
_column_51,
],
attachedDatabase: database,
),
alias: null);
late final Shape2 signalIdentityKeyStores = Shape2(
source: i0.VersionedTable(
entityName: 'signal_identity_key_stores',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(device_id, name)',
],
columns: [
_column_31,
_column_32,
_column_33,
_column_10,
],
attachedDatabase: database,
),
alias: null);
late final Shape3 signalPreKeyStores = Shape3(
source: i0.VersionedTable(
entityName: 'signal_pre_key_stores',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(pre_key_id)',
],
columns: [
_column_34,
_column_35,
_column_10,
],
attachedDatabase: database,
),
alias: null);
late final Shape4 signalSenderKeyStores = Shape4(
source: i0.VersionedTable(
entityName: 'signal_sender_key_stores',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(sender_key_name)',
],
columns: [
_column_36,
_column_37,
],
attachedDatabase: database,
),
alias: null);
late final Shape5 signalSessionStores = Shape5(
source: i0.VersionedTable(
entityName: 'signal_session_stores',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(device_id, name)',
],
columns: [
_column_31,
_column_32,
_column_38,
_column_10,
],
attachedDatabase: database,
),
alias: null);
late final Shape14 signalContactPreKeys = Shape14(
source: i0.VersionedTable(
entityName: 'signal_contact_pre_keys',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(contact_id, pre_key_id)',
],
columns: [
_column_58,
_column_34,
_column_35,
_column_10,
],
attachedDatabase: database,
),
alias: null);
late final Shape15 signalContactSignedPreKeys = Shape15(
source: i0.VersionedTable(
entityName: 'signal_contact_signed_pre_keys',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(contact_id)',
],
columns: [
_column_58,
_column_59,
_column_60,
_column_61,
_column_10,
],
attachedDatabase: database,
),
alias: null);
late final Shape17 messageRetransmissions = Shape17(
source: i0.VersionedTable(
entityName: 'message_retransmissions',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_62,
_column_63,
_column_64,
_column_65,
_column_66,
_column_68,
_column_67,
],
attachedDatabase: database,
),
alias: null);
}
class Shape17 extends i0.VersionedTable {
Shape17({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get retransmissionId =>
columnsByName['retransmission_id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get contactId =>
columnsByName['contact_id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get messageId =>
columnsByName['message_id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<i2.Uint8List> get plaintextContent =>
columnsByName['plaintext_content']! as i1.GeneratedColumn<i2.Uint8List>;
i1.GeneratedColumn<i2.Uint8List> get pushData =>
columnsByName['push_data']! as i1.GeneratedColumn<i2.Uint8List>;
i1.GeneratedColumn<bool> get willNotGetACKByUser =>
columnsByName['will_not_get_a_c_k_by_user']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<DateTime> get acknowledgeByServerAt =>
columnsByName['acknowledge_by_server_at']!
as i1.GeneratedColumn<DateTime>;
}
i1.GeneratedColumn<bool> _column_68(String aliasedName) =>
i1.GeneratedColumn<bool>('will_not_get_a_c_k_by_user', aliasedName, false,
type: i1.DriftSqlType.bool,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'CHECK ("will_not_get_a_c_k_by_user" IN (0, 1))'),
defaultValue: const CustomExpression('0'));
i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
@ -2476,6 +2733,7 @@ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9,
required Future<void> Function(i1.Migrator m, Schema10 schema) from9To10,
required Future<void> Function(i1.Migrator m, Schema11 schema) from10To11,
required Future<void> Function(i1.Migrator m, Schema12 schema) from11To12,
}) {
return (currentVersion, database) async {
switch (currentVersion) {
@ -2529,6 +2787,11 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema);
await from10To11(migrator, schema);
return 11;
case 11:
final schema = Schema12(database: database);
final migrator = i1.Migrator(database, schema);
await from11To12(migrator, schema);
return 12;
default:
throw ArgumentError.value('Unknown migration from $currentVersion');
}
@ -2546,6 +2809,7 @@ i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9,
required Future<void> Function(i1.Migrator m, Schema10 schema) from9To10,
required Future<void> Function(i1.Migrator m, Schema11 schema) from10To11,
required Future<void> Function(i1.Migrator m, Schema12 schema) from11To12,
}) =>
i0.VersionedSchema.stepByStepHelper(
step: migrationSteps(
@ -2559,4 +2823,5 @@ i1.OnUpgrade stepByStep({
from8To9: from8To9,
from9To10: from9To10,
from10To11: from10To11,
from11To12: from11To12,
));

View file

@ -37,11 +37,13 @@ class MessageJson {
final MessageKind kind;
final MessageContent? content;
final int? messageId;
int? retransId;
DateTime timestamp;
MessageJson({
required this.kind,
this.messageId,
this.retransId,
required this.content,
required this.timestamp,
});
@ -57,6 +59,7 @@ class MessageJson {
return MessageJson(
kind: kind,
messageId: (json['messageId'] as num?)?.toInt(),
retransId: (json['retransId'] as num?)?.toInt(),
content: MessageContent.fromJson(
kind, json['content'] as Map<String, dynamic>),
timestamp: DateTime.fromMillisecondsSinceEpoch(json['timestamp']),
@ -67,6 +70,7 @@ class MessageJson {
'kind': kind.name,
'content': content?.toJson(),
'messageId': messageId,
'retransId': retransId,
'timestamp': timestamp.toUtc().millisecondsSinceEpoch,
};
}
@ -90,6 +94,8 @@ class MessageContent {
return ReopenedMediaFileContent.fromJson(json);
case MessageKind.flameSync:
return FlameSyncContent.fromJson(json);
case MessageKind.ack:
return AckContent.fromJson(json);
default:
return null;
}
@ -216,6 +222,27 @@ class ReopenedMediaFileContent extends MessageContent {
}
}
class AckContent extends MessageContent {
int? messageIdToAck;
int retransIdToAck;
AckContent({required this.messageIdToAck, required this.retransIdToAck});
static AckContent fromJson(Map json) {
return AckContent(
messageIdToAck: json['messageIdToAck'],
retransIdToAck: json['retransIdToAck'],
);
}
@override
Map toJson() {
return {
'messageIdToAck': messageIdToAck,
'retransIdToAck': retransIdToAck,
};
}
}
class ProfileContent extends MessageContent {
String avatarSvg;
String displayName;

View file

@ -62,6 +62,14 @@ class UserData {
DateTime? signalLastSignedPreKeyUpdated;
// -- Custom DATA --
@JsonKey(defaultValue: 100_000)
int currentPreKeyIndexStart = 100_000;
@JsonKey(defaultValue: 100_000)
int currentSignedPreKeyIndexStart = 100_000;
// --- BACKUP ---
DateTime? nextTimeToShowBackupNotice;

View file

@ -48,6 +48,10 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
json['signalLastSignedPreKeyUpdated'] == null
? null
: DateTime.parse(json['signalLastSignedPreKeyUpdated'] as String)
..currentPreKeyIndexStart =
(json['currentPreKeyIndexStart'] as num?)?.toInt() ?? 100000
..currentSignedPreKeyIndexStart =
(json['currentSignedPreKeyIndexStart'] as num?)?.toInt() ?? 100000
..nextTimeToShowBackupNotice = json['nextTimeToShowBackupNotice'] == null
? null
: DateTime.parse(json['nextTimeToShowBackupNotice'] as String)
@ -83,6 +87,8 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'myBestFriendContactId': instance.myBestFriendContactId,
'signalLastSignedPreKeyUpdated':
instance.signalLastSignedPreKeyUpdated?.toIso8601String(),
'currentPreKeyIndexStart': instance.currentPreKeyIndexStart,
'currentSignedPreKeyIndexStart': instance.currentSignedPreKeyIndexStart,
'nextTimeToShowBackupNotice':
instance.nextTimeToShowBackupNotice?.toIso8601String(),
'backupServer': instance.backupServer,

View file

@ -1,7 +1,6 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:cryptography_plus/cryptography_plus.dart';
import 'package:drift/drift.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly_database.dart';
@ -9,60 +8,23 @@ import 'package:twonly/src/database/tables/messages_table.dart';
import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/model/json/userdata.dart';
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
import 'package:twonly/src/services/api/media_upload.dart';
import 'package:twonly/src/services/api/utils.dart';
import 'package:twonly/src/services/signal/encryption.signal.dart';
import 'package:twonly/src/services/notification.service.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/storage.dart';
class DirtyResendingItem {
DirtyResendingItem({required this.gotLastAck});
DateTime gotLastAck;
Timer? timer;
}
class DirtyResending {
static final Map<int, DirtyResendingItem> _gotLastAck = {};
static Future gotAckFromUser(int contactID) async {
_gotLastAck[contactID]?.timer?.cancel();
_gotLastAck[contactID] = DirtyResendingItem(gotLastAck: DateTime.now());
_gotLastAck[contactID]?.timer = Timer(Duration(seconds: 10), () async {
_gotLastAck.remove(contactID);
_handleNonACKMessagesForUser(contactID);
});
}
static Future _handleNonACKMessagesForUser(int contactID) async {
final List<Message> toResendMessages =
await twonlyDB.messagesDao.getAllNonACKMessagesFromUser();
for (final Message message in toResendMessages) {
Log.info("Got newer ACKs from user ${message.messageId}");
await twonlyDB.messagesDao.updateMessageByMessageId(
message.messageId,
MessagesCompanion(
errorWhileSending: Value(true),
),
);
}
}
}
Future handleOlderNonAckMessages() async {}
Future tryTransmitMessages() async {
final retransIds =
await twonlyDB.messageRetransmissionDao.getRetransmitAbleMessages();
if (retransIds.isEmpty) return;
Log.info("Retransmitting ${retransIds.length} text messages");
if (retransIds.isEmpty) return;
for (final retransId in retransIds) {
sendRetransmitMessage(retransId);
//twonlyDB.messageRetransmissionDao.deleteRetransmissionById(retransId);
}
}
@ -100,10 +62,6 @@ Future sendRetransmitMessage(int retransId) async {
return;
}
var hash = uint8ListToHex(
Uint8List.fromList((await Sha256().hash(encryptedBytes)).bytes));
Log.info("Sending message: ${hash.substring(0, 10)}");
Result resp = await apiService.sendTextMessage(
retrans.contactId,
encryptedBytes,
@ -142,13 +100,23 @@ Future sendRetransmitMessage(int retransId) async {
}
if (!retry) {
await twonlyDB.messageRetransmissionDao.deleteRetransmissionById(retransId);
if (retrans.willNotGetACKByUser) {
await twonlyDB.messageRetransmissionDao
.deleteRetransmissionById(retransId);
} else {
await twonlyDB.messageRetransmissionDao.updateRetransmission(
retransId,
MessageRetransmissionsCompanion(
acknowledgeByServerAt: Value(DateTime.now()),
),
);
}
}
}
// encrypts and stores the message and then sends it in the background
Future encryptAndSendMessageAsync(int? messageId, int userId, MessageJson msg,
{PushKind? pushKind}) async {
{PushKind? pushKind, bool willNotGetACKByUser = false}) async {
if (gIsDemoUser) {
return;
}
@ -158,15 +126,13 @@ Future encryptAndSendMessageAsync(int? messageId, int userId, MessageJson msg,
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),
plaintextContent: Value(Uint8List(0)),
pushData: Value(pushData),
willNotGetACKByUser: Value(willNotGetACKByUser),
),
);
@ -175,6 +141,16 @@ Future encryptAndSendMessageAsync(int? messageId, int userId, MessageJson msg,
return;
}
msg.retransId = retransId;
Uint8List plaintextContent =
Uint8List.fromList(gzip.encode(utf8.encode(jsonEncode(msg.toJson()))));
await twonlyDB.messageRetransmissionDao.updateRetransmission(
retransId,
MessageRetransmissionsCompanion(
plaintextContent: Value(plaintextContent)));
// this can now be done in the background...
sendRetransmitMessage(retransId);
}

View file

@ -1,5 +1,4 @@
import 'dart:convert';
import 'package:cryptography_plus/cryptography_plus.dart';
import 'package:drift/drift.dart';
import 'package:fixnum/fixnum.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
@ -14,13 +13,13 @@ import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pbserve
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'
as server;
import 'package:twonly/src/services/api/media_upload.dart';
import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/api/utils.dart';
import 'package:twonly/src/services/api/media_download.dart';
import 'package:twonly/src/services/notification.service.dart';
import 'package:twonly/src/services/signal/encryption.signal.dart';
import 'package:twonly/src/services/signal/identity.signal.dart';
import 'package:twonly/src/services/signal/prekeys.signal.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
@ -36,9 +35,6 @@ Future handleServerMessage(server.ServerToClient msg) async {
} else if (msg.v0.hasNewMessage()) {
Uint8List body = Uint8List.fromList(msg.v0.newMessage.body);
int fromUserId = msg.v0.newMessage.fromUserId.toInt();
var hash = uint8ListToHex(Uint8List.fromList(
(await Sha256().hash(msg.v0.newMessage.body)).bytes));
Log.info("Got new message from server: ${hash.substring(0, 10)}");
response = await handleNewMessage(fromUserId, body);
} else {
Log.error("Got a new message from the server: $msg");
@ -56,9 +52,24 @@ Future handleServerMessage(server.ServerToClient msg) async {
});
}
DateTime lastSignalDecryptMessage = DateTime.now().subtract(Duration(hours: 1));
DateTime lastPushKeyRequest = DateTime.now().subtract(Duration(hours: 1));
Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
MessageJson? message = await signalDecryptMessage(fromUserId, body);
if (message == null) {
await encryptAndSendMessageAsync(
null,
fromUserId,
MessageJson(
kind: MessageKind.signalDecryptError,
content: MessageContent(),
timestamp: DateTime.now(),
),
);
Log.error("Could not decrypt others message!");
// Message is not valid, so server can delete it
var ok = client.Response_Ok()..none = true;
return client.Response()..ok = ok;
@ -66,7 +77,58 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
Log.info("Got: ${message.kind}");
if (message.kind != MessageKind.ack && message.retransId != null) {
Log.info("Sending ACK for ${message.kind}");
/// ACK every message
await encryptAndSendMessageAsync(
null,
fromUserId,
MessageJson(
kind: MessageKind.ack,
messageId: null,
content: AckContent(
messageIdToAck: message.messageId,
retransIdToAck: message.retransId!),
timestamp: DateTime.now(),
),
willNotGetACKByUser: true,
);
}
switch (message.kind) {
case MessageKind.ack:
final content = message.content;
if (content is AckContent) {
if (content.messageIdToAck != null) {
final update = MessagesCompanion(
acknowledgeByUser: Value(true),
errorWhileSending: Value(false),
);
await twonlyDB.messagesDao.updateMessageByOtherUser(
fromUserId,
content.messageIdToAck!,
update,
);
}
await twonlyDB.messageRetransmissionDao
.deleteRetransmissionById(content.retransIdToAck);
}
break;
case MessageKind.signalDecryptError:
if (lastSignalDecryptMessage
.isBefore(DateTime.now().subtract(Duration(seconds: 60)))) {
Log.error(
"Got signal decrypt error from other user! Sending all non ACK messages again.");
lastSignalDecryptMessage = DateTime.now();
await twonlyDB.signalDao.deleteAllPreKeysByContactId(fromUserId);
await requestNewPrekeysForContact(fromUserId);
await twonlyDB.messageRetransmissionDao.resetAckStatusForAllMessages();
tryTransmitMessages();
}
break;
case MessageKind.contactRequest:
return handleContactRequest(fromUserId, message);
@ -148,21 +210,12 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
}
break;
case MessageKind.ack:
final update = MessagesCompanion(
acknowledgeByUser: Value(true),
errorWhileSending: Value(false),
);
await twonlyDB.messagesDao.updateMessageByOtherUser(
fromUserId,
message.messageId!,
update,
);
// search for older messages, that where not yet ack by the other party
DirtyResending.gotAckFromUser(fromUserId);
break;
case MessageKind.requestPushKey:
if (lastPushKeyRequest
.isBefore(DateTime.now().subtract(Duration(seconds: 60)))) {
lastPushKeyRequest = DateTime.now();
setupNotificationWithUsers(force: true);
}
case MessageKind.pushKey:
if (message.content != null) {
@ -277,17 +330,6 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
}
}
await encryptAndSendMessageAsync(
null,
fromUserId,
MessageJson(
kind: MessageKind.ack,
messageId: message.messageId!,
content: MessageContent(),
timestamp: DateTime.now(),
),
);
// unarchive contact when receiving a new message
await twonlyDB.contactsDao.updateContact(
fromUserId,

View file

@ -254,6 +254,15 @@ Future<Uint8List?> getPushData(int toUserId, PushKind kind) async {
// this will be enforced after every app uses this system... :/
// return null;
Log.error("Using insecure key as the receiver does not send a push key!");
await encryptAndSendMessageAsync(
null,
toUserId,
my.MessageJson(
kind: MessageKind.requestPushKey,
content: my.MessageContent(),
timestamp: DateTime.now(),
),
);
}
} else {
try {

View file

@ -3,7 +3,6 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/constants/secure_storage_keys.dart';
import 'package:twonly/src/database/signal/connect_pre_key_store.dart';
import 'package:twonly/src/model/json/signal_identity.dart';
import 'package:twonly/src/database/signal/connect_signal_protocol_store.dart';
import 'package:twonly/src/model/json/userdata.dart';
@ -59,9 +58,14 @@ Future signalHandleNewServerConnection() async {
}
Future<List<PreKeyRecord>> signalGetPreKeys() async {
int? start = await ConnectPreKeyStore.getNextPreKeyId();
if (start == null) return [];
print(start);
final user = await getUser();
if (user == null) return [];
int start = user.currentPreKeyIndexStart;
await updateUserdata((user) {
user.currentPreKeyIndexStart += 200;
return user;
});
final preKeys = generatePreKeys(start, 200);
final signalStore = await getSignalStore();
if (signalStore == null) return [];
@ -123,11 +127,17 @@ Future createIfNotExistsSignalIdentity() async {
Future<SignedPreKeyRecord?> _getNewSignalSignedPreKey() async {
var identityKeyPair = await getSignalIdentityKeyPair();
if (identityKeyPair == null) return null;
final user = await getUser();
final signalStore = await getSignalStore();
if (signalStore == null) return null;
if (identityKeyPair == null || signalStore == null || user == null) {
return null;
}
int signedPreKeyId = await signalStore.signedPreKeyStore.getNextKeyId();
int signedPreKeyId = user.currentSignedPreKeyIndexStart;
await updateUserdata((user) {
user.currentSignedPreKeyIndexStart += 1;
return user;
});
final SignedPreKeyRecord signedPreKey = generateSignedPreKey(
identityKeyPair,

View file

@ -1,4 +1,5 @@
import 'package:drift/drift.dart';
import 'package:mutex/mutex.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/utils/log.dart';
@ -18,10 +19,22 @@ class OtherPreKeys {
final List<int> signedPreKeySignature;
}
Mutex requestNewKeys = Mutex();
DateTime lastPreKeyRequest = DateTime.now().subtract(Duration(hours: 1));
DateTime lastSignedPreKeyRequest = DateTime.now().subtract(Duration(hours: 1));
Future requestNewPrekeysForContact(int contactId) async {
if (lastPreKeyRequest
.isAfter(DateTime.now().subtract(Duration(seconds: 60)))) {
Log.info("last pre request was 60s before");
return;
}
lastPreKeyRequest = DateTime.now();
requestNewKeys.protect(() async {
final otherKeys = await apiService.getPreKeysByUserId(contactId);
if (otherKeys != null) {
Log.info("got fresh pre keys from other $contactId!");
Log.info(
"got fresh ${otherKeys.preKeys.length} pre keys from other $contactId!");
final preKeys = otherKeys.preKeys
.map(
(preKey) => SignalContactPreKeysCompanion(
@ -35,20 +48,26 @@ Future requestNewPrekeysForContact(int contactId) async {
} else {
Log.error("could not load new pre keys for user $contactId");
}
});
}
Future<SignalContactPreKey?> getPreKeyByContactId(int contactId) async {
int count = await twonlyDB.signalDao.countPreKeysByContactId(contactId);
if (count < 10) {
Log.info(
"There are $count < 10 prekeys for $contactId. Loading fresh once from the server.",
);
Log.info("Requesting new prekeys: $count < 10");
requestNewPrekeysForContact(contactId);
}
return twonlyDB.signalDao.popPreKeyByContactId(contactId);
}
Future requestNewSignedPreKeyForContact(int contactId) async {
if (lastSignedPreKeyRequest
.isAfter(DateTime.now().subtract(Duration(seconds: 60)))) {
Log.info("last signed pre request was 60s before");
return;
}
lastSignedPreKeyRequest = DateTime.now();
await requestNewKeys.protect(() async {
final signedPreKey = await apiService.getSignedKeyByUserId(contactId);
if (signedPreKey != null) {
Log.info("got fresh signed pre keys from other $contactId!");
@ -63,6 +82,7 @@ Future requestNewSignedPreKeyForContact(int contactId) async {
} else {
Log.error("could not load new signed pre key for user $contactId");
}
});
}
Future<SignalContactSignedPreKey?> getSignedPreKeyByContactId(

View file

@ -1,212 +0,0 @@
import 'dart:convert';
import 'dart:io';
import 'package:background_downloader/background_downloader.dart';
import 'package:cryptography_plus/cryptography_plus.dart';
import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:twonly/src/constants/secure_storage_keys.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/model/json/userdata.dart';
import 'package:twonly/src/model/protobuf/backup/backup.pb.dart';
import 'package:twonly/src/services/api/media_upload.dart';
import 'package:twonly/src/services/twonly_safe/common.twonly_safe.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/settings/backup/backup.view.dart';
Future performTwonlySafeBackup({bool force = false}) async {
final user = await getUser();
if (user == null || user.twonlySafeBackup == null || user.isDemoUser) {
Log.warn("perform twonly safe backup was called while it is disabled");
return;
}
if (user.twonlySafeBackup!.backupUploadState ==
LastBackupUploadState.pending) {
Log.warn("Backup upload is already pending.");
return;
}
DateTime? lastUpdateTime = user.twonlySafeBackup!.lastBackupDone;
if (!force && lastUpdateTime != null) {
if (lastUpdateTime.isAfter(DateTime.now().subtract(Duration(days: 1)))) {
return;
}
}
Log.info("Starting new twonly Safe-Backup.");
final baseDir = (await getApplicationSupportDirectory()).path;
final backupDir = Directory(join(baseDir, "backup_twonly_safe/"));
await backupDir.create(recursive: true);
final backupDatabaseFile =
File(join(backupDir.path, "twonly_database.backup.sqlite"));
// copy database
final originalDatabase = File(join(baseDir, "twonly_database.sqlite"));
await originalDatabase.copy(backupDatabaseFile.path);
driftRuntimeOptions.dontWarnAboutMultipleDatabases = true;
final backupDB = TwonlyDatabase(
driftDatabase(
name: "twonly_database.backup",
native: DriftNativeOptions(
databaseDirectory: () async {
return backupDir;
},
),
),
);
await backupDB.deleteDataForTwonlySafe();
var secureStorageBackup = {};
final storage = FlutterSecureStorage();
secureStorageBackup[SecureStorageKeys.signalIdentity] =
await storage.read(key: SecureStorageKeys.signalIdentity);
secureStorageBackup[SecureStorageKeys.signalSignedPreKey] =
await storage.read(key: SecureStorageKeys.signalSignedPreKey);
var userBackup = await getUser();
if (userBackup == null) return;
// FILTER settings which should not be in the backup
userBackup.twonlySafeBackup = null;
userBackup.lastImageSend = null;
userBackup.todaysImageCounter = null;
userBackup.lastPlanBallance = "";
userBackup.additionalUserInvites = "";
userBackup.signalLastSignedPreKeyUpdated = null;
secureStorageBackup[SecureStorageKeys.userData] = jsonEncode(userBackup);
// Compress and convert backup data
final twonlyDatabaseBytes = await backupDatabaseFile.readAsBytes();
await backupDatabaseFile.delete();
final backupProto = TwonlySafeBackupContent(
secureStorageJson: jsonEncode(secureStorageBackup),
twonlyDatabase: twonlyDatabaseBytes,
);
final backupBytes = gzip.encode(backupProto.writeToBuffer());
final backupHash = uint8ListToHex((await Sha256().hash(backupBytes)).bytes);
if (user.twonlySafeBackup!.lastBackupDone == null ||
user.twonlySafeBackup!.lastBackupDone!
.isAfter(DateTime.now().subtract(Duration(days: 90)))) {
force = true;
}
final lastHash =
await storage.read(key: SecureStorageKeys.twonlySafeLastBackupHash);
if (lastHash != null && !force) {
if (backupHash == lastHash) {
Log.info("Since last backup nothing has changed.");
return;
}
}
await storage.write(
key: SecureStorageKeys.twonlySafeLastBackupHash,
value: backupHash,
);
// Encrypt backup data
final xchacha20 = Xchacha20.poly1305Aead();
final nonce = xchacha20.newNonce();
final secretBox = await xchacha20.encrypt(
backupBytes,
secretKey: SecretKey(user.twonlySafeBackup!.encryptionKey),
nonce: nonce,
);
final encryptedBackupBytes = (TwonlySafeBackupEncrypted(
mac: secretBox.mac.bytes,
nonce: nonce,
cipherText: secretBox.cipherText,
)).writeToBuffer();
Log.info("Backup files created.");
var encryptedBackupBytesFile =
File(join(backupDir.path, "twonly_safe.backup"));
await encryptedBackupBytesFile.writeAsBytes(encryptedBackupBytes);
Log.info(
"Create twonly Safe backup with a size of ${encryptedBackupBytes.length} bytes.");
if (user.backupServer != null) {
if (encryptedBackupBytes.length > user.backupServer!.maxBackupBytes) {
Log.error("Backup is to big for the alternative backup server.");
await updateUserdata((user) {
user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.failed;
return user;
});
return;
}
}
final task = UploadTask.fromFile(
taskId: "backup",
file: encryptedBackupBytesFile,
httpRequestMethod: "PUT",
url: (await getTwonlySafeBackupUrl())!,
requiresWiFi: true,
priority: 5,
retries: 2,
headers: {
"Content-Type": "application/octet-stream",
},
);
if (await FileDownloader().enqueue(task)) {
Log.info("Starting upload from twonly Safe backup.");
await updateUserdata((user) {
user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.pending;
user.twonlySafeBackup!.lastBackupDone = DateTime.now();
user.twonlySafeBackup!.lastBackupSize = encryptedBackupBytes.length;
return user;
});
gUpdateBackupView();
} else {
Log.error("Error starting UploadTask for twonly Safe.");
}
}
Future handleBackupStatusUpdate(TaskStatusUpdate update) async {
if (update.status == TaskStatus.failed ||
update.status == TaskStatus.canceled) {
Log.error(
"twonly Safe upload failed. ${update.responseStatusCode} ${update.responseBody} ${update.responseHeaders} ${update.exception}");
await updateUserdata((user) {
if (user.twonlySafeBackup != null) {
user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.failed;
}
return user;
});
} else if (update.status == TaskStatus.complete) {
Log.error(
"twonly Safe uploaded with status code ${update.responseStatusCode}");
await updateUserdata((user) {
if (user.twonlySafeBackup != null) {
user.twonlySafeBackup!.backupUploadState =
LastBackupUploadState.success;
}
return user;
});
} else {
Log.info("Backup is in state: ${update.status}");
return;
}
gUpdateBackupView();
}

View file

@ -21,7 +21,7 @@ Future performTwonlySafeBackup({bool force = false}) async {
final user = await getUser();
if (user == null || user.twonlySafeBackup == null || user.isDemoUser) {
// Log.warn("perform twonly safe backup was called while it is disabled");
Log.warn("perform twonly safe backup was called while it is disabled");
return;
}
@ -38,7 +38,7 @@ Future performTwonlySafeBackup({bool force = false}) async {
}
}
Log.info("Starting new twonly Safe-Backup.");
Log.info("Starting new twonly Safe-Backup!");
final baseDir = (await getApplicationSupportDirectory()).path;
@ -48,6 +48,9 @@ Future performTwonlySafeBackup({bool force = false}) async {
final backupDatabaseFile =
File(join(backupDir.path, "twonly_database.backup.sqlite"));
final backupDatabaseFileCleaned =
File(join(backupDir.path, "twonly_database.backup.cleaned.sqlite"));
// copy database
final originalDatabase = File(join(baseDir, "twonly_database.sqlite"));
await originalDatabase.copy(backupDatabaseFile.path);
@ -66,6 +69,10 @@ Future performTwonlySafeBackup({bool force = false}) async {
await backupDB.deleteDataForTwonlySafe();
await backupDB
.customStatement('VACUUM INTO ?', [backupDatabaseFileCleaned.path]);
backupDB.close();
var secureStorageBackup = {};
final storage = FlutterSecureStorage();
secureStorageBackup[SecureStorageKeys.signalIdentity] =
@ -87,8 +94,11 @@ Future performTwonlySafeBackup({bool force = false}) async {
// Compress and convert backup data
final twonlyDatabaseBytes = await backupDatabaseFile.readAsBytes();
final twonlyDatabaseBytes = await backupDatabaseFileCleaned.readAsBytes();
await backupDatabaseFile.delete();
await backupDatabaseFileCleaned.delete();
print("twonlyDatabaseBytes = ${twonlyDatabaseBytes.lengthInBytes}");
final backupProto = TwonlySafeBackupContent(
secureStorageJson: jsonEncode(secureStorageBackup),
@ -163,8 +173,8 @@ Future performTwonlySafeBackup({bool force = false}) async {
httpRequestMethod: "PUT",
url: (await getTwonlySafeBackupUrl())!,
requiresWiFi: true,
post: 'binary',
priority: 5,
post: 'binary',
retries: 2,
headers: {
"Content-Type": "application/octet-stream",

View file

@ -7,6 +7,8 @@ import 'package:http/http.dart' as http;
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:twonly/src/constants/secure_storage_keys.dart';
import 'package:twonly/src/database/tables/messages_table.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/model/json/userdata.dart';
import 'package:twonly/src/model/protobuf/backup/backup.pb.dart';
import 'package:twonly/src/services/twonly_safe/common.twonly_safe.dart';
@ -85,6 +87,40 @@ Future handleBackupData(
final originalDatabase = File(join(baseDir, "twonly_database.sqlite"));
await originalDatabase.writeAsBytes(backupContent.twonlyDatabase);
/// When restoring the last message ID must be increased otherwise
/// receivers would mark them as duplicates as they where already
/// send.
final database = TwonlyDatabase();
var lastMessageSend = 0;
int? randomUserId;
final contacts = await database.contactsDao.getAllNotBlockedContacts();
for (final contact in contacts) {
randomUserId = contact.userId;
final days = DateTime.now().difference(contact.lastMessageExchange).inDays;
if (days < lastMessageSend) {
lastMessageSend = days;
}
}
if (randomUserId != null) {
// for each day add 400 message ids
var dummyMessagesCounter = (lastMessageSend + 1) * 400;
Log.info(
"Creating $dummyMessagesCounter dummy messages to increase message counter as last message was $lastMessageSend days ago.");
for (var i = 0; i < dummyMessagesCounter; i++) {
await database.messagesDao.insertMessage(
MessagesCompanion(
contactId: Value(randomUserId),
kind: Value(MessageKind.ack),
acknowledgeByServer: Value(true),
errorWhileSending: Value(true),
),
);
}
await database.messagesDao.deleteAllMessagesByContactId(randomUserId);
}
final storage = FlutterSecureStorage();
final secureStorage = jsonDecode(backupContent.secureStorageJson);

View file

@ -288,7 +288,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
TextMessageContent? content = TextMessageContent.fromJson(
jsonDecode(messages[index].contentJson!));
if (EmojiAnimation.supported(content.text)) {
size = 95;
size = 99;
} else {
size = 11 +
calculateNumberOfLines(content.text,

View file

@ -218,7 +218,7 @@ class _RegisterViewState extends State<RegisterView> {
},
));
},
label: Text("Restore identity"),
label: Text(context.lang.twonlySafeRecoverBtn),
),
],
),

View file

@ -59,7 +59,7 @@ class _BackupViewState extends State<BackupView> {
String backupStatus(LastBackupUploadState status) {
switch (status) {
case LastBackupUploadState.none:
return '';
return context.lang.backupPending;
case LastBackupUploadState.pending:
return context.lang.backupPending;
case LastBackupUploadState.failed:

View file

@ -22,7 +22,9 @@ class _TwonlyIdentityBackupViewState extends State<TwonlyIdentityBackupView> {
final TextEditingController repeatedPasswordCtrl = TextEditingController();
Future onPressedEnableTwonlySafe() async {
if (!mounted) return;
setState(() {
isLoading = true;
});
if (!await isSecurePassword(passwordCtrl.text)) {
if (!mounted) return;
bool ignore = await showAlertDialog(
@ -33,6 +35,11 @@ class _TwonlyIdentityBackupViewState extends State<TwonlyIdentityBackupView> {
customOk: context.lang.backupInsecurePasswordCancel,
);
if (ignore) {
if (mounted) {
setState(() {
isLoading = false;
});
}
return;
}
}
@ -119,6 +126,7 @@ class _TwonlyIdentityBackupViewState extends State<TwonlyIdentityBackupView> {
child: Text(
context.lang.backupPasswordRequirement,
style: TextStyle(
fontSize: 13,
color: ((passwordCtrl.text.length < 8 &&
passwordCtrl.text.isNotEmpty))
? Colors.red
@ -143,6 +151,7 @@ class _TwonlyIdentityBackupViewState extends State<TwonlyIdentityBackupView> {
child: Text(
context.lang.passwordRepeatedNotEqual,
style: TextStyle(
fontSize: 13,
color: (passwordCtrl.text != repeatedPasswordCtrl.text &&
repeatedPasswordCtrl.text.isNotEmpty)
? Colors.red

View file

@ -14,6 +14,7 @@ import 'schema_v8.dart' as v8;
import 'schema_v9.dart' as v9;
import 'schema_v10.dart' as v10;
import 'schema_v11.dart' as v11;
import 'schema_v12.dart' as v12;
class GeneratedHelper implements SchemaInstantiationHelper {
@override
@ -41,10 +42,12 @@ class GeneratedHelper implements SchemaInstantiationHelper {
return v10.DatabaseAtV10(db);
case 11:
return v11.DatabaseAtV11(db);
case 12:
return v12.DatabaseAtV12(db);
default:
throw MissingSchemaException(version, versions);
}
}
static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
}

File diff suppressed because it is too large Load diff