Merge pull request #429 from twonlyapp/hotfix

Hotfix
This commit is contained in:
Tobi 2026-09-24 15:57:03 +02:00 committed by GitHub
commit 09bc880401
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 4008 additions and 290 deletions

1
.gitignore vendored
View file

@ -57,3 +57,4 @@ rust/target
rust_dependencies/target rust_dependencies/target
fastlane/repo/status/running.json fastlane/repo/status/running.json
.cache/ .cache/
APKS/*.apk

4
.gitmodules vendored
View file

@ -3,7 +3,3 @@
path = dependencies path = dependencies
# url = ssh://git@git.twonly.eu:22222/twonly/twonly-app-dependencies.git # url = ssh://git@git.twonly.eu:22222/twonly/twonly-app-dependencies.git
url = https://git.twonly.eu/twonly/twonly-app-dependencies.git url = https://git.twonly.eu/twonly/twonly-app-dependencies.git
[submodule "lib/src/localization/translations"]
path = lib/src/localization/translations
# url = ssh://git@git.twonly.eu:22222/twonly/twonly-translations.git
url = https://git.twonly.eu/twonly/twonly-translations

View file

@ -1,5 +1,10 @@
# Changelog # Changelog
## 0.5.3
- Hotfix: Fixed stuck Signal sessions
- Improve: Gracefully upgrade all sessions to PQXDH
## 0.5.2 ## 0.5.2
- Hotfix: Fixed an infinite loop involving delivery receipts - Hotfix: Fixed an infinite loop involving delivery receipts

View file

@ -14,8 +14,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/google/app-check.git", "location" : "https://github.com/google/app-check.git",
"state" : { "state" : {
"revision" : "61b85103a1aeed8218f17c794687781505fbbef5", "revision" : "97f7d74dd0e8f3d0fe5fc75cc8957aa73bdc948a",
"version" : "11.2.0" "version" : "11.3.2"
} }
}, },
{ {
@ -23,8 +23,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/firebase-ios-sdk", "location" : "https://github.com/firebase/firebase-ios-sdk",
"state" : { "state" : {
"revision" : "8d5b4189f1f482df8d5c58c9985ea70491ef5382", "revision" : "346daa9f46316aa372b35b317e18224acc2e9063",
"version" : "12.14.0" "version" : "12.18.0"
} }
}, },
{ {
@ -50,8 +50,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleAppMeasurement.git", "location" : "https://github.com/google/GoogleAppMeasurement.git",
"state" : { "state" : {
"revision" : "219e564a8510e983e675c94f77f7f7c50049f22d", "revision" : "f04760d460296cc0fa430935a7be212e5bd67fc5",
"version" : "12.14.0" "version" : "12.18.0"
} }
}, },
{ {

View file

@ -1 +0,0 @@
ce49e7d90cd902197f9a9cbc84219d23

View file

@ -47,6 +47,14 @@ class RustSignal {
bundle: bundle, bundle: bundle,
); );
/// Removes the current v2 session for a contact. The Dart hotfix layer
/// immediately establishes its replacement from a freshly fetched bundle.
static Future<void> resetContactSession({required PlatformInt64 contactId}) =>
RustLib.instance.api
.crateBridgeWrapperSignalRustSignalResetContactSession(
contactId: contactId,
);
@override @override
int get hashCode => 0; int get hashCode => 0;

View file

@ -78,7 +78,7 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
String get codegenVersion => '2.12.0'; String get codegenVersion => '2.12.0';
@override @override
int get rustContentHash => 1109927244; int get rustContentHash => -2042422055;
static const kDefaultExternalLibraryLoaderConfig = static const kDefaultExternalLibraryLoaderConfig =
ExternalLibraryLoaderConfig( ExternalLibraryLoaderConfig(
@ -284,6 +284,10 @@ abstract class RustLibApi extends BaseApi {
required FrbPreKeyBundle bundle, required FrbPreKeyBundle bundle,
}); });
Future<void> crateBridgeWrapperSignalRustSignalResetContactSession({
required PlatformInt64 contactId,
});
Future<List<Uint8List>> crateBridgeWrapperRustUtilsGenerateShares({ Future<List<Uint8List>> crateBridgeWrapperRustUtilsGenerateShares({
required List<int> secret, required List<int> secret,
required int total, required int total,
@ -1853,6 +1857,41 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
argNames: ["name", "deviceId", "bundle"], argNames: ["name", "deviceId", "bundle"],
); );
@override
Future<void> crateBridgeWrapperSignalRustSignalResetContactSession({
required PlatformInt64 contactId,
}) {
return handler.executeNormal(
NormalTask(
callFfi: (port_) {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_i_64(contactId, serializer);
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 37,
port: port_,
);
},
codec: SseCodec(
decodeSuccessData: sse_decode_unit,
decodeErrorData: sse_decode_AnyhowException,
),
constMeta:
kCrateBridgeWrapperSignalRustSignalResetContactSessionConstMeta,
argValues: [contactId],
apiImpl: this,
),
);
}
TaskConstMeta
get kCrateBridgeWrapperSignalRustSignalResetContactSessionConstMeta =>
const TaskConstMeta(
debugName: "rust_signal_reset_contact_session",
argNames: ["contactId"],
);
@override @override
Future<List<Uint8List>> crateBridgeWrapperRustUtilsGenerateShares({ Future<List<Uint8List>> crateBridgeWrapperRustUtilsGenerateShares({
required List<int> secret, required List<int> secret,
@ -1869,7 +1908,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 37, funcId: 38,
port: port_, port: port_,
); );
}, },
@ -1904,7 +1943,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 38, funcId: 39,
port: port_, port: port_,
); );
}, },
@ -1940,7 +1979,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 39, funcId: 40,
port: port_, port: port_,
); );
}, },
@ -1977,7 +2016,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 40, funcId: 41,
port: port_, port: port_,
); );
}, },
@ -2015,7 +2054,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 41, funcId: 42,
port: port_, port: port_,
); );
}, },
@ -2053,7 +2092,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 42, funcId: 43,
port: port_, port: port_,
); );
}, },
@ -2091,7 +2130,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 43, funcId: 44,
port: port_, port: port_,
); );
}, },
@ -2130,7 +2169,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 44, funcId: 45,
port: port_, port: port_,
); );
}, },
@ -2169,7 +2208,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 45, funcId: 46,
port: port_, port: port_,
); );
}, },
@ -2214,7 +2253,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 46, funcId: 47,
port: port_, port: port_,
); );
}, },
@ -2266,7 +2305,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 47, funcId: 48,
port: port_, port: port_,
); );
}, },
@ -2307,7 +2346,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 48, funcId: 49,
port: port_, port: port_,
); );
}, },
@ -2345,7 +2384,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 49, funcId: 50,
port: port_, port: port_,
); );
}, },
@ -2383,7 +2422,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 50, funcId: 51,
port: port_, port: port_,
); );
}, },
@ -2421,7 +2460,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 51, funcId: 52,
port: port_, port: port_,
); );
}, },
@ -2459,7 +2498,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 52, funcId: 53,
port: port_, port: port_,
); );
}, },
@ -2501,7 +2540,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 53, funcId: 54,
port: port_, port: port_,
); );
}, },
@ -2541,7 +2580,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 54, funcId: 55,
port: port_, port: port_,
); );
}, },

View file

@ -150,6 +150,43 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
.get(); .get();
} }
/// Finds contacts for which repeated, unacknowledged message delivery is a
/// strong indication that only the outgoing Signal ratchet is broken.
Future<List<int>> getContactsWithStuckSignalMessages({
required int retryThreshold,
int minimumMessages = 2,
}) async {
final messageCount = countAll();
final query = selectOnly(receipts)
..addColumns([receipts.contactId, messageCount])
..where(
receipts.retryCount.isBiggerThanValue(retryThreshold) &
receipts.willBeRetriedByMediaUpload.equals(false),
)
..groupBy([receipts.contactId]);
final rows = await query.get();
return rows
.where((row) => (row.read(messageCount) ?? 0) >= minimumMessages)
.map((row) => row.read(receipts.contactId)!)
.toList();
}
/// Starts retry accounting again after establishing a fresh Signal session.
Future<void> resetMessageRetryCountsForContact(int contactId) async {
await (update(receipts)..where(
(receipt) =>
receipt.contactId.equals(contactId) &
receipt.willBeRetriedByMediaUpload.equals(false),
))
.write(
const ReceiptsCompanion(
retryCount: Value(0),
lastRetry: Value(null),
),
);
}
Future<List<Receipt>> getReceiptsForMediaRetransmissions() async { Future<List<Receipt>> getReceiptsForMediaRetransmissions() async {
final markedRetriesTime = clock.now().subtract( final markedRetriesTime = clock.now().subtract(
const Duration( const Duration(

View file

@ -635,13 +635,13 @@ abstract class AppLocalizations {
/// No description provided for @settingsNotifyResetTitle. /// No description provided for @settingsNotifyResetTitle.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Didn\'t receive a test notification?'** /// **'Reset notification tokens'**
String get settingsNotifyResetTitle; String get settingsNotifyResetTitle;
/// No description provided for @settingsNotifyResetTitleSubtitle. /// No description provided for @settingsNotifyResetTitleSubtitle.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'If you haven\'t received any test notifications, click here to reset your notification tokens.'** /// **'Reset your notification tokens if you have problems receiving push notifications.'**
String get settingsNotifyResetTitleSubtitle; String get settingsNotifyResetTitleSubtitle;
/// No description provided for @settingsNotifyResetTitleReset. /// No description provided for @settingsNotifyResetTitleReset.
@ -956,6 +956,42 @@ abstract class AppLocalizations {
/// **'Block'** /// **'Block'**
String get contactBlock; String get contactBlock;
/// No description provided for @contactTroubleshoot.
///
/// In en, this message translates to:
/// **'Troubleshoot connection'**
String get contactTroubleshoot;
/// No description provided for @contactResetSessionTitle.
///
/// In en, this message translates to:
/// **'Reset connection with {username}?'**
String contactResetSessionTitle(String username);
/// No description provided for @contactResetSessionBody.
///
/// In en, this message translates to:
/// **'twonly will remove the current encrypted connection and establish a new one. Messages currently in transit may be lost.'**
String get contactResetSessionBody;
/// No description provided for @contactResetSessionConfirm.
///
/// In en, this message translates to:
/// **'Reset connection'**
String get contactResetSessionConfirm;
/// No description provided for @contactResetSessionSuccess.
///
/// In en, this message translates to:
/// **'The encrypted connection with {username} was re-established.'**
String contactResetSessionSuccess(String username);
/// No description provided for @contactResetSessionFailed.
///
/// In en, this message translates to:
/// **'The connection could not be reset. Check your internet connection and try again.'**
String get contactResetSessionFailed;
/// No description provided for @contactBlockTitle. /// No description provided for @contactBlockTitle.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -1226,6 +1262,18 @@ abstract class AppLocalizations {
/// **'You are part of the paid plan of {username}!'** /// **'You are part of the paid plan of {username}!'**
String partOfPaidPlanOf(Object username); String partOfPaidPlanOf(Object username);
/// No description provided for @partOfPaidPlanFrom.
///
/// In en, this message translates to:
/// **'Part of the paid plan of'**
String get partOfPaidPlanFrom;
/// No description provided for @subscriptionCurrentPlanBadge.
///
/// In en, this message translates to:
/// **'Current'**
String get subscriptionCurrentPlanBadge;
/// No description provided for @year. /// No description provided for @year.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -1253,67 +1301,67 @@ abstract class AppLocalizations {
/// No description provided for @proFeature1. /// No description provided for @proFeature1.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'✓ Unlimited media file uploads'** /// **'Unlimited media file uploads'**
String get proFeature1; String get proFeature1;
/// No description provided for @proFeature2. /// No description provided for @proFeature2.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'✓ 1 additional Plus user'** /// **'1 additional Plus user'**
String get proFeature2; String get proFeature2;
/// No description provided for @proFeature3. /// No description provided for @proFeature3.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'✓ 25GB Memories storage'** /// **'25GB Memories storage'**
String get proFeature3; String get proFeature3;
/// No description provided for @proFeature4. /// No description provided for @proFeature4.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'✓ Restore flames'** /// **'Restore flames'**
String get proFeature4; String get proFeature4;
/// No description provided for @familyFeature1. /// No description provided for @familyFeature1.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'✓ Unlimited media file uploads'** /// **'Unlimited media file uploads'**
String get familyFeature1; String get familyFeature1;
/// No description provided for @familyFeature2. /// No description provided for @familyFeature2.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'✓ 4 additional Plus user'** /// **'4 additional Plus user'**
String get familyFeature2; String get familyFeature2;
/// No description provided for @familyFeature3. /// No description provided for @familyFeature3.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'✓ 50GB Memories storage'** /// **'50GB Memories storage'**
String get familyFeature3; String get familyFeature3;
/// No description provided for @familyFeature4. /// No description provided for @familyFeature4.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'✓ Support twonly'** /// **'Restore flames'**
String get familyFeature4; String get familyFeature4;
/// No description provided for @freeFeature1. /// No description provided for @freeFeature1.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'✓ 10 Media file uploads per day'** /// **'10 Media file uploads per day'**
String get freeFeature1; String get freeFeature1;
/// No description provided for @plusFeature1. /// No description provided for @plusFeature1.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'✓ Unlimited media file uploads'** /// **'Unlimited media file uploads'**
String get plusFeature1; String get plusFeature1;
/// No description provided for @plusFeature2. /// No description provided for @plusFeature2.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'✓ Additional features (coming-soon)'** /// **'Additional features (coming-soon)'**
String get plusFeature2; String get plusFeature2;
/// No description provided for @manageAdditionalUsers. /// No description provided for @manageAdditionalUsers.
@ -1838,6 +1886,12 @@ abstract class AppLocalizations {
/// **'Edit history'** /// **'Edit history'**
String get editHistory; String get editHistory;
/// No description provided for @fileSize.
///
/// In en, this message translates to:
/// **'File size'**
String get fileSize;
/// No description provided for @archivedChats. /// No description provided for @archivedChats.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -2228,6 +2282,12 @@ abstract class AppLocalizations {
/// **'Messages from other users.'** /// **'Messages from other users.'**
String get notificationCategoryMessageDesc; String get notificationCategoryMessageDesc;
/// No description provided for @notificationConnectionFallback.
///
/// In en, this message translates to:
/// **'You may have new messages.'**
String get notificationConnectionFallback;
/// No description provided for @groupContextMenuDeleteGroup. /// No description provided for @groupContextMenuDeleteGroup.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -4537,6 +4597,42 @@ abstract class AppLocalizations {
/// In en, this message translates to: /// In en, this message translates to:
/// **'Exporting memories...'** /// **'Exporting memories...'**
String get memoriesExportingProgress; String get memoriesExportingProgress;
/// No description provided for @fileLimitReachedTitle.
///
/// In en, this message translates to:
/// **'File too large to send'**
String get fileLimitReachedTitle;
/// No description provided for @fileLimitReachedDetail.
///
/// In en, this message translates to:
/// **'This file is {size}, but your plan allows at most {limit} per send.'**
String fileLimitReachedDetail(String size, String limit);
/// No description provided for @fileLimitReachedDetailNoSize.
///
/// In en, this message translates to:
/// **'Your plan allows at most {limit} per send.'**
String fileLimitReachedDetailNoSize(String limit);
/// No description provided for @fileLimitReachedHint.
///
/// In en, this message translates to:
/// **'Record a shorter video and send it again.'**
String get fileLimitReachedHint;
/// No description provided for @fileLimitReachedHintFree.
///
/// In en, this message translates to:
/// **'Record a shorter video, or upgrade your plan to send larger files.'**
String get fileLimitReachedHintFree;
/// No description provided for @fileLimitReachedUpgrade.
///
/// In en, this message translates to:
/// **'Upgrade plan'**
String get fileLimitReachedUpgrade;
} }
class _AppLocalizationsDelegate class _AppLocalizationsDelegate

View file

@ -294,11 +294,11 @@ class AppLocalizationsDe extends AppLocalizations {
'Um eine Testbenachrichtigung zu erhalten, klicke auf OK. Falls du die Testbenachrichtigung nicht erhältst, klicke bitte auf den neuen Menüpunkt, der nach dem Klicken auf „OK“ angezeigt wird.'; 'Um eine Testbenachrichtigung zu erhalten, klicke auf OK. Falls du die Testbenachrichtigung nicht erhältst, klicke bitte auf den neuen Menüpunkt, der nach dem Klicken auf „OK“ angezeigt wird.';
@override @override
String get settingsNotifyResetTitle => 'Keine Testbenachrichtigung erhalten?'; String get settingsNotifyResetTitle => 'Benachrichtigungstoken zurücksetzen';
@override @override
String get settingsNotifyResetTitleSubtitle => String get settingsNotifyResetTitleSubtitle =>
'Falls du keine Testbenachrichtigungen erhalten hast, klicke hier, um deine Benachrichtigungstoken zurückzusetzen.'; 'Setze deine Benachrichtigungstoken zurück, wenn du Probleme beim Empfang von Push-Benachrichtigungen hast.';
@override @override
String get settingsNotifyResetTitleReset => String get settingsNotifyResetTitleReset =>
@ -475,6 +475,30 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get contactBlock => 'Blockieren'; String get contactBlock => 'Blockieren';
@override
String get contactTroubleshoot => 'Verbindungsprobleme beheben';
@override
String contactResetSessionTitle(String username) {
return 'Verbindung zu $username zurücksetzen?';
}
@override
String get contactResetSessionBody =>
'twonly löscht die aktuelle verschlüsselte Verbindung und baut sie anschließend neu auf. Nachrichten, die gerade übertragen werden, können dabei verloren gehen.';
@override
String get contactResetSessionConfirm => 'Verbindung zurücksetzen';
@override
String contactResetSessionSuccess(String username) {
return 'Die verschlüsselte Verbindung zu $username wurde neu aufgebaut.';
}
@override
String get contactResetSessionFailed =>
'Die Verbindung konnte nicht zurückgesetzt werden. Prüfe deine Internetverbindung und versuche es erneut.';
@override @override
String contactBlockTitle(Object username) { String contactBlockTitle(Object username) {
return 'Blockiere $username'; return 'Blockiere $username';
@ -633,6 +657,12 @@ class AppLocalizationsDe extends AppLocalizations {
return 'Du bist Teil des bezahlten Plans von $username!'; return 'Du bist Teil des bezahlten Plans von $username!';
} }
@override
String get partOfPaidPlanFrom => 'Teil des bezahlten Plans von';
@override
String get subscriptionCurrentPlanBadge => 'Aktuell';
@override @override
String get year => 'Jahr'; String get year => 'Jahr';
@ -646,37 +676,37 @@ class AppLocalizationsDe extends AppLocalizations {
String get monthly => 'Monatlich'; String get monthly => 'Monatlich';
@override @override
String get proFeature1 => '✓ Unbegrenzte Medien-Datei-Uploads'; String get proFeature1 => 'Unbegrenzte Medien-Datei-Uploads';
@override @override
String get proFeature2 => '✓ 1 zusätzlicher Plus Benutzer'; String get proFeature2 => '1 zusätzlicher Plus Benutzer';
@override @override
String get proFeature3 => '✓ 25GB Memories Speicher'; String get proFeature3 => '25GB Memories Speicher';
@override @override
String get proFeature4 => '✓ Flammen wiederherstellen'; String get proFeature4 => 'Flammen wiederherstellen';
@override @override
String get familyFeature1 => '✓ Unbegrenzte Medien-Datei-Uploads'; String get familyFeature1 => 'Unbegrenzte Medien-Datei-Uploads';
@override @override
String get familyFeature2 => '✓ 4 zusätzliche Plus Benutzer'; String get familyFeature2 => '4 zusätzliche Plus Benutzer';
@override @override
String get familyFeature3 => '✓ 50GB Memories Speicher'; String get familyFeature3 => '50GB Memories Speicher';
@override @override
String get familyFeature4 => '✓ Flammen wiederherstellen'; String get familyFeature4 => 'Flammen wiederherstellen';
@override @override
String get freeFeature1 => '✓ 10 Medien-Datei-Uploads pro Tag'; String get freeFeature1 => '10 Medien-Datei-Uploads pro Tag';
@override @override
String get plusFeature1 => '✓ Unbegrenzte Medien-Datei-Uploads'; String get plusFeature1 => 'Unbegrenzte Medien-Datei-Uploads';
@override @override
String get plusFeature2 => '✓ Zusatzfunktionen (coming-soon)'; String get plusFeature2 => 'Zusatzfunktionen (coming-soon)';
@override @override
String get manageAdditionalUsers => 'Zusätzliche Benutzer verwalten'; String get manageAdditionalUsers => 'Zusätzliche Benutzer verwalten';
@ -983,6 +1013,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get editHistory => 'Bearbeitungshistorie'; String get editHistory => 'Bearbeitungshistorie';
@override
String get fileSize => 'Dateigröße';
@override @override
String get archivedChats => 'Archivierte Chats'; String get archivedChats => 'Archivierte Chats';
@ -1241,6 +1274,10 @@ class AppLocalizationsDe extends AppLocalizations {
String get notificationCategoryMessageDesc => String get notificationCategoryMessageDesc =>
'Nachrichten von anderen Benutzern.'; 'Nachrichten von anderen Benutzern.';
@override
String get notificationConnectionFallback =>
'Du könntest neue Nachrichten haben.';
@override @override
String get groupContextMenuDeleteGroup => String get groupContextMenuDeleteGroup =>
'Dadurch werden alle Nachrichten in diesem Chat dauerhaft gelöscht.'; 'Dadurch werden alle Nachrichten in diesem Chat dauerhaft gelöscht.';
@ -2622,4 +2659,28 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get memoriesExportingProgress => 'Erinnerungen werden exportiert...'; String get memoriesExportingProgress => 'Erinnerungen werden exportiert...';
@override
String get fileLimitReachedTitle => 'Datei zu groß zum Senden';
@override
String fileLimitReachedDetail(String size, String limit) {
return 'Diese Datei ist $size groß, dein Tarif erlaubt aber höchstens $limit pro Sendung.';
}
@override
String fileLimitReachedDetailNoSize(String limit) {
return 'Dein Tarif erlaubt höchstens $limit pro Sendung.';
}
@override
String get fileLimitReachedHint =>
'Nimm ein kürzeres Video auf und sende es erneut.';
@override
String get fileLimitReachedHintFree =>
'Nimm ein kürzeres Video auf oder wechsle den Tarif, um größere Dateien zu senden.';
@override
String get fileLimitReachedUpgrade => 'Tarif wechseln';
} }

View file

@ -290,11 +290,11 @@ class AppLocalizationsEn extends AppLocalizations {
'Press OK to receive a test notification. If you do not receive the test notification, please click on the new menu item that appears after you click “OK”.'; 'Press OK to receive a test notification. If you do not receive the test notification, please click on the new menu item that appears after you click “OK”.';
@override @override
String get settingsNotifyResetTitle => 'Didn\'t receive a test notification?'; String get settingsNotifyResetTitle => 'Reset notification tokens';
@override @override
String get settingsNotifyResetTitleSubtitle => String get settingsNotifyResetTitleSubtitle =>
'If you haven\'t received any test notifications, click here to reset your notification tokens.'; 'Reset your notification tokens if you have problems receiving push notifications.';
@override @override
String get settingsNotifyResetTitleReset => String get settingsNotifyResetTitleReset =>
@ -471,6 +471,30 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get contactBlock => 'Block'; String get contactBlock => 'Block';
@override
String get contactTroubleshoot => 'Troubleshoot connection';
@override
String contactResetSessionTitle(String username) {
return 'Reset connection with $username?';
}
@override
String get contactResetSessionBody =>
'twonly will remove the current encrypted connection and establish a new one. Messages currently in transit may be lost.';
@override
String get contactResetSessionConfirm => 'Reset connection';
@override
String contactResetSessionSuccess(String username) {
return 'The encrypted connection with $username was re-established.';
}
@override
String get contactResetSessionFailed =>
'The connection could not be reset. Check your internet connection and try again.';
@override @override
String contactBlockTitle(Object username) { String contactBlockTitle(Object username) {
return 'Block $username'; return 'Block $username';
@ -628,6 +652,12 @@ class AppLocalizationsEn extends AppLocalizations {
return 'You are part of the paid plan of $username!'; return 'You are part of the paid plan of $username!';
} }
@override
String get partOfPaidPlanFrom => 'Part of the paid plan of';
@override
String get subscriptionCurrentPlanBadge => 'Current';
@override @override
String get year => 'year'; String get year => 'year';
@ -641,37 +671,37 @@ class AppLocalizationsEn extends AppLocalizations {
String get monthly => 'Monthly'; String get monthly => 'Monthly';
@override @override
String get proFeature1 => '✓ Unlimited media file uploads'; String get proFeature1 => 'Unlimited media file uploads';
@override @override
String get proFeature2 => '✓ 1 additional Plus user'; String get proFeature2 => '1 additional Plus user';
@override @override
String get proFeature3 => '✓ 25GB Memories storage'; String get proFeature3 => '25GB Memories storage';
@override @override
String get proFeature4 => '✓ Restore flames'; String get proFeature4 => 'Restore flames';
@override @override
String get familyFeature1 => '✓ Unlimited media file uploads'; String get familyFeature1 => 'Unlimited media file uploads';
@override @override
String get familyFeature2 => '✓ 4 additional Plus user'; String get familyFeature2 => '4 additional Plus user';
@override @override
String get familyFeature3 => '✓ 50GB Memories storage'; String get familyFeature3 => '50GB Memories storage';
@override @override
String get familyFeature4 => '✓ Support twonly'; String get familyFeature4 => 'Restore flames';
@override @override
String get freeFeature1 => '✓ 10 Media file uploads per day'; String get freeFeature1 => '10 Media file uploads per day';
@override @override
String get plusFeature1 => '✓ Unlimited media file uploads'; String get plusFeature1 => 'Unlimited media file uploads';
@override @override
String get plusFeature2 => '✓ Additional features (coming-soon)'; String get plusFeature2 => 'Additional features (coming-soon)';
@override @override
String get manageAdditionalUsers => 'Manage additional users'; String get manageAdditionalUsers => 'Manage additional users';
@ -976,6 +1006,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get editHistory => 'Edit history'; String get editHistory => 'Edit history';
@override
String get fileSize => 'File size';
@override @override
String get archivedChats => 'Archived chats'; String get archivedChats => 'Archived chats';
@ -1233,6 +1266,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get notificationCategoryMessageDesc => 'Messages from other users.'; String get notificationCategoryMessageDesc => 'Messages from other users.';
@override
String get notificationConnectionFallback => 'You may have new messages.';
@override @override
String get groupContextMenuDeleteGroup => String get groupContextMenuDeleteGroup =>
'This will permanently delete all messages in this chat.'; 'This will permanently delete all messages in this chat.';
@ -2597,4 +2633,28 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get memoriesExportingProgress => 'Exporting memories...'; String get memoriesExportingProgress => 'Exporting memories...';
@override
String get fileLimitReachedTitle => 'File too large to send';
@override
String fileLimitReachedDetail(String size, String limit) {
return 'This file is $size, but your plan allows at most $limit per send.';
}
@override
String fileLimitReachedDetailNoSize(String limit) {
return 'Your plan allows at most $limit per send.';
}
@override
String get fileLimitReachedHint =>
'Record a shorter video and send it again.';
@override
String get fileLimitReachedHintFree =>
'Record a shorter video, or upgrade your plan to send larger files.';
@override
String get fileLimitReachedUpgrade => 'Upgrade plan';
} }

@ -1 +0,0 @@
Subproject commit 50cc5cb69dd91a5c626534fe4c015e50d2a0a486

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -259,6 +259,7 @@ class Handshake_Register extends $pb.GeneratedMessage {
$core.String? langCode, $core.String? langCode,
$fixnum.Int64? proofOfWork, $fixnum.Int64? proofOfWork,
$core.List<$core.int>? loginToken, $core.List<$core.int>? loginToken,
Handshake_InitialPqcKeys? initialPqcKeys,
}) { }) {
final result = create(); final result = create();
if (username != null) result.username = username; if (username != null) result.username = username;
@ -273,6 +274,7 @@ class Handshake_Register extends $pb.GeneratedMessage {
if (langCode != null) result.langCode = langCode; if (langCode != null) result.langCode = langCode;
if (proofOfWork != null) result.proofOfWork = proofOfWork; if (proofOfWork != null) result.proofOfWork = proofOfWork;
if (loginToken != null) result.loginToken = loginToken; if (loginToken != null) result.loginToken = loginToken;
if (initialPqcKeys != null) result.initialPqcKeys = initialPqcKeys;
return result; return result;
} }
@ -305,6 +307,8 @@ class Handshake_Register extends $pb.GeneratedMessage {
..aInt64(10, _omitFieldNames ? '' : 'proofOfWork') ..aInt64(10, _omitFieldNames ? '' : 'proofOfWork')
..a<$core.List<$core.int>>( ..a<$core.List<$core.int>>(
11, _omitFieldNames ? '' : 'loginToken', $pb.PbFieldType.OY) 11, _omitFieldNames ? '' : 'loginToken', $pb.PbFieldType.OY)
..aOM<Handshake_InitialPqcKeys>(12, _omitFieldNames ? '' : 'initialPqcKeys',
subBuilder: Handshake_InitialPqcKeys.create)
..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.')
@ -344,6 +348,8 @@ class Handshake_Register extends $pb.GeneratedMessage {
@$pb.TagNumber(2) @$pb.TagNumber(2)
void clearInviteCode() => $_clearField(2); void clearInviteCode() => $_clearField(2);
/// Deprecated legacy Signal fields. The server ignores these fields; they
/// remain allocated only to avoid reusing their wire tags.
@$pb.TagNumber(3) @$pb.TagNumber(3)
$core.List<$core.int> get publicIdentityKey => $_getN(2); $core.List<$core.int> get publicIdentityKey => $_getN(2);
@$pb.TagNumber(3) @$pb.TagNumber(3)
@ -425,6 +431,287 @@ class Handshake_Register extends $pb.GeneratedMessage {
$core.bool hasLoginToken() => $_has(10); $core.bool hasLoginToken() => $_has(10);
@$pb.TagNumber(11) @$pb.TagNumber(11)
void clearLoginToken() => $_clearField(11); void clearLoginToken() => $_clearField(11);
@$pb.TagNumber(12)
Handshake_InitialPqcKeys get initialPqcKeys => $_getN(11);
@$pb.TagNumber(12)
set initialPqcKeys(Handshake_InitialPqcKeys value) => $_setField(12, value);
@$pb.TagNumber(12)
$core.bool hasInitialPqcKeys() => $_has(11);
@$pb.TagNumber(12)
void clearInitialPqcKeys() => $_clearField(12);
@$pb.TagNumber(12)
Handshake_InitialPqcKeys ensureInitialPqcKeys() => $_ensure(11);
}
class Handshake_InitialPqcKeys_PqcPreKey extends $pb.GeneratedMessage {
factory Handshake_InitialPqcKeys_PqcPreKey({
$fixnum.Int64? eccPreKeyId,
$core.List<$core.int>? eccPreKey,
$fixnum.Int64? kyberPreKeyId,
$core.List<$core.int>? kyberPreKey,
$core.List<$core.int>? kyberPreKeySignature,
}) {
final result = create();
if (eccPreKeyId != null) result.eccPreKeyId = eccPreKeyId;
if (eccPreKey != null) result.eccPreKey = eccPreKey;
if (kyberPreKeyId != null) result.kyberPreKeyId = kyberPreKeyId;
if (kyberPreKey != null) result.kyberPreKey = kyberPreKey;
if (kyberPreKeySignature != null)
result.kyberPreKeySignature = kyberPreKeySignature;
return result;
}
Handshake_InitialPqcKeys_PqcPreKey._();
factory Handshake_InitialPqcKeys_PqcPreKey.fromBuffer(
$core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory Handshake_InitialPqcKeys_PqcPreKey.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'Handshake.InitialPqcKeys.PqcPreKey',
package:
const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'),
createEmptyInstance: create)
..aInt64(1, _omitFieldNames ? '' : 'eccPreKeyId')
..a<$core.List<$core.int>>(
2, _omitFieldNames ? '' : 'eccPreKey', $pb.PbFieldType.OY)
..aInt64(3, _omitFieldNames ? '' : 'kyberPreKeyId')
..a<$core.List<$core.int>>(
4, _omitFieldNames ? '' : 'kyberPreKey', $pb.PbFieldType.OY)
..a<$core.List<$core.int>>(
5, _omitFieldNames ? '' : 'kyberPreKeySignature', $pb.PbFieldType.OY)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
Handshake_InitialPqcKeys_PqcPreKey clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
Handshake_InitialPqcKeys_PqcPreKey copyWith(
void Function(Handshake_InitialPqcKeys_PqcPreKey) updates) =>
super.copyWith((message) =>
updates(message as Handshake_InitialPqcKeys_PqcPreKey))
as Handshake_InitialPqcKeys_PqcPreKey;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static Handshake_InitialPqcKeys_PqcPreKey create() =>
Handshake_InitialPqcKeys_PqcPreKey._();
@$core.override
Handshake_InitialPqcKeys_PqcPreKey createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static Handshake_InitialPqcKeys_PqcPreKey getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<Handshake_InitialPqcKeys_PqcPreKey>(
create);
static Handshake_InitialPqcKeys_PqcPreKey? _defaultInstance;
@$pb.TagNumber(1)
$fixnum.Int64 get eccPreKeyId => $_getI64(0);
@$pb.TagNumber(1)
set eccPreKeyId($fixnum.Int64 value) => $_setInt64(0, value);
@$pb.TagNumber(1)
$core.bool hasEccPreKeyId() => $_has(0);
@$pb.TagNumber(1)
void clearEccPreKeyId() => $_clearField(1);
@$pb.TagNumber(2)
$core.List<$core.int> get eccPreKey => $_getN(1);
@$pb.TagNumber(2)
set eccPreKey($core.List<$core.int> value) => $_setBytes(1, value);
@$pb.TagNumber(2)
$core.bool hasEccPreKey() => $_has(1);
@$pb.TagNumber(2)
void clearEccPreKey() => $_clearField(2);
@$pb.TagNumber(3)
$fixnum.Int64 get kyberPreKeyId => $_getI64(2);
@$pb.TagNumber(3)
set kyberPreKeyId($fixnum.Int64 value) => $_setInt64(2, value);
@$pb.TagNumber(3)
$core.bool hasKyberPreKeyId() => $_has(2);
@$pb.TagNumber(3)
void clearKyberPreKeyId() => $_clearField(3);
@$pb.TagNumber(4)
$core.List<$core.int> get kyberPreKey => $_getN(3);
@$pb.TagNumber(4)
set kyberPreKey($core.List<$core.int> value) => $_setBytes(3, value);
@$pb.TagNumber(4)
$core.bool hasKyberPreKey() => $_has(3);
@$pb.TagNumber(4)
void clearKyberPreKey() => $_clearField(4);
@$pb.TagNumber(5)
$core.List<$core.int> get kyberPreKeySignature => $_getN(4);
@$pb.TagNumber(5)
set kyberPreKeySignature($core.List<$core.int> value) => $_setBytes(4, value);
@$pb.TagNumber(5)
$core.bool hasKyberPreKeySignature() => $_has(4);
@$pb.TagNumber(5)
void clearKyberPreKeySignature() => $_clearField(5);
}
class Handshake_InitialPqcKeys extends $pb.GeneratedMessage {
factory Handshake_InitialPqcKeys({
$core.List<$core.int>? publicIdentityKey,
$fixnum.Int64? registrationId,
$fixnum.Int64? eccSignedPrekeyId,
$core.List<$core.int>? eccSignedPrekey,
$core.List<$core.int>? eccSignedPrekeySignature,
$fixnum.Int64? kyberSignedPrekeyId,
$core.List<$core.int>? kyberSignedPrekey,
$core.List<$core.int>? kyberSignedPrekeySignature,
$core.Iterable<Handshake_InitialPqcKeys_PqcPreKey>? prekeys,
}) {
final result = create();
if (publicIdentityKey != null) result.publicIdentityKey = publicIdentityKey;
if (registrationId != null) result.registrationId = registrationId;
if (eccSignedPrekeyId != null) result.eccSignedPrekeyId = eccSignedPrekeyId;
if (eccSignedPrekey != null) result.eccSignedPrekey = eccSignedPrekey;
if (eccSignedPrekeySignature != null)
result.eccSignedPrekeySignature = eccSignedPrekeySignature;
if (kyberSignedPrekeyId != null)
result.kyberSignedPrekeyId = kyberSignedPrekeyId;
if (kyberSignedPrekey != null) result.kyberSignedPrekey = kyberSignedPrekey;
if (kyberSignedPrekeySignature != null)
result.kyberSignedPrekeySignature = kyberSignedPrekeySignature;
if (prekeys != null) result.prekeys.addAll(prekeys);
return result;
}
Handshake_InitialPqcKeys._();
factory Handshake_InitialPqcKeys.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory Handshake_InitialPqcKeys.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'Handshake.InitialPqcKeys',
package:
const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'),
createEmptyInstance: create)
..a<$core.List<$core.int>>(
1, _omitFieldNames ? '' : 'publicIdentityKey', $pb.PbFieldType.OY)
..aInt64(2, _omitFieldNames ? '' : 'registrationId')
..aInt64(3, _omitFieldNames ? '' : 'eccSignedPrekeyId')
..a<$core.List<$core.int>>(
4, _omitFieldNames ? '' : 'eccSignedPrekey', $pb.PbFieldType.OY)
..a<$core.List<$core.int>>(5,
_omitFieldNames ? '' : 'eccSignedPrekeySignature', $pb.PbFieldType.OY)
..aInt64(6, _omitFieldNames ? '' : 'kyberSignedPrekeyId')
..a<$core.List<$core.int>>(
7, _omitFieldNames ? '' : 'kyberSignedPrekey', $pb.PbFieldType.OY)
..a<$core.List<$core.int>>(8,
_omitFieldNames ? '' : 'kyberSignedPrekeySignature', $pb.PbFieldType.OY)
..pPM<Handshake_InitialPqcKeys_PqcPreKey>(
9, _omitFieldNames ? '' : 'prekeys',
subBuilder: Handshake_InitialPqcKeys_PqcPreKey.create)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
Handshake_InitialPqcKeys clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
Handshake_InitialPqcKeys copyWith(
void Function(Handshake_InitialPqcKeys) updates) =>
super.copyWith((message) => updates(message as Handshake_InitialPqcKeys))
as Handshake_InitialPqcKeys;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static Handshake_InitialPqcKeys create() => Handshake_InitialPqcKeys._();
@$core.override
Handshake_InitialPqcKeys createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static Handshake_InitialPqcKeys getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<Handshake_InitialPqcKeys>(create);
static Handshake_InitialPqcKeys? _defaultInstance;
@$pb.TagNumber(1)
$core.List<$core.int> get publicIdentityKey => $_getN(0);
@$pb.TagNumber(1)
set publicIdentityKey($core.List<$core.int> value) => $_setBytes(0, value);
@$pb.TagNumber(1)
$core.bool hasPublicIdentityKey() => $_has(0);
@$pb.TagNumber(1)
void clearPublicIdentityKey() => $_clearField(1);
@$pb.TagNumber(2)
$fixnum.Int64 get registrationId => $_getI64(1);
@$pb.TagNumber(2)
set registrationId($fixnum.Int64 value) => $_setInt64(1, value);
@$pb.TagNumber(2)
$core.bool hasRegistrationId() => $_has(1);
@$pb.TagNumber(2)
void clearRegistrationId() => $_clearField(2);
@$pb.TagNumber(3)
$fixnum.Int64 get eccSignedPrekeyId => $_getI64(2);
@$pb.TagNumber(3)
set eccSignedPrekeyId($fixnum.Int64 value) => $_setInt64(2, value);
@$pb.TagNumber(3)
$core.bool hasEccSignedPrekeyId() => $_has(2);
@$pb.TagNumber(3)
void clearEccSignedPrekeyId() => $_clearField(3);
@$pb.TagNumber(4)
$core.List<$core.int> get eccSignedPrekey => $_getN(3);
@$pb.TagNumber(4)
set eccSignedPrekey($core.List<$core.int> value) => $_setBytes(3, value);
@$pb.TagNumber(4)
$core.bool hasEccSignedPrekey() => $_has(3);
@$pb.TagNumber(4)
void clearEccSignedPrekey() => $_clearField(4);
@$pb.TagNumber(5)
$core.List<$core.int> get eccSignedPrekeySignature => $_getN(4);
@$pb.TagNumber(5)
set eccSignedPrekeySignature($core.List<$core.int> value) =>
$_setBytes(4, value);
@$pb.TagNumber(5)
$core.bool hasEccSignedPrekeySignature() => $_has(4);
@$pb.TagNumber(5)
void clearEccSignedPrekeySignature() => $_clearField(5);
@$pb.TagNumber(6)
$fixnum.Int64 get kyberSignedPrekeyId => $_getI64(5);
@$pb.TagNumber(6)
set kyberSignedPrekeyId($fixnum.Int64 value) => $_setInt64(5, value);
@$pb.TagNumber(6)
$core.bool hasKyberSignedPrekeyId() => $_has(5);
@$pb.TagNumber(6)
void clearKyberSignedPrekeyId() => $_clearField(6);
@$pb.TagNumber(7)
$core.List<$core.int> get kyberSignedPrekey => $_getN(6);
@$pb.TagNumber(7)
set kyberSignedPrekey($core.List<$core.int> value) => $_setBytes(6, value);
@$pb.TagNumber(7)
$core.bool hasKyberSignedPrekey() => $_has(6);
@$pb.TagNumber(7)
void clearKyberSignedPrekey() => $_clearField(7);
@$pb.TagNumber(8)
$core.List<$core.int> get kyberSignedPrekeySignature => $_getN(7);
@$pb.TagNumber(8)
set kyberSignedPrekeySignature($core.List<$core.int> value) =>
$_setBytes(7, value);
@$pb.TagNumber(8)
$core.bool hasKyberSignedPrekeySignature() => $_has(7);
@$pb.TagNumber(8)
void clearKyberSignedPrekeySignature() => $_clearField(8);
@$pb.TagNumber(9)
$pb.PbList<Handshake_InitialPqcKeys_PqcPreKey> get prekeys => $_getList(8);
} }
class Handshake_GetAuthChallenge extends $pb.GeneratedMessage { class Handshake_GetAuthChallenge extends $pb.GeneratedMessage {
@ -602,6 +889,7 @@ class Handshake_Authenticate extends $pb.GeneratedMessage {
$core.String? appVersion, $core.String? appVersion,
$fixnum.Int64? deviceId, $fixnum.Int64? deviceId,
$core.bool? inBackground, $core.bool? inBackground,
$core.bool? supportsMailboxV2,
}) { }) {
final result = create(); final result = create();
if (userId != null) result.userId = userId; if (userId != null) result.userId = userId;
@ -609,6 +897,7 @@ class Handshake_Authenticate extends $pb.GeneratedMessage {
if (appVersion != null) result.appVersion = appVersion; if (appVersion != null) result.appVersion = appVersion;
if (deviceId != null) result.deviceId = deviceId; if (deviceId != null) result.deviceId = deviceId;
if (inBackground != null) result.inBackground = inBackground; if (inBackground != null) result.inBackground = inBackground;
if (supportsMailboxV2 != null) result.supportsMailboxV2 = supportsMailboxV2;
return result; return result;
} }
@ -632,6 +921,7 @@ class Handshake_Authenticate extends $pb.GeneratedMessage {
..aOS(3, _omitFieldNames ? '' : 'appVersion') ..aOS(3, _omitFieldNames ? '' : 'appVersion')
..aInt64(4, _omitFieldNames ? '' : 'deviceId') ..aInt64(4, _omitFieldNames ? '' : 'deviceId')
..aOB(5, _omitFieldNames ? '' : 'inBackground') ..aOB(5, _omitFieldNames ? '' : 'inBackground')
..aOB(6, _omitFieldNames ? '' : 'supportsMailboxV2')
..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.')
@ -698,6 +988,17 @@ class Handshake_Authenticate extends $pb.GeneratedMessage {
$core.bool hasInBackground() => $_has(4); $core.bool hasInBackground() => $_has(4);
@$pb.TagNumber(5) @$pb.TagNumber(5)
void clearInBackground() => $_clearField(5); void clearInBackground() => $_clearField(5);
/// Set by clients that understand PendingMessagesV2 / RequestPendingMessages.
/// Absent or false means the legacy NewMessage(s) mailbox protocol is used.
@$pb.TagNumber(6)
$core.bool get supportsMailboxV2 => $_getBF(5);
@$pb.TagNumber(6)
set supportsMailboxV2($core.bool value) => $_setBool(5, value);
@$pb.TagNumber(6)
$core.bool hasSupportsMailboxV2() => $_has(5);
@$pb.TagNumber(6)
void clearSupportsMailboxV2() => $_clearField(6);
} }
class Handshake_AuthenticateWithLoginToken extends $pb.GeneratedMessage { class Handshake_AuthenticateWithLoginToken extends $pb.GeneratedMessage {
@ -707,6 +1008,7 @@ class Handshake_AuthenticateWithLoginToken extends $pb.GeneratedMessage {
$core.String? appVersion, $core.String? appVersion,
$fixnum.Int64? deviceId, $fixnum.Int64? deviceId,
$core.bool? inBackground, $core.bool? inBackground,
$core.bool? supportsMailboxV2,
}) { }) {
final result = create(); final result = create();
if (userId != null) result.userId = userId; if (userId != null) result.userId = userId;
@ -714,6 +1016,7 @@ class Handshake_AuthenticateWithLoginToken extends $pb.GeneratedMessage {
if (appVersion != null) result.appVersion = appVersion; if (appVersion != null) result.appVersion = appVersion;
if (deviceId != null) result.deviceId = deviceId; if (deviceId != null) result.deviceId = deviceId;
if (inBackground != null) result.inBackground = inBackground; if (inBackground != null) result.inBackground = inBackground;
if (supportsMailboxV2 != null) result.supportsMailboxV2 = supportsMailboxV2;
return result; return result;
} }
@ -738,6 +1041,7 @@ class Handshake_AuthenticateWithLoginToken extends $pb.GeneratedMessage {
..aOS(3, _omitFieldNames ? '' : 'appVersion') ..aOS(3, _omitFieldNames ? '' : 'appVersion')
..aInt64(4, _omitFieldNames ? '' : 'deviceId') ..aInt64(4, _omitFieldNames ? '' : 'deviceId')
..aOB(5, _omitFieldNames ? '' : 'inBackground') ..aOB(5, _omitFieldNames ? '' : 'inBackground')
..aOB(6, _omitFieldNames ? '' : 'supportsMailboxV2')
..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.')
@ -807,6 +1111,16 @@ class Handshake_AuthenticateWithLoginToken extends $pb.GeneratedMessage {
$core.bool hasInBackground() => $_has(4); $core.bool hasInBackground() => $_has(4);
@$pb.TagNumber(5) @$pb.TagNumber(5)
void clearInBackground() => $_clearField(5); void clearInBackground() => $_clearField(5);
/// See Authenticate.supports_mailbox_v2.
@$pb.TagNumber(6)
$core.bool get supportsMailboxV2 => $_getBF(5);
@$pb.TagNumber(6)
set supportsMailboxV2($core.bool value) => $_setBool(5, value);
@$pb.TagNumber(6)
$core.bool hasSupportsMailboxV2() => $_has(5);
@$pb.TagNumber(6)
void clearSupportsMailboxV2() => $_clearField(6);
} }
class Handshake_GetServerKeyForPasswordLessRecovery class Handshake_GetServerKeyForPasswordLessRecovery
@ -1388,11 +1702,13 @@ class ApplicationData_TextMessage extends $pb.GeneratedMessage {
$fixnum.Int64? userId, $fixnum.Int64? userId,
$core.List<$core.int>? body, $core.List<$core.int>? body,
$core.List<$core.int>? pushData, $core.List<$core.int>? pushData,
$core.bool? wakeReceiver,
}) { }) {
final result = create(); final result = create();
if (userId != null) result.userId = userId; if (userId != null) result.userId = userId;
if (body != null) result.body = body; if (body != null) result.body = body;
if (pushData != null) result.pushData = pushData; if (pushData != null) result.pushData = pushData;
if (wakeReceiver != null) result.wakeReceiver = wakeReceiver;
return result; return result;
} }
@ -1415,6 +1731,7 @@ class ApplicationData_TextMessage extends $pb.GeneratedMessage {
3, _omitFieldNames ? '' : 'body', $pb.PbFieldType.OY) 3, _omitFieldNames ? '' : 'body', $pb.PbFieldType.OY)
..a<$core.List<$core.int>>( ..a<$core.List<$core.int>>(
4, _omitFieldNames ? '' : 'pushData', $pb.PbFieldType.OY) 4, _omitFieldNames ? '' : 'pushData', $pb.PbFieldType.OY)
..aOB(5, _omitFieldNames ? '' : 'wakeReceiver')
..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.')
@ -1465,6 +1782,15 @@ class ApplicationData_TextMessage extends $pb.GeneratedMessage {
$core.bool hasPushData() => $_has(2); $core.bool hasPushData() => $_has(2);
@$pb.TagNumber(4) @$pb.TagNumber(4)
void clearPushData() => $_clearField(4); void clearPushData() => $_clearField(4);
@$pb.TagNumber(5)
$core.bool get wakeReceiver => $_getBF(3);
@$pb.TagNumber(5)
set wakeReceiver($core.bool value) => $_setBool(3, value);
@$pb.TagNumber(5)
$core.bool hasWakeReceiver() => $_has(3);
@$pb.TagNumber(5)
void clearWakeReceiver() => $_clearField(5);
} }
class ApplicationData_GetUserByUsername extends $pb.GeneratedMessage { class ApplicationData_GetUserByUsername extends $pb.GeneratedMessage {
@ -2176,6 +2502,8 @@ class ApplicationData_UploadPqcPreKeys extends $pb.GeneratedMessage {
$core.List<$core.int>? kyberSignedPrekey, $core.List<$core.int>? kyberSignedPrekey,
$core.List<$core.int>? kyberSignedPrekeySignature, $core.List<$core.int>? kyberSignedPrekeySignature,
$core.Iterable<ApplicationData_PqcPreKey>? prekeys, $core.Iterable<ApplicationData_PqcPreKey>? prekeys,
$core.List<$core.int>? publicIdentityKey,
$fixnum.Int64? registrationId,
}) { }) {
final result = create(); final result = create();
if (eccSignedPrekeyId != null) result.eccSignedPrekeyId = eccSignedPrekeyId; if (eccSignedPrekeyId != null) result.eccSignedPrekeyId = eccSignedPrekeyId;
@ -2188,6 +2516,8 @@ class ApplicationData_UploadPqcPreKeys extends $pb.GeneratedMessage {
if (kyberSignedPrekeySignature != null) if (kyberSignedPrekeySignature != null)
result.kyberSignedPrekeySignature = kyberSignedPrekeySignature; result.kyberSignedPrekeySignature = kyberSignedPrekeySignature;
if (prekeys != null) result.prekeys.addAll(prekeys); if (prekeys != null) result.prekeys.addAll(prekeys);
if (publicIdentityKey != null) result.publicIdentityKey = publicIdentityKey;
if (registrationId != null) result.registrationId = registrationId;
return result; return result;
} }
@ -2218,6 +2548,9 @@ class ApplicationData_UploadPqcPreKeys extends $pb.GeneratedMessage {
_omitFieldNames ? '' : 'kyberSignedPrekeySignature', $pb.PbFieldType.OY) _omitFieldNames ? '' : 'kyberSignedPrekeySignature', $pb.PbFieldType.OY)
..pPM<ApplicationData_PqcPreKey>(7, _omitFieldNames ? '' : 'prekeys', ..pPM<ApplicationData_PqcPreKey>(7, _omitFieldNames ? '' : 'prekeys',
subBuilder: ApplicationData_PqcPreKey.create) subBuilder: ApplicationData_PqcPreKey.create)
..a<$core.List<$core.int>>(
8, _omitFieldNames ? '' : 'publicIdentityKey', $pb.PbFieldType.OY)
..aInt64(9, _omitFieldNames ? '' : 'registrationId')
..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.')
@ -2301,6 +2634,26 @@ class ApplicationData_UploadPqcPreKeys extends $pb.GeneratedMessage {
@$pb.TagNumber(7) @$pb.TagNumber(7)
$pb.PbList<ApplicationData_PqcPreKey> get prekeys => $_getList(6); $pb.PbList<ApplicationData_PqcPreKey> get prekeys => $_getList(6);
/// Required when uploading keys for the first time after a keyless
/// registration. Optional for later key refreshes.
@$pb.TagNumber(8)
$core.List<$core.int> get publicIdentityKey => $_getN(7);
@$pb.TagNumber(8)
set publicIdentityKey($core.List<$core.int> value) => $_setBytes(7, value);
@$pb.TagNumber(8)
$core.bool hasPublicIdentityKey() => $_has(7);
@$pb.TagNumber(8)
void clearPublicIdentityKey() => $_clearField(8);
@$pb.TagNumber(9)
$fixnum.Int64 get registrationId => $_getI64(8);
@$pb.TagNumber(9)
set registrationId($fixnum.Int64 value) => $_setInt64(8, value);
@$pb.TagNumber(9)
$core.bool hasRegistrationId() => $_has(8);
@$pb.TagNumber(9)
void clearRegistrationId() => $_clearField(9);
} }
class ApplicationData_DownloadDone extends $pb.GeneratedMessage { class ApplicationData_DownloadDone extends $pb.GeneratedMessage {
@ -3301,6 +3654,52 @@ class ApplicationData_DisableMemoriesBackup extends $pb.GeneratedMessage {
static ApplicationData_DisableMemoriesBackup? _defaultInstance; static ApplicationData_DisableMemoriesBackup? _defaultInstance;
} }
/// Asks the server to (re)start a mailbox drain for the authenticated user.
/// The server responds immediately; messages arrive as PendingMessagesV2.
class ApplicationData_RequestPendingMessages extends $pb.GeneratedMessage {
factory ApplicationData_RequestPendingMessages() => create();
ApplicationData_RequestPendingMessages._();
factory ApplicationData_RequestPendingMessages.fromBuffer(
$core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory ApplicationData_RequestPendingMessages.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'ApplicationData.RequestPendingMessages',
package:
const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'),
createEmptyInstance: create)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
ApplicationData_RequestPendingMessages clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
ApplicationData_RequestPendingMessages copyWith(
void Function(ApplicationData_RequestPendingMessages) updates) =>
super.copyWith((message) =>
updates(message as ApplicationData_RequestPendingMessages))
as ApplicationData_RequestPendingMessages;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static ApplicationData_RequestPendingMessages create() =>
ApplicationData_RequestPendingMessages._();
@$core.override
ApplicationData_RequestPendingMessages createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static ApplicationData_RequestPendingMessages getDefault() =>
_defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<
ApplicationData_RequestPendingMessages>(create);
static ApplicationData_RequestPendingMessages? _defaultInstance;
}
enum ApplicationData_ApplicationData { enum ApplicationData_ApplicationData {
textMessage, textMessage,
getUserByUsername, getUserByUsername,
@ -3330,6 +3729,7 @@ enum ApplicationData_ApplicationData {
deleteMemory, deleteMemory,
disableMemoriesBackup, disableMemoriesBackup,
uploadPqcPrekeys, uploadPqcPrekeys,
requestPendingMessages,
notSet notSet
} }
@ -3363,6 +3763,7 @@ class ApplicationData extends $pb.GeneratedMessage {
ApplicationData_DeleteMemory? deleteMemory, ApplicationData_DeleteMemory? deleteMemory,
ApplicationData_DisableMemoriesBackup? disableMemoriesBackup, ApplicationData_DisableMemoriesBackup? disableMemoriesBackup,
ApplicationData_UploadPqcPreKeys? uploadPqcPrekeys, ApplicationData_UploadPqcPreKeys? uploadPqcPrekeys,
ApplicationData_RequestPendingMessages? requestPendingMessages,
}) { }) {
final result = create(); final result = create();
if (textMessage != null) result.textMessage = textMessage; if (textMessage != null) result.textMessage = textMessage;
@ -3404,6 +3805,8 @@ class ApplicationData extends $pb.GeneratedMessage {
if (disableMemoriesBackup != null) if (disableMemoriesBackup != null)
result.disableMemoriesBackup = disableMemoriesBackup; result.disableMemoriesBackup = disableMemoriesBackup;
if (uploadPqcPrekeys != null) result.uploadPqcPrekeys = uploadPqcPrekeys; if (uploadPqcPrekeys != null) result.uploadPqcPrekeys = uploadPqcPrekeys;
if (requestPendingMessages != null)
result.requestPendingMessages = requestPendingMessages;
return result; return result;
} }
@ -3446,6 +3849,7 @@ class ApplicationData extends $pb.GeneratedMessage {
38: ApplicationData_ApplicationData.deleteMemory, 38: ApplicationData_ApplicationData.deleteMemory,
39: ApplicationData_ApplicationData.disableMemoriesBackup, 39: ApplicationData_ApplicationData.disableMemoriesBackup,
40: ApplicationData_ApplicationData.uploadPqcPrekeys, 40: ApplicationData_ApplicationData.uploadPqcPrekeys,
43: ApplicationData_ApplicationData.requestPendingMessages,
0: ApplicationData_ApplicationData.notSet 0: ApplicationData_ApplicationData.notSet
}; };
static final $pb.BuilderInfo _i = $pb.BuilderInfo( static final $pb.BuilderInfo _i = $pb.BuilderInfo(
@ -3481,7 +3885,8 @@ class ApplicationData extends $pb.GeneratedMessage {
37, 37,
38, 38,
39, 39,
40 40,
43
]) ])
..aOM<ApplicationData_TextMessage>(1, _omitFieldNames ? '' : 'textMessage', ..aOM<ApplicationData_TextMessage>(1, _omitFieldNames ? '' : 'textMessage',
protoName: 'textMessage', protoName: 'textMessage',
@ -3579,6 +3984,9 @@ class ApplicationData extends $pb.GeneratedMessage {
..aOM<ApplicationData_UploadPqcPreKeys>( ..aOM<ApplicationData_UploadPqcPreKeys>(
40, _omitFieldNames ? '' : 'uploadPqcPrekeys', 40, _omitFieldNames ? '' : 'uploadPqcPrekeys',
subBuilder: ApplicationData_UploadPqcPreKeys.create) subBuilder: ApplicationData_UploadPqcPreKeys.create)
..aOM<ApplicationData_RequestPendingMessages>(
43, _omitFieldNames ? '' : 'requestPendingMessages',
subBuilder: ApplicationData_RequestPendingMessages.create)
..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.')
@ -3628,6 +4036,7 @@ class ApplicationData extends $pb.GeneratedMessage {
@$pb.TagNumber(38) @$pb.TagNumber(38)
@$pb.TagNumber(39) @$pb.TagNumber(39)
@$pb.TagNumber(40) @$pb.TagNumber(40)
@$pb.TagNumber(43)
ApplicationData_ApplicationData whichApplicationData() => ApplicationData_ApplicationData whichApplicationData() =>
_ApplicationData_ApplicationDataByTag[$_whichOneof(0)]!; _ApplicationData_ApplicationDataByTag[$_whichOneof(0)]!;
@$pb.TagNumber(1) @$pb.TagNumber(1)
@ -3658,6 +4067,7 @@ class ApplicationData extends $pb.GeneratedMessage {
@$pb.TagNumber(38) @$pb.TagNumber(38)
@$pb.TagNumber(39) @$pb.TagNumber(39)
@$pb.TagNumber(40) @$pb.TagNumber(40)
@$pb.TagNumber(43)
void clearApplicationData() => $_clearField($_whichOneof(0)); void clearApplicationData() => $_clearField($_whichOneof(0));
@$pb.TagNumber(1) @$pb.TagNumber(1)
@ -4003,6 +4413,20 @@ class ApplicationData extends $pb.GeneratedMessage {
void clearUploadPqcPrekeys() => $_clearField(40); void clearUploadPqcPrekeys() => $_clearField(40);
@$pb.TagNumber(40) @$pb.TagNumber(40)
ApplicationData_UploadPqcPreKeys ensureUploadPqcPrekeys() => $_ensure(27); ApplicationData_UploadPqcPreKeys ensureUploadPqcPrekeys() => $_ensure(27);
@$pb.TagNumber(43)
ApplicationData_RequestPendingMessages get requestPendingMessages =>
$_getN(28);
@$pb.TagNumber(43)
set requestPendingMessages(ApplicationData_RequestPendingMessages value) =>
$_setField(43, value);
@$pb.TagNumber(43)
$core.bool hasRequestPendingMessages() => $_has(28);
@$pb.TagNumber(43)
void clearRequestPendingMessages() => $_clearField(43);
@$pb.TagNumber(43)
ApplicationData_RequestPendingMessages ensureRequestPendingMessages() =>
$_ensure(28);
} }
class Response_PreKey extends $pb.GeneratedMessage { class Response_PreKey extends $pb.GeneratedMessage {
@ -4173,18 +4597,84 @@ class Response_PqcPrekeys extends $pb.GeneratedMessage {
$pb.PbList<ApplicationData_PqcPreKey> get prekeys => $_getList(0); $pb.PbList<ApplicationData_PqcPreKey> get prekeys => $_getList(0);
} }
enum Response_Ok_Ok { none, prekeys, prekeysPqc, notSet } /// Response to a PendingMessagesV2 batch. Contains only the delivery IDs the
/// client has durably persisted; the server deletes exactly those rows.
class Response_AcknowledgedPendingMessages extends $pb.GeneratedMessage {
factory Response_AcknowledgedPendingMessages({
$core.Iterable<$fixnum.Int64>? deliveryIds,
}) {
final result = create();
if (deliveryIds != null) result.deliveryIds.addAll(deliveryIds);
return result;
}
Response_AcknowledgedPendingMessages._();
factory Response_AcknowledgedPendingMessages.fromBuffer(
$core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory Response_AcknowledgedPendingMessages.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'Response.AcknowledgedPendingMessages',
package:
const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'),
createEmptyInstance: create)
..p<$fixnum.Int64>(
1, _omitFieldNames ? '' : 'deliveryIds', $pb.PbFieldType.K6)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
Response_AcknowledgedPendingMessages clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
Response_AcknowledgedPendingMessages copyWith(
void Function(Response_AcknowledgedPendingMessages) updates) =>
super.copyWith((message) =>
updates(message as Response_AcknowledgedPendingMessages))
as Response_AcknowledgedPendingMessages;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static Response_AcknowledgedPendingMessages create() =>
Response_AcknowledgedPendingMessages._();
@$core.override
Response_AcknowledgedPendingMessages createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static Response_AcknowledgedPendingMessages getDefault() =>
_defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<
Response_AcknowledgedPendingMessages>(create);
static Response_AcknowledgedPendingMessages? _defaultInstance;
@$pb.TagNumber(1)
$pb.PbList<$fixnum.Int64> get deliveryIds => $_getList(0);
}
enum Response_Ok_Ok {
none,
prekeys,
prekeysPqc,
acknowledgedPendingMessages,
notSet
}
class Response_Ok extends $pb.GeneratedMessage { class Response_Ok extends $pb.GeneratedMessage {
factory Response_Ok({ factory Response_Ok({
$core.bool? none, $core.bool? none,
Response_Prekeys? prekeys, Response_Prekeys? prekeys,
Response_PqcPrekeys? prekeysPqc, Response_PqcPrekeys? prekeysPqc,
Response_AcknowledgedPendingMessages? acknowledgedPendingMessages,
}) { }) {
final result = create(); final result = create();
if (none != null) result.none = none; if (none != null) result.none = none;
if (prekeys != null) result.prekeys = prekeys; if (prekeys != null) result.prekeys = prekeys;
if (prekeysPqc != null) result.prekeysPqc = prekeysPqc; if (prekeysPqc != null) result.prekeysPqc = prekeysPqc;
if (acknowledgedPendingMessages != null)
result.acknowledgedPendingMessages = acknowledgedPendingMessages;
return result; return result;
} }
@ -4201,6 +4691,7 @@ class Response_Ok extends $pb.GeneratedMessage {
1: Response_Ok_Ok.none, 1: Response_Ok_Ok.none,
2: Response_Ok_Ok.prekeys, 2: Response_Ok_Ok.prekeys,
3: Response_Ok_Ok.prekeysPqc, 3: Response_Ok_Ok.prekeysPqc,
4: Response_Ok_Ok.acknowledgedPendingMessages,
0: Response_Ok_Ok.notSet 0: Response_Ok_Ok.notSet
}; };
static final $pb.BuilderInfo _i = $pb.BuilderInfo( static final $pb.BuilderInfo _i = $pb.BuilderInfo(
@ -4208,12 +4699,15 @@ class Response_Ok extends $pb.GeneratedMessage {
package: package:
const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'), const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'),
createEmptyInstance: create) createEmptyInstance: create)
..oo(0, [1, 2, 3]) ..oo(0, [1, 2, 3, 4])
..aOB(1, _omitFieldNames ? '' : 'None', protoName: 'None') ..aOB(1, _omitFieldNames ? '' : 'None', protoName: 'None')
..aOM<Response_Prekeys>(2, _omitFieldNames ? '' : 'prekeys', ..aOM<Response_Prekeys>(2, _omitFieldNames ? '' : 'prekeys',
subBuilder: Response_Prekeys.create) subBuilder: Response_Prekeys.create)
..aOM<Response_PqcPrekeys>(3, _omitFieldNames ? '' : 'prekeysPqc', ..aOM<Response_PqcPrekeys>(3, _omitFieldNames ? '' : 'prekeysPqc',
subBuilder: Response_PqcPrekeys.create) subBuilder: Response_PqcPrekeys.create)
..aOM<Response_AcknowledgedPendingMessages>(
4, _omitFieldNames ? '' : 'acknowledgedPendingMessages',
subBuilder: Response_AcknowledgedPendingMessages.create)
..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.')
@ -4238,10 +4732,12 @@ class Response_Ok extends $pb.GeneratedMessage {
@$pb.TagNumber(1) @$pb.TagNumber(1)
@$pb.TagNumber(2) @$pb.TagNumber(2)
@$pb.TagNumber(3) @$pb.TagNumber(3)
@$pb.TagNumber(4)
Response_Ok_Ok whichOk() => _Response_Ok_OkByTag[$_whichOneof(0)]!; Response_Ok_Ok whichOk() => _Response_Ok_OkByTag[$_whichOneof(0)]!;
@$pb.TagNumber(1) @$pb.TagNumber(1)
@$pb.TagNumber(2) @$pb.TagNumber(2)
@$pb.TagNumber(3) @$pb.TagNumber(3)
@$pb.TagNumber(4)
void clearOk() => $_clearField($_whichOneof(0)); void clearOk() => $_clearField($_whichOneof(0));
@$pb.TagNumber(1) @$pb.TagNumber(1)
@ -4274,6 +4770,20 @@ class Response_Ok extends $pb.GeneratedMessage {
void clearPrekeysPqc() => $_clearField(3); void clearPrekeysPqc() => $_clearField(3);
@$pb.TagNumber(3) @$pb.TagNumber(3)
Response_PqcPrekeys ensurePrekeysPqc() => $_ensure(2); Response_PqcPrekeys ensurePrekeysPqc() => $_ensure(2);
@$pb.TagNumber(4)
Response_AcknowledgedPendingMessages get acknowledgedPendingMessages =>
$_getN(3);
@$pb.TagNumber(4)
set acknowledgedPendingMessages(Response_AcknowledgedPendingMessages value) =>
$_setField(4, value);
@$pb.TagNumber(4)
$core.bool hasAcknowledgedPendingMessages() => $_has(3);
@$pb.TagNumber(4)
void clearAcknowledgedPendingMessages() => $_clearField(4);
@$pb.TagNumber(4)
Response_AcknowledgedPendingMessages ensureAcknowledgedPendingMessages() =>
$_ensure(3);
} }
enum Response_Response { ok, error, notSet } enum Response_Response { ok, error, notSet }

View file

@ -183,6 +183,7 @@ const Handshake$json = {
'3': [ '3': [
Handshake_RequestPOW$json, Handshake_RequestPOW$json,
Handshake_Register$json, Handshake_Register$json,
Handshake_InitialPqcKeys$json,
Handshake_GetAuthChallenge$json, Handshake_GetAuthChallenge$json,
Handshake_GetUserIdByUsername$json, Handshake_GetUserIdByUsername$json,
Handshake_GetAuthToken$json, Handshake_GetAuthToken$json,
@ -221,18 +222,46 @@ const Handshake_Register$json = {
'3': 3, '3': 3,
'4': 1, '4': 1,
'5': 12, '5': 12,
'10': 'publicIdentityKey' '9': 1,
'10': 'publicIdentityKey',
'17': true
},
{
'1': 'signed_prekey',
'3': 4,
'4': 1,
'5': 12,
'9': 2,
'10': 'signedPrekey',
'17': true
}, },
{'1': 'signed_prekey', '3': 4, '4': 1, '5': 12, '10': 'signedPrekey'},
{ {
'1': 'signed_prekey_signature', '1': 'signed_prekey_signature',
'3': 5, '3': 5,
'4': 1, '4': 1,
'5': 12, '5': 12,
'10': 'signedPrekeySignature' '9': 3,
'10': 'signedPrekeySignature',
'17': true
},
{
'1': 'signed_prekey_id',
'3': 6,
'4': 1,
'5': 3,
'9': 4,
'10': 'signedPrekeyId',
'17': true
},
{
'1': 'registration_id',
'3': 7,
'4': 1,
'5': 3,
'9': 5,
'10': 'registrationId',
'17': true
}, },
{'1': 'signed_prekey_id', '3': 6, '4': 1, '5': 3, '10': 'signedPrekeyId'},
{'1': 'registration_id', '3': 7, '4': 1, '5': 3, '10': 'registrationId'},
{'1': 'is_ios', '3': 8, '4': 1, '5': 8, '10': 'isIos'}, {'1': 'is_ios', '3': 8, '4': 1, '5': 8, '10': 'isIos'},
{'1': 'lang_code', '3': 9, '4': 1, '5': 9, '10': 'langCode'}, {'1': 'lang_code', '3': 9, '4': 1, '5': 9, '10': 'langCode'},
{'1': 'proof_of_work', '3': 10, '4': 1, '5': 3, '10': 'proofOfWork'}, {'1': 'proof_of_work', '3': 10, '4': 1, '5': 3, '10': 'proofOfWork'},
@ -241,14 +270,114 @@ const Handshake_Register$json = {
'3': 11, '3': 11,
'4': 1, '4': 1,
'5': 12, '5': 12,
'9': 1, '9': 6,
'10': 'loginToken', '10': 'loginToken',
'17': true '17': true
}, },
{
'1': 'initial_pqc_keys',
'3': 12,
'4': 1,
'5': 11,
'6': '.client_to_server.Handshake.InitialPqcKeys',
'9': 7,
'10': 'initialPqcKeys',
'17': true
},
], ],
'8': [ '8': [
{'1': '_invite_code'}, {'1': '_invite_code'},
{'1': '_public_identity_key'},
{'1': '_signed_prekey'},
{'1': '_signed_prekey_signature'},
{'1': '_signed_prekey_id'},
{'1': '_registration_id'},
{'1': '_login_token'}, {'1': '_login_token'},
{'1': '_initial_pqc_keys'},
],
};
@$core.Deprecated('Use handshakeDescriptor instead')
const Handshake_InitialPqcKeys$json = {
'1': 'InitialPqcKeys',
'2': [
{
'1': 'public_identity_key',
'3': 1,
'4': 1,
'5': 12,
'10': 'publicIdentityKey'
},
{'1': 'registration_id', '3': 2, '4': 1, '5': 3, '10': 'registrationId'},
{
'1': 'ecc_signed_prekey_id',
'3': 3,
'4': 1,
'5': 3,
'10': 'eccSignedPrekeyId'
},
{
'1': 'ecc_signed_prekey',
'3': 4,
'4': 1,
'5': 12,
'10': 'eccSignedPrekey'
},
{
'1': 'ecc_signed_prekey_signature',
'3': 5,
'4': 1,
'5': 12,
'10': 'eccSignedPrekeySignature'
},
{
'1': 'kyber_signed_prekey_id',
'3': 6,
'4': 1,
'5': 3,
'10': 'kyberSignedPrekeyId'
},
{
'1': 'kyber_signed_prekey',
'3': 7,
'4': 1,
'5': 12,
'10': 'kyberSignedPrekey'
},
{
'1': 'kyber_signed_prekey_signature',
'3': 8,
'4': 1,
'5': 12,
'10': 'kyberSignedPrekeySignature'
},
{
'1': 'prekeys',
'3': 9,
'4': 3,
'5': 11,
'6': '.client_to_server.Handshake.InitialPqcKeys.PqcPreKey',
'10': 'prekeys'
},
],
'3': [Handshake_InitialPqcKeys_PqcPreKey$json],
};
@$core.Deprecated('Use handshakeDescriptor instead')
const Handshake_InitialPqcKeys_PqcPreKey$json = {
'1': 'PqcPreKey',
'2': [
{'1': 'ecc_pre_key_id', '3': 1, '4': 1, '5': 3, '10': 'eccPreKeyId'},
{'1': 'ecc_pre_key', '3': 2, '4': 1, '5': 12, '10': 'eccPreKey'},
{'1': 'kyber_pre_key_id', '3': 3, '4': 1, '5': 3, '10': 'kyberPreKeyId'},
{'1': 'kyber_pre_key', '3': 4, '4': 1, '5': 12, '10': 'kyberPreKey'},
{
'1': 'kyber_pre_key_signature',
'3': 5,
'4': 1,
'5': 12,
'10': 'kyberPreKeySignature'
},
], ],
}; };
@ -307,11 +436,21 @@ const Handshake_Authenticate$json = {
'10': 'inBackground', '10': 'inBackground',
'17': true '17': true
}, },
{
'1': 'supports_mailbox_v2',
'3': 6,
'4': 1,
'5': 8,
'9': 3,
'10': 'supportsMailboxV2',
'17': true
},
], ],
'8': [ '8': [
{'1': '_app_version'}, {'1': '_app_version'},
{'1': '_device_id'}, {'1': '_device_id'},
{'1': '_in_background'}, {'1': '_in_background'},
{'1': '_supports_mailbox_v2'},
], ],
}; };
@ -330,6 +469,18 @@ const Handshake_AuthenticateWithLoginToken$json = {
{'1': 'app_version', '3': 3, '4': 1, '5': 9, '10': 'appVersion'}, {'1': 'app_version', '3': 3, '4': 1, '5': 9, '10': 'appVersion'},
{'1': 'device_id', '3': 4, '4': 1, '5': 3, '10': 'deviceId'}, {'1': 'device_id', '3': 4, '4': 1, '5': 3, '10': 'deviceId'},
{'1': 'in_background', '3': 5, '4': 1, '5': 8, '10': 'inBackground'}, {'1': 'in_background', '3': 5, '4': 1, '5': 8, '10': 'inBackground'},
{
'1': 'supports_mailbox_v2',
'3': 6,
'4': 1,
'5': 8,
'9': 0,
'10': 'supportsMailboxV2',
'17': true
},
],
'8': [
{'1': '_supports_mailbox_v2'},
], ],
}; };
@ -443,39 +594,59 @@ final $typed_data.Uint8List handshakeDescriptor = $convert.base64Decode(
'ZmljYXRpb25IAFIgcmVnaXN0ZXJQYXNzd29yZGxlc3NOb3RpZmljYXRpb24SjQEKI2NoZWNrX2' 'ZmljYXRpb25IAFIgcmVnaXN0ZXJQYXNzd29yZGxlc3NOb3RpZmljYXRpb24SjQEKI2NoZWNrX2'
'Zvcl9wYXNzd29yZGxlc3Nfbm90aWZpY2F0aW9uGAogASgLMjwuY2xpZW50X3RvX3NlcnZlci5I' 'Zvcl9wYXNzd29yZGxlc3Nfbm90aWZpY2F0aW9uGAogASgLMjwuY2xpZW50X3RvX3NlcnZlci5I'
'YW5kc2hha2UuQ2hlY2tGb3JQYXNzd29yZGxlc3NOb3RpZmljYXRpb25IAFIgY2hlY2tGb3JQYX' 'YW5kc2hha2UuQ2hlY2tGb3JQYXNzd29yZGxlc3NOb3RpZmljYXRpb25IAFIgY2hlY2tGb3JQYX'
'Nzd29yZGxlc3NOb3RpZmljYXRpb24aDAoKUmVxdWVzdFBPVxrKAwoIUmVnaXN0ZXISGgoIdXNl' 'Nzd29yZGxlc3NOb3RpZmljYXRpb24aDAoKUmVxdWVzdFBPVxrCBQoIUmVnaXN0ZXISGgoIdXNl'
'cm5hbWUYASABKAlSCHVzZXJuYW1lEiQKC2ludml0ZV9jb2RlGAIgASgJSABSCmludml0ZUNvZG' 'cm5hbWUYASABKAlSCHVzZXJuYW1lEiQKC2ludml0ZV9jb2RlGAIgASgJSABSCmludml0ZUNvZG'
'WIAQESLgoTcHVibGljX2lkZW50aXR5X2tleRgDIAEoDFIRcHVibGljSWRlbnRpdHlLZXkSIwoN' 'WIAQESMwoTcHVibGljX2lkZW50aXR5X2tleRgDIAEoDEgBUhFwdWJsaWNJZGVudGl0eUtleYgB'
'c2lnbmVkX3ByZWtleRgEIAEoDFIMc2lnbmVkUHJla2V5EjYKF3NpZ25lZF9wcmVrZXlfc2lnbm' 'ARIoCg1zaWduZWRfcHJla2V5GAQgASgMSAJSDHNpZ25lZFByZWtleYgBARI7ChdzaWduZWRfcH'
'F0dXJlGAUgASgMUhVzaWduZWRQcmVrZXlTaWduYXR1cmUSKAoQc2lnbmVkX3ByZWtleV9pZBgG' 'Jla2V5X3NpZ25hdHVyZRgFIAEoDEgDUhVzaWduZWRQcmVrZXlTaWduYXR1cmWIAQESLQoQc2ln'
'IAEoA1IOc2lnbmVkUHJla2V5SWQSJwoPcmVnaXN0cmF0aW9uX2lkGAcgASgDUg5yZWdpc3RyYX' 'bmVkX3ByZWtleV9pZBgGIAEoA0gEUg5zaWduZWRQcmVrZXlJZIgBARIsCg9yZWdpc3RyYXRpb2'
'Rpb25JZBIVCgZpc19pb3MYCCABKAhSBWlzSW9zEhsKCWxhbmdfY29kZRgJIAEoCVIIbGFuZ0Nv' '5faWQYByABKANIBVIOcmVnaXN0cmF0aW9uSWSIAQESFQoGaXNfaW9zGAggASgIUgVpc0lvcxIb'
'ZGUSIgoNcHJvb2Zfb2Zfd29yaxgKIAEoA1ILcHJvb2ZPZldvcmsSJAoLbG9naW5fdG9rZW4YCy' 'CglsYW5nX2NvZGUYCSABKAlSCGxhbmdDb2RlEiIKDXByb29mX29mX3dvcmsYCiABKANSC3Byb2'
'ABKAxIAVIKbG9naW5Ub2tlbogBAUIOCgxfaW52aXRlX2NvZGVCDgoMX2xvZ2luX3Rva2VuGhIK' '9mT2ZXb3JrEiQKC2xvZ2luX3Rva2VuGAsgASgMSAZSCmxvZ2luVG9rZW6IAQESWQoQaW5pdGlh'
'EEdldEF1dGhDaGFsbGVuZ2UaMQoTR2V0VXNlcklkQnlVc2VybmFtZRIaCgh1c2VybmFtZRgBIA' 'bF9wcWNfa2V5cxgMIAEoCzIqLmNsaWVudF90b19zZXJ2ZXIuSGFuZHNoYWtlLkluaXRpYWxQcW'
'EoCVIIdXNlcm5hbWUaQwoMR2V0QXV0aFRva2VuEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBIa' 'NLZXlzSAdSDmluaXRpYWxQcWNLZXlziAEBQg4KDF9pbnZpdGVfY29kZUIWChRfcHVibGljX2lk'
'CghyZXNwb25zZRgCIAEoDFIIcmVzcG9uc2Ua6AEKDEF1dGhlbnRpY2F0ZRIXCgd1c2VyX2lkGA' 'ZW50aXR5X2tleUIQCg5fc2lnbmVkX3ByZWtleUIaChhfc2lnbmVkX3ByZWtleV9zaWduYXR1cm'
'EgASgDUgZ1c2VySWQSHQoKYXV0aF90b2tlbhgCIAEoDFIJYXV0aFRva2VuEiQKC2FwcF92ZXJz' 'VCEwoRX3NpZ25lZF9wcmVrZXlfaWRCEgoQX3JlZ2lzdHJhdGlvbl9pZEIOCgxfbG9naW5fdG9r'
'aW9uGAMgASgJSABSCmFwcFZlcnNpb26IAQESIAoJZGV2aWNlX2lkGAQgASgDSAFSCGRldmljZU' 'ZW5CEwoRX2luaXRpYWxfcHFjX2tleXMa1AUKDkluaXRpYWxQcWNLZXlzEi4KE3B1YmxpY19pZG'
'lkiAEBEigKDWluX2JhY2tncm91bmQYBSABKAhIAlIMaW5CYWNrZ3JvdW5kiAEBQg4KDF9hcHBf' 'VudGl0eV9rZXkYASABKAxSEXB1YmxpY0lkZW50aXR5S2V5EicKD3JlZ2lzdHJhdGlvbl9pZBgC'
'dmVyc2lvbkIMCgpfZGV2aWNlX2lkQhAKDl9pbl9iYWNrZ3JvdW5kGsYBChpBdXRoZW50aWNhdG' 'IAEoA1IOcmVnaXN0cmF0aW9uSWQSLwoUZWNjX3NpZ25lZF9wcmVrZXlfaWQYAyABKANSEWVjY1'
'VXaXRoTG9naW5Ub2tlbhIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQSLAoSc2VjcmV0X2xvZ2lu' 'NpZ25lZFByZWtleUlkEioKEWVjY19zaWduZWRfcHJla2V5GAQgASgMUg9lY2NTaWduZWRQcmVr'
'X3Rva2VuGAIgASgMUhBzZWNyZXRMb2dpblRva2VuEh8KC2FwcF92ZXJzaW9uGAMgASgJUgphcH' 'ZXkSPQobZWNjX3NpZ25lZF9wcmVrZXlfc2lnbmF0dXJlGAUgASgMUhhlY2NTaWduZWRQcmVrZX'
'BWZXJzaW9uEhsKCWRldmljZV9pZBgEIAEoA1IIZGV2aWNlSWQSIwoNaW5fYmFja2dyb3VuZBgF' 'lTaWduYXR1cmUSMwoWa3liZXJfc2lnbmVkX3ByZWtleV9pZBgGIAEoA1ITa3liZXJTaWduZWRQ'
'IAEoCFIMaW5CYWNrZ3JvdW5kGqUCCiNHZXRTZXJ2ZXJLZXlGb3JQYXNzd29yZExlc3NSZWNvdm' 'cmVrZXlJZBIuChNreWJlcl9zaWduZWRfcHJla2V5GAcgASgMUhFreWJlclNpZ25lZFByZWtleR'
'VyeRIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQSMgoVc2VydmVyX2tleV9wcm90ZWN0aW9uGAIg' 'JBCh1reWJlcl9zaWduZWRfcHJla2V5X3NpZ25hdHVyZRgIIAEoDFIaa3liZXJTaWduZWRQcmVr'
'ASgMUhNzZXJ2ZXJLZXlQcm90ZWN0aW9uEi0KEHBpbl91bmxvY2tfdG9rZW4YAyABKAxIAFIOcG' 'ZXlTaWduYXR1cmUSTgoHcHJla2V5cxgJIAMoCzI0LmNsaWVudF90b19zZXJ2ZXIuSGFuZHNoYW'
'luVW5sb2NrVG9rZW6IAQESMQoScGluX3Byb3RlY3Rpb25fa2V5GAQgASgMSAFSEHBpblByb3Rl' 'tlLkluaXRpYWxQcWNLZXlzLlBxY1ByZUtleVIHcHJla2V5cxrUAQoJUHFjUHJlS2V5EiMKDmVj'
'Y3Rpb25LZXmIAQESGQoFZW1haWwYBSABKAlIAlIFZW1haWyIAQFCEwoRX3Bpbl91bmxvY2tfdG' 'Y19wcmVfa2V5X2lkGAEgASgDUgtlY2NQcmVLZXlJZBIeCgtlY2NfcHJlX2tleRgCIAEoDFIJZW'
'9rZW5CFQoTX3Bpbl9wcm90ZWN0aW9uX2tleUIICgZfZW1haWwaywEKIFJlZ2lzdGVyUGFzc3dv' 'NjUHJlS2V5EicKEGt5YmVyX3ByZV9rZXlfaWQYAyABKANSDWt5YmVyUHJlS2V5SWQSIgoNa3li'
'cmRsZXNzTm90aWZpY2F0aW9uEicKD25vdGlmaWNhdGlvbl9pZBgBIAEoCVIObm90aWZpY2F0aW' 'ZXJfcHJlX2tleRgEIAEoDFILa3liZXJQcmVLZXkSNQoXa3liZXJfcHJlX2tleV9zaWduYXR1cm'
'9uSWQSLgoTZG93bmxvYWRfYXV0aF90b2tlbhgCIAEoDFIRZG93bmxvYWRBdXRoVG9rZW4SGwoJ' 'UYBSABKAxSFGt5YmVyUHJlS2V5U2lnbmF0dXJlGhIKEEdldEF1dGhDaGFsbGVuZ2UaMQoTR2V0'
'bGFuZ19jb2RlGAMgASgJUghsYW5nQ29kZRIiCgpnb29nbGVfZmNtGAQgASgJSABSCWdvb2dsZU' 'VXNlcklkQnlVc2VybmFtZRIaCgh1c2VybmFtZRgBIAEoCVIIdXNlcm5hbWUaQwoMR2V0QXV0aF'
'ZjbYgBAUINCgtfZ29vZ2xlX2ZjbRq8AQogQ2hlY2tGb3JQYXNzd29yZGxlc3NOb3RpZmljYXRp' 'Rva2VuEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBIaCghyZXNwb25zZRgCIAEoDFIIcmVzcG9u'
'b24SJwoPbm90aWZpY2F0aW9uX2lkGAEgASgJUg5ub3RpZmljYXRpb25JZBIuChNkb3dubG9hZF' 'c2UatQIKDEF1dGhlbnRpY2F0ZRIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQSHQoKYXV0aF90b2'
'9hdXRoX3Rva2VuGAIgASgMUhFkb3dubG9hZEF1dGhUb2tlbhI/ChxhbHJlYWR5X3JlY2VpdmVk' 'tlbhgCIAEoDFIJYXV0aFRva2VuEiQKC2FwcF92ZXJzaW9uGAMgASgJSABSCmFwcFZlcnNpb26I'
'X21lc3NhZ2VfaWRzGAMgAygDUhlhbHJlYWR5UmVjZWl2ZWRNZXNzYWdlSWRzQgsKCUhhbmRzaG' 'AQESIAoJZGV2aWNlX2lkGAQgASgDSAFSCGRldmljZUlkiAEBEigKDWluX2JhY2tncm91bmQYBS'
'FrZQ=='); 'ABKAhIAlIMaW5CYWNrZ3JvdW5kiAEBEjMKE3N1cHBvcnRzX21haWxib3hfdjIYBiABKAhIA1IR'
'c3VwcG9ydHNNYWlsYm94VjKIAQFCDgoMX2FwcF92ZXJzaW9uQgwKCl9kZXZpY2VfaWRCEAoOX2'
'luX2JhY2tncm91bmRCFgoUX3N1cHBvcnRzX21haWxib3hfdjIakwIKGkF1dGhlbnRpY2F0ZVdp'
'dGhMb2dpblRva2VuEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBIsChJzZWNyZXRfbG9naW5fdG'
'9rZW4YAiABKAxSEHNlY3JldExvZ2luVG9rZW4SHwoLYXBwX3ZlcnNpb24YAyABKAlSCmFwcFZl'
'cnNpb24SGwoJZGV2aWNlX2lkGAQgASgDUghkZXZpY2VJZBIjCg1pbl9iYWNrZ3JvdW5kGAUgAS'
'gIUgxpbkJhY2tncm91bmQSMwoTc3VwcG9ydHNfbWFpbGJveF92MhgGIAEoCEgAUhFzdXBwb3J0'
'c01haWxib3hWMogBAUIWChRfc3VwcG9ydHNfbWFpbGJveF92MhqlAgojR2V0U2VydmVyS2V5Rm'
'9yUGFzc3dvcmRMZXNzUmVjb3ZlcnkSFwoHdXNlcl9pZBgBIAEoA1IGdXNlcklkEjIKFXNlcnZl'
'cl9rZXlfcHJvdGVjdGlvbhgCIAEoDFITc2VydmVyS2V5UHJvdGVjdGlvbhItChBwaW5fdW5sb2'
'NrX3Rva2VuGAMgASgMSABSDnBpblVubG9ja1Rva2VuiAEBEjEKEnBpbl9wcm90ZWN0aW9uX2tl'
'eRgEIAEoDEgBUhBwaW5Qcm90ZWN0aW9uS2V5iAEBEhkKBWVtYWlsGAUgASgJSAJSBWVtYWlsiA'
'EBQhMKEV9waW5fdW5sb2NrX3Rva2VuQhUKE19waW5fcHJvdGVjdGlvbl9rZXlCCAoGX2VtYWls'
'GssBCiBSZWdpc3RlclBhc3N3b3JkbGVzc05vdGlmaWNhdGlvbhInCg9ub3RpZmljYXRpb25faW'
'QYASABKAlSDm5vdGlmaWNhdGlvbklkEi4KE2Rvd25sb2FkX2F1dGhfdG9rZW4YAiABKAxSEWRv'
'd25sb2FkQXV0aFRva2VuEhsKCWxhbmdfY29kZRgDIAEoCVIIbGFuZ0NvZGUSIgoKZ29vZ2xlX2'
'ZjbRgEIAEoCUgAUglnb29nbGVGY22IAQFCDQoLX2dvb2dsZV9mY20avAEKIENoZWNrRm9yUGFz'
'c3dvcmRsZXNzTm90aWZpY2F0aW9uEicKD25vdGlmaWNhdGlvbl9pZBgBIAEoCVIObm90aWZpY2'
'F0aW9uSWQSLgoTZG93bmxvYWRfYXV0aF90b2tlbhgCIAEoDFIRZG93bmxvYWRBdXRoVG9rZW4S'
'PwocYWxyZWFkeV9yZWNlaXZlZF9tZXNzYWdlX2lkcxgDIAMoA1IZYWxyZWFkeVJlY2VpdmVkTW'
'Vzc2FnZUlkc0ILCglIYW5kc2hha2U=');
@$core.Deprecated('Use applicationDataDescriptor instead') @$core.Deprecated('Use applicationDataDescriptor instead')
const ApplicationData$json = { const ApplicationData$json = {
@ -733,6 +904,15 @@ const ApplicationData$json = {
'9': 0, '9': 0,
'10': 'uploadPqcPrekeys' '10': 'uploadPqcPrekeys'
}, },
{
'1': 'request_pending_messages',
'3': 43,
'4': 1,
'5': 11,
'6': '.client_to_server.ApplicationData.RequestPendingMessages',
'9': 0,
'10': 'requestPendingMessages'
},
], ],
'3': [ '3': [
ApplicationData_TextMessage$json, ApplicationData_TextMessage$json,
@ -763,7 +943,8 @@ const ApplicationData$json = {
ApplicationData_GetMemoriesUrl$json, ApplicationData_GetMemoriesUrl$json,
ApplicationData_GetMemoriesUsage$json, ApplicationData_GetMemoriesUsage$json,
ApplicationData_DeleteMemory$json, ApplicationData_DeleteMemory$json,
ApplicationData_DisableMemoriesBackup$json ApplicationData_DisableMemoriesBackup$json,
ApplicationData_RequestPendingMessages$json
], ],
'8': [ '8': [
{'1': 'ApplicationData'}, {'1': 'ApplicationData'},
@ -773,6 +954,8 @@ const ApplicationData$json = {
{'1': 11, '2': 12}, {'1': 11, '2': 12},
{'1': 13, '2': 18}, {'1': 13, '2': 18},
{'1': 19, '2': 20}, {'1': 19, '2': 20},
{'1': 41, '2': 42},
{'1': 42, '2': 43},
], ],
}; };
@ -791,6 +974,7 @@ const ApplicationData_TextMessage$json = {
'10': 'pushData', '10': 'pushData',
'17': true '17': true
}, },
{'1': 'wake_receiver', '3': 5, '4': 1, '5': 8, '10': 'wakeReceiver'},
], ],
'8': [ '8': [
{'1': '_push_data'}, {'1': '_push_data'},
@ -951,6 +1135,28 @@ const ApplicationData_UploadPqcPreKeys$json = {
'6': '.client_to_server.ApplicationData.PqcPreKey', '6': '.client_to_server.ApplicationData.PqcPreKey',
'10': 'prekeys' '10': 'prekeys'
}, },
{
'1': 'public_identity_key',
'3': 8,
'4': 1,
'5': 12,
'9': 0,
'10': 'publicIdentityKey',
'17': true
},
{
'1': 'registration_id',
'3': 9,
'4': 1,
'5': 3,
'9': 1,
'10': 'registrationId',
'17': true
},
],
'8': [
{'1': '_public_identity_key'},
{'1': '_registration_id'},
], ],
}; };
@ -1108,6 +1314,11 @@ const ApplicationData_DisableMemoriesBackup$json = {
'1': 'DisableMemoriesBackup', '1': 'DisableMemoriesBackup',
}; };
@$core.Deprecated('Use applicationDataDescriptor instead')
const ApplicationData_RequestPendingMessages$json = {
'1': 'RequestPendingMessages',
};
/// Descriptor for `ApplicationData`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `ApplicationData`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List applicationDataDescriptor = $convert.base64Decode( final $typed_data.Uint8List applicationDataDescriptor = $convert.base64Decode(
'Cg9BcHBsaWNhdGlvbkRhdGESUQoLdGV4dE1lc3NhZ2UYASABKAsyLS5jbGllbnRfdG9fc2Vydm' 'Cg9BcHBsaWNhdGlvbkRhdGESUQoLdGV4dE1lc3NhZ2UYASABKAsyLS5jbGllbnRfdG9fc2Vydm'
@ -1161,49 +1372,55 @@ final $typed_data.Uint8List applicationDataDescriptor = $convert.base64Decode(
'c2VydmVyLkFwcGxpY2F0aW9uRGF0YS5EaXNhYmxlTWVtb3JpZXNCYWNrdXBIAFIVZGlzYWJsZU' 'c2VydmVyLkFwcGxpY2F0aW9uRGF0YS5EaXNhYmxlTWVtb3JpZXNCYWNrdXBIAFIVZGlzYWJsZU'
'1lbW9yaWVzQmFja3VwEmIKEnVwbG9hZF9wcWNfcHJla2V5cxgoIAEoCzIyLmNsaWVudF90b19z' '1lbW9yaWVzQmFja3VwEmIKEnVwbG9hZF9wcWNfcHJla2V5cxgoIAEoCzIyLmNsaWVudF90b19z'
'ZXJ2ZXIuQXBwbGljYXRpb25EYXRhLlVwbG9hZFBxY1ByZUtleXNIAFIQdXBsb2FkUHFjUHJla2' 'ZXJ2ZXIuQXBwbGljYXRpb25EYXRhLlVwbG9hZFBxY1ByZUtleXNIAFIQdXBsb2FkUHFjUHJla2'
'V5cxpqCgtUZXh0TWVzc2FnZRIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQSEgoEYm9keRgDIAEo' 'V5cxJ0ChhyZXF1ZXN0X3BlbmRpbmdfbWVzc2FnZXMYKyABKAsyOC5jbGllbnRfdG9fc2VydmVy'
'DFIEYm9keRIgCglwdXNoX2RhdGEYBCABKAxIAFIIcHVzaERhdGGIAQFCDAoKX3B1c2hfZGF0YR' 'LkFwcGxpY2F0aW9uRGF0YS5SZXF1ZXN0UGVuZGluZ01lc3NhZ2VzSABSFnJlcXVlc3RQZW5kaW'
'ovChFHZXRVc2VyQnlVc2VybmFtZRIaCgh1c2VybmFtZRgBIAEoCVIIdXNlcm5hbWUaLAoOQ2hh' '5nTWVzc2FnZXMajwEKC1RleHRNZXNzYWdlEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBISCgRi'
'bmdlVXNlcm5hbWUSGgoIdXNlcm5hbWUYASABKAlSCHVzZXJuYW1lGjUKFFVwZGF0ZUdvb2dsZU' 'b2R5GAMgASgMUgRib2R5EiAKCXB1c2hfZGF0YRgEIAEoDEgAUghwdXNoRGF0YYgBARIjCg13YW'
'ZjbVRva2VuEh0KCmdvb2dsZV9mY20YASABKAlSCWdvb2dsZUZjbRomCgtHZXRVc2VyQnlJZBIX' 'tlX3JlY2VpdmVyGAUgASgIUgx3YWtlUmVjZWl2ZXJCDAoKX3B1c2hfZGF0YRovChFHZXRVc2Vy'
'Cgd1c2VyX2lkGAEgASgDUgZ1c2VySWQaEwoRR2V0QXZhaWxhYmxlUGxhbnMaFQoTR2V0Q3Vycm' 'QnlVc2VybmFtZRIaCgh1c2VybmFtZRgBIAEoCVIIdXNlcm5hbWUaLAoOQ2hhbmdlVXNlcm5hbW'
'VudFBsYW5JbmZvcxovChRSZW1vdmVBZGRpdGlvbmFsVXNlchIXCgd1c2VyX2lkGAEgASgDUgZ1' 'USGgoIdXNlcm5hbWUYASABKAlSCHVzZXJuYW1lGjUKFFVwZGF0ZUdvb2dsZUZjbVRva2VuEh0K'
'c2VySWQaLQoSR2V0UHJla2V5c0J5VXNlcklkEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBoyCh' 'Cmdvb2dsZV9mY20YASABKAlSCWdvb2dsZUZjbRomCgtHZXRVc2VyQnlJZBIXCgd1c2VyX2lkGA'
'dHZXRTaWduZWRQcmVLZXlCeVVzZXJJZBIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQamwEKElVw' 'EgASgDUgZ1c2VySWQaEwoRR2V0QXZhaWxhYmxlUGxhbnMaFQoTR2V0Q3VycmVudFBsYW5JbmZv'
'ZGF0ZVNpZ25lZFByZUtleRIoChBzaWduZWRfcHJla2V5X2lkGAEgASgDUg5zaWduZWRQcmVrZX' 'cxovChRSZW1vdmVBZGRpdGlvbmFsVXNlchIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQaLQoSR2'
'lJZBIjCg1zaWduZWRfcHJla2V5GAIgASgMUgxzaWduZWRQcmVrZXkSNgoXc2lnbmVkX3ByZWtl' 'V0UHJla2V5c0J5VXNlcklkEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBoyChdHZXRTaWduZWRQ'
'eV9zaWduYXR1cmUYAyABKAxSFXNpZ25lZFByZWtleVNpZ25hdHVyZRrUAQoJUHFjUHJlS2V5Ei' 'cmVLZXlCeVVzZXJJZBIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQamwEKElVwZGF0ZVNpZ25lZF'
'MKDmVjY19wcmVfa2V5X2lkGAEgASgDUgtlY2NQcmVLZXlJZBIeCgtlY2NfcHJlX2tleRgCIAEo' 'ByZUtleRIoChBzaWduZWRfcHJla2V5X2lkGAEgASgDUg5zaWduZWRQcmVrZXlJZBIjCg1zaWdu'
'DFIJZWNjUHJlS2V5EicKEGt5YmVyX3ByZV9rZXlfaWQYAyABKANSDWt5YmVyUHJlS2V5SWQSIg' 'ZWRfcHJla2V5GAIgASgMUgxzaWduZWRQcmVrZXkSNgoXc2lnbmVkX3ByZWtleV9zaWduYXR1cm'
'oNa3liZXJfcHJlX2tleRgEIAEoDFILa3liZXJQcmVLZXkSNQoXa3liZXJfcHJlX2tleV9zaWdu' 'UYAyABKAxSFXNpZ25lZFByZWtleVNpZ25hdHVyZRrUAQoJUHFjUHJlS2V5EiMKDmVjY19wcmVf'
'YXR1cmUYBSABKAxSFGt5YmVyUHJlS2V5U2lnbmF0dXJlGp0DChBVcGxvYWRQcWNQcmVLZXlzEi' 'a2V5X2lkGAEgASgDUgtlY2NQcmVLZXlJZBIeCgtlY2NfcHJlX2tleRgCIAEoDFIJZWNjUHJlS2'
'8KFGVjY19zaWduZWRfcHJla2V5X2lkGAEgASgDUhFlY2NTaWduZWRQcmVrZXlJZBIqChFlY2Nf' 'V5EicKEGt5YmVyX3ByZV9rZXlfaWQYAyABKANSDWt5YmVyUHJlS2V5SWQSIgoNa3liZXJfcHJl'
'c2lnbmVkX3ByZWtleRgCIAEoDFIPZWNjU2lnbmVkUHJla2V5Ej0KG2VjY19zaWduZWRfcHJla2' 'X2tleRgEIAEoDFILa3liZXJQcmVLZXkSNQoXa3liZXJfcHJlX2tleV9zaWduYXR1cmUYBSABKA'
'V5X3NpZ25hdHVyZRgDIAEoDFIYZWNjU2lnbmVkUHJla2V5U2lnbmF0dXJlEjMKFmt5YmVyX3Np' 'xSFGt5YmVyUHJlS2V5U2lnbmF0dXJlGqwEChBVcGxvYWRQcWNQcmVLZXlzEi8KFGVjY19zaWdu'
'Z25lZF9wcmVrZXlfaWQYBCABKANSE2t5YmVyU2lnbmVkUHJla2V5SWQSLgoTa3liZXJfc2lnbm' 'ZWRfcHJla2V5X2lkGAEgASgDUhFlY2NTaWduZWRQcmVrZXlJZBIqChFlY2Nfc2lnbmVkX3ByZW'
'VkX3ByZWtleRgFIAEoDFIRa3liZXJTaWduZWRQcmVrZXkSQQoda3liZXJfc2lnbmVkX3ByZWtl' 'tleRgCIAEoDFIPZWNjU2lnbmVkUHJla2V5Ej0KG2VjY19zaWduZWRfcHJla2V5X3NpZ25hdHVy'
'eV9zaWduYXR1cmUYBiABKAxSGmt5YmVyU2lnbmVkUHJla2V5U2lnbmF0dXJlEkUKB3ByZWtleX' 'ZRgDIAEoDFIYZWNjU2lnbmVkUHJla2V5U2lnbmF0dXJlEjMKFmt5YmVyX3NpZ25lZF9wcmVrZX'
'MYByADKAsyKy5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5QcWNQcmVLZXlSB3By' 'lfaWQYBCABKANSE2t5YmVyU2lnbmVkUHJla2V5SWQSLgoTa3liZXJfc2lnbmVkX3ByZWtleRgF'
'ZWtleXMaNQoMRG93bmxvYWREb25lEiUKDmRvd25sb2FkX3Rva2VuGAEgASgMUg1kb3dubG9hZF' 'IAEoDFIRa3liZXJTaWduZWRQcmVrZXkSQQoda3liZXJfc2lnbmVkX3ByZWtleV9zaWduYXR1cm'
'Rva2VuGk4KClJlcG9ydFVzZXISKAoQcmVwb3J0ZWRfdXNlcl9pZBgBIAEoA1IOcmVwb3J0ZWRV' 'UYBiABKAxSGmt5YmVyU2lnbmVkUHJla2V5U2lnbmF0dXJlEkUKB3ByZWtleXMYByADKAsyKy5j'
'c2VySWQSFgoGcmVhc29uGAIgASgJUgZyZWFzb24acQoLSVBBUHVyY2hhc2USHQoKcHJvZHVjdF' 'bGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5QcWNQcmVLZXlSB3ByZWtleXMSMwoTcH'
'9pZBgBIAEoCVIJcHJvZHVjdElkEhYKBnNvdXJjZRgCIAEoCVIGc291cmNlEisKEXZlcmlmaWNh' 'VibGljX2lkZW50aXR5X2tleRgIIAEoDEgAUhFwdWJsaWNJZGVudGl0eUtleYgBARIsCg9yZWdp'
'dGlvbl9kYXRhGAMgASgJUhB2ZXJpZmljYXRpb25EYXRhGg8KDUlQQUZvcmNlQ2hlY2saDwoNRG' 'c3RyYXRpb25faWQYCSABKANIAVIOcmVnaXN0cmF0aW9uSWSIAQFCFgoUX3B1YmxpY19pZGVudG'
'VsZXRlQWNjb3VudBosChFBZGRBZGRpdGlvbmFsVXNlchIXCgd1c2VyX2lkGAEgASgDUgZ1c2Vy' 'l0eV9rZXlCEgoQX3JlZ2lzdHJhdGlvbl9pZBo1CgxEb3dubG9hZERvbmUSJQoOZG93bmxvYWRf'
'SWQaMAoNU2V0TG9naW5Ub2tlbhIfCgtsb2dpbl90b2tlbhgBIAEoDFIKbG9naW5Ub2tlbhqOAQ' 'dG9rZW4YASABKAxSDWRvd25sb2FkVG9rZW4aTgoKUmVwb3J0VXNlchIoChByZXBvcnRlZF91c2'
'ocUmVnaXN0ZXJQYXNzd29yZExlc3NSZWNvdmVyeRIuChJlbmNyeXB0ZWRTZXJ2ZXJLZXkYASAB' 'VyX2lkGAEgASgDUg5yZXBvcnRlZFVzZXJJZBIWCgZyZWFzb24YAiABKAlSBnJlYXNvbhpxCgtJ'
'KAxSEmVuY3J5cHRlZFNlcnZlcktleRIrCg5waW5VbmxvY2tUb2tlbhgCIAEoDEgAUg5waW5Vbm' 'UEFQdXJjaGFzZRIdCgpwcm9kdWN0X2lkGAEgASgJUglwcm9kdWN0SWQSFgoGc291cmNlGAIgAS'
'xvY2tUb2tlbogBAUIRCg9fcGluVW5sb2NrVG9rZW4acAoYUGFzc3dvcmRsZXNzTm90aWZpY2F0' 'gJUgZzb3VyY2USKwoRdmVyaWZpY2F0aW9uX2RhdGEYAyABKAlSEHZlcmlmaWNhdGlvbkRhdGEa'
'aW9uEicKD25vdGlmaWNhdGlvbl9pZBgBIAEoCVIObm90aWZpY2F0aW9uSWQSKwoRZW5jcnlwdG' 'DwoNSVBBRm9yY2VDaGVjaxoPCg1EZWxldGVBY2NvdW50GiwKEUFkZEFkZGl0aW9uYWxVc2VyEh'
'VkX21lc3NhZ2UYAiABKAxSEGVuY3J5cHRlZE1lc3NhZ2UaawoVUmVxdWVzdE1lbW9yaWVzVXBs' 'cKB3VzZXJfaWQYASABKANSBnVzZXJJZBowCg1TZXRMb2dpblRva2VuEh8KC2xvZ2luX3Rva2Vu'
'b2FkEhIKBHNpemUYASABKANSBHNpemUSIwoNb3JpZ2luYWxfZGF0ZRgCIAEoA1IMb3JpZ2luYW' 'GAEgASgMUgpsb2dpblRva2VuGo4BChxSZWdpc3RlclBhc3N3b3JkTGVzc1JlY292ZXJ5Ei4KEm'
'xEYXRlEhkKCG1lZGlhX2lkGAMgASgJUgdtZWRpYUlkGjIKFUNvbmZpcm1NZW1vcmllc1VwbG9h' 'VuY3J5cHRlZFNlcnZlcktleRgBIAEoDFISZW5jcnlwdGVkU2VydmVyS2V5EisKDnBpblVubG9j'
'ZBIZCghtZWRpYV9pZBgBIAEoCVIHbWVkaWFJZBpICg9HZXRNZW1vcmllc0xpc3QSHwoLb2Zmc2' 'a1Rva2VuGAIgASgMSABSDnBpblVubG9ja1Rva2VuiAEBQhEKD19waW5VbmxvY2tUb2tlbhpwCh'
'V0X2RhdGUYASABKANSCm9mZnNldERhdGUSFAoFbGltaXQYAiABKANSBWxpbWl0GkkKDkdldE1l' 'hQYXNzd29yZGxlc3NOb3RpZmljYXRpb24SJwoPbm90aWZpY2F0aW9uX2lkGAEgASgJUg5ub3Rp'
'bW9yaWVzVXJsEhkKCG1lZGlhX2lkGAEgASgJUgdtZWRpYUlkEhwKCXRodW1ibmFpbBgCIAEoCF' 'ZmljYXRpb25JZBIrChFlbmNyeXB0ZWRfbWVzc2FnZRgCIAEoDFIQZW5jcnlwdGVkTWVzc2FnZR'
'IJdGh1bWJuYWlsGhIKEEdldE1lbW9yaWVzVXNhZ2UaKQoMRGVsZXRlTWVtb3J5EhkKCG1lZGlh' 'prChVSZXF1ZXN0TWVtb3JpZXNVcGxvYWQSEgoEc2l6ZRgBIAEoA1IEc2l6ZRIjCg1vcmlnaW5h'
'X2lkGAEgASgJUgdtZWRpYUlkGhcKFURpc2FibGVNZW1vcmllc0JhY2t1cEIRCg9BcHBsaWNhdG' 'bF9kYXRlGAIgASgDUgxvcmlnaW5hbERhdGUSGQoIbWVkaWFfaWQYAyABKAlSB21lZGlhSWQaMg'
'lvbkRhdGFKBAgJEApKBAgLEAxKBAgNEBJKBAgTEBQ='); 'oVQ29uZmlybU1lbW9yaWVzVXBsb2FkEhkKCG1lZGlhX2lkGAEgASgJUgdtZWRpYUlkGkgKD0dl'
'dE1lbW9yaWVzTGlzdBIfCgtvZmZzZXRfZGF0ZRgBIAEoA1IKb2Zmc2V0RGF0ZRIUCgVsaW1pdB'
'gCIAEoA1IFbGltaXQaSQoOR2V0TWVtb3JpZXNVcmwSGQoIbWVkaWFfaWQYASABKAlSB21lZGlh'
'SWQSHAoJdGh1bWJuYWlsGAIgASgIUgl0aHVtYm5haWwaEgoQR2V0TWVtb3JpZXNVc2FnZRopCg'
'xEZWxldGVNZW1vcnkSGQoIbWVkaWFfaWQYASABKAlSB21lZGlhSWQaFwoVRGlzYWJsZU1lbW9y'
'aWVzQmFja3VwGhgKFlJlcXVlc3RQZW5kaW5nTWVzc2FnZXNCEQoPQXBwbGljYXRpb25EYXRhSg'
'QICRAKSgQICxAMSgQIDRASSgQIExAUSgQIKRAqSgQIKhAr');
@$core.Deprecated('Use responseDescriptor instead') @$core.Deprecated('Use responseDescriptor instead')
const Response$json = { const Response$json = {
@ -1232,6 +1449,7 @@ const Response$json = {
Response_PreKey$json, Response_PreKey$json,
Response_Prekeys$json, Response_Prekeys$json,
Response_PqcPrekeys$json, Response_PqcPrekeys$json,
Response_AcknowledgedPendingMessages$json,
Response_Ok$json Response_Ok$json
], ],
'8': [ '8': [
@ -1278,6 +1496,14 @@ const Response_PqcPrekeys$json = {
], ],
}; };
@$core.Deprecated('Use responseDescriptor instead')
const Response_AcknowledgedPendingMessages$json = {
'1': 'AcknowledgedPendingMessages',
'2': [
{'1': 'delivery_ids', '3': 1, '4': 3, '5': 3, '10': 'deliveryIds'},
],
};
@$core.Deprecated('Use responseDescriptor instead') @$core.Deprecated('Use responseDescriptor instead')
const Response_Ok$json = { const Response_Ok$json = {
'1': 'Ok', '1': 'Ok',
@ -1301,6 +1527,15 @@ const Response_Ok$json = {
'9': 0, '9': 0,
'10': 'prekeysPqc' '10': 'prekeysPqc'
}, },
{
'1': 'acknowledged_pending_messages',
'3': 4,
'4': 1,
'5': 11,
'6': '.client_to_server.Response.AcknowledgedPendingMessages',
'9': 0,
'10': 'acknowledgedPendingMessages'
},
], ],
'8': [ '8': [
{'1': 'Ok'}, {'1': 'Ok'},
@ -1314,7 +1549,11 @@ final $typed_data.Uint8List responseDescriptor = $convert.base64Decode(
'Eg4KAmlkGAEgASgDUgJpZBIWCgZwcmVrZXkYAiABKAxSBnByZWtleRpGCgdQcmVrZXlzEjsKB3' 'Eg4KAmlkGAEgASgDUgJpZBIWCgZwcmVrZXkYAiABKAxSBnByZWtleRpGCgdQcmVrZXlzEjsKB3'
'ByZWtleXMYASADKAsyIS5jbGllbnRfdG9fc2VydmVyLlJlc3BvbnNlLlByZUtleVIHcHJla2V5' 'ByZWtleXMYASADKAsyIS5jbGllbnRfdG9fc2VydmVyLlJlc3BvbnNlLlByZUtleVIHcHJla2V5'
'cxpTCgpQcWNQcmVrZXlzEkUKB3ByZWtleXMYASADKAsyKy5jbGllbnRfdG9fc2VydmVyLkFwcG' 'cxpTCgpQcWNQcmVrZXlzEkUKB3ByZWtleXMYASADKAsyKy5jbGllbnRfdG9fc2VydmVyLkFwcG'
'xpY2F0aW9uRGF0YS5QcWNQcmVLZXlSB3ByZWtleXMaqgEKAk9rEhQKBE5vbmUYASABKAhIAFIE' 'xpY2F0aW9uRGF0YS5QcWNQcmVLZXlSB3ByZWtleXMaQAobQWNrbm93bGVkZ2VkUGVuZGluZ01l'
'Tm9uZRI+CgdwcmVrZXlzGAIgASgLMiIuY2xpZW50X3RvX3NlcnZlci5SZXNwb25zZS5QcmVrZX' 'c3NhZ2VzEiEKDGRlbGl2ZXJ5X2lkcxgBIAMoA1ILZGVsaXZlcnlJZHMaqAIKAk9rEhQKBE5vbm'
'lzSABSB3ByZWtleXMSSAoLcHJla2V5c19wcWMYAyABKAsyJS5jbGllbnRfdG9fc2VydmVyLlJl' 'UYASABKAhIAFIETm9uZRI+CgdwcmVrZXlzGAIgASgLMiIuY2xpZW50X3RvX3NlcnZlci5SZXNw'
'c3BvbnNlLlBxY1ByZWtleXNIAFIKcHJla2V5c1BxY0IECgJPa0IKCghSZXNwb25zZQ=='); 'b25zZS5QcmVrZXlzSABSB3ByZWtleXMSSAoLcHJla2V5c19wcWMYAyABKAsyJS5jbGllbnRfdG'
'9fc2VydmVyLlJlc3BvbnNlLlBxY1ByZWtleXNIAFIKcHJla2V5c1BxYxJ8Ch1hY2tub3dsZWRn'
'ZWRfcGVuZGluZ19tZXNzYWdlcxgEIAEoCzI2LmNsaWVudF90b19zZXJ2ZXIuUmVzcG9uc2UuQW'
'Nrbm93bGVkZ2VkUGVuZGluZ01lc3NhZ2VzSABSG2Fja25vd2xlZGdlZFBlbmRpbmdNZXNzYWdl'
'c0IECgJPa0IKCghSZXNwb25zZQ==');

View file

@ -96,6 +96,7 @@ enum V0_Kind {
error, error,
newMessages, newMessages,
requestNewPqcPreKeys, requestNewPqcPreKeys,
mailboxDrained,
notSet notSet
} }
@ -108,6 +109,7 @@ class V0 extends $pb.GeneratedMessage {
$0.ErrorCode? error, $0.ErrorCode? error,
NewMessages? newMessages, NewMessages? newMessages,
$core.bool? requestNewPqcPreKeys, $core.bool? requestNewPqcPreKeys,
$core.bool? mailboxDrained,
}) { }) {
final result = create(); final result = create();
if (seq != null) result.seq = seq; if (seq != null) result.seq = seq;
@ -118,6 +120,7 @@ class V0 extends $pb.GeneratedMessage {
if (newMessages != null) result.newMessages = newMessages; if (newMessages != null) result.newMessages = newMessages;
if (requestNewPqcPreKeys != null) if (requestNewPqcPreKeys != null)
result.requestNewPqcPreKeys = requestNewPqcPreKeys; result.requestNewPqcPreKeys = requestNewPqcPreKeys;
if (mailboxDrained != null) result.mailboxDrained = mailboxDrained;
return result; return result;
} }
@ -137,6 +140,7 @@ class V0 extends $pb.GeneratedMessage {
6: V0_Kind.error, 6: V0_Kind.error,
7: V0_Kind.newMessages, 7: V0_Kind.newMessages,
8: V0_Kind.requestNewPqcPreKeys, 8: V0_Kind.requestNewPqcPreKeys,
11: V0_Kind.mailboxDrained,
0: V0_Kind.notSet 0: V0_Kind.notSet
}; };
static final $pb.BuilderInfo _i = $pb.BuilderInfo( static final $pb.BuilderInfo _i = $pb.BuilderInfo(
@ -144,7 +148,7 @@ class V0 extends $pb.GeneratedMessage {
package: package:
const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'), const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'),
createEmptyInstance: create) createEmptyInstance: create)
..oo(0, [2, 3, 4, 6, 7, 8]) ..oo(0, [2, 3, 4, 6, 7, 8, 11])
..a<$fixnum.Int64>(1, _omitFieldNames ? '' : 'seq', $pb.PbFieldType.OU6, ..a<$fixnum.Int64>(1, _omitFieldNames ? '' : 'seq', $pb.PbFieldType.OU6,
defaultOrMaker: $fixnum.Int64.ZERO) defaultOrMaker: $fixnum.Int64.ZERO)
..aOM<Response>(2, _omitFieldNames ? '' : 'response', ..aOM<Response>(2, _omitFieldNames ? '' : 'response',
@ -159,6 +163,8 @@ class V0 extends $pb.GeneratedMessage {
protoName: 'newMessages', subBuilder: NewMessages.create) protoName: 'newMessages', subBuilder: NewMessages.create)
..aOB(8, _omitFieldNames ? '' : 'RequestNewPqcPreKeys', ..aOB(8, _omitFieldNames ? '' : 'RequestNewPqcPreKeys',
protoName: 'RequestNewPqcPreKeys') protoName: 'RequestNewPqcPreKeys')
..aOB(11, _omitFieldNames ? '' : 'mailboxDrained',
protoName: 'mailboxDrained')
..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.')
@ -185,6 +191,7 @@ class V0 extends $pb.GeneratedMessage {
@$pb.TagNumber(6) @$pb.TagNumber(6)
@$pb.TagNumber(7) @$pb.TagNumber(7)
@$pb.TagNumber(8) @$pb.TagNumber(8)
@$pb.TagNumber(11)
V0_Kind whichKind() => _V0_KindByTag[$_whichOneof(0)]!; V0_Kind whichKind() => _V0_KindByTag[$_whichOneof(0)]!;
@$pb.TagNumber(2) @$pb.TagNumber(2)
@$pb.TagNumber(3) @$pb.TagNumber(3)
@ -192,6 +199,7 @@ class V0 extends $pb.GeneratedMessage {
@$pb.TagNumber(6) @$pb.TagNumber(6)
@$pb.TagNumber(7) @$pb.TagNumber(7)
@$pb.TagNumber(8) @$pb.TagNumber(8)
@$pb.TagNumber(11)
void clearKind() => $_clearField($_whichOneof(0)); void clearKind() => $_clearField($_whichOneof(0));
@$pb.TagNumber(1) @$pb.TagNumber(1)
@ -262,6 +270,15 @@ class V0 extends $pb.GeneratedMessage {
$core.bool hasRequestNewPqcPreKeys() => $_has(6); $core.bool hasRequestNewPqcPreKeys() => $_has(6);
@$pb.TagNumber(8) @$pb.TagNumber(8)
void clearRequestNewPqcPreKeys() => $_clearField(8); void clearRequestNewPqcPreKeys() => $_clearField(8);
@$pb.TagNumber(11)
$core.bool get mailboxDrained => $_getBF(7);
@$pb.TagNumber(11)
set mailboxDrained($core.bool value) => $_setBool(7, value);
@$pb.TagNumber(11)
$core.bool hasMailboxDrained() => $_has(7);
@$pb.TagNumber(11)
void clearMailboxDrained() => $_clearField(11);
} }
class NewMessage extends $pb.GeneratedMessage { class NewMessage extends $pb.GeneratedMessage {

View file

@ -96,6 +96,14 @@ const V0$json = {
'9': 0, '9': 0,
'10': 'error' '10': 'error'
}, },
{
'1': 'mailboxDrained',
'3': 11,
'4': 1,
'5': 8,
'9': 0,
'10': 'mailboxDrained'
},
], ],
'8': [ '8': [
{'1': 'Kind'}, {'1': 'Kind'},
@ -110,7 +118,8 @@ final $typed_data.Uint8List v0Descriptor = $convert.base64Decode(
'0uc2VydmVyX3RvX2NsaWVudC5OZXdNZXNzYWdlc0gAUgtuZXdNZXNzYWdlcxIuChFSZXF1ZXN0' '0uc2VydmVyX3RvX2NsaWVudC5OZXdNZXNzYWdlc0gAUgtuZXdNZXNzYWdlcxIuChFSZXF1ZXN0'
'TmV3UHJlS2V5cxgEIAEoCEgAUhFSZXF1ZXN0TmV3UHJlS2V5cxI0ChRSZXF1ZXN0TmV3UHFjUH' 'TmV3UHJlS2V5cxgEIAEoCEgAUhFSZXF1ZXN0TmV3UHJlS2V5cxI0ChRSZXF1ZXN0TmV3UHFjUH'
'JlS2V5cxgIIAEoCEgAUhRSZXF1ZXN0TmV3UHFjUHJlS2V5cxIoCgVlcnJvchgGIAEoDjIQLmVy' 'JlS2V5cxgIIAEoCEgAUhRSZXF1ZXN0TmV3UHFjUHJlS2V5cxIoCgVlcnJvchgGIAEoDjIQLmVy'
'cm9yLkVycm9yQ29kZUgAUgVlcnJvckIGCgRLaW5k'); 'cm9yLkVycm9yQ29kZUgAUgVlcnJvchIoCg5tYWlsYm94RHJhaW5lZBgLIAEoCEgAUg5tYWlsYm'
'94RHJhaW5lZEIGCgRLaW5k');
@$core.Deprecated('Use newMessageDescriptor instead') @$core.Deprecated('Use newMessageDescriptor instead')
const NewMessage$json = { const NewMessage$json = {

View file

@ -1016,6 +1016,8 @@ class ApiService {
Uint8List kyberSignedPreKey, Uint8List kyberSignedPreKey,
Uint8List kyberSignedPreKeySignature, Uint8List kyberSignedPreKeySignature,
List<client.ApplicationData_PqcPreKey> prekeys, List<client.ApplicationData_PqcPreKey> prekeys,
Uint8List publicIdentityKey,
int registrationId,
) async { ) async {
final get = ApplicationData_UploadPqcPreKeys() final get = ApplicationData_UploadPqcPreKeys()
..eccSignedPrekeyId = Int64(eccSignedPreKeyId) ..eccSignedPrekeyId = Int64(eccSignedPreKeyId)
@ -1024,7 +1026,9 @@ class ApiService {
..kyberSignedPrekeyId = Int64(kyberSignedPreKeyId) ..kyberSignedPrekeyId = Int64(kyberSignedPreKeyId)
..kyberSignedPrekey = kyberSignedPreKey ..kyberSignedPrekey = kyberSignedPreKey
..kyberSignedPrekeySignature = kyberSignedPreKeySignature ..kyberSignedPrekeySignature = kyberSignedPreKeySignature
..prekeys.addAll(prekeys); ..prekeys.addAll(prekeys)
..publicIdentityKey = publicIdentityKey
..registrationId = Int64(registrationId);
final appData = ApplicationData()..uploadPqcPrekeys = get; final appData = ApplicationData()..uploadPqcPrekeys = get;
final req = createClientToServerFromApplicationData(appData); final req = createClientToServerFromApplicationData(appData);
return sendRequestSync(req); return sendRequestSync(req);

View file

@ -18,6 +18,7 @@ import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
import 'package:twonly/src/model/protobuf/client/generated/push_notification.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/push_notification.pb.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/services/signal/encryption.signal.dart'; import 'package:twonly/src/services/signal/encryption.signal.dart';
import 'package:twonly/src/services/signal/protocol_state.signal.dart';
import 'package:twonly/src/services/signal/session.signal.dart'; import 'package:twonly/src/services/signal/session.signal.dart';
import 'package:twonly/src/services/user.service.dart' show UserService; import 'package:twonly/src/services/user.service.dart' show UserService;
import 'package:twonly/src/services/user_discovery.service.dart'; import 'package:twonly/src/services/user_discovery.service.dart';
@ -26,8 +27,47 @@ import 'package:twonly/src/utils/misc.dart';
final lockRetransmission = Mutex(); final lockRetransmission = Mutex();
const int _outOfSyncRetryThreshold = 10;
Future<void> _resetStuckOutgoingSignalSessions() async {
final contactIds = await twonlyDB.receiptsDao
.getContactsWithStuckSignalMessages(
retryThreshold: _outOfSyncRetryThreshold,
);
for (final contactId in contactIds) {
if (!shouldAttemptResync(contactId)) continue;
Log.warn(
'Detected an out-of-sync outgoing Signal session with $contactId: '
'multiple messages exceeded $_outOfSyncRetryThreshold retries.',
);
var reset = false;
try {
reset = await resetSignalSession(contactId);
} catch (error) {
Log.error(
'Could not automatically reset session with $contactId: $error',
);
}
if (!reset) {
recordResyncAttempt(contactId, success: false);
continue;
}
// Avoid resetting the same healthy replacement session on the next
// retransmission pass. Outstanding messages will now be encrypted again
// using the freshly established session.
await twonlyDB.receiptsDao.resetMessageRetryCountsForContact(contactId);
}
}
Future<void> retransmitAllMessages() async { Future<void> retransmitAllMessages() async {
return lockRetransmission.protect(() async { return lockRetransmission.protect(() async {
await _resetStuckOutgoingSignalSessions();
final receipts = await twonlyDB.receiptsDao.getReceiptsForRetransmission(); final receipts = await twonlyDB.receiptsDao.getReceiptsForRetransmission();
if (receipts.isEmpty) return; if (receipts.isEmpty) return;
@ -159,36 +199,24 @@ Future<(Uint8List, Uint8List?)?> _tryToSendCompleteMessageInternal({
.writeToBuffer(); .writeToBuffer();
} }
if (message.type == pb.Message_Type.CIPHERTEXT) { if (message.type == pb.Message_Type.CIPHERTEXT ||
final encryptResult = await signalEncryptMessage( message.type == pb.Message_Type.CIPHERTEXT_V2) {
final useV2 = contact.signalVersion == SignalVersion.v2;
final encryptResult = useV2
? await signalEncryptMessageV2(
receipt.contactId,
Uint8List.fromList(message.encryptedContent),
)
: await signalEncryptMessage(
receipt.contactId, receipt.contactId,
Uint8List.fromList(message.encryptedContent), Uint8List.fromList(message.encryptedContent),
); );
if (encryptResult == null) { if (encryptResult == null) {
Log.error( Log.error(
'[${receipt.receiptId}] Could not encrypt the message for user ${receipt.contactId}. Aborting and trying again.', '[${receipt.receiptId}] Could not encrypt the message '
); '(${useV2 ? 'V2' : 'V1'}) for user ${receipt.contactId}. '
if (receipt.messageId != null) { 'Aborting and trying again.',
await twonlyDB.messagesDao.handleMessageAckByServer(
receipt.contactId,
receipt.messageId!,
clock.now(),
);
}
await twonlyDB.receiptsDao.deleteReceipt(receipt.receiptId);
return null;
}
message
..encryptedContent = encryptResult.ciphertext
..type = encryptResult.type;
} else if (message.type == pb.Message_Type.CIPHERTEXT_V2) {
final encryptResult = await signalEncryptMessageV2(
receipt.contactId,
Uint8List.fromList(message.encryptedContent),
);
if (encryptResult == null) {
Log.error(
'[${receipt.receiptId}] Could not encrypt the message (V2) for user ${receipt.contactId}. Aborting and trying again.',
); );
if (receipt.messageId != null) { if (receipt.messageId != null) {
await twonlyDB.messagesDao.handleMessageAckByServer( await twonlyDB.messagesDao.handleMessageAckByServer(
@ -200,6 +228,7 @@ Future<(Uint8List, Uint8List?)?> _tryToSendCompleteMessageInternal({
await twonlyDB.receiptsDao.deleteReceipt(receipt.receiptId); await twonlyDB.receiptsDao.deleteReceipt(receipt.receiptId);
return null; return null;
} }
message message
..encryptedContent = encryptResult.ciphertext ..encryptedContent = encryptResult.ciphertext
..type = encryptResult.type; ..type = encryptResult.type;

View file

@ -39,8 +39,27 @@ import 'package:twonly/src/services/signal/session.signal.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
const _signalUpgradeMailboxIdleDelay = Duration(milliseconds: 500);
Timer? _signalUpgradeMailboxIdleTimer;
void _scheduleSignalUpgradeAfterMailboxIdle() {
_signalUpgradeMailboxIdleTimer?.cancel();
_signalUpgradeMailboxIdleTimer = Timer(_signalUpgradeMailboxIdleDelay, () {
_signalUpgradeMailboxIdleTimer = null;
unawaited(upgradeSignalSessionsToV2());
});
}
void _upgradeSignalSessionsAfterMailboxDrain() {
_signalUpgradeMailboxIdleTimer?.cancel();
_signalUpgradeMailboxIdleTimer = null;
unawaited(upgradeSignalSessionsToV2());
}
Future<void> handleServerMessage(server.ServerToClient msg) async { Future<void> handleServerMessage(server.ServerToClient msg) async {
Log.info('Processing a message from the server.'); Log.info('Processing a message from the server.');
var receivedIncomingMessages = false;
var mailboxDrained = false;
/// Returns means, that the server can delete the message from the server. /// Returns means, that the server can delete the message from the server.
final ok = client.Response_Ok()..none = true; final ok = client.Response_Ok()..none = true;
@ -54,6 +73,7 @@ Future<void> handleServerMessage(server.ServerToClient msg) async {
} else if (msg.v0.hasNewMessage()) { } else if (msg.v0.hasNewMessage()) {
Log.info('Got 1 message from the server.'); Log.info('Got 1 message from the server.');
await handleClient2ClientMessage(msg.v0.newMessage); await handleClient2ClientMessage(msg.v0.newMessage);
receivedIncomingMessages = true;
} else if (msg.v0.hasNewMessages()) { } else if (msg.v0.hasNewMessages()) {
Log.info( Log.info(
'Got ${msg.v0.newMessages.newMessages.length} messages from the server.', 'Got ${msg.v0.newMessages.newMessages.length} messages from the server.',
@ -69,6 +89,10 @@ Future<void> handleServerMessage(server.ServerToClient msg) async {
Log.error(e); Log.error(e);
} }
} }
receivedIncomingMessages = true;
} else if (msg.v0.hasMailboxDrained()) {
Log.info('All pending messages have been downloaded.');
mailboxDrained = true;
} else { } else {
Log.error('Unknown server message: $msg'); Log.error('Unknown server message: $msg');
} }
@ -90,6 +114,15 @@ Future<void> handleServerMessage(server.ServerToClient msg) async {
} }
AppState.gotMessageFromServer = true; AppState.gotMessageFromServer = true;
Log.info('All messages from the server processed.'); Log.info('All messages from the server processed.');
if (mailboxDrained) {
_upgradeSignalSessionsAfterMailboxDrain();
} else if (receivedIncomingMessages) {
// Released servers do not send a mailbox-drained marker. They wait for
// this ACK before sending the next batch, so a short, resettable idle
// window approximates the queue boundary without blocking delivery.
_scheduleSignalUpgradeAfterMailboxIdle();
}
} }
DateTime lastPushKeyRequest = clock.now().subtract(const Duration(hours: 1)); DateTime lastPushKeyRequest = clock.now().subtract(const Duration(hours: 1));

View file

@ -60,6 +60,8 @@ class SignalIdentityService {
bundle.kyberPreKeyPublic, bundle.kyberPreKeyPublic,
bundle.kyberPreKeySignature, bundle.kyberPreKeySignature,
[], [],
bundle.identityKey,
bundle.registrationId,
); );
if (pqcRes.isError) { if (pqcRes.isError) {

View file

@ -2,6 +2,7 @@ import 'dart:typed_data';
import 'package:drift/drift.dart' as drift; import 'package:drift/drift.dart' as drift;
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:mutex/mutex.dart';
import 'package:twonly/core/bridge/wrapper/signal.dart'; import 'package:twonly/core/bridge/wrapper/signal.dart';
import 'package:twonly/core/signal/engine.dart'; import 'package:twonly/core/signal/engine.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
@ -13,12 +14,83 @@ import 'package:twonly/src/services/signal/protocol_state.signal.dart';
import 'package:twonly/src/services/signal/utils.signal.dart'; import 'package:twonly/src/services/signal/utils.signal.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
final Mutex _upgradeSignalSessionsLock = Mutex();
Future<bool> processSignalUserData(Response_UserData userData) async { Future<bool> processSignalUserData(Response_UserData userData) async {
return lockingSignalProtocol.protect(() async { return lockingSignalProtocol.protect(() async {
return _processSignalUserData(userData); return _processSignalUserData(userData);
}); });
} }
/// Upgrades every known v1 contact that currently publishes a PQXDH bundle.
///
/// The legacy session is deliberately retained: messages that were already in
/// flight before the upgrade can still be decrypted, while newly queued
/// messages use the contact's updated v2 session.
Future<void> upgradeSignalSessionsToV2() async {
if (_upgradeSignalSessionsLock.isLocked) return;
try {
await _upgradeSignalSessionsLock.protect(() async {
final contacts = await twonlyDB.contactsDao.getAllContacts();
final legacyContacts = contacts.where(
(contact) =>
contact.signalVersion == SignalVersion.v1 &&
!contact.accountDeleted,
);
for (final contact in legacyContacts) {
try {
final userData = await apiService.getUserById(contact.userId);
if (userData == null || !userData.hasPqcBundle()) continue;
if (await processSignalUserData(userData)) {
Log.info('Upgraded Signal session with ${contact.userId} to v2.');
}
} catch (error) {
// Migration is best-effort. A contact that is temporarily
// unavailable remains on v1 and is retried after a later message
// queue drain.
Log.warn(
'Could not upgrade Signal session with ${contact.userId}: $error',
);
}
}
});
} catch (error) {
// This function is started without awaiting it from the message loop.
Log.warn('Could not start Signal session upgrades: $error');
}
}
/// Replaces the current session with a freshly downloaded pre-key bundle.
Future<bool> resetSignalSession(int contactId) async {
final userData = await apiService.getUserById(contactId);
if (userData == null) return false;
return lockingSignalProtocol.protect(() async {
if (userData.hasPqcBundle()) {
await RustSignal.resetContactSession(contactId: contactId);
} else {
final signalStore = await getSignalStore();
await signalStore?.deleteAllSessions(contactId.toString());
}
final reset = await _processSignalUserData(userData);
if (reset) recordResyncAttempt(contactId, success: true);
return reset;
});
}
ECPublicKey _decodePublicKey(List<int> bytes, String fieldName) {
if (bytes.length != 33 || bytes.first != Curve.djbType) {
throw InvalidKeyException(
'$fieldName must be a serialized 33-byte Curve25519 public key '
'(received ${bytes.length} bytes)',
);
}
return Curve.decodePoint(Uint8List.fromList(bytes), 0);
}
Future<bool> _processSignalUserData(Response_UserData userData) async { Future<bool> _processSignalUserData(Response_UserData userData) async {
if (userData.hasPqcBundle()) { if (userData.hasPqcBundle()) {
return _processSignalUserDataV2(userData); return _processSignalUserDataV2(userData);
@ -29,17 +101,20 @@ Future<bool> _processSignalUserData(Response_UserData userData) async {
Future<bool> _processSignalUserDataV2(Response_UserData userData) async { Future<bool> _processSignalUserDataV2(Response_UserData userData) async {
try { try {
final tempIdentityKey = IdentityKey( final tempIdentityKey = IdentityKey(
Curve.decodePoint( _decodePublicKey(userData.publicIdentityKey, 'identity key'),
DjbECPublicKey(
Uint8List.fromList(userData.publicIdentityKey),
).serialize(),
1,
),
); );
final contact = await twonlyDB.contactsDao.getContactById(
userData.userId.toInt(),
);
// Only the one-time V1 -> V2 migration needs to compare against the
// legacy Dart identity store. Established V2 contacts are authenticated
// by the identity retained in the Rust store; consulting the obsolete
// Dart store here can make a valid V2 reset fail on legacy/corrupt data.
if (contact?.signalVersion != SignalVersion.v2) {
final signalStore = await getSignalStore(); final signalStore = await getSignalStore();
if (signalStore != null) { final existingIdentity = await signalStore?.getIdentity(
final existingIdentity = await signalStore.getIdentity(
SignalProtocolAddress(userData.userId.toString(), defaultDeviceId), SignalProtocolAddress(userData.userId.toString(), defaultDeviceId),
); );
@ -60,11 +135,9 @@ Future<bool> _processSignalUserDataV2(Response_UserData userData) async {
); );
} else if (userData.prekeys.isNotEmpty) { } else if (userData.prekeys.isNotEmpty) {
tempEccPreKeyId = userData.prekeys.first.id.toInt(); tempEccPreKeyId = userData.prekeys.first.id.toInt();
tempEccPreKeyPublic = Curve.decodePoint( tempEccPreKeyPublic = _decodePublicKey(
DjbECPublicKey( userData.prekeys.first.prekey,
Uint8List.fromList(userData.prekeys.first.prekey), 'prekey',
).serialize(),
1,
).serialize(); ).serialize();
} }
@ -102,9 +175,6 @@ Future<bool> _processSignalUserDataV2(Response_UserData userData) async {
bundle: rustBundle, bundle: rustBundle,
); );
final contact = await twonlyDB.contactsDao.getContactById(
userData.userId.toInt(),
);
if (contact != null && contact.signalVersion != SignalVersion.v2) { if (contact != null && contact.signalVersion != SignalVersion.v2) {
await twonlyDB.contactsDao.updateContact( await twonlyDB.contactsDao.updateContact(
contact.userId, contact.userId,
@ -120,6 +190,7 @@ Future<bool> _processSignalUserDataV2(Response_UserData userData) async {
} }
Future<bool> _processSignalUserDataV1(Response_UserData userData) async { Future<bool> _processSignalUserDataV1(Response_UserData userData) async {
try {
final SignalProtocolStore? signalStore = await getSignalStore(); final SignalProtocolStore? signalStore = await getSignalStore();
if (signalStore == null) { if (signalStore == null) {
@ -137,20 +208,18 @@ Future<bool> _processSignalUserDataV1(Response_UserData userData) async {
int? tempPreKeyId; int? tempPreKeyId;
if (userData.prekeys.isNotEmpty) { if (userData.prekeys.isNotEmpty) {
tempPrePublicKey = Curve.decodePoint( tempPrePublicKey = _decodePublicKey(
DjbECPublicKey( userData.prekeys.first.prekey,
Uint8List.fromList(userData.prekeys.first.prekey), 'prekey',
).serialize(),
1,
); );
tempPreKeyId = userData.prekeys.first.id.toInt(); tempPreKeyId = userData.prekeys.first.id.toInt();
} }
final tempSignedPreKeyId = userData.signedPrekeyId.toInt(); final tempSignedPreKeyId = userData.signedPrekeyId.toInt();
final tempSignedPreKeyPublic = Curve.decodePoint( final tempSignedPreKeyPublic = _decodePublicKey(
DjbECPublicKey(Uint8List.fromList(userData.signedPrekey)).serialize(), userData.signedPrekey,
1, 'signed prekey',
); );
final tempSignedPreKeySignature = Uint8List.fromList( final tempSignedPreKeySignature = Uint8List.fromList(
@ -158,12 +227,7 @@ Future<bool> _processSignalUserDataV1(Response_UserData userData) async {
); );
final tempIdentityKey = IdentityKey( final tempIdentityKey = IdentityKey(
Curve.decodePoint( _decodePublicKey(userData.publicIdentityKey, 'identity key'),
DjbECPublicKey(
Uint8List.fromList(userData.publicIdentityKey),
).serialize(),
1,
),
); );
final preKeyBundle = PreKeyBundle( final preKeyBundle = PreKeyBundle(
@ -177,7 +241,6 @@ Future<bool> _processSignalUserDataV1(Response_UserData userData) async {
tempIdentityKey, tempIdentityKey,
); );
try {
await sessionBuilder.processPreKeyBundle(preKeyBundle); await sessionBuilder.processPreKeyBundle(preKeyBundle);
return true; return true;
} catch (e) { } catch (e) {

View file

@ -127,8 +127,10 @@ class MainCameraController {
try { try {
await cameraControllerTemp?.stopImageStream(); await cameraControllerTemp?.stopImageStream();
// ignore: empty_catches // ignore: empty_catches
} catch (e) {} } catch (e) {
} finally {
await cameraControllerTemp?.dispose(); await cameraControllerTemp?.dispose();
}
}, },
); );
initCameraStarted = false; initCameraStarted = false;

View file

@ -10,6 +10,7 @@ import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/signal/session.signal.dart'; import 'package:twonly/src/services/signal/session.signal.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/alert.dialog.dart'; import 'package:twonly/src/visual/components/alert.dialog.dart';
import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
@ -38,6 +39,7 @@ class ContactView extends StatefulWidget {
class _ContactViewState extends State<ContactView> { class _ContactViewState extends State<ContactView> {
Contact? _contact; Contact? _contact;
List<GroupMember> _memberOfGroups = []; List<GroupMember> _memberOfGroups = [];
bool _isResettingSignalSession = false;
late StreamSubscription<Contact?> _streamContact; late StreamSubscription<Contact?> _streamContact;
late StreamSubscription<List<GroupMember>> _streamMemberOfGroups; late StreamSubscription<List<GroupMember>> _streamMemberOfGroups;
@ -136,6 +138,37 @@ class _ContactViewState extends State<ContactView> {
} }
} }
Future<void> handleResetSignalSession(Contact contact) async {
if (_isResettingSignalSession) return;
final displayName = getContactDisplayName(contact);
final confirmed = await showAlertDialog(
context,
context.lang.contactResetSessionTitle(displayName),
context.lang.contactResetSessionBody,
customOk: context.lang.contactResetSessionConfirm,
);
if (!confirmed || !mounted) return;
setState(() => _isResettingSignalSession = true);
var success = false;
try {
success = await resetSignalSession(contact.userId);
} catch (error) {
Log.error('Could not reset Signal session: $error');
}
if (!mounted) return;
setState(() => _isResettingSignalSession = false);
showSnackbar(
context,
success
? context.lang.contactResetSessionSuccess(displayName)
: context.lang.contactResetSessionFailed,
level: success ? SnackbarLevel.success : SnackbarLevel.error,
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_contact == null) return Container(); if (_contact == null) return Container();
@ -268,6 +301,19 @@ class _ContactViewState extends State<ContactView> {
text: context.lang.contactBlock, text: context.lang.contactBlock,
onTap: () => handleUserBlockRequest(contact), onTap: () => handleUserBlockRequest(contact),
), ),
BetterListTile(
icon: FontAwesomeIcons.screwdriverWrench,
text: context.lang.contactTroubleshoot,
trailing: _isResettingSignalSession
? const SizedBox.square(
dimension: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: null,
onTap: _isResettingSignalSession
? null
: () => handleResetSignalSession(contact),
),
BetterListTile( BetterListTile(
icon: FontAwesomeIcons.userMinus, icon: FontAwesomeIcons.userMinus,
iconSize: 16, iconSize: 16,

View file

@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec
publish_to: 'none' publish_to: 'none'
version: 0.5.2+172 version: 0.5.3+173
environment: environment:
sdk: ^3.11.0 sdk: ^3.11.0

View file

@ -13,6 +13,65 @@ use zip::{CompressionMethod, ZipArchive, ZipWriter};
pub(crate) struct BackupArchive {} pub(crate) struct BackupArchive {}
impl BackupArchive { impl BackupArchive {
async fn reset_restored_signal_sessions(
file_name: &str,
database_path: &Path,
encryption_key: Option<&str>,
) -> Result<()> {
let table = match file_name {
"twonly.sqlite" => "signal_session_stores",
"rust_db.sqlite" => "signal_sessions",
_ => return Ok(()),
};
let database =
Database::new(&database_path.display().to_string(), encryption_key, false).await?;
let table_exists: bool = sqlx::query_scalar(
"SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?)",
)
.bind(table)
.fetch_one(&database.pool)
.await?;
if table_exists {
match file_name {
"twonly.sqlite" => {
sqlx::query("DELETE FROM signal_session_stores")
.execute(&database.pool)
.await?;
}
"rust_db.sqlite" => {
sqlx::query("DELETE FROM signal_sessions")
.execute(&database.pool)
.await?;
}
_ => unreachable!(),
}
}
// The hotfix chooses the encryption implementation from this column.
// A restored v2 marker without its restored session would make the
// first outgoing message fail before the normal migration can rebuild
// it. Mark contacts as v1; the post-download migration upgrades every
// peer that currently publishes a PQXDH bundle.
if file_name == "twonly.sqlite" {
let signal_version_exists: bool = sqlx::query_scalar(
"SELECT EXISTS(SELECT 1 FROM pragma_table_info('contacts') WHERE name = 'signal_version')",
)
.fetch_one(&database.pool)
.await?;
if signal_version_exists {
sqlx::query("UPDATE contacts SET signal_version = 'v1'")
.execute(&database.pool)
.await?;
}
}
database.pool.close().await;
Ok(())
}
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
fn get_backup_files( fn get_backup_files(
ctx: &Context, ctx: &Context,
@ -147,9 +206,19 @@ impl BackupArchive {
} }
} }
for (file_name, target_dir, is_db, _) in Self::get_backup_files(ctx, &key_manager)? { for (file_name, target_dir, is_db, mut encryption_key) in
Self::get_backup_files(ctx, &key_manager)?
{
let src = restore_temp_dir.join(file_name); let src = restore_temp_dir.join(file_name);
if src.exists() { if src.exists() {
if is_db {
Self::reset_restored_signal_sessions(
file_name,
&src,
encryption_key.as_deref(),
)
.await?;
}
std::fs::create_dir_all(&target_dir)?; std::fs::create_dir_all(&target_dir)?;
let dst = target_dir.join(file_name); let dst = target_dir.join(file_name);
if is_db { if is_db {
@ -161,6 +230,7 @@ impl BackupArchive {
std::fs::copy(src, dst)?; std::fs::copy(src, dst)?;
} }
encryption_key.zeroize();
} }
std::fs::remove_dir_all(&restore_temp_dir)?; std::fs::remove_dir_all(&restore_temp_dir)?;
@ -210,6 +280,18 @@ mod tests {
ReceivedMessage::insert(&db.pool, 1, b"original message") ReceivedMessage::insert(&db.pool, 1, b"original message")
.await .await
.unwrap(); .unwrap();
sqlx::query(
"INSERT INTO signal_identities(name, identity_key, timestamp) VALUES('restored-peer', x'040506', 1)",
)
.execute(&db.pool)
.await
.unwrap();
sqlx::query(
"INSERT INTO signal_sessions(name, device_id, record_bytes) VALUES('restored-peer', 1, x'010203')",
)
.execute(&db.pool)
.await
.unwrap();
// Add a file // Add a file
let config_file = PathBuf::from(&config.data_dir).join("user_discovery_config.json"); let config_file = PathBuf::from(&config.data_dir).join("user_discovery_config.json");
@ -217,6 +299,39 @@ mod tests {
key_manager.main_key.get_login_token() key_manager.main_key.get_login_token()
}; };
// The hotfix still carries the legacy Dart Signal store alongside the
// Rust PQXDH store. Both session stores are part of an archive.
{
let config = ctx.get_config().unwrap();
let app_db_path = PathBuf::from(&config.database_dir).join("twonly.sqlite");
let app_db = Database::new(&app_db_path.display().to_string(), None, false)
.await
.unwrap();
sqlx::query(
"CREATE TABLE contacts(user_id INTEGER PRIMARY KEY, signal_version TEXT NOT NULL)",
)
.execute(&app_db.pool)
.await
.unwrap();
sqlx::query(
"CREATE TABLE signal_session_stores(device_id INTEGER NOT NULL, name TEXT NOT NULL, session_record BLOB NOT NULL, PRIMARY KEY(device_id, name))",
)
.execute(&app_db.pool)
.await
.unwrap();
sqlx::query("INSERT INTO contacts(user_id, signal_version) VALUES(7, 'v2')")
.execute(&app_db.pool)
.await
.unwrap();
sqlx::query(
"INSERT INTO signal_session_stores(device_id, name, session_record) VALUES(1, '7', x'070809')",
)
.execute(&app_db.pool)
.await
.unwrap();
app_db.pool.close().await;
}
// 2. Create backup // 2. Create backup
let backup_path = BackupArchive::create_backup(&ctx).await.unwrap(); let backup_path = BackupArchive::create_backup(&ctx).await.unwrap();
assert!(backup_path.exists()); assert!(backup_path.exists());
@ -266,6 +381,44 @@ mod tests {
assert_eq!(messages[0].sender_id, 1); assert_eq!(messages[0].sender_id, 1);
assert_eq!(messages[0].content, b"original message"); assert_eq!(messages[0].content, b"original message");
let session_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM signal_sessions")
.fetch_one(&db.pool)
.await
.unwrap();
assert_eq!(session_count, 0, "restored PQXDH sessions must be reset");
let identity_key: Vec<u8> = sqlx::query_scalar(
"SELECT identity_key FROM signal_identities WHERE name = 'restored-peer'",
)
.fetch_one(&db.pool)
.await
.unwrap();
assert_eq!(
identity_key,
vec![4, 5, 6],
"trusted contact identities must survive session reset"
);
let app_db_path = PathBuf::from(&config.database_dir).join("twonly.sqlite");
let app_db = Database::new(&app_db_path.display().to_string(), None, false)
.await
.unwrap();
let legacy_session_count: i64 =
sqlx::query_scalar("SELECT COUNT(*) FROM signal_session_stores")
.fetch_one(&app_db.pool)
.await
.unwrap();
assert_eq!(
legacy_session_count, 0,
"restored legacy Signal sessions must be reset"
);
let signal_version: String =
sqlx::query_scalar("SELECT signal_version FROM contacts WHERE user_id = 7")
.fetch_one(&app_db.pool)
.await
.unwrap();
assert_eq!(signal_version, "v1");
let config_file = PathBuf::from(&config.data_dir).join("user_discovery_config.json"); let config_file = PathBuf::from(&config.data_dir).join("user_discovery_config.json");
let config_content = std::fs::read_to_string(config_file).unwrap(); let config_content = std::fs::read_to_string(config_file).unwrap();
assert_eq!(config_content, "original config"); assert_eq!(config_content, "original config");

View file

@ -29,6 +29,17 @@ impl RustSignal {
engine.process_prekey_bundle(name, device_id, bundle).await engine.process_prekey_bundle(name, device_id, bundle).await
} }
/// Removes the current v2 session for a contact. The Dart hotfix layer
/// immediately establishes its replacement from a freshly fetched bundle.
pub async fn reset_contact_session(contact_id: i64) -> Result<()> {
let guard = get_twonly_flutter()?.signal_engine.lock().await;
let engine = guard.as_ref().ok_or(TwonlyError::Initialization)?;
engine
.reset_session(&contact_id.to_string(), 1, true)
.await?;
Ok(())
}
pub async fn encrypt(name: String, device_id: u32, plaintext: Vec<u8>) -> Result<Vec<u8>> { pub async fn encrypt(name: String, device_id: u32, plaintext: Vec<u8>) -> Result<Vec<u8>> {
let guard = get_twonly_flutter()?.signal_engine.lock().await; let guard = get_twonly_flutter()?.signal_engine.lock().await;
let engine = guard.as_ref().ok_or(TwonlyError::Initialization)?; let engine = guard.as_ref().ok_or(TwonlyError::Initialization)?;

View file

@ -40,7 +40,7 @@ flutter_rust_bridge::frb_generated_boilerplate!(
default_rust_auto_opaque = RustAutoOpaqueMoi, default_rust_auto_opaque = RustAutoOpaqueMoi,
); );
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0"; pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0";
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 1109927244; pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -2042422055;
// Section: executor // Section: executor
@ -915,6 +915,46 @@ fn wire__crate__bridge__wrapper__signal__rust_signal_process_prekey_bundle_impl(
}, },
) )
} }
fn wire__crate__bridge__wrapper__signal__rust_signal_reset_contact_session_impl(
port_: flutter_rust_bridge::for_generated::MessagePort,
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
rust_vec_len_: i32,
data_len_: i32,
) {
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec, _, _, _>(
flutter_rust_bridge::for_generated::TaskInfo {
debug_name: "rust_signal_reset_contact_session",
port: Some(port_),
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
},
move || {
let message = unsafe {
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
ptr_,
rust_vec_len_,
data_len_,
)
};
let mut deserializer =
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
let api_contact_id = <i64>::sse_decode(&mut deserializer);
deserializer.end();
move |context| async move {
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>(
(move || async move {
let output_ok =
crate::bridge::wrapper::signal::RustSignal::reset_contact_session(
api_contact_id,
)
.await?;
Ok(output_ok)
})()
.await,
)
}
},
)
}
fn wire__crate__bridge__wrapper__rust_utils_generate_shares_impl( fn wire__crate__bridge__wrapper__rust_utils_generate_shares_impl(
port_: flutter_rust_bridge::for_generated::MessagePort, port_: flutter_rust_bridge::for_generated::MessagePort,
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
@ -2201,24 +2241,25 @@ fn pde_ffi_dispatcher_primary_impl(
34 => wire__crate__bridge__wrapper__signal__rust_signal_generate_bundle_impl(port, ptr, rust_vec_len, data_len), 34 => wire__crate__bridge__wrapper__signal__rust_signal_generate_bundle_impl(port, ptr, rust_vec_len, data_len),
35 => wire__crate__bridge__wrapper__signal__rust_signal_generate_pqc_prekeys_impl(port, ptr, rust_vec_len, data_len), 35 => wire__crate__bridge__wrapper__signal__rust_signal_generate_pqc_prekeys_impl(port, ptr, rust_vec_len, data_len),
36 => wire__crate__bridge__wrapper__signal__rust_signal_process_prekey_bundle_impl(port, ptr, rust_vec_len, data_len), 36 => wire__crate__bridge__wrapper__signal__rust_signal_process_prekey_bundle_impl(port, ptr, rust_vec_len, data_len),
37 => wire__crate__bridge__wrapper__rust_utils_generate_shares_impl(port, ptr, rust_vec_len, data_len), 37 => wire__crate__bridge__wrapper__signal__rust_signal_reset_contact_session_impl(port, ptr, rust_vec_len, data_len),
38 => wire__crate__bridge__wrapper__rust_utils_recover_secret_impl(port, ptr, rust_vec_len, data_len), 38 => wire__crate__bridge__wrapper__rust_utils_generate_shares_impl(port, ptr, rust_vec_len, data_len),
39 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_announced_user_by_public_id_impl(port, ptr, rust_vec_len, data_len), 39 => wire__crate__bridge__wrapper__rust_utils_recover_secret_impl(port, ptr, rust_vec_len, data_len),
40 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_config_impl(port, ptr, rust_vec_len, data_len), 40 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_announced_user_by_public_id_impl(port, ptr, rust_vec_len, data_len),
41 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_contact_promotion_impl(port, ptr, rust_vec_len, data_len), 41 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_config_impl(port, ptr, rust_vec_len, data_len),
42 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_contact_version_impl(port, ptr, rust_vec_len, data_len), 42 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_contact_promotion_impl(port, ptr, rust_vec_len, data_len),
43 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_other_promotions_by_public_id_impl(port, ptr, rust_vec_len, data_len), 43 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_contact_version_impl(port, ptr, rust_vec_len, data_len),
44 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_own_promotions_after_version_impl(port, ptr, rust_vec_len, data_len), 44 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_other_promotions_by_public_id_impl(port, ptr, rust_vec_len, data_len),
45 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_share_for_contact_impl(port, ptr, rust_vec_len, data_len), 45 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_own_promotions_after_version_impl(port, ptr, rust_vec_len, data_len),
46 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_push_new_user_relation_impl(port, ptr, rust_vec_len, data_len), 46 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_share_for_contact_impl(port, ptr, rust_vec_len, data_len),
47 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_push_own_promotion_and_clear_old_version_impl(port, ptr, rust_vec_len, data_len), 47 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_push_new_user_relation_impl(port, ptr, rust_vec_len, data_len),
48 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_set_contact_version_impl(port, ptr, rust_vec_len, data_len), 48 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_push_own_promotion_and_clear_old_version_impl(port, ptr, rust_vec_len, data_len),
49 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_set_shares_impl(port, ptr, rust_vec_len, data_len), 49 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_set_contact_version_impl(port, ptr, rust_vec_len, data_len),
50 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_store_other_promotion_impl(port, ptr, rust_vec_len, data_len), 50 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_set_shares_impl(port, ptr, rust_vec_len, data_len),
51 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_update_config_impl(port, ptr, rust_vec_len, data_len), 51 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_store_other_promotion_impl(port, ptr, rust_vec_len, data_len),
52 => wire__crate__bridge__callbacks__user_discovery__user_discovery_utils_flutter_sign_data_impl(port, ptr, rust_vec_len, data_len), 52 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_update_config_impl(port, ptr, rust_vec_len, data_len),
53 => wire__crate__bridge__callbacks__user_discovery__user_discovery_utils_flutter_verify_signature_impl(port, ptr, rust_vec_len, data_len), 53 => wire__crate__bridge__callbacks__user_discovery__user_discovery_utils_flutter_sign_data_impl(port, ptr, rust_vec_len, data_len),
54 => wire__crate__bridge__callbacks__user_discovery__user_discovery_utils_flutter_verify_stored_pubkey_impl(port, ptr, rust_vec_len, data_len), 54 => wire__crate__bridge__callbacks__user_discovery__user_discovery_utils_flutter_verify_signature_impl(port, ptr, rust_vec_len, data_len),
55 => wire__crate__bridge__callbacks__user_discovery__user_discovery_utils_flutter_verify_stored_pubkey_impl(port, ptr, rust_vec_len, data_len),
_ => unreachable!(), _ => unreachable!(),
} }
} }

View file

@ -2,8 +2,8 @@ use crate::error::{Result, TwonlyError};
use libsignal_protocol::{ use libsignal_protocol::{
message_encrypt, process_prekey_bundle, CiphertextMessageType, DeviceId, GenericSignedPreKey, message_encrypt, process_prekey_bundle, CiphertextMessageType, DeviceId, GenericSignedPreKey,
IdentityKey, IdentityKeyPair, IdentityKeyStore, KyberPreKeyId, KyberPreKeyStore, PreKeyBundle, IdentityKey, IdentityKeyPair, IdentityKeyStore, KyberPreKeyId, KyberPreKeyStore, PreKeyBundle,
PreKeyId, PreKeySignalMessage, PreKeyStore, ProtocolAddress, PublicKey, SignalMessage, PreKeyId, PreKeySignalMessage, PreKeyStore, ProtocolAddress, PublicKey, SessionStore,
SignedPreKeyId, SignedPreKeyStore, Timestamp, SignalMessage, SignedPreKeyId, SignedPreKeyStore, Timestamp,
}; };
use std::sync::Arc; use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
@ -414,6 +414,58 @@ impl RustSignalEngine {
Ok(()) Ok(())
} }
/// Retires the session with a peer so a fresh one can replace it.
///
/// A hard reset deletes the complete record, including archived states.
/// A soft reset archives only the current state so in-flight messages can
/// still be decrypted while a replacement session is established.
///
/// Returns whether a session existed.
pub async fn reset_session(&self, name: &str, device_id: u32, hard: bool) -> Result<bool> {
let mut store_guard = self.store.lock().await;
let store = &mut *store_guard;
let d_id = DeviceId::try_from(device_id)
.map_err(|_| TwonlyError::Generic(format!("Invalid device id: {}", device_id)))?;
let address = ProtocolAddress::new(name.to_owned(), d_id);
if hard {
let deleted =
sqlx::query("DELETE FROM signal_sessions WHERE name = ? AND device_id = ?")
.bind(name)
.bind(device_id)
.execute(&store.pool)
.await?
.rows_affected();
tracing::warn!(name, device_id, "deleted the signal session");
return Ok(deleted != 0);
}
let Some(mut record) = store
.session_store
.load_session(&address)
.assert_send()
.await
.map_err(|e| TwonlyError::Signal(e.to_string()))?
else {
return Ok(false);
};
record
.archive_current_state()
.map_err(|e| TwonlyError::Signal(e.to_string()))?;
store
.session_store
.store_session(&address, &record)
.assert_send()
.await
.map_err(|e| TwonlyError::Signal(e.to_string()))?;
tracing::warn!(name, device_id, "archived the signal session for a reset");
Ok(true)
}
pub async fn encrypt_message( pub async fn encrypt_message(
&self, &self,
name: String, name: String,
@ -618,4 +670,40 @@ mod tests {
assert_eq!(plaintext.to_vec(), decrypted); assert_eq!(plaintext.to_vec(), decrypted);
} }
#[tokio::test]
async fn hard_reset_deletes_and_allows_rebuilding_a_contact_session() {
let (alice_engine, _alice_dir) = create_test_engine("alice").await;
let (bob_engine, _bob_dir) = create_test_engine("bob").await;
alice_engine
.process_prekey_bundle(
"bob".to_string(),
1,
bob_engine.generate_bundle().await.unwrap(),
)
.await
.unwrap();
assert!(alice_engine.reset_session("bob", 1, true).await.unwrap());
assert!(!alice_engine.reset_session("bob", 1, true).await.unwrap());
assert!(alice_engine
.encrypt_message("bob".to_string(), 1, b"missing".to_vec())
.await
.is_err());
alice_engine
.process_prekey_bundle(
"bob".to_string(),
1,
bob_engine.generate_bundle().await.unwrap(),
)
.await
.unwrap();
assert!(alice_engine
.encrypt_message("bob".to_string(), 1, b"rebuilt".to_vec())
.await
.is_ok());
}
} }

View file

@ -0,0 +1,13 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart';
void main() {
test('decodes the server mailbox-drained signal', () {
// ServerToClient.V0 (field 1) containing V0.mailboxDrained (field 11).
final message = ServerToClient.fromBuffer([0x0a, 0x02, 0x58, 0x01]);
expect(message.v0.hasMailboxDrained(), isTrue);
expect(message.v0.mailboxDrained, isTrue);
expect(message.v0.whichKind(), V0_Kind.mailboxDrained);
});
}

View file

@ -0,0 +1,27 @@
import 'package:fixnum/fixnum.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pb.dart';
void main() {
test('PQC upload preserves migration identity fields on the wire', () {
final request = ApplicationData_UploadPqcPreKeys(
eccSignedPrekeyId: Int64(11),
eccSignedPrekey: [5, 6],
eccSignedPrekeySignature: [7, 8],
kyberSignedPrekeyId: Int64(12),
kyberSignedPrekey: [9, 10],
kyberSignedPrekeySignature: [11, 12],
publicIdentityKey: List<int>.filled(33, 1),
registrationId: Int64(42),
);
final decoded = ApplicationData_UploadPqcPreKeys.fromBuffer(
request.writeToBuffer(),
);
expect(decoded.publicIdentityKey, hasLength(33));
expect(decoded.registrationId, Int64(42));
expect(decoded.hasPublicIdentityKey(), isTrue);
expect(decoded.hasRegistrationId(), isTrue);
});
}

View file

@ -0,0 +1,86 @@
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:twonly/src/database/twonly.db.dart';
void main() {
late TwonlyDB db;
setUp(() async {
db = TwonlyDB.forTesting(
DatabaseConnection(
NativeDatabase.memory(),
closeStreamsSynchronously: true,
),
);
for (final contactId in [10, 11, 12]) {
await db.contactsDao.insertContact(
ContactsCompanion.insert(
userId: Value(contactId),
username: 'contact-$contactId',
),
);
}
});
tearDown(() => db.close());
Future<void> addReceipt(
int contactId,
int retryCount, {
bool mediaUpload = false,
}) async {
await db.receiptsDao.insertReceipt(
ReceiptsCompanion(
contactId: Value(contactId),
message: Value(Uint8List.fromList([contactId])),
retryCount: Value(retryCount),
willBeRetriedByMediaUpload: Value(mediaUpload),
),
);
}
test(
'detects multiple message retries above the ratchet threshold',
() async {
await addReceipt(10, 21);
await addReceipt(10, 30);
await addReceipt(11, 21);
await addReceipt(11, 20);
await addReceipt(12, 25, mediaUpload: true);
await addReceipt(12, 25, mediaUpload: true);
final contactIds = await db.receiptsDao
.getContactsWithStuckSignalMessages(
retryThreshold: 20,
);
expect(contactIds, [10]);
},
);
test('clears message counters after a successful session reset', () async {
await addReceipt(10, 21);
await addReceipt(10, 30);
await addReceipt(10, 25, mediaUpload: true);
await db.receiptsDao.resetMessageRetryCountsForContact(10);
final receipts = await (db.select(
db.receipts,
)..where((receipt) => receipt.contactId.equals(10))).get();
final messageReceipts = receipts.where(
(receipt) => !receipt.willBeRetriedByMediaUpload,
);
final mediaReceipt = receipts.singleWhere(
(receipt) => receipt.willBeRetriedByMediaUpload,
);
expect(
messageReceipts.map((receipt) => receipt.retryCount),
everyElement(0),
);
expect(mediaReceipt.retryCount, 25);
});
}