fix: group auto rentry when it was deleted
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run

This commit is contained in:
otsmr 2026-06-16 15:04:51 +02:00
parent e23ac5932e
commit e7301020f6
16 changed files with 392 additions and 162 deletions

View file

@ -368,10 +368,12 @@ class EncryptedContent_GroupCreate extends $pb.GeneratedMessage {
factory EncryptedContent_GroupCreate({ factory EncryptedContent_GroupCreate({
$core.List<$core.int>? stateKey, $core.List<$core.int>? stateKey,
$core.List<$core.int>? groupPublicKey, $core.List<$core.int>? groupPublicKey,
$core.String? groupName,
}) { }) {
final result = create(); final result = create();
if (stateKey != null) result.stateKey = stateKey; if (stateKey != null) result.stateKey = stateKey;
if (groupPublicKey != null) result.groupPublicKey = groupPublicKey; if (groupPublicKey != null) result.groupPublicKey = groupPublicKey;
if (groupName != null) result.groupName = groupName;
return result; return result;
} }
@ -391,6 +393,7 @@ class EncryptedContent_GroupCreate extends $pb.GeneratedMessage {
3, _omitFieldNames ? '' : 'stateKey', $pb.PbFieldType.OY) 3, _omitFieldNames ? '' : 'stateKey', $pb.PbFieldType.OY)
..a<$core.List<$core.int>>( ..a<$core.List<$core.int>>(
4, _omitFieldNames ? '' : 'groupPublicKey', $pb.PbFieldType.OY) 4, _omitFieldNames ? '' : 'groupPublicKey', $pb.PbFieldType.OY)
..aOS(5, _omitFieldNames ? '' : 'groupName')
..hasRequiredFields = false; ..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
@ -433,6 +436,15 @@ class EncryptedContent_GroupCreate extends $pb.GeneratedMessage {
$core.bool hasGroupPublicKey() => $_has(1); $core.bool hasGroupPublicKey() => $_has(1);
@$pb.TagNumber(4) @$pb.TagNumber(4)
void clearGroupPublicKey() => $_clearField(4); void clearGroupPublicKey() => $_clearField(4);
@$pb.TagNumber(5)
$core.String get groupName => $_getSZ(2);
@$pb.TagNumber(5)
set groupName($core.String value) => $_setString(2, value);
@$pb.TagNumber(5)
$core.bool hasGroupName() => $_has(2);
@$pb.TagNumber(5)
void clearGroupName() => $_clearField(5);
} }
class EncryptedContent_GroupJoin extends $pb.GeneratedMessage { class EncryptedContent_GroupJoin extends $pb.GeneratedMessage {

View file

@ -79,16 +79,20 @@ class EncryptedContent_ErrorMessages_Type extends $pb.ProtobufEnum {
static const EncryptedContent_ErrorMessages_Type SESSION_OUT_OF_SYNC = static const EncryptedContent_ErrorMessages_Type SESSION_OUT_OF_SYNC =
EncryptedContent_ErrorMessages_Type._( EncryptedContent_ErrorMessages_Type._(
3, _omitEnumNames ? '' : 'SESSION_OUT_OF_SYNC'); 3, _omitEnumNames ? '' : 'SESSION_OUT_OF_SYNC');
static const EncryptedContent_ErrorMessages_Type
GROUP_NOT_FOUND_OR_NOT_A_MEMBER = EncryptedContent_ErrorMessages_Type._(
4, _omitEnumNames ? '' : 'GROUP_NOT_FOUND_OR_NOT_A_MEMBER');
static const $core.List<EncryptedContent_ErrorMessages_Type> values = static const $core.List<EncryptedContent_ErrorMessages_Type> values =
<EncryptedContent_ErrorMessages_Type>[ <EncryptedContent_ErrorMessages_Type>[
ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD, ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD,
UNKNOWN_MESSAGE_TYPE, UNKNOWN_MESSAGE_TYPE,
SESSION_OUT_OF_SYNC, SESSION_OUT_OF_SYNC,
GROUP_NOT_FOUND_OR_NOT_A_MEMBER,
]; ];
static final $core.List<EncryptedContent_ErrorMessages_Type?> _byValue = static final $core.List<EncryptedContent_ErrorMessages_Type?> _byValue =
$pb.ProtobufEnum.$_initByValueList(values, 3); $pb.ProtobufEnum.$_initByValueList(values, 4);
static EncryptedContent_ErrorMessages_Type? valueOf($core.int value) => static EncryptedContent_ErrorMessages_Type? valueOf($core.int value) =>
value < 0 || value >= _byValue.length ? null : _byValue[value]; value < 0 || value >= _byValue.length ? null : _byValue[value];

View file

@ -465,6 +465,7 @@ const EncryptedContent_ErrorMessages_Type$json = {
{'1': 'ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD', '2': 0}, {'1': 'ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD', '2': 0},
{'1': 'UNKNOWN_MESSAGE_TYPE', '2': 2}, {'1': 'UNKNOWN_MESSAGE_TYPE', '2': 2},
{'1': 'SESSION_OUT_OF_SYNC', '2': 3}, {'1': 'SESSION_OUT_OF_SYNC', '2': 3},
{'1': 'GROUP_NOT_FOUND_OR_NOT_A_MEMBER', '2': 4},
], ],
}; };
@ -474,6 +475,18 @@ const EncryptedContent_GroupCreate$json = {
'2': [ '2': [
{'1': 'state_key', '3': 3, '4': 1, '5': 12, '10': 'stateKey'}, {'1': 'state_key', '3': 3, '4': 1, '5': 12, '10': 'stateKey'},
{'1': 'group_public_key', '3': 4, '4': 1, '5': 12, '10': 'groupPublicKey'}, {'1': 'group_public_key', '3': 4, '4': 1, '5': 12, '10': 'groupPublicKey'},
{
'1': 'group_name',
'3': 5,
'4': 1,
'5': 9,
'9': 0,
'10': 'groupName',
'17': true
},
],
'8': [
{'1': '_group_name'},
], ],
}; };
@ -975,78 +988,79 @@ final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode(
'aXNjb3ZlcnlSZXF1ZXN0iAEBEl4KFXVzZXJfZGlzY292ZXJ5X3VwZGF0ZRgXIAEoCzIlLkVuY3' 'aXNjb3ZlcnlSZXF1ZXN0iAEBEl4KFXVzZXJfZGlzY292ZXJ5X3VwZGF0ZRgXIAEoCzIlLkVuY3'
'J5cHRlZENvbnRlbnQuVXNlckRpc2NvdmVyeVVwZGF0ZUgWUhN1c2VyRGlzY292ZXJ5VXBkYXRl' 'J5cHRlZENvbnRlbnQuVXNlckRpc2NvdmVyeVVwZGF0ZUgWUhN1c2VyRGlzY292ZXJ5VXBkYXRl'
'iAEBEmEKFmtleV92ZXJpZmljYXRpb25fcHJvb2YYGCABKAsyJi5FbmNyeXB0ZWRDb250ZW50Lk' 'iAEBEmEKFmtleV92ZXJpZmljYXRpb25fcHJvb2YYGCABKAsyJi5FbmNyeXB0ZWRDb250ZW50Lk'
'tleVZlcmlmaWNhdGlvblByb29mSBdSFGtleVZlcmlmaWNhdGlvblByb29miAEBGvABCg1FcnJv' 'tleVZlcmlmaWNhdGlvblByb29mSBdSFGtleVZlcmlmaWNhdGlvblByb29miAEBGpYCCg1FcnJv'
'ck1lc3NhZ2VzEjgKBHR5cGUYASABKA4yJC5FbmNyeXB0ZWRDb250ZW50LkVycm9yTWVzc2FnZX' 'ck1lc3NhZ2VzEjgKBHR5cGUYASABKA4yJC5FbmNyeXB0ZWRDb250ZW50LkVycm9yTWVzc2FnZX'
'MuVHlwZVIEdHlwZRIsChJyZWxhdGVkX3JlY2VpcHRfaWQYAiABKAlSEHJlbGF0ZWRSZWNlaXB0' 'MuVHlwZVIEdHlwZRIsChJyZWxhdGVkX3JlY2VpcHRfaWQYAiABKAlSEHJlbGF0ZWRSZWNlaXB0'
'SWQidwoEVHlwZRI8CjhFUlJPUl9QUk9DRVNTSU5HX01FU1NBR0VfQ1JFQVRFRF9BQ0NPVU5UX1' 'SWQinAEKBFR5cGUSPAo4RVJST1JfUFJPQ0VTU0lOR19NRVNTQUdFX0NSRUFURURfQUNDT1VOVF'
'JFUVVFU1RfSU5TVEVBRBAAEhgKFFVOS05PV05fTUVTU0FHRV9UWVBFEAISFwoTU0VTU0lPTl9P' '9SRVFVRVNUX0lOU1RFQUQQABIYChRVTktOT1dOX01FU1NBR0VfVFlQRRACEhcKE1NFU1NJT05f'
'VVRfT0ZfU1lOQxADGlQKC0dyb3VwQ3JlYXRlEhsKCXN0YXRlX2tleRgDIAEoDFIIc3RhdGVLZX' 'T1VUX09GX1NZTkMQAxIjCh9HUk9VUF9OT1RfRk9VTkRfT1JfTk9UX0FfTUVNQkVSEAQahwEKC0'
'kSKAoQZ3JvdXBfcHVibGljX2tleRgEIAEoDFIOZ3JvdXBQdWJsaWNLZXkaNQoJR3JvdXBKb2lu' 'dyb3VwQ3JlYXRlEhsKCXN0YXRlX2tleRgDIAEoDFIIc3RhdGVLZXkSKAoQZ3JvdXBfcHVibGlj'
'EigKEGdyb3VwX3B1YmxpY19rZXkYASABKAxSDmdyb3VwUHVibGljS2V5GhYKFFJlc2VuZEdyb3' 'X2tleRgEIAEoDFIOZ3JvdXBQdWJsaWNLZXkSIgoKZ3JvdXBfbmFtZRgFIAEoCUgAUglncm91cE'
'VwUHVibGljS2V5GsgCCgtHcm91cFVwZGF0ZRIqChFncm91cF9hY3Rpb25fdHlwZRgBIAEoCVIP' '5hbWWIAQFCDQoLX2dyb3VwX25hbWUaNQoJR3JvdXBKb2luEigKEGdyb3VwX3B1YmxpY19rZXkY'
'Z3JvdXBBY3Rpb25UeXBlEjMKE2FmZmVjdGVkX2NvbnRhY3RfaWQYAiABKANIAFIRYWZmZWN0ZW' 'ASABKAxSDmdyb3VwUHVibGljS2V5GhYKFFJlc2VuZEdyb3VwUHVibGljS2V5GsgCCgtHcm91cF'
'RDb250YWN0SWSIAQESKQoObmV3X2dyb3VwX25hbWUYAyABKAlIAVIMbmV3R3JvdXBOYW1liAEB' 'VwZGF0ZRIqChFncm91cF9hY3Rpb25fdHlwZRgBIAEoCVIPZ3JvdXBBY3Rpb25UeXBlEjMKE2Fm'
'ElcKJm5ld19kZWxldGVfbWVzc2FnZXNfYWZ0ZXJfbWlsbGlzZWNvbmRzGAQgASgDSAJSIm5ld0' 'ZmVjdGVkX2NvbnRhY3RfaWQYAiABKANIAFIRYWZmZWN0ZWRDb250YWN0SWSIAQESKQoObmV3X2'
'RlbGV0ZU1lc3NhZ2VzQWZ0ZXJNaWxsaXNlY29uZHOIAQFCFgoUX2FmZmVjdGVkX2NvbnRhY3Rf' 'dyb3VwX25hbWUYAyABKAlIAVIMbmV3R3JvdXBOYW1liAEBElcKJm5ld19kZWxldGVfbWVzc2Fn'
'aWRCEQoPX25ld19ncm91cF9uYW1lQikKJ19uZXdfZGVsZXRlX21lc3NhZ2VzX2FmdGVyX21pbG' 'ZXNfYWZ0ZXJfbWlsbGlzZWNvbmRzGAQgASgDSAJSIm5ld0RlbGV0ZU1lc3NhZ2VzQWZ0ZXJNaW'
'xpc2Vjb25kcxqvAQoLVGV4dE1lc3NhZ2USKgoRc2VuZGVyX21lc3NhZ2VfaWQYASABKAlSD3Nl' 'xsaXNlY29uZHOIAQFCFgoUX2FmZmVjdGVkX2NvbnRhY3RfaWRCEQoPX25ld19ncm91cF9uYW1l'
'bmRlck1lc3NhZ2VJZBISCgR0ZXh0GAIgASgJUgR0ZXh0EhwKCXRpbWVzdGFtcBgDIAEoA1IJdG' 'QikKJ19uZXdfZGVsZXRlX21lc3NhZ2VzX2FmdGVyX21pbGxpc2Vjb25kcxqvAQoLVGV4dE1lc3'
'ltZXN0YW1wEi0KEHF1b3RlX21lc3NhZ2VfaWQYBCABKAlIAFIOcXVvdGVNZXNzYWdlSWSIAQFC' 'NhZ2USKgoRc2VuZGVyX21lc3NhZ2VfaWQYASABKAlSD3NlbmRlck1lc3NhZ2VJZBISCgR0ZXh0'
'EwoRX3F1b3RlX21lc3NhZ2VfaWQazgEKFUFkZGl0aW9uYWxEYXRhTWVzc2FnZRIqChFzZW5kZX' 'GAIgASgJUgR0ZXh0EhwKCXRpbWVzdGFtcBgDIAEoA1IJdGltZXN0YW1wEi0KEHF1b3RlX21lc3'
'JfbWVzc2FnZV9pZBgBIAEoCVIPc2VuZGVyTWVzc2FnZUlkEhwKCXRpbWVzdGFtcBgCIAEoA1IJ' 'NhZ2VfaWQYBCABKAlIAFIOcXVvdGVNZXNzYWdlSWSIAQFCEwoRX3F1b3RlX21lc3NhZ2VfaWQa'
'dGltZXN0YW1wEhIKBHR5cGUYAyABKAlSBHR5cGUSOwoXYWRkaXRpb25hbF9tZXNzYWdlX2RhdG' 'zgEKFUFkZGl0aW9uYWxEYXRhTWVzc2FnZRIqChFzZW5kZXJfbWVzc2FnZV9pZBgBIAEoCVIPc2'
'EYBCABKAxIAFIVYWRkaXRpb25hbE1lc3NhZ2VEYXRhiAEBQhoKGF9hZGRpdGlvbmFsX21lc3Nh' 'VuZGVyTWVzc2FnZUlkEhwKCXRpbWVzdGFtcBgCIAEoA1IJdGltZXN0YW1wEhIKBHR5cGUYAyAB'
'Z2VfZGF0YRpkCghSZWFjdGlvbhIqChF0YXJnZXRfbWVzc2FnZV9pZBgBIAEoCVIPdGFyZ2V0TW' 'KAlSBHR5cGUSOwoXYWRkaXRpb25hbF9tZXNzYWdlX2RhdGEYBCABKAxIAFIVYWRkaXRpb25hbE'
'Vzc2FnZUlkEhQKBWVtb2ppGAIgASgJUgVlbW9qaRIWCgZyZW1vdmUYAyABKAhSBnJlbW92ZRq+' '1lc3NhZ2VEYXRhiAEBQhoKGF9hZGRpdGlvbmFsX21lc3NhZ2VfZGF0YRpkCghSZWFjdGlvbhIq'
'AgoNTWVzc2FnZVVwZGF0ZRI4CgR0eXBlGAEgASgOMiQuRW5jcnlwdGVkQ29udGVudC5NZXNzYW' 'ChF0YXJnZXRfbWVzc2FnZV9pZBgBIAEoCVIPdGFyZ2V0TWVzc2FnZUlkEhQKBWVtb2ppGAIgAS'
'dlVXBkYXRlLlR5cGVSBHR5cGUSLwoRc2VuZGVyX21lc3NhZ2VfaWQYAiABKAlIAFIPc2VuZGVy' 'gJUgVlbW9qaRIWCgZyZW1vdmUYAyABKAhSBnJlbW92ZRq+AgoNTWVzc2FnZVVwZGF0ZRI4CgR0'
'TWVzc2FnZUlkiAEBEj0KG211bHRpcGxlX3RhcmdldF9tZXNzYWdlX2lkcxgDIAMoCVIYbXVsdG' 'eXBlGAEgASgOMiQuRW5jcnlwdGVkQ29udGVudC5NZXNzYWdlVXBkYXRlLlR5cGVSBHR5cGUSLw'
'lwbGVUYXJnZXRNZXNzYWdlSWRzEhcKBHRleHQYBCABKAlIAVIEdGV4dIgBARIcCgl0aW1lc3Rh' 'oRc2VuZGVyX21lc3NhZ2VfaWQYAiABKAlIAFIPc2VuZGVyTWVzc2FnZUlkiAEBEj0KG211bHRp'
'bXAYBSABKANSCXRpbWVzdGFtcCItCgRUeXBlEgoKBkRFTEVURRAAEg0KCUVESVRfVEVYVBABEg' 'cGxlX3RhcmdldF9tZXNzYWdlX2lkcxgDIAMoCVIYbXVsdGlwbGVUYXJnZXRNZXNzYWdlSWRzEh'
'oKBk9QRU5FRBACQhQKEl9zZW5kZXJfbWVzc2FnZV9pZEIHCgVfdGV4dBqFBgoFTWVkaWESKgoR' 'cKBHRleHQYBCABKAlIAVIEdGV4dIgBARIcCgl0aW1lc3RhbXAYBSABKANSCXRpbWVzdGFtcCIt'
'c2VuZGVyX21lc3NhZ2VfaWQYASABKAlSD3NlbmRlck1lc3NhZ2VJZBIwCgR0eXBlGAIgASgOMh' 'CgRUeXBlEgoKBkRFTEVURRAAEg0KCUVESVRfVEVYVBABEgoKBk9QRU5FRBACQhQKEl9zZW5kZX'
'wuRW5jcnlwdGVkQ29udGVudC5NZWRpYS5UeXBlUgR0eXBlEkYKHWRpc3BsYXlfbGltaXRfaW5f' 'JfbWVzc2FnZV9pZEIHCgVfdGV4dBqFBgoFTWVkaWESKgoRc2VuZGVyX21lc3NhZ2VfaWQYASAB'
'bWlsbGlzZWNvbmRzGAMgASgDSABSGmRpc3BsYXlMaW1pdEluTWlsbGlzZWNvbmRziAEBEjcKF3' 'KAlSD3NlbmRlck1lc3NhZ2VJZBIwCgR0eXBlGAIgASgOMhwuRW5jcnlwdGVkQ29udGVudC5NZW'
'JlcXVpcmVzX2F1dGhlbnRpY2F0aW9uGAQgASgIUhZyZXF1aXJlc0F1dGhlbnRpY2F0aW9uEhwK' 'RpYS5UeXBlUgR0eXBlEkYKHWRpc3BsYXlfbGltaXRfaW5fbWlsbGlzZWNvbmRzGAMgASgDSABS'
'CXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1wEi0KEHF1b3RlX21lc3NhZ2VfaWQYBiABKAlIAV' 'GmRpc3BsYXlMaW1pdEluTWlsbGlzZWNvbmRziAEBEjcKF3JlcXVpcmVzX2F1dGhlbnRpY2F0aW'
'IOcXVvdGVNZXNzYWdlSWSIAQESKgoOZG93bmxvYWRfdG9rZW4YByABKAxIAlINZG93bmxvYWRU' '9uGAQgASgIUhZyZXF1aXJlc0F1dGhlbnRpY2F0aW9uEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGlt'
'b2tlbogBARIqCg5lbmNyeXB0aW9uX2tleRgIIAEoDEgDUg1lbmNyeXB0aW9uS2V5iAEBEioKDm' 'ZXN0YW1wEi0KEHF1b3RlX21lc3NhZ2VfaWQYBiABKAlIAVIOcXVvdGVNZXNzYWdlSWSIAQESKg'
'VuY3J5cHRpb25fbWFjGAkgASgMSARSDWVuY3J5cHRpb25NYWOIAQESLgoQZW5jcnlwdGlvbl9u' 'oOZG93bmxvYWRfdG9rZW4YByABKAxIAlINZG93bmxvYWRUb2tlbogBARIqCg5lbmNyeXB0aW9u'
'b25jZRgKIAEoDEgFUg9lbmNyeXB0aW9uTm9uY2WIAQESOwoXYWRkaXRpb25hbF9tZXNzYWdlX2' 'X2tleRgIIAEoDEgDUg1lbmNyeXB0aW9uS2V5iAEBEioKDmVuY3J5cHRpb25fbWFjGAkgASgMSA'
'RhdGEYCyABKAxIBlIVYWRkaXRpb25hbE1lc3NhZ2VEYXRhiAEBIj4KBFR5cGUSDAoIUkVVUExP' 'RSDWVuY3J5cHRpb25NYWOIAQESLgoQZW5jcnlwdGlvbl9ub25jZRgKIAEoDEgFUg9lbmNyeXB0'
'QUQQABIJCgVJTUFHRRABEgkKBVZJREVPEAISBwoDR0lGEAMSCQoFQVVESU8QBEIgCh5fZGlzcG' 'aW9uTm9uY2WIAQESOwoXYWRkaXRpb25hbF9tZXNzYWdlX2RhdGEYCyABKAxIBlIVYWRkaXRpb2'
'xheV9saW1pdF9pbl9taWxsaXNlY29uZHNCEwoRX3F1b3RlX21lc3NhZ2VfaWRCEQoPX2Rvd25s' '5hbE1lc3NhZ2VEYXRhiAEBIj4KBFR5cGUSDAoIUkVVUExPQUQQABIJCgVJTUFHRRABEgkKBVZJ'
'b2FkX3Rva2VuQhEKD19lbmNyeXB0aW9uX2tleUIRCg9fZW5jcnlwdGlvbl9tYWNCEwoRX2VuY3' 'REVPEAISBwoDR0lGEAMSCQoFQVVESU8QBEIgCh5fZGlzcGxheV9saW1pdF9pbl9taWxsaXNlY2'
'J5cHRpb25fbm9uY2VCGgoYX2FkZGl0aW9uYWxfbWVzc2FnZV9kYXRhGqkBCgtNZWRpYVVwZGF0' '9uZHNCEwoRX3F1b3RlX21lc3NhZ2VfaWRCEQoPX2Rvd25sb2FkX3Rva2VuQhEKD19lbmNyeXB0'
'ZRI2CgR0eXBlGAEgASgOMiIuRW5jcnlwdGVkQ29udGVudC5NZWRpYVVwZGF0ZS5UeXBlUgR0eX' 'aW9uX2tleUIRCg9fZW5jcnlwdGlvbl9tYWNCEwoRX2VuY3J5cHRpb25fbm9uY2VCGgoYX2FkZG'
'BlEioKEXRhcmdldF9tZXNzYWdlX2lkGAIgASgJUg90YXJnZXRNZXNzYWdlSWQiNgoEVHlwZRIM' 'l0aW9uYWxfbWVzc2FnZV9kYXRhGqkBCgtNZWRpYVVwZGF0ZRI2CgR0eXBlGAEgASgOMiIuRW5j'
'CghSRU9QRU5FRBAAEgoKBlNUT1JFRBABEhQKEERFQ1JZUFRJT05fRVJST1IQAhp4Cg5Db250YW' 'cnlwdGVkQ29udGVudC5NZWRpYVVwZGF0ZS5UeXBlUgR0eXBlEioKEXRhcmdldF9tZXNzYWdlX2'
'N0UmVxdWVzdBI5CgR0eXBlGAEgASgOMiUuRW5jcnlwdGVkQ29udGVudC5Db250YWN0UmVxdWVz' 'lkGAIgASgJUg90YXJnZXRNZXNzYWdlSWQiNgoEVHlwZRIMCghSRU9QRU5FRBAAEgoKBlNUT1JF'
'dC5UeXBlUgR0eXBlIisKBFR5cGUSCwoHUkVRVUVTVBAAEgoKBlJFSkVDVBABEgoKBkFDQ0VQVB' 'RBABEhQKEERFQ1JZUFRJT05fRVJST1IQAhp4Cg5Db250YWN0UmVxdWVzdBI5CgR0eXBlGAEgAS'
'ACGqQCCg1Db250YWN0VXBkYXRlEjgKBHR5cGUYASABKA4yJC5FbmNyeXB0ZWRDb250ZW50LkNv' 'gOMiUuRW5jcnlwdGVkQ29udGVudC5Db250YWN0UmVxdWVzdC5UeXBlUgR0eXBlIisKBFR5cGUS'
'bnRhY3RVcGRhdGUuVHlwZVIEdHlwZRI3ChVhdmF0YXJfc3ZnX2NvbXByZXNzZWQYAiABKAxIAF' 'CwoHUkVRVUVTVBAAEgoKBlJFSkVDVBABEgoKBkFDQ0VQVBACGqQCCg1Db250YWN0VXBkYXRlEj'
'ITYXZhdGFyU3ZnQ29tcHJlc3NlZIgBARIfCgh1c2VybmFtZRgDIAEoCUgBUgh1c2VybmFtZYgB' 'gKBHR5cGUYASABKA4yJC5FbmNyeXB0ZWRDb250ZW50LkNvbnRhY3RVcGRhdGUuVHlwZVIEdHlw'
'ARImCgxkaXNwbGF5X25hbWUYBCABKAlIAlILZGlzcGxheU5hbWWIAQEiHwoEVHlwZRILCgdSRV' 'ZRI3ChVhdmF0YXJfc3ZnX2NvbXByZXNzZWQYAiABKAxIAFITYXZhdGFyU3ZnQ29tcHJlc3NlZI'
'FVRVNUEAASCgoGVVBEQVRFEAFCGAoWX2F2YXRhcl9zdmdfY29tcHJlc3NlZEILCglfdXNlcm5h' 'gBARIfCgh1c2VybmFtZRgDIAEoCUgBUgh1c2VybmFtZYgBARImCgxkaXNwbGF5X25hbWUYBCAB'
'bWVCDwoNX2Rpc3BsYXlfbmFtZRrZAQoIUHVzaEtleXMSMwoEdHlwZRgBIAEoDjIfLkVuY3J5cH' 'KAlIAlILZGlzcGxheU5hbWWIAQEiHwoEVHlwZRILCgdSRVFVRVNUEAASCgoGVVBEQVRFEAFCGA'
'RlZENvbnRlbnQuUHVzaEtleXMuVHlwZVIEdHlwZRIaCgZrZXlfaWQYAiABKANIAFIFa2V5SWSI' 'oWX2F2YXRhcl9zdmdfY29tcHJlc3NlZEILCglfdXNlcm5hbWVCDwoNX2Rpc3BsYXlfbmFtZRrZ'
'AQESFQoDa2V5GAMgASgMSAFSA2tleYgBARIiCgpjcmVhdGVkX2F0GAQgASgDSAJSCWNyZWF0ZW' 'AQoIUHVzaEtleXMSMwoEdHlwZRgBIAEoDjIfLkVuY3J5cHRlZENvbnRlbnQuUHVzaEtleXMuVH'
'RBdIgBASIfCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZVUERBVEUQAUIJCgdfa2V5X2lkQgYKBF9r' 'lwZVIEdHlwZRIaCgZrZXlfaWQYAiABKANIAFIFa2V5SWSIAQESFQoDa2V5GAMgASgMSAFSA2tl'
'ZXlCDQoLX2NyZWF0ZWRfYXQarwEKCUZsYW1lU3luYxIjCg1mbGFtZV9jb3VudGVyGAEgASgDUg' 'eYgBARIiCgpjcmVhdGVkX2F0GAQgASgDSAJSCWNyZWF0ZWRBdIgBASIfCgRUeXBlEgsKB1JFUV'
'xmbGFtZUNvdW50ZXISOQoZbGFzdF9mbGFtZV9jb3VudGVyX2NoYW5nZRgCIAEoA1IWbGFzdEZs' 'VFU1QQABIKCgZVUERBVEUQAUIJCgdfa2V5X2lkQgYKBF9rZXlCDQoLX2NyZWF0ZWRfYXQarwEK'
'YW1lQ291bnRlckNoYW5nZRIfCgtiZXN0X2ZyaWVuZBgDIAEoCFIKYmVzdEZyaWVuZBIhCgxmb3' 'CUZsYW1lU3luYxIjCg1mbGFtZV9jb3VudGVyGAEgASgDUgxmbGFtZUNvdW50ZXISOQoZbGFzdF'
'JjZV91cGRhdGUYBCABKAhSC2ZvcmNlVXBkYXRlGk0KD1R5cGluZ0luZGljYXRvchIbCglpc190' '9mbGFtZV9jb3VudGVyX2NoYW5nZRgCIAEoA1IWbGFzdEZsYW1lQ291bnRlckNoYW5nZRIfCgti'
'eXBpbmcYASABKAhSCGlzVHlwaW5nEh0KCmNyZWF0ZWRfYXQYAiABKANSCWNyZWF0ZWRBdBo/Ch' 'ZXN0X2ZyaWVuZBgDIAEoCFIKYmVzdEZyaWVuZBIhCgxmb3JjZV91cGRhdGUYBCABKAhSC2Zvcm'
'RVc2VyRGlzY292ZXJ5UmVxdWVzdBInCg9jdXJyZW50X3ZlcnNpb24YASABKAxSDmN1cnJlbnRW' 'NlVXBkYXRlGk0KD1R5cGluZ0luZGljYXRvchIbCglpc190eXBpbmcYASABKAhSCGlzVHlwaW5n'
'ZXJzaW9uGjEKE1VzZXJEaXNjb3ZlcnlVcGRhdGUSGgoIbWVzc2FnZXMYASADKAxSCG1lc3NhZ2' 'Eh0KCmNyZWF0ZWRfYXQYAiABKANSCWNyZWF0ZWRBdBo/ChRVc2VyRGlzY292ZXJ5UmVxdWVzdB'
'VzGj0KFEtleVZlcmlmaWNhdGlvblByb29mEiUKDmNhbGN1bGF0ZWRfbWFjGAEgASgMUg1jYWxj' 'InCg9jdXJyZW50X3ZlcnNpb24YASABKAxSDmN1cnJlbnRWZXJzaW9uGjEKE1VzZXJEaXNjb3Zl'
'dWxhdGVkTWFjQgsKCV9ncm91cF9pZEIRCg9faXNfZGlyZWN0X2NoYXRCGQoXX3NlbmRlcl9wcm' 'cnlVcGRhdGUSGgoIbWVzc2FnZXMYASADKAxSCG1lc3NhZ2VzGj0KFEtleVZlcmlmaWNhdGlvbl'
'9maWxlX2NvdW50ZXJCIAoeX3NlbmRlcl91c2VyX2Rpc2NvdmVyeV92ZXJzaW9uQhwKGl9hc2tf' 'Byb29mEiUKDmNhbGN1bGF0ZWRfbWFjGAEgASgMUg1jYWxjdWxhdGVkTWFjQgsKCV9ncm91cF9p'
'Zm9yX2ZyaWVuZF9wcm9tb3Rpb25zQhEKD19tZXNzYWdlX3VwZGF0ZUIICgZfbWVkaWFCDwoNX2' 'ZEIRCg9faXNfZGlyZWN0X2NoYXRCGQoXX3NlbmRlcl9wcm9maWxlX2NvdW50ZXJCIAoeX3Nlbm'
'1lZGlhX3VwZGF0ZUIRCg9fY29udGFjdF91cGRhdGVCEgoQX2NvbnRhY3RfcmVxdWVzdEINCgtf' 'Rlcl91c2VyX2Rpc2NvdmVyeV92ZXJzaW9uQhwKGl9hc2tfZm9yX2ZyaWVuZF9wcm9tb3Rpb25z'
'ZmxhbWVfc3luY0IMCgpfcHVzaF9rZXlzQgsKCV9yZWFjdGlvbkIPCg1fdGV4dF9tZXNzYWdlQg' 'QhEKD19tZXNzYWdlX3VwZGF0ZUIICgZfbWVkaWFCDwoNX21lZGlhX3VwZGF0ZUIRCg9fY29udG'
'8KDV9ncm91cF9jcmVhdGVCDQoLX2dyb3VwX2pvaW5CDwoNX2dyb3VwX3VwZGF0ZUIaChhfcmVz' 'FjdF91cGRhdGVCEgoQX2NvbnRhY3RfcmVxdWVzdEINCgtfZmxhbWVfc3luY0IMCgpfcHVzaF9r'
'ZW5kX2dyb3VwX3B1YmxpY19rZXlCEQoPX2Vycm9yX21lc3NhZ2VzQhoKGF9hZGRpdGlvbmFsX2' 'ZXlzQgsKCV9yZWFjdGlvbkIPCg1fdGV4dF9tZXNzYWdlQg8KDV9ncm91cF9jcmVhdGVCDQoLX2'
'RhdGFfbWVzc2FnZUITChFfdHlwaW5nX2luZGljYXRvckIZChdfdXNlcl9kaXNjb3ZlcnlfcmVx' 'dyb3VwX2pvaW5CDwoNX2dyb3VwX3VwZGF0ZUIaChhfcmVzZW5kX2dyb3VwX3B1YmxpY19rZXlC'
'dWVzdEIYChZfdXNlcl9kaXNjb3ZlcnlfdXBkYXRlQhkKF19rZXlfdmVyaWZpY2F0aW9uX3Byb2' 'EQoPX2Vycm9yX21lc3NhZ2VzQhoKGF9hZGRpdGlvbmFsX2RhdGFfbWVzc2FnZUITChFfdHlwaW'
'9m'); '5nX2luZGljYXRvckIZChdfdXNlcl9kaXNjb3ZlcnlfcmVxdWVzdEIYChZfdXNlcl9kaXNjb3Zl'
'cnlfdXBkYXRlQhkKF19rZXlfdmVyaWZpY2F0aW9uX3Byb29m');

