diff --git a/lib/core/bridge/webxdc.dart b/lib/core/bridge/webxdc.dart index 6ca06dd5..eb601b38 100644 --- a/lib/core/bridge/webxdc.dart +++ b/lib/core/bridge/webxdc.dart @@ -12,11 +12,25 @@ Future refreshCatalog() => /// What the in-app store offers, newest version of each app only. /// -/// `languages` is what the UI prefers, most preferred first. Descriptions are -/// cached in every language the catalog carries and picked here, so which -/// language a user reads is never sent anywhere. -Future> catalog({required List languages}) => - RustLib.instance.api.crateBridgeWebxdcCatalog(languages: languages); +/// `languages` is what the UI prefers, most preferred first. Names and +/// descriptions are cached in every language the catalog carries and picked +/// here, so which language a user reads is never sent anywhere. +Future> catalog({ + required String groupId, + required List languages, +}) => RustLib.instance.api.crateBridgeWebxdcCatalog( + groupId: groupId, + languages: languages, +); + +/// One-time apps already placed into a chat, for the profile shortcut. +Future> oneTimeInstances({ + required String groupId, + required List languages, +}) => RustLib.instance.api.crateBridgeWebxdcOneTimeInstances( + groupId: groupId, + languages: languages, +); /// Places an app into a chat and returns the id of the message that carries it. Future createInstance({ @@ -32,12 +46,17 @@ Future createInstance({ Future instance({required String instanceId}) => RustLib.instance.api.crateBridgeWebxdcInstance(instanceId: instanceId); -/// Downloads and verifies the bundle if it is not already on disk, and returns -/// where it landed. Called when the user starts an app, never on message +/// Opens cached code, adopting an update only if already downloaded. +/// Downloads and verifies the pinned bundle only when it is missing. Called when the user starts an app, never on message /// arrival: a message must not be able to make a device fetch anything. Future prepareBundle({required String instanceId}) => RustLib.instance.api.crateBridgeWebxdcPrepareBundle(instanceId: instanceId); +/// Background update check after a webxdc app closes. Downloads only; the +/// running version changes on a later prepare_bundle call, never mid-session. +Future cacheUpdate({required String instanceId}) => + RustLib.instance.api.crateBridgeWebxdcCacheUpdate(instanceId: instanceId); + /// Answers one request the webview made for a file inside the bundle. /// /// The platform shim has already established that the request came from this @@ -65,6 +84,7 @@ Future sendUpdate({ String? href, String? summary, String? document, + String? notify, }) => RustLib.instance.api.crateBridgeWebxdcSendUpdate( instanceId: instanceId, payload: payload, @@ -72,6 +92,7 @@ Future sendUpdate({ href: href, summary: summary, document: document, + notify: notify, ); /// The address the running app sees for a participant. Stable inside one @@ -93,6 +114,10 @@ Future deleteInstance({required String instanceId}) => RustLib .api .crateBridgeWebxdcDeleteInstance(instanceId: instanceId); +/// Current members of the instance's chat; no account identifiers leave Rust. +Future members({required String instanceId}) => + RustLib.instance.api.crateBridgeWebxdcMembers(instanceId: instanceId); + class WebxdcInstanceInfo { final String instanceId; final String groupId; @@ -140,6 +165,41 @@ class WebxdcInstanceInfo { document == other.document; } +class WebxdcOneTimeInstance { + final String instanceId; + final String name; + final Uint8List? icon; + final String? summary; + final String? document; + + const WebxdcOneTimeInstance({ + required this.instanceId, + required this.name, + this.icon, + this.summary, + this.document, + }); + + @override + int get hashCode => + instanceId.hashCode ^ + name.hashCode ^ + icon.hashCode ^ + summary.hashCode ^ + document.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is WebxdcOneTimeInstance && + runtimeType == other.runtimeType && + instanceId == other.instanceId && + name == other.name && + icon == other.icon && + summary == other.summary && + document == other.document; +} + class WebxdcResponse { final int status; final String mime; @@ -187,6 +247,11 @@ class WebxdcStoreApp { final String? sourceCodeUrl; final Uint8List? icon; final PlatformInt64 bundleBytes; + final bool proOnly; + final bool oneTime; + + /// The existing instance in this chat when a one-time app was already placed. + final String? instanceId; const WebxdcStoreApp({ required this.appId, @@ -196,6 +261,9 @@ class WebxdcStoreApp { this.sourceCodeUrl, this.icon, required this.bundleBytes, + required this.proOnly, + required this.oneTime, + this.instanceId, }); @override @@ -206,7 +274,10 @@ class WebxdcStoreApp { description.hashCode ^ sourceCodeUrl.hashCode ^ icon.hashCode ^ - bundleBytes.hashCode; + bundleBytes.hashCode ^ + proOnly.hashCode ^ + oneTime.hashCode ^ + instanceId.hashCode; @override bool operator ==(Object other) => @@ -219,7 +290,10 @@ class WebxdcStoreApp { description == other.description && sourceCodeUrl == other.sourceCodeUrl && icon == other.icon && - bundleBytes == other.bundleBytes; + bundleBytes == other.bundleBytes && + proOnly == other.proOnly && + oneTime == other.oneTime && + instanceId == other.instanceId; } class WebxdcUpdateEntry { diff --git a/lib/core/frb_generated.dart b/lib/core/frb_generated.dart index fbfb801c..315bd0c8 100644 --- a/lib/core/frb_generated.dart +++ b/lib/core/frb_generated.dart @@ -87,7 +87,7 @@ class RustLib extends BaseEntrypoint { String get codegenVersion => '2.12.0'; @override - int get rustContentHash => 1362207365; + int get rustContentHash => -1591152700; static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig( @@ -113,7 +113,10 @@ abstract class RustLibApi extends BaseApi { required PlatformInt64 userId, }); + Future crateBridgeWebxdcCacheUpdate({required String instanceId}); + Future> crateBridgeWebxdcCatalog({ + required String groupId, required List languages, }); @@ -188,6 +191,13 @@ abstract class RustLibApi extends BaseApi { required bool remove, }); + Future crateBridgeWebxdcMembers({required String instanceId}); + + Future> crateBridgeWebxdcOneTimeInstances({ + required String groupId, + required List languages, + }); + Future crateBridgeWebxdcPrepareBundle({required String instanceId}); Future crateBridgeLoggingReadLastLogLines({required int lineCount}); @@ -699,6 +709,7 @@ abstract class RustLibApi extends BaseApi { String? href, String? summary, String? document, + String? notify, }); Future crateBridgeWebxdcServe({ @@ -868,14 +879,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); @override - Future> crateBridgeWebxdcCatalog({ - required List languages, - }) { + Future crateBridgeWebxdcCacheUpdate({required String instanceId}) { return handler.executeNormal( NormalTask( callFfi: (port_) { final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_list_String(languages, serializer); + sse_encode_String(instanceId, serializer); pdeCallFfi( generalizedFrbRustBinding, serializer, @@ -883,12 +892,47 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { port: port_, ); }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateBridgeWebxdcCacheUpdateConstMeta, + argValues: [instanceId], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateBridgeWebxdcCacheUpdateConstMeta => + const TaskConstMeta( + debugName: "cache_update", + argNames: ["instanceId"], + ); + + @override + Future> crateBridgeWebxdcCatalog({ + required String groupId, + required List languages, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(groupId, serializer); + sse_encode_list_String(languages, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 5, + port: port_, + ); + }, codec: SseCodec( decodeSuccessData: sse_decode_list_webxdc_store_app, decodeErrorData: sse_decode_AnyhowException, ), constMeta: kCrateBridgeWebxdcCatalogConstMeta, - argValues: [languages], + argValues: [groupId, languages], apiImpl: this, ), ); @@ -896,7 +940,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { TaskConstMeta get kCrateBridgeWebxdcCatalogConstMeta => const TaskConstMeta( debugName: "catalog", - argNames: ["languages"], + argNames: ["groupId", "languages"], ); @override @@ -908,7 +952,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 5, + funcId: 6, port: port_, ); }, @@ -938,7 +982,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 6, + funcId: 7, port: port_, ); }, @@ -975,7 +1019,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 7, + funcId: 8, port: port_, ); }, @@ -1010,7 +1054,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 8, + funcId: 9, port: port_, ); }, @@ -1043,7 +1087,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 9, + funcId: 10, port: port_, ); }, @@ -1074,7 +1118,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 10, + funcId: 11, port: port_, ); }, @@ -1104,7 +1148,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 11, + funcId: 12, port: port_, ); }, @@ -1140,7 +1184,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 12, + funcId: 13, port: port_, ); }, @@ -1178,7 +1222,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 13, + funcId: 14, port: port_, ); }, @@ -1214,7 +1258,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 14, + funcId: 15, port: port_, ); }, @@ -1257,7 +1301,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 15, + funcId: 16, port: port_, ); }, @@ -1302,7 +1346,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 16, + funcId: 17, port: port_, ); }, @@ -1339,7 +1383,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 17, + funcId: 18, port: port_, ); }, @@ -1372,7 +1416,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 18, + funcId: 19, port: port_, ); }, @@ -1405,7 +1449,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 19, + funcId: 20, port: port_, ); }, @@ -1435,7 +1479,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 20, + funcId: 21, port: port_, ); }, @@ -1465,7 +1509,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 21, + funcId: 22, port: port_, ); }, @@ -1502,7 +1546,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 22, + funcId: 23, port: port_, ); }, @@ -1523,6 +1567,71 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["groupId", "contactId", "remove"], ); + @override + Future crateBridgeWebxdcMembers({required String instanceId}) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(instanceId, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 24, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_String, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateBridgeWebxdcMembersConstMeta, + argValues: [instanceId], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateBridgeWebxdcMembersConstMeta => const TaskConstMeta( + debugName: "members", + argNames: ["instanceId"], + ); + + @override + Future> crateBridgeWebxdcOneTimeInstances({ + required String groupId, + required List languages, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(groupId, serializer); + sse_encode_list_String(languages, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 25, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_list_webxdc_one_time_instance, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateBridgeWebxdcOneTimeInstancesConstMeta, + argValues: [groupId, languages], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateBridgeWebxdcOneTimeInstancesConstMeta => + const TaskConstMeta( + debugName: "one_time_instances", + argNames: ["groupId", "languages"], + ); + @override Future crateBridgeWebxdcPrepareBundle({required String instanceId}) { return handler.executeNormal( @@ -1533,7 +1642,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 23, + funcId: 26, port: port_, ); }, @@ -1564,7 +1673,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 24, + funcId: 27, port: port_, ); }, @@ -1594,7 +1703,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 25, + funcId: 28, port: port_, ); }, @@ -1629,7 +1738,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 26, + funcId: 29, port: port_, ); }, @@ -1660,7 +1769,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 27, + funcId: 30, port: port_, ); }, @@ -1693,7 +1802,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 28, + funcId: 31, port: port_, ); }, @@ -1723,7 +1832,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 29, + funcId: 32, port: port_, ); }, @@ -1751,7 +1860,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(protocol, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 30)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 33)!; }, codec: SseCodec( decodeSuccessData: sse_decode_String, @@ -1779,7 +1888,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 31, + funcId: 34, port: port_, ); }, @@ -1811,7 +1920,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_i_64(contactId, serializer); sse_encode_i_64(profileCounter, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 32)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 35)!; }, codec: SseCodec( decodeSuccessData: sse_decode_String, @@ -1840,7 +1949,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 33, + funcId: 36, port: port_, ); }, @@ -1870,7 +1979,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 34, + funcId: 37, port: port_, ); }, @@ -1911,7 +2020,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 35, + funcId: 38, port: port_, ); }, @@ -1952,7 +2061,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 36, + funcId: 39, port: port_, ); }, @@ -1987,7 +2096,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 37, + funcId: 40, port: port_, ); }, @@ -2019,7 +2128,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 38, + funcId: 41, port: port_, ); }, @@ -2051,7 +2160,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 39, + funcId: 42, port: port_, ); }, @@ -2081,7 +2190,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 40, + funcId: 43, port: port_, ); }, @@ -2111,7 +2220,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 41, + funcId: 44, port: port_, ); }, @@ -2144,7 +2253,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 42, + funcId: 45, port: port_, ); }, @@ -2175,7 +2284,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 43, + funcId: 46, port: port_, ); }, @@ -2205,7 +2314,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_list_prim_u_8_loose(avatarSvgCompressed, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 44)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 47)!; }, codec: SseCodec( decodeSuccessData: sse_decode_String, @@ -2233,7 +2342,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 45, + funcId: 48, port: port_, ); }, @@ -2264,7 +2373,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 46, + funcId: 49, port: port_, ); }, @@ -2297,7 +2406,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 47, + funcId: 50, port: port_, ); }, @@ -2327,7 +2436,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 48, + funcId: 51, port: port_, ); }, @@ -2358,7 +2467,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 49, + funcId: 52, port: port_, ); }, @@ -2389,7 +2498,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 50, + funcId: 53, port: port_, ); }, @@ -2419,7 +2528,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 51, + funcId: 54, port: port_, ); }, @@ -2452,7 +2561,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 52, + funcId: 55, port: port_, ); }, @@ -2487,7 +2596,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 53, + funcId: 56, port: port_, ); }, @@ -2520,7 +2629,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 54, + funcId: 57, port: port_, ); }, @@ -2552,7 +2661,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 55, + funcId: 58, port: port_, ); }, @@ -2582,7 +2691,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 56, + funcId: 59, port: port_, ); }, @@ -2617,7 +2726,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 57, + funcId: 60, port: port_, ); }, @@ -2647,7 +2756,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 58, + funcId: 61, port: port_, ); }, @@ -2677,7 +2786,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 59, + funcId: 62, port: port_, ); }, @@ -2707,7 +2816,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 60, + funcId: 63, port: port_, ); }, @@ -2748,7 +2857,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 61, + funcId: 64, port: port_, ); }, @@ -2795,7 +2904,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 62, + funcId: 65, port: port_, ); }, @@ -2828,7 +2937,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 63, + funcId: 66, port: port_, ); }, @@ -2861,7 +2970,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 64, + funcId: 67, port: port_, ); }, @@ -2891,7 +3000,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 65, + funcId: 68, port: port_, ); }, @@ -2931,7 +3040,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 66, + funcId: 69, port: port_, ); }, @@ -2970,7 +3079,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 67, + funcId: 70, port: port_, ); }, @@ -3006,7 +3115,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 68, + funcId: 71, port: port_, ); }, @@ -3041,7 +3150,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 69, + funcId: 72, port: port_, ); }, @@ -3083,7 +3192,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 70, + funcId: 73, port: port_, ); }, @@ -3125,7 +3234,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 71, + funcId: 74, port: port_, ); }, @@ -3155,7 +3264,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 72, + funcId: 75, port: port_, ); }, @@ -3188,7 +3297,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 73, + funcId: 76, port: port_, ); }, @@ -3223,7 +3332,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 74, + funcId: 77, port: port_, ); }, @@ -3253,7 +3362,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 75, + funcId: 78, port: port_, ); }, @@ -3288,7 +3397,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 76, + funcId: 79, port: port_, ); }, @@ -3318,7 +3427,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 77, + funcId: 80, port: port_, ); }, @@ -3351,7 +3460,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 78, + funcId: 81, port: port_, ); }, @@ -3381,7 +3490,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 79, + funcId: 82, port: port_, ); }, @@ -3411,7 +3520,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 80, + funcId: 83, port: port_, ); }, @@ -3441,7 +3550,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 81, + funcId: 84, port: port_, ); }, @@ -3480,7 +3589,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 82, + funcId: 85, port: port_, ); }, @@ -3515,7 +3624,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 83, + funcId: 86, port: port_, ); }, @@ -3554,7 +3663,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 84, + funcId: 87, port: port_, ); }, @@ -3596,7 +3705,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 85, + funcId: 88, port: port_, ); }, @@ -3627,7 +3736,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 86, + funcId: 89, port: port_, ); }, @@ -3660,7 +3769,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 87, + funcId: 90, port: port_, ); }, @@ -3693,7 +3802,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 88, + funcId: 91, port: port_, ); }, @@ -3728,7 +3837,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 89, + funcId: 92, port: port_, ); }, @@ -3761,7 +3870,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 90, + funcId: 93, port: port_, ); }, @@ -3794,7 +3903,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 91, + funcId: 94, port: port_, ); }, @@ -3827,7 +3936,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 92, + funcId: 95, port: port_, ); }, @@ -3864,7 +3973,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 93, + funcId: 96, port: port_, ); }, @@ -3894,7 +4003,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 94, + funcId: 97, port: port_, ); }, @@ -3924,7 +4033,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 95, + funcId: 98, port: port_, ); }, @@ -3954,7 +4063,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 96, + funcId: 99, port: port_, ); }, @@ -3987,7 +4096,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 97, + funcId: 100, port: port_, ); }, @@ -4018,7 +4127,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 98, + funcId: 101, port: port_, ); }, @@ -4051,7 +4160,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 99, + funcId: 102, port: port_, ); }, @@ -4097,7 +4206,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 100, + funcId: 103, port: port_, ); }, @@ -4153,7 +4262,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 101, + funcId: 104, port: port_, ); }, @@ -4201,7 +4310,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 102, + funcId: 105, port: port_, ); }, @@ -4239,7 +4348,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 103, + funcId: 106, port: port_, ); }, @@ -4274,7 +4383,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 104, + funcId: 107, port: port_, ); }, @@ -4309,7 +4418,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 105, + funcId: 108, port: port_, ); }, @@ -4342,7 +4451,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 106, + funcId: 109, port: port_, ); }, @@ -4379,7 +4488,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 107, + funcId: 110, port: port_, ); }, @@ -4410,7 +4519,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 108, + funcId: 111, port: port_, ); }, @@ -4448,7 +4557,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 109, + funcId: 112, port: port_, ); }, @@ -4483,7 +4592,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 110, + funcId: 113, port: port_, ); }, @@ -4522,7 +4631,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 111, + funcId: 114, port: port_, ); }, @@ -4555,7 +4664,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 112, + funcId: 115, port: port_, ); }, @@ -4586,7 +4695,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 113, + funcId: 116, port: port_, ); }, @@ -4621,7 +4730,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 114, + funcId: 117, port: port_, ); }, @@ -4651,7 +4760,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 115, + funcId: 118, port: port_, ); }, @@ -4684,7 +4793,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 116, + funcId: 119, port: port_, ); }, @@ -4719,7 +4828,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 117, + funcId: 120, port: port_, ); }, @@ -4752,7 +4861,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 118, + funcId: 121, port: port_, ); }, @@ -4783,7 +4892,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 119, + funcId: 122, port: port_, ); }, @@ -4820,7 +4929,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 120, + funcId: 123, port: port_, ); }, @@ -4872,7 +4981,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 121, + funcId: 124, port: port_, ); }, @@ -4925,7 +5034,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 122, + funcId: 125, port: port_, ); }, @@ -4965,7 +5074,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 123, + funcId: 126, port: port_, ); }, @@ -4998,7 +5107,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 124, + funcId: 127, port: port_, ); }, @@ -5031,7 +5140,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 125, + funcId: 128, port: port_, ); }, @@ -5068,7 +5177,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 126, + funcId: 129, port: port_, ); }, @@ -5100,7 +5209,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 127, + funcId: 130, port: port_, ); }, @@ -5133,7 +5242,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 128, + funcId: 131, port: port_, ); }, @@ -5168,7 +5277,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 129, + funcId: 132, port: port_, ); }, @@ -5200,7 +5309,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 130, + funcId: 133, port: port_, ); }, @@ -5238,7 +5347,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 131, + funcId: 134, port: port_, ); }, @@ -5271,7 +5380,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 132, + funcId: 135, port: port_, ); }, @@ -5309,7 +5418,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 133, + funcId: 136, port: port_, ); }, @@ -5346,7 +5455,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 134, + funcId: 137, port: port_, ); }, @@ -5383,7 +5492,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 135, + funcId: 138, port: port_, ); }, @@ -5421,7 +5530,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 136, + funcId: 139, port: port_, ); }, @@ -5459,7 +5568,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 137, + funcId: 140, port: port_, ); }, @@ -5492,7 +5601,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 138, + funcId: 141, port: port_, ); }, @@ -5524,7 +5633,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 139, + funcId: 142, port: port_, ); }, @@ -5559,7 +5668,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 140, + funcId: 143, port: port_, ); }, @@ -5596,7 +5705,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 141, + funcId: 144, port: port_, ); }, @@ -5629,7 +5738,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 142, + funcId: 145, port: port_, ); }, @@ -5661,7 +5770,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 143, + funcId: 146, port: port_, ); }, @@ -5696,7 +5805,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 144, + funcId: 147, port: port_, ); }, @@ -5735,7 +5844,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 145, + funcId: 148, port: port_, ); }, @@ -5772,7 +5881,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 146, + funcId: 149, port: port_, ); }, @@ -5802,7 +5911,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 147, + funcId: 150, port: port_, ); }, @@ -5834,7 +5943,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 148, + funcId: 151, port: port_, ); }, @@ -5869,7 +5978,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 149, + funcId: 152, port: port_, ); }, @@ -5901,7 +6010,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 150, + funcId: 153, port: port_, ); }, @@ -5939,7 +6048,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 151, + funcId: 154, port: port_, ); }, @@ -5978,7 +6087,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 152, + funcId: 155, port: port_, ); }, @@ -6013,7 +6122,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 153, + funcId: 156, port: port_, ); }, @@ -6042,6 +6151,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { String? href, String? summary, String? document, + String? notify, }) { return handler.executeNormal( NormalTask( @@ -6053,10 +6163,11 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_opt_String(href, serializer); sse_encode_opt_String(summary, serializer); sse_encode_opt_String(document, serializer); + sse_encode_opt_String(notify, serializer); pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 154, + funcId: 157, port: port_, ); }, @@ -6065,7 +6176,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { decodeErrorData: sse_decode_AnyhowException, ), constMeta: kCrateBridgeWebxdcSendUpdateConstMeta, - argValues: [instanceId, payload, info, href, summary, document], + argValues: [instanceId, payload, info, href, summary, document, notify], apiImpl: this, ), ); @@ -6081,6 +6192,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { "href", "summary", "document", + "notify", ], ); @@ -6098,7 +6210,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 155, + funcId: 158, port: port_, ); }, @@ -6132,7 +6244,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 156, + funcId: 159, port: port_, ); }, @@ -6167,7 +6279,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 157, + funcId: 160, port: port_, ); }, @@ -6202,7 +6314,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 158, + funcId: 161, port: port_, ); }, @@ -6235,7 +6347,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 159, + funcId: 162, )!; }, codec: SseCodec( @@ -6275,7 +6387,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 160, + funcId: 163, port: port_, ); }, @@ -6320,7 +6432,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 161, + funcId: 164, port: port_, ); }, @@ -6350,7 +6462,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 162, + funcId: 165, port: port_, ); }, @@ -6383,7 +6495,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 163, + funcId: 166, port: port_, ); }, @@ -6418,7 +6530,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 164, + funcId: 167, port: port_, ); }, @@ -6457,7 +6569,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 165, + funcId: 168, )!; }, codec: SseCodec( @@ -7067,6 +7179,16 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return (raw as List).map(dco_decode_sql_value).toList(); } + @protected + List dco_decode_list_webxdc_one_time_instance( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return (raw as List) + .map(dco_decode_webxdc_one_time_instance) + .toList(); + } + @protected List dco_decode_list_webxdc_store_app(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -7559,6 +7681,21 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + WebxdcOneTimeInstance dco_decode_webxdc_one_time_instance(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 5) + throw Exception('unexpected arr length: expect 5 but see ${arr.length}'); + return WebxdcOneTimeInstance( + instanceId: dco_decode_String(arr[0]), + name: dco_decode_String(arr[1]), + icon: dco_decode_opt_list_prim_u_8_strict(arr[2]), + summary: dco_decode_opt_String(arr[3]), + document: dco_decode_opt_String(arr[4]), + ); + } + @protected WebxdcResponse dco_decode_webxdc_response(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -7578,8 +7715,8 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { WebxdcStoreApp dco_decode_webxdc_store_app(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs final arr = raw as List; - if (arr.length != 7) - throw Exception('unexpected arr length: expect 7 but see ${arr.length}'); + if (arr.length != 10) + throw Exception('unexpected arr length: expect 10 but see ${arr.length}'); return WebxdcStoreApp( appId: dco_decode_String(arr[0]), version: dco_decode_i_64(arr[1]), @@ -7588,6 +7725,9 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sourceCodeUrl: dco_decode_opt_String(arr[4]), icon: dco_decode_opt_list_prim_u_8_strict(arr[5]), bundleBytes: dco_decode_i_64(arr[6]), + proOnly: dco_decode_bool(arr[7]), + oneTime: dco_decode_bool(arr[8]), + instanceId: dco_decode_opt_String(arr[9]), ); } @@ -8219,6 +8359,20 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return ans_; } + @protected + List sse_decode_list_webxdc_one_time_instance( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + var len_ = sse_decode_i_32(deserializer); + var ans_ = []; + for (var idx_ = 0; idx_ < len_; ++idx_) { + ans_.add(sse_decode_webxdc_one_time_instance(deserializer)); + } + return ans_; + } + @protected List sse_decode_list_webxdc_store_app( SseDeserializer deserializer, @@ -8869,6 +9023,25 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + WebxdcOneTimeInstance sse_decode_webxdc_one_time_instance( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_instanceId = sse_decode_String(deserializer); + var var_name = sse_decode_String(deserializer); + var var_icon = sse_decode_opt_list_prim_u_8_strict(deserializer); + var var_summary = sse_decode_opt_String(deserializer); + var var_document = sse_decode_opt_String(deserializer); + return WebxdcOneTimeInstance( + instanceId: var_instanceId, + name: var_name, + icon: var_icon, + summary: var_summary, + document: var_document, + ); + } + @protected WebxdcResponse sse_decode_webxdc_response(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -8896,6 +9069,9 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { var var_sourceCodeUrl = sse_decode_opt_String(deserializer); var var_icon = sse_decode_opt_list_prim_u_8_strict(deserializer); var var_bundleBytes = sse_decode_i_64(deserializer); + var var_proOnly = sse_decode_bool(deserializer); + var var_oneTime = sse_decode_bool(deserializer); + var var_instanceId = sse_decode_opt_String(deserializer); return WebxdcStoreApp( appId: var_appId, version: var_version, @@ -8904,6 +9080,9 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sourceCodeUrl: var_sourceCodeUrl, icon: var_icon, bundleBytes: var_bundleBytes, + proOnly: var_proOnly, + oneTime: var_oneTime, + instanceId: var_instanceId, ); } @@ -9549,6 +9728,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + void sse_encode_list_webxdc_one_time_instance( + List self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.length, serializer); + for (final item in self) { + sse_encode_webxdc_one_time_instance(item, serializer); + } + } + @protected void sse_encode_list_webxdc_store_app( List self, @@ -10083,6 +10274,19 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_opt_String(self.document, serializer); } + @protected + void sse_encode_webxdc_one_time_instance( + WebxdcOneTimeInstance self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_String(self.instanceId, serializer); + sse_encode_String(self.name, serializer); + sse_encode_opt_list_prim_u_8_strict(self.icon, serializer); + sse_encode_opt_String(self.summary, serializer); + sse_encode_opt_String(self.document, serializer); + } + @protected void sse_encode_webxdc_response( WebxdcResponse self, @@ -10109,6 +10313,9 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_opt_String(self.sourceCodeUrl, serializer); sse_encode_opt_list_prim_u_8_strict(self.icon, serializer); sse_encode_i_64(self.bundleBytes, serializer); + sse_encode_bool(self.proOnly, serializer); + sse_encode_bool(self.oneTime, serializer); + sse_encode_opt_String(self.instanceId, serializer); } @protected diff --git a/lib/core/frb_generated.io.dart b/lib/core/frb_generated.io.dart index 7df08c07..545e4ce5 100644 --- a/lib/core/frb_generated.io.dart +++ b/lib/core/frb_generated.io.dart @@ -238,6 +238,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected List dco_decode_list_sql_value(dynamic raw); + @protected + List dco_decode_list_webxdc_one_time_instance( + dynamic raw, + ); + @protected List dco_decode_list_webxdc_store_app(dynamic raw); @@ -393,6 +398,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected WebxdcInstanceInfo dco_decode_webxdc_instance_info(dynamic raw); + @protected + WebxdcOneTimeInstance dco_decode_webxdc_one_time_instance(dynamic raw); + @protected WebxdcResponse dco_decode_webxdc_response(dynamic raw); @@ -641,6 +649,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected List sse_decode_list_sql_value(SseDeserializer deserializer); + @protected + List sse_decode_list_webxdc_one_time_instance( + SseDeserializer deserializer, + ); + @protected List sse_decode_list_webxdc_store_app( SseDeserializer deserializer, @@ -820,6 +833,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); + @protected + WebxdcOneTimeInstance sse_decode_webxdc_one_time_instance( + SseDeserializer deserializer, + ); + @protected WebxdcResponse sse_decode_webxdc_response(SseDeserializer deserializer); @@ -1143,6 +1161,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_list_sql_value(List self, SseSerializer serializer); + @protected + void sse_encode_list_webxdc_one_time_instance( + List self, + SseSerializer serializer, + ); + @protected void sse_encode_list_webxdc_store_app( List self, @@ -1362,6 +1386,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_webxdc_one_time_instance( + WebxdcOneTimeInstance self, + SseSerializer serializer, + ); + @protected void sse_encode_webxdc_response( WebxdcResponse self, diff --git a/lib/core/frb_generated.web.dart b/lib/core/frb_generated.web.dart index 6ba83798..b53dd885 100644 --- a/lib/core/frb_generated.web.dart +++ b/lib/core/frb_generated.web.dart @@ -240,6 +240,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected List dco_decode_list_sql_value(dynamic raw); + @protected + List dco_decode_list_webxdc_one_time_instance( + dynamic raw, + ); + @protected List dco_decode_list_webxdc_store_app(dynamic raw); @@ -395,6 +400,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected WebxdcInstanceInfo dco_decode_webxdc_instance_info(dynamic raw); + @protected + WebxdcOneTimeInstance dco_decode_webxdc_one_time_instance(dynamic raw); + @protected WebxdcResponse dco_decode_webxdc_response(dynamic raw); @@ -643,6 +651,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected List sse_decode_list_sql_value(SseDeserializer deserializer); + @protected + List sse_decode_list_webxdc_one_time_instance( + SseDeserializer deserializer, + ); + @protected List sse_decode_list_webxdc_store_app( SseDeserializer deserializer, @@ -822,6 +835,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); + @protected + WebxdcOneTimeInstance sse_decode_webxdc_one_time_instance( + SseDeserializer deserializer, + ); + @protected WebxdcResponse sse_decode_webxdc_response(SseDeserializer deserializer); @@ -1145,6 +1163,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_list_sql_value(List self, SseSerializer serializer); + @protected + void sse_encode_list_webxdc_one_time_instance( + List self, + SseSerializer serializer, + ); + @protected void sse_encode_list_webxdc_store_app( List self, @@ -1364,6 +1388,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_webxdc_one_time_instance( + WebxdcOneTimeInstance self, + SseSerializer serializer, + ); + @protected void sse_encode_webxdc_response( WebxdcResponse self, diff --git a/lib/src/database/daos/messages.dao.dart b/lib/src/database/daos/messages.dao.dart index 72e63240..e79ce35d 100644 --- a/lib/src/database/daos/messages.dao.dart +++ b/lib/src/database/daos/messages.dao.dart @@ -641,8 +641,34 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { return (delete(messages)..where((t) => t.messageId.equals(messageId))).go(); } - Future deleteMessagesByGroupId(String groupId) { - return (delete(messages)..where((t) => t.groupId.equals(groupId))).go(); + Future deleteMessagesByGroupId(String groupId) async { + await transaction(() async { + // A one-time app is the chat's durable shared object. Keep its card (and + // therefore its instance and update log) while removing the visible chat + // history and every ordinary app. + await customStatement( + ''' + DELETE FROM messages + WHERE group_id = ? + AND NOT EXISTS ( + SELECT 1 + FROM webxdc_instances AS instance + LEFT JOIN webxdc_apps AS app + ON app.app_id = instance.app_id + AND app.version = instance.version + WHERE instance.instance_id = messages.message_id + AND (app.one_time = 1 OR app.app_id IS NULL) + )''', + [groupId], + ); + await customStatement( + ''' + UPDATE groups + SET deleted_content = 1, archived = 0, pinned = 0, draft_message = NULL + WHERE group_id = ?''', + [groupId], + ); + }); } SingleOrNullSelectable getMessageById(String messageId) { diff --git a/lib/src/database/tables/webxdc.table.dart b/lib/src/database/tables/webxdc.table.dart index 4a904a9a..dd522dbc 100644 --- a/lib/src/database/tables/webxdc.table.dart +++ b/lib/src/database/tables/webxdc.table.dart @@ -31,6 +31,9 @@ class WebxdcApps extends Table { IntColumn get bundleBytes => integer()(); BoolColumn get published => boolean().withDefault(const Constant(true))(); IntColumn get cachedAt => integer()(); + BoolColumn get proOnly => boolean().withDefault(const Constant(false))(); + IntColumn get sortOrder => integer().withDefault(const Constant(0))(); + BoolColumn get oneTime => boolean().withDefault(const Constant(false))(); @override Set get primaryKey => {appId, version}; diff --git a/lib/src/database/twonly.db.g.dart b/lib/src/database/twonly.db.g.dart index 8f554af8..48d99d8f 100644 --- a/lib/src/database/twonly.db.g.dart +++ b/lib/src/database/twonly.db.g.dart @@ -12663,6 +12663,48 @@ class $WebxdcAppsTable extends WebxdcApps type: DriftSqlType.int, requiredDuringInsert: true, ); + static const VerificationMeta _proOnlyMeta = const VerificationMeta( + 'proOnly', + ); + @override + late final GeneratedColumn proOnly = GeneratedColumn( + 'pro_only', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("pro_only" IN (0, 1))', + ), + defaultValue: const Constant(false), + ); + static const VerificationMeta _sortOrderMeta = const VerificationMeta( + 'sortOrder', + ); + @override + late final GeneratedColumn sortOrder = GeneratedColumn( + 'sort_order', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0), + ); + static const VerificationMeta _oneTimeMeta = const VerificationMeta( + 'oneTime', + ); + @override + late final GeneratedColumn oneTime = GeneratedColumn( + 'one_time', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("one_time" IN (0, 1))', + ), + defaultValue: const Constant(false), + ); @override List get $columns => [ appId, @@ -12676,6 +12718,9 @@ class $WebxdcAppsTable extends WebxdcApps bundleBytes, published, cachedAt, + proOnly, + sortOrder, + oneTime, ]; @override String get aliasedName => _alias ?? actualTableName; @@ -12782,6 +12827,24 @@ class $WebxdcAppsTable extends WebxdcApps } else if (isInserting) { context.missing(_cachedAtMeta); } + if (data.containsKey('pro_only')) { + context.handle( + _proOnlyMeta, + proOnly.isAcceptableOrUnknown(data['pro_only']!, _proOnlyMeta), + ); + } + if (data.containsKey('sort_order')) { + context.handle( + _sortOrderMeta, + sortOrder.isAcceptableOrUnknown(data['sort_order']!, _sortOrderMeta), + ); + } + if (data.containsKey('one_time')) { + context.handle( + _oneTimeMeta, + oneTime.isAcceptableOrUnknown(data['one_time']!, _oneTimeMeta), + ); + } return context; } @@ -12835,6 +12898,18 @@ class $WebxdcAppsTable extends WebxdcApps DriftSqlType.int, data['${effectivePrefix}cached_at'], )!, + proOnly: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}pro_only'], + )!, + sortOrder: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}sort_order'], + )!, + oneTime: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}one_time'], + )!, ); } @@ -12866,6 +12941,9 @@ class WebxdcApp extends DataClass implements Insertable { final int bundleBytes; final bool published; final int cachedAt; + final bool proOnly; + final int sortOrder; + final bool oneTime; const WebxdcApp({ required this.appId, required this.version, @@ -12878,6 +12956,9 @@ class WebxdcApp extends DataClass implements Insertable { required this.bundleBytes, required this.published, required this.cachedAt, + required this.proOnly, + required this.sortOrder, + required this.oneTime, }); @override Map toColumns(bool nullToAbsent) { @@ -12897,6 +12978,9 @@ class WebxdcApp extends DataClass implements Insertable { map['bundle_bytes'] = Variable(bundleBytes); map['published'] = Variable(published); map['cached_at'] = Variable(cachedAt); + map['pro_only'] = Variable(proOnly); + map['sort_order'] = Variable(sortOrder); + map['one_time'] = Variable(oneTime); return map; } @@ -12915,6 +12999,9 @@ class WebxdcApp extends DataClass implements Insertable { bundleBytes: Value(bundleBytes), published: Value(published), cachedAt: Value(cachedAt), + proOnly: Value(proOnly), + sortOrder: Value(sortOrder), + oneTime: Value(oneTime), ); } @@ -12935,6 +13022,9 @@ class WebxdcApp extends DataClass implements Insertable { bundleBytes: serializer.fromJson(json['bundleBytes']), published: serializer.fromJson(json['published']), cachedAt: serializer.fromJson(json['cachedAt']), + proOnly: serializer.fromJson(json['proOnly']), + sortOrder: serializer.fromJson(json['sortOrder']), + oneTime: serializer.fromJson(json['oneTime']), ); } @override @@ -12952,6 +13042,9 @@ class WebxdcApp extends DataClass implements Insertable { 'bundleBytes': serializer.toJson(bundleBytes), 'published': serializer.toJson(published), 'cachedAt': serializer.toJson(cachedAt), + 'proOnly': serializer.toJson(proOnly), + 'sortOrder': serializer.toJson(sortOrder), + 'oneTime': serializer.toJson(oneTime), }; } @@ -12967,6 +13060,9 @@ class WebxdcApp extends DataClass implements Insertable { int? bundleBytes, bool? published, int? cachedAt, + bool? proOnly, + int? sortOrder, + bool? oneTime, }) => WebxdcApp( appId: appId ?? this.appId, version: version ?? this.version, @@ -12981,6 +13077,9 @@ class WebxdcApp extends DataClass implements Insertable { bundleBytes: bundleBytes ?? this.bundleBytes, published: published ?? this.published, cachedAt: cachedAt ?? this.cachedAt, + proOnly: proOnly ?? this.proOnly, + sortOrder: sortOrder ?? this.sortOrder, + oneTime: oneTime ?? this.oneTime, ); WebxdcApp copyWithCompanion(WebxdcAppsCompanion data) { return WebxdcApp( @@ -13005,6 +13104,9 @@ class WebxdcApp extends DataClass implements Insertable { : this.bundleBytes, published: data.published.present ? data.published.value : this.published, cachedAt: data.cachedAt.present ? data.cachedAt.value : this.cachedAt, + proOnly: data.proOnly.present ? data.proOnly.value : this.proOnly, + sortOrder: data.sortOrder.present ? data.sortOrder.value : this.sortOrder, + oneTime: data.oneTime.present ? data.oneTime.value : this.oneTime, ); } @@ -13021,7 +13123,10 @@ class WebxdcApp extends DataClass implements Insertable { ..write('bundleSha256: $bundleSha256, ') ..write('bundleBytes: $bundleBytes, ') ..write('published: $published, ') - ..write('cachedAt: $cachedAt') + ..write('cachedAt: $cachedAt, ') + ..write('proOnly: $proOnly, ') + ..write('sortOrder: $sortOrder, ') + ..write('oneTime: $oneTime') ..write(')')) .toString(); } @@ -13039,6 +13144,9 @@ class WebxdcApp extends DataClass implements Insertable { bundleBytes, published, cachedAt, + proOnly, + sortOrder, + oneTime, ); @override bool operator ==(Object other) => @@ -13054,7 +13162,10 @@ class WebxdcApp extends DataClass implements Insertable { other.bundleSha256 == this.bundleSha256 && other.bundleBytes == this.bundleBytes && other.published == this.published && - other.cachedAt == this.cachedAt); + other.cachedAt == this.cachedAt && + other.proOnly == this.proOnly && + other.sortOrder == this.sortOrder && + other.oneTime == this.oneTime); } class WebxdcAppsCompanion extends UpdateCompanion { @@ -13069,6 +13180,9 @@ class WebxdcAppsCompanion extends UpdateCompanion { final Value bundleBytes; final Value published; final Value cachedAt; + final Value proOnly; + final Value sortOrder; + final Value oneTime; final Value rowid; const WebxdcAppsCompanion({ this.appId = const Value.absent(), @@ -13082,6 +13196,9 @@ class WebxdcAppsCompanion extends UpdateCompanion { this.bundleBytes = const Value.absent(), this.published = const Value.absent(), this.cachedAt = const Value.absent(), + this.proOnly = const Value.absent(), + this.sortOrder = const Value.absent(), + this.oneTime = const Value.absent(), this.rowid = const Value.absent(), }); WebxdcAppsCompanion.insert({ @@ -13096,6 +13213,9 @@ class WebxdcAppsCompanion extends UpdateCompanion { required int bundleBytes, this.published = const Value.absent(), required int cachedAt, + this.proOnly = const Value.absent(), + this.sortOrder = const Value.absent(), + this.oneTime = const Value.absent(), this.rowid = const Value.absent(), }) : appId = Value(appId), version = Value(version), @@ -13115,6 +13235,9 @@ class WebxdcAppsCompanion extends UpdateCompanion { Expression? bundleBytes, Expression? published, Expression? cachedAt, + Expression? proOnly, + Expression? sortOrder, + Expression? oneTime, Expression? rowid, }) { return RawValuesInsertable({ @@ -13129,6 +13252,9 @@ class WebxdcAppsCompanion extends UpdateCompanion { if (bundleBytes != null) 'bundle_bytes': bundleBytes, if (published != null) 'published': published, if (cachedAt != null) 'cached_at': cachedAt, + if (proOnly != null) 'pro_only': proOnly, + if (sortOrder != null) 'sort_order': sortOrder, + if (oneTime != null) 'one_time': oneTime, if (rowid != null) 'rowid': rowid, }); } @@ -13145,6 +13271,9 @@ class WebxdcAppsCompanion extends UpdateCompanion { Value? bundleBytes, Value? published, Value? cachedAt, + Value? proOnly, + Value? sortOrder, + Value? oneTime, Value? rowid, }) { return WebxdcAppsCompanion( @@ -13159,6 +13288,9 @@ class WebxdcAppsCompanion extends UpdateCompanion { bundleBytes: bundleBytes ?? this.bundleBytes, published: published ?? this.published, cachedAt: cachedAt ?? this.cachedAt, + proOnly: proOnly ?? this.proOnly, + sortOrder: sortOrder ?? this.sortOrder, + oneTime: oneTime ?? this.oneTime, rowid: rowid ?? this.rowid, ); } @@ -13199,6 +13331,15 @@ class WebxdcAppsCompanion extends UpdateCompanion { if (cachedAt.present) { map['cached_at'] = Variable(cachedAt.value); } + if (proOnly.present) { + map['pro_only'] = Variable(proOnly.value); + } + if (sortOrder.present) { + map['sort_order'] = Variable(sortOrder.value); + } + if (oneTime.present) { + map['one_time'] = Variable(oneTime.value); + } if (rowid.present) { map['rowid'] = Variable(rowid.value); } @@ -13219,6 +13360,9 @@ class WebxdcAppsCompanion extends UpdateCompanion { ..write('bundleBytes: $bundleBytes, ') ..write('published: $published, ') ..write('cachedAt: $cachedAt, ') + ..write('proOnly: $proOnly, ') + ..write('sortOrder: $sortOrder, ') + ..write('oneTime: $oneTime, ') ..write('rowid: $rowid') ..write(')')) .toString(); @@ -25803,6 +25947,9 @@ typedef $$WebxdcAppsTableCreateCompanionBuilder = required int bundleBytes, Value published, required int cachedAt, + Value proOnly, + Value sortOrder, + Value oneTime, Value rowid, }); typedef $$WebxdcAppsTableUpdateCompanionBuilder = @@ -25818,6 +25965,9 @@ typedef $$WebxdcAppsTableUpdateCompanionBuilder = Value bundleBytes, Value published, Value cachedAt, + Value proOnly, + Value sortOrder, + Value oneTime, Value rowid, }); @@ -25884,6 +26034,21 @@ class $$WebxdcAppsTableFilterComposer column: $table.cachedAt, builder: (column) => ColumnFilters(column), ); + + ColumnFilters get proOnly => $composableBuilder( + column: $table.proOnly, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get sortOrder => $composableBuilder( + column: $table.sortOrder, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get oneTime => $composableBuilder( + column: $table.oneTime, + builder: (column) => ColumnFilters(column), + ); } class $$WebxdcAppsTableOrderingComposer @@ -25949,6 +26114,21 @@ class $$WebxdcAppsTableOrderingComposer column: $table.cachedAt, builder: (column) => ColumnOrderings(column), ); + + ColumnOrderings get proOnly => $composableBuilder( + column: $table.proOnly, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get sortOrder => $composableBuilder( + column: $table.sortOrder, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get oneTime => $composableBuilder( + column: $table.oneTime, + builder: (column) => ColumnOrderings(column), + ); } class $$WebxdcAppsTableAnnotationComposer @@ -26002,6 +26182,15 @@ class $$WebxdcAppsTableAnnotationComposer GeneratedColumn get cachedAt => $composableBuilder(column: $table.cachedAt, builder: (column) => column); + + GeneratedColumn get proOnly => + $composableBuilder(column: $table.proOnly, builder: (column) => column); + + GeneratedColumn get sortOrder => + $composableBuilder(column: $table.sortOrder, builder: (column) => column); + + GeneratedColumn get oneTime => + $composableBuilder(column: $table.oneTime, builder: (column) => column); } class $$WebxdcAppsTableTableManager @@ -26043,6 +26232,9 @@ class $$WebxdcAppsTableTableManager Value bundleBytes = const Value.absent(), Value published = const Value.absent(), Value cachedAt = const Value.absent(), + Value proOnly = const Value.absent(), + Value sortOrder = const Value.absent(), + Value oneTime = const Value.absent(), Value rowid = const Value.absent(), }) => WebxdcAppsCompanion( appId: appId, @@ -26056,6 +26248,9 @@ class $$WebxdcAppsTableTableManager bundleBytes: bundleBytes, published: published, cachedAt: cachedAt, + proOnly: proOnly, + sortOrder: sortOrder, + oneTime: oneTime, rowid: rowid, ), createCompanionCallback: @@ -26071,6 +26266,9 @@ class $$WebxdcAppsTableTableManager required int bundleBytes, Value published = const Value.absent(), required int cachedAt, + Value proOnly = const Value.absent(), + Value sortOrder = const Value.absent(), + Value oneTime = const Value.absent(), Value rowid = const Value.absent(), }) => WebxdcAppsCompanion.insert( appId: appId, @@ -26084,6 +26282,9 @@ class $$WebxdcAppsTableTableManager bundleBytes: bundleBytes, published: published, cachedAt: cachedAt, + proOnly: proOnly, + sortOrder: sortOrder, + oneTime: oneTime, rowid: rowid, ), withReferenceMapper: (p0) => p0 diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index 14b7a54a..b4ab479f 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -98,6 +98,12 @@ abstract class AppLocalizations { Locale('en'), ]; + /// No description provided for @subscriptionAllApps. + /// + /// In en, this message translates to: + /// **'Access to all apps'** + String get subscriptionAllApps; + /// No description provided for @registerSlogan. /// /// In en, this message translates to: @@ -4736,6 +4742,30 @@ abstract class AppLocalizations { /// **'The app could not be added to this chat.'** String get webxdcStoreFailed; + /// No description provided for @webxdcStoreAlreadyAdded. + /// + /// In en, this message translates to: + /// **'Already added to this chat'** + String get webxdcStoreAlreadyAdded; + + /// No description provided for @webxdcChatAppsMenu. + /// + /// In en, this message translates to: + /// **'Chat apps'** + String get webxdcChatAppsMenu; + + /// No description provided for @webxdcChatAppsTitle. + /// + /// In en, this message translates to: + /// **'Chat apps'** + String get webxdcChatAppsTitle; + + /// No description provided for @webxdcChatAppsEmpty. + /// + /// In en, this message translates to: + /// **'No one-time apps have been added to this chat yet.'** + String get webxdcChatAppsEmpty; + /// No description provided for @webxdcUnavailable. /// /// In en, this message translates to: diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index f2c22dbf..cca01180 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -8,6 +8,9 @@ import 'app_localizations.dart'; class AppLocalizationsDe extends AppLocalizations { AppLocalizationsDe([String locale = 'de']) : super(locale); + @override + String get subscriptionAllApps => 'Zugriff auf alle Apps'; + @override String get registerSlogan => 'Privat in Kontakt bleiben.'; @@ -2754,6 +2757,19 @@ class AppLocalizationsDe extends AppLocalizations { String get webxdcStoreFailed => 'Die App konnte diesem Chat nicht hinzugefügt werden.'; + @override + String get webxdcStoreAlreadyAdded => 'Bereits zu diesem Chat hinzugefügt'; + + @override + String get webxdcChatAppsMenu => 'Chat-Apps'; + + @override + String get webxdcChatAppsTitle => 'Chat-Apps'; + + @override + String get webxdcChatAppsEmpty => + 'Diesem Chat wurden noch keine einmaligen Apps hinzugefügt.'; + @override String get webxdcUnavailable => 'Diese App konnte nicht geladen werden.'; diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index acd518fa..fe195e8e 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -8,6 +8,9 @@ import 'app_localizations.dart'; class AppLocalizationsEn extends AppLocalizations { AppLocalizationsEn([String locale = 'en']) : super(locale); + @override + String get subscriptionAllApps => 'Access to all apps'; + @override String get registerSlogan => 'Stay in touch privately.'; @@ -2726,6 +2729,19 @@ class AppLocalizationsEn extends AppLocalizations { @override String get webxdcStoreFailed => 'The app could not be added to this chat.'; + @override + String get webxdcStoreAlreadyAdded => 'Already added to this chat'; + + @override + String get webxdcChatAppsMenu => 'Chat apps'; + + @override + String get webxdcChatAppsTitle => 'Chat apps'; + + @override + String get webxdcChatAppsEmpty => + 'No one-time apps have been added to this chat yet.'; + @override String get webxdcUnavailable => 'This app could not be loaded.'; diff --git a/lib/src/localization/translations/de.arb b/lib/src/localization/translations/de.arb index 8a2a686f..3b3f5b85 100644 --- a/lib/src/localization/translations/de.arb +++ b/lib/src/localization/translations/de.arb @@ -1,4 +1,5 @@ { + "subscriptionAllApps": "Zugriff auf alle Apps", "@@locale": "de", "registerSlogan": "Privat in Kontakt bleiben.", "onboardingWelcomeTitle": "Willkommen bei twonly!", @@ -1037,6 +1038,10 @@ "webxdcStoreEmpty": "Es sind noch keine Apps verfügbar.", "webxdcStoreOffline": "Die App-Liste konnte nicht geladen werden. Prüfe deine Verbindung und versuche es erneut.", "webxdcStoreFailed": "Die App konnte diesem Chat nicht hinzugefügt werden.", + "webxdcStoreAlreadyAdded": "Bereits zu diesem Chat hinzugefügt", + "webxdcChatAppsMenu": "Chat-Apps", + "webxdcChatAppsTitle": "Chat-Apps", + "webxdcChatAppsEmpty": "Diesem Chat wurden noch keine einmaligen Apps hinzugefügt.", "webxdcUnavailable": "Diese App konnte nicht geladen werden.", "webxdcDeleteConfirm": "Alles, was diese App auf diesem Gerät gespeichert hat, wird gelöscht. Andere Mitglieder behalten ihre eigene Kopie.", "webxdcExternalLinkTitle": "twonly verlassen?", diff --git a/lib/src/localization/translations/en.arb b/lib/src/localization/translations/en.arb index eeb36aca..517e8ec3 100644 --- a/lib/src/localization/translations/en.arb +++ b/lib/src/localization/translations/en.arb @@ -1,4 +1,5 @@ { + "subscriptionAllApps": "Access to all apps", "@@locale": "en", "registerSlogan": "Stay in touch privately.", "onboardingWelcomeTitle": "Welcome to twonly!", @@ -1047,6 +1048,10 @@ "webxdcStoreEmpty": "No apps are available yet.", "webxdcStoreOffline": "The app list could not be loaded. Check your connection and try again.", "webxdcStoreFailed": "The app could not be added to this chat.", + "webxdcStoreAlreadyAdded": "Already added to this chat", + "webxdcChatAppsMenu": "Chat apps", + "webxdcChatAppsTitle": "Chat apps", + "webxdcChatAppsEmpty": "No one-time apps have been added to this chat yet.", "webxdcUnavailable": "This app could not be loaded.", "webxdcDeleteConfirm": "Everything this app saved on this device is deleted. Other members keep their own copy.", "webxdcExternalLinkTitle": "Leave twonly?", diff --git a/lib/src/model/protobuf/client/generated/data.pb.dart b/lib/src/model/protobuf/client/generated/data.pb.dart index 7d41a9b2..8634c355 100644 --- a/lib/src/model/protobuf/client/generated/data.pb.dart +++ b/lib/src/model/protobuf/client/generated/data.pb.dart @@ -109,6 +109,8 @@ class AdditionalMessageData extends $pb.GeneratedMessage { WebxdcApp? webxdcApp, WebxdcUpdate? webxdcUpdate, WebxdcOrigin? webxdcOrigin, + WebxdcSyncChunk? webxdcSync, + WebxdcSyncRequest? webxdcSyncRequest, }) { final result = create(); if (type != null) result.type = type; @@ -120,6 +122,8 @@ class AdditionalMessageData extends $pb.GeneratedMessage { if (webxdcApp != null) result.webxdcApp = webxdcApp; if (webxdcUpdate != null) result.webxdcUpdate = webxdcUpdate; if (webxdcOrigin != null) result.webxdcOrigin = webxdcOrigin; + if (webxdcSync != null) result.webxdcSync = webxdcSync; + if (webxdcSyncRequest != null) result.webxdcSyncRequest = webxdcSyncRequest; return result; } @@ -148,6 +152,10 @@ class AdditionalMessageData extends $pb.GeneratedMessage { subBuilder: WebxdcUpdate.create) ..aOM(8, _omitFieldNames ? '' : 'webxdcOrigin', subBuilder: WebxdcOrigin.create) + ..aOM(9, _omitFieldNames ? '' : 'webxdcSync', + subBuilder: WebxdcSyncChunk.create) + ..aOM(10, _omitFieldNames ? '' : 'webxdcSyncRequest', + subBuilder: WebxdcSyncRequest.create) ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -241,6 +249,464 @@ class AdditionalMessageData extends $pb.GeneratedMessage { void clearWebxdcOrigin() => $_clearField(8); @$pb.TagNumber(8) WebxdcOrigin ensureWebxdcOrigin() => $_ensure(7); + + @$pb.TagNumber(9) + WebxdcSyncChunk get webxdcSync => $_getN(8); + @$pb.TagNumber(9) + set webxdcSync(WebxdcSyncChunk value) => $_setField(9, value); + @$pb.TagNumber(9) + $core.bool hasWebxdcSync() => $_has(8); + @$pb.TagNumber(9) + void clearWebxdcSync() => $_clearField(9); + @$pb.TagNumber(9) + WebxdcSyncChunk ensureWebxdcSync() => $_ensure(8); + + @$pb.TagNumber(10) + WebxdcSyncRequest get webxdcSyncRequest => $_getN(9); + @$pb.TagNumber(10) + set webxdcSyncRequest(WebxdcSyncRequest value) => $_setField(10, value); + @$pb.TagNumber(10) + $core.bool hasWebxdcSyncRequest() => $_has(9); + @$pb.TagNumber(10) + void clearWebxdcSyncRequest() => $_clearField(10); + @$pb.TagNumber(10) + WebxdcSyncRequest ensureWebxdcSyncRequest() => $_ensure(9); +} + +/// A peer-to-peer transfer of existing one-time app state. The encoded payload +/// is split into bounded hidden messages and installed only after every chunk +/// arrived and the digest matches. +class WebxdcSyncChunk extends $pb.GeneratedMessage { + factory WebxdcSyncChunk({ + $core.String? transferId, + $core.int? chunkIndex, + $core.int? chunkCount, + $core.List<$core.int>? payloadSha256, + $core.List<$core.int>? payload, + }) { + final result = create(); + if (transferId != null) result.transferId = transferId; + if (chunkIndex != null) result.chunkIndex = chunkIndex; + if (chunkCount != null) result.chunkCount = chunkCount; + if (payloadSha256 != null) result.payloadSha256 = payloadSha256; + if (payload != null) result.payload = payload; + return result; + } + + WebxdcSyncChunk._(); + + factory WebxdcSyncChunk.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory WebxdcSyncChunk.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'WebxdcSyncChunk', + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'transferId') + ..aI(2, _omitFieldNames ? '' : 'chunkIndex', fieldType: $pb.PbFieldType.OU3) + ..aI(3, _omitFieldNames ? '' : 'chunkCount', fieldType: $pb.PbFieldType.OU3) + ..a<$core.List<$core.int>>( + 4, _omitFieldNames ? '' : 'payloadSha256', $pb.PbFieldType.OY) + ..a<$core.List<$core.int>>( + 5, _omitFieldNames ? '' : 'payload', $pb.PbFieldType.OY) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + WebxdcSyncChunk clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + WebxdcSyncChunk copyWith(void Function(WebxdcSyncChunk) updates) => + super.copyWith((message) => updates(message as WebxdcSyncChunk)) + as WebxdcSyncChunk; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static WebxdcSyncChunk create() => WebxdcSyncChunk._(); + @$core.override + WebxdcSyncChunk createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static WebxdcSyncChunk getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static WebxdcSyncChunk? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get transferId => $_getSZ(0); + @$pb.TagNumber(1) + set transferId($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasTransferId() => $_has(0); + @$pb.TagNumber(1) + void clearTransferId() => $_clearField(1); + + @$pb.TagNumber(2) + $core.int get chunkIndex => $_getIZ(1); + @$pb.TagNumber(2) + set chunkIndex($core.int value) => $_setUnsignedInt32(1, value); + @$pb.TagNumber(2) + $core.bool hasChunkIndex() => $_has(1); + @$pb.TagNumber(2) + void clearChunkIndex() => $_clearField(2); + + @$pb.TagNumber(3) + $core.int get chunkCount => $_getIZ(2); + @$pb.TagNumber(3) + set chunkCount($core.int value) => $_setUnsignedInt32(2, value); + @$pb.TagNumber(3) + $core.bool hasChunkCount() => $_has(2); + @$pb.TagNumber(3) + void clearChunkCount() => $_clearField(3); + + @$pb.TagNumber(4) + $core.List<$core.int> get payloadSha256 => $_getN(3); + @$pb.TagNumber(4) + set payloadSha256($core.List<$core.int> value) => $_setBytes(3, value); + @$pb.TagNumber(4) + $core.bool hasPayloadSha256() => $_has(3); + @$pb.TagNumber(4) + void clearPayloadSha256() => $_clearField(4); + + @$pb.TagNumber(5) + $core.List<$core.int> get payload => $_getN(4); + @$pb.TagNumber(5) + set payload($core.List<$core.int> value) => $_setBytes(4, value); + @$pb.TagNumber(5) + $core.bool hasPayload() => $_has(4); + @$pb.TagNumber(5) + void clearPayload() => $_clearField(5); +} + +class WebxdcSyncRequest extends $pb.GeneratedMessage { + factory WebxdcSyncRequest() => create(); + + WebxdcSyncRequest._(); + + factory WebxdcSyncRequest.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory WebxdcSyncRequest.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'WebxdcSyncRequest', + createEmptyInstance: create) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + WebxdcSyncRequest clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + WebxdcSyncRequest copyWith(void Function(WebxdcSyncRequest) updates) => + super.copyWith((message) => updates(message as WebxdcSyncRequest)) + as WebxdcSyncRequest; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static WebxdcSyncRequest create() => WebxdcSyncRequest._(); + @$core.override + WebxdcSyncRequest createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static WebxdcSyncRequest getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static WebxdcSyncRequest? _defaultInstance; +} + +class WebxdcSyncPayload extends $pb.GeneratedMessage { + factory WebxdcSyncPayload({ + $core.Iterable? instances, + }) { + final result = create(); + if (instances != null) result.instances.addAll(instances); + return result; + } + + WebxdcSyncPayload._(); + + factory WebxdcSyncPayload.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory WebxdcSyncPayload.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'WebxdcSyncPayload', + createEmptyInstance: create) + ..pPM(1, _omitFieldNames ? '' : 'instances', + subBuilder: WebxdcSyncInstance.create) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + WebxdcSyncPayload clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + WebxdcSyncPayload copyWith(void Function(WebxdcSyncPayload) updates) => + super.copyWith((message) => updates(message as WebxdcSyncPayload)) + as WebxdcSyncPayload; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static WebxdcSyncPayload create() => WebxdcSyncPayload._(); + @$core.override + WebxdcSyncPayload createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static WebxdcSyncPayload getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static WebxdcSyncPayload? _defaultInstance; + + @$pb.TagNumber(1) + $pb.PbList get instances => $_getList(0); +} + +class WebxdcSyncInstance extends $pb.GeneratedMessage { + factory WebxdcSyncInstance({ + $core.String? instanceId, + $core.String? appId, + $fixnum.Int64? version, + $core.String? summary, + $core.String? document, + $fixnum.Int64? createdAt, + $fixnum.Int64? lastUpdateAt, + $core.Iterable? updates, + }) { + final result = create(); + if (instanceId != null) result.instanceId = instanceId; + if (appId != null) result.appId = appId; + if (version != null) result.version = version; + if (summary != null) result.summary = summary; + if (document != null) result.document = document; + if (createdAt != null) result.createdAt = createdAt; + if (lastUpdateAt != null) result.lastUpdateAt = lastUpdateAt; + if (updates != null) result.updates.addAll(updates); + return result; + } + + WebxdcSyncInstance._(); + + factory WebxdcSyncInstance.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory WebxdcSyncInstance.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'WebxdcSyncInstance', + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'instanceId') + ..aOS(2, _omitFieldNames ? '' : 'appId') + ..aInt64(3, _omitFieldNames ? '' : 'version') + ..aOS(4, _omitFieldNames ? '' : 'summary') + ..aOS(5, _omitFieldNames ? '' : 'document') + ..aInt64(6, _omitFieldNames ? '' : 'createdAt') + ..aInt64(7, _omitFieldNames ? '' : 'lastUpdateAt') + ..pPM(8, _omitFieldNames ? '' : 'updates', + subBuilder: WebxdcSyncUpdate.create) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + WebxdcSyncInstance clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + WebxdcSyncInstance copyWith(void Function(WebxdcSyncInstance) updates) => + super.copyWith((message) => updates(message as WebxdcSyncInstance)) + as WebxdcSyncInstance; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static WebxdcSyncInstance create() => WebxdcSyncInstance._(); + @$core.override + WebxdcSyncInstance createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static WebxdcSyncInstance getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static WebxdcSyncInstance? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get instanceId => $_getSZ(0); + @$pb.TagNumber(1) + set instanceId($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasInstanceId() => $_has(0); + @$pb.TagNumber(1) + void clearInstanceId() => $_clearField(1); + + @$pb.TagNumber(2) + $core.String get appId => $_getSZ(1); + @$pb.TagNumber(2) + set appId($core.String value) => $_setString(1, value); + @$pb.TagNumber(2) + $core.bool hasAppId() => $_has(1); + @$pb.TagNumber(2) + void clearAppId() => $_clearField(2); + + @$pb.TagNumber(3) + $fixnum.Int64 get version => $_getI64(2); + @$pb.TagNumber(3) + set version($fixnum.Int64 value) => $_setInt64(2, value); + @$pb.TagNumber(3) + $core.bool hasVersion() => $_has(2); + @$pb.TagNumber(3) + void clearVersion() => $_clearField(3); + + @$pb.TagNumber(4) + $core.String get summary => $_getSZ(3); + @$pb.TagNumber(4) + set summary($core.String value) => $_setString(3, value); + @$pb.TagNumber(4) + $core.bool hasSummary() => $_has(3); + @$pb.TagNumber(4) + void clearSummary() => $_clearField(4); + + @$pb.TagNumber(5) + $core.String get document => $_getSZ(4); + @$pb.TagNumber(5) + set document($core.String value) => $_setString(4, value); + @$pb.TagNumber(5) + $core.bool hasDocument() => $_has(4); + @$pb.TagNumber(5) + void clearDocument() => $_clearField(5); + + @$pb.TagNumber(6) + $fixnum.Int64 get createdAt => $_getI64(5); + @$pb.TagNumber(6) + set createdAt($fixnum.Int64 value) => $_setInt64(5, value); + @$pb.TagNumber(6) + $core.bool hasCreatedAt() => $_has(5); + @$pb.TagNumber(6) + void clearCreatedAt() => $_clearField(6); + + @$pb.TagNumber(7) + $fixnum.Int64 get lastUpdateAt => $_getI64(6); + @$pb.TagNumber(7) + set lastUpdateAt($fixnum.Int64 value) => $_setInt64(6, value); + @$pb.TagNumber(7) + $core.bool hasLastUpdateAt() => $_has(6); + @$pb.TagNumber(7) + void clearLastUpdateAt() => $_clearField(7); + + @$pb.TagNumber(8) + $pb.PbList get updates => $_getList(7); +} + +class WebxdcSyncUpdate extends $pb.GeneratedMessage { + factory WebxdcSyncUpdate({ + $core.String? messageId, + $fixnum.Int64? senderId, + $core.String? payload, + $core.String? info, + $core.String? href, + $fixnum.Int64? receivedAt, + }) { + final result = create(); + if (messageId != null) result.messageId = messageId; + if (senderId != null) result.senderId = senderId; + if (payload != null) result.payload = payload; + if (info != null) result.info = info; + if (href != null) result.href = href; + if (receivedAt != null) result.receivedAt = receivedAt; + return result; + } + + WebxdcSyncUpdate._(); + + factory WebxdcSyncUpdate.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory WebxdcSyncUpdate.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'WebxdcSyncUpdate', + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'messageId') + ..aInt64(2, _omitFieldNames ? '' : 'senderId') + ..aOS(3, _omitFieldNames ? '' : 'payload') + ..aOS(4, _omitFieldNames ? '' : 'info') + ..aOS(5, _omitFieldNames ? '' : 'href') + ..aInt64(6, _omitFieldNames ? '' : 'receivedAt') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + WebxdcSyncUpdate clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + WebxdcSyncUpdate copyWith(void Function(WebxdcSyncUpdate) updates) => + super.copyWith((message) => updates(message as WebxdcSyncUpdate)) + as WebxdcSyncUpdate; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static WebxdcSyncUpdate create() => WebxdcSyncUpdate._(); + @$core.override + WebxdcSyncUpdate createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static WebxdcSyncUpdate getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static WebxdcSyncUpdate? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get messageId => $_getSZ(0); + @$pb.TagNumber(1) + set messageId($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasMessageId() => $_has(0); + @$pb.TagNumber(1) + void clearMessageId() => $_clearField(1); + + @$pb.TagNumber(2) + $fixnum.Int64 get senderId => $_getI64(1); + @$pb.TagNumber(2) + set senderId($fixnum.Int64 value) => $_setInt64(1, value); + @$pb.TagNumber(2) + $core.bool hasSenderId() => $_has(1); + @$pb.TagNumber(2) + void clearSenderId() => $_clearField(2); + + @$pb.TagNumber(3) + $core.String get payload => $_getSZ(2); + @$pb.TagNumber(3) + set payload($core.String value) => $_setString(2, value); + @$pb.TagNumber(3) + $core.bool hasPayload() => $_has(2); + @$pb.TagNumber(3) + void clearPayload() => $_clearField(3); + + @$pb.TagNumber(4) + $core.String get info => $_getSZ(3); + @$pb.TagNumber(4) + set info($core.String value) => $_setString(3, value); + @$pb.TagNumber(4) + $core.bool hasInfo() => $_has(3); + @$pb.TagNumber(4) + void clearInfo() => $_clearField(4); + + @$pb.TagNumber(5) + $core.String get href => $_getSZ(4); + @$pb.TagNumber(5) + set href($core.String value) => $_setString(4, value); + @$pb.TagNumber(5) + $core.bool hasHref() => $_has(4); + @$pb.TagNumber(5) + void clearHref() => $_clearField(5); + + @$pb.TagNumber(6) + $fixnum.Int64 get receivedAt => $_getI64(5); + @$pb.TagNumber(6) + set receivedAt($fixnum.Int64 value) => $_setInt64(5, value); + @$pb.TagNumber(6) + $core.bool hasReceivedAt() => $_has(5); + @$pb.TagNumber(6) + void clearReceivedAt() => $_clearField(6); } /// Attached to a message a webxdc app asked the user to send, so the chat can @@ -402,6 +868,7 @@ class WebxdcUpdate extends $pb.GeneratedMessage { $core.String? href, $core.String? summary, $core.String? document, + $core.String? notify, }) { final result = create(); if (instanceId != null) result.instanceId = instanceId; @@ -410,6 +877,7 @@ class WebxdcUpdate extends $pb.GeneratedMessage { if (href != null) result.href = href; if (summary != null) result.summary = summary; if (document != null) result.document = document; + if (notify != null) result.notify = notify; return result; } @@ -431,6 +899,7 @@ class WebxdcUpdate extends $pb.GeneratedMessage { ..aOS(4, _omitFieldNames ? '' : 'href') ..aOS(5, _omitFieldNames ? '' : 'summary') ..aOS(6, _omitFieldNames ? '' : 'document') + ..aOS(7, _omitFieldNames ? '' : 'notify') ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -507,6 +976,17 @@ class WebxdcUpdate extends $pb.GeneratedMessage { $core.bool hasDocument() => $_has(5); @$pb.TagNumber(6) void clearDocument() => $_clearField(6); + + /// JSON map of app-scoped member addresses to notification text. + /// Absent preserves legacy broadcast; an empty map notifies nobody. + @$pb.TagNumber(7) + $core.String get notify => $_getSZ(6); + @$pb.TagNumber(7) + set notify($core.String value) => $_setString(6, value); + @$pb.TagNumber(7) + $core.bool hasNotify() => $_has(6); + @$pb.TagNumber(7) + void clearNotify() => $_clearField(7); } const $core.bool _omitFieldNames = diff --git a/lib/src/model/protobuf/client/generated/data.pbenum.dart b/lib/src/model/protobuf/client/generated/data.pbenum.dart index 999e4075..0bf236a8 100644 --- a/lib/src/model/protobuf/client/generated/data.pbenum.dart +++ b/lib/src/model/protobuf/client/generated/data.pbenum.dart @@ -30,6 +30,11 @@ class AdditionalMessageData_Type extends $pb.ProtobufEnum { AdditionalMessageData_Type._(5, _omitEnumNames ? '' : 'WEBXDC_UPDATE'); static const AdditionalMessageData_Type WEBXDC_SENT = AdditionalMessageData_Type._(6, _omitEnumNames ? '' : 'WEBXDC_SENT'); + static const AdditionalMessageData_Type WEBXDC_SYNC = + AdditionalMessageData_Type._(7, _omitEnumNames ? '' : 'WEBXDC_SYNC'); + static const AdditionalMessageData_Type WEBXDC_SYNC_REQUEST = + AdditionalMessageData_Type._( + 8, _omitEnumNames ? '' : 'WEBXDC_SYNC_REQUEST'); static const $core.List values = [ @@ -40,10 +45,12 @@ class AdditionalMessageData_Type extends $pb.ProtobufEnum { WEBXDC_APP, WEBXDC_UPDATE, WEBXDC_SENT, + WEBXDC_SYNC, + WEBXDC_SYNC_REQUEST, ]; static final $core.List _byValue = - $pb.ProtobufEnum.$_initByValueList(values, 6); + $pb.ProtobufEnum.$_initByValueList(values, 8); static AdditionalMessageData_Type? valueOf($core.int value) => value < 0 || value >= _byValue.length ? null : _byValue[value]; diff --git a/lib/src/model/protobuf/client/generated/data.pbjson.dart b/lib/src/model/protobuf/client/generated/data.pbjson.dart index c393bc68..b742fe42 100644 --- a/lib/src/model/protobuf/client/generated/data.pbjson.dart +++ b/lib/src/model/protobuf/client/generated/data.pbjson.dart @@ -106,6 +106,26 @@ const AdditionalMessageData$json = { '10': 'webxdcOrigin', '17': true }, + { + '1': 'webxdc_sync', + '3': 9, + '4': 1, + '5': 11, + '6': '.WebxdcSyncChunk', + '9': 6, + '10': 'webxdcSync', + '17': true + }, + { + '1': 'webxdc_sync_request', + '3': 10, + '4': 1, + '5': 11, + '6': '.WebxdcSyncRequest', + '9': 7, + '10': 'webxdcSyncRequest', + '17': true + }, ], '4': [AdditionalMessageData_Type$json], '8': [ @@ -115,6 +135,8 @@ const AdditionalMessageData$json = { {'1': '_webxdc_app'}, {'1': '_webxdc_update'}, {'1': '_webxdc_origin'}, + {'1': '_webxdc_sync'}, + {'1': '_webxdc_sync_request'}, ], }; @@ -129,6 +151,8 @@ const AdditionalMessageData_Type$json = { {'1': 'WEBXDC_APP', '2': 4}, {'1': 'WEBXDC_UPDATE', '2': 5}, {'1': 'WEBXDC_SENT', '2': 6}, + {'1': 'WEBXDC_SYNC', '2': 7}, + {'1': 'WEBXDC_SYNC_REQUEST', '2': 8}, ], }; @@ -141,11 +165,148 @@ final $typed_data.Uint8List additionalMessageDataDescriptor = $convert.base64Dec 'BSABKANIAlIOYXNrQWJvdXRVc2VySWSIAQESLgoKd2VieGRjX2FwcBgGIAEoCzIKLldlYnhkY0' 'FwcEgDUgl3ZWJ4ZGNBcHCIAQESNwoNd2VieGRjX3VwZGF0ZRgHIAEoCzINLldlYnhkY1VwZGF0' 'ZUgEUgx3ZWJ4ZGNVcGRhdGWIAQESNwoNd2VieGRjX29yaWdpbhgIIAEoCzINLldlYnhkY09yaW' - 'dpbkgFUgx3ZWJ4ZGNPcmlnaW6IAQEiggEKBFR5cGUSCAoETElOSxAAEgwKCENPTlRBQ1RTEAES' - 'GgoWUkVTVE9SRURfRkxBTUVfQ09VTlRFUhACEhIKDkFTS19BQk9VVF9VU0VSEAMSDgoKV0VCWE' - 'RDX0FQUBAEEhEKDVdFQlhEQ19VUERBVEUQBRIPCgtXRUJYRENfU0VOVBAGQgcKBV9saW5rQhkK' - 'F19yZXN0b3JlZF9mbGFtZV9jb3VudGVyQhQKEl9hc2tfYWJvdXRfdXNlcl9pZEINCgtfd2VieG' - 'RjX2FwcEIQCg5fd2VieGRjX3VwZGF0ZUIQCg5fd2VieGRjX29yaWdpbg=='); + 'dpbkgFUgx3ZWJ4ZGNPcmlnaW6IAQESNgoLd2VieGRjX3N5bmMYCSABKAsyEC5XZWJ4ZGNTeW5j' + 'Q2h1bmtIBlIKd2VieGRjU3luY4gBARJHChN3ZWJ4ZGNfc3luY19yZXF1ZXN0GAogASgLMhIuV2' + 'VieGRjU3luY1JlcXVlc3RIB1IRd2VieGRjU3luY1JlcXVlc3SIAQEirAEKBFR5cGUSCAoETElO' + 'SxAAEgwKCENPTlRBQ1RTEAESGgoWUkVTVE9SRURfRkxBTUVfQ09VTlRFUhACEhIKDkFTS19BQk' + '9VVF9VU0VSEAMSDgoKV0VCWERDX0FQUBAEEhEKDVdFQlhEQ19VUERBVEUQBRIPCgtXRUJYRENf' + 'U0VOVBAGEg8KC1dFQlhEQ19TWU5DEAcSFwoTV0VCWERDX1NZTkNfUkVRVUVTVBAIQgcKBV9saW' + '5rQhkKF19yZXN0b3JlZF9mbGFtZV9jb3VudGVyQhQKEl9hc2tfYWJvdXRfdXNlcl9pZEINCgtf' + 'd2VieGRjX2FwcEIQCg5fd2VieGRjX3VwZGF0ZUIQCg5fd2VieGRjX29yaWdpbkIOCgxfd2VieG' + 'RjX3N5bmNCFgoUX3dlYnhkY19zeW5jX3JlcXVlc3Q='); + +@$core.Deprecated('Use webxdcSyncChunkDescriptor instead') +const WebxdcSyncChunk$json = { + '1': 'WebxdcSyncChunk', + '2': [ + {'1': 'transfer_id', '3': 1, '4': 1, '5': 9, '10': 'transferId'}, + {'1': 'chunk_index', '3': 2, '4': 1, '5': 13, '10': 'chunkIndex'}, + {'1': 'chunk_count', '3': 3, '4': 1, '5': 13, '10': 'chunkCount'}, + {'1': 'payload_sha256', '3': 4, '4': 1, '5': 12, '10': 'payloadSha256'}, + {'1': 'payload', '3': 5, '4': 1, '5': 12, '10': 'payload'}, + ], +}; + +/// Descriptor for `WebxdcSyncChunk`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List webxdcSyncChunkDescriptor = $convert.base64Decode( + 'Cg9XZWJ4ZGNTeW5jQ2h1bmsSHwoLdHJhbnNmZXJfaWQYASABKAlSCnRyYW5zZmVySWQSHwoLY2' + 'h1bmtfaW5kZXgYAiABKA1SCmNodW5rSW5kZXgSHwoLY2h1bmtfY291bnQYAyABKA1SCmNodW5r' + 'Q291bnQSJQoOcGF5bG9hZF9zaGEyNTYYBCABKAxSDXBheWxvYWRTaGEyNTYSGAoHcGF5bG9hZB' + 'gFIAEoDFIHcGF5bG9hZA=='); + +@$core.Deprecated('Use webxdcSyncRequestDescriptor instead') +const WebxdcSyncRequest$json = { + '1': 'WebxdcSyncRequest', +}; + +/// Descriptor for `WebxdcSyncRequest`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List webxdcSyncRequestDescriptor = + $convert.base64Decode('ChFXZWJ4ZGNTeW5jUmVxdWVzdA=='); + +@$core.Deprecated('Use webxdcSyncPayloadDescriptor instead') +const WebxdcSyncPayload$json = { + '1': 'WebxdcSyncPayload', + '2': [ + { + '1': 'instances', + '3': 1, + '4': 3, + '5': 11, + '6': '.WebxdcSyncInstance', + '10': 'instances' + }, + ], +}; + +/// Descriptor for `WebxdcSyncPayload`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List webxdcSyncPayloadDescriptor = $convert.base64Decode( + 'ChFXZWJ4ZGNTeW5jUGF5bG9hZBIxCglpbnN0YW5jZXMYASADKAsyEy5XZWJ4ZGNTeW5jSW5zdG' + 'FuY2VSCWluc3RhbmNlcw=='); + +@$core.Deprecated('Use webxdcSyncInstanceDescriptor instead') +const WebxdcSyncInstance$json = { + '1': 'WebxdcSyncInstance', + '2': [ + {'1': 'instance_id', '3': 1, '4': 1, '5': 9, '10': 'instanceId'}, + {'1': 'app_id', '3': 2, '4': 1, '5': 9, '10': 'appId'}, + {'1': 'version', '3': 3, '4': 1, '5': 3, '10': 'version'}, + { + '1': 'summary', + '3': 4, + '4': 1, + '5': 9, + '9': 0, + '10': 'summary', + '17': true + }, + { + '1': 'document', + '3': 5, + '4': 1, + '5': 9, + '9': 1, + '10': 'document', + '17': true + }, + {'1': 'created_at', '3': 6, '4': 1, '5': 3, '10': 'createdAt'}, + {'1': 'last_update_at', '3': 7, '4': 1, '5': 3, '10': 'lastUpdateAt'}, + { + '1': 'updates', + '3': 8, + '4': 3, + '5': 11, + '6': '.WebxdcSyncUpdate', + '10': 'updates' + }, + ], + '8': [ + {'1': '_summary'}, + {'1': '_document'}, + ], +}; + +/// Descriptor for `WebxdcSyncInstance`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List webxdcSyncInstanceDescriptor = $convert.base64Decode( + 'ChJXZWJ4ZGNTeW5jSW5zdGFuY2USHwoLaW5zdGFuY2VfaWQYASABKAlSCmluc3RhbmNlSWQSFQ' + 'oGYXBwX2lkGAIgASgJUgVhcHBJZBIYCgd2ZXJzaW9uGAMgASgDUgd2ZXJzaW9uEh0KB3N1bW1h' + 'cnkYBCABKAlIAFIHc3VtbWFyeYgBARIfCghkb2N1bWVudBgFIAEoCUgBUghkb2N1bWVudIgBAR' + 'IdCgpjcmVhdGVkX2F0GAYgASgDUgljcmVhdGVkQXQSJAoObGFzdF91cGRhdGVfYXQYByABKANS' + 'DGxhc3RVcGRhdGVBdBIrCgd1cGRhdGVzGAggAygLMhEuV2VieGRjU3luY1VwZGF0ZVIHdXBkYX' + 'Rlc0IKCghfc3VtbWFyeUILCglfZG9jdW1lbnQ='); + +@$core.Deprecated('Use webxdcSyncUpdateDescriptor instead') +const WebxdcSyncUpdate$json = { + '1': 'WebxdcSyncUpdate', + '2': [ + {'1': 'message_id', '3': 1, '4': 1, '5': 9, '10': 'messageId'}, + { + '1': 'sender_id', + '3': 2, + '4': 1, + '5': 3, + '9': 0, + '10': 'senderId', + '17': true + }, + {'1': 'payload', '3': 3, '4': 1, '5': 9, '10': 'payload'}, + {'1': 'info', '3': 4, '4': 1, '5': 9, '9': 1, '10': 'info', '17': true}, + {'1': 'href', '3': 5, '4': 1, '5': 9, '9': 2, '10': 'href', '17': true}, + {'1': 'received_at', '3': 6, '4': 1, '5': 3, '10': 'receivedAt'}, + ], + '8': [ + {'1': '_sender_id'}, + {'1': '_info'}, + {'1': '_href'}, + ], +}; + +/// Descriptor for `WebxdcSyncUpdate`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List webxdcSyncUpdateDescriptor = $convert.base64Decode( + 'ChBXZWJ4ZGNTeW5jVXBkYXRlEh0KCm1lc3NhZ2VfaWQYASABKAlSCW1lc3NhZ2VJZBIgCglzZW' + '5kZXJfaWQYAiABKANIAFIIc2VuZGVySWSIAQESGAoHcGF5bG9hZBgDIAEoCVIHcGF5bG9hZBIX' + 'CgRpbmZvGAQgASgJSAFSBGluZm+IAQESFwoEaHJlZhgFIAEoCUgCUgRocmVmiAEBEh8KC3JlY2' + 'VpdmVkX2F0GAYgASgDUgpyZWNlaXZlZEF0QgwKCl9zZW5kZXJfaWRCBwoFX2luZm9CBwoFX2hy' + 'ZWY='); @$core.Deprecated('Use webxdcOriginDescriptor instead') const WebxdcOrigin$json = { @@ -202,12 +363,14 @@ const WebxdcUpdate$json = { '10': 'document', '17': true }, + {'1': 'notify', '3': 7, '4': 1, '5': 9, '9': 4, '10': 'notify', '17': true}, ], '8': [ {'1': '_info'}, {'1': '_href'}, {'1': '_summary'}, {'1': '_document'}, + {'1': '_notify'}, ], }; @@ -216,5 +379,5 @@ final $typed_data.Uint8List webxdcUpdateDescriptor = $convert.base64Decode( 'CgxXZWJ4ZGNVcGRhdGUSHwoLaW5zdGFuY2VfaWQYASABKAlSCmluc3RhbmNlSWQSGAoHcGF5bG' '9hZBgCIAEoCVIHcGF5bG9hZBIXCgRpbmZvGAMgASgJSABSBGluZm+IAQESFwoEaHJlZhgEIAEo' 'CUgBUgRocmVmiAEBEh0KB3N1bW1hcnkYBSABKAlIAlIHc3VtbWFyeYgBARIfCghkb2N1bWVudB' - 'gGIAEoCUgDUghkb2N1bWVudIgBAUIHCgVfaW5mb0IHCgVfaHJlZkIKCghfc3VtbWFyeUILCglf' - 'ZG9jdW1lbnQ='); + 'gGIAEoCUgDUghkb2N1bWVudIgBARIbCgZub3RpZnkYByABKAlIBFIGbm90aWZ5iAEBQgcKBV9p' + 'bmZvQgcKBV9ocmVmQgoKCF9zdW1tYXJ5QgsKCV9kb2N1bWVudEIJCgdfbm90aWZ5'); diff --git a/lib/src/services/webxdc/webxdc.service.dart b/lib/src/services/webxdc/webxdc.service.dart index 0c28a3b6..3c19eed9 100644 --- a/lib/src/services/webxdc/webxdc.service.dart +++ b/lib/src/services/webxdc/webxdc.service.dart @@ -46,16 +46,32 @@ class WebxdcService { /// cached with all its translations, so choosing one costs nothing and is /// never sent anywhere. static Future> catalog( + String groupId, List languages, ) async { try { - return await rust_webxdc.catalog(languages: languages); + return await rust_webxdc.catalog(groupId: groupId, languages: languages); } catch (error) { Log.warn('reading the webxdc catalog failed: $error'); return []; } } + static Future> oneTimeInstances( + String groupId, + List languages, + ) async { + try { + return await rust_webxdc.oneTimeInstances( + groupId: groupId, + languages: languages, + ); + } catch (error) { + Log.warn('reading one-time webxdc apps for $groupId failed: $error'); + return []; + } + } + /// Places an app into a chat. Returns the id of the message carrying it, /// which is also the instance id. static Future createInstance( @@ -77,10 +93,8 @@ class WebxdcService { /// Gets the bundle an instance runs onto disk, verified. /// - /// Rust checks the store for a newer version first and moves the instance to - /// it if there is one, so an app updates between two runs and never during - /// one. Called when the user starts an app and never on message arrival: a - /// message must not be able to make a device fetch anything. + /// Opens cached code without a network update check. Only a bundle that + /// has not been downloaded yet needs the server. static Future prepareBundle(String instanceId) async { try { return await rust_webxdc.prepareBundle(instanceId: instanceId); @@ -90,6 +104,20 @@ class WebxdcService { } } + static final Set _updatingInstances = {}; + + /// Best effort after closing. A failed check leaves the existing code usable. + static Future cacheUpdate(String instanceId) async { + if (!_updatingInstances.add(instanceId)) return; + try { + await rust_webxdc.cacheUpdate(instanceId: instanceId); + } catch (error) { + Log.warn('checking for a webxdc update after closing failed: $error'); + } finally { + _updatingInstances.remove(instanceId); + } + } + static Future instance( String instanceId, ) async { @@ -126,6 +154,7 @@ class WebxdcService { String? href, String? summary, String? document, + String? notify, }) => rust_webxdc.sendUpdate( instanceId: instanceId, payload: payload, @@ -133,6 +162,7 @@ class WebxdcService { href: href, summary: summary, document: document, + notify: notify, ); /// Removes an instance, its whole update log, and the web storage its origin diff --git a/lib/src/services/webxdc/webxdc_host.dart b/lib/src/services/webxdc/webxdc_host.dart index b139076a..d7857e0b 100644 --- a/lib/src/services/webxdc/webxdc_host.dart +++ b/lib/src/services/webxdc/webxdc_host.dart @@ -90,8 +90,7 @@ class WebxdcHost { /// Gets an app ready to run: makes sure the bundle is on disk and verified, /// and returns what the view needs to show it. /// - /// Rust checks the store for a newer version as part of this, so an app - /// updates here and nowhere else. Returns a failure instead when the bundle + /// Cached apps open without a network update check. Returns a failure when the bundle /// cannot be had at all -- a first start with no network, or an app that was /// taken out of the store before this device ever downloaded it. /// `languages` is what the reader prefers, most preferred first: the title @@ -104,8 +103,7 @@ class WebxdcHost { if (instance == null) { return const WebxdcLaunch.failed('unavailable'); } - // The only place a bundle is ever fetched. A message arriving in a chat - // never reaches this. + // A first launch may need a download. Existing bundles are read locally. final bundlePath = await WebxdcService.prepareBundle(instanceId); if (bundlePath == null) { return const WebxdcLaunch.failed('unavailable'); @@ -148,6 +146,8 @@ class WebxdcHost { _updateSubscription = null; _openInstanceId = null; _closeView = null; + // Closing never waits for the server; downloaded code is used next time. + unawaited(WebxdcService.cacheUpdate(instanceId)); } static Future _clearOrigin(String origin) async { @@ -222,6 +222,10 @@ class WebxdcHost { final params = (message['params'] as Map?) ?? {}; try { switch (message['method']) { + case 'getMembers': + final members = await rust_webxdc.members(instanceId: instanceId); + return jsonEncode({'id': id, 'result': jsonDecode(members)}); + case 'sendUpdate': // `payload` is re-encoded rather than passed through, so what is // stored and sent is JSON this side produced. @@ -232,6 +236,9 @@ class WebxdcHost { href: params['href'] as String?, summary: params['summary'] as String?, document: params['document'] as String?, + notify: params['notify'] == null + ? null + : jsonEncode(params['notify']), ); return jsonEncode({'id': id, 'result': null}); diff --git a/lib/src/visual/context_menu/group.context_menu.dart b/lib/src/visual/context_menu/group.context_menu.dart index 76449ada..a2a03534 100644 --- a/lib/src/visual/context_menu/group.context_menu.dart +++ b/lib/src/visual/context_menu/group.context_menu.dart @@ -45,11 +45,10 @@ class GroupContextMenu extends StatelessWidget { ), ContextMenuItem( title: context.lang.contextMenuOpenChat, - onTap: () => - navigator.context.push( - Routes.chatsMessages(group.groupId), - extra: group, - ), + onTap: () => navigator.context.push( + Routes.chatsMessages(group.groupId), + extra: group, + ), icon: FontAwesomeIcons.comments, ), if (!group.archived) @@ -76,17 +75,6 @@ class GroupContextMenu extends StatelessWidget { ); if (ok) { await twonlyDB.messagesDao.deleteMessagesByGroupId(group.groupId); - if (group.isDirectChat) { - await twonlyDB.groupsDao.deleteGroup(group.groupId); - } else { - await twonlyDB.groupsDao.deleteGroup(group.groupId); - // await twonlyDB.groupsDao.updateGroup( - // group.groupId, - // const GroupsCompanion( - // deletedContent: Value(true), - // ), - // ); - } } }, ), diff --git a/lib/src/visual/elements/pro_badge.element.dart b/lib/src/visual/elements/pro_badge.element.dart new file mode 100644 index 00000000..97b02947 --- /dev/null +++ b/lib/src/visual/elements/pro_badge.element.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/src/utils/misc.dart'; + +class ProBadge extends StatelessWidget { + const ProBadge({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: Colors.amber.shade700, + borderRadius: BorderRadius.circular(6), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const FaIcon( + FontAwesomeIcons.star, + size: 10, + color: Colors.white, + ), + const SizedBox(width: 4), + Text( + context.lang.backupCloudProBadge, + style: const TextStyle( + fontSize: 10, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ); + } +} diff --git a/lib/src/visual/views/chats/chat_messages_components/bottom_sheets/webxdc_store.bottom_sheet.dart b/lib/src/visual/views/chats/chat_messages_components/bottom_sheets/webxdc_store.bottom_sheet.dart index 7c55c5e3..f7ce4c95 100644 --- a/lib/src/visual/views/chats/chat_messages_components/bottom_sheets/webxdc_store.bottom_sheet.dart +++ b/lib/src/visual/views/chats/chat_messages_components/bottom_sheets/webxdc_store.bottom_sheet.dart @@ -2,11 +2,17 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; import 'package:twonly/core/bridge/webxdc.dart' as rust_webxdc; import 'package:twonly/locator.dart'; +import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/providers/purchases.provider.dart'; +import 'package:twonly/src/services/subscription.service.dart'; import 'package:twonly/src/services/webxdc/webxdc.service.dart'; import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/elements/pro_badge.element.dart'; /// The in-app store. /// @@ -44,11 +50,13 @@ class _WebxdcStoreViewState extends State { // The cached listing is shown first so the sheet is never empty while the // refresh is in flight. final languages = [language]; - final cached = await WebxdcService.catalog(languages); + final cached = await WebxdcService.catalog(widget.group.groupId, languages); if (mounted) setState(() => _apps = cached); final refreshed = await WebxdcService.refreshCatalog(); - final apps = refreshed ? await WebxdcService.catalog(languages) : cached; + final apps = refreshed + ? await WebxdcService.catalog(widget.group.groupId, languages) + : cached; if (!mounted) return; setState(() { _apps = apps; @@ -57,6 +65,10 @@ class _WebxdcStoreViewState extends State { } Future _place(rust_webxdc.WebxdcStoreApp app) async { + if (app.proOnly && !isPayingUser(context.read().plan)) { + await context.push(Routes.settingsSubscription); + return; + } setState(() => _placing = app.appId); final instanceId = await WebxdcService.createInstance( widget.group.groupId, @@ -140,44 +152,70 @@ class _WebxdcStoreViewState extends State { ); } - return ListView.builder( - controller: controller, - itemCount: apps.length, - itemBuilder: (context, index) { - final app = apps[index]; - final icon = app.icon; - // What the app says about itself, in the reader's language, as the - // store published it. Plain text, bounded and stripped of control - // characters before it was ever accepted for publishing. - final subtitle = userService.currentUser.isDeveloper - ? ['v${app.version}', ?app.description].join(' · ') - : app.description; + final available = apps + .where((app) => !app.oneTime || app.instanceId == null) + .toList(); + final alreadyAdded = apps + .where((app) => app.oneTime && app.instanceId != null) + .toList(); - return ListTile( - leading: SizedBox( - width: 40, - height: 40, - child: icon == null - ? const FaIcon(FontAwesomeIcons.puzzlePiece) - : ClipRRect( - borderRadius: BorderRadius.circular(8), - child: Image.memory(icon, fit: BoxFit.cover), - ), + return ListView( + controller: controller, + children: [ + ...available.map(_appTile), + if (alreadyAdded.isNotEmpty) ...[ + Padding( + padding: const EdgeInsets.fromLTRB(20, 18, 20, 6), + child: Text( + context.lang.webxdcStoreAlreadyAdded, + style: Theme.of(context).textTheme.labelMedium?.copyWith( + color: context.color.onSurfaceVariant, + fontWeight: FontWeight.w600, + ), + ), ), - title: Text(app.name), - subtitle: subtitle == null - ? null - : Text(subtitle, maxLines: 2, overflow: TextOverflow.ellipsis), - trailing: _placing == app.appId - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : null, - onTap: _placing == null ? () => _place(app) : null, - ); - }, + ...alreadyAdded.map( + (app) => + Opacity(opacity: 0.45, child: _appTile(app, disabled: true)), + ), + ], + ], + ); + } + + Widget _appTile( + rust_webxdc.WebxdcStoreApp app, { + bool disabled = false, + }) { + final icon = app.icon; + final subtitle = userService.currentUser.isDeveloper + ? ['v${app.version}', ?app.description].join(' · ') + : app.description; + return ListTile( + leading: SizedBox( + width: 40, + height: 40, + child: icon == null + ? const FaIcon(FontAwesomeIcons.puzzlePiece) + : ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.memory(icon, fit: BoxFit.cover), + ), + ), + title: Text(app.name), + subtitle: subtitle == null + ? null + : Text(subtitle, maxLines: 2, overflow: TextOverflow.ellipsis), + trailing: _placing == app.appId + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : app.proOnly + ? const ProBadge() + : null, + onTap: !disabled && _placing == null ? () => _place(app) : null, ); } } diff --git a/lib/src/visual/views/contact/contact.view.dart b/lib/src/visual/views/contact/contact.view.dart index f2d147cf..2e10af78 100644 --- a/lib/src/visual/views/contact/contact.view.dart +++ b/lib/src/visual/views/contact/contact.view.dart @@ -24,6 +24,7 @@ import 'package:twonly/src/visual/views/contact/contact_components/user_discover import 'package:twonly/src/visual/views/contact/contact_components/verification_expansion_tile.comp.dart'; import 'package:twonly/src/visual/views/contact/select_contact_groups.view.dart'; import 'package:twonly/src/visual/views/groups/group.view.dart'; +import 'package:twonly/src/visual/views/webxdc/webxdc_chat_apps.view.dart'; class ContactView extends StatefulWidget { const ContactView(this.userId, {super.key}); @@ -233,6 +234,18 @@ class _ContactViewState extends State { userService.currentUser.userId, ), ), + BetterListTile( + icon: FontAwesomeIcons.layerGroup, + text: context.lang.webxdcChatAppsMenu, + onTap: () => context.navPush( + WebxdcChatAppsView( + groupId: getUUIDforDirectChat( + widget.userId, + userService.currentUser.userId, + ), + ), + ), + ), ContactGroupsSubtitleBuilder( userId: contact.userId, builder: (context, subtitleWidget) { diff --git a/lib/src/visual/views/groups/group.view.dart b/lib/src/visual/views/groups/group.view.dart index 151383e7..e8eb9a58 100644 --- a/lib/src/visual/views/groups/group.view.dart +++ b/lib/src/visual/views/groups/group.view.dart @@ -23,6 +23,7 @@ import 'package:twonly/src/visual/views/contact/select_contact_groups.view.dart' import 'package:twonly/src/visual/views/groups/group_create_select_members.view.dart'; import 'package:twonly/src/visual/views/groups/group_member.context.dart'; import 'package:twonly/src/visual/views/settings/profile/profile.view.dart'; +import 'package:twonly/src/visual/views/webxdc/webxdc_chat_apps.view.dart'; class GroupView extends StatefulWidget { const GroupView(this.groupId, {super.key}); @@ -213,6 +214,13 @@ class _GroupViewState extends State { groupId: widget.groupId, disabled: !_group!.isGroupAdmin, ), + BetterListTile( + icon: FontAwesomeIcons.layerGroup, + text: context.lang.webxdcChatAppsMenu, + onTap: () => context.navPush( + WebxdcChatAppsView(groupId: widget.groupId), + ), + ), ContactGroupsSubtitleBuilder( groupId: widget.groupId, builder: (context, subtitleWidget) { diff --git a/lib/src/visual/views/memories/components/cloud_backup_promo.comp.dart b/lib/src/visual/views/memories/components/cloud_backup_promo.comp.dart index 65534d27..fbc50e7c 100644 --- a/lib/src/visual/views/memories/components/cloud_backup_promo.comp.dart +++ b/lib/src/visual/views/memories/components/cloud_backup_promo.comp.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; import 'package:twonly/locator.dart'; @@ -12,6 +11,7 @@ import 'package:twonly/src/services/subscription.service.dart'; import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/elements/my_button.element.dart'; +import 'package:twonly/src/visual/elements/pro_badge.element.dart'; class MemoriesCloudBackupPromoComp extends StatelessWidget { const MemoriesCloudBackupPromoComp({super.key}); @@ -75,35 +75,7 @@ class MemoriesCloudBackupPromoComp extends StatelessWidget { ), ), const SizedBox(width: 8), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 6, - vertical: 2, - ), - decoration: BoxDecoration( - color: Colors.amber.shade700, - borderRadius: BorderRadius.circular(6), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const FaIcon( - FontAwesomeIcons.star, - size: 10, - color: Colors.white, - ), - const SizedBox(width: 4), - Text( - context.lang.backupCloudProBadge, - style: const TextStyle( - fontSize: 10, - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ), + const ProBadge(), ], ), const SizedBox(height: 6), diff --git a/lib/src/visual/views/settings/backup/backup_settings.view.dart b/lib/src/visual/views/settings/backup/backup_settings.view.dart index f963a766..140b1a32 100644 --- a/lib/src/visual/views/settings/backup/backup_settings.view.dart +++ b/lib/src/visual/views/settings/backup/backup_settings.view.dart @@ -15,6 +15,7 @@ import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/elements/better_list_title.element.dart'; import 'package:twonly/src/visual/elements/my_button.element.dart'; +import 'package:twonly/src/visual/elements/pro_badge.element.dart'; import 'package:twonly/src/visual/themes/light.dart'; import 'package:twonly/src/visual/views/settings/backup/backup_utils.dart'; import 'package:twonly/src/visual/views/settings/backup/components/recovery_card.comp.dart'; @@ -206,39 +207,12 @@ class _BackupViewState extends State { : '-')), ), trailing: isFreePlan - ? Row( + ? const Row( mainAxisSize: MainAxisSize.min, children: [ - Container( - padding: const EdgeInsets.symmetric( - horizontal: 6, - vertical: 2, - ), - decoration: BoxDecoration( - color: Colors.amber.shade700, - borderRadius: BorderRadius.circular(4), - ), - child: Row( - children: [ - const FaIcon( - FontAwesomeIcons.star, - size: 10, - color: Colors.white, - ), - const SizedBox(width: 4), - Text( - context.lang.backupCloudProBadge, - style: const TextStyle( - fontSize: 10, - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ), - const SizedBox(width: 8), - const Switch( + ProBadge(), + SizedBox(width: 8), + Switch( value: false, onChanged: null, ), diff --git a/lib/src/visual/views/settings/subscription/subscription.view.dart b/lib/src/visual/views/settings/subscription/subscription.view.dart index 96337785..8de8655b 100644 --- a/lib/src/visual/views/settings/subscription/subscription.view.dart +++ b/lib/src/visual/views/settings/subscription/subscription.view.dart @@ -376,7 +376,7 @@ class _PlanCardState extends State { context.lang.proFeature2, context.lang.proFeature3, context.lang.proFeature4, - // context.lang.proFeature4, + context.lang.subscriptionAllApps, ]; case 'Family': features = [ @@ -384,6 +384,7 @@ class _PlanCardState extends State { context.lang.familyFeature2, context.lang.familyFeature3, context.lang.familyFeature4, + context.lang.subscriptionAllApps, ]; default: } diff --git a/lib/src/visual/views/webxdc/webxdc_chat_apps.view.dart b/lib/src/visual/views/webxdc/webxdc_chat_apps.view.dart new file mode 100644 index 00000000..d0f8f6d2 --- /dev/null +++ b/lib/src/visual/views/webxdc/webxdc_chat_apps.view.dart @@ -0,0 +1,112 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/core/bridge/webxdc.dart' as rust_webxdc; +import 'package:twonly/src/services/webxdc/webxdc.service.dart'; +import 'package:twonly/src/services/webxdc/webxdc_host.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/common.dart'; +import 'package:twonly/src/visual/views/webxdc/webxdc_app.view.dart'; + +/// One-time apps already attached to a chat, kept close to its profile. +class WebxdcChatAppsView extends StatefulWidget { + const WebxdcChatAppsView({required this.groupId, super.key}); + + final String groupId; + + @override + State createState() => _WebxdcChatAppsViewState(); +} + +class _WebxdcChatAppsViewState extends State { + List? _apps; + bool _loading = false; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (_loading) return; + _loading = true; + _load(); + } + + Future _load() async { + final apps = await WebxdcService.oneTimeInstances( + widget.groupId, + readerLanguages(context), + ); + if (mounted) { + setState(() { + _apps = apps; + _loading = false; + }); + } + } + + Future _open(rust_webxdc.WebxdcOneTimeInstance app) async { + final messenger = ScaffoldMessenger.of(context); + final unavailable = context.lang.webxdcUnavailable; + final launch = await WebxdcHost.prepare( + app.instanceId, + readerLanguages(context), + ); + if (launch.failure != null) { + messenger.showSnackBar(SnackBar(content: Text(unavailable))); + return; + } + if (!mounted) return; + await context.navPush(WebxdcAppView(launch: launch)); + await _load(); + } + + @override + Widget build(BuildContext context) { + final apps = _apps; + return Scaffold( + appBar: AppBar(title: Text(context.lang.webxdcChatAppsTitle)), + body: apps == null + ? const Center(child: CircularProgressIndicator()) + : apps.isEmpty + ? Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Text( + context.lang.webxdcChatAppsEmpty, + textAlign: TextAlign.center, + ), + ), + ) + : ListView.builder( + padding: const EdgeInsets.symmetric(vertical: 8), + itemCount: apps.length, + itemBuilder: (context, index) { + final app = apps[index]; + final subtitle = app.document ?? app.summary; + return ListTile( + leading: SizedBox( + width: 44, + height: 44, + child: app.icon == null + ? const Center( + child: FaIcon(FontAwesomeIcons.puzzlePiece), + ) + : ClipRRect( + borderRadius: BorderRadius.circular(9), + child: Image.memory(app.icon!, fit: BoxFit.cover), + ), + ), + title: Text(app.name), + subtitle: subtitle == null + ? null + : Text( + subtitle, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + trailing: const Icon(Icons.chevron_right), + onTap: () => _open(app), + ); + }, + ), + ); + } +} diff --git a/rust/.sqlx/query-b71a58e0499f8b69debddba47cf1ec86c4b99309441a4506e998beedfcead834.json b/rust/.sqlx/query-1eb6b6e8e4ed43f43c3e6c570ffad96638f7c02d4baa4bdea006ea2a439ec24f.json similarity index 55% rename from rust/.sqlx/query-b71a58e0499f8b69debddba47cf1ec86c4b99309441a4506e998beedfcead834.json rename to rust/.sqlx/query-1eb6b6e8e4ed43f43c3e6c570ffad96638f7c02d4baa4bdea006ea2a439ec24f.json index b18b769a..2ba08135 100644 --- a/rust/.sqlx/query-b71a58e0499f8b69debddba47cf1ec86c4b99309441a4506e998beedfcead834.json +++ b/rust/.sqlx/query-1eb6b6e8e4ed43f43c3e6c570ffad96638f7c02d4baa4bdea006ea2a439ec24f.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT app_id AS \"app_id!: String\", version AS \"version!: i64\",\n name AS \"name!: String\",\n name_translations AS \"name_translations!: String\",\n source_code_url AS \"source_code_url: String\",\n description AS \"description!: String\",\n icon AS \"icon: Vec\", bundle_bytes AS \"bundle_bytes!: i64\"\n FROM webxdc_apps\n WHERE published = 1\n AND version = (SELECT MAX(version) FROM webxdc_apps AS newer\n WHERE newer.app_id = webxdc_apps.app_id AND newer.published = 1)\n ORDER BY name ASC", + "query": "SELECT app_id AS \"app_id!: String\", version AS \"version!: i64\",\n name AS \"name!: String\",\n pro_only AS \"pro_only!: bool\",\n one_time AS \"one_time!: bool\",\n (SELECT instance_id FROM webxdc_instances\n WHERE webxdc_instances.group_id = ?\n AND webxdc_instances.app_id = webxdc_apps.app_id\n ORDER BY created_at ASC LIMIT 1) AS \"instance_id: String\",\n name_translations AS \"name_translations!: String\",\n source_code_url AS \"source_code_url: String\",\n description AS \"description!: String\",\n icon AS \"icon: Vec\", bundle_bytes AS \"bundle_bytes!: i64\"\n FROM webxdc_apps\n WHERE published = 1\n AND version = (SELECT MAX(version) FROM webxdc_apps AS newer\n WHERE newer.app_id = webxdc_apps.app_id AND newer.published = 1)\n ORDER BY sort_order ASC, name ASC, app_id ASC", "describe": { "columns": [ { @@ -37,8 +37,41 @@ } }, { - "name": "name_translations!: String", + "name": "pro_only!: bool", "ordinal": 3, + "type_info": "Bool", + "origin": { + "Table": { + "table": "webxdc_apps", + "name": "pro_only" + } + } + }, + { + "name": "one_time!: bool", + "ordinal": 4, + "type_info": "Bool", + "origin": { + "Table": { + "table": "webxdc_apps", + "name": "one_time" + } + } + }, + { + "name": "instance_id: String", + "ordinal": 5, + "type_info": "Text", + "origin": { + "Table": { + "table": "webxdc_instances", + "name": "instance_id" + } + } + }, + { + "name": "name_translations!: String", + "ordinal": 6, "type_info": "Text", "origin": { "Table": { @@ -49,7 +82,7 @@ }, { "name": "source_code_url: String", - "ordinal": 4, + "ordinal": 7, "type_info": "Text", "origin": { "Table": { @@ -60,7 +93,7 @@ }, { "name": "description!: String", - "ordinal": 5, + "ordinal": 8, "type_info": "Text", "origin": { "Table": { @@ -71,7 +104,7 @@ }, { "name": "icon: Vec", - "ordinal": 6, + "ordinal": 9, "type_info": "Blob", "origin": { "Table": { @@ -82,7 +115,7 @@ }, { "name": "bundle_bytes!: i64", - "ordinal": 7, + "ordinal": 10, "type_info": "Integer", "origin": { "Table": { @@ -93,18 +126,21 @@ } ], "parameters": { - "Right": 0 + "Right": 1 }, "nullable": [ false, false, false, false, + false, + true, + false, true, false, true, false ] }, - "hash": "b71a58e0499f8b69debddba47cf1ec86c4b99309441a4506e998beedfcead834" + "hash": "1eb6b6e8e4ed43f43c3e6c570ffad96638f7c02d4baa4bdea006ea2a439ec24f" } diff --git a/rust/.sqlx/query-3d1fdb8baa14a261fd2e2943ce5712ed95f12e337e88c18619aed086ebbd42aa.json b/rust/.sqlx/query-3d1fdb8baa14a261fd2e2943ce5712ed95f12e337e88c18619aed086ebbd42aa.json deleted file mode 100644 index e84cb676..00000000 --- a/rust/.sqlx/query-3d1fdb8baa14a261fd2e2943ce5712ed95f12e337e88c18619aed086ebbd42aa.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n UPDATE groups\n SET last_message_exchange = MAX(last_message_exchange, ?)\n WHERE group_id = ?\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 2 - }, - "nullable": [] - }, - "hash": "3d1fdb8baa14a261fd2e2943ce5712ed95f12e337e88c18619aed086ebbd42aa" -} diff --git a/rust/.sqlx/query-3e9671207184c4df6b3d3c96bf4201160e557039a18115c4aa1737a80ebb5153.json b/rust/.sqlx/query-3e9671207184c4df6b3d3c96bf4201160e557039a18115c4aa1737a80ebb5153.json new file mode 100644 index 00000000..dcdd796a --- /dev/null +++ b/rust/.sqlx/query-3e9671207184c4df6b3d3c96bf4201160e557039a18115c4aa1737a80ebb5153.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n UPDATE groups\n SET last_message_exchange = MAX(last_message_exchange, ?),\n deleted_content = 0\n WHERE group_id = ?\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "3e9671207184c4df6b3d3c96bf4201160e557039a18115c4aa1737a80ebb5153" +} diff --git a/rust/.sqlx/query-7a89aa45a3186a9a4c712976e21d7a4e3c289b449bd5ce432845cbdc02b234a6.json b/rust/.sqlx/query-7a89aa45a3186a9a4c712976e21d7a4e3c289b449bd5ce432845cbdc02b234a6.json new file mode 100644 index 00000000..9d4e1c1f --- /dev/null +++ b/rust/.sqlx/query-7a89aa45a3186a9a4c712976e21d7a4e3c289b449bd5ce432845cbdc02b234a6.json @@ -0,0 +1,86 @@ +{ + "db_name": "SQLite", + "query": "SELECT instance.instance_id AS \"instance_id!: String\",\n app.name AS \"name!: String\",\n app.name_translations AS \"name_translations!: String\",\n app.icon AS \"icon: Vec\",\n instance.summary AS \"summary: String\",\n instance.document AS \"document: String\"\n FROM webxdc_instances AS instance\n JOIN webxdc_apps AS app\n ON app.app_id = instance.app_id AND app.version = instance.version\n WHERE instance.group_id = ? AND app.one_time = 1\n ORDER BY instance.last_update_at DESC, instance.created_at DESC", + "describe": { + "columns": [ + { + "name": "instance_id!: String", + "ordinal": 0, + "type_info": "Text", + "origin": { + "Table": { + "table": "webxdc_instances", + "name": "instance_id" + } + } + }, + { + "name": "name!: String", + "ordinal": 1, + "type_info": "Text", + "origin": { + "Table": { + "table": "webxdc_apps", + "name": "name" + } + } + }, + { + "name": "name_translations!: String", + "ordinal": 2, + "type_info": "Text", + "origin": { + "Table": { + "table": "webxdc_apps", + "name": "name_translations" + } + } + }, + { + "name": "icon: Vec", + "ordinal": 3, + "type_info": "Blob", + "origin": { + "Table": { + "table": "webxdc_apps", + "name": "icon" + } + } + }, + { + "name": "summary: String", + "ordinal": 4, + "type_info": "Text", + "origin": { + "Table": { + "table": "webxdc_instances", + "name": "summary" + } + } + }, + { + "name": "document: String", + "ordinal": 5, + "type_info": "Text", + "origin": { + "Table": { + "table": "webxdc_instances", + "name": "document" + } + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + true, + true, + true + ] + }, + "hash": "7a89aa45a3186a9a4c712976e21d7a4e3c289b449bd5ce432845cbdc02b234a6" +} diff --git a/rust/.sqlx/query-cc6625aa285f9e6dcdc1a73d7bfb6ad23cff6c8932f61b48645c431dd7adaf48.json b/rust/.sqlx/query-cc6625aa285f9e6dcdc1a73d7bfb6ad23cff6c8932f61b48645c431dd7adaf48.json deleted file mode 100644 index 8ae27d71..00000000 --- a/rust/.sqlx/query-cc6625aa285f9e6dcdc1a73d7bfb6ad23cff6c8932f61b48645c431dd7adaf48.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO webxdc_apps\n (app_id, version, name, name_translations, source_code_url,\n description, icon, bundle_sha256, bundle_bytes, published,\n cached_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?)\n ON CONFLICT(app_id, version) DO UPDATE SET\n name = excluded.name,\n name_translations = excluded.name_translations,\n source_code_url = excluded.source_code_url,\n description = excluded.description,\n icon = excluded.icon,\n bundle_sha256 = excluded.bundle_sha256,\n bundle_bytes = excluded.bundle_bytes,\n published = 1,\n cached_at = excluded.cached_at", - "describe": { - "columns": [], - "parameters": { - "Right": 10 - }, - "nullable": [] - }, - "hash": "cc6625aa285f9e6dcdc1a73d7bfb6ad23cff6c8932f61b48645c431dd7adaf48" -} diff --git a/rust/.sqlx/query-cdd3c7feb92de75c85642d5f99d0ac3f8bb715da50ba1fd25484041cd50a2818.json b/rust/.sqlx/query-cdd3c7feb92de75c85642d5f99d0ac3f8bb715da50ba1fd25484041cd50a2818.json new file mode 100644 index 00000000..7ce416fb --- /dev/null +++ b/rust/.sqlx/query-cdd3c7feb92de75c85642d5f99d0ac3f8bb715da50ba1fd25484041cd50a2818.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE groups SET last_message_exchange = CAST(strftime('%s','now') AS INTEGER), deleted_content = 0 WHERE group_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "cdd3c7feb92de75c85642d5f99d0ac3f8bb715da50ba1fd25484041cd50a2818" +} diff --git a/rust/.sqlx/query-fa52c200d8a2e33db876b41d77b460d0d697d3818061c59cf7c20106c1a64175.json b/rust/.sqlx/query-fa52c200d8a2e33db876b41d77b460d0d697d3818061c59cf7c20106c1a64175.json new file mode 100644 index 00000000..0f55d5e8 --- /dev/null +++ b/rust/.sqlx/query-fa52c200d8a2e33db876b41d77b460d0d697d3818061c59cf7c20106c1a64175.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO webxdc_apps\n (app_id, version, name, name_translations, source_code_url,\n description, icon, bundle_sha256, bundle_bytes, published,\n cached_at, pro_only, sort_order, one_time)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?)\n ON CONFLICT(app_id, version) DO UPDATE SET\n name = excluded.name,\n name_translations = excluded.name_translations,\n source_code_url = excluded.source_code_url,\n description = excluded.description,\n icon = excluded.icon,\n bundle_sha256 = excluded.bundle_sha256,\n bundle_bytes = excluded.bundle_bytes,\n published = 1,\n pro_only = excluded.pro_only,\n sort_order = excluded.sort_order,\n one_time = excluded.one_time,\n cached_at = excluded.cached_at", + "describe": { + "columns": [], + "parameters": { + "Right": 13 + }, + "nullable": [] + }, + "hash": "fa52c200d8a2e33db876b41d77b460d0d697d3818061c59cf7c20106c1a64175" +} diff --git a/rust/src/api/messages/incoming.rs b/rust/src/api/messages/incoming.rs index 6a7b0b0e..344d16a1 100644 --- a/rust/src/api/messages/incoming.rs +++ b/rust/src/api/messages/incoming.rs @@ -680,6 +680,7 @@ pub(crate) async fn handle_encrypted( let notification_content = content.clone(); handle_encrypted_inner(ctx, t, from_user_id, receipt_id, content).await?; crate::services::notifications::record_incoming_event( + ctx, t, from_user_id, receipt_id, diff --git a/rust/src/api/messages/incoming/additional_data.rs b/rust/src/api/messages/incoming/additional_data.rs index 46a4590d..efa3b210 100644 --- a/rust/src/api/messages/incoming/additional_data.rs +++ b/rust/src/api/messages/incoming/additional_data.rs @@ -17,7 +17,7 @@ use prost::Message as ProstMessage; use sqlx::{Sqlite, Transaction}; pub(crate) async fn handle_additional_data_message( - ctx: &Context, + ctx: &std::sync::Arc, tr: &mut Transaction<'_, Sqlite>, from_user_id: i64, group_id: &str, @@ -40,7 +40,20 @@ pub(crate) async fn handle_additional_data_message( }; if let Some(data) = data.as_ref() { - if let Some(update) = data.webxdc_update.as_ref() { + if data.webxdc_sync_request.is_some() { + let ctx = ctx.clone(); + let group_id = group_id.to_owned(); + tokio::spawn(async move { + if let Err(error) = WebxdcService::new(&ctx) + .send_one_time_apps_to_contact(&group_id, from_user_id) + .await + { + tracing::warn!(%error, "could not answer one-time app sync request"); + } + }); + } else if let Some(sync) = data.webxdc_sync.as_ref() { + WebxdcService::handle_sync_chunk(tr, group_id, from_user_id, sync).await?; + } else if let Some(update) = data.webxdc_update.as_ref() { // App state belongs in the instance's own log, which is ordered, // gap free, and outside the reach of the chat's deletion timer. WebxdcService::handle_incoming_update( diff --git a/rust/src/api/messages/incoming/groups.rs b/rust/src/api/messages/incoming/groups.rs index c69824c3..3d220f91 100644 --- a/rust/src/api/messages/incoming/groups.rs +++ b/rust/src/api/messages/incoming/groups.rs @@ -92,6 +92,11 @@ pub(crate) async fn handle_group_create( .await?; GroupService::spawn_state_refresh_and_announce(ctx, group_id.to_owned()); + crate::services::webxdc::WebxdcService::spawn_sync_request( + ctx, + group_id.to_owned(), + from_user_id, + ); Ok(()) } diff --git a/rust/src/api/proto/api/http/http_requests.proto b/rust/src/api/proto/api/http/http_requests.proto index 34f78208..75813e15 100644 --- a/rust/src/api/proto/api/http/http_requests.proto +++ b/rust/src/api/proto/api/http/http_requests.proto @@ -133,8 +133,13 @@ message WebxdcCatalogEntry { // The name in every language the bundle carries it in, keyed the same way. // `name` is what a client shows when none of them is a language it reads. map name_translations = 10; + bool pro_only = 11; + // Only one instance of this app may be placed into a chat. + bool one_time = 12; } message WebxdcCatalog { + // Admin-defined app order, with versions of each app grouped newest first. + // Clients retain this order when caching and displaying the drawer. repeated WebxdcCatalogEntry entries = 1; } diff --git a/rust/src/api/proto/client/data.proto b/rust/src/api/proto/client/data.proto index 97a57576..9502e6af 100644 --- a/rust/src/api/proto/client/data.proto +++ b/rust/src/api/proto/client/data.proto @@ -15,6 +15,8 @@ message AdditionalMessageData { WEBXDC_APP = 4; WEBXDC_UPDATE = 5; WEBXDC_SENT = 6; + WEBXDC_SYNC = 7; + WEBXDC_SYNC_REQUEST = 8; } Type type = 1; @@ -25,6 +27,45 @@ message AdditionalMessageData { optional WebxdcApp webxdc_app = 6; optional WebxdcUpdate webxdc_update = 7; optional WebxdcOrigin webxdc_origin = 8; + optional WebxdcSyncChunk webxdc_sync = 9; + optional WebxdcSyncRequest webxdc_sync_request = 10; +} + +// A peer-to-peer transfer of existing one-time app state. The encoded payload +// is split into bounded hidden messages and installed only after every chunk +// arrived and the digest matches. +message WebxdcSyncChunk { + string transfer_id = 1; + uint32 chunk_index = 2; + uint32 chunk_count = 3; + bytes payload_sha256 = 4; + bytes payload = 5; +} + +message WebxdcSyncRequest {} + +message WebxdcSyncPayload { + repeated WebxdcSyncInstance instances = 1; +} + +message WebxdcSyncInstance { + string instance_id = 1; + string app_id = 2; + int64 version = 3; + optional string summary = 4; + optional string document = 5; + int64 created_at = 6; + int64 last_update_at = 7; + repeated WebxdcSyncUpdate updates = 8; +} + +message WebxdcSyncUpdate { + string message_id = 1; + optional int64 sender_id = 2; + string payload = 3; + optional string info = 4; + optional string href = 5; + int64 received_at = 6; } // Attached to a message a webxdc app asked the user to send, so the chat can @@ -57,4 +98,7 @@ message WebxdcUpdate { optional string href = 4; optional string summary = 5; optional string document = 6; -} \ No newline at end of file + // JSON map of app-scoped member addresses to notification text. + // Absent preserves legacy broadcast; an empty map notifies nobody. + optional string notify = 7; +} diff --git a/rust/src/api/server/passwordless.rs b/rust/src/api/server/passwordless.rs index 8c632117..a8a77f4a 100644 --- a/rust/src/api/server/passwordless.rs +++ b/rust/src/api/server/passwordless.rs @@ -126,9 +126,9 @@ impl Server { ResponseOk::PasswordlessNotificationMessages(msgs) => Some(msgs), // The server answers with `None` whenever the poll finds no unseen // message, which is the normal outcome once every share arrived. - ResponseOk::None(_) => Some( - proto::server_to_client::response::PasswordlessNotificationMessages::default(), - ), + ResponseOk::None(_) => { + Some(proto::server_to_client::response::PasswordlessNotificationMessages::default()) + } _ => None, }) } diff --git a/rust/src/bridge/webxdc.rs b/rust/src/bridge/webxdc.rs index 2538f4e0..7bab3e1d 100644 --- a/rust/src/bridge/webxdc.rs +++ b/rust/src/bridge/webxdc.rs @@ -23,6 +23,18 @@ pub struct WebxdcStoreApp { pub source_code_url: Option, pub icon: Option>, pub bundle_bytes: i64, + pub pro_only: bool, + pub one_time: bool, + /// The existing instance in this chat when a one-time app was already placed. + pub instance_id: Option, +} + +pub struct WebxdcOneTimeInstance { + pub instance_id: String, + pub name: String, + pub icon: Option>, + pub summary: Option, + pub document: Option, } pub struct WebxdcInstanceInfo { @@ -66,12 +78,18 @@ pub async fn refresh_catalog() -> Result<()> { /// `languages` is what the UI prefers, most preferred first. Names and /// descriptions are cached in every language the catalog carries and picked /// here, so which language a user reads is never sent anywhere. -pub async fn catalog(languages: Vec) -> Result> { +pub async fn catalog(group_id: String, languages: Vec) -> Result> { let ctx = crate::context::Context::get_static()?; let database = ctx.app_db.read().await.clone(); let rows = sqlx::query!( r#"SELECT app_id AS "app_id!: String", version AS "version!: i64", name AS "name!: String", + pro_only AS "pro_only!: bool", + one_time AS "one_time!: bool", + (SELECT instance_id FROM webxdc_instances + WHERE webxdc_instances.group_id = ? + AND webxdc_instances.app_id = webxdc_apps.app_id + ORDER BY created_at ASC LIMIT 1) AS "instance_id: String", name_translations AS "name_translations!: String", source_code_url AS "source_code_url: String", description AS "description!: String", @@ -80,11 +98,12 @@ pub async fn catalog(languages: Vec) -> Result> { WHERE published = 1 AND version = (SELECT MAX(version) FROM webxdc_apps AS newer WHERE newer.app_id = webxdc_apps.app_id AND newer.published = 1) - ORDER BY name ASC"# + ORDER BY sort_order ASC, name ASC, app_id ASC"#, + group_id, ) .fetch_all(&database.pool) .await?; - let mut apps: Vec = rows + let apps: Vec = rows .into_iter() .map(|row| WebxdcStoreApp { app_id: row.app_id, @@ -103,14 +122,54 @@ pub async fn catalog(languages: Vec) -> Result> { source_code_url: row.source_code_url, icon: row.icon, bundle_bytes: row.bundle_bytes, + pro_only: row.pro_only, + one_time: row.one_time, + instance_id: row.instance_id, }) .collect(); - // The query orders by the untranslated name, which is not the order the - // list is read in once the names are the reader's. - apps.sort_by_key(|app| app.name.to_lowercase()); + // Keep the server order even when names are localized for the reader. Ok(apps) } +/// One-time apps already placed into a chat, for the profile shortcut. +pub async fn one_time_instances( + group_id: String, + languages: Vec, +) -> Result> { + let ctx = crate::context::Context::get_static()?; + let database = ctx.app_db.read().await.clone(); + let rows = sqlx::query!( + r#"SELECT instance.instance_id AS "instance_id!: String", + app.name AS "name!: String", + app.name_translations AS "name_translations!: String", + app.icon AS "icon: Vec", + instance.summary AS "summary: String", + instance.document AS "document: String" + FROM webxdc_instances AS instance + JOIN webxdc_apps AS app + ON app.app_id = instance.app_id AND app.version = instance.version + WHERE instance.group_id = ? AND app.one_time = 1 + ORDER BY instance.last_update_at DESC, instance.created_at DESC"#, + group_id, + ) + .fetch_all(&database.pool) + .await?; + Ok(rows + .into_iter() + .map(|row| WebxdcOneTimeInstance { + instance_id: row.instance_id, + name: crate::services::webxdc::store::pick_localized( + &row.name_translations, + &languages, + ) + .unwrap_or(row.name), + icon: row.icon, + summary: row.summary, + document: row.document, + }) + .collect()) +} + /// Places an app into a chat and returns the id of the message that carries it. pub async fn create_instance(group_id: String, app_id: String, version: i64) -> Result { let ctx = crate::context::Context::get_static()?; @@ -135,8 +194,8 @@ pub async fn instance(instance_id: String) -> Result> })) } -/// Downloads and verifies the bundle if it is not already on disk, and returns -/// where it landed. Called when the user starts an app, never on message +/// Opens cached code, adopting an update only if already downloaded. +/// Downloads and verifies the pinned bundle only when it is missing. Called when the user starts an app, never on message /// arrival: a message must not be able to make a device fetch anything. pub async fn prepare_bundle(instance_id: String) -> Result { let ctx = crate::context::Context::get_static()?; @@ -144,6 +203,13 @@ pub async fn prepare_bundle(instance_id: String) -> Result { Ok(path.display().to_string()) } +/// Background update check after a webxdc app closes. Downloads only; the +/// running version changes on a later prepare_bundle call, never mid-session. +pub async fn cache_update(instance_id: String) -> Result<()> { + let ctx = crate::context::Context::get_static()?; + WebxdcService::new(ctx).cache_update(&instance_id).await +} + /// Answers one request the webview made for a file inside the bundle. /// /// The platform shim has already established that the request came from this @@ -153,7 +219,7 @@ pub async fn serve(instance_id: String, request_path: String) -> Result, summary: Option, document: Option, + notify: Option, ) -> Result<()> { let ctx = crate::context::Context::get_static()?; WebxdcService::new(ctx) - .send_update(instance_id, payload, info, href, summary, document) + .send_update(instance_id, payload, info, href, summary, document, notify) .await } @@ -211,3 +278,9 @@ pub async fn delete_instance(instance_id: String) -> Result> { let ctx = crate::context::Context::get_static()?; WebxdcService::new(ctx).delete_instance(&instance_id).await } + +/// Current members of the instance's chat; no account identifiers leave Rust. +pub async fn members(instance_id: String) -> Result { + let ctx = crate::context::Context::get_static()?; + WebxdcService::new(ctx).members(&instance_id).await +} diff --git a/rust/src/database/app/migrations/0019_webxdc_pro.sql b/rust/src/database/app/migrations/0019_webxdc_pro.sql new file mode 100644 index 00000000..2a93f42c --- /dev/null +++ b/rust/src/database/app/migrations/0019_webxdc_pro.sql @@ -0,0 +1 @@ +ALTER TABLE webxdc_apps ADD COLUMN pro_only BOOLEAN NOT NULL DEFAULT FALSE; diff --git a/rust/src/database/app/migrations/0020_webxdc_order.sql b/rust/src/database/app/migrations/0020_webxdc_order.sql new file mode 100644 index 00000000..ceea9b69 --- /dev/null +++ b/rust/src/database/app/migrations/0020_webxdc_order.sql @@ -0,0 +1,2 @@ +-- Retain the server catalog order when showing cached apps offline. +ALTER TABLE webxdc_apps ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0; diff --git a/rust/src/database/app/migrations/0021_webxdc_one_time.sql b/rust/src/database/app/migrations/0021_webxdc_one_time.sql new file mode 100644 index 00000000..d07b2c2e --- /dev/null +++ b/rust/src/database/app/migrations/0021_webxdc_one_time.sql @@ -0,0 +1,2 @@ +-- Apps such as a shared expense ledger have one instance per chat. +ALTER TABLE webxdc_apps ADD COLUMN one_time BOOLEAN NOT NULL DEFAULT FALSE; diff --git a/rust/src/database/app/migrations/0022_webxdc_sync.sql b/rust/src/database/app/migrations/0022_webxdc_sync.sql new file mode 100644 index 00000000..31448c1b --- /dev/null +++ b/rust/src/database/app/migrations/0022_webxdc_sync.sql @@ -0,0 +1,34 @@ +-- Chunks survive process restarts and out-of-order delivery. They are removed +-- as soon as a complete, verified state transfer has been installed. +CREATE TABLE webxdc_sync_chunks ( + transfer_id TEXT NOT NULL, + group_id TEXT NOT NULL REFERENCES groups(group_id) ON DELETE CASCADE, + sender_id INTEGER NOT NULL REFERENCES contacts(user_id) ON DELETE CASCADE, + chunk_index INTEGER NOT NULL, + chunk_count INTEGER NOT NULL, + payload_sha256 BLOB NOT NULL, + payload BLOB NOT NULL, + created_at INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), + PRIMARY KEY (transfer_id, sender_id, chunk_index) +); + +CREATE INDEX idx_webxdc_sync_chunks_created_at + ON webxdc_sync_chunks(created_at); + +-- A newly joined member can receive a live update before the instance snapshot. +-- Keep it until the app card arrives instead of dropping shared state. +CREATE TABLE webxdc_pending_updates ( + message_id TEXT NOT NULL PRIMARY KEY, + instance_id TEXT NOT NULL, + group_id TEXT NOT NULL REFERENCES groups(group_id) ON DELETE CASCADE, + sender_id INTEGER NOT NULL REFERENCES contacts(user_id) ON DELETE CASCADE, + payload TEXT NOT NULL, + info TEXT, + href TEXT, + summary TEXT, + document TEXT, + received_at INTEGER NOT NULL +); + +CREATE INDEX idx_webxdc_pending_updates_instance + ON webxdc_pending_updates(instance_id, received_at); diff --git a/rust/src/database/app/tables/groups.rs b/rust/src/database/app/tables/groups.rs index b0ca0b01..ccd8640c 100644 --- a/rust/src/database/app/tables/groups.rs +++ b/rust/src/database/app/tables/groups.rs @@ -330,7 +330,8 @@ impl Group { sqlx::query!( r#" UPDATE groups - SET last_message_exchange = MAX(last_message_exchange, ?) + SET last_message_exchange = MAX(last_message_exchange, ?), + deleted_content = 0 WHERE group_id = ? "#, timestamp, diff --git a/rust/src/frb_generated.rs b/rust/src/frb_generated.rs index 9ae6c6d2..9303aa76 100644 --- a/rust/src/frb_generated.rs +++ b/rust/src/frb_generated.rs @@ -38,7 +38,7 @@ flutter_rust_bridge::frb_generated_boilerplate!( default_rust_auto_opaque = RustAutoOpaqueMoi, ); pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0"; -pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 1362207365; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -1591152700; // Section: executor @@ -161,6 +161,43 @@ fn wire__crate__bridge__webxdc__address_for_impl( }, ) } +fn wire__crate__bridge__webxdc__cache_update_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::TaskInfo { + debug_name: "cache_update", + 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_instance_id = ::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::webxdc::cache_update(api_instance_id).await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__bridge__webxdc__catalog_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -183,12 +220,14 @@ fn wire__crate__bridge__webxdc__catalog_impl( }; let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_group_id = ::sse_decode(&mut deserializer); let api_languages = >::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::webxdc::catalog(api_languages).await?; + let output_ok = + crate::bridge::webxdc::catalog(api_group_id, api_languages).await?; Ok(output_ok) })() .await, @@ -820,6 +859,81 @@ fn wire__crate__bridge__groups__manage_admin_state_impl( }, ) } +fn wire__crate__bridge__webxdc__members_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::TaskInfo { + debug_name: "members", + 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_instance_id = ::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::webxdc::members(api_instance_id).await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__bridge__webxdc__one_time_instances_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::TaskInfo { + debug_name: "one_time_instances", + 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_group_id = ::sse_decode(&mut deserializer); + let api_languages = >::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::webxdc::one_time_instances(api_group_id, api_languages) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__bridge__webxdc__prepare_bundle_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -5592,6 +5706,7 @@ fn wire__crate__bridge__webxdc__send_update_impl( let api_href = >::sse_decode(&mut deserializer); let api_summary = >::sse_decode(&mut deserializer); let api_document = >::sse_decode(&mut deserializer); + let api_notify = >::sse_decode(&mut deserializer); deserializer.end(); move |context| async move { transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( @@ -5603,6 +5718,7 @@ fn wire__crate__bridge__webxdc__send_update_impl( api_href, api_summary, api_document, + api_notify, ) .await?; Ok(output_ok) @@ -6671,6 +6787,20 @@ impl SseDecode for Vec { } } +impl SseDecode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut len_ = ::sse_decode(deserializer); + let mut ans_ = Vec::with_capacity(len_ as usize); + for idx_ in 0..len_ { + ans_.push(::sse_decode( + deserializer, + )); + } + return ans_; + } +} + impl SseDecode for Vec { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -7330,6 +7460,24 @@ impl SseDecode for crate::bridge::webxdc::WebxdcInstanceInfo { } } +impl SseDecode for crate::bridge::webxdc::WebxdcOneTimeInstance { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_instanceId = ::sse_decode(deserializer); + let mut var_name = ::sse_decode(deserializer); + let mut var_icon = >>::sse_decode(deserializer); + let mut var_summary = >::sse_decode(deserializer); + let mut var_document = >::sse_decode(deserializer); + return crate::bridge::webxdc::WebxdcOneTimeInstance { + instance_id: var_instanceId, + name: var_name, + icon: var_icon, + summary: var_summary, + document: var_document, + }; + } +} + impl SseDecode for crate::bridge::webxdc::WebxdcResponse { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -7358,6 +7506,9 @@ impl SseDecode for crate::bridge::webxdc::WebxdcStoreApp { let mut var_sourceCodeUrl = >::sse_decode(deserializer); let mut var_icon = >>::sse_decode(deserializer); let mut var_bundleBytes = ::sse_decode(deserializer); + let mut var_proOnly = ::sse_decode(deserializer); + let mut var_oneTime = ::sse_decode(deserializer); + let mut var_instanceId = >::sse_decode(deserializer); return crate::bridge::webxdc::WebxdcStoreApp { app_id: var_appId, version: var_version, @@ -7366,6 +7517,9 @@ impl SseDecode for crate::bridge::webxdc::WebxdcStoreApp { source_code_url: var_sourceCodeUrl, icon: var_icon, bundle_bytes: var_bundleBytes, + pro_only: var_proOnly, + one_time: var_oneTime, + instance_id: var_instanceId, }; } } @@ -7400,163 +7554,166 @@ fn pde_ffi_dispatcher_primary_impl( 1 => wire__crate__bridge__groups__add_hidden_contact_impl(port, ptr, rust_vec_len, data_len), 2 => wire__crate__bridge__groups__add_new_group_members_impl(port, ptr, rust_vec_len, data_len), 3 => wire__crate__bridge__webxdc__address_for_impl(port, ptr, rust_vec_len, data_len), -4 => wire__crate__bridge__webxdc__catalog_impl(port, ptr, rust_vec_len, data_len), -5 => wire__crate__bridge__logging__clean_log_file_impl(port, ptr, rust_vec_len, data_len), -6 => wire__crate__bridge__logging__clear_log_file_impl(port, ptr, rust_vec_len, data_len), -7 => wire__crate__bridge__webxdc__create_instance_impl(port, ptr, rust_vec_len, data_len), -8 => wire__crate__bridge__groups__create_new_group_impl(port, ptr, rust_vec_len, data_len), -9 => wire__crate__bridge__webxdc__delete_instance_impl(port, ptr, rust_vec_len, data_len), -10 => wire__crate__bridge__groups__fetch_group_state_impl(port, ptr, rust_vec_len, data_len), -11 => wire__crate__bridge__groups__fetch_group_states_for_unjoined_groups_impl(port, ptr, rust_vec_len, data_len), -12 => wire__crate__bridge__groups__fetch_missing_group_public_keys_impl(port, ptr, rust_vec_len, data_len), -13 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_change_exclusion_for_contact_impl(port, ptr, rust_vec_len, data_len), -14 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_get_current_version_impl(port, ptr, rust_vec_len, data_len), -15 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_update_verification_state_for_user_impl(port, ptr, rust_vec_len, data_len), -16 => wire__crate__bridge__callbacks__init_flutter_callbacks_impl(port, ptr, rust_vec_len, data_len), -17 => wire__crate__bridge__initialize_twonly_flutter_impl(port, ptr, rust_vec_len, data_len), -18 => wire__crate__bridge__initialize_twonly_standalone_impl(port, ptr, rust_vec_len, data_len), -19 => wire__crate__bridge__webxdc__instance_impl(port, ptr, rust_vec_len, data_len), -20 => wire__crate__bridge__groups__leave_group_impl(port, ptr, rust_vec_len, data_len), -21 => wire__crate__bridge__logging__load_log_file_impl(port, ptr, rust_vec_len, data_len), -22 => wire__crate__bridge__groups__manage_admin_state_impl(port, ptr, rust_vec_len, data_len), -23 => wire__crate__bridge__webxdc__prepare_bundle_impl(port, ptr, rust_vec_len, data_len), -24 => wire__crate__bridge__logging__read_last_log_lines_impl(port, ptr, rust_vec_len, data_len), -25 => wire__crate__bridge__webxdc__refresh_catalog_impl(port, ptr, rust_vec_len, data_len), -26 => wire__crate__bridge__groups__remove_member_from_group_impl(port, ptr, rust_vec_len, data_len), -27 => wire__crate__bridge__api__rust_api_abandon_media_impl(port, ptr, rust_vec_len, data_len), -28 => wire__crate__bridge__api__rust_api_add_additional_user_impl(port, ptr, rust_vec_len, data_len), -29 => wire__crate__bridge__api__rust_api_allocate_sequence_impl(port, ptr, rust_vec_len, data_len), -31 => wire__crate__bridge__api__rust_api_authentication_headers_impl(port, ptr, rust_vec_len, data_len), -33 => wire__crate__bridge__api__rust_api_change_username_impl(port, ptr, rust_vec_len, data_len), -34 => wire__crate__bridge__api__rust_api_check_for_deleted_usernames_impl(port, ptr, rust_vec_len, data_len), -35 => wire__crate__bridge__api__rust_api_check_for_passwordless_notification_impl(port, ptr, rust_vec_len, data_len), -36 => wire__crate__bridge__api__rust_api_clear_contact_request_notifications_impl(port, ptr, rust_vec_len, data_len), -37 => wire__crate__bridge__api__rust_api_clear_conversation_notifications_impl(port, ptr, rust_vec_len, data_len), -38 => wire__crate__bridge__api__rust_api_close_impl(port, ptr, rust_vec_len, data_len), -39 => wire__crate__bridge__api__rust_api_confirm_memories_upload_impl(port, ptr, rust_vec_len, data_len), -40 => wire__crate__bridge__api__rust_api_connect_impl(port, ptr, rust_vec_len, data_len), -41 => wire__crate__bridge__api__rust_api_connection_state_impl(port, ptr, rust_vec_len, data_len), -42 => wire__crate__bridge__api__rust_api_crop_media_transparent_borders_impl(port, ptr, rust_vec_len, data_len), -43 => wire__crate__bridge__api__rust_api_current_user_avatar_path_impl(port, ptr, rust_vec_len, data_len), -45 => wire__crate__bridge__api__rust_api_delete_account_impl(port, ptr, rust_vec_len, data_len), -46 => wire__crate__bridge__api__rust_api_delete_memory_impl(port, ptr, rust_vec_len, data_len), -47 => wire__crate__bridge__api__rust_api_delete_widget_media_impl(port, ptr, rust_vec_len, data_len), -48 => wire__crate__bridge__api__rust_api_disable_memories_backup_impl(port, ptr, rust_vec_len, data_len), -49 => wire__crate__bridge__api__rust_api_download_done_impl(port, ptr, rust_vec_len, data_len), -50 => wire__crate__bridge__api__rust_api_download_media_impl(port, ptr, rust_vec_len, data_len), -51 => wire__crate__bridge__api__rust_api_download_pending_media_impl(port, ptr, rust_vec_len, data_len), -52 => wire__crate__bridge__api__rust_api_ensure_avatar_png_impl(port, ptr, rust_vec_len, data_len), -53 => wire__crate__bridge__api__rust_api_establish_signal_session_impl(port, ptr, rust_vec_len, data_len), -54 => wire__crate__bridge__api__rust_api_events_impl(port, ptr, rust_vec_len, data_len), -55 => wire__crate__bridge__api__rust_api_finish_started_media_uploads_impl(port, ptr, rust_vec_len, data_len), -56 => wire__crate__bridge__api__rust_api_force_ipa_check_impl(port, ptr, rust_vec_len, data_len), -57 => wire__crate__bridge__api__rust_api_get_memories_url_impl(port, ptr, rust_vec_len, data_len), -58 => wire__crate__bridge__api__rust_api_get_memories_usage_impl(port, ptr, rust_vec_len, data_len), -59 => wire__crate__bridge__api__rust_api_get_plan_balance_impl(port, ptr, rust_vec_len, data_len), -60 => wire__crate__bridge__api__rust_api_get_proof_of_work_impl(port, ptr, rust_vec_len, data_len), -61 => wire__crate__bridge__api__rust_api_get_server_key_for_passwordless_recovery_impl(port, ptr, rust_vec_len, data_len), -62 => wire__crate__bridge__api__rust_api_get_user_by_id_impl(port, ptr, rust_vec_len, data_len), -63 => wire__crate__bridge__api__rust_api_get_user_data_impl(port, ptr, rust_vec_len, data_len), -64 => wire__crate__bridge__api__rust_api_get_user_id_from_username_impl(port, ptr, rust_vec_len, data_len), -65 => wire__crate__bridge__api__rust_api_hand_outbox_to_os_impl(port, ptr, rust_vec_len, data_len), -66 => wire__crate__bridge__api__rust_api_initialize_media_upload_impl(port, ptr, rust_vec_len, data_len), -67 => wire__crate__bridge__api__rust_api_insert_and_send_additional_data_impl(port, ptr, rust_vec_len, data_len), -68 => wire__crate__bridge__api__rust_api_insert_and_send_ask_about_user_impl(port, ptr, rust_vec_len, data_len), -69 => wire__crate__bridge__api__rust_api_insert_and_send_contact_share_impl(port, ptr, rust_vec_len, data_len), -70 => wire__crate__bridge__api__rust_api_insert_and_send_text_impl(port, ptr, rust_vec_len, data_len), -71 => wire__crate__bridge__api__rust_api_ipa_purchase_impl(port, ptr, rust_vec_len, data_len), -72 => wire__crate__bridge__api__rust_api_load_plan_balance_impl(port, ptr, rust_vec_len, data_len), -73 => wire__crate__bridge__api__rust_api_media_size_limit_report_impl(port, ptr, rust_vec_len, data_len), -74 => wire__crate__bridge__api__rust_api_media_step_finished_impl(port, ptr, rust_vec_len, data_len), -75 => wire__crate__bridge__api__rust_api_notification_badge_count_impl(port, ptr, rust_vec_len, data_len), -76 => wire__crate__bridge__api__rust_api_notify_messages_opened_impl(port, ptr, rust_vec_len, data_len), -77 => wire__crate__bridge__api__rust_api_perform_passwordless_recovery_heartbeat_impl(port, ptr, rust_vec_len, data_len), -78 => wire__crate__bridge__api__rust_api_prerender_media_impl(port, ptr, rust_vec_len, data_len), -79 => wire__crate__bridge__api__rust_api_purge_media_temp_folder_impl(port, ptr, rust_vec_len, data_len), -80 => wire__crate__bridge__api__rust_api_purge_widget_media_impl(port, ptr, rust_vec_len, data_len), -81 => wire__crate__bridge__api__rust_api_refresh_widget_manifest_impl(port, ptr, rust_vec_len, data_len), -82 => wire__crate__bridge__api__rust_api_register_impl(port, ptr, rust_vec_len, data_len), -83 => wire__crate__bridge__api__rust_api_register_home_widget_impl(port, ptr, rust_vec_len, data_len), -84 => wire__crate__bridge__api__rust_api_register_passwordless_notification_impl(port, ptr, rust_vec_len, data_len), -85 => wire__crate__bridge__api__rust_api_register_passwordless_recovery_impl(port, ptr, rust_vec_len, data_len), -86 => wire__crate__bridge__api__rust_api_reload_configuration_impl(port, ptr, rust_vec_len, data_len), -87 => wire__crate__bridge__api__rust_api_remove_additional_user_impl(port, ptr, rust_vec_len, data_len), -88 => wire__crate__bridge__api__rust_api_remove_media_files_impl(port, ptr, rust_vec_len, data_len), -89 => wire__crate__bridge__api__rust_api_report_user_impl(port, ptr, rust_vec_len, data_len), -90 => wire__crate__bridge__api__rust_api_request_binary_impl(port, ptr, rust_vec_len, data_len), -91 => wire__crate__bridge__api__rust_api_request_contact_by_username_impl(port, ptr, rust_vec_len, data_len), -92 => wire__crate__bridge__api__rust_api_request_media_reupload_impl(port, ptr, rust_vec_len, data_len), -93 => wire__crate__bridge__api__rust_api_request_memories_upload_impl(port, ptr, rust_vec_len, data_len), -94 => wire__crate__bridge__api__rust_api_retransmit_all_messages_impl(port, ptr, rust_vec_len, data_len), -95 => wire__crate__bridge__api__rust_api_retry_pending_media_reuploads_impl(port, ptr, rust_vec_len, data_len), -96 => wire__crate__bridge__api__rust_api_reupload_pending_media_impl(port, ptr, rust_vec_len, data_len), -97 => wire__crate__bridge__api__rust_api_save_media_to_gallery_impl(port, ptr, rust_vec_len, data_len), -98 => wire__crate__bridge__api__rust_api_send_binary_impl(port, ptr, rust_vec_len, data_len), -99 => wire__crate__bridge__api__rust_api_send_contact_profile_impl(port, ptr, rust_vec_len, data_len), -100 => wire__crate__bridge__api__rust_api_send_encrypted_content_impl(port, ptr, rust_vec_len, data_len), -101 => wire__crate__bridge__api__rust_api_send_encrypted_content_to_group_impl(port, ptr, rust_vec_len, data_len), -102 => wire__crate__bridge__api__rust_api_send_media_to_groups_impl(port, ptr, rust_vec_len, data_len), -103 => wire__crate__bridge__api__rust_api_send_queued_message_impl(port, ptr, rust_vec_len, data_len), -104 => wire__crate__bridge__api__rust_api_send_text_message_impl(port, ptr, rust_vec_len, data_len), -105 => wire__crate__bridge__api__rust_api_send_typing_impl(port, ptr, rust_vec_len, data_len), -106 => wire__crate__bridge__api__rust_api_set_background_impl(port, ptr, rust_vec_len, data_len), -107 => wire__crate__bridge__api__rust_api_set_home_widget_groups_impl(port, ptr, rust_vec_len, data_len), -108 => wire__crate__bridge__api__rust_api_set_login_token_impl(port, ptr, rust_vec_len, data_len), -109 => wire__crate__bridge__api__rust_api_set_media_display_limit_impl(port, ptr, rust_vec_len, data_len), -110 => wire__crate__bridge__api__rust_api_set_media_requires_authentication_impl(port, ptr, rust_vec_len, data_len), -111 => wire__crate__bridge__api__rust_api_set_media_trim_impl(port, ptr, rust_vec_len, data_len), -112 => wire__crate__bridge__api__rust_api_set_network_available_impl(port, ptr, rust_vec_len, data_len), -113 => wire__crate__bridge__api__rust_api_store_media_impl(port, ptr, rust_vec_len, data_len), -114 => wire__crate__bridge__api__rust_api_submit_recovery_share_impl(port, ptr, rust_vec_len, data_len), -115 => wire__crate__bridge__api__rust_api_sync_widget_permissions_impl(port, ptr, rust_vec_len, data_len), -116 => wire__crate__bridge__api__rust_api_toggle_media_remove_audio_impl(port, ptr, rust_vec_len, data_len), -117 => wire__crate__bridge__api__rust_api_try_request_contact_by_id_impl(port, ptr, rust_vec_len, data_len), -118 => wire__crate__bridge__api__rust_api_unregister_home_widget_impl(port, ptr, rust_vec_len, data_len), -119 => wire__crate__bridge__api__rust_api_update_fcm_token_impl(port, ptr, rust_vec_len, data_len), -120 => wire__crate__bridge__api__rust_api_update_signed_pre_key_impl(port, ptr, rust_vec_len, data_len), -121 => wire__crate__bridge__api__rust_api_upload_pqc_pre_keys_impl(port, ptr, rust_vec_len, data_len), -122 => wire__crate__bridge__wrapper__app_database__rust_app_database_changes_impl(port, ptr, rust_vec_len, data_len), -123 => wire__crate__bridge__wrapper__app_database__rust_app_database_execute_impl(port, ptr, rust_vec_len, data_len), -124 => wire__crate__bridge__wrapper__app_database__rust_app_database_legacy_import_complete_impl(port, ptr, rust_vec_len, data_len), -125 => wire__crate__bridge__wrapper__app_database__rust_app_database_migrate_legacy_database_impl(port, ptr, rust_vec_len, data_len), -126 => wire__crate__bridge__wrapper__app_database__rust_app_database_select_impl(port, ptr, rust_vec_len, data_len), -127 => wire__crate__bridge__wrapper__backup__rust_backup_archive_create_backup_archive_impl(port, ptr, rust_vec_len, data_len), -128 => wire__crate__bridge__wrapper__backup__rust_backup_archive_get_backup_download_token_impl(port, ptr, rust_vec_len, data_len), -129 => wire__crate__bridge__wrapper__backup__rust_backup_archive_restore_backup_archive_impl(port, ptr, rust_vec_len, data_len), -130 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_id_impl(port, ptr, rust_vec_len, data_len), -131 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), -132 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_identity_backup_bytes_impl(port, ptr, rust_vec_len, data_len), -133 => wire__crate__bridge__wrapper__backup__rust_backup_identity_import_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), -134 => wire__crate__bridge__wrapper__backup__rust_backup_identity_restore_identity_backup_impl(port, ptr, rust_vec_len, data_len), -135 => wire__crate__bridge__wrapper__backup__rust_backup_identity_set_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), -136 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_decrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len), -137 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_encrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len), -138 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_signal_identity_impl(port, ptr, rust_vec_len, data_len), -139 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_user_id_impl(port, ptr, rust_vec_len, data_len), -140 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_serialized_impl(port, ptr, rust_vec_len, data_len), -141 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_signal_identity_impl(port, ptr, rust_vec_len, data_len), -142 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_local_credentials_impl(port, ptr, rust_vec_len, data_len), -143 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_serialize_impl(port, ptr, rust_vec_len, data_len), -144 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_set_user_id_impl(port, ptr, rust_vec_len, data_len), -145 => wire__crate__bridge__wrapper__signal__rust_signal_decrypt_impl(port, ptr, rust_vec_len, data_len), -146 => wire__crate__bridge__wrapper__signal__rust_signal_encrypt_impl(port, ptr, rust_vec_len, data_len), -147 => wire__crate__bridge__wrapper__signal__rust_signal_generate_bundle_impl(port, ptr, rust_vec_len, data_len), -148 => wire__crate__bridge__wrapper__signal__rust_signal_generate_pqc_prekeys_impl(port, ptr, rust_vec_len, data_len), -149 => wire__crate__bridge__wrapper__signal__rust_signal_get_contact_public_key_impl(port, ptr, rust_vec_len, data_len), -150 => wire__crate__bridge__wrapper__signal__rust_signal_get_user_public_key_impl(port, ptr, rust_vec_len, data_len), -151 => wire__crate__bridge__wrapper__signal__rust_signal_process_prekey_bundle_impl(port, ptr, rust_vec_len, data_len), -152 => wire__crate__bridge__wrapper__rust_utils_generate_shares_impl(port, ptr, rust_vec_len, data_len), -153 => wire__crate__bridge__wrapper__rust_utils_recover_secret_impl(port, ptr, rust_vec_len, data_len), -154 => wire__crate__bridge__webxdc__send_update_impl(port, ptr, rust_vec_len, data_len), -155 => wire__crate__bridge__webxdc__serve_impl(port, ptr, rust_vec_len, data_len), -156 => wire__crate__bridge__groups__update_chat_deletion_time_impl(port, ptr, rust_vec_len, data_len), -157 => wire__crate__bridge__groups__update_group_name_impl(port, ptr, rust_vec_len, data_len), -158 => wire__crate__bridge__webxdc__updates_after_impl(port, ptr, rust_vec_len, data_len), -160 => wire__crate__bridge__user_config__user_config_api_create_impl(port, ptr, rust_vec_len, data_len), -161 => wire__crate__bridge__user_config__user_config_api_import_json_impl(port, ptr, rust_vec_len, data_len), -162 => wire__crate__bridge__user_config__user_config_api_load_impl(port, ptr, rust_vec_len, data_len), -163 => wire__crate__bridge__user_config__user_config_api_save_impl(port, ptr, rust_vec_len, data_len), -164 => wire__crate__bridge__user_config__user_config_api_update_impl(port, ptr, rust_vec_len, data_len), +4 => wire__crate__bridge__webxdc__cache_update_impl(port, ptr, rust_vec_len, data_len), +5 => wire__crate__bridge__webxdc__catalog_impl(port, ptr, rust_vec_len, data_len), +6 => wire__crate__bridge__logging__clean_log_file_impl(port, ptr, rust_vec_len, data_len), +7 => wire__crate__bridge__logging__clear_log_file_impl(port, ptr, rust_vec_len, data_len), +8 => wire__crate__bridge__webxdc__create_instance_impl(port, ptr, rust_vec_len, data_len), +9 => wire__crate__bridge__groups__create_new_group_impl(port, ptr, rust_vec_len, data_len), +10 => wire__crate__bridge__webxdc__delete_instance_impl(port, ptr, rust_vec_len, data_len), +11 => wire__crate__bridge__groups__fetch_group_state_impl(port, ptr, rust_vec_len, data_len), +12 => wire__crate__bridge__groups__fetch_group_states_for_unjoined_groups_impl(port, ptr, rust_vec_len, data_len), +13 => wire__crate__bridge__groups__fetch_missing_group_public_keys_impl(port, ptr, rust_vec_len, data_len), +14 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_change_exclusion_for_contact_impl(port, ptr, rust_vec_len, data_len), +15 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_get_current_version_impl(port, ptr, rust_vec_len, data_len), +16 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_update_verification_state_for_user_impl(port, ptr, rust_vec_len, data_len), +17 => wire__crate__bridge__callbacks__init_flutter_callbacks_impl(port, ptr, rust_vec_len, data_len), +18 => wire__crate__bridge__initialize_twonly_flutter_impl(port, ptr, rust_vec_len, data_len), +19 => wire__crate__bridge__initialize_twonly_standalone_impl(port, ptr, rust_vec_len, data_len), +20 => wire__crate__bridge__webxdc__instance_impl(port, ptr, rust_vec_len, data_len), +21 => wire__crate__bridge__groups__leave_group_impl(port, ptr, rust_vec_len, data_len), +22 => wire__crate__bridge__logging__load_log_file_impl(port, ptr, rust_vec_len, data_len), +23 => wire__crate__bridge__groups__manage_admin_state_impl(port, ptr, rust_vec_len, data_len), +24 => wire__crate__bridge__webxdc__members_impl(port, ptr, rust_vec_len, data_len), +25 => wire__crate__bridge__webxdc__one_time_instances_impl(port, ptr, rust_vec_len, data_len), +26 => wire__crate__bridge__webxdc__prepare_bundle_impl(port, ptr, rust_vec_len, data_len), +27 => wire__crate__bridge__logging__read_last_log_lines_impl(port, ptr, rust_vec_len, data_len), +28 => wire__crate__bridge__webxdc__refresh_catalog_impl(port, ptr, rust_vec_len, data_len), +29 => wire__crate__bridge__groups__remove_member_from_group_impl(port, ptr, rust_vec_len, data_len), +30 => wire__crate__bridge__api__rust_api_abandon_media_impl(port, ptr, rust_vec_len, data_len), +31 => wire__crate__bridge__api__rust_api_add_additional_user_impl(port, ptr, rust_vec_len, data_len), +32 => wire__crate__bridge__api__rust_api_allocate_sequence_impl(port, ptr, rust_vec_len, data_len), +34 => wire__crate__bridge__api__rust_api_authentication_headers_impl(port, ptr, rust_vec_len, data_len), +36 => wire__crate__bridge__api__rust_api_change_username_impl(port, ptr, rust_vec_len, data_len), +37 => wire__crate__bridge__api__rust_api_check_for_deleted_usernames_impl(port, ptr, rust_vec_len, data_len), +38 => wire__crate__bridge__api__rust_api_check_for_passwordless_notification_impl(port, ptr, rust_vec_len, data_len), +39 => wire__crate__bridge__api__rust_api_clear_contact_request_notifications_impl(port, ptr, rust_vec_len, data_len), +40 => wire__crate__bridge__api__rust_api_clear_conversation_notifications_impl(port, ptr, rust_vec_len, data_len), +41 => wire__crate__bridge__api__rust_api_close_impl(port, ptr, rust_vec_len, data_len), +42 => wire__crate__bridge__api__rust_api_confirm_memories_upload_impl(port, ptr, rust_vec_len, data_len), +43 => wire__crate__bridge__api__rust_api_connect_impl(port, ptr, rust_vec_len, data_len), +44 => wire__crate__bridge__api__rust_api_connection_state_impl(port, ptr, rust_vec_len, data_len), +45 => wire__crate__bridge__api__rust_api_crop_media_transparent_borders_impl(port, ptr, rust_vec_len, data_len), +46 => wire__crate__bridge__api__rust_api_current_user_avatar_path_impl(port, ptr, rust_vec_len, data_len), +48 => wire__crate__bridge__api__rust_api_delete_account_impl(port, ptr, rust_vec_len, data_len), +49 => wire__crate__bridge__api__rust_api_delete_memory_impl(port, ptr, rust_vec_len, data_len), +50 => wire__crate__bridge__api__rust_api_delete_widget_media_impl(port, ptr, rust_vec_len, data_len), +51 => wire__crate__bridge__api__rust_api_disable_memories_backup_impl(port, ptr, rust_vec_len, data_len), +52 => wire__crate__bridge__api__rust_api_download_done_impl(port, ptr, rust_vec_len, data_len), +53 => wire__crate__bridge__api__rust_api_download_media_impl(port, ptr, rust_vec_len, data_len), +54 => wire__crate__bridge__api__rust_api_download_pending_media_impl(port, ptr, rust_vec_len, data_len), +55 => wire__crate__bridge__api__rust_api_ensure_avatar_png_impl(port, ptr, rust_vec_len, data_len), +56 => wire__crate__bridge__api__rust_api_establish_signal_session_impl(port, ptr, rust_vec_len, data_len), +57 => wire__crate__bridge__api__rust_api_events_impl(port, ptr, rust_vec_len, data_len), +58 => wire__crate__bridge__api__rust_api_finish_started_media_uploads_impl(port, ptr, rust_vec_len, data_len), +59 => wire__crate__bridge__api__rust_api_force_ipa_check_impl(port, ptr, rust_vec_len, data_len), +60 => wire__crate__bridge__api__rust_api_get_memories_url_impl(port, ptr, rust_vec_len, data_len), +61 => wire__crate__bridge__api__rust_api_get_memories_usage_impl(port, ptr, rust_vec_len, data_len), +62 => wire__crate__bridge__api__rust_api_get_plan_balance_impl(port, ptr, rust_vec_len, data_len), +63 => wire__crate__bridge__api__rust_api_get_proof_of_work_impl(port, ptr, rust_vec_len, data_len), +64 => wire__crate__bridge__api__rust_api_get_server_key_for_passwordless_recovery_impl(port, ptr, rust_vec_len, data_len), +65 => wire__crate__bridge__api__rust_api_get_user_by_id_impl(port, ptr, rust_vec_len, data_len), +66 => wire__crate__bridge__api__rust_api_get_user_data_impl(port, ptr, rust_vec_len, data_len), +67 => wire__crate__bridge__api__rust_api_get_user_id_from_username_impl(port, ptr, rust_vec_len, data_len), +68 => wire__crate__bridge__api__rust_api_hand_outbox_to_os_impl(port, ptr, rust_vec_len, data_len), +69 => wire__crate__bridge__api__rust_api_initialize_media_upload_impl(port, ptr, rust_vec_len, data_len), +70 => wire__crate__bridge__api__rust_api_insert_and_send_additional_data_impl(port, ptr, rust_vec_len, data_len), +71 => wire__crate__bridge__api__rust_api_insert_and_send_ask_about_user_impl(port, ptr, rust_vec_len, data_len), +72 => wire__crate__bridge__api__rust_api_insert_and_send_contact_share_impl(port, ptr, rust_vec_len, data_len), +73 => wire__crate__bridge__api__rust_api_insert_and_send_text_impl(port, ptr, rust_vec_len, data_len), +74 => wire__crate__bridge__api__rust_api_ipa_purchase_impl(port, ptr, rust_vec_len, data_len), +75 => wire__crate__bridge__api__rust_api_load_plan_balance_impl(port, ptr, rust_vec_len, data_len), +76 => wire__crate__bridge__api__rust_api_media_size_limit_report_impl(port, ptr, rust_vec_len, data_len), +77 => wire__crate__bridge__api__rust_api_media_step_finished_impl(port, ptr, rust_vec_len, data_len), +78 => wire__crate__bridge__api__rust_api_notification_badge_count_impl(port, ptr, rust_vec_len, data_len), +79 => wire__crate__bridge__api__rust_api_notify_messages_opened_impl(port, ptr, rust_vec_len, data_len), +80 => wire__crate__bridge__api__rust_api_perform_passwordless_recovery_heartbeat_impl(port, ptr, rust_vec_len, data_len), +81 => wire__crate__bridge__api__rust_api_prerender_media_impl(port, ptr, rust_vec_len, data_len), +82 => wire__crate__bridge__api__rust_api_purge_media_temp_folder_impl(port, ptr, rust_vec_len, data_len), +83 => wire__crate__bridge__api__rust_api_purge_widget_media_impl(port, ptr, rust_vec_len, data_len), +84 => wire__crate__bridge__api__rust_api_refresh_widget_manifest_impl(port, ptr, rust_vec_len, data_len), +85 => wire__crate__bridge__api__rust_api_register_impl(port, ptr, rust_vec_len, data_len), +86 => wire__crate__bridge__api__rust_api_register_home_widget_impl(port, ptr, rust_vec_len, data_len), +87 => wire__crate__bridge__api__rust_api_register_passwordless_notification_impl(port, ptr, rust_vec_len, data_len), +88 => wire__crate__bridge__api__rust_api_register_passwordless_recovery_impl(port, ptr, rust_vec_len, data_len), +89 => wire__crate__bridge__api__rust_api_reload_configuration_impl(port, ptr, rust_vec_len, data_len), +90 => wire__crate__bridge__api__rust_api_remove_additional_user_impl(port, ptr, rust_vec_len, data_len), +91 => wire__crate__bridge__api__rust_api_remove_media_files_impl(port, ptr, rust_vec_len, data_len), +92 => wire__crate__bridge__api__rust_api_report_user_impl(port, ptr, rust_vec_len, data_len), +93 => wire__crate__bridge__api__rust_api_request_binary_impl(port, ptr, rust_vec_len, data_len), +94 => wire__crate__bridge__api__rust_api_request_contact_by_username_impl(port, ptr, rust_vec_len, data_len), +95 => wire__crate__bridge__api__rust_api_request_media_reupload_impl(port, ptr, rust_vec_len, data_len), +96 => wire__crate__bridge__api__rust_api_request_memories_upload_impl(port, ptr, rust_vec_len, data_len), +97 => wire__crate__bridge__api__rust_api_retransmit_all_messages_impl(port, ptr, rust_vec_len, data_len), +98 => wire__crate__bridge__api__rust_api_retry_pending_media_reuploads_impl(port, ptr, rust_vec_len, data_len), +99 => wire__crate__bridge__api__rust_api_reupload_pending_media_impl(port, ptr, rust_vec_len, data_len), +100 => wire__crate__bridge__api__rust_api_save_media_to_gallery_impl(port, ptr, rust_vec_len, data_len), +101 => wire__crate__bridge__api__rust_api_send_binary_impl(port, ptr, rust_vec_len, data_len), +102 => wire__crate__bridge__api__rust_api_send_contact_profile_impl(port, ptr, rust_vec_len, data_len), +103 => wire__crate__bridge__api__rust_api_send_encrypted_content_impl(port, ptr, rust_vec_len, data_len), +104 => wire__crate__bridge__api__rust_api_send_encrypted_content_to_group_impl(port, ptr, rust_vec_len, data_len), +105 => wire__crate__bridge__api__rust_api_send_media_to_groups_impl(port, ptr, rust_vec_len, data_len), +106 => wire__crate__bridge__api__rust_api_send_queued_message_impl(port, ptr, rust_vec_len, data_len), +107 => wire__crate__bridge__api__rust_api_send_text_message_impl(port, ptr, rust_vec_len, data_len), +108 => wire__crate__bridge__api__rust_api_send_typing_impl(port, ptr, rust_vec_len, data_len), +109 => wire__crate__bridge__api__rust_api_set_background_impl(port, ptr, rust_vec_len, data_len), +110 => wire__crate__bridge__api__rust_api_set_home_widget_groups_impl(port, ptr, rust_vec_len, data_len), +111 => wire__crate__bridge__api__rust_api_set_login_token_impl(port, ptr, rust_vec_len, data_len), +112 => wire__crate__bridge__api__rust_api_set_media_display_limit_impl(port, ptr, rust_vec_len, data_len), +113 => wire__crate__bridge__api__rust_api_set_media_requires_authentication_impl(port, ptr, rust_vec_len, data_len), +114 => wire__crate__bridge__api__rust_api_set_media_trim_impl(port, ptr, rust_vec_len, data_len), +115 => wire__crate__bridge__api__rust_api_set_network_available_impl(port, ptr, rust_vec_len, data_len), +116 => wire__crate__bridge__api__rust_api_store_media_impl(port, ptr, rust_vec_len, data_len), +117 => wire__crate__bridge__api__rust_api_submit_recovery_share_impl(port, ptr, rust_vec_len, data_len), +118 => wire__crate__bridge__api__rust_api_sync_widget_permissions_impl(port, ptr, rust_vec_len, data_len), +119 => wire__crate__bridge__api__rust_api_toggle_media_remove_audio_impl(port, ptr, rust_vec_len, data_len), +120 => wire__crate__bridge__api__rust_api_try_request_contact_by_id_impl(port, ptr, rust_vec_len, data_len), +121 => wire__crate__bridge__api__rust_api_unregister_home_widget_impl(port, ptr, rust_vec_len, data_len), +122 => wire__crate__bridge__api__rust_api_update_fcm_token_impl(port, ptr, rust_vec_len, data_len), +123 => wire__crate__bridge__api__rust_api_update_signed_pre_key_impl(port, ptr, rust_vec_len, data_len), +124 => wire__crate__bridge__api__rust_api_upload_pqc_pre_keys_impl(port, ptr, rust_vec_len, data_len), +125 => wire__crate__bridge__wrapper__app_database__rust_app_database_changes_impl(port, ptr, rust_vec_len, data_len), +126 => wire__crate__bridge__wrapper__app_database__rust_app_database_execute_impl(port, ptr, rust_vec_len, data_len), +127 => wire__crate__bridge__wrapper__app_database__rust_app_database_legacy_import_complete_impl(port, ptr, rust_vec_len, data_len), +128 => wire__crate__bridge__wrapper__app_database__rust_app_database_migrate_legacy_database_impl(port, ptr, rust_vec_len, data_len), +129 => wire__crate__bridge__wrapper__app_database__rust_app_database_select_impl(port, ptr, rust_vec_len, data_len), +130 => wire__crate__bridge__wrapper__backup__rust_backup_archive_create_backup_archive_impl(port, ptr, rust_vec_len, data_len), +131 => wire__crate__bridge__wrapper__backup__rust_backup_archive_get_backup_download_token_impl(port, ptr, rust_vec_len, data_len), +132 => wire__crate__bridge__wrapper__backup__rust_backup_archive_restore_backup_archive_impl(port, ptr, rust_vec_len, data_len), +133 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_id_impl(port, ptr, rust_vec_len, data_len), +134 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), +135 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_identity_backup_bytes_impl(port, ptr, rust_vec_len, data_len), +136 => wire__crate__bridge__wrapper__backup__rust_backup_identity_import_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), +137 => wire__crate__bridge__wrapper__backup__rust_backup_identity_restore_identity_backup_impl(port, ptr, rust_vec_len, data_len), +138 => wire__crate__bridge__wrapper__backup__rust_backup_identity_set_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), +139 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_decrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len), +140 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_encrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len), +141 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_signal_identity_impl(port, ptr, rust_vec_len, data_len), +142 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_user_id_impl(port, ptr, rust_vec_len, data_len), +143 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_serialized_impl(port, ptr, rust_vec_len, data_len), +144 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_signal_identity_impl(port, ptr, rust_vec_len, data_len), +145 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_local_credentials_impl(port, ptr, rust_vec_len, data_len), +146 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_serialize_impl(port, ptr, rust_vec_len, data_len), +147 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_set_user_id_impl(port, ptr, rust_vec_len, data_len), +148 => wire__crate__bridge__wrapper__signal__rust_signal_decrypt_impl(port, ptr, rust_vec_len, data_len), +149 => wire__crate__bridge__wrapper__signal__rust_signal_encrypt_impl(port, ptr, rust_vec_len, data_len), +150 => wire__crate__bridge__wrapper__signal__rust_signal_generate_bundle_impl(port, ptr, rust_vec_len, data_len), +151 => wire__crate__bridge__wrapper__signal__rust_signal_generate_pqc_prekeys_impl(port, ptr, rust_vec_len, data_len), +152 => wire__crate__bridge__wrapper__signal__rust_signal_get_contact_public_key_impl(port, ptr, rust_vec_len, data_len), +153 => wire__crate__bridge__wrapper__signal__rust_signal_get_user_public_key_impl(port, ptr, rust_vec_len, data_len), +154 => wire__crate__bridge__wrapper__signal__rust_signal_process_prekey_bundle_impl(port, ptr, rust_vec_len, data_len), +155 => wire__crate__bridge__wrapper__rust_utils_generate_shares_impl(port, ptr, rust_vec_len, data_len), +156 => wire__crate__bridge__wrapper__rust_utils_recover_secret_impl(port, ptr, rust_vec_len, data_len), +157 => wire__crate__bridge__webxdc__send_update_impl(port, ptr, rust_vec_len, data_len), +158 => wire__crate__bridge__webxdc__serve_impl(port, ptr, rust_vec_len, data_len), +159 => wire__crate__bridge__groups__update_chat_deletion_time_impl(port, ptr, rust_vec_len, data_len), +160 => wire__crate__bridge__groups__update_group_name_impl(port, ptr, rust_vec_len, data_len), +161 => wire__crate__bridge__webxdc__updates_after_impl(port, ptr, rust_vec_len, data_len), +163 => wire__crate__bridge__user_config__user_config_api_create_impl(port, ptr, rust_vec_len, data_len), +164 => wire__crate__bridge__user_config__user_config_api_import_json_impl(port, ptr, rust_vec_len, data_len), +165 => wire__crate__bridge__user_config__user_config_api_load_impl(port, ptr, rust_vec_len, data_len), +166 => wire__crate__bridge__user_config__user_config_api_save_impl(port, ptr, rust_vec_len, data_len), +167 => wire__crate__bridge__user_config__user_config_api_update_impl(port, ptr, rust_vec_len, data_len), _ => unreachable!(), } } @@ -7569,17 +7726,17 @@ fn pde_ffi_dispatcher_sync_impl( ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { // Codec=Pde (Serialization + dispatch), see doc to use other codecs match func_id { - 30 => wire__crate__bridge__api__rust_api_api_base_url_impl(ptr, rust_vec_len, data_len), - 32 => wire__crate__bridge__api__rust_api_avatar_png_path_impl(ptr, rust_vec_len, data_len), - 44 => { + 33 => wire__crate__bridge__api__rust_api_api_base_url_impl(ptr, rust_vec_len, data_len), + 35 => wire__crate__bridge__api__rust_api_avatar_png_path_impl(ptr, rust_vec_len, data_len), + 47 => { wire__crate__bridge__api__rust_api_decode_avatar_svg_impl(ptr, rust_vec_len, data_len) } - 159 => wire__crate__bridge__user_config__user_config_api_clone_impl( + 162 => wire__crate__bridge__user_config__user_config_api_clone_impl( ptr, rust_vec_len, data_len, ), - 165 => wire__crate__bridge__logging__write_log_impl(ptr, rust_vec_len, data_len), + 168 => wire__crate__bridge__logging__write_log_impl(ptr, rust_vec_len, data_len), _ => unreachable!(), } } @@ -8528,6 +8685,30 @@ impl flutter_rust_bridge::IntoIntoDart flutter_rust_bridge::for_generated::DartAbi { + [ + self.instance_id.into_into_dart().into_dart(), + self.name.into_into_dart().into_dart(), + self.icon.into_into_dart().into_dart(), + self.summary.into_into_dart().into_dart(), + self.document.into_into_dart().into_dart(), + ] + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::bridge::webxdc::WebxdcOneTimeInstance +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::bridge::webxdc::WebxdcOneTimeInstance +{ + fn into_into_dart(self) -> crate::bridge::webxdc::WebxdcOneTimeInstance { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs impl flutter_rust_bridge::IntoDart for crate::bridge::webxdc::WebxdcResponse { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { [ @@ -8562,6 +8743,9 @@ impl flutter_rust_bridge::IntoDart for crate::bridge::webxdc::WebxdcStoreApp { self.source_code_url.into_into_dart().into_dart(), self.icon.into_into_dart().into_dart(), self.bundle_bytes.into_into_dart().into_dart(), + self.pro_only.into_into_dart().into_dart(), + self.one_time.into_into_dart().into_dart(), + self.instance_id.into_into_dart().into_dart(), ] .into_dart() } @@ -9047,6 +9231,16 @@ impl SseEncode for Vec { } } +impl SseEncode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.len() as _, serializer); + for item in self { + ::sse_encode(item, serializer); + } + } +} + impl SseEncode for Vec { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -9570,6 +9764,17 @@ impl SseEncode for crate::bridge::webxdc::WebxdcInstanceInfo { } } +impl SseEncode for crate::bridge::webxdc::WebxdcOneTimeInstance { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.instance_id, serializer); + ::sse_encode(self.name, serializer); + >>::sse_encode(self.icon, serializer); + >::sse_encode(self.summary, serializer); + >::sse_encode(self.document, serializer); + } +} + impl SseEncode for crate::bridge::webxdc::WebxdcResponse { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -9591,6 +9796,9 @@ impl SseEncode for crate::bridge::webxdc::WebxdcStoreApp { >::sse_encode(self.source_code_url, serializer); >>::sse_encode(self.icon, serializer); ::sse_encode(self.bundle_bytes, serializer); + ::sse_encode(self.pro_only, serializer); + ::sse_encode(self.one_time, serializer); + >::sse_encode(self.instance_id, serializer); } } diff --git a/rust/src/services/messages.rs b/rust/src/services/messages.rs index 440f08b0..9b3ca33f 100644 --- a/rust/src/services/messages.rs +++ b/rust/src/services/messages.rs @@ -39,7 +39,7 @@ impl MessageService { || content.media.is_some() || content.text_message.is_some() { - sqlx::query!("UPDATE groups SET last_message_exchange = CAST(strftime('%s','now') AS INTEGER) WHERE group_id = ?", group_id) + sqlx::query!("UPDATE groups SET last_message_exchange = CAST(strftime('%s','now') AS INTEGER), deleted_content = 0 WHERE group_id = ?", group_id) .execute(&database.pool).await?; } let members = sqlx::query_scalar!( @@ -412,6 +412,8 @@ impl MessageService { webxdc_app: None, webxdc_update: None, webxdc_origin: None, + webxdc_sync: None, + webxdc_sync_request: None, } .encode_to_vec(); self.insert_and_send_additional_data(group_id, "contacts".into(), data, false) @@ -447,6 +449,8 @@ impl MessageService { webxdc_app: None, webxdc_update: None, webxdc_origin: None, + webxdc_sync: None, + webxdc_sync_request: None, } .encode_to_vec(); self.insert_and_send_additional_data(group_id, "askAboutUser".into(), data, false) diff --git a/rust/src/services/notifications.rs b/rust/src/services/notifications.rs index 7a75f3c7..31fdc25b 100644 --- a/rust/src/services/notifications.rs +++ b/rust/src/services/notifications.rs @@ -31,12 +31,30 @@ static DE_TRANSLATIONS: LazyLock> = /// The update itself is hidden -- app state rather than something a person /// sent -- but an `info` is written to be read, and becomes a chat row of its /// own on arrival. That row is what the receiver is woken and notified for. -fn webxdc_announcement(message: &encrypted_content::AdditionalDataMessage) -> Option { +fn webxdc_announcement( + message: &encrypted_content::AdditionalDataMessage, + user_id: i64, +) -> Option { let data = ::decode( message.additional_message_data.as_deref()?, ) .ok()?; - crate::services::webxdc::announcement(&data.webxdc_update?) + let update = data.webxdc_update?; + // Keep chat announcements shared, but only wake recipients selected by the app. + let announcement = crate::services::webxdc::announcement(&update)?; + match update.notify.as_deref() { + None => Some(announcement), + Some(raw) => { + let recipients: std::collections::BTreeMap = + serde_json::from_str(raw).ok()?; + let address = + crate::services::webxdc::WebxdcService::address_for(&update.instance_id, user_id); + let text = recipients.get(&address)?; + let mut notification = update; + notification.info = Some(text.clone()); + crate::services::webxdc::announcement(¬ification) + } + } } /// Who wrote the message with this id, as this device has it. @@ -85,7 +103,9 @@ pub(crate) async fn should_wake_receiver( || content .additional_data_message .as_ref() - .is_some_and(|message| !message.hidden || webxdc_announcement(message).is_some()) + .is_some_and(|message| { + !message.hidden || webxdc_announcement(message, target_user_id).is_some() + }) || content.group_create.is_some() || content.media.as_ref().is_some_and(|value| { // Widget media lands already opened and is deliberately recorded @@ -199,6 +219,7 @@ impl NotificationDraft { /// transaction that commits that message. Transport retries are deduplicated /// by the receipt-derived event ID. pub(crate) async fn record_incoming_event( + ctx: &Context, transaction: &mut Transaction<'_, Sqlite>, from_user_id: i64, receipt_id: &str, @@ -270,9 +291,10 @@ pub(crate) async fn record_incoming_event( // announcement -- and only once the row it materialised into exists, // which it does not when the update was dropped for an unknown // instance, a mismatched chat, or an app that is over budget. + let self_id = UserConfig::load_required_from(ctx)?.user_id; let announcement = message .hidden - .then(|| webxdc_announcement(message)) + .then(|| webxdc_announcement(message, self_id)) .flatten(); let message_id = if message.hidden { crate::services::webxdc::WebxdcService::info_message_id(&message.sender_message_id) @@ -842,7 +864,7 @@ mod tests { } fn announcement_of(content: &proto::EncryptedContent) -> Option { - webxdc_announcement(content.additional_data_message.as_ref().unwrap()) + webxdc_announcement(content.additional_data_message.as_ref().unwrap(), 42) } #[test] @@ -857,6 +879,62 @@ mod tests { assert!(announcement_of(&webxdc_update(Some(" \u{200e} "))).is_none()); } + fn targeted_update(notify: &str) -> proto::EncryptedContent { + let mut content = webxdc_update(Some("Dinner added")); + let message = content.additional_data_message.as_mut().unwrap(); + let mut data = ::decode( + message.additional_message_data.as_deref().unwrap(), + ) + .unwrap(); + data.webxdc_update.as_mut().unwrap().notify = Some(notify.to_owned()); + message.additional_message_data = Some(prost::Message::encode_to_vec(&data)); + content + } + + #[tokio::test] + async fn expense_notifications_only_wake_and_record_affected_members() -> Result<()> { + let directory = tempfile::tempdir()?; + let ctx = + Context::init_for_testing(directory.path().join("db"), directory.path().join("data")) + .await?; + UserConfig::save_json( + &ctx, + r#"{"userId":42,"username":"ben","displayName":"Ben"}"#, + )?; + let database = ctx.app_db.read().await.clone(); + let mut tx = database.pool.begin().await?; + sqlx::query("INSERT INTO contacts(user_id, username) VALUES (7, 'anna')") + .execute(&mut *tx) + .await?; + sqlx::query("INSERT INTO groups(group_id, group_name) VALUES ('g', 'Trip')") + .execute(&mut *tx) + .await?; + sqlx::query("INSERT INTO messages(message_id, group_id, sender_id, type, content) VALUES ('message-info', 'g', 7, 'text', 'Dinner added')").execute(&mut *tx).await?; + let address = crate::services::webxdc::WebxdcService::address_for("instance", 42); + let raw = serde_json::json!({address: "You share dinner: €20"}).to_string(); + let content = targeted_update(&raw); + assert!(should_wake_receiver(&mut tx, 42, &content).await?); + assert!(!should_wake_receiver(&mut tx, 43, &content).await?); + record_incoming_event(&ctx, &mut tx, 7, "included", &content).await?; + record_incoming_event(&ctx, &mut tx, 7, "included", &content).await?; + for raw in ["{}", "null", "not JSON", r#"{"someone-else":"Dinner"}"#] { + let content = targeted_update(raw); + assert!(!should_wake_receiver(&mut tx, 42, &content).await?); + record_incoming_event(&ctx, &mut tx, 7, "excluded", &content).await?; + } + let rows: Vec<(String, String)> = + sqlx::query_as("SELECT event_id, content FROM notification_outbox") + .fetch_all(&mut *tx) + .await?; + assert_eq!( + rows, + vec![("included".into(), "You share dinner: €20".into())] + ); + // Older apps omit notify and retain their existing announcements. + assert!(should_wake_receiver(&mut tx, 43, &webxdc_update(Some("Your turn"))).await?); + Ok(()) + } + fn pending_row(kind: &str) -> PendingRow { PendingRow { event_id: "event".into(), diff --git a/rust/src/services/webxdc/mod.rs b/rust/src/services/webxdc/mod.rs index daa5aa5d..941b2752 100644 --- a/rust/src/services/webxdc/mod.rs +++ b/rust/src/services/webxdc/mod.rs @@ -13,12 +13,13 @@ pub mod bundle; pub mod store; +use crate::api::messages::outgoing::send_c2c_message_to_contact; use crate::api::proto::client as proto; use crate::context::Context; use crate::database::app::tables::{MessageType, NewMessage}; use crate::error::{Result, TwonlyError}; use hmac::{Hmac, Mac}; -use sha2::Sha256; +use sha2::{Digest, Sha256}; use sqlx::{Sqlite, Transaction}; use std::sync::Arc; @@ -34,6 +35,41 @@ const MAX_UPDATES_PER_INSTANCE: i64 = 100_000; const MAX_PAYLOAD_BYTES_PER_INSTANCE: i64 = 32 * 1024 * 1024; /// Sending burst allowance, counted over the trailing minute. const MAX_UPDATES_PER_MINUTE: i64 = 12; +const SYNC_CHUNK_BYTES: usize = 48 * 1024; +const MAX_SYNC_CHUNKS: u32 = 700; + +#[derive(sqlx::FromRow)] +struct SyncInstanceRow { + instance_id: String, + app_id: String, + version: i64, + summary: Option, + document: Option, + created_at: i64, + last_update_at: i64, +} + +#[derive(sqlx::FromRow)] +struct SyncUpdateRow { + message_id: String, + sender_id: Option, + payload: String, + info: Option, + href: Option, + received_at: i64, +} + +#[derive(sqlx::FromRow)] +struct PendingUpdateRow { + message_id: String, + sender_id: i64, + payload: String, + info: Option, + href: Option, + summary: Option, + document: Option, + received_at: i64, +} /// Text an app can put in front of the user, outside its own sandbox. Bounded /// so a chat row or a notification cannot be filled with app-controlled text. @@ -74,6 +110,340 @@ impl WebxdcService { store::WebxdcStore::new(&self.ctx) } + /// Requests existing one-time app state after joining a group. Spawned so + /// the group-create transaction can commit before the sender needs the app + /// database connection. + pub(crate) fn spawn_sync_request(ctx: &Arc, group_id: String, contact_id: i64) { + let ctx = ctx.clone(); + tokio::spawn(async move { + let data = proto::AdditionalMessageData { + r#type: proto::additional_message_data::Type::WebxdcSyncRequest as i32, + webxdc_sync_request: Some(proto::WebxdcSyncRequest {}), + ..Default::default() + }; + let content = proto::EncryptedContent { + group_id: Some(group_id), + additional_data_message: Some(proto::encrypted_content::AdditionalDataMessage { + sender_message_id: uuid::Uuid::new_v4().to_string(), + timestamp: chrono::Utc::now().timestamp_millis(), + r#type: "webxdcSyncRequest".into(), + additional_message_data: Some(prost::Message::encode_to_vec(&data)), + hidden: true, + }), + ..Default::default() + }; + if let Err(error) = send_c2c_message_to_contact() + .ctx(&ctx) + .contact_id(contact_id) + .encrypted_content(prost::Message::encode_to_vec(&content)) + .call() + .await + { + tracing::warn!(%error, "could not request one-time app state"); + } + }); + } + + /// Sends every one-time app and its authoritative update log to one new + /// member. Transfers are hidden, encrypted contact-to-contact messages. + pub async fn send_one_time_apps_to_contact( + &self, + group_id: &str, + contact_id: i64, + ) -> Result<()> { + let database = self.ctx.app_db.read().await.clone(); + let local_user_id = self.ctx.user_id().await?; + let instances = sqlx::query_as::<_, SyncInstanceRow>( + r#"SELECT instance.instance_id, instance.app_id, instance.version, + instance.summary, instance.document, instance.created_at, + instance.last_update_at + FROM webxdc_instances AS instance + JOIN webxdc_apps AS app + ON app.app_id = instance.app_id AND app.version = instance.version + WHERE instance.group_id = ? AND app.one_time = 1 + ORDER BY instance.created_at, instance.instance_id"#, + ) + .bind(group_id) + .fetch_all(&database.pool) + .await?; + + for instance in instances { + let updates = sqlx::query_as::<_, SyncUpdateRow>( + r#"SELECT message_id, sender_id, payload, info, href, received_at + FROM webxdc_updates WHERE instance_id = ? ORDER BY serial"#, + ) + .bind(&instance.instance_id) + .fetch_all(&database.pool) + .await?; + let payload = proto::WebxdcSyncPayload { + instances: vec![proto::WebxdcSyncInstance { + instance_id: instance.instance_id, + app_id: instance.app_id, + version: instance.version, + summary: instance.summary, + document: instance.document, + created_at: instance.created_at, + last_update_at: instance.last_update_at, + updates: updates + .into_iter() + .map(|update| proto::WebxdcSyncUpdate { + message_id: update.message_id, + sender_id: update.sender_id.or(Some(local_user_id)), + payload: update.payload, + info: update.info, + href: update.href, + received_at: update.received_at, + }) + .collect(), + }], + }; + let encoded = prost::Message::encode_to_vec(&payload); + let chunk_count = encoded.len().div_ceil(SYNC_CHUNK_BYTES) as u32; + if chunk_count == 0 || chunk_count > MAX_SYNC_CHUNKS { + tracing::warn!( + instance_id = payload.instances[0].instance_id, + "one-time app is too large to sync" + ); + continue; + } + let transfer_id = uuid::Uuid::new_v4().to_string(); + let digest = Sha256::digest(&encoded).to_vec(); + for (chunk_index, bytes) in encoded.chunks(SYNC_CHUNK_BYTES).enumerate() { + let data = proto::AdditionalMessageData { + r#type: proto::additional_message_data::Type::WebxdcSync as i32, + webxdc_sync: Some(proto::WebxdcSyncChunk { + transfer_id: transfer_id.clone(), + chunk_index: chunk_index as u32, + chunk_count, + payload_sha256: digest.clone(), + payload: bytes.to_vec(), + }), + ..Default::default() + }; + let content = proto::EncryptedContent { + group_id: Some(group_id.to_owned()), + additional_data_message: Some( + proto::encrypted_content::AdditionalDataMessage { + sender_message_id: uuid::Uuid::new_v4().to_string(), + timestamp: chrono::Utc::now().timestamp_millis(), + r#type: "webxdcSync".into(), + additional_message_data: Some(prost::Message::encode_to_vec(&data)), + hidden: true, + }, + ), + ..Default::default() + }; + send_c2c_message_to_contact() + .ctx(&self.ctx) + .contact_id(contact_id) + .encrypted_content(prost::Message::encode_to_vec(&content)) + .call() + .await?; + } + } + Ok(()) + } + + pub(crate) async fn handle_sync_chunk( + transaction: &mut Transaction<'_, Sqlite>, + group_id: &str, + sender_id: i64, + chunk: &proto::WebxdcSyncChunk, + ) -> Result<()> { + if chunk.transfer_id.is_empty() + || chunk.chunk_count == 0 + || chunk.chunk_count > MAX_SYNC_CHUNKS + || chunk.chunk_index >= chunk.chunk_count + || chunk.payload.len() > SYNC_CHUNK_BYTES + || chunk.payload_sha256.len() != 32 + { + return Err(TwonlyError::Generic("invalid webxdc sync chunk".into())); + } + let sender_is_member: bool = sqlx::query_scalar( + "SELECT EXISTS(SELECT 1 FROM group_members WHERE group_id = ? AND contact_id = ? AND (member_state IS NULL OR member_state != 'leftGroup'))", + ) + .bind(group_id) + .bind(sender_id) + .fetch_one(&mut **transaction) + .await?; + if !sender_is_member { + return Err(TwonlyError::Generic( + "webxdc sync sender is not a group member".into(), + )); + } + + sqlx::query( + r#"INSERT INTO webxdc_sync_chunks + (transfer_id, group_id, sender_id, chunk_index, chunk_count, + payload_sha256, payload) + VALUES (?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(transfer_id, sender_id, chunk_index) DO NOTHING"#, + ) + .bind(&chunk.transfer_id) + .bind(group_id) + .bind(sender_id) + .bind(chunk.chunk_index as i64) + .bind(chunk.chunk_count as i64) + .bind(&chunk.payload_sha256) + .bind(&chunk.payload) + .execute(&mut **transaction) + .await?; + sqlx::query( + "DELETE FROM webxdc_sync_chunks WHERE created_at < strftime('%s','now') - 86400", + ) + .execute(&mut **transaction) + .await?; + + let rows: Vec<(i64, i64, Vec, Vec)> = sqlx::query_as( + r#"SELECT chunk_index, chunk_count, payload_sha256, payload + FROM webxdc_sync_chunks + WHERE transfer_id = ? AND sender_id = ? + ORDER BY chunk_index"#, + ) + .bind(&chunk.transfer_id) + .bind(sender_id) + .fetch_all(&mut **transaction) + .await?; + if rows.len() != chunk.chunk_count as usize { + return Ok(()); + } + if rows.iter().enumerate().any(|(index, row)| { + row.0 != index as i64 + || row.1 != chunk.chunk_count as i64 + || row.2 != chunk.payload_sha256 + }) { + return Err(TwonlyError::Generic( + "inconsistent webxdc sync chunks".into(), + )); + } + let encoded: Vec = rows.into_iter().flat_map(|row| row.3).collect(); + if Sha256::digest(&encoded).as_slice() != chunk.payload_sha256.as_slice() { + return Err(TwonlyError::Generic("webxdc sync digest mismatch".into())); + } + let payload = ::decode(encoded.as_slice())?; + Self::install_sync_payload(transaction, group_id, sender_id, payload).await?; + sqlx::query("DELETE FROM webxdc_sync_chunks WHERE transfer_id = ? AND sender_id = ?") + .bind(&chunk.transfer_id) + .bind(sender_id) + .execute(&mut **transaction) + .await?; + Ok(()) + } + + async fn install_sync_payload( + transaction: &mut Transaction<'_, Sqlite>, + group_id: &str, + sender_id: i64, + payload: proto::WebxdcSyncPayload, + ) -> Result<()> { + if payload.instances.len() > 1 { + return Err(TwonlyError::Generic("invalid webxdc sync payload".into())); + } + for instance in payload.instances { + if instance.instance_id.is_empty() + || instance.app_id.is_empty() + || instance.updates.len() as i64 > MAX_UPDATES_PER_INSTANCE + || instance + .updates + .iter() + .map(|update| update.payload.len() as i64) + .sum::() + > MAX_PAYLOAD_BYTES_PER_INSTANCE + { + return Err(TwonlyError::Generic("invalid webxdc sync payload".into())); + } + let exists: bool = sqlx::query_scalar( + "SELECT EXISTS(SELECT 1 FROM webxdc_instances WHERE instance_id = ?)", + ) + .bind(&instance.instance_id) + .fetch_one(&mut **transaction) + .await?; + if exists { + Self::apply_pending_updates( + transaction, + &instance.instance_id, + &instance.app_id, + instance.version, + group_id, + ) + .await?; + continue; + } + let app_data = proto::AdditionalMessageData { + r#type: proto::additional_message_data::Type::WebxdcApp as i32, + webxdc_app: Some(proto::WebxdcApp { + app_id: instance.app_id.clone(), + version: instance.version, + }), + ..Default::default() + }; + sqlx::query( + r#"INSERT INTO messages + (group_id, message_id, sender_id, type, + additional_message_data, created_at, ack_by_server) + VALUES (?, ?, ?, 'webxdcApp', ?, ?, strftime('%s','now')) + ON CONFLICT(message_id) DO NOTHING"#, + ) + .bind(group_id) + .bind(&instance.instance_id) + .bind(sender_id) + .bind(prost::Message::encode_to_vec(&app_data)) + .bind(instance.created_at) + .execute(&mut **transaction) + .await?; + Self::insert_instance( + transaction, + &instance.instance_id, + group_id, + &instance.app_id, + instance.version, + ) + .await?; + sqlx::query("UPDATE webxdc_instances SET created_at = ? WHERE instance_id = ?") + .bind(instance.created_at) + .bind(&instance.instance_id) + .execute(&mut **transaction) + .await?; + for update in instance.updates { + if update.message_id.is_empty() || update.payload.len() > SEND_UPDATE_MAX_SIZE { + return Err(TwonlyError::Generic("invalid webxdc sync update".into())); + } + let info = sanitize(update.info.as_deref(), MAX_INFO_CHARS); + let href = update.href.as_deref().and_then(sanitize_href); + Self::append_update( + transaction, + &instance.instance_id, + &update.message_id, + update.sender_id, + &update.payload, + info.as_deref(), + href.as_deref(), + update.received_at, + ) + .await?; + } + let summary = sanitize(instance.summary.as_deref(), MAX_SUMMARY_CHARS); + let document = sanitize(instance.document.as_deref(), MAX_DOCUMENT_CHARS); + Self::apply_instance_labels( + transaction, + &instance.instance_id, + summary.as_deref(), + document.as_deref(), + instance.last_update_at, + ) + .await?; + Self::apply_pending_updates( + transaction, + &instance.instance_id, + &instance.app_id, + instance.version, + group_id, + ) + .await?; + } + Ok(()) + } + /// Places an app into a chat and tells the other members which app and /// which version, so every participant runs the same code. pub async fn create_instance( @@ -87,6 +457,16 @@ impl WebxdcService { // device. self.store().resolve_bundle_sha256(&app_id, version).await?; + if self + .existing_instance_if_limited(&group_id, &app_id, version) + .await? + .is_some() + { + return Err(TwonlyError::Generic( + "this app has already been added to the chat".into(), + )); + } + let data = proto::AdditionalMessageData { r#type: proto::additional_message_data::Type::WebxdcApp as i32, webxdc_app: Some(proto::WebxdcApp { @@ -113,6 +493,32 @@ impl WebxdcService { Ok(message_id) } + async fn existing_instance_if_limited( + &self, + group_id: &str, + app_id: &str, + version: i64, + ) -> Result> { + let database = self.ctx.app_db.read().await.clone(); + let one_time: bool = sqlx::query_scalar( + "SELECT one_time FROM webxdc_apps WHERE app_id = ? AND version = ? AND published = 1", + ) + .bind(app_id) + .bind(version) + .fetch_one(&database.pool) + .await?; + if !one_time { + return Ok(None); + } + Ok(sqlx::query_scalar( + "SELECT instance_id FROM webxdc_instances WHERE group_id = ? AND app_id = ? LIMIT 1", + ) + .bind(group_id) + .bind(app_id) + .fetch_optional(&database.pool) + .await?) + } + /// Records the instance a peer placed into a chat. Nothing is downloaded /// here: a message must not be able to make a device fetch a bundle, only /// the user tapping Start may. @@ -122,7 +528,9 @@ impl WebxdcService { group_id: &str, app: &proto::WebxdcApp, ) -> Result<()> { - Self::insert_instance(transaction, message_id, group_id, &app.app_id, app.version).await + Self::insert_instance(transaction, message_id, group_id, &app.app_id, app.version).await?; + Self::apply_pending_updates(transaction, message_id, &app.app_id, app.version, group_id) + .await } async fn insert_instance( @@ -165,6 +573,10 @@ impl WebxdcService { update: &proto::WebxdcUpdate, received_at: i64, ) -> Result<()> { + if update.instance_id.is_empty() || update.payload.len() > SEND_UPDATE_MAX_SIZE { + tracing::warn!("invalid webxdc update, dropping"); + return Ok(()); + } let instance = sqlx::query!( r#"SELECT group_id AS "group_id!: String", app_id AS "app_id!: String", version AS "version!: i64" @@ -175,20 +587,53 @@ impl WebxdcService { .await?; let Some(instance) = instance else { - // The card may still be on its way, or may already have been - // deleted. Dropping the update is the only safe answer: a log entry - // with no instance cannot be replayed in order. - tracing::info!("webxdc update for an unknown instance, dropping"); + // A newly joined member can receive a live update before the app + // snapshot. Keep it until the instance arrives; deletion removes + // these rows with the group. + sqlx::query( + "DELETE FROM webxdc_pending_updates WHERE received_at < strftime('%s','now') - 604800", + ) + .execute(&mut **transaction) + .await?; + let pending: (i64, i64) = sqlx::query_as( + r#"SELECT COUNT(*), COALESCE(SUM(LENGTH(payload)), 0) + FROM webxdc_pending_updates WHERE instance_id = ? AND group_id = ?"#, + ) + .bind(&update.instance_id) + .bind(group_id) + .fetch_one(&mut **transaction) + .await?; + if pending.0 >= MAX_UPDATES_PER_INSTANCE + || pending.1 + update.payload.len() as i64 > MAX_PAYLOAD_BYTES_PER_INSTANCE + { + tracing::warn!("pending webxdc instance is over budget, dropping update"); + return Ok(()); + } + sqlx::query( + r#"INSERT INTO webxdc_pending_updates + (message_id, instance_id, group_id, sender_id, payload, + info, href, summary, document, received_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(message_id) DO NOTHING"#, + ) + .bind(message_id) + .bind(&update.instance_id) + .bind(group_id) + .bind(sender_id) + .bind(&update.payload) + .bind(&update.info) + .bind(&update.href) + .bind(&update.summary) + .bind(&update.document) + .bind(received_at) + .execute(&mut **transaction) + .await?; return Ok(()); }; if instance.group_id != group_id { tracing::warn!("webxdc update arrived in the wrong chat, dropping"); return Ok(()); } - if update.payload.len() > SEND_UPDATE_MAX_SIZE { - tracing::warn!("webxdc update exceeds the payload limit, dropping"); - return Ok(()); - } if !Self::within_instance_budget(transaction, &update.instance_id).await? { tracing::warn!("webxdc instance is over budget, dropping update"); return Ok(()); @@ -237,6 +682,79 @@ impl WebxdcService { Ok(()) } + #[allow(clippy::too_many_arguments)] + async fn apply_pending_updates( + transaction: &mut Transaction<'_, Sqlite>, + instance_id: &str, + app_id: &str, + version: i64, + group_id: &str, + ) -> Result<()> { + let rows = sqlx::query_as::<_, PendingUpdateRow>( + r#"SELECT message_id, sender_id, payload, info, href, summary, document, received_at + FROM webxdc_pending_updates + WHERE instance_id = ? AND group_id = ? + ORDER BY received_at, message_id"#, + ) + .bind(instance_id) + .bind(group_id) + .fetch_all(&mut **transaction) + .await?; + + for row in rows { + if row.message_id.is_empty() || row.payload.len() > SEND_UPDATE_MAX_SIZE { + continue; + } + if !Self::within_instance_budget(transaction, instance_id).await? { + tracing::warn!("webxdc instance is over budget, dropping pending updates"); + break; + } + let info = sanitize(row.info.as_deref(), MAX_INFO_CHARS); + let href = row.href.as_deref().and_then(sanitize_href); + let summary = sanitize(row.summary.as_deref(), MAX_SUMMARY_CHARS); + let document = sanitize(row.document.as_deref(), MAX_DOCUMENT_CHARS); + Self::append_update( + transaction, + instance_id, + &row.message_id, + Some(row.sender_id), + &row.payload, + info.as_deref(), + href.as_deref(), + row.received_at, + ) + .await?; + Self::apply_instance_labels( + transaction, + instance_id, + summary.as_deref(), + document.as_deref(), + row.received_at, + ) + .await?; + if let Some(info) = info.as_deref() { + Self::insert_info_message( + transaction, + instance_id, + app_id, + version, + group_id, + &row.message_id, + Some(row.sender_id), + info, + row.received_at, + ) + .await?; + } + } + sqlx::query("DELETE FROM webxdc_pending_updates WHERE instance_id = ? AND group_id = ?") + .bind(instance_id) + .bind(group_id) + .execute(&mut **transaction) + .await?; + Ok(()) + } + /// Writes an update's `info` into the chat as a message of its own. /// /// An app announces things a person is meant to read -- "a new game @@ -388,6 +906,7 @@ impl WebxdcService { href: Option, summary: Option, document: Option, + notify: Option, ) -> Result<()> { if payload.len() > SEND_UPDATE_MAX_SIZE { return Err(TwonlyError::Generic(format!( @@ -417,6 +936,38 @@ impl WebxdcService { )); } + let notify = if let Some(raw) = notify { + if raw.len() > SEND_UPDATE_MAX_SIZE { + return Err(TwonlyError::Generic( + "notification list is too large".into(), + )); + } + let entries: std::collections::BTreeMap = serde_json::from_str(&raw) + .map_err(|_| { + TwonlyError::Generic("notify must map member addresses to text".into()) + })?; + let members = self.members(&instance_id).await?; + let members: serde_json::Value = serde_json::from_str(&members)?; + let mut cleaned = std::collections::BTreeMap::new(); + for (address, text) in entries { + if !members["members"] + .as_array() + .unwrap() + .iter() + .any(|m| m["id"] == address) + { + return Err(TwonlyError::Generic( + "unknown notification recipient".into(), + )); + } + if let Some(text) = sanitize(Some(&text), MAX_INFO_CHARS) { + cleaned.insert(address, text); + } + } + Some(serde_json::to_string(&cleaned)?) + } else { + None + }; let info = sanitize(info.as_deref(), MAX_INFO_CHARS); let href = href.as_deref().and_then(sanitize_href); let summary = sanitize(summary.as_deref(), MAX_SUMMARY_CHARS); @@ -431,6 +982,7 @@ impl WebxdcService { href: href.clone(), summary: summary.clone(), document: document.clone(), + notify, }), ..Default::default() }; @@ -540,32 +1092,36 @@ impl WebxdcService { .collect()) } - /// Starts an app: checks the store for a newer version, and makes the - /// bundle the instance ends up on available on disk. - /// - /// Called when the user starts the app, never on message arrival, and never - /// again while the app runs. This is the one moment an update may land: the - /// code behind a game changes between two of its runs, never between two of - /// its own updates. + /// Starts with local code. Only a missing bundle needs the network. + /// Updates downloaded after a previous close are adopted here, before any + /// files are served; an in-flight background download never changes a run. pub async fn prepare_bundle(&self, instance_id: &str) -> Result { let instance = self .instance(instance_id) .await? .ok_or_else(|| TwonlyError::Generic("unknown webxdc instance".into()))?; - - // A refresh that fails means this device is offline or the server is - // down. Neither is a reason to refuse to start an app whose bundle is - // already here, so the failure is logged and the instance keeps the - // version it has. - if let Err(error) = self.store().refresh_catalog().await { - tracing::info!("checking for a webxdc update failed: {error}"); - } - if let Some(path) = self.apply_update(&instance, instance_id).await? { + if let Some(path) = self.apply_cached_update(&instance, instance_id).await? { return Ok(path); } self.pinned_bundle(instance_id).await } + /// Checks and downloads after the app closes, without moving its pin. + /// The user may already have reopened it when the network finishes. + pub async fn cache_update(&self, instance_id: &str) -> Result<()> { + let Some(instance) = self.instance(instance_id).await? else { + return Ok(()); + }; + let store = self.store(); + store.refresh_catalog().await?; + if let Some((version, sha256)) = store.newest_published(&instance.app_id).await? { + if version > instance.version { + store.ensure_bundle(&sha256).await?; + } + } + Ok(()) + } + /// The bundle an instance is pinned to, on disk and verified. /// /// No catalog lookup and no update check: this answers every file the @@ -592,14 +1148,9 @@ impl WebxdcService { store.ensure_bundle(&sha256).await } - /// Moves an instance to a newer published version, if there is one. - /// - /// Returns the bundle it moved to, or `None` when the instance is to stay - /// where it is -- because nothing newer is published, or because the newer - /// bundle could not be fetched. The pin is written only after the download - /// verified: an instance must never end up pointing at code this device - /// does not have, when it does have the code it was running before. - async fn apply_update( + /// Adopts a newer version only if its complete, verified download is on disk. + /// Never fetch here: cached apps must open even while the server is down. + async fn apply_cached_update( &self, instance: &WebxdcInstance, instance_id: &str, @@ -608,32 +1159,20 @@ impl WebxdcService { let Some((version, sha256)) = store.newest_published(&instance.app_id).await? else { return Ok(None); }; - if version <= instance.version { + let path = store.bundle_path(&sha256); + if version <= instance.version || !path.is_file() { return Ok(None); } - - match store.ensure_bundle(&sha256).await { - Ok(path) => { - Self::pin(&self.ctx, instance_id, version, &sha256).await?; - tracing::info!( - "webxdc {} moved from version {} to {version}", - instance.app_id, - instance.version, - ); - if let Some(previous) = &instance.bundle_sha256 { - self.discard_unused_bundle(previous).await; - } - Ok(Some(path)) - } - // Nothing has been written yet, so the caller starts the version - // the instance already had. - Err(error) if instance.bundle_sha256.is_some() => { - tracing::warn!("fetching the webxdc update failed: {error}"); - Ok(None) - } - // Nothing to fall back to: this instance has never run here. - Err(error) => Err(error), + Self::pin(&self.ctx, instance_id, version, &sha256).await?; + tracing::info!( + "webxdc {} moved from version {} to {version}", + instance.app_id, + instance.version + ); + if let Some(previous) = &instance.bundle_sha256 { + self.discard_unused_bundle(previous).await; } + Ok(Some(path)) } /// Deletes a bundle nothing points at any more, after an update moved the @@ -673,6 +1212,34 @@ impl WebxdcService { Ok(()) } + /// Current chat members, using the same instance-scoped IDs as selfAddr. + pub async fn members(&self, instance_id: &str) -> Result { + let instance = self + .instance(instance_id) + .await? + .ok_or_else(|| TwonlyError::Generic("unknown webxdc instance".into()))?; + let user = crate::user_config::UserConfig::load_required_from(&self.ctx)?; + let database = self.ctx.app_db.read().await.clone(); + let rows: Vec<(i64, String)> = sqlx::query_as( + "SELECT c.user_id, COALESCE(NULLIF(c.display_name, ''), c.username) + FROM group_members m JOIN contacts c ON c.user_id = m.contact_id + WHERE m.group_id = ? AND (m.member_state IS NULL OR m.member_state != 'leftGroup') + ORDER BY c.user_id", + ) + .bind(&instance.group_id) + .fetch_all(&database.pool) + .await?; + let self_id = Self::address_for(instance_id, user.user_id); + let mut members = + vec![serde_json::json!({"id": self_id, "displayName": user.display_name})]; + for (id, name) in rows { + if id != user.user_id { + members.push(serde_json::json!({"id": Self::address_for(instance_id, id), "displayName": name})); + } + } + Ok(serde_json::json!({"selfId": self_id, "members": members}).to_string()) + } + /// The values `webxdc.js` is served with, as a JSON object. /// /// Produced here rather than passed in, so what the app is told about its @@ -790,6 +1357,225 @@ fn sanitize_href(href: &str) -> Option { #[cfg(test)] mod tests { use super::{proto, sanitize, sanitize_href, Context, Result, WebxdcService}; + use sha2::{Digest, Sha256}; + + #[tokio::test] + async fn sync_chunks_install_one_time_app_only_when_complete() -> Result<()> { + let directory = tempfile::tempdir()?; + let ctx = + Context::init_for_testing(directory.path().join("db"), directory.path().join("data")) + .await?; + let database = ctx.app_db.read().await.clone(); + for statement in [ + "INSERT INTO contacts(user_id, username) VALUES (7, 'sender')", + "INSERT INTO groups(group_id, group_name) VALUES ('g', 'Chat')", + "INSERT INTO group_members(group_id, contact_id) VALUES ('g', 7)", + ] { + sqlx::query(statement).execute(&database.pool).await?; + } + let payload = proto::WebxdcSyncPayload { + instances: vec![proto::WebxdcSyncInstance { + instance_id: "card".into(), + app_id: "expenses".into(), + version: 1, + summary: Some("Trip".into()), + document: Some("3 expenses".into()), + created_at: 100, + last_update_at: 200, + updates: vec![proto::WebxdcSyncUpdate { + message_id: "update".into(), + sender_id: Some(7), + payload: r#"{"expense":1}"#.into(), + info: Some("Dinner".into()), + href: None, + received_at: 150, + }], + }], + }; + let encoded = prost::Message::encode_to_vec(&payload); + let split = encoded.len() / 2; + let digest = Sha256::digest(&encoded).to_vec(); + let chunks = [ + proto::WebxdcSyncChunk { + transfer_id: "transfer".into(), + chunk_index: 0, + chunk_count: 2, + payload_sha256: digest.clone(), + payload: encoded[..split].to_vec(), + }, + proto::WebxdcSyncChunk { + transfer_id: "transfer".into(), + chunk_index: 1, + chunk_count: 2, + payload_sha256: digest, + payload: encoded[split..].to_vec(), + }, + ]; + let mut transaction = database.pool.begin().await?; + WebxdcService::handle_incoming_update( + &mut transaction, + "live-update", + "g", + 7, + &proto::WebxdcUpdate { + instance_id: "card".into(), + payload: r#"{"expense":2}"#.into(), + info: Some("Taxi".into()), + href: None, + summary: Some("Trip updated".into()), + document: Some("2 expenses".into()), + notify: None, + }, + 250, + ) + .await?; + WebxdcService::handle_sync_chunk(&mut transaction, "g", 7, &chunks[1]).await?; + let before: i64 = + sqlx::query_scalar("SELECT COUNT(*) FROM webxdc_instances WHERE instance_id = 'card'") + .fetch_one(&mut *transaction) + .await?; + assert_eq!(before, 0); + WebxdcService::handle_sync_chunk(&mut transaction, "g", 7, &chunks[0]).await?; + transaction.commit().await?; + + let instance = WebxdcService::new(&ctx).instance("card").await?.unwrap(); + assert_eq!(instance.app_id, "expenses"); + assert_eq!(instance.summary.as_deref(), Some("Trip updated")); + assert_eq!(instance.document.as_deref(), Some("2 expenses")); + let updates = WebxdcService::new(&ctx).updates_after("card", 0).await?; + assert_eq!(updates.len(), 2); + assert_eq!(updates[0].payload, r#"{"expense":1}"#); + assert_eq!(updates[1].payload, r#"{"expense":2}"#); + let pending: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM webxdc_pending_updates") + .fetch_one(&database.pool) + .await?; + assert_eq!(pending, 0); + let announcements: i64 = + sqlx::query_scalar("SELECT COUNT(*) FROM messages WHERE message_id = 'live-update-info'") + .fetch_one(&database.pool) + .await?; + assert_eq!(announcements, 1); + Ok(()) + } + + #[tokio::test] + async fn one_time_app_refuses_another_instance_in_the_same_chat() -> Result<()> { + let directory = tempfile::tempdir()?; + let ctx = + Context::init_for_testing(directory.path().join("db"), directory.path().join("data")) + .await?; + let database = ctx.app_db.read().await.clone(); + for statement in [ + "INSERT INTO groups(group_id, group_name) VALUES ('g', 'Chat'), ('other', 'Other')", + "INSERT INTO messages(message_id, group_id, type) VALUES ('card', 'g', 'webxdcApp')", + "INSERT INTO webxdc_apps(app_id, version, name, bundle_sha256, bundle_bytes, cached_at, published, one_time) VALUES ('expenses', 1, 'Expenses', 'hash', 10, 0, 1, 1)", + "INSERT INTO webxdc_instances(instance_id, group_id, app_id, version, origin_token) VALUES ('card', 'g', 'expenses', 1, 'origin')", + ] { + sqlx::query(statement).execute(&database.pool).await?; + } + let service = WebxdcService::new(&ctx); + assert_eq!( + service + .existing_instance_if_limited("g", "expenses", 1) + .await? + .as_deref(), + Some("card") + ); + assert_eq!( + service + .existing_instance_if_limited("other", "expenses", 1) + .await?, + None + ); + Ok(()) + } + + #[tokio::test] + async fn cached_launch_never_waits_for_updates_and_pins_only_between_runs() -> Result<()> { + let directory = tempfile::tempdir()?; + let ctx = + Context::init_for_testing(directory.path().join("db"), directory.path().join("data")) + .await?; + let database = ctx.app_db.read().await.clone(); + for statement in [ + "INSERT INTO groups(group_id, group_name) VALUES ('g', 'Chat')", + "INSERT INTO messages(message_id, group_id, type) VALUES ('card', 'g', 'webxdcApp')", + "INSERT INTO webxdc_instances(instance_id, group_id, app_id, version, origin_token, bundle_sha256) VALUES ('card', 'g', 'expenses', 1, 'origin', 'old-hash')", + "INSERT INTO webxdc_apps(app_id, version, name, bundle_sha256, bundle_bytes, cached_at, published) VALUES ('expenses', 2, 'Expenses', 'new-hash', 10, 0, 1)", + ] { + sqlx::query(statement).execute(&database.pool).await?; + } + let service = WebxdcService::new(&ctx); + let old = service.store().bundle_path("old-hash"); + let new = service.store().bundle_path("new-hash"); + std::fs::create_dir_all(old.parent().unwrap())?; + std::fs::write(&old, b"previously downloaded code")?; + // A newer catalog row and an unfinished download cannot block launch. + std::fs::write(new.with_extension("partial"), b"unfinished")?; + let path = tokio::time::timeout( + std::time::Duration::from_millis(500), + service.prepare_bundle("card"), + ) + .await + .expect("cached launch attempted a network request")?; + assert_eq!(path, old); + assert_eq!(service.instance("card").await?.unwrap().version, 1); + + // Simulate a background download finishing after the user reopened. + std::fs::write(&new, b"new downloaded code")?; + assert_eq!(service.pinned_bundle("card").await?, old); + assert_eq!(service.instance("card").await?.unwrap().version, 1); + // Only the next launch adopts the already downloaded update. + assert_eq!(service.prepare_bundle("card").await?, new); + assert_eq!(service.instance("card").await?.unwrap().version, 2); + assert!(!old.exists()); + Ok(()) + } + + #[tokio::test] + async fn member_api_is_scoped_and_excludes_departed_members() -> Result<()> { + let directory = tempfile::tempdir()?; + let ctx = + Context::init_for_testing(directory.path().join("db"), directory.path().join("data")) + .await?; + crate::user_config::UserConfig::save_json( + &ctx, + r#"{"userId":42,"username":"anna","displayName":"Anna"}"#, + )?; + let database = ctx.app_db.read().await.clone(); + for statement in [ + "INSERT INTO contacts(user_id, username, display_name) VALUES (7, 'ben', 'Ben'), (8, 'clara', 'Clara'), (9, 'dan', 'Dan')", + "INSERT INTO groups(group_id, group_name) VALUES ('g', 'Trip'), ('other', 'Other')", + "INSERT INTO group_members(group_id, contact_id, member_state) VALUES ('g', 7, NULL), ('g', 8, 'leftGroup'), ('other', 9, NULL)", + "INSERT INTO messages(message_id, group_id, type) VALUES ('card', 'g', 'webxdcApp')", + "INSERT INTO webxdc_instances(instance_id, group_id, app_id, version, origin_token) VALUES ('card', 'g', 'expenses', 1, 'origin')", + ] { + sqlx::query(statement).execute(&database.pool).await?; + } + let service = WebxdcService::new(&ctx); + let members: serde_json::Value = serde_json::from_str(&service.members("card").await?)?; + assert_eq!(members["selfId"], WebxdcService::address_for("card", 42)); + assert_eq!(members["members"].as_array().unwrap().len(), 2); + assert_eq!(members["members"][1]["displayName"], "Ben"); + assert_eq!( + members["members"][1]["id"], + WebxdcService::address_for("card", 7) + ); + assert!(service.members("missing").await.is_err()); + assert!(service + .send_update( + "card".into(), + "{}".into(), + Some("Dinner".into()), + None, + None, + None, + Some(r#"{"unrelated-member":"Dinner"}"#.into()) + ) + .await + .is_err()); + Ok(()) + } /// The announcement lands in the chat as an ordinary message, and lands /// there once however often the update it came with is delivered. diff --git a/rust/src/services/webxdc/store.rs b/rust/src/services/webxdc/store.rs index 73c735d7..d89a4ef9 100644 --- a/rust/src/services/webxdc/store.rs +++ b/rust/src/services/webxdc/store.rs @@ -134,7 +134,8 @@ impl WebxdcStore { .execute(&mut *transaction) .await?; let now = chrono::Utc::now().timestamp(); - for entry in &catalog.entries { + for (position, entry) in catalog.entries.iter().enumerate() { + let sort_order = position as i64; if !Self::entry_is_sane(entry) { tracing::warn!(app_id = entry.app_id, "skipping unusable catalog entry"); continue; @@ -151,8 +152,8 @@ impl WebxdcStore { r#"INSERT INTO webxdc_apps (app_id, version, name, name_translations, source_code_url, description, icon, bundle_sha256, bundle_bytes, published, - cached_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?) + cached_at, pro_only, sort_order, one_time) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?) ON CONFLICT(app_id, version) DO UPDATE SET name = excluded.name, name_translations = excluded.name_translations, @@ -162,6 +163,9 @@ impl WebxdcStore { bundle_sha256 = excluded.bundle_sha256, bundle_bytes = excluded.bundle_bytes, published = 1, + pro_only = excluded.pro_only, + sort_order = excluded.sort_order, + one_time = excluded.one_time, cached_at = excluded.cached_at"#, entry.app_id, entry.version, @@ -173,6 +177,9 @@ impl WebxdcStore { entry.bundle_sha256, entry.bundle_bytes, now, + entry.pro_only, + sort_order, + entry.one_time, ) .execute(&mut *transaction) .await?; diff --git a/rust/src/services/webxdc/webxdc.js b/rust/src/services/webxdc/webxdc.js index 574bcbe9..e2ae0f92 100644 --- a/rust/src/services/webxdc/webxdc.js +++ b/rust/src/services/webxdc/webxdc.js @@ -97,6 +97,10 @@ sendUpdateInterval: init.sendUpdateInterval, sendUpdateMaxSize: init.sendUpdateMaxSize, + getMembers: function () { + return call('getMembers'); + }, + sendUpdate: function (update, descr) { return call('sendUpdate', { payload: update.payload === undefined ? null : update.payload, diff --git a/test/mocks/user_config.dart b/test/mocks/user_config.dart index c578f42a..303a8703 100644 --- a/test/mocks/user_config.dart +++ b/test/mocks/user_config.dart @@ -44,6 +44,7 @@ UserConfig testUserConfig({ currentSignedPreKeyIndexStart: 100000, hideChangeLog: true, hideMemoriesBackupPromo: false, + hideWidgetShareExplainer: false, updateFcmToken: true, canUseLoginTokenForAuth: true, isBackupEnabled: false, diff --git a/test/services/messages_purge_test.dart b/test/services/messages_purge_test.dart index 6159a991..fa2c09b2 100644 --- a/test/services/messages_purge_test.dart +++ b/test/services/messages_purge_test.dart @@ -125,4 +125,32 @@ void main() { expect(remainingIds.contains('msg_d_sender_deleted_expired'), isFalse); }, ); + + test('deleting a chat retains only one-time app state', () async { + await twonlyDB.groupsDao.createNewGroup( + GroupsCompanion.insert(groupId: 'chat', groupName: 'Trip'), + ); + for (final statement in [ + "INSERT INTO messages(message_id, group_id, type) VALUES ('text', 'chat', 'text')", + "INSERT INTO messages(message_id, group_id, type) VALUES ('expenses-card', 'chat', 'webxdcApp')", + "INSERT INTO messages(message_id, group_id, type) VALUES ('game-card', 'chat', 'webxdcApp')", + "INSERT INTO webxdc_apps(app_id, version, name, bundle_sha256, bundle_bytes, cached_at, one_time) VALUES ('expenses', 1, 'Expenses', 'expense-hash', 1, 0, 1)", + "INSERT INTO webxdc_apps(app_id, version, name, bundle_sha256, bundle_bytes, cached_at, one_time) VALUES ('game', 1, 'Game', 'game-hash', 1, 0, 0)", + "INSERT INTO webxdc_instances(instance_id, group_id, app_id, version, origin_token, created_at, last_update_at) VALUES ('expenses-card', 'chat', 'expenses', 1, 'expenses-origin', 0, 0)", + "INSERT INTO webxdc_instances(instance_id, group_id, app_id, version, origin_token, created_at, last_update_at) VALUES ('game-card', 'chat', 'game', 1, 'game-origin', 0, 0)", + "INSERT INTO webxdc_updates(instance_id, serial, message_id, payload, received_at) VALUES ('expenses-card', 1, 'expense-update', '{}', 0)", + ]) { + await twonlyDB.customStatement(statement); + } + + await twonlyDB.messagesDao.deleteMessagesByGroupId('chat'); + + final messages = await twonlyDB.select(twonlyDB.messages).get(); + expect(messages.map((message) => message.messageId), ['expenses-card']); + final instances = await twonlyDB.select(twonlyDB.webxdcInstances).get(); + expect(instances.map((instance) => instance.instanceId), ['expenses-card']); + final updates = await twonlyDB.select(twonlyDB.webxdcUpdates).get(); + expect(updates, hasLength(1)); + expect((await twonlyDB.groupsDao.getGroup('chat'))?.deletedContent, isTrue); + }); }