allow user deletion

This commit is contained in:
otsmr 2026-01-17 16:59:58 +01:00
parent b1802111f5
commit f91f2c2fb9
24 changed files with 7664 additions and 269 deletions

View file

@ -194,6 +194,14 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
.watch();
}
Stream<List<GroupMember>> watchContactGroupMember(int contactId) {
return (select(groupMembers)
..where(
(g) => g.contactId.equals(contactId),
))
.watch();
}
Stream<Group?> watchGroup(String groupId) {
return (select(groups)..where((t) => t.groupId.equals(groupId)))
.watchSingleOrNull();

View file

@ -92,7 +92,9 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
..where(
(t) =>
t.ackByServerAt.isNull() |
t.markForRetry.isSmallerThanValue(markedRetriesTime),
t.markForRetry.isSmallerThanValue(markedRetriesTime) |
t.markForRetryAfterAccepted
.isSmallerThanValue(markedRetriesTime),
))
.get();
}
@ -109,6 +111,19 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
.write(updates);
}
Future<void> updateReceiptWidthUserId(
int fromUserId,
String receiptId,
ReceiptsCompanion updates,
) async {
await (update(receipts)
..where(
(c) =>
c.receiptId.equals(receiptId) & c.contactId.equals(fromUserId),
))
.write(updates);
}
Future<void> markMessagesForRetry(int contactId) async {
await (update(receipts)..where((c) => c.contactId.equals(contactId))).write(
ReceiptsCompanion(

File diff suppressed because one or more lines are too long

View file

@ -21,6 +21,7 @@ class Receipts extends Table {
boolean().withDefault(const Constant(true))();
DateTimeColumn get markForRetry => dateTime().nullable()();
DateTimeColumn get markForRetryAfterAccepted => dateTime().nullable()();
DateTimeColumn get ackByServerAt => dateTime().nullable()();

View file

@ -68,7 +68,7 @@ class TwonlyDB extends _$TwonlyDB {
TwonlyDB.forTesting(DatabaseConnection super.connection);
@override
int get schemaVersion => 5;
int get schemaVersion => 6;
static QueryExecutor _openConnection() {
return driftDatabase(
@ -111,6 +111,12 @@ class TwonlyDB extends _$TwonlyDB {
schema.mediaFiles.storedFileHash,
);
},
from5To6: (m, schema) async {
await m.addColumn(
schema.receipts,
schema.receipts.markForRetryAfterAccepted,
);
},
),
);
}

View file

@ -4598,6 +4598,13 @@ class $ReceiptsTable extends Receipts with TableInfo<$ReceiptsTable, Receipt> {
late final GeneratedColumn<DateTime> markForRetry = GeneratedColumn<DateTime>(
'mark_for_retry', aliasedName, true,
type: DriftSqlType.dateTime, requiredDuringInsert: false);
static const VerificationMeta _markForRetryAfterAcceptedMeta =
const VerificationMeta('markForRetryAfterAccepted');
@override
late final GeneratedColumn<DateTime> markForRetryAfterAccepted =
GeneratedColumn<DateTime>(
'mark_for_retry_after_accepted', aliasedName, true,
type: DriftSqlType.dateTime, requiredDuringInsert: false);
static const VerificationMeta _ackByServerAtMeta =
const VerificationMeta('ackByServerAt');
@override
@ -4634,6 +4641,7 @@ class $ReceiptsTable extends Receipts with TableInfo<$ReceiptsTable, Receipt> {
message,
contactWillSendsReceipt,
markForRetry,
markForRetryAfterAccepted,
ackByServerAt,
retryCount,
lastRetry,
@ -4684,6 +4692,13 @@ class $ReceiptsTable extends Receipts with TableInfo<$ReceiptsTable, Receipt> {
markForRetry.isAcceptableOrUnknown(
data['mark_for_retry']!, _markForRetryMeta));
}
if (data.containsKey('mark_for_retry_after_accepted')) {
context.handle(
_markForRetryAfterAcceptedMeta,
markForRetryAfterAccepted.isAcceptableOrUnknown(
data['mark_for_retry_after_accepted']!,
_markForRetryAfterAcceptedMeta));
}
if (data.containsKey('ack_by_server_at')) {
context.handle(
_ackByServerAtMeta,
@ -4726,6 +4741,9 @@ class $ReceiptsTable extends Receipts with TableInfo<$ReceiptsTable, Receipt> {
data['${effectivePrefix}contact_will_sends_receipt'])!,
markForRetry: attachedDatabase.typeMapping.read(
DriftSqlType.dateTime, data['${effectivePrefix}mark_for_retry']),
markForRetryAfterAccepted: attachedDatabase.typeMapping.read(
DriftSqlType.dateTime,
data['${effectivePrefix}mark_for_retry_after_accepted']),
ackByServerAt: attachedDatabase.typeMapping.read(
DriftSqlType.dateTime, data['${effectivePrefix}ack_by_server_at']),
retryCount: attachedDatabase.typeMapping
@ -4752,6 +4770,7 @@ class Receipt extends DataClass implements Insertable<Receipt> {
final Uint8List message;
final bool contactWillSendsReceipt;
final DateTime? markForRetry;
final DateTime? markForRetryAfterAccepted;
final DateTime? ackByServerAt;
final int retryCount;
final DateTime? lastRetry;
@ -4763,6 +4782,7 @@ class Receipt extends DataClass implements Insertable<Receipt> {
required this.message,
required this.contactWillSendsReceipt,
this.markForRetry,
this.markForRetryAfterAccepted,
this.ackByServerAt,
required this.retryCount,
this.lastRetry,
@ -4780,6 +4800,10 @@ class Receipt extends DataClass implements Insertable<Receipt> {
if (!nullToAbsent || markForRetry != null) {
map['mark_for_retry'] = Variable<DateTime>(markForRetry);
}
if (!nullToAbsent || markForRetryAfterAccepted != null) {
map['mark_for_retry_after_accepted'] =
Variable<DateTime>(markForRetryAfterAccepted);
}
if (!nullToAbsent || ackByServerAt != null) {
map['ack_by_server_at'] = Variable<DateTime>(ackByServerAt);
}
@ -4803,6 +4827,10 @@ class Receipt extends DataClass implements Insertable<Receipt> {
markForRetry: markForRetry == null && nullToAbsent
? const Value.absent()
: Value(markForRetry),
markForRetryAfterAccepted:
markForRetryAfterAccepted == null && nullToAbsent
? const Value.absent()
: Value(markForRetryAfterAccepted),
ackByServerAt: ackByServerAt == null && nullToAbsent
? const Value.absent()
: Value(ackByServerAt),
@ -4825,6 +4853,8 @@ class Receipt extends DataClass implements Insertable<Receipt> {
contactWillSendsReceipt:
serializer.fromJson<bool>(json['contactWillSendsReceipt']),
markForRetry: serializer.fromJson<DateTime?>(json['markForRetry']),
markForRetryAfterAccepted:
serializer.fromJson<DateTime?>(json['markForRetryAfterAccepted']),
ackByServerAt: serializer.fromJson<DateTime?>(json['ackByServerAt']),
retryCount: serializer.fromJson<int>(json['retryCount']),
lastRetry: serializer.fromJson<DateTime?>(json['lastRetry']),
@ -4842,6 +4872,8 @@ class Receipt extends DataClass implements Insertable<Receipt> {
'contactWillSendsReceipt':
serializer.toJson<bool>(contactWillSendsReceipt),
'markForRetry': serializer.toJson<DateTime?>(markForRetry),
'markForRetryAfterAccepted':
serializer.toJson<DateTime?>(markForRetryAfterAccepted),
'ackByServerAt': serializer.toJson<DateTime?>(ackByServerAt),
'retryCount': serializer.toJson<int>(retryCount),
'lastRetry': serializer.toJson<DateTime?>(lastRetry),
@ -4856,6 +4888,7 @@ class Receipt extends DataClass implements Insertable<Receipt> {
Uint8List? message,
bool? contactWillSendsReceipt,
Value<DateTime?> markForRetry = const Value.absent(),
Value<DateTime?> markForRetryAfterAccepted = const Value.absent(),
Value<DateTime?> ackByServerAt = const Value.absent(),
int? retryCount,
Value<DateTime?> lastRetry = const Value.absent(),
@ -4869,6 +4902,9 @@ class Receipt extends DataClass implements Insertable<Receipt> {
contactWillSendsReceipt ?? this.contactWillSendsReceipt,
markForRetry:
markForRetry.present ? markForRetry.value : this.markForRetry,
markForRetryAfterAccepted: markForRetryAfterAccepted.present
? markForRetryAfterAccepted.value
: this.markForRetryAfterAccepted,
ackByServerAt:
ackByServerAt.present ? ackByServerAt.value : this.ackByServerAt,
retryCount: retryCount ?? this.retryCount,
@ -4887,6 +4923,9 @@ class Receipt extends DataClass implements Insertable<Receipt> {
markForRetry: data.markForRetry.present
? data.markForRetry.value
: this.markForRetry,
markForRetryAfterAccepted: data.markForRetryAfterAccepted.present
? data.markForRetryAfterAccepted.value
: this.markForRetryAfterAccepted,
ackByServerAt: data.ackByServerAt.present
? data.ackByServerAt.value
: this.ackByServerAt,
@ -4906,6 +4945,7 @@ class Receipt extends DataClass implements Insertable<Receipt> {
..write('message: $message, ')
..write('contactWillSendsReceipt: $contactWillSendsReceipt, ')
..write('markForRetry: $markForRetry, ')
..write('markForRetryAfterAccepted: $markForRetryAfterAccepted, ')
..write('ackByServerAt: $ackByServerAt, ')
..write('retryCount: $retryCount, ')
..write('lastRetry: $lastRetry, ')
@ -4922,6 +4962,7 @@ class Receipt extends DataClass implements Insertable<Receipt> {
$driftBlobEquality.hash(message),
contactWillSendsReceipt,
markForRetry,
markForRetryAfterAccepted,
ackByServerAt,
retryCount,
lastRetry,
@ -4936,6 +4977,7 @@ class Receipt extends DataClass implements Insertable<Receipt> {
$driftBlobEquality.equals(other.message, this.message) &&
other.contactWillSendsReceipt == this.contactWillSendsReceipt &&
other.markForRetry == this.markForRetry &&
other.markForRetryAfterAccepted == this.markForRetryAfterAccepted &&
other.ackByServerAt == this.ackByServerAt &&
other.retryCount == this.retryCount &&
other.lastRetry == this.lastRetry &&
@ -4949,6 +4991,7 @@ class ReceiptsCompanion extends UpdateCompanion<Receipt> {
final Value<Uint8List> message;
final Value<bool> contactWillSendsReceipt;
final Value<DateTime?> markForRetry;
final Value<DateTime?> markForRetryAfterAccepted;
final Value<DateTime?> ackByServerAt;
final Value<int> retryCount;
final Value<DateTime?> lastRetry;
@ -4961,6 +5004,7 @@ class ReceiptsCompanion extends UpdateCompanion<Receipt> {
this.message = const Value.absent(),
this.contactWillSendsReceipt = const Value.absent(),
this.markForRetry = const Value.absent(),
this.markForRetryAfterAccepted = const Value.absent(),
this.ackByServerAt = const Value.absent(),
this.retryCount = const Value.absent(),
this.lastRetry = const Value.absent(),
@ -4974,6 +5018,7 @@ class ReceiptsCompanion extends UpdateCompanion<Receipt> {
required Uint8List message,
this.contactWillSendsReceipt = const Value.absent(),
this.markForRetry = const Value.absent(),
this.markForRetryAfterAccepted = const Value.absent(),
this.ackByServerAt = const Value.absent(),
this.retryCount = const Value.absent(),
this.lastRetry = const Value.absent(),
@ -4989,6 +5034,7 @@ class ReceiptsCompanion extends UpdateCompanion<Receipt> {
Expression<Uint8List>? message,
Expression<bool>? contactWillSendsReceipt,
Expression<DateTime>? markForRetry,
Expression<DateTime>? markForRetryAfterAccepted,
Expression<DateTime>? ackByServerAt,
Expression<int>? retryCount,
Expression<DateTime>? lastRetry,
@ -5003,6 +5049,8 @@ class ReceiptsCompanion extends UpdateCompanion<Receipt> {
if (contactWillSendsReceipt != null)
'contact_will_sends_receipt': contactWillSendsReceipt,
if (markForRetry != null) 'mark_for_retry': markForRetry,
if (markForRetryAfterAccepted != null)
'mark_for_retry_after_accepted': markForRetryAfterAccepted,
if (ackByServerAt != null) 'ack_by_server_at': ackByServerAt,
if (retryCount != null) 'retry_count': retryCount,
if (lastRetry != null) 'last_retry': lastRetry,
@ -5018,6 +5066,7 @@ class ReceiptsCompanion extends UpdateCompanion<Receipt> {
Value<Uint8List>? message,
Value<bool>? contactWillSendsReceipt,
Value<DateTime?>? markForRetry,
Value<DateTime?>? markForRetryAfterAccepted,
Value<DateTime?>? ackByServerAt,
Value<int>? retryCount,
Value<DateTime?>? lastRetry,
@ -5031,6 +5080,8 @@ class ReceiptsCompanion extends UpdateCompanion<Receipt> {
contactWillSendsReceipt:
contactWillSendsReceipt ?? this.contactWillSendsReceipt,
markForRetry: markForRetry ?? this.markForRetry,
markForRetryAfterAccepted:
markForRetryAfterAccepted ?? this.markForRetryAfterAccepted,
ackByServerAt: ackByServerAt ?? this.ackByServerAt,
retryCount: retryCount ?? this.retryCount,
lastRetry: lastRetry ?? this.lastRetry,
@ -5061,6 +5112,10 @@ class ReceiptsCompanion extends UpdateCompanion<Receipt> {
if (markForRetry.present) {
map['mark_for_retry'] = Variable<DateTime>(markForRetry.value);
}
if (markForRetryAfterAccepted.present) {
map['mark_for_retry_after_accepted'] =
Variable<DateTime>(markForRetryAfterAccepted.value);
}
if (ackByServerAt.present) {
map['ack_by_server_at'] = Variable<DateTime>(ackByServerAt.value);
}
@ -5088,6 +5143,7 @@ class ReceiptsCompanion extends UpdateCompanion<Receipt> {
..write('message: $message, ')
..write('contactWillSendsReceipt: $contactWillSendsReceipt, ')
..write('markForRetry: $markForRetry, ')
..write('markForRetryAfterAccepted: $markForRetryAfterAccepted, ')
..write('ackByServerAt: $ackByServerAt, ')
..write('retryCount: $retryCount, ')
..write('lastRetry: $lastRetry, ')
@ -11771,6 +11827,7 @@ typedef $$ReceiptsTableCreateCompanionBuilder = ReceiptsCompanion Function({
required Uint8List message,
Value<bool> contactWillSendsReceipt,
Value<DateTime?> markForRetry,
Value<DateTime?> markForRetryAfterAccepted,
Value<DateTime?> ackByServerAt,
Value<int> retryCount,
Value<DateTime?> lastRetry,
@ -11784,6 +11841,7 @@ typedef $$ReceiptsTableUpdateCompanionBuilder = ReceiptsCompanion Function({
Value<Uint8List> message,
Value<bool> contactWillSendsReceipt,
Value<DateTime?> markForRetry,
Value<DateTime?> markForRetryAfterAccepted,
Value<DateTime?> ackByServerAt,
Value<int> retryCount,
Value<DateTime?> lastRetry,
@ -11848,6 +11906,10 @@ class $$ReceiptsTableFilterComposer
ColumnFilters<DateTime> get markForRetry => $composableBuilder(
column: $table.markForRetry, builder: (column) => ColumnFilters(column));
ColumnFilters<DateTime> get markForRetryAfterAccepted => $composableBuilder(
column: $table.markForRetryAfterAccepted,
builder: (column) => ColumnFilters(column));
ColumnFilters<DateTime> get ackByServerAt => $composableBuilder(
column: $table.ackByServerAt, builder: (column) => ColumnFilters(column));
@ -11924,6 +11986,10 @@ class $$ReceiptsTableOrderingComposer
column: $table.markForRetry,
builder: (column) => ColumnOrderings(column));
ColumnOrderings<DateTime> get markForRetryAfterAccepted => $composableBuilder(
column: $table.markForRetryAfterAccepted,
builder: (column) => ColumnOrderings(column));
ColumnOrderings<DateTime> get ackByServerAt => $composableBuilder(
column: $table.ackByServerAt,
builder: (column) => ColumnOrderings(column));
@ -11999,6 +12065,9 @@ class $$ReceiptsTableAnnotationComposer
GeneratedColumn<DateTime> get markForRetry => $composableBuilder(
column: $table.markForRetry, builder: (column) => column);
GeneratedColumn<DateTime> get markForRetryAfterAccepted => $composableBuilder(
column: $table.markForRetryAfterAccepted, builder: (column) => column);
GeneratedColumn<DateTime> get ackByServerAt => $composableBuilder(
column: $table.ackByServerAt, builder: (column) => column);
@ -12081,6 +12150,7 @@ class $$ReceiptsTableTableManager extends RootTableManager<
Value<Uint8List> message = const Value.absent(),
Value<bool> contactWillSendsReceipt = const Value.absent(),
Value<DateTime?> markForRetry = const Value.absent(),
Value<DateTime?> markForRetryAfterAccepted = const Value.absent(),
Value<DateTime?> ackByServerAt = const Value.absent(),
Value<int> retryCount = const Value.absent(),
Value<DateTime?> lastRetry = const Value.absent(),
@ -12094,6 +12164,7 @@ class $$ReceiptsTableTableManager extends RootTableManager<
message: message,
contactWillSendsReceipt: contactWillSendsReceipt,
markForRetry: markForRetry,
markForRetryAfterAccepted: markForRetryAfterAccepted,
ackByServerAt: ackByServerAt,
retryCount: retryCount,
lastRetry: lastRetry,
@ -12107,6 +12178,7 @@ class $$ReceiptsTableTableManager extends RootTableManager<
required Uint8List message,
Value<bool> contactWillSendsReceipt = const Value.absent(),
Value<DateTime?> markForRetry = const Value.absent(),
Value<DateTime?> markForRetryAfterAccepted = const Value.absent(),
Value<DateTime?> ackByServerAt = const Value.absent(),
Value<int> retryCount = const Value.absent(),
Value<DateTime?> lastRetry = const Value.absent(),
@ -12120,6 +12192,7 @@ class $$ReceiptsTableTableManager extends RootTableManager<
message: message,
contactWillSendsReceipt: contactWillSendsReceipt,
markForRetry: markForRetry,
markForRetryAfterAccepted: markForRetryAfterAccepted,
ackByServerAt: ackByServerAt,
retryCount: retryCount,
lastRetry: lastRetry,

View file

@ -2393,11 +2393,423 @@ class Shape19 extends i0.VersionedTable {
i1.GeneratedColumn<DateTime> _column_103(String aliasedName) =>
i1.GeneratedColumn<DateTime>('mark_for_retry', aliasedName, true,
type: i1.DriftSqlType.dateTime);
final class Schema6 extends i0.VersionedSchema {
Schema6({required super.database}) : super(version: 6);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
contacts,
groups,
mediaFiles,
messages,
messageHistories,
reactions,
groupMembers,
receipts,
receivedReceipts,
signalIdentityKeyStores,
signalPreKeyStores,
signalSenderKeyStores,
signalSessionStores,
signalContactPreKeys,
signalContactSignedPreKeys,
messageActions,
groupHistories,
];
late final Shape0 contacts = Shape0(
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_10,
_column_11,
_column_12,
],
attachedDatabase: database,
),
alias: null);
late final Shape17 groups = Shape17(
source: i0.VersionedTable(
entityName: 'groups',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(group_id)',
],
columns: [
_column_13,
_column_14,
_column_15,
_column_16,
_column_17,
_column_18,
_column_19,
_column_20,
_column_21,
_column_22,
_column_23,
_column_24,
_column_100,
_column_25,
_column_26,
_column_27,
_column_12,
_column_28,
_column_29,
_column_30,
_column_31,
_column_32,
_column_33,
_column_34,
_column_35,
],
attachedDatabase: database,
),
alias: null);
late final Shape18 mediaFiles = Shape18(
source: i0.VersionedTable(
entityName: 'media_files',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(media_id)',
],
columns: [
_column_36,
_column_37,
_column_38,
_column_39,
_column_40,
_column_41,
_column_42,
_column_43,
_column_44,
_column_45,
_column_46,
_column_47,
_column_48,
_column_49,
_column_102,
_column_12,
],
attachedDatabase: database,
),
alias: null);
late final Shape3 messages = Shape3(
source: i0.VersionedTable(
entityName: 'messages',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(message_id)',
],
columns: [
_column_50,
_column_51,
_column_52,
_column_37,
_column_53,
_column_54,
_column_55,
_column_56,
_column_46,
_column_57,
_column_58,
_column_59,
_column_60,
_column_12,
_column_61,
_column_62,
_column_63,
],
attachedDatabase: database,
),
alias: null);
late final Shape4 messageHistories = Shape4(
source: i0.VersionedTable(
entityName: 'message_histories',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(id)',
],
columns: [
_column_64,
_column_65,
_column_66,
_column_53,
_column_12,
],
attachedDatabase: database,
),
alias: null);
late final Shape5 reactions = Shape5(
source: i0.VersionedTable(
entityName: 'reactions',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(message_id, sender_id, emoji)',
],
columns: [
_column_65,
_column_67,
_column_68,
_column_12,
],
attachedDatabase: database,
),
alias: null);
late final Shape6 groupMembers = Shape6(
source: i0.VersionedTable(
entityName: 'group_members',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(group_id, contact_id)',
],
columns: [
_column_50,
_column_69,
_column_70,
_column_71,
_column_72,
_column_12,
],
attachedDatabase: database,
),
alias: null);
late final Shape20 receipts = Shape20(
source: i0.VersionedTable(
entityName: 'receipts',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(receipt_id)',
],
columns: [
_column_73,
_column_74,
_column_75,
_column_76,
_column_77,
_column_103,
_column_104,
_column_78,
_column_79,
_column_80,
_column_12,
],
attachedDatabase: database,
),
alias: null);
late final Shape8 receivedReceipts = Shape8(
source: i0.VersionedTable(
entityName: 'received_receipts',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(receipt_id)',
],
columns: [
_column_73,
_column_12,
],
attachedDatabase: database,
),
alias: null);
late final Shape9 signalIdentityKeyStores = Shape9(
source: i0.VersionedTable(
entityName: 'signal_identity_key_stores',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(device_id, name)',
],
columns: [
_column_81,
_column_82,
_column_83,
_column_12,
],
attachedDatabase: database,
),
alias: null);
late final Shape10 signalPreKeyStores = Shape10(
source: i0.VersionedTable(
entityName: 'signal_pre_key_stores',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(pre_key_id)',
],
columns: [
_column_84,
_column_85,
_column_12,
],
attachedDatabase: database,
),
alias: null);
late final Shape11 signalSenderKeyStores = Shape11(
source: i0.VersionedTable(
entityName: 'signal_sender_key_stores',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(sender_key_name)',
],
columns: [
_column_86,
_column_87,
],
attachedDatabase: database,
),
alias: null);
late final Shape12 signalSessionStores = Shape12(
source: i0.VersionedTable(
entityName: 'signal_session_stores',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(device_id, name)',
],
columns: [
_column_81,
_column_82,
_column_88,
_column_12,
],
attachedDatabase: database,
),
alias: null);
late final Shape13 signalContactPreKeys = Shape13(
source: i0.VersionedTable(
entityName: 'signal_contact_pre_keys',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(contact_id, pre_key_id)',
],
columns: [
_column_74,
_column_84,
_column_85,
_column_12,
],
attachedDatabase: database,
),
alias: null);
late final Shape14 signalContactSignedPreKeys = Shape14(
source: i0.VersionedTable(
entityName: 'signal_contact_signed_pre_keys',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(contact_id)',
],
columns: [
_column_74,
_column_89,
_column_90,
_column_91,
_column_12,
],
attachedDatabase: database,
),
alias: null);
late final Shape15 messageActions = Shape15(
source: i0.VersionedTable(
entityName: 'message_actions',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(message_id, contact_id, type)',
],
columns: [
_column_65,
_column_92,
_column_37,
_column_93,
],
attachedDatabase: database,
),
alias: null);
late final Shape16 groupHistories = Shape16(
source: i0.VersionedTable(
entityName: 'group_histories',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(group_history_id)',
],
columns: [
_column_94,
_column_50,
_column_95,
_column_101,
_column_97,
_column_98,
_column_99,
_column_37,
_column_93,
],
attachedDatabase: database,
),
alias: null);
}
class Shape20 extends i0.VersionedTable {
Shape20({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get receiptId =>
columnsByName['receipt_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get contactId =>
columnsByName['contact_id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get messageId =>
columnsByName['message_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<i2.Uint8List> get message =>
columnsByName['message']! as i1.GeneratedColumn<i2.Uint8List>;
i1.GeneratedColumn<bool> get contactWillSendsReceipt =>
columnsByName['contact_will_sends_receipt']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<DateTime> get markForRetry =>
columnsByName['mark_for_retry']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<DateTime> get markForRetryAfterAccepted =>
columnsByName['mark_for_retry_after_accepted']!
as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<DateTime> get ackByServerAt =>
columnsByName['ack_by_server_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<int> get retryCount =>
columnsByName['retry_count']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<DateTime> get lastRetry =>
columnsByName['last_retry']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<DateTime> get createdAt =>
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
}
i1.GeneratedColumn<DateTime> _column_104(String aliasedName) =>
i1.GeneratedColumn<DateTime>(
'mark_for_retry_after_accepted', aliasedName, true,
type: i1.DriftSqlType.dateTime);
i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
required Future<void> Function(i1.Migrator m, Schema5 schema) from4To5,
required Future<void> Function(i1.Migrator m, Schema6 schema) from5To6,
}) {
return (currentVersion, database) async {
switch (currentVersion) {
@ -2421,6 +2833,11 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema);
await from4To5(migrator, schema);
return 5;
case 5:
final schema = Schema6(database: database);
final migrator = i1.Migrator(database, schema);
await from5To6(migrator, schema);
return 6;
default:
throw ArgumentError.value('Unknown migration from $currentVersion');
}
@ -2432,6 +2849,7 @@ i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
required Future<void> Function(i1.Migrator m, Schema5 schema) from4To5,
required Future<void> Function(i1.Migrator m, Schema6 schema) from5To6,
}) =>
i0.VersionedSchema.stepByStepHelper(
step: migrationSteps(
@ -2439,4 +2857,5 @@ i1.OnUpgrade stepByStep({
from2To3: from2To3,
from3To4: from3To4,
from4To5: from4To5,
from5To6: from5To6,
));

View file

@ -1021,7 +1021,7 @@ abstract class AppLocalizations {
/// No description provided for @contactRemoveBody.
///
/// In en, this message translates to:
/// **'Remove the user and permanently delete the chat and all associated media files. This will also delete YOUR ACCOUNT FROM YOUR CONTACT\'S PHONE.'**
/// **'Permanently remove the user. If the user tries to send you a new message, you will have to accept the user again first.'**
String get contactRemoveBody;
/// No description provided for @undo.
@ -2925,6 +2925,12 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Store as default'**
String get storeAsDefault;
/// No description provided for @deleteUserErrorMessage.
///
/// In en, this message translates to:
/// **'You can only delete the contact once the direct chat has been deleted and the contact is no longer a member of a group.'**
String get deleteUserErrorMessage;
}
class _AppLocalizationsDelegate

View file

@ -517,7 +517,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get contactRemoveBody =>
'Entferne den Benutzer und lösche den Chat sowie alle zugehörigen Mediendateien dauerhaft. Dadurch wird auch DEIN KONTO VON DEM TELEFON DEINES KONTAKTS gelöscht.';
'Den Benutzer dauerhaft entfernen. Wenn der Benutzer versucht, dir eine neue Nachricht zu senden, musst du den Benutzer erst wieder akzeptieren.';
@override
String get undo => 'Rückgängig';
@ -1627,4 +1627,8 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get storeAsDefault => 'Als Standard speichern';
@override
String get deleteUserErrorMessage =>
'Du kannst den Kontakt erst löschen, wenn der direkte Chat gelöscht wurde und der Kontakt nicht mehr Mitglied einer Gruppe ist.';
}

View file

@ -512,7 +512,7 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get contactRemoveBody =>
'Remove the user and permanently delete the chat and all associated media files. This will also delete YOUR ACCOUNT FROM YOUR CONTACT\'S PHONE.';
'Permanently remove the user. If the user tries to send you a new message, you will have to accept the user again first.';
@override
String get undo => 'Undo';
@ -1615,4 +1615,8 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get storeAsDefault => 'Store as default';
@override
String get deleteUserErrorMessage =>
'You can only delete the contact once the direct chat has been deleted and the contact is no longer a member of a group.';
}

View file

@ -512,7 +512,7 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get contactRemoveBody =>
'Remove the user and permanently delete the chat and all associated media files. This will also delete YOUR ACCOUNT FROM YOUR CONTACT\'S PHONE.';
'Permanently remove the user. If the user tries to send you a new message, you will have to accept the user again first.';
@override
String get undo => 'Undo';
@ -1615,4 +1615,8 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get storeAsDefault => 'Store as default';
@override
String get deleteUserErrorMessage =>
'You can only delete the contact once the direct chat has been deleted and the contact is no longer a member of a group.';
}

@ -1 +1 @@
Subproject commit 775c0ffd9523177478681ecff4e8c4613bf57ee3
Subproject commit 4096b342802fc07be92070c7db6f46353790876b

View file

@ -311,6 +311,82 @@ class PlaintextContent extends $pb.GeneratedMessage {
PlaintextContent_RetryErrorMessage ensureRetryControlError() => $_ensure(1);
}
class EncryptedContent_ErrorMessages extends $pb.GeneratedMessage {
factory EncryptedContent_ErrorMessages({
EncryptedContent_ErrorMessages_Type? type,
$core.String? relatedReceiptId,
}) {
final result = create();
if (type != null) result.type = type;
if (relatedReceiptId != null) result.relatedReceiptId = relatedReceiptId;
return result;
}
EncryptedContent_ErrorMessages._();
factory EncryptedContent_ErrorMessages.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory EncryptedContent_ErrorMessages.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'EncryptedContent.ErrorMessages',
createEmptyInstance: create)
..e<EncryptedContent_ErrorMessages_Type>(
1, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE,
defaultOrMaker: EncryptedContent_ErrorMessages_Type
.ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD,
valueOf: EncryptedContent_ErrorMessages_Type.valueOf,
enumValues: EncryptedContent_ErrorMessages_Type.values)
..aOS(2, _omitFieldNames ? '' : 'relatedReceiptId')
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
EncryptedContent_ErrorMessages clone() =>
EncryptedContent_ErrorMessages()..mergeFromMessage(this);
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
EncryptedContent_ErrorMessages copyWith(
void Function(EncryptedContent_ErrorMessages) updates) =>
super.copyWith(
(message) => updates(message as EncryptedContent_ErrorMessages))
as EncryptedContent_ErrorMessages;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static EncryptedContent_ErrorMessages create() =>
EncryptedContent_ErrorMessages._();
@$core.override
EncryptedContent_ErrorMessages createEmptyInstance() => create();
static $pb.PbList<EncryptedContent_ErrorMessages> createRepeated() =>
$pb.PbList<EncryptedContent_ErrorMessages>();
@$core.pragma('dart2js:noInline')
static EncryptedContent_ErrorMessages getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<EncryptedContent_ErrorMessages>(create);
static EncryptedContent_ErrorMessages? _defaultInstance;
@$pb.TagNumber(1)
EncryptedContent_ErrorMessages_Type get type => $_getN(0);
@$pb.TagNumber(1)
set type(EncryptedContent_ErrorMessages_Type value) => $_setField(1, value);
@$pb.TagNumber(1)
$core.bool hasType() => $_has(0);
@$pb.TagNumber(1)
void clearType() => $_clearField(1);
@$pb.TagNumber(2)
$core.String get relatedReceiptId => $_getSZ(1);
@$pb.TagNumber(2)
set relatedReceiptId($core.String value) => $_setString(1, value);
@$pb.TagNumber(2)
$core.bool hasRelatedReceiptId() => $_has(1);
@$pb.TagNumber(2)
void clearRelatedReceiptId() => $_clearField(2);
}
class EncryptedContent_GroupCreate extends $pb.GeneratedMessage {
factory EncryptedContent_GroupCreate({
$core.List<$core.int>? stateKey,
@ -1519,6 +1595,7 @@ class EncryptedContent extends $pb.GeneratedMessage {
EncryptedContent_GroupJoin? groupJoin,
EncryptedContent_GroupUpdate? groupUpdate,
EncryptedContent_ResendGroupPublicKey? resendGroupPublicKey,
EncryptedContent_ErrorMessages? errorMessages,
}) {
final result = create();
if (groupId != null) result.groupId = groupId;
@ -1539,6 +1616,7 @@ class EncryptedContent extends $pb.GeneratedMessage {
if (groupUpdate != null) result.groupUpdate = groupUpdate;
if (resendGroupPublicKey != null)
result.resendGroupPublicKey = resendGroupPublicKey;
if (errorMessages != null) result.errorMessages = errorMessages;
return result;
}
@ -1599,6 +1677,9 @@ class EncryptedContent extends $pb.GeneratedMessage {
17, _omitFieldNames ? '' : 'resendGroupPublicKey',
protoName: 'resendGroupPublicKey',
subBuilder: EncryptedContent_ResendGroupPublicKey.create)
..aOM<EncryptedContent_ErrorMessages>(
18, _omitFieldNames ? '' : 'errorMessages',
subBuilder: EncryptedContent_ErrorMessages.create)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
@ -1797,6 +1878,18 @@ class EncryptedContent extends $pb.GeneratedMessage {
@$pb.TagNumber(17)
EncryptedContent_ResendGroupPublicKey ensureResendGroupPublicKey() =>
$_ensure(15);
@$pb.TagNumber(18)
EncryptedContent_ErrorMessages get errorMessages => $_getN(16);
@$pb.TagNumber(18)
set errorMessages(EncryptedContent_ErrorMessages value) =>
$_setField(18, value);
@$pb.TagNumber(18)
$core.bool hasErrorMessages() => $_has(16);
@$pb.TagNumber(18)
void clearErrorMessages() => $_clearField(18);
@$pb.TagNumber(18)
EncryptedContent_ErrorMessages ensureErrorMessages() => $_ensure(16);
}
const $core.bool _omitFieldNames =

View file

@ -65,6 +65,32 @@ class PlaintextContent_DecryptionErrorMessage_Type extends $pb.ProtobufEnum {
const PlaintextContent_DecryptionErrorMessage_Type._(super.value, super.name);
}
class EncryptedContent_ErrorMessages_Type extends $pb.ProtobufEnum {
static const EncryptedContent_ErrorMessages_Type
ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD =
EncryptedContent_ErrorMessages_Type._(
0,
_omitEnumNames
? ''
: 'ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD');
static const EncryptedContent_ErrorMessages_Type UNKNOWN_MESSAGE_TYPE =
EncryptedContent_ErrorMessages_Type._(
2, _omitEnumNames ? '' : 'UNKNOWN_MESSAGE_TYPE');
static const $core.List<EncryptedContent_ErrorMessages_Type> values =
<EncryptedContent_ErrorMessages_Type>[
ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD,
UNKNOWN_MESSAGE_TYPE,
];
static final $core.Map<$core.int, EncryptedContent_ErrorMessages_Type>
_byValue = $pb.ProtobufEnum.initByValue(values);
static EncryptedContent_ErrorMessages_Type? valueOf($core.int value) =>
_byValue[value];
const EncryptedContent_ErrorMessages_Type._(super.value, super.name);
}
class EncryptedContent_MessageUpdate_Type extends $pb.ProtobufEnum {
static const EncryptedContent_MessageUpdate_Type DELETE =
EncryptedContent_MessageUpdate_Type._(0, _omitEnumNames ? '' : 'DELETE');

View file

@ -306,8 +306,19 @@ const EncryptedContent$json = {
'10': 'resendGroupPublicKey',
'17': true
},
{
'1': 'error_messages',
'3': 18,
'4': 1,
'5': 11,
'6': '.EncryptedContent.ErrorMessages',
'9': 16,
'10': 'errorMessages',
'17': true
},
],
'3': [
EncryptedContent_ErrorMessages$json,
EncryptedContent_GroupCreate$json,
EncryptedContent_GroupJoin$json,
EncryptedContent_ResendGroupPublicKey$json,
@ -339,6 +350,39 @@ const EncryptedContent$json = {
{'1': '_groupJoin'},
{'1': '_groupUpdate'},
{'1': '_resendGroupPublicKey'},
{'1': '_error_messages'},
],
};
@$core.Deprecated('Use encryptedContentDescriptor instead')
const EncryptedContent_ErrorMessages$json = {
'1': 'ErrorMessages',
'2': [
{
'1': 'type',
'3': 1,
'4': 1,
'5': 14,
'6': '.EncryptedContent.ErrorMessages.Type',
'10': 'type'
},
{
'1': 'related_receipt_id',
'3': 2,
'4': 1,
'5': 9,
'10': 'relatedReceiptId'
},
],
'4': [EncryptedContent_ErrorMessages_Type$json],
};
@$core.Deprecated('Use encryptedContentDescriptor instead')
const EncryptedContent_ErrorMessages_Type$json = {
'1': 'Type',
'2': [
{'1': 'ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD', '2': 0},
{'1': 'UNKNOWN_MESSAGE_TYPE', '2': 2},
],
};
@ -772,57 +816,62 @@ final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode(
'AQESRAoLZ3JvdXBVcGRhdGUYECABKAsyHS5FbmNyeXB0ZWRDb250ZW50Lkdyb3VwVXBkYXRlSA'
'5SC2dyb3VwVXBkYXRliAEBEl8KFHJlc2VuZEdyb3VwUHVibGljS2V5GBEgASgLMiYuRW5jcnlw'
'dGVkQ29udGVudC5SZXNlbmRHcm91cFB1YmxpY0tleUgPUhRyZXNlbmRHcm91cFB1YmxpY0tleY'
'gBARpRCgtHcm91cENyZWF0ZRIaCghzdGF0ZUtleRgDIAEoDFIIc3RhdGVLZXkSJgoOZ3JvdXBQ'
'dWJsaWNLZXkYBCABKAxSDmdyb3VwUHVibGljS2V5GjMKCUdyb3VwSm9pbhImCg5ncm91cFB1Ym'
'xpY0tleRgBIAEoDFIOZ3JvdXBQdWJsaWNLZXkaFgoUUmVzZW5kR3JvdXBQdWJsaWNLZXkatgIK'
'C0dyb3VwVXBkYXRlEigKD2dyb3VwQWN0aW9uVHlwZRgBIAEoCVIPZ3JvdXBBY3Rpb25UeXBlEj'
'EKEWFmZmVjdGVkQ29udGFjdElkGAIgASgDSABSEWFmZmVjdGVkQ29udGFjdElkiAEBEicKDG5l'
'd0dyb3VwTmFtZRgDIAEoCUgBUgxuZXdHcm91cE5hbWWIAQESUwoibmV3RGVsZXRlTWVzc2FnZX'
'NBZnRlck1pbGxpc2Vjb25kcxgEIAEoA0gCUiJuZXdEZWxldGVNZXNzYWdlc0FmdGVyTWlsbGlz'
'ZWNvbmRziAEBQhQKEl9hZmZlY3RlZENvbnRhY3RJZEIPCg1fbmV3R3JvdXBOYW1lQiUKI19uZX'
'dEZWxldGVNZXNzYWdlc0FmdGVyTWlsbGlzZWNvbmRzGqkBCgtUZXh0TWVzc2FnZRIoCg9zZW5k'
'ZXJNZXNzYWdlSWQYASABKAlSD3NlbmRlck1lc3NhZ2VJZBISCgR0ZXh0GAIgASgJUgR0ZXh0Eh'
'wKCXRpbWVzdGFtcBgDIAEoA1IJdGltZXN0YW1wEisKDnF1b3RlTWVzc2FnZUlkGAQgASgJSABS'
'DnF1b3RlTWVzc2FnZUlkiAEBQhEKD19xdW90ZU1lc3NhZ2VJZBpiCghSZWFjdGlvbhIoCg90YX'
'JnZXRNZXNzYWdlSWQYASABKAlSD3RhcmdldE1lc3NhZ2VJZBIUCgVlbW9qaRgCIAEoCVIFZW1v'
'amkSFgoGcmVtb3ZlGAMgASgIUgZyZW1vdmUatwIKDU1lc3NhZ2VVcGRhdGUSOAoEdHlwZRgBIA'
'EoDjIkLkVuY3J5cHRlZENvbnRlbnQuTWVzc2FnZVVwZGF0ZS5UeXBlUgR0eXBlEi0KD3NlbmRl'
'ck1lc3NhZ2VJZBgCIAEoCUgAUg9zZW5kZXJNZXNzYWdlSWSIAQESOgoYbXVsdGlwbGVUYXJnZX'
'RNZXNzYWdlSWRzGAMgAygJUhhtdWx0aXBsZVRhcmdldE1lc3NhZ2VJZHMSFwoEdGV4dBgEIAEo'
'CUgBUgR0ZXh0iAEBEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1wIi0KBFR5cGUSCgoGRE'
'VMRVRFEAASDQoJRURJVF9URVhUEAESCgoGT1BFTkVEEAJCEgoQX3NlbmRlck1lc3NhZ2VJZEIH'
'CgVfdGV4dBqXBQoFTWVkaWESKAoPc2VuZGVyTWVzc2FnZUlkGAEgASgJUg9zZW5kZXJNZXNzYW'
'dlSWQSMAoEdHlwZRgCIAEoDjIcLkVuY3J5cHRlZENvbnRlbnQuTWVkaWEuVHlwZVIEdHlwZRJD'
'ChpkaXNwbGF5TGltaXRJbk1pbGxpc2Vjb25kcxgDIAEoA0gAUhpkaXNwbGF5TGltaXRJbk1pbG'
'xpc2Vjb25kc4gBARI2ChZyZXF1aXJlc0F1dGhlbnRpY2F0aW9uGAQgASgIUhZyZXF1aXJlc0F1'
'dGhlbnRpY2F0aW9uEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1wEisKDnF1b3RlTWVzc2'
'FnZUlkGAYgASgJSAFSDnF1b3RlTWVzc2FnZUlkiAEBEikKDWRvd25sb2FkVG9rZW4YByABKAxI'
'AlINZG93bmxvYWRUb2tlbogBARIpCg1lbmNyeXB0aW9uS2V5GAggASgMSANSDWVuY3J5cHRpb2'
'5LZXmIAQESKQoNZW5jcnlwdGlvbk1hYxgJIAEoDEgEUg1lbmNyeXB0aW9uTWFjiAEBEi0KD2Vu'
'Y3J5cHRpb25Ob25jZRgKIAEoDEgFUg9lbmNyeXB0aW9uTm9uY2WIAQEiPgoEVHlwZRIMCghSRV'
'VQTE9BRBAAEgkKBUlNQUdFEAESCQoFVklERU8QAhIHCgNHSUYQAxIJCgVBVURJTxAEQh0KG19k'
'aXNwbGF5TGltaXRJbk1pbGxpc2Vjb25kc0IRCg9fcXVvdGVNZXNzYWdlSWRCEAoOX2Rvd25sb2'
'FkVG9rZW5CEAoOX2VuY3J5cHRpb25LZXlCEAoOX2VuY3J5cHRpb25NYWNCEgoQX2VuY3J5cHRp'
'b25Ob25jZRqnAQoLTWVkaWFVcGRhdGUSNgoEdHlwZRgBIAEoDjIiLkVuY3J5cHRlZENvbnRlbn'
'QuTWVkaWFVcGRhdGUuVHlwZVIEdHlwZRIoCg90YXJnZXRNZXNzYWdlSWQYAiABKAlSD3Rhcmdl'
'dE1lc3NhZ2VJZCI2CgRUeXBlEgwKCFJFT1BFTkVEEAASCgoGU1RPUkVEEAESFAoQREVDUllQVE'
'lPTl9FUlJPUhACGngKDkNvbnRhY3RSZXF1ZXN0EjkKBHR5cGUYASABKA4yJS5FbmNyeXB0ZWRD'
'b250ZW50LkNvbnRhY3RSZXF1ZXN0LlR5cGVSBHR5cGUiKwoEVHlwZRILCgdSRVFVRVNUEAASCg'
'oGUkVKRUNUEAESCgoGQUNDRVBUEAIangIKDUNvbnRhY3RVcGRhdGUSOAoEdHlwZRgBIAEoDjIk'
'LkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFVwZGF0ZS5UeXBlUgR0eXBlEjUKE2F2YXRhclN2Z0'
'NvbXByZXNzZWQYAiABKAxIAFITYXZhdGFyU3ZnQ29tcHJlc3NlZIgBARIfCgh1c2VybmFtZRgD'
'IAEoCUgBUgh1c2VybmFtZYgBARIlCgtkaXNwbGF5TmFtZRgEIAEoCUgCUgtkaXNwbGF5TmFtZY'
'gBASIfCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZVUERBVEUQAUIWChRfYXZhdGFyU3ZnQ29tcHJl'
'c3NlZEILCglfdXNlcm5hbWVCDgoMX2Rpc3BsYXlOYW1lGtUBCghQdXNoS2V5cxIzCgR0eXBlGA'
'EgASgOMh8uRW5jcnlwdGVkQ29udGVudC5QdXNoS2V5cy5UeXBlUgR0eXBlEhkKBWtleUlkGAIg'
'ASgDSABSBWtleUlkiAEBEhUKA2tleRgDIAEoDEgBUgNrZXmIAQESIQoJY3JlYXRlZEF0GAQgAS'
'gDSAJSCWNyZWF0ZWRBdIgBASIfCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZVUERBVEUQAUIICgZf'
'a2V5SWRCBgoEX2tleUIMCgpfY3JlYXRlZEF0GqkBCglGbGFtZVN5bmMSIgoMZmxhbWVDb3VudG'
'VyGAEgASgDUgxmbGFtZUNvdW50ZXISNgoWbGFzdEZsYW1lQ291bnRlckNoYW5nZRgCIAEoA1IW'
'bGFzdEZsYW1lQ291bnRlckNoYW5nZRIeCgpiZXN0RnJpZW5kGAMgASgIUgpiZXN0RnJpZW5kEi'
'AKC2ZvcmNlVXBkYXRlGAQgASgIUgtmb3JjZVVwZGF0ZUIKCghfZ3JvdXBJZEIPCg1faXNEaXJl'
'Y3RDaGF0QhcKFV9zZW5kZXJQcm9maWxlQ291bnRlckIQCg5fbWVzc2FnZVVwZGF0ZUIICgZfbW'
'VkaWFCDgoMX21lZGlhVXBkYXRlQhAKDl9jb250YWN0VXBkYXRlQhEKD19jb250YWN0UmVxdWVz'
'dEIMCgpfZmxhbWVTeW5jQgsKCV9wdXNoS2V5c0ILCglfcmVhY3Rpb25CDgoMX3RleHRNZXNzYW'
'dlQg4KDF9ncm91cENyZWF0ZUIMCgpfZ3JvdXBKb2luQg4KDF9ncm91cFVwZGF0ZUIXChVfcmVz'
'ZW5kR3JvdXBQdWJsaWNLZXk=');
'gBARJLCg5lcnJvcl9tZXNzYWdlcxgSIAEoCzIfLkVuY3J5cHRlZENvbnRlbnQuRXJyb3JNZXNz'
'YWdlc0gQUg1lcnJvck1lc3NhZ2VziAEBGtcBCg1FcnJvck1lc3NhZ2VzEjgKBHR5cGUYASABKA'
'4yJC5FbmNyeXB0ZWRDb250ZW50LkVycm9yTWVzc2FnZXMuVHlwZVIEdHlwZRIsChJyZWxhdGVk'
'X3JlY2VpcHRfaWQYAiABKAlSEHJlbGF0ZWRSZWNlaXB0SWQiXgoEVHlwZRI8CjhFUlJPUl9QUk'
'9DRVNTSU5HX01FU1NBR0VfQ1JFQVRFRF9BQ0NPVU5UX1JFUVVFU1RfSU5TVEVBRBAAEhgKFFVO'
'S05PV05fTUVTU0FHRV9UWVBFEAIaUQoLR3JvdXBDcmVhdGUSGgoIc3RhdGVLZXkYAyABKAxSCH'
'N0YXRlS2V5EiYKDmdyb3VwUHVibGljS2V5GAQgASgMUg5ncm91cFB1YmxpY0tleRozCglHcm91'
'cEpvaW4SJgoOZ3JvdXBQdWJsaWNLZXkYASABKAxSDmdyb3VwUHVibGljS2V5GhYKFFJlc2VuZE'
'dyb3VwUHVibGljS2V5GrYCCgtHcm91cFVwZGF0ZRIoCg9ncm91cEFjdGlvblR5cGUYASABKAlS'
'D2dyb3VwQWN0aW9uVHlwZRIxChFhZmZlY3RlZENvbnRhY3RJZBgCIAEoA0gAUhFhZmZlY3RlZE'
'NvbnRhY3RJZIgBARInCgxuZXdHcm91cE5hbWUYAyABKAlIAVIMbmV3R3JvdXBOYW1liAEBElMK'
'Im5ld0RlbGV0ZU1lc3NhZ2VzQWZ0ZXJNaWxsaXNlY29uZHMYBCABKANIAlIibmV3RGVsZXRlTW'
'Vzc2FnZXNBZnRlck1pbGxpc2Vjb25kc4gBAUIUChJfYWZmZWN0ZWRDb250YWN0SWRCDwoNX25l'
'd0dyb3VwTmFtZUIlCiNfbmV3RGVsZXRlTWVzc2FnZXNBZnRlck1pbGxpc2Vjb25kcxqpAQoLVG'
'V4dE1lc3NhZ2USKAoPc2VuZGVyTWVzc2FnZUlkGAEgASgJUg9zZW5kZXJNZXNzYWdlSWQSEgoE'
'dGV4dBgCIAEoCVIEdGV4dBIcCgl0aW1lc3RhbXAYAyABKANSCXRpbWVzdGFtcBIrCg5xdW90ZU'
'1lc3NhZ2VJZBgEIAEoCUgAUg5xdW90ZU1lc3NhZ2VJZIgBAUIRCg9fcXVvdGVNZXNzYWdlSWQa'
'YgoIUmVhY3Rpb24SKAoPdGFyZ2V0TWVzc2FnZUlkGAEgASgJUg90YXJnZXRNZXNzYWdlSWQSFA'
'oFZW1vamkYAiABKAlSBWVtb2ppEhYKBnJlbW92ZRgDIAEoCFIGcmVtb3ZlGrcCCg1NZXNzYWdl'
'VXBkYXRlEjgKBHR5cGUYASABKA4yJC5FbmNyeXB0ZWRDb250ZW50Lk1lc3NhZ2VVcGRhdGUuVH'
'lwZVIEdHlwZRItCg9zZW5kZXJNZXNzYWdlSWQYAiABKAlIAFIPc2VuZGVyTWVzc2FnZUlkiAEB'
'EjoKGG11bHRpcGxlVGFyZ2V0TWVzc2FnZUlkcxgDIAMoCVIYbXVsdGlwbGVUYXJnZXRNZXNzYW'
'dlSWRzEhcKBHRleHQYBCABKAlIAVIEdGV4dIgBARIcCgl0aW1lc3RhbXAYBSABKANSCXRpbWVz'
'dGFtcCItCgRUeXBlEgoKBkRFTEVURRAAEg0KCUVESVRfVEVYVBABEgoKBk9QRU5FRBACQhIKEF'
'9zZW5kZXJNZXNzYWdlSWRCBwoFX3RleHQalwUKBU1lZGlhEigKD3NlbmRlck1lc3NhZ2VJZBgB'
'IAEoCVIPc2VuZGVyTWVzc2FnZUlkEjAKBHR5cGUYAiABKA4yHC5FbmNyeXB0ZWRDb250ZW50Lk'
'1lZGlhLlR5cGVSBHR5cGUSQwoaZGlzcGxheUxpbWl0SW5NaWxsaXNlY29uZHMYAyABKANIAFIa'
'ZGlzcGxheUxpbWl0SW5NaWxsaXNlY29uZHOIAQESNgoWcmVxdWlyZXNBdXRoZW50aWNhdGlvbh'
'gEIAEoCFIWcmVxdWlyZXNBdXRoZW50aWNhdGlvbhIcCgl0aW1lc3RhbXAYBSABKANSCXRpbWVz'
'dGFtcBIrCg5xdW90ZU1lc3NhZ2VJZBgGIAEoCUgBUg5xdW90ZU1lc3NhZ2VJZIgBARIpCg1kb3'
'dubG9hZFRva2VuGAcgASgMSAJSDWRvd25sb2FkVG9rZW6IAQESKQoNZW5jcnlwdGlvbktleRgI'
'IAEoDEgDUg1lbmNyeXB0aW9uS2V5iAEBEikKDWVuY3J5cHRpb25NYWMYCSABKAxIBFINZW5jcn'
'lwdGlvbk1hY4gBARItCg9lbmNyeXB0aW9uTm9uY2UYCiABKAxIBVIPZW5jcnlwdGlvbk5vbmNl'
'iAEBIj4KBFR5cGUSDAoIUkVVUExPQUQQABIJCgVJTUFHRRABEgkKBVZJREVPEAISBwoDR0lGEA'
'MSCQoFQVVESU8QBEIdChtfZGlzcGxheUxpbWl0SW5NaWxsaXNlY29uZHNCEQoPX3F1b3RlTWVz'
'c2FnZUlkQhAKDl9kb3dubG9hZFRva2VuQhAKDl9lbmNyeXB0aW9uS2V5QhAKDl9lbmNyeXB0aW'
'9uTWFjQhIKEF9lbmNyeXB0aW9uTm9uY2UapwEKC01lZGlhVXBkYXRlEjYKBHR5cGUYASABKA4y'
'Ii5FbmNyeXB0ZWRDb250ZW50Lk1lZGlhVXBkYXRlLlR5cGVSBHR5cGUSKAoPdGFyZ2V0TWVzc2'
'FnZUlkGAIgASgJUg90YXJnZXRNZXNzYWdlSWQiNgoEVHlwZRIMCghSRU9QRU5FRBAAEgoKBlNU'
'T1JFRBABEhQKEERFQ1JZUFRJT05fRVJST1IQAhp4Cg5Db250YWN0UmVxdWVzdBI5CgR0eXBlGA'
'EgASgOMiUuRW5jcnlwdGVkQ29udGVudC5Db250YWN0UmVxdWVzdC5UeXBlUgR0eXBlIisKBFR5'
'cGUSCwoHUkVRVUVTVBAAEgoKBlJFSkVDVBABEgoKBkFDQ0VQVBACGp4CCg1Db250YWN0VXBkYX'
'RlEjgKBHR5cGUYASABKA4yJC5FbmNyeXB0ZWRDb250ZW50LkNvbnRhY3RVcGRhdGUuVHlwZVIE'
'dHlwZRI1ChNhdmF0YXJTdmdDb21wcmVzc2VkGAIgASgMSABSE2F2YXRhclN2Z0NvbXByZXNzZW'
'SIAQESHwoIdXNlcm5hbWUYAyABKAlIAVIIdXNlcm5hbWWIAQESJQoLZGlzcGxheU5hbWUYBCAB'
'KAlIAlILZGlzcGxheU5hbWWIAQEiHwoEVHlwZRILCgdSRVFVRVNUEAASCgoGVVBEQVRFEAFCFg'
'oUX2F2YXRhclN2Z0NvbXByZXNzZWRCCwoJX3VzZXJuYW1lQg4KDF9kaXNwbGF5TmFtZRrVAQoI'
'UHVzaEtleXMSMwoEdHlwZRgBIAEoDjIfLkVuY3J5cHRlZENvbnRlbnQuUHVzaEtleXMuVHlwZV'
'IEdHlwZRIZCgVrZXlJZBgCIAEoA0gAUgVrZXlJZIgBARIVCgNrZXkYAyABKAxIAVIDa2V5iAEB'
'EiEKCWNyZWF0ZWRBdBgEIAEoA0gCUgljcmVhdGVkQXSIAQEiHwoEVHlwZRILCgdSRVFVRVNUEA'
'ASCgoGVVBEQVRFEAFCCAoGX2tleUlkQgYKBF9rZXlCDAoKX2NyZWF0ZWRBdBqpAQoJRmxhbWVT'
'eW5jEiIKDGZsYW1lQ291bnRlchgBIAEoA1IMZmxhbWVDb3VudGVyEjYKFmxhc3RGbGFtZUNvdW'
'50ZXJDaGFuZ2UYAiABKANSFmxhc3RGbGFtZUNvdW50ZXJDaGFuZ2USHgoKYmVzdEZyaWVuZBgD'
'IAEoCFIKYmVzdEZyaWVuZBIgCgtmb3JjZVVwZGF0ZRgEIAEoCFILZm9yY2VVcGRhdGVCCgoIX2'
'dyb3VwSWRCDwoNX2lzRGlyZWN0Q2hhdEIXChVfc2VuZGVyUHJvZmlsZUNvdW50ZXJCEAoOX21l'
'c3NhZ2VVcGRhdGVCCAoGX21lZGlhQg4KDF9tZWRpYVVwZGF0ZUIQCg5fY29udGFjdFVwZGF0ZU'
'IRCg9fY29udGFjdFJlcXVlc3RCDAoKX2ZsYW1lU3luY0ILCglfcHVzaEtleXNCCwoJX3JlYWN0'
'aW9uQg4KDF90ZXh0TWVzc2FnZUIOCgxfZ3JvdXBDcmVhdGVCDAoKX2dyb3VwSm9pbkIOCgxfZ3'
'JvdXBVcGRhdGVCFwoVX3Jlc2VuZEdyb3VwUHVibGljS2V5QhEKD19lcnJvcl9tZXNzYWdlcw==');

View file

@ -51,6 +51,17 @@ message EncryptedContent {
optional GroupJoin groupJoin = 15;
optional GroupUpdate groupUpdate = 16;
optional ResendGroupPublicKey resendGroupPublicKey = 17;
optional ErrorMessages error_messages = 18;
message ErrorMessages {
enum Type {
ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD = 0;
UNKNOWN_MESSAGE_TYPE = 2;
}
Type type = 1;
string related_receipt_id = 2;
}
message GroupCreate {

View file

@ -13,6 +13,43 @@ import 'package:twonly/src/utils/avatars.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
Future<bool> handleNewContactRequest(int fromUserId) async {
final contact = await twonlyDB.contactsDao
.getContactByUserId(fromUserId)
.getSingleOrNull();
if (contact != null) {
if (contact.accepted) {
// contact was already accepted, so just accept the request in the background.
await sendCipherText(
contact.userId,
EncryptedContent(
contactRequest: EncryptedContent_ContactRequest(
type: EncryptedContent_ContactRequest_Type.ACCEPT,
),
),
);
return true;
}
}
// Request the username by the server so an attacker can not
// forge the displayed username in the contact request
final user = await apiService.getUserById(fromUserId);
if (user == null) {
return false;
}
await twonlyDB.contactsDao.insertOnConflictUpdate(
ContactsCompanion(
username: Value(utf8.decode(user.username)),
userId: Value(fromUserId),
requested: const Value(true),
deletedByUser: const Value(false),
),
);
await setupNotificationWithUsers();
return true;
}
Future<bool> handleContactRequest(
int fromUserId,
EncryptedContent_ContactRequest contactRequest,
@ -20,38 +57,7 @@ Future<bool> handleContactRequest(
switch (contactRequest.type) {
case EncryptedContent_ContactRequest_Type.REQUEST:
Log.info('Got a contact request from $fromUserId');
final contact = await twonlyDB.contactsDao
.getContactByUserId(fromUserId)
.getSingleOrNull();
if (contact != null) {
if (contact.accepted) {
// contact was already accepted, so just accept the request in the background.
await sendCipherText(
contact.userId,
EncryptedContent(
contactRequest: EncryptedContent_ContactRequest(
type: EncryptedContent_ContactRequest_Type.ACCEPT,
),
),
);
return true;
}
}
// Request the username by the server so an attacker can not
// forge the displayed username in the contact request
final user = await apiService.getUserById(fromUserId);
if (user == null) {
return false;
}
await twonlyDB.contactsDao.insertOnConflictUpdate(
ContactsCompanion(
username: Value(utf8.decode(user.username)),
userId: Value(fromUserId),
requested: const Value(true),
deletedByUser: const Value(false),
),
);
await setupNotificationWithUsers();
return handleNewContactRequest(fromUserId);
case EncryptedContent_ContactRequest_Type.ACCEPT:
Log.info('Got a contact accept from $fromUserId');
await twonlyDB.contactsDao.updateContact(

View file

@ -0,0 +1,29 @@
import 'package:clock/clock.dart';
import 'package:drift/drift.dart' show Value;
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart';
Future<void> handleErrorMessage(
int fromUserId,
EncryptedContent_ErrorMessages error,
) async {
switch (error.type) {
case EncryptedContent_ErrorMessages_Type.UNKNOWN_MESSAGE_TYPE:
break;
case EncryptedContent_ErrorMessages_Type
.ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD:
await twonlyDB.receiptsDao.updateReceiptWidthUserId(
fromUserId,
error.relatedReceiptId,
ReceiptsCompanion(markForRetryAfterAccepted: Value(clock.now())),
);
await twonlyDB.contactsDao.updateContact(
fromUserId,
const ContactsCompanion(
accepted: Value(false),
requested: Value(true),
),
);
}
}

View file

@ -28,7 +28,29 @@ Future<void> tryTransmitMessages() async {
Log.info('Reuploading ${receipts.length} messages to the server.');
final contacts = <int, Contact>{};
for (final receipt in receipts) {
if (receipt.markForRetryAfterAccepted != null) {
if (!contacts.containsKey(receipt.contactId)) {
final contact = await twonlyDB.contactsDao
.getContactByUserId(receipt.contactId)
.getSingleOrNull();
if (contact == null) {
Log.error(
'Contact does not exists, but has a record in receipts, this should not be possible, because of the DELETE CASCADE relation.',
);
continue;
}
contacts[receipt.contactId] = contact;
}
if (!(contacts[receipt.contactId]?.accepted ?? true)) {
Log.warn(
'Could not send message as contact has still not yet accepted.',
);
continue;
}
}
await tryToSendCompleteMessage(receipt: receipt);
}
});

View file

@ -1,5 +1,4 @@
import 'dart:async';
import 'dart:convert';
import 'package:clock/clock.dart';
import 'package:drift/drift.dart';
import 'package:hashlib/random.dart';
@ -15,6 +14,7 @@ import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pbserver.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/client2client/contact.c2c.dart';
import 'package:twonly/src/services/api/client2client/errors.c2c.dart';
import 'package:twonly/src/services/api/client2client/groups.c2c.dart';
import 'package:twonly/src/services/api/client2client/media.c2c.dart';
import 'package:twonly/src/services/api/client2client/messages.c2c.dart';
@ -23,6 +23,7 @@ import 'package:twonly/src/services/api/client2client/pushkeys.c2c.dart';
import 'package:twonly/src/services/api/client2client/reaction.c2c.dart';
import 'package:twonly/src/services/api/client2client/text_message.c2c.dart';
import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/group.services.dart';
import 'package:twonly/src/services/signal/encryption.signal.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
@ -117,47 +118,60 @@ Future<void> handleClient2ClientMessage(NewMessage newMessage) async {
case Message_Type.CIPHERTEXT:
case Message_Type.PREKEY_BUNDLE:
if (message.hasEncryptedContent()) {
Value<String>? receiptIdDB;
final encryptedContentRaw =
Uint8List.fromList(message.encryptedContent);
if (await twonlyDB.contactsDao
.getContactByUserId(fromUserId)
.getSingleOrNull() ==
null) {
final user = await apiService.getUserById(fromUserId);
Message? response;
/// In case the user does not exists, just create a dummy user which was deleted by the user, so the message
/// can be inserted into the receipts database
await twonlyDB.contactsDao.insertContact(
ContactsCompanion(
userId: Value(fromUserId),
deletedByUser: const Value(true),
username: Value(
user == null ? '[Unknown]' : utf8.decode(user.username),
final user = await twonlyDB.contactsDao
.getContactByUserId(fromUserId)
.getSingleOrNull();
if (user == null) {
if (!await addNewHiddenContact(fromUserId)) {
// in case the user could not be added, send a retry error message as this error should only happen in case
// it was not possible to load the user from the server
response = Message(
receiptId: receiptId,
type: Message_Type.PLAINTEXT_CONTENT,
plaintextContent: PlaintextContent(
retryControlError: PlaintextContent_RetryErrorMessage(),
),
),
);
);
}
}
final responsePlaintextContent = await handleEncryptedMessage(
fromUserId,
encryptedContentRaw,
message.type,
);
Message response;
if (responsePlaintextContent != null) {
response = Message()
..receiptId = receiptId
..type = Message_Type.PLAINTEXT_CONTENT
..plaintextContent = responsePlaintextContent;
} else {
response = Message()..type = Message_Type.SENDER_DELIVERY_RECEIPT;
if (response == null) {
final (encryptedContent, plainTextContent) =
await handleEncryptedMessage(
fromUserId,
encryptedContentRaw,
message.type,
receiptId,
);
if (plainTextContent != null) {
response = Message(
receiptId: receiptId,
type: Message_Type.PLAINTEXT_CONTENT,
plaintextContent: plainTextContent,
);
} else if (encryptedContent != null) {
response = Message(
type: Message_Type.CIPHERTEXT,
encryptedContent: encryptedContent.writeToBuffer(),
);
receiptIdDB = const Value.absent();
}
}
response ??= Message(type: Message_Type.SENDER_DELIVERY_RECEIPT);
try {
await twonlyDB.receiptsDao.insertReceipt(
ReceiptsCompanion(
receiptId: Value(receiptId),
receiptId: receiptIdDB ?? Value(receiptId),
contactId: Value(fromUserId),
message: Value(response.writeToBuffer()),
contactWillSendsReceipt: const Value(false),
@ -173,10 +187,11 @@ Future<void> handleClient2ClientMessage(NewMessage newMessage) async {
}
}
Future<PlaintextContent?> handleEncryptedMessage(
Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
int fromUserId,
Uint8List encryptedContentRaw,
Message_Type messageType,
String receiptId,
) async {
final (content, decryptionErrorType) = await signalDecryptMessage(
fromUserId,
@ -185,9 +200,12 @@ Future<PlaintextContent?> handleEncryptedMessage(
);
if (content == null) {
return PlaintextContent()
..decryptionErrorMessage = (PlaintextContent_DecryptionErrorMessage()
..type = decryptionErrorType!);
return (
null,
PlaintextContent()
..decryptionErrorMessage = (PlaintextContent_DecryptionErrorMessage()
..type = decryptionErrorType!)
);
}
// We got a valid message fromUserId, so mark all messages which where
@ -203,10 +221,21 @@ Future<PlaintextContent?> handleEncryptedMessage(
if (content.hasContactRequest()) {
if (!await handleContactRequest(fromUserId, content.contactRequest)) {
return PlaintextContent()
..retryControlError = PlaintextContent_RetryErrorMessage();
return (
null,
PlaintextContent()
..retryControlError = PlaintextContent_RetryErrorMessage()
);
}
return null;
return (null, null);
}
if (content.hasErrorMessages()) {
await handleErrorMessage(
fromUserId,
content.errorMessages,
);
return (null, null);
}
if (content.hasContactUpdate()) {
@ -215,17 +244,17 @@ Future<PlaintextContent?> handleEncryptedMessage(
content.contactUpdate,
senderProfileCounter,
);
return null;
return (null, null);
}
if (content.hasFlameSync()) {
await handleFlameSync(fromUserId, content.flameSync);
return null;
return (null, null);
}
if (content.hasPushKeys()) {
await handlePushKey(fromUserId, content.pushKeys);
return null;
return (null, null);
}
if (content.hasMessageUpdate()) {
@ -233,7 +262,7 @@ Future<PlaintextContent?> handleEncryptedMessage(
fromUserId,
content.messageUpdate,
);
return null;
return (null, null);
}
if (content.hasMediaUpdate()) {
@ -241,12 +270,12 @@ Future<PlaintextContent?> handleEncryptedMessage(
fromUserId,
content.mediaUpdate,
);
return null;
return (null, null);
}
if (!content.hasGroupId()) {
Log.error('Messages should have a groupId $fromUserId.');
return null;
return (null, null);
}
if (content.hasGroupCreate()) {
@ -255,7 +284,7 @@ Future<PlaintextContent?> handleEncryptedMessage(
content.groupId,
content.groupCreate,
);
return null;
return (null, null);
}
/// Verify that the user is (still) in that group...
@ -265,10 +294,20 @@ Future<PlaintextContent?> handleEncryptedMessage(
.getContactByUserId(fromUserId)
.getSingleOrNull();
if (contact == null || contact.deletedByUser) {
await handleNewContactRequest(fromUserId);
Log.error(
'User tries to send message to direct chat while the user does not exists !',
);
return null;
return (
EncryptedContent(
errorMessages: EncryptedContent_ErrorMessages(
type: EncryptedContent_ErrorMessages_Type
.ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD,
relatedReceiptId: receiptId,
),
),
null
);
}
Log.info(
'Creating new DirectChat between two users',
@ -285,12 +324,15 @@ Future<PlaintextContent?> handleEncryptedMessage(
'Got group join message, but group does not exists yet, retry later. As probably the GroupCreate was not yet received.',
);
// In case the group join was received before the GroupCreate the sender should send it later again.
return PlaintextContent()
..retryControlError = PlaintextContent_RetryErrorMessage();
return (
null,
PlaintextContent()
..retryControlError = PlaintextContent_RetryErrorMessage()
);
}
Log.error('User $fromUserId tried to access group ${content.groupId}.');
return null;
return (null, null);
}
}
@ -300,7 +342,7 @@ Future<PlaintextContent?> handleEncryptedMessage(
content.groupId,
content.groupUpdate,
);
return null;
return (null, null);
}
if (content.hasGroupJoin()) {
@ -309,10 +351,13 @@ Future<PlaintextContent?> handleEncryptedMessage(
content.groupId,
content.groupJoin,
)) {
return PlaintextContent()
..retryControlError = PlaintextContent_RetryErrorMessage();
return (
null,
PlaintextContent()
..retryControlError = PlaintextContent_RetryErrorMessage()
);
}
return null;
return (null, null);
}
if (content.hasResendGroupPublicKey()) {
@ -321,7 +366,7 @@ Future<PlaintextContent?> handleEncryptedMessage(
content.groupId,
content.groupJoin,
);
return null;
return (null, null);
}
if (content.hasTextMessage()) {
@ -330,7 +375,7 @@ Future<PlaintextContent?> handleEncryptedMessage(
content.groupId,
content.textMessage,
);
return null;
return (null, null);
}
if (content.hasReaction()) {
@ -339,7 +384,7 @@ Future<PlaintextContent?> handleEncryptedMessage(
content.groupId,
content.reaction,
);
return null;
return (null, null);
}
if (content.hasMedia()) {
@ -348,8 +393,8 @@ Future<PlaintextContent?> handleEncryptedMessage(
content.groupId,
content.media,
);
return null;
return (null, null);
}
return null;
return (null, null);
}

View file

@ -83,13 +83,16 @@ class GroupContextMenu extends StatelessWidget {
);
if (ok) {
await twonlyDB.messagesDao.deleteMessagesByGroupId(group.groupId);
// await twonlyDB.groupsDao.deleteGroup(group.groupId);
await twonlyDB.groupsDao.updateGroup(
group.groupId,
const GroupsCompanion(
deletedContent: Value(true),
),
);
if (group.isDirectChat) {
await twonlyDB.groupsDao.deleteGroup(group.groupId);
} else {
await twonlyDB.groupsDao.updateGroup(
group.groupId,
const GroupsCompanion(
deletedContent: Value(true),
),
);
}
}
},
),

View file

@ -26,7 +26,48 @@ class ContactView extends StatefulWidget {
}
class _ContactViewState extends State<ContactView> {
Contact? _contact;
bool _contactIsStillAGroupMember = true;
late StreamSubscription<Contact?> _contactSub;
late StreamSubscription<List<GroupMember>> _groupMemberSub;
@override
void initState() {
_contactSub =
twonlyDB.contactsDao.watchContact(widget.userId).listen((update) {
setState(() {
_contact = update;
});
});
_groupMemberSub = twonlyDB.groupsDao
.watchContactGroupMember(widget.userId)
.listen((update) {
setState(() {
_contactIsStillAGroupMember = update.isNotEmpty;
});
});
super.initState();
}
@override
void dispose() {
_contactSub.cancel();
_groupMemberSub.cancel();
super.dispose();
}
Future<void> handleUserRemoveRequest(Contact contact) async {
if (_contactIsStillAGroupMember) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.lang.deleteUserErrorMessage),
duration: const Duration(seconds: 8),
),
);
return;
}
final remove = await showAlertDialog(
context,
context.lang
@ -84,128 +125,117 @@ class _ContactViewState extends State<ContactView> {
@override
Widget build(BuildContext context) {
final contact = twonlyDB.contactsDao
.getContactByUserId(widget.userId)
.watchSingleOrNull();
if (_contact == null) return Container();
final contact = _contact!;
return Scaffold(
appBar: AppBar(
title: const Text(''),
),
body: StreamBuilder(
stream: contact,
builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data == null) {
return Container();
}
final contact = snapshot.data!;
return ListView(
key: ValueKey(contact.userId),
body: ListView(
key: ValueKey(contact.userId),
children: [
Padding(
padding: const EdgeInsets.all(10),
child: AvatarIcon(contactId: contact.userId, fontSize: 30),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.all(10),
child: AvatarIcon(contactId: contact.userId, fontSize: 30),
padding: const EdgeInsets.only(right: 10),
child: VerifiedShield(
key: GlobalKey(),
contact: contact,
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.only(right: 10),
child: VerifiedShield(
key: GlobalKey(),
contact: contact,
),
),
Text(
getContactDisplayName(contact, maxLength: 20),
style: const TextStyle(fontSize: 20),
),
FlameCounterWidget(
contactId: contact.userId,
prefix: true,
),
],
Text(
getContactDisplayName(contact, maxLength: 20),
style: const TextStyle(fontSize: 20),
),
if (getContactDisplayName(contact) != contact.username)
Center(child: Text('(${contact.username})')),
const SizedBox(height: 50),
BetterListTile(
icon: FontAwesomeIcons.pencil,
text: context.lang.contactNickname,
onTap: () async {
final nickName =
await showNicknameChangeDialog(context, contact);
if (context.mounted && nickName != null && nickName != '') {
final update = ContactsCompanion(nickName: Value(nickName));
await twonlyDB.contactsDao
.updateContact(contact.userId, update);
}
},
FlameCounterWidget(
contactId: contact.userId,
prefix: true,
),
const Divider(),
SelectChatDeletionTimeListTitle(
groupId: getUUIDforDirectChat(widget.userId, gUser.userId),
),
const Divider(),
MaxFlameListTitle(
contactId: widget.userId,
),
BetterListTile(
icon: FontAwesomeIcons.shieldHeart,
text: context.lang.contactVerifyNumberTitle,
onTap: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return const PublicProfileView();
},
),
);
setState(() {});
},
),
// BetterListTile(
// icon: FontAwesomeIcons.eraser,
// iconSize: 16,
// text: context.lang.deleteAllContactMessages,
// onTap: () async {
// final block = await showAlertDialog(
// context,
// context.lang.deleteAllContactMessages,
// context.lang.deleteAllContactMessagesBody(
// getContactDisplayName(contact),
// ),
// );
// if (block) {
// if (context.mounted) {
// await twonlyDB.messagesDao
// .deleteMessagesByContactId(contact.userId);
// }
// }
// },
// ),
BetterListTile(
icon: FontAwesomeIcons.flag,
text: context.lang.reportUser,
onTap: () => handleReportUser(contact),
),
BetterListTile(
icon: FontAwesomeIcons.ban,
text: context.lang.contactBlock,
onTap: () => handleUserBlockRequest(contact),
),
// BetterListTile(
// icon: FontAwesomeIcons.userMinus,
// iconSize: 16,
// color: Colors.red,
// text: context.lang.contactRemove,
// onTap: () => handleUserRemoveRequest(contact),
// ),
],
);
},
),
if (getContactDisplayName(contact) != contact.username)
Center(child: Text('(${contact.username})')),
const SizedBox(height: 50),
BetterListTile(
icon: FontAwesomeIcons.pencil,
text: context.lang.contactNickname,
onTap: () async {
final nickName = await showNicknameChangeDialog(context, contact);
if (context.mounted && nickName != null && nickName != '') {
final update = ContactsCompanion(nickName: Value(nickName));
await twonlyDB.contactsDao
.updateContact(contact.userId, update);
}
},
),
const Divider(),
SelectChatDeletionTimeListTitle(
groupId: getUUIDforDirectChat(widget.userId, gUser.userId),
),
const Divider(),
MaxFlameListTitle(
contactId: widget.userId,
),
BetterListTile(
icon: FontAwesomeIcons.shieldHeart,
text: context.lang.contactVerifyNumberTitle,
onTap: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return const PublicProfileView();
},
),
);
setState(() {});
},
),
// BetterListTile(
// icon: FontAwesomeIcons.eraser,
// iconSize: 16,
// text: context.lang.deleteAllContactMessages,
// onTap: () async {
// final block = await showAlertDialog(
// context,
// context.lang.deleteAllContactMessages,
// context.lang.deleteAllContactMessagesBody(
// getContactDisplayName(contact),
// ),
// );
// if (block) {
// if (context.mounted) {
// await twonlyDB.messagesDao
// .deleteMessagesByContactId(contact.userId);
// }
// }
// },
// ),
BetterListTile(
icon: FontAwesomeIcons.flag,
text: context.lang.reportUser,
onTap: () => handleReportUser(contact),
),
BetterListTile(
icon: FontAwesomeIcons.ban,
text: context.lang.contactBlock,
onTap: () => handleUserBlockRequest(contact),
),
BetterListTile(
icon: FontAwesomeIcons.userMinus,
iconSize: 16,
color: Colors.red,
text: context.lang.contactRemove,
onTap: () => handleUserRemoveRequest(contact),
),
],
),
);
}

View file

@ -8,6 +8,7 @@ import 'schema_v2.dart' as v2;
import 'schema_v3.dart' as v3;
import 'schema_v4.dart' as v4;
import 'schema_v5.dart' as v5;
import 'schema_v6.dart' as v6;
class GeneratedHelper implements SchemaInstantiationHelper {
@override
@ -23,10 +24,12 @@ class GeneratedHelper implements SchemaInstantiationHelper {
return v4.DatabaseAtV4(db);
case 5:
return v5.DatabaseAtV5(db);
case 6:
return v6.DatabaseAtV6(db);
default:
throw MissingSchemaException(version, versions);
}
}
static const versions = const [1, 2, 3, 4, 5];
static const versions = const [1, 2, 3, 4, 5, 6];
}

File diff suppressed because it is too large Load diff