View file

@ -64,6 +64,7 @@ message EncryptedContent {
ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD = 0; ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD = 0;
UNKNOWN_MESSAGE_TYPE = 2; UNKNOWN_MESSAGE_TYPE = 2;
SESSION_OUT_OF_SYNC = 3; SESSION_OUT_OF_SYNC = 3;
GROUP_NOT_FOUND_OR_NOT_A_MEMBER = 4;
} }
Type type = 1; Type type = 1;
string related_receipt_id = 2; string related_receipt_id = 2;
@ -73,6 +74,7 @@ message EncryptedContent {
// key for the state stored on the server // key for the state stored on the server
bytes state_key = 3; bytes state_key = 3;
bytes group_public_key = 4; bytes group_public_key = 4;
optional string group_name = 5;
} }
message GroupJoin { message GroupJoin {

View file

@ -1,15 +1,22 @@
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:drift/drift.dart' show Value; import 'package:drift/drift.dart' show Value;
import 'package:fixnum/fixnum.dart' show Int64;
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'
show IdentityKeyPair;
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart';
import 'package:twonly/src/services/api/messages.api.dart'
show sendCipherText, tryToSendCompleteMessage;
import 'package:twonly/src/services/group.service.dart' show fetchGroupState;
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
Future<void> handleErrorMessage( Future<void> handleErrorMessage(
int fromUserId, int fromUserId,
EncryptedContent_ErrorMessages error, EncryptedContent_ErrorMessages error,
String receiptId, String receiptId, {
) async { String? groupId,
}) async {
Log.error('[$receiptId] Got error from $fromUserId: $error'); Log.error('[$receiptId] Got error from $fromUserId: $error');
switch (error.type) { switch (error.type) {
@ -29,6 +36,59 @@ Future<void> handleErrorMessage(
); );
case EncryptedContent_ErrorMessages_Type.SESSION_OUT_OF_SYNC: case EncryptedContent_ErrorMessages_Type.SESSION_OUT_OF_SYNC:
break; // The other user initiated a new signal session, so ignore the error in this case, as the new session works... break; // The other user initiated a new signal session, so ignore the error in this case, as the new session works...
case EncryptedContent_ErrorMessages_Type.GROUP_NOT_FOUND_OR_NOT_A_MEMBER:
if (groupId == null) {
Log.error(
'[$receiptId] GROUP_NOT_FOUND_OR_NOT_A_MEMBER error received, but groupId is null.',
);
return;
}
final group = await twonlyDB.groupsDao.getGroup(groupId);
if (group == null) {
Log.error(
'[$receiptId] GROUP_NOT_FOUND_OR_NOT_A_MEMBER error received, but group $groupId is not found in database.',
);
return;
}
// Update group state from the server to ensure the user is still part of the group...
final updatedState = await fetchGroupState(group);
if (updatedState != null) {
final (_, state) = updatedState;
final isStillMember = state.memberIds.contains(Int64(fromUserId));
if (isStillMember) {
final keyPair = IdentityKeyPair.fromSerialized(
group.myGroupPrivateKey!,
);
await sendCipherText(
fromUserId,
EncryptedContent(
groupId: groupId,
groupCreate: EncryptedContent_GroupCreate(
stateKey: group.stateEncryptionKey,
groupPublicKey: keyPair.getPublicKey().serialize(),
),
),
);
}
final r = await twonlyDB.receiptsDao.getReceiptById(
error.relatedReceiptId,
);
if (r != null) {
await twonlyDB.receiptsDao.updateReceiptWidthUserId(
fromUserId,
error.relatedReceiptId,
ReceiptsCompanion(
markForRetry: Value(clock.now()),
retryCount: Value(r.retryCount + 1),
),
);
}
// then resend: error.relatedReceiptId
await tryToSendCompleteMessage(
receiptId: error.relatedReceiptId,
blocking: false,
);
} else {}
// ignore: no_default_cases // ignore: no_default_cases
default: default:
break; break;

View file

@ -17,7 +17,9 @@ Future<void> handleGroupCreate(
EncryptedContent_GroupCreate newGroup, EncryptedContent_GroupCreate newGroup,
String receiptId, String receiptId,
) async { ) async {
final user = await twonlyDB.contactsDao.getContactByUserId(fromUserId).getSingleOrNull(); final user = await twonlyDB.contactsDao
.getContactByUserId(fromUserId)
.getSingleOrNull();
if (user == null) { if (user == null) {
// Only contacts can invite other contacts, so this can (via the UI) not happen. // Only contacts can invite other contacts, so this can (via the UI) not happen.
Log.error( Log.error(
@ -43,11 +45,26 @@ Future<void> handleGroupCreate(
stateVersionId: const Value(0), stateVersionId: const Value(0),
stateEncryptionKey: Value(Uint8List.fromList(newGroup.stateKey)), stateEncryptionKey: Value(Uint8List.fromList(newGroup.stateKey)),
myGroupPrivateKey: Value(myGroupKey.serialize()), myGroupPrivateKey: Value(myGroupKey.serialize()),
groupName: const Value(''), groupName: Value(newGroup.hasGroupName() ? newGroup.groupName : ''),
joinedGroup: const Value(false), joinedGroup: const Value(false),
), ),
); );
} else { } else {
// In this case make a group state update and check if the fromUserId is still a admin. otherwise return with an log error message
final updatedState = await fetchGroupState(group);
if (updatedState == null) {
Log.error(
'[$receiptId] Received group invite/create for $groupId, but failed to fetch group state from server.',
);
return;
}
final (_, state) = updatedState;
if (!state.adminIds.any((id) => id.toInt() == fromUserId)) {
Log.error(
'[$receiptId] Received group invite/create for $groupId from $fromUserId, but they are not an admin of this group.',
);
return;
}
// User was already in the group, so update leftGroup back to false // User was already in the group, so update leftGroup back to false
await twonlyDB.groupsDao.updateGroup( await twonlyDB.groupsDao.updateGroup(
groupId, groupId,
@ -55,7 +72,6 @@ Future<void> handleGroupCreate(
stateVersionId: const Value(0), stateVersionId: const Value(0),
stateEncryptionKey: Value(Uint8List.fromList(newGroup.stateKey)), stateEncryptionKey: Value(Uint8List.fromList(newGroup.stateKey)),
myGroupPrivateKey: Value(myGroupKey.serialize()), myGroupPrivateKey: Value(myGroupKey.serialize()),
groupName: const Value(''),
joinedGroup: const Value(false), joinedGroup: const Value(false),
leftGroup: const Value(false), leftGroup: const Value(false),
deletedContent: const Value(false), deletedContent: const Value(false),
@ -79,18 +95,8 @@ Future<void> handleGroupCreate(
), ),
); );
await twonlyDB.groupsDao.insertOrUpdateGroupMember( // Load group members from the server as this is the single source of truth.
GroupMembersCompanion( // This can be done in the background, so the WebSocket message can be ACKed.
groupId: Value(groupId),
contactId: Value(fromUserId),
memberState: const Value(
MemberState.admin, // is the group creator, so must be admin...
),
groupPublicKey: Value(Uint8List.fromList(newGroup.groupPublicKey)),
),
);
// can be done in the background -> websocket message can be ACK
unawaited(fetchGroupStatesForUnjoinedGroups()); unawaited(fetchGroupStatesForUnjoinedGroups());
await sendCipherTextToGroup( await sendCipherTextToGroup(
@ -113,7 +119,9 @@ Future<void> handleGroupUpdate(
final actionType = groupActionTypeFromString(update.groupActionType); final actionType = groupActionTypeFromString(update.groupActionType);
if (actionType == null) { if (actionType == null) {
Log.error('[$receiptId] Group action ${update.groupActionType} is unknown ignoring.'); Log.error(
'[$receiptId] Group action ${update.groupActionType} is unknown ignoring.',
);
return; return;
} }

View file

@ -177,7 +177,17 @@ Future<(Uint8List, Uint8List?)?> _tryToSendCompleteMessageInternal({
Uint8List.fromList(message.encryptedContent), Uint8List.fromList(message.encryptedContent),
); );
if (cipherText == null) { if (cipherText == null) {
Log.error('Could not encrypt the message. Aborting and trying again.'); Log.error(
'[${receipt.receiptId}] Could not encrypt the message for user ${receipt.contactId}. Aborting and trying again.',
);
if (receipt.messageId != null) {
await twonlyDB.messagesDao.handleMessageAckByServer(
receipt.contactId,
receipt.messageId!,
clock.now(),
);
}
await twonlyDB.receiptsDao.deleteReceipt(receipt.receiptId);
return null; return null;
} }
message.encryptedContent = cipherText.serialize(); message.encryptedContent = cipherText.serialize();
@ -435,7 +445,7 @@ Future<(Uint8List, Uint8List?)?> sendCipherText(
final openReceipts = await twonlyDB.receiptsDao.getReceiptCountForContact( final openReceipts = await twonlyDB.receiptsDao.getReceiptCountForContact(
contactId, contactId,
); );
if (openReceipts > 6) { if (openReceipts > 10) {
// this prevents that these types of messages are send in case the receiver is offline // this prevents that these types of messages are send in case the receiver is offline
return null; return null;
} }

View file

@ -211,6 +211,8 @@ Future<void> _handleClient2ClientMessage(
type: Message_Type.CIPHERTEXT, type: Message_Type.CIPHERTEXT,
encryptedContent: encryptedContent.writeToBuffer(), encryptedContent: encryptedContent.writeToBuffer(),
); );
// Use Value.absent() for CIPHERTEXT messages so that insertReceipt generates a new UUID.
// This prevents receipt ID collisions and ensures the recipient's ACK is tracked correctly.
receiptIdDB = const Value.absent(); receiptIdDB = const Value.absent();
} else { } else {
// Message was successful processed // Message was successful processed
@ -219,8 +221,9 @@ Future<void> _handleClient2ClientMessage(
response ??= Message(type: Message_Type.SENDER_DELIVERY_RECEIPT); response ??= Message(type: Message_Type.SENDER_DELIVERY_RECEIPT);
String? targetReceiptId;
try { try {
await twonlyDB.receiptsDao.insertReceipt( final inserted = await twonlyDB.receiptsDao.insertReceipt(
ReceiptsCompanion( ReceiptsCompanion(
receiptId: receiptIdDB ?? Value(receiptId), receiptId: receiptIdDB ?? Value(receiptId),
contactId: Value(fromUserId), contactId: Value(fromUserId),
@ -228,10 +231,18 @@ Future<void> _handleClient2ClientMessage(
contactWillSendsReceipt: const Value(false), contactWillSendsReceipt: const Value(false),
), ),
); );
// Use the inserted receipt's ID because for CIPHERTEXT messages we generate a new UUID
// (receiptIdDB is Value.absent()) to avoid ID collisions and properly track individual ACKs.
targetReceiptId = inserted?.receiptId;
} catch (e) { } catch (e) {
Log.warn('[$receiptId] Error inserting receipt: $e'); Log.warn('[$receiptId] Error inserting receipt: $e');
} }
await tryToSendCompleteMessage(receiptId: receiptId, blocking: false); if (targetReceiptId != null) {
await tryToSendCompleteMessage(
receiptId: targetReceiptId,
blocking: false,
);
}
} }
case Message_Type.TEST_NOTIFICATION: case Message_Type.TEST_NOTIFICATION:
break; break;
@ -350,6 +361,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
fromUserId, fromUserId,
content.errorMessages, content.errorMessages,
receiptId, receiptId,
groupId: content.hasGroupId() ? content.groupId : null,
); );
return (null, null); return (null, null);
} }
@ -430,6 +442,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
/// Verify that the user is (still) in that group... /// Verify that the user is (still) in that group...
if (!await twonlyDB.groupsDao.isContactInGroup(fromUserId, content.groupId)) { if (!await twonlyDB.groupsDao.isContactInGroup(fromUserId, content.groupId)) {
// Check if this is a direct chat...
if (getUUIDforDirectChat(userService.currentUser.userId, fromUserId) == if (getUUIDforDirectChat(userService.currentUser.userId, fromUserId) ==
content.groupId) { content.groupId) {
final contact = await twonlyDB.contactsDao final contact = await twonlyDB.contactsDao
@ -477,9 +490,19 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
} }
Log.error( Log.error(
'[$receiptId] User $fromUserId tried to access group ${content.groupId}.', '[$receiptId] User $fromUserId tried to access group ${content.groupId}. Sending GROUP_NOT_FOUND_OR_NOT_A_MEMBER error.',
);
return (
EncryptedContent(
groupId: content.groupId,
errorMessages: EncryptedContent_ErrorMessages(
type: EncryptedContent_ErrorMessages_Type
.GROUP_NOT_FOUND_OR_NOT_A_MEMBER,
relatedReceiptId: receiptId,
),
),
null,
); );
return (null, null);
} }
} }

View file

@ -139,6 +139,7 @@ Future<bool> createNewGroup(String groupName, List<Contact> members) async {
groupCreate: EncryptedContent_GroupCreate( groupCreate: EncryptedContent_GroupCreate(
stateKey: stateEncryptionKey, stateKey: stateEncryptionKey,
groupPublicKey: myGroupKey.getPublicKey().serialize(), groupPublicKey: myGroupKey.getPublicKey().serialize(),
groupName: group.groupName,
), ),
), ),
); );
@ -770,6 +771,7 @@ Future<bool> addNewGroupMembers(
groupCreate: EncryptedContent_GroupCreate( groupCreate: EncryptedContent_GroupCreate(
stateKey: group.stateEncryptionKey, stateKey: group.stateEncryptionKey,
groupPublicKey: keyPair.getPublicKey().serialize(), groupPublicKey: keyPair.getPublicKey().serialize(),
groupName: group.groupName,
), ),
), ),
); );

View file

@ -29,7 +29,7 @@ Future<CiphertextMessage?> _signalEncryptMessage(
final session = SessionCipher.fromStore(signalStore, address); final session = SessionCipher.fromStore(signalStore, address);
return await session.encrypt(plaintextContent); return await session.encrypt(plaintextContent);
} catch (e) { } catch (e) {
Log.error(e.toString()); Log.error('Could not encrypt message for target $target: $e');
return null; return null;
} }
} }

View file

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

View file

@ -141,13 +141,13 @@ class _ContactRowState extends State<_ContactRow> {
), ),
); );
if (added > 0) await importSignalContactAndCreateRequest(userdata);
await KeyVerificationService.verifySharedContact( await KeyVerificationService.verifySharedContact(
contactId: userdata.userId.toInt(), contactId: userdata.userId.toInt(),
sharedPublicIdentityKey: widget.contact.publicIdentityKey, sharedPublicIdentityKey: widget.contact.publicIdentityKey,
senderId: widget.message.senderId!, senderId: widget.message.senderId!,
); );
if (added > 0) await importSignalContactAndCreateRequest(userdata);
} catch (e) { } catch (e) {
Log.error(e); Log.error(e);
} finally { } finally {

View file

@ -45,7 +45,7 @@ class _TypingIndicatorState extends State<TypingIndicator> {
StreamSubscription<List<(Contact, GroupMember)>>? membersSub; StreamSubscription<List<(Contact, GroupMember)>>? membersSub;
Timer? _periodicUpdate; Timer? _periodicUpdate;
bool _wasShownOnce = false; double _wasShownOnce = 0;
@override @override
void initState() { void initState() {
@ -81,19 +81,22 @@ class _TypingIndicatorState extends State<TypingIndicator> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_groupMembers.isEmpty) { if (_groupMembers.isEmpty) {
return SizedBox(height: _wasShownOnce ? 19 : 0); return SizedBox(height: _wasShownOnce);
} else {
_wasShownOnce = true;
} }
final height =
(widget.group.isDirectChat ? 20 : 24) * _groupMembers.length.toDouble();
_wasShownOnce = height;
return SizedBox( return SizedBox(
height: 19, height: height,
child: Align( child: Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Padding( child: Padding(
padding: const EdgeInsets.only(left: 12), padding: const EdgeInsets.only(left: 12),
child: Row( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _groupMembers children: _groupMembers
.map( .map(
(member) => Padding( (member) => Padding(

View file

@ -65,7 +65,7 @@ packages:
source: hosted source: hosted
version: "1.0.4" version: "1.0.4"
archive: archive:
dependency: "direct main" dependency: transitive
description: description:
name: archive name: archive
sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff
@ -389,18 +389,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: device_info_plus name: device_info_plus
sha256: b4fed1b2835da9d670d7bed7db79ae2a94b0f5ad6312268158a9b5479abbacdd sha256: "6a642e1daa10190af89ba6cb6386c0df7d071a3592080bfe1e44faa63ae1df65"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "12.4.0" version: "13.1.0"
device_info_plus_platform_interface: device_info_plus_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: device_info_plus_platform_interface name: device_info_plus_platform_interface
sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f sha256: "04b173a92e2d9161dfead145667037c8d834db725ce2e7b942bfe18fd2f45a46"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.0.3" version: "8.1.0"
dots_indicator: dots_indicator:
dependency: "direct overridden" dependency: "direct overridden"
description: description:
@ -471,6 +471,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.0" version: "2.2.0"
ffi_leak_tracker:
dependency: transitive
description:
name: ffi_leak_tracker
sha256: "4093d4ef9ca06ffe2786e73bfb25e22aa92112b9bb4ec941f11e3e6b61489a97"
url: "https://pub.dev"
source: hosted
version: "0.1.2"
file: file:
dependency: transitive dependency: transitive
description: description:
@ -479,14 +487,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.0.1" version: "7.0.1"
file_picker:
dependency: "direct main"
description:
name: file_picker
sha256: "57d9a1dd5063f85fa3107fb42d1faffda52fdc948cefd5fe5ea85267a5fc7343"
url: "https://pub.dev"
source: hosted
version: "10.3.10"
file_selector_linux: file_selector_linux:
dependency: transitive dependency: transitive
description: description:
@ -834,10 +834,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: flutter_secure_storage_windows name: flutter_secure_storage_windows
sha256: "3b7c8e068875dfd46719ff57c90d8c459c87f2302ed6b00ff006b3c9fcad1613" sha256: "471951813a97006d899db4948acc654a4f28c440083ea08178935ce20b173ec1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.0" version: "4.2.2"
flutter_sharing_intent: flutter_sharing_intent:
dependency: "direct main" dependency: "direct main"
description: description:
@ -892,14 +892,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.2" version: "2.3.2"
get:
dependency: "direct main"
description:
name: get
sha256: "5ed34a7925b85336e15d472cc4cfe7d9ebf4ab8e8b9f688585bf6b50f4c3d79a"
url: "https://pub.dev"
source: hosted
version: "4.7.3"
get_it: get_it:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1037,10 +1029,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: image_picker name: image_picker
sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320" sha256: "91c025426c2881c551100bce834e201c835a170151545f58d17da5180ca7d9ac"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.1" version: "1.2.2"
image_picker_android: image_picker_android:
dependency: transitive dependency: transitive
description: description:
@ -1381,18 +1373,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: package_info_plus name: package_info_plus
sha256: "468c26b4254ab01979fa5e4a98cb343ea3631b9acee6f21028997419a80e1a20" sha256: "4bf625947f6c7713ee242296a682e23e44823c09cf9d79e4f1238923c92db852"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "9.0.1" version: "10.1.0"
package_info_plus_platform_interface: package_info_plus_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: package_info_plus_platform_interface name: package_info_plus_platform_interface
sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" sha256: db762cb2f4f25ee60fb6359773861b0f199e00b90d237bd85a76a1e806b46ef4
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.2.1" version: "4.1.0"
path: path:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1694,18 +1686,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: share_plus name: share_plus
sha256: "223873d106614442ea6f20db5a038685cc5b32a2fba81cdecaefbbae0523f7fa" sha256: a857d8b1479250aff6b57a51b2c02d31ca05848d441817c43f1640c885c286c0
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "12.0.2" version: "13.1.0"
share_plus_platform_interface: share_plus_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: share_plus_platform_interface name: share_plus_platform_interface
sha256: "88023e53a13429bd65d8e85e11a9b484f49d4c190abbd96c7932b74d6927cc9a" sha256: "7f7ae28cf400d13f811e297ff37742dba83b79e0a6f5dce14eec0248274e6ce9"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.1.0" version: "7.1.0"
shared_preferences: shared_preferences:
dependency: transitive dependency: transitive
description: description:
@ -2170,18 +2162,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: win32 name: win32
sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e sha256: ba6f4bba816c8d7e3c1580e170f3786d216951cc6b94babc3b814c08d2cb2738
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.15.0" version: "6.3.0"
win32_registry: win32_registry:
dependency: transitive dependency: transitive
description: description:
name: win32_registry name: win32_registry
sha256: "6f1b564492d0147b330dd794fee8f512cec4977957f310f9951b5f9d83618dae" sha256: "73b1d78920a9d6e03f8b4e43e612b87bf3152a0e5c5e5150267762b7c4116904"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.0" version: "3.0.3"
workmanager: workmanager:
dependency: "direct main" dependency: "direct main"
description: description:

View file

@ -17,8 +17,8 @@ dependencies:
# Trusted published dart.dev or tools.dart.dev # Trusted published dart.dev or tools.dart.dev
collection: ^1.18.0 collection: ^1.18.0
fixnum: ^1.1.1 fixnum: ^1.1.1
meta: ^1.17.0 meta: ^1.17.0 # used by overwritten dependencies...
http: ^1.3.0 http: ^1.6.0
intl: ^0.20.2 intl: ^0.20.2
path: ^1.9.0 path: ^1.9.0
logging: ^1.3.0 logging: ^1.3.0
@ -32,7 +32,7 @@ dependencies:
# Trusted publisher flutter.dev # Trusted publisher flutter.dev
camera: ^0.12.0+1 camera: ^0.12.0+1
flutter_svg: ^2.0.17 flutter_svg: ^2.0.17
image_picker: ^1.1.2 image_picker: ^1.2.2
local_auth: ^3.0.0 local_auth: ^3.0.0
path_provider: ^2.1.5 path_provider: ^2.1.5
url_launcher: ^6.3.2 url_launcher: ^6.3.2
@ -44,10 +44,10 @@ dependencies:
# Trusted publisher fluttercommunity.dev # Trusted publisher fluttercommunity.dev
connectivity_plus: ^7.0.0 connectivity_plus: ^7.0.0
device_info_plus: ^12.1.0 device_info_plus: ^13.1.0
font_awesome_flutter: ^11.0.0 font_awesome_flutter: ^11.0.0
share_plus: ^12.0.0 share_plus: ^13.1.0
package_info_plus: ^9.0.0 package_info_plus: ^10.1.0
workmanager: ^0.9.0+3 workmanager: ^0.9.0+3
@ -91,14 +91,11 @@ dependencies:
# With high download. (But should be checked nonetheless.) # With high download. (But should be checked nonetheless.)
app_links: ^7.0.0 # 1.6 mio app_links: ^7.0.0 # 1.6 mio
image: ^4.3.0 # 3.3 mio image: ^4.3.0 # 3.3 mio
archive: ^4.0.7 # 6.5 mio
file_picker: ^10.3.6 # 2 mio
get: ^4.7.2 # 740 k
flutter_secure_storage: ^10.3.1 # 1.85 mio flutter_secure_storage: ^10.3.1 # 1.85 mio
permission_handler: ^12.0.0+1 # 2 mio permission_handler: ^12.0.0+1 # 2 mio
# Not yet checked # Not yet checked
audio_waveforms: ^2.0.0 audio_waveforms: ^2.0.2
avatar_maker: ^0.4.0 avatar_maker: ^0.4.0
background_downloader: ^9.4.0 background_downloader: ^9.4.0
cached_network_image: ^3.4.1 cached_network_image: ^3.4.1

102
scripts/check_dependencies.py Executable file
View file

@ -0,0 +1,102 @@
#!/usr/bin/env python3
import os
import re
import sys
def parse_dependencies(pubspec_path):
deps = []
in_deps = False
with open(pubspec_path, 'r', encoding='utf-8') as f:
for line in f:
stripped = line.strip()
if not stripped or stripped.startswith('#'):
continue
# Detect starting of dependencies or ending
if line[0].isalpha() or line.startswith('dev_dependencies') or line.startswith('dependency_overrides'):
if stripped.startswith('dependencies:'):
in_deps = True
else:
in_deps = False
continue
if in_deps:
# Matches exactly 2 spaces indentation, followed by package name and colon
match = re.match(r'^ ([a-zA-Z0-9_-]+):', line)
if match:
dep = match.group(1)
# Ignore Flutter SDK built-ins and custom local rust bridge wrapper
if dep not in ['flutter', 'flutter_localizations', 'rust_lib_twonly']:
deps.append(dep)
return deps
def find_used_packages(root_dir):
used = set()
# Matches: import 'package:package_name/...'; or export 'package:package_name/...';
pattern = re.compile(r'(?:import|export)\s+[\'"]package:([a-zA-Z0-9_-]+)/')
for dirpath, _, filenames in os.walk(root_dir):
# Skip hidden directories (.git, .dart_tool, etc.)
if any(part.startswith('.') for part in dirpath.split(os.sep)):
continue
for filename in filenames:
if filename.endswith('.dart'):
filepath = os.path.join(dirpath, filename)
try:
with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
for line in f:
match = pattern.search(line)
if match:
used.add(match.group(1))
except Exception:
pass
return used
def main():
# Locate project root (assuming script is in scripts/ or root/)
script_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.abspath(os.path.join(script_dir, '..'))
pubspec_path = os.path.join(project_root, 'pubspec.yaml')
if not os.path.exists(pubspec_path):
print(f"Error: pubspec.yaml not found at {pubspec_path}")
sys.exit(1)
print("Parsing dependencies from pubspec.yaml...")
declared_deps = parse_dependencies(pubspec_path)
print(f"Found {len(declared_deps)} runtime dependencies.")
print("\nScanning codebase for imports/exports in lib/, test/, integration_test/...")
used_in_code = set()
for folder in ['lib', 'test', 'integration_test']:
folder_path = os.path.join(project_root, folder)
if os.path.exists(folder_path):
used_in_code.update(find_used_packages(folder_path))
unused_deps = []
used_deps = []
for dep in declared_deps:
if dep in used_in_code:
used_deps.append(dep)
else:
unused_deps.append(dep)
print("\n" + "=" * 50)
print(" RESULTS")
print("=" * 50)
if unused_deps:
print(f"\n❌ UNUSED DEPENDENCIES ({len(unused_deps)}):")
for dep in sorted(unused_deps):
print(f" - {dep}")
else:
print("\n✅ All dependencies listed in pubspec.yaml are used in the codebase!")
print(f"\n✨ USED DEPENDENCIES ({len(used_deps)}):")
for dep in sorted(used_deps):
print(f" - {dep}")
if __name__ == '__main__':
main()