diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c214313..12ecc08d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 0.2.11 + +- New: Create custom shortcuts to quickly share images with pre-selected groups +- New: Seamless recovery for iOS reinstallations +- Improved: Redesigned snackbar notifications +- Improved: New backup mechanism to allow larger backup files +- Improved: Move keys into a centralized Rust-owned structure stored in secure storage +- Fix: Messages occasionally not received until app restart +- Fix: Multiple smaller issues + ## 0.2.10 - Fix: Issue with push notifications on Android diff --git a/android/app/src/main/kotlin/eu/twonly/MainActivity.kt b/android/app/src/main/kotlin/eu/twonly/MainActivity.kt index 0ef89f3d..e87f278e 100644 --- a/android/app/src/main/kotlin/eu/twonly/MainActivity.kt +++ b/android/app/src/main/kotlin/eu/twonly/MainActivity.kt @@ -6,6 +6,8 @@ import dev.darttools.flutter_android_volume_keydown.FlutterAndroidVolumeKeydownP import android.view.KeyEvent.KEYCODE_VOLUME_DOWN import android.view.KeyEvent.KEYCODE_VOLUME_UP import io.flutter.embedding.engine.FlutterEngine +import android.content.Context +import io.crates.keyring.Keyring class MainActivity : FlutterFragmentActivity() { @@ -24,6 +26,8 @@ class MainActivity : FlutterFragmentActivity() { override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) + Keyring.initializeNdkContext(applicationContext) + MediaStoreChannel.configure(flutterEngine, applicationContext) VideoCompressionChannel.configure(flutterEngine, applicationContext) } diff --git a/android/app/src/main/kotlin/eu/twonly/MyApplication.kt b/android/app/src/main/kotlin/eu/twonly/MyApplication.kt index 231e247e..9bd91360 100644 --- a/android/app/src/main/kotlin/eu/twonly/MyApplication.kt +++ b/android/app/src/main/kotlin/eu/twonly/MyApplication.kt @@ -3,10 +3,12 @@ package eu.twonly import io.flutter.app.FlutterApplication import dev.fluttercommunity.workmanager.WorkmanagerDebug import dev.fluttercommunity.workmanager.LoggingDebugHandler +import io.crates.keyring.Keyring class MyApplication : FlutterApplication() { override fun onCreate() { super.onCreate() + Keyring.initializeNdkContext(this) // This enables the internal plugin logging to Logcat WorkmanagerDebug.setCurrent(LoggingDebugHandler()) } diff --git a/android/app/src/main/kotlin/io/crates/keyring/Keyring.kt b/android/app/src/main/kotlin/io/crates/keyring/Keyring.kt new file mode 100644 index 00000000..58830901 --- /dev/null +++ b/android/app/src/main/kotlin/io/crates/keyring/Keyring.kt @@ -0,0 +1,14 @@ +package io.crates.keyring + +import android.content.Context + +class Keyring { + companion object { + init { + // Replace with the name of your compiled Rust library + System.loadLibrary("rust_lib_twonly") + } + // The underlying Rust crate provides the implementation for this + external fun initializeNdkContext(context: Context) + } +} diff --git a/assets/animated_icons/distorted_face.json b/assets/animated_icons/distorted_face.json new file mode 100644 index 00000000..e7d592d1 --- /dev/null +++ b/assets/animated_icons/distorted_face.json @@ -0,0 +1 @@ +{"v":"5.8.1","fr":60,"ip":0,"op":168,"w":1024,"h":1024,"nm":"emoji_u1faea","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"Null ALL","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.913},"o":{"x":1,"y":0},"t":16,"s":[507.75,502.75,0],"to":[0.667,1.333,0],"ti":[-0.667,-1.5,0]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.441},"t":50,"s":[511.75,510.75,0],"to":[0.667,1.5,0],"ti":[0.667,1.333,0]},{"i":{"x":0.667,"y":1},"o":{"x":1,"y":0},"t":100,"s":[511.75,511.75,0],"to":[-0.667,-1.333,0],"ti":[0.667,1.5,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.936,"y":0.936},"t":118,"s":[507.75,502.75,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0.333},"t":136,"s":[507.75,502.75,0],"to":[0,0,0],"ti":[0,0,0]},{"t":144,"s":[507.75,502.75,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.887,0.887,0.634]},"o":{"x":[1,1,0.333],"y":[0,0,0]},"t":16,"s":[92.6,92.6,100]},{"i":{"x":[0,0,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.246,0.246,0.539]},"t":50,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[1,1,0.167],"y":[0,0,0]},"t":100,"s":[105,105,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.936,0.936,0.333],"y":[0,0,0]},"t":118,"s":[91,91,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":136,"s":[93,93,100]},{"t":144,"s":[92.6,92.6,100]}],"ix":6,"l":2}},"ao":0,"ip":0,"op":168,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":3,"nm":"Null Face","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[511.454,512.01,0],"ix":2,"l":2},"a":{"a":0,"k":[512,512,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":118,"s":[92.6,92.6,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":136,"s":[94,94,100]},{"t":146,"s":[92.6,92.6,100]}],"ix":6,"l":2}},"ao":0,"ip":0,"op":168,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"shine left","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[116.81,357.989,0],"ix":2,"l":2},"a":{"a":0,"k":[149.008,369.396,0],"ix":1,"l":2},"s":{"a":0,"k":[107.991,107.991,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":1,"y":0},"t":16,"s":[{"i":[[-5.883,9.757],[8.261,4.981],[5.883,-9.757],[-8.261,-4.981]],"o":[[5.883,-9.757],[-8.261,-4.981],[-5.883,9.757],[8.261,4.981]],"v":[[15.455,8.925],[11.15,-17.76],[-14.459,-9.111],[-10.154,17.574]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[-6.08,10.48],[11.28,11.28],[14,-19.04],[-15.76,-1.68]],"o":[[8.72,-14.88],[-6.16,-6.16],[-12.72,17.36],[15.12,1.68]],"v":[[-181.004,-10.698],[-177.724,-46.618],[-215.564,-30.938],[-211.164,16.742]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":1,"y":0},"t":100,"s":[{"i":[[-6.382,11.001],[14.664,8.084],[14.695,-19.986],[-16.543,-1.763]],"o":[[9.153,-15.619],[-11.258,-6.206],[-15.789,25.084],[15.871,1.763]],"v":[[-198.478,-16.956],[-187.286,-69.411],[-234.755,-38.201],[-231.136,17.347]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.936,"y":0},"t":118,"s":[{"i":[[-5.883,9.757],[8.261,4.981],[5.883,-9.757],[-8.261,-4.981]],"o":[[5.883,-9.757],[-8.261,-4.981],[-5.883,9.757],[8.261,4.981]],"v":[[28.455,21.925],[24.15,-4.76],[-1.459,3.889],[2.846,30.574]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.333,"y":0},"t":136,"s":[{"i":[[-5.883,9.757],[8.261,4.981],[5.883,-9.757],[-8.261,-4.981]],"o":[[5.883,-9.757],[-8.261,-4.981],[-5.883,9.757],[8.261,4.981]],"v":[[15.455,8.925],[11.15,-17.76],[-14.459,-9.111],[-10.154,17.574]],"c":true}]},{"t":144,"s":[{"i":[[-5.883,9.757],[8.261,4.981],[5.883,-9.757],[-8.261,-4.981]],"o":[[5.883,-9.757],[-8.261,-4.981],[-5.883,9.757],[8.261,4.981]],"v":[[15.455,8.925],[11.15,-17.76],[-14.459,-9.111],[-10.154,17.574]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.536999990426,0.375999989229,0.141000007181,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[352.044,386.618],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":168,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"eye left","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[171.81,383.989,0],"ix":2,"l":2},"a":{"a":0,"k":[199.938,393.472,0],"ix":1,"l":2},"s":{"a":0,"k":[107.991,107.991,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":1,"y":0},"t":16,"s":[{"i":[[0.844,-22.972],[37.984,-0.548],[-0.099,34.288],[-38.391,-1.235],[-9.099,-7.403]],"o":[[-1.234,33.593],[-39.975,-1.225],[0.103,-35.532],[14.321,0.461],[17.054,13.875]],"v":[[60.251,3.381],[0.971,70.183],[-58.381,0.378],[0.971,-69.427],[36.081,-56.873]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[19.6,-38.48],[33.12,5.12],[-3.12,49.04],[-43.44,5.92],[-11.84,-31.84]],"o":[[-19.6,38.48],[-32.72,-5.12],[3.2,-49.2],[28.64,-3.92],[5.36,14.4]],"v":[[-123.315,25.142],[-206.115,80.742],[-258.435,-3.978],[-179.395,-102.698],[-115.315,-63.098]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":1,"y":0},"t":100,"s":[{"i":[[19.723,-42.25],[33.328,5.622],[-3.14,53.844],[-43.709,6.528],[-11.925,-34.956]],"o":[[-19.723,42.25],[-32.926,-5.622],[3.22,-54.02],[28.82,-4.304],[5.394,15.811]],"v":[[-146.258,23.56],[-229.578,84.607],[-282.227,-8.412],[-202.69,-116.803],[-138.208,-73.324]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.936,"y":0},"t":118,"s":[{"i":[[0.844,-22.972],[37.984,-0.548],[-0.099,34.288],[-38.391,-1.235],[-9.099,-7.403]],"o":[[-1.234,33.593],[-39.975,-1.225],[0.103,-35.532],[14.321,0.461],[17.054,13.875]],"v":[[73.251,15.381],[13.971,82.183],[-45.381,12.378],[13.971,-57.427],[49.081,-44.873]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.333,"y":0},"t":136,"s":[{"i":[[0.844,-22.972],[37.984,-0.548],[-0.099,34.288],[-38.391,-1.235],[-9.099,-7.403]],"o":[[-1.234,33.593],[-39.975,-1.225],[0.103,-35.532],[14.321,0.461],[17.054,13.875]],"v":[[60.251,3.381],[0.971,70.183],[-58.381,0.378],[0.971,-69.427],[36.081,-56.873]],"c":true}]},{"t":144,"s":[{"i":[[0.844,-22.972],[37.984,-0.548],[-0.099,34.288],[-38.391,-1.235],[-9.099,-7.403]],"o":[[-1.234,33.593],[-39.975,-1.225],[0.103,-35.532],[14.321,0.461],[17.054,13.875]],"v":[[60.251,3.381],[0.971,70.183],[-58.381,0.378],[0.971,-69.427],[36.081,-56.873]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.258999992819,0.169000004787,0.051000000449,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[366.035,410.138],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":168,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"shine right","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[834.207,359.19,0],"ix":2,"l":2},"a":{"a":0,"k":[813.792,370.554,0],"ix":1,"l":2},"s":{"a":0,"k":[107.991,107.991,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":1,"y":0},"t":16,"s":[{"i":[[6.227,-9.542],[8.078,5.272],[-6.227,9.541],[-8.077,-5.272]],"o":[[-6.227,9.541],[-8.078,-5.272],[6.227,-9.542],[8.078,5.272]],"v":[[14.627,9.546],[-11.274,17.276],[-14.627,-9.545],[11.274,-17.276]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[-1.12,-15.12],[14.96,-1.68],[-4,20.96],[-9.6,-2.16]],"o":[[1.04,14.24],[-17.6,1.92],[4.4,-23.12],[17.28,3.84]],"v":[[202.293,-15.017],[185.413,18.503],[159.893,-24.777],[190.453,-54.137]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":1,"y":0},"t":100,"s":[{"i":[[-2.68,-17.086],[13.925,-3.242],[-1.52,24.18],[-9.284,-1.6]],"o":[[2.51,16.093],[-16.388,3.751],[1.665,-26.671],[16.705,2.825]],"v":[[225.056,-29.984],[212.782,18.945],[184.03,-37.299],[212.959,-86.104]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.936,"y":0},"t":118,"s":[{"i":[[6.227,-9.542],[8.078,5.272],[-6.227,9.541],[-8.077,-5.272]],"o":[[-6.227,9.541],[-8.078,-5.272],[6.227,-9.542],[8.078,5.272]],"v":[[3.627,22.546],[-22.274,30.276],[-25.627,3.455],[0.274,-4.276]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.333,"y":0},"t":136,"s":[{"i":[[6.227,-9.542],[8.078,5.272],[-6.227,9.541],[-8.077,-5.272]],"o":[[-6.227,9.541],[-8.078,-5.272],[6.227,-9.542],[8.078,5.272]],"v":[[14.627,9.546],[-11.274,17.276],[-14.627,-9.545],[11.274,-17.276]],"c":true}]},{"t":144,"s":[{"i":[[6.227,-9.542],[8.078,5.272],[-6.227,9.541],[-8.077,-5.272]],"o":[[-6.227,9.541],[-8.078,-5.272],[6.227,-9.542],[8.078,5.272]],"v":[[14.627,9.546],[-11.274,17.276],[-14.627,-9.545],[11.274,-17.276]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.536999990426,0.375999989229,0.141000007181,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[636.907,386.618],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":168,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"eye right","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[508.27,511.989,0],"ix":2,"l":2},"a":{"a":0,"k":[512,512,0],"ix":1,"l":2},"s":{"a":0,"k":[107.991,107.991,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":1,"y":0},"t":16,"s":[{"i":[[1.293,-35.229],[37.984,-0.548],[-0.1,34.287],[-13.751,13.431],[-16.083,0.202]],"o":[[-1.234,33.593],[-39.975,-1.225],[0.059,-20.421],[9.588,-9.364],[41.162,1.324]],"v":[[58.719,3.004],[-0.56,69.805],[-59.913,0.001],[-39.099,-54.27],[-0.56,-69.805]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[-3.2,-49.2],[32.72,-5.04],[19.6,38.56],[-5.36,14.32],[-28.64,-3.92]],"o":[[3.12,49.04],[-33.12,5.12],[-19.6,-38.56],[11.84,-31.84],[43.44,5.92]],"v":[[272.37,-3.977],[220.05,80.743],[137.25,25.143],[129.25,-63.097],[193.33,-102.697]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":1,"y":0},"t":100,"s":[{"i":[[-8.302,-55.637],[30.309,-8.645],[22.622,42.082],[-3.515,16.755],[-27.425,-1.903]],"o":[[8.21,55.462],[-30.677,8.77],[-22.622,-42.082],[7.744,-37.245],[41.593,2.858]],"v":[[292.976,-22.813],[252.746,78.145],[168.705,22.326],[151.683,-77.258],[207.848,-127.979]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.936,"y":0},"t":118,"s":[{"i":[[1.293,-35.229],[37.984,-0.548],[-0.1,34.287],[-13.751,13.431],[-16.083,0.202]],"o":[[-1.234,33.593],[-39.975,-1.225],[0.059,-20.421],[9.588,-9.364],[41.162,1.324]],"v":[[47.719,15.004],[-11.56,81.805],[-70.913,12.001],[-50.099,-42.27],[-11.56,-57.805]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.333,"y":0},"t":136,"s":[{"i":[[1.293,-35.229],[37.984,-0.548],[-0.1,34.287],[-13.751,13.431],[-16.083,0.202]],"o":[[-1.234,33.593],[-39.975,-1.225],[0.059,-20.421],[9.588,-9.364],[41.162,1.324]],"v":[[58.719,3.004],[-0.56,69.805],[-59.913,0.001],[-39.099,-54.27],[-0.56,-69.805]],"c":true}]},{"t":144,"s":[{"i":[[1.293,-35.229],[37.984,-0.548],[-0.1,34.287],[-13.751,13.431],[-16.083,0.202]],"o":[[-1.234,33.593],[-39.975,-1.225],[0.059,-20.421],[9.588,-9.364],[41.162,1.324]],"v":[[58.719,3.004],[-0.56,69.805],[-59.913,0.001],[-39.099,-54.27],[-0.56,-69.805]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.258999992819,0.169000004787,0.051000000449,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[652.03,410.138],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":168,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"mouth","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":22,"s":[509.35,511.989,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":46,"s":[509.35,546.546,0],"to":[0,0,0],"ti":[0,0,0]},{"t":50,"s":[509.35,511.989,0]}],"ix":2,"l":2},"a":{"a":0,"k":[512,512,0],"ix":1,"l":2},"s":{"a":0,"k":[107.991,107.991,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":1,"y":0},"t":16,"s":[{"i":[[-64.094,0],[-35.811,20.627],[-3.378,-7.206],[3.486,-3.932],[69.99,0],[45.029,50.01],[-2.152,4.749],[-6.618,-3.804]],"o":[[64.041,0],[7,-4.032],[2.225,4.749],[-44.811,50.541],[-67.284,0.519],[-3.413,-3.932],[3.549,-7.834],[35.757,20.553]],"v":[[-0.519,-1.249],[160.087,-47.326],[179.078,-41.167],[177.001,-26.922],[-0.519,50.839],[-177.074,-26.922],[-179.151,-41.167],[-160.161,-47.326]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":46,"s":[{"i":[[-51.075,-0.008],[-34.178,12.673],[-0.904,-13.025],[10.471,-3.743],[49.496,0.006],[44.735,16.547],[-0.149,11.611],[-15.508,-2.7]],"o":[[51.044,0.008],[15.766,-2.835],[0.225,11.611],[-46.608,16.659],[-47.697,-0.006],[-10.436,-3.86],[0.972,-13.395],[34.13,12.629]],"v":[[2.516,49.284],[136.851,21.378],[166.98,40.825],[144.344,66.831],[0.077,100.479],[-138.621,67.048],[-161.29,40.792],[-131.227,21.345]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":48,"s":[{"i":[[-41.753,-0.014],[-33.009,6.976],[0.868,-17.193],[16.349,-5.332],[42.028,0.007],[37.157,12.349],[1.286,16.526],[-21.874,-1.91]],"o":[[41.737,0.014],[22.043,-1.977],[-1.208,16.526],[-36.976,12.058],[-41.231,0.146],[-15.397,-5.117],[-0.874,-17.378],[32.965,6.954]],"v":[[4.689,85.47],[120.211,70.577],[158.316,99.541],[118.958,132.894],[3.545,150.013],[-112.585,132.337],[-148.499,99.484],[-110.508,70.521]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[-32.43,-0.02],[-31.84,1.28],[2.64,-21.36],[26.16,-1.28],[30.36,0.01],[29.84,1.44],[2.72,21.44],[-28.24,-1.12]],"o":[[32.43,0.02],[28.32,-1.12],[-2.64,21.44],[0,0],[-30.36,-0.01],[-26.24,-1.28],[-2.72,-21.36],[31.8,1.28]],"v":[[6.862,121.657],[103.572,119.777],[149.652,158.257],[97.572,197.457],[7.012,199.547],[-83.548,197.377],[-135.708,158.177],[-89.788,119.697]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":1,"y":0},"t":100,"s":[{"i":[[-35.621,-0.069],[-34.224,5.539],[0.581,-29.504],[29.748,-4.705],[38.643,0.482],[31.109,4.857],[0.53,28.798],[-29.253,-4.457]],"o":[[35.706,0.069],[29.411,-4.76],[-0.581,29.504],[-33.134,5.678],[-32.94,-0.411],[-29.725,-4.641],[-0.53,-28.798],[34.015,5.183]],"v":[[6.932,148.388],[112.602,140.3],[168.383,177.604],[112.88,228.658],[-1.256,236.583],[-98.087,228.656],[-153.749,178.092],[-98.291,140.393]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.936,"y":0},"t":118,"s":[{"i":[[-64.094,0],[-35.811,20.627],[-3.378,-7.206],[3.486,-3.932],[69.99,0],[45.029,50.01],[-2.152,4.749],[-6.618,-3.804]],"o":[[64.041,0],[7,-4.032],[2.225,4.749],[-44.811,50.541],[-67.284,0.519],[-3.413,-3.932],[3.549,-7.834],[35.757,20.553]],"v":[[-1.019,-14.249],[147.587,-59.326],[166.578,-53.167],[164.501,-38.922],[-1.519,36.339],[-165.074,-38.922],[-167.151,-53.167],[-148.161,-59.326]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.333,"y":0},"t":136,"s":[{"i":[[-64.094,0],[-35.811,20.627],[-3.378,-7.206],[3.486,-3.932],[69.99,0],[45.029,50.01],[-2.152,4.749],[-6.618,-3.804]],"o":[[64.041,0],[7,-4.032],[2.225,4.749],[-44.811,50.541],[-67.284,0.519],[-3.413,-3.932],[3.549,-7.834],[35.757,20.553]],"v":[[-0.519,-1.249],[160.087,-47.326],[179.078,-41.167],[177.001,-26.922],[-0.519,50.839],[-177.074,-26.922],[-179.151,-41.167],[-160.161,-47.326]],"c":true}]},{"t":144,"s":[{"i":[[-64.094,0],[-35.811,20.627],[-3.378,-7.206],[3.486,-3.932],[69.99,0],[45.029,50.01],[-2.152,4.749],[-6.618,-3.804]],"o":[[64.041,0],[7,-4.032],[2.225,4.749],[-44.811,50.541],[-67.284,0.519],[-3.413,-3.932],[3.549,-7.834],[35.757,20.553]],"v":[[-0.519,-1.249],[160.087,-47.326],[179.078,-41.167],[177.001,-26.922],[-0.519,50.839],[-177.074,-26.922],[-179.151,-41.167],[-160.161,-47.326]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.258999992819,0.169000004787,0.051000000449,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[508.068,665.343],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":168,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"eye left 2","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[509.35,511.989,0],"ix":2,"l":2},"a":{"a":0,"k":[512,512,0],"ix":1,"l":2},"s":{"a":0,"k":[107.991,107.991,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":1,"y":0},"t":20,"s":[{"i":[[-38.08,-1.235],[1.283,-35.229],[37.66,-0.548],[-0.098,34.287]],"o":[[40.81,1.324],[-1.223,33.593],[-39.634,-1.225],[0.102,-35.551]],"v":[[0.409,-69.187],[59.183,3.621],[0.409,70.422],[-58.436,0.618]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[-93.84,16.08],[28.08,-147.76],[105.12,28.48],[-15.12,96.88]],"o":[[93.84,-16],[-28.08,147.76],[-88.48,-24],[12.16,-99.2]],"v":[[-138.835,-207.429],[19.965,5.451],[-207.475,208.571],[-314.275,-1.429]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":1,"y":0},"t":100,"s":[{"i":[[-98.501,16.879],[29.475,-155.099],[110.341,29.895],[-15.871,101.692]],"o":[[98.501,-16.795],[-29.475,155.099],[-92.875,-25.192],[12.764,-104.127]],"v":[[-156.75,-234.822],[-6.063,2.631],[-230.799,229.84],[-334.904,-6.591]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.936,"y":0},"t":118,"s":[{"i":[[-38.08,-1.235],[1.283,-35.229],[37.66,-0.548],[-0.098,34.287]],"o":[[40.81,1.324],[-1.223,33.593],[-39.634,-1.225],[0.102,-35.551]],"v":[[0.409,-69.187],[59.183,3.621],[0.409,70.422],[-58.436,0.618]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.333,"y":0},"t":136,"s":[{"i":[[-38.08,-1.235],[1.283,-35.229],[37.66,-0.548],[-0.098,34.287]],"o":[[40.81,1.324],[-1.223,33.593],[-39.634,-1.225],[0.102,-35.551]],"v":[[0.409,-69.187],[59.183,3.621],[0.409,70.422],[-58.436,0.618]],"c":true}]},{"t":144,"s":[{"i":[[-38.08,-1.235],[1.283,-35.229],[37.66,-0.548],[-0.098,34.287]],"o":[[40.81,1.324],[-1.223,33.593],[-39.634,-1.225],[0.102,-35.551]],"v":[[0.409,-69.187],[59.183,3.621],[0.409,70.422],[-58.436,0.618]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"gf","o":{"a":0,"k":100,"ix":10},"r":1,"bm":0,"g":{"p":5,"k":{"a":0,"k":[0.72,1,1,1,0.805,0.925,0.925,0.925,0.89,0.851,0.851,0.851,0.945,0.796,0.796,0.796,1,0.741,0.741,0.741],"ix":9}},"s":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":46,"s":[-70.438,22.25],"to":[-5.333,-0.333],"ti":[7.333,0.667]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":50,"s":[-102.438,20.25],"to":[-7.333,-0.667],"ti":[2,0.333]},{"t":100,"s":[-114.438,18.25]}],"ix":5},"e":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":46,"s":[40,196],"to":[9.667,-5],"ti":[-10,-0.333]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":50,"s":[98,166],"to":[10,0.333],"ti":[-0.333,-5.333]},{"t":100,"s":[100,198]}],"ix":6},"t":2,"h":{"a":0,"k":0,"ix":7},"a":{"a":0,"k":0,"ix":8},"nm":"Gradient Fill 1","mn":"ADBE Vector Graphic - G-Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[366.035,409.52],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":36,"op":116,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"eye right 2","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[509.35,511.989,0],"ix":2,"l":2},"a":{"a":0,"k":[512,512,0],"ix":1,"l":2},"s":{"a":0,"k":[107.991,107.991,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":1,"y":0},"t":20,"s":[{"i":[[-1.365,-36.287],[33.601,0.184],[0.703,35.21],[-37.03,0.178]],"o":[[-0.365,35.713],[-37.701,-0.207],[-0.915,-45.839],[34.901,-0.167]],"v":[[58.968,2.767],[0.804,70.187],[-58.6,4.77],[1.133,-68.198]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":50,"s":[{"i":[[-12.16,-99.2],[88.48,-24],[28.08,147.84],[-93.84,-16]],"o":[[15.12,96.96],[-105.12,28.48],[-28.08,-147.84],[93.84,16]],"v":[[324.393,-1.429],[217.593,208.571],[-9.847,5.451],[148.953,-207.429]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":1,"y":0},"t":100,"s":[{"i":[[-22.121,-111.67],[80.846,-35.163],[42.357,165.537],[-90.197,-9.824]],"o":[[24.672,108.86],[-96.054,41.738],[-42.357,-165.537],[90.197,9.824]],"v":[[341.061,-24.658],[262.923,223.551],[26.66,12.946],[153.518,-243.171]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.936,"y":0},"t":118,"s":[{"i":[[-1.365,-36.287],[33.601,0.184],[0.703,35.21],[-37.03,0.178]],"o":[[-0.365,35.713],[-37.701,-0.207],[-0.915,-45.839],[34.901,-0.167]],"v":[[58.968,2.767],[0.804,70.187],[-58.6,4.77],[1.133,-68.198]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.333,"y":0},"t":136,"s":[{"i":[[-1.365,-36.287],[33.601,0.184],[0.703,35.21],[-37.03,0.178]],"o":[[-0.365,35.713],[-37.701,-0.207],[-0.915,-45.839],[34.901,-0.167]],"v":[[58.968,2.767],[0.804,70.187],[-58.6,4.77],[1.133,-68.198]],"c":true}]},{"t":144,"s":[{"i":[[-1.365,-36.287],[33.601,0.184],[0.703,35.21],[-37.03,0.178]],"o":[[-0.365,35.713],[-37.701,-0.207],[-0.915,-45.839],[34.901,-0.167]],"v":[[58.968,2.767],[0.804,70.187],[-58.6,4.77],[1.133,-68.198]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"gf","o":{"a":0,"k":100,"ix":10},"r":1,"bm":0,"g":{"p":5,"k":{"a":0,"k":[0.72,1,1,1,0.805,0.925,0.925,0.925,0.89,0.851,0.851,0.851,0.945,0.796,0.796,0.796,1,0.741,0.741,0.741],"ix":9}},"s":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":46,"s":[70.438,22.25],"to":[5.333,-0.333],"ti":[-7.333,0.667]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":50,"s":[102.438,20.25],"to":[7.333,-0.667],"ti":[-2,0.333]},{"t":100,"s":[114.438,18.25]}],"ix":5},"e":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":46,"s":[-40,196],"to":[-9.667,-5],"ti":[10,-0.333]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":50,"s":[-98,166],"to":[-10,0.333],"ti":[0.333,-5.333]},{"t":100,"s":[-100,198]}],"ix":6},"t":2,"h":{"a":0,"k":0,"ix":7},"a":{"a":0,"k":0,"ix":8},"nm":"Gradient Fill 1","mn":"ADBE Vector Graphic - G-Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[650.897,409.52],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":36,"op":116,"st":0,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"base matte","parent":12,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-2,-3,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[15.4,0],[0,38.4],[-27.9,0],[-10.6,-9.3],[0,-16.9],[11.5,-10.2]],"o":[[-27.9,0],[0,-38.4],[15.5,0],[11.5,10.2],[0,16.9],[-10.6,9.3]],"v":[[0,55.9],[-58,0],[0,-55.9],[40.4,-41.5],[58,0],[40.4,41.4]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"gf","o":{"a":0,"k":100,"ix":10},"r":1,"bm":0,"g":{"p":5,"k":{"a":0,"k":[0.48,0.992,0.878,0.188,0.691,0.98,0.816,0.178,0.902,0.969,0.753,0.169,0.966,0.963,0.694,0.153,1,0.957,0.635,0.137],"ix":9}},"s":{"a":0,"k":[0,0],"ix":5},"e":{"a":0,"k":[56.225,0],"ix":6},"t":2,"h":{"a":0,"k":0,"ix":7},"a":{"a":0,"k":0,"ix":8},"nm":"Gradient Fill 1","mn":"ADBE Vector Graphic - G-Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":168,"st":0,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":"base shadow","parent":12,"tt":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[15.4,0],[0,38.4],[-27.9,0],[-10.6,-9.3],[0,-16.9],[11.5,-10.2]],"o":[[-27.9,0],[0,-38.4],[15.5,0],[11.5,10.2],[0,16.9],[-10.6,9.3]],"v":[[0,55.9],[-58,0],[0,-55.9],[40.4,-41.5],[58,0],[40.4,41.4]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.921568627451,0.560784313725,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":168,"st":0,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":"base normal","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[800,800,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[15.4,0],[0,38.4],[-27.9,0],[-10.6,-9.3],[0,-16.9],[11.5,-10.2]],"o":[[-27.9,0],[0,-38.4],[15.5,0],[11.5,10.2],[0,16.9],[-10.6,9.3]],"v":[[0,55.9],[-58,0],[0,-55.9],[40.4,-41.5],[58,0],[40.4,41.4]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"gf","o":{"a":0,"k":100,"ix":10},"r":1,"bm":0,"g":{"p":5,"k":{"a":0,"k":[0.48,0.992,0.878,0.188,0.691,0.98,0.816,0.178,0.902,0.969,0.753,0.169,0.966,0.963,0.694,0.153,1,0.957,0.635,0.137],"ix":9}},"s":{"a":0,"k":[0,0],"ix":5},"e":{"a":0,"k":[56.225,0],"ix":6},"t":2,"h":{"a":0,"k":0,"ix":7},"a":{"a":0,"k":0,"ix":8},"nm":"Gradient Fill 1","mn":"ADBE Vector Graphic - G-Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":168,"st":0,"bm":0}],"markers":[{"tm":52,"cm":"rest","dr":0}]} \ No newline at end of file diff --git a/lib/app.dart b/lib/app.dart index 02be717f..a43651dd 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -18,11 +18,17 @@ import 'package:twonly/src/visual/views/home.view.dart'; import 'package:twonly/src/visual/views/onboarding/onboarding.view.dart'; import 'package:twonly/src/visual/views/onboarding/register.view.dart'; import 'package:twonly/src/visual/views/onboarding/setup.view.dart'; +import 'package:twonly/src/visual/views/recovery.view.dart'; import 'package:twonly/src/visual/views/unlock_twonly.view.dart'; class App extends StatefulWidget { - const App({required this.storageError, super.key}); + const App({ + required this.storageError, + required this.recoveryPossible, + super.key, + }); final bool storageError; + final bool recoveryPossible; @override State createState() => _AppState(); } @@ -77,7 +83,6 @@ class _AppState extends State with WidgetsBindingObserver { if (widget.storageError) { return MaterialApp( - scaffoldMessengerKey: AppGlobalKeys.scaffoldMessengerKey, localizationsDelegates: localizationsDelegates, debugShowCheckedModeBanner: false, supportedLocales: supportedLocales, @@ -89,9 +94,21 @@ class _AppState extends State with WidgetsBindingObserver { ); } + if (widget.recoveryPossible) { + return MaterialApp( + localizationsDelegates: localizationsDelegates, + debugShowCheckedModeBanner: false, + supportedLocales: supportedLocales, + title: 'twonly', + theme: lightTheme, + darkTheme: darkTheme, + themeMode: context.read().themeMode, + home: const RecoveryView(), + ); + } + return MaterialApp.router( routerConfig: routerProvider, - scaffoldMessengerKey: AppGlobalKeys.scaffoldMessengerKey, localizationsDelegates: localizationsDelegates, debugShowCheckedModeBanner: false, supportedLocales: supportedLocales, diff --git a/lib/core/backup/backup_password.dart b/lib/core/backup/backup_password.dart new file mode 100644 index 00000000..eacdead5 --- /dev/null +++ b/lib/core/backup/backup_password.dart @@ -0,0 +1,29 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.12.0. + +// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import + +import '../frb_generated.dart'; +import '../lib.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; + +class BackupPasswordKeys { + final U8Array32 backupId; + final U8Array32 encryptionKey; + + const BackupPasswordKeys({ + required this.backupId, + required this.encryptionKey, + }); + + @override + int get hashCode => backupId.hashCode ^ encryptionKey.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is BackupPasswordKeys && + runtimeType == other.runtimeType && + backupId == other.backupId && + encryptionKey == other.encryptionKey; +} diff --git a/lib/core/bridge.dart b/lib/core/bridge.dart index dfa22807..d3c9d0f4 100644 --- a/lib/core/bridge.dart +++ b/lib/core/bridge.dart @@ -9,7 +9,7 @@ import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; // These functions are ignored because they are not marked as `pub`: `get_twonly_flutter` // These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `TwonlyFlutter` -Future initializeTwonlyFlutter({required TwonlyConfig config}) => +Future initializeTwonlyFlutter({required InitConfig config}) => RustLib.instance.api.crateBridgeInitializeTwonlyFlutter(config: config); class AnnouncedUser { @@ -36,6 +36,27 @@ class AnnouncedUser { publicId == other.publicId; } +class InitConfig { + final String databaseDir; + final String dataDir; + + const InitConfig({ + required this.databaseDir, + required this.dataDir, + }); + + @override + int get hashCode => databaseDir.hashCode ^ dataDir.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is InitConfig && + runtimeType == other.runtimeType && + databaseDir == other.databaseDir && + dataDir == other.dataDir; +} + class OtherPromotion { final int promotionId; final PlatformInt64 publicId; @@ -74,24 +95,3 @@ class OtherPromotion { announcementShare == other.announcementShare && publicKeyVerifiedTimestamp == other.publicKeyVerifiedTimestamp; } - -class TwonlyConfig { - final String databasePath; - final String dataDirectory; - - const TwonlyConfig({ - required this.databasePath, - required this.dataDirectory, - }); - - @override - int get hashCode => databasePath.hashCode ^ dataDirectory.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is TwonlyConfig && - runtimeType == other.runtimeType && - databasePath == other.databasePath && - dataDirectory == other.dataDirectory; -} diff --git a/lib/core/bridge/wrapper/backup.dart b/lib/core/bridge/wrapper/backup.dart new file mode 100644 index 00000000..454ed238 --- /dev/null +++ b/lib/core/bridge/wrapper/backup.dart @@ -0,0 +1,87 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.12.0. + +// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import + +import '../../frb_generated.dart'; +import '../../keys/backup_password_keys.dart'; +import '../../lib.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; + +class RustBackupArchive { + const RustBackupArchive(); + + static Future<(String, String)> createBackupArchive() => RustLib.instance.api + .crateBridgeWrapperBackupRustBackupArchiveCreateBackupArchive(); + + static Future getBackupDownloadToken() => RustLib.instance.api + .crateBridgeWrapperBackupRustBackupArchiveGetBackupDownloadToken(); + + static Future restoreBackupArchive({required String filePath}) => + RustLib.instance.api + .crateBridgeWrapperBackupRustBackupArchiveRestoreBackupArchive( + filePath: filePath, + ); + + @override + int get hashCode => 0; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is RustBackupArchive && runtimeType == other.runtimeType; +} + +class RustBackupIdentity { + const RustBackupIdentity(); + + static Future getBackupId() => RustLib.instance.api + .crateBridgeWrapperBackupRustBackupIdentityGetBackupId(); + + static Future getBackupPasswordKeys({ + required PlatformInt64 userId, + required String password, + }) => RustLib.instance.api + .crateBridgeWrapperBackupRustBackupIdentityGetBackupPasswordKeys( + userId: userId, + password: password, + ); + + static Future getIdentityBackupBytes() => RustLib.instance.api + .crateBridgeWrapperBackupRustBackupIdentityGetIdentityBackupBytes(); + + static Future importBackupPasswordKeys({ + required List backupId, + required List encryptionKey, + }) => RustLib.instance.api + .crateBridgeWrapperBackupRustBackupIdentityImportBackupPasswordKeys( + backupId: backupId, + encryptionKey: encryptionKey, + ); + + static Future restoreIdentityBackup({ + required BackupPasswordKeys keys, + required List encryptedBytes, + }) => RustLib.instance.api + .crateBridgeWrapperBackupRustBackupIdentityRestoreIdentityBackup( + keys: keys, + encryptedBytes: encryptedBytes, + ); + + static Future setBackupPasswordKeys({ + required PlatformInt64 userId, + required String password, + }) => RustLib.instance.api + .crateBridgeWrapperBackupRustBackupIdentitySetBackupPasswordKeys( + userId: userId, + password: password, + ); + + @override + int get hashCode => 0; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is RustBackupIdentity && runtimeType == other.runtimeType; +} diff --git a/lib/core/bridge/wrapper/key_manager.dart b/lib/core/bridge/wrapper/key_manager.dart new file mode 100644 index 00000000..46c57e90 --- /dev/null +++ b/lib/core/bridge/wrapper/key_manager.dart @@ -0,0 +1,77 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.12.0. + +// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import + +import '../../frb_generated.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; + +class RustKeyManager { + const RustKeyManager(); + + static Future getLoginToken() => RustLib.instance.api + .crateBridgeWrapperKeyManagerRustKeyManagerGetLoginToken(); + + static Future<(Uint8List, PlatformInt64)> getSignalIdentity() => RustLib + .instance + .api + .crateBridgeWrapperKeyManagerRustKeyManagerGetSignalIdentity(); + + static Future getUserId() => RustLib.instance.api + .crateBridgeWrapperKeyManagerRustKeyManagerGetUserId(); + + static Future importSignalIdentity({ + required List identityKeyPairStructure, + required PlatformInt64 registrationId, + required Map signedPreKeyStore, + }) => RustLib.instance.api + .crateBridgeWrapperKeyManagerRustKeyManagerImportSignalIdentity( + identityKeyPairStructure: identityKeyPairStructure, + registrationId: registrationId, + signedPreKeyStore: signedPreKeyStore, + ); + + static Future loadSignedPrekey({ + required PlatformInt64 signedPreKeyId, + }) => RustLib.instance.api + .crateBridgeWrapperKeyManagerRustKeyManagerLoadSignedPrekey( + signedPreKeyId: signedPreKeyId, + ); + + static Future> loadSignedPrekeys() => RustLib + .instance + .api + .crateBridgeWrapperKeyManagerRustKeyManagerLoadSignedPrekeys(); + + static Future removeKeyManager() => RustLib.instance.api + .crateBridgeWrapperKeyManagerRustKeyManagerRemoveKeyManager(); + + static Future removeSignedPrekey({ + required PlatformInt64 signedPreKeyId, + }) => RustLib.instance.api + .crateBridgeWrapperKeyManagerRustKeyManagerRemoveSignedPrekey( + signedPreKeyId: signedPreKeyId, + ); + + static Future setUserId({required PlatformInt64 userId}) => RustLib + .instance + .api + .crateBridgeWrapperKeyManagerRustKeyManagerSetUserId(userId: userId); + + static Future storeSignedPrekey({ + required PlatformInt64 signedPreKeyId, + required List record, + }) => RustLib.instance.api + .crateBridgeWrapperKeyManagerRustKeyManagerStoreSignedPrekey( + signedPreKeyId: signedPreKeyId, + record: record, + ); + + @override + int get hashCode => 0; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is RustKeyManager && runtimeType == other.runtimeType; +} diff --git a/lib/core/context.dart b/lib/core/context.dart new file mode 100644 index 00000000..7294be61 --- /dev/null +++ b/lib/core/context.dart @@ -0,0 +1,28 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.12.0. + +// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import + +import 'frb_generated.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; + +class InitConfig { + final String databasePath; + final String dataDirectory; + + const InitConfig({ + required this.databasePath, + required this.dataDirectory, + }); + + @override + int get hashCode => databasePath.hashCode ^ dataDirectory.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is InitConfig && + runtimeType == other.runtimeType && + databasePath == other.databasePath && + dataDirectory == other.dataDirectory; +} diff --git a/lib/core/frb_generated.dart b/lib/core/frb_generated.dart index 28dac796..ae786047 100644 --- a/lib/core/frb_generated.dart +++ b/lib/core/frb_generated.dart @@ -5,12 +5,16 @@ import 'bridge.dart'; import 'bridge/callbacks.dart'; +import 'bridge/wrapper/backup.dart'; +import 'bridge/wrapper/key_manager.dart'; import 'bridge/wrapper/user_discovery.dart'; import 'dart:async'; import 'dart:convert'; import 'frb_generated.dart'; import 'frb_generated.io.dart' if (dart.library.js_interop) 'frb_generated.web.dart'; +import 'keys/backup_password_keys.dart'; +import 'lib.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; /// Main entrypoint of the Rust API @@ -70,7 +74,7 @@ class RustLib extends BaseEntrypoint { String get codegenVersion => '2.12.0'; @override - int get rustContentHash => 1680338106; + int get rustContentHash => -1867463121; static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig( @@ -152,8 +156,79 @@ abstract class RustLibApi extends BaseApi { userDiscoveryGetContactPromotion, }); - Future crateBridgeInitializeTwonlyFlutter({ - required TwonlyConfig config, + Future crateBridgeInitializeTwonlyFlutter({required InitConfig config}); + + Future<(String, String)> + crateBridgeWrapperBackupRustBackupArchiveCreateBackupArchive(); + + Future + crateBridgeWrapperBackupRustBackupArchiveGetBackupDownloadToken(); + + Future crateBridgeWrapperBackupRustBackupArchiveRestoreBackupArchive({ + required String filePath, + }); + + Future crateBridgeWrapperBackupRustBackupIdentityGetBackupId(); + + Future + crateBridgeWrapperBackupRustBackupIdentityGetBackupPasswordKeys({ + required PlatformInt64 userId, + required String password, + }); + + Future + crateBridgeWrapperBackupRustBackupIdentityGetIdentityBackupBytes(); + + Future + crateBridgeWrapperBackupRustBackupIdentityImportBackupPasswordKeys({ + required List backupId, + required List encryptionKey, + }); + + Future crateBridgeWrapperBackupRustBackupIdentityRestoreIdentityBackup({ + required BackupPasswordKeys keys, + required List encryptedBytes, + }); + + Future crateBridgeWrapperBackupRustBackupIdentitySetBackupPasswordKeys({ + required PlatformInt64 userId, + required String password, + }); + + Future crateBridgeWrapperKeyManagerRustKeyManagerGetLoginToken(); + + Future<(Uint8List, PlatformInt64)> + crateBridgeWrapperKeyManagerRustKeyManagerGetSignalIdentity(); + + Future crateBridgeWrapperKeyManagerRustKeyManagerGetUserId(); + + Future crateBridgeWrapperKeyManagerRustKeyManagerImportSignalIdentity({ + required List identityKeyPairStructure, + required PlatformInt64 registrationId, + required Map signedPreKeyStore, + }); + + Future + crateBridgeWrapperKeyManagerRustKeyManagerLoadSignedPrekey({ + required PlatformInt64 signedPreKeyId, + }); + + Future> + crateBridgeWrapperKeyManagerRustKeyManagerLoadSignedPrekeys(); + + Future crateBridgeWrapperKeyManagerRustKeyManagerRemoveKeyManager(); + + Future crateBridgeWrapperKeyManagerRustKeyManagerRemoveSignedPrekey({ + required PlatformInt64 signedPreKeyId, + }); + + Future crateBridgeWrapperKeyManagerRustKeyManagerSetUserId({ + required PlatformInt64 userId, + }); + + Future crateBridgeWrapperKeyManagerRustKeyManagerStoreSignedPrekey({ + required PlatformInt64 signedPreKeyId, + required List record, }); } @@ -556,13 +631,13 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { @override Future crateBridgeInitializeTwonlyFlutter({ - required TwonlyConfig config, + required InitConfig config, }) { return handler.executeNormal( NormalTask( callFfi: (port_) { final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_box_autoadd_twonly_config(config, serializer); + sse_encode_box_autoadd_init_config(config, serializer); pdeCallFfi( generalizedFrbRustBinding, serializer, @@ -587,6 +662,677 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["config"], ); + @override + Future<(String, String)> + crateBridgeWrapperBackupRustBackupArchiveCreateBackupArchive() { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 9, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_record_string_string, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: + kCrateBridgeWrapperBackupRustBackupArchiveCreateBackupArchiveConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateBridgeWrapperBackupRustBackupArchiveCreateBackupArchiveConstMeta => + const TaskConstMeta( + debugName: "rust_backup_archive_create_backup_archive", + argNames: [], + ); + + @override + Future + crateBridgeWrapperBackupRustBackupArchiveGetBackupDownloadToken() { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 10, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_opt_String, + decodeErrorData: null, + ), + constMeta: + kCrateBridgeWrapperBackupRustBackupArchiveGetBackupDownloadTokenConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateBridgeWrapperBackupRustBackupArchiveGetBackupDownloadTokenConstMeta => + const TaskConstMeta( + debugName: "rust_backup_archive_get_backup_download_token", + argNames: [], + ); + + @override + Future crateBridgeWrapperBackupRustBackupArchiveRestoreBackupArchive({ + required String filePath, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(filePath, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 11, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: + kCrateBridgeWrapperBackupRustBackupArchiveRestoreBackupArchiveConstMeta, + argValues: [filePath], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateBridgeWrapperBackupRustBackupArchiveRestoreBackupArchiveConstMeta => + const TaskConstMeta( + debugName: "rust_backup_archive_restore_backup_archive", + argNames: ["filePath"], + ); + + @override + Future crateBridgeWrapperBackupRustBackupIdentityGetBackupId() { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 12, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_opt_String, + decodeErrorData: null, + ), + constMeta: + kCrateBridgeWrapperBackupRustBackupIdentityGetBackupIdConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateBridgeWrapperBackupRustBackupIdentityGetBackupIdConstMeta => + const TaskConstMeta( + debugName: "rust_backup_identity_get_backup_id", + argNames: [], + ); + + @override + Future + crateBridgeWrapperBackupRustBackupIdentityGetBackupPasswordKeys({ + required PlatformInt64 userId, + required String password, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_i_64(userId, serializer); + sse_encode_String(password, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 13, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_backup_password_keys, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: + kCrateBridgeWrapperBackupRustBackupIdentityGetBackupPasswordKeysConstMeta, + argValues: [userId, password], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateBridgeWrapperBackupRustBackupIdentityGetBackupPasswordKeysConstMeta => + const TaskConstMeta( + debugName: "rust_backup_identity_get_backup_password_keys", + argNames: ["userId", "password"], + ); + + @override + Future + crateBridgeWrapperBackupRustBackupIdentityGetIdentityBackupBytes() { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 14, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_list_prim_u_8_strict, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: + kCrateBridgeWrapperBackupRustBackupIdentityGetIdentityBackupBytesConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateBridgeWrapperBackupRustBackupIdentityGetIdentityBackupBytesConstMeta => + const TaskConstMeta( + debugName: "rust_backup_identity_get_identity_backup_bytes", + argNames: [], + ); + + @override + Future + crateBridgeWrapperBackupRustBackupIdentityImportBackupPasswordKeys({ + required List backupId, + required List encryptionKey, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_list_prim_u_8_loose(backupId, serializer); + sse_encode_list_prim_u_8_loose(encryptionKey, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 15, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: + kCrateBridgeWrapperBackupRustBackupIdentityImportBackupPasswordKeysConstMeta, + argValues: [backupId, encryptionKey], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateBridgeWrapperBackupRustBackupIdentityImportBackupPasswordKeysConstMeta => + const TaskConstMeta( + debugName: "rust_backup_identity_import_backup_password_keys", + argNames: ["backupId", "encryptionKey"], + ); + + @override + Future crateBridgeWrapperBackupRustBackupIdentityRestoreIdentityBackup({ + required BackupPasswordKeys keys, + required List encryptedBytes, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_box_autoadd_backup_password_keys(keys, serializer); + sse_encode_list_prim_u_8_loose(encryptedBytes, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 16, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: + kCrateBridgeWrapperBackupRustBackupIdentityRestoreIdentityBackupConstMeta, + argValues: [keys, encryptedBytes], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateBridgeWrapperBackupRustBackupIdentityRestoreIdentityBackupConstMeta => + const TaskConstMeta( + debugName: "rust_backup_identity_restore_identity_backup", + argNames: ["keys", "encryptedBytes"], + ); + + @override + Future crateBridgeWrapperBackupRustBackupIdentitySetBackupPasswordKeys({ + required PlatformInt64 userId, + required String password, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_i_64(userId, serializer); + sse_encode_String(password, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 17, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: + kCrateBridgeWrapperBackupRustBackupIdentitySetBackupPasswordKeysConstMeta, + argValues: [userId, password], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateBridgeWrapperBackupRustBackupIdentitySetBackupPasswordKeysConstMeta => + const TaskConstMeta( + debugName: "rust_backup_identity_set_backup_password_keys", + argNames: ["userId", "password"], + ); + + @override + Future crateBridgeWrapperKeyManagerRustKeyManagerGetLoginToken() { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 18, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_list_prim_u_8_strict, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: + kCrateBridgeWrapperKeyManagerRustKeyManagerGetLoginTokenConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateBridgeWrapperKeyManagerRustKeyManagerGetLoginTokenConstMeta => + const TaskConstMeta( + debugName: "rust_key_manager_get_login_token", + argNames: [], + ); + + @override + Future<(Uint8List, PlatformInt64)> + crateBridgeWrapperKeyManagerRustKeyManagerGetSignalIdentity() { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 19, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_record_list_prim_u_8_strict_i_64, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: + kCrateBridgeWrapperKeyManagerRustKeyManagerGetSignalIdentityConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateBridgeWrapperKeyManagerRustKeyManagerGetSignalIdentityConstMeta => + const TaskConstMeta( + debugName: "rust_key_manager_get_signal_identity", + argNames: [], + ); + + @override + Future crateBridgeWrapperKeyManagerRustKeyManagerGetUserId() { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 20, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_opt_box_autoadd_i_64, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: + kCrateBridgeWrapperKeyManagerRustKeyManagerGetUserIdConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateBridgeWrapperKeyManagerRustKeyManagerGetUserIdConstMeta => + const TaskConstMeta( + debugName: "rust_key_manager_get_user_id", + argNames: [], + ); + + @override + Future crateBridgeWrapperKeyManagerRustKeyManagerImportSignalIdentity({ + required List identityKeyPairStructure, + required PlatformInt64 registrationId, + required Map signedPreKeyStore, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_list_prim_u_8_loose(identityKeyPairStructure, serializer); + sse_encode_i_64(registrationId, serializer); + sse_encode_Map_i_64_list_prim_u_8_strict_None( + signedPreKeyStore, + serializer, + ); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 21, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: + kCrateBridgeWrapperKeyManagerRustKeyManagerImportSignalIdentityConstMeta, + argValues: [ + identityKeyPairStructure, + registrationId, + signedPreKeyStore, + ], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateBridgeWrapperKeyManagerRustKeyManagerImportSignalIdentityConstMeta => + const TaskConstMeta( + debugName: "rust_key_manager_import_signal_identity", + argNames: [ + "identityKeyPairStructure", + "registrationId", + "signedPreKeyStore", + ], + ); + + @override + Future + crateBridgeWrapperKeyManagerRustKeyManagerLoadSignedPrekey({ + required PlatformInt64 signedPreKeyId, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_i_64(signedPreKeyId, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 22, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_opt_list_prim_u_8_strict, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: + kCrateBridgeWrapperKeyManagerRustKeyManagerLoadSignedPrekeyConstMeta, + argValues: [signedPreKeyId], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateBridgeWrapperKeyManagerRustKeyManagerLoadSignedPrekeyConstMeta => + const TaskConstMeta( + debugName: "rust_key_manager_load_signed_prekey", + argNames: ["signedPreKeyId"], + ); + + @override + Future> + crateBridgeWrapperKeyManagerRustKeyManagerLoadSignedPrekeys() { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 23, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_Map_i_64_list_prim_u_8_strict_None, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: + kCrateBridgeWrapperKeyManagerRustKeyManagerLoadSignedPrekeysConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateBridgeWrapperKeyManagerRustKeyManagerLoadSignedPrekeysConstMeta => + const TaskConstMeta( + debugName: "rust_key_manager_load_signed_prekeys", + argNames: [], + ); + + @override + Future crateBridgeWrapperKeyManagerRustKeyManagerRemoveKeyManager() { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 24, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: + kCrateBridgeWrapperKeyManagerRustKeyManagerRemoveKeyManagerConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateBridgeWrapperKeyManagerRustKeyManagerRemoveKeyManagerConstMeta => + const TaskConstMeta( + debugName: "rust_key_manager_remove_key_manager", + argNames: [], + ); + + @override + Future crateBridgeWrapperKeyManagerRustKeyManagerRemoveSignedPrekey({ + required PlatformInt64 signedPreKeyId, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_i_64(signedPreKeyId, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 25, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: + kCrateBridgeWrapperKeyManagerRustKeyManagerRemoveSignedPrekeyConstMeta, + argValues: [signedPreKeyId], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateBridgeWrapperKeyManagerRustKeyManagerRemoveSignedPrekeyConstMeta => + const TaskConstMeta( + debugName: "rust_key_manager_remove_signed_prekey", + argNames: ["signedPreKeyId"], + ); + + @override + Future crateBridgeWrapperKeyManagerRustKeyManagerSetUserId({ + required PlatformInt64 userId, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_i_64(userId, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 26, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: + kCrateBridgeWrapperKeyManagerRustKeyManagerSetUserIdConstMeta, + argValues: [userId], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateBridgeWrapperKeyManagerRustKeyManagerSetUserIdConstMeta => + const TaskConstMeta( + debugName: "rust_key_manager_set_user_id", + argNames: ["userId"], + ); + + @override + Future crateBridgeWrapperKeyManagerRustKeyManagerStoreSignedPrekey({ + required PlatformInt64 signedPreKeyId, + required List record, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_i_64(signedPreKeyId, serializer); + sse_encode_list_prim_u_8_loose(record, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 27, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: + kCrateBridgeWrapperKeyManagerRustKeyManagerStoreSignedPrekeyConstMeta, + argValues: [signedPreKeyId, record], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateBridgeWrapperKeyManagerRustKeyManagerStoreSignedPrekeyConstMeta => + const TaskConstMeta( + debugName: "rust_key_manager_store_signed_prekey", + argNames: ["signedPreKeyId", "record"], + ); + Future Function( int, ) @@ -1136,6 +1882,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return decodeDartOpaque(raw, generalizedFrbRustBinding); } + @protected + Map dco_decode_Map_i_64_list_prim_u_8_strict_None( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return Map.fromEntries( + dco_decode_list_record_i_64_list_prim_u_8_strict( + raw, + ).map((e) => MapEntry(e.$1, e.$2)), + ); + } + @protected RustStreamSink dco_decode_StreamSink_String_Sse(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -1161,6 +1919,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + BackupPasswordKeys dco_decode_backup_password_keys(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 2) + throw Exception('unexpected arr length: expect 2 but see ${arr.length}'); + return BackupPasswordKeys( + backupId: dco_decode_u_8_array_32(arr[0]), + encryptionKey: dco_decode_u_8_array_32(arr[1]), + ); + } + @protected bool dco_decode_bool(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -1173,6 +1943,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return dco_decode_announced_user(raw); } + @protected + BackupPasswordKeys dco_decode_box_autoadd_backup_password_keys(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return dco_decode_backup_password_keys(raw); + } + @protected PlatformInt64 dco_decode_box_autoadd_i_64(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -1180,9 +1956,9 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } @protected - TwonlyConfig dco_decode_box_autoadd_twonly_config(dynamic raw) { + InitConfig dco_decode_box_autoadd_init_config(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs - return dco_decode_twonly_config(raw); + return dco_decode_init_config(raw); } @protected @@ -1200,6 +1976,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return dcoDecodeI64(raw); } + @protected + InitConfig dco_decode_init_config(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 2) + throw Exception('unexpected arr length: expect 2 but see ${arr.length}'); + return InitConfig( + databaseDir: dco_decode_String(arr[0]), + dataDir: dco_decode_String(arr[1]), + ); + } + @protected PlatformInt64 dco_decode_isize(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -1230,6 +2018,21 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return raw as Uint8List; } + @protected + List<(PlatformInt64, Uint8List)> + dco_decode_list_record_i_64_list_prim_u_8_strict(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return (raw as List) + .map(dco_decode_record_i_64_list_prim_u_8_strict) + .toList(); + } + + @protected + String? dco_decode_opt_String(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw == null ? null : dco_decode_String(raw); + } + @protected AnnouncedUser? dco_decode_opt_box_autoadd_announced_user(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -1277,17 +2080,75 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } @protected - TwonlyConfig dco_decode_twonly_config(dynamic raw) { + (PlatformInt64, Uint8List) dco_decode_record_i_64_list_prim_u_8_strict( + dynamic raw, + ) { // Codec=Dco (DartCObject based), see doc to use other codecs final arr = raw as List; - if (arr.length != 2) - throw Exception('unexpected arr length: expect 2 but see ${arr.length}'); - return TwonlyConfig( - databasePath: dco_decode_String(arr[0]), - dataDirectory: dco_decode_String(arr[1]), + if (arr.length != 2) { + throw Exception('Expected 2 elements, got ${arr.length}'); + } + return ( + dco_decode_i_64(arr[0]), + dco_decode_list_prim_u_8_strict(arr[1]), ); } + @protected + (Uint8List, PlatformInt64) dco_decode_record_list_prim_u_8_strict_i_64( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 2) { + throw Exception('Expected 2 elements, got ${arr.length}'); + } + return ( + dco_decode_list_prim_u_8_strict(arr[0]), + dco_decode_i_64(arr[1]), + ); + } + + @protected + (String, String) dco_decode_record_string_string(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 2) { + throw Exception('Expected 2 elements, got ${arr.length}'); + } + return ( + dco_decode_String(arr[0]), + dco_decode_String(arr[1]), + ); + } + + @protected + RustBackupArchive dco_decode_rust_backup_archive(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 0) + throw Exception('unexpected arr length: expect 0 but see ${arr.length}'); + return RustBackupArchive(); + } + + @protected + RustBackupIdentity dco_decode_rust_backup_identity(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 0) + throw Exception('unexpected arr length: expect 0 but see ${arr.length}'); + return RustBackupIdentity(); + } + + @protected + RustKeyManager dco_decode_rust_key_manager(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 0) + throw Exception('unexpected arr length: expect 0 but see ${arr.length}'); + return RustKeyManager(); + } + @protected int dco_decode_u_32(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -1300,6 +2161,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return raw as int; } + @protected + U8Array32 dco_decode_u_8_array_32(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return U8Array32(dco_decode_list_prim_u_8_strict(raw)); + } + @protected void dco_decode_unit(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -1326,6 +2193,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return decodeDartOpaque(inner, generalizedFrbRustBinding); } + @protected + Map sse_decode_Map_i_64_list_prim_u_8_strict_None( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + var inner = sse_decode_list_record_i_64_list_prim_u_8_strict(deserializer); + return Map.fromEntries(inner.map((e) => MapEntry(e.$1, e.$2))); + } + @protected RustStreamSink sse_decode_StreamSink_String_Sse( SseDeserializer deserializer, @@ -1354,6 +2230,19 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + BackupPasswordKeys sse_decode_backup_password_keys( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_backupId = sse_decode_u_8_array_32(deserializer); + var var_encryptionKey = sse_decode_u_8_array_32(deserializer); + return BackupPasswordKeys( + backupId: var_backupId, + encryptionKey: var_encryptionKey, + ); + } + @protected bool sse_decode_bool(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -1368,6 +2257,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return (sse_decode_announced_user(deserializer)); } + @protected + BackupPasswordKeys sse_decode_box_autoadd_backup_password_keys( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return (sse_decode_backup_password_keys(deserializer)); + } + @protected PlatformInt64 sse_decode_box_autoadd_i_64(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -1375,11 +2272,9 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } @protected - TwonlyConfig sse_decode_box_autoadd_twonly_config( - SseDeserializer deserializer, - ) { + InitConfig sse_decode_box_autoadd_init_config(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs - return (sse_decode_twonly_config(deserializer)); + return (sse_decode_init_config(deserializer)); } @protected @@ -1396,6 +2291,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return deserializer.buffer.getPlatformInt64(); } + @protected + InitConfig sse_decode_init_config(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_databaseDir = sse_decode_String(deserializer); + var var_dataDir = sse_decode_String(deserializer); + return InitConfig(databaseDir: var_databaseDir, dataDir: var_dataDir); + } + @protected PlatformInt64 sse_decode_isize(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -1444,6 +2347,32 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return deserializer.buffer.getUint8List(len_); } + @protected + List<(PlatformInt64, Uint8List)> + sse_decode_list_record_i_64_list_prim_u_8_strict( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + var len_ = sse_decode_i_32(deserializer); + var ans_ = <(PlatformInt64, Uint8List)>[]; + for (var idx_ = 0; idx_ < len_; ++idx_) { + ans_.add(sse_decode_record_i_64_list_prim_u_8_strict(deserializer)); + } + return ans_; + } + + @protected + String? sse_decode_opt_String(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + if (sse_decode_bool(deserializer)) { + return (sse_decode_String(deserializer)); + } else { + return null; + } + } + @protected AnnouncedUser? sse_decode_opt_box_autoadd_announced_user( SseDeserializer deserializer, @@ -1527,14 +2456,55 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } @protected - TwonlyConfig sse_decode_twonly_config(SseDeserializer deserializer) { + (PlatformInt64, Uint8List) sse_decode_record_i_64_list_prim_u_8_strict( + SseDeserializer deserializer, + ) { // Codec=Sse (Serialization based), see doc to use other codecs - var var_databasePath = sse_decode_String(deserializer); - var var_dataDirectory = sse_decode_String(deserializer); - return TwonlyConfig( - databasePath: var_databasePath, - dataDirectory: var_dataDirectory, - ); + var var_field0 = sse_decode_i_64(deserializer); + var var_field1 = sse_decode_list_prim_u_8_strict(deserializer); + return (var_field0, var_field1); + } + + @protected + (Uint8List, PlatformInt64) sse_decode_record_list_prim_u_8_strict_i_64( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_field0 = sse_decode_list_prim_u_8_strict(deserializer); + var var_field1 = sse_decode_i_64(deserializer); + return (var_field0, var_field1); + } + + @protected + (String, String) sse_decode_record_string_string( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_field0 = sse_decode_String(deserializer); + var var_field1 = sse_decode_String(deserializer); + return (var_field0, var_field1); + } + + @protected + RustBackupArchive sse_decode_rust_backup_archive( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return RustBackupArchive(); + } + + @protected + RustBackupIdentity sse_decode_rust_backup_identity( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return RustBackupIdentity(); + } + + @protected + RustKeyManager sse_decode_rust_key_manager(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return RustKeyManager(); } @protected @@ -1549,6 +2519,13 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return deserializer.buffer.getUint8(); } + @protected + U8Array32 sse_decode_u_8_array_32(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var inner = sse_decode_list_prim_u_8_strict(deserializer); + return U8Array32(inner); + } + @protected void sse_decode_unit(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -1764,6 +2741,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + void sse_encode_Map_i_64_list_prim_u_8_strict_None( + Map self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_list_record_i_64_list_prim_u_8_strict( + self.entries.map((e) => (e.key, e.value)).toList(), + serializer, + ); + } + @protected void sse_encode_StreamSink_String_Sse( RustStreamSink self, @@ -1795,6 +2784,16 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_i_64(self.publicId, serializer); } + @protected + void sse_encode_backup_password_keys( + BackupPasswordKeys self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_u_8_array_32(self.backupId, serializer); + sse_encode_u_8_array_32(self.encryptionKey, serializer); + } + @protected void sse_encode_bool(bool self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -1810,6 +2809,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_announced_user(self, serializer); } + @protected + void sse_encode_box_autoadd_backup_password_keys( + BackupPasswordKeys self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_backup_password_keys(self, serializer); + } + @protected void sse_encode_box_autoadd_i_64( PlatformInt64 self, @@ -1820,12 +2828,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } @protected - void sse_encode_box_autoadd_twonly_config( - TwonlyConfig self, + void sse_encode_box_autoadd_init_config( + InitConfig self, SseSerializer serializer, ) { // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_twonly_config(self, serializer); + sse_encode_init_config(self, serializer); } @protected @@ -1842,6 +2850,13 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { serializer.buffer.putPlatformInt64(self); } + @protected + void sse_encode_init_config(InitConfig self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_String(self.databaseDir, serializer); + sse_encode_String(self.dataDir, serializer); + } + @protected void sse_encode_isize(PlatformInt64 self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -1894,6 +2909,28 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { serializer.buffer.putUint8List(self); } + @protected + void sse_encode_list_record_i_64_list_prim_u_8_strict( + List<(PlatformInt64, Uint8List)> 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_record_i_64_list_prim_u_8_strict(item, serializer); + } + } + + @protected + void sse_encode_opt_String(String? self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + sse_encode_bool(self != null, serializer); + if (self != null) { + sse_encode_String(self, serializer); + } + } + @protected void sse_encode_opt_box_autoadd_announced_user( AnnouncedUser? self, @@ -1977,10 +3014,57 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } @protected - void sse_encode_twonly_config(TwonlyConfig self, SseSerializer serializer) { + void sse_encode_record_i_64_list_prim_u_8_strict( + (PlatformInt64, Uint8List) self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_64(self.$1, serializer); + sse_encode_list_prim_u_8_strict(self.$2, serializer); + } + + @protected + void sse_encode_record_list_prim_u_8_strict_i_64( + (Uint8List, PlatformInt64) self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_list_prim_u_8_strict(self.$1, serializer); + sse_encode_i_64(self.$2, serializer); + } + + @protected + void sse_encode_record_string_string( + (String, String) self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_String(self.$1, serializer); + sse_encode_String(self.$2, serializer); + } + + @protected + void sse_encode_rust_backup_archive( + RustBackupArchive self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + } + + @protected + void sse_encode_rust_backup_identity( + RustBackupIdentity self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + } + + @protected + void sse_encode_rust_key_manager( + RustKeyManager self, + SseSerializer serializer, + ) { // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_String(self.databasePath, serializer); - sse_encode_String(self.dataDirectory, serializer); } @protected @@ -1995,6 +3079,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { serializer.buffer.putUint8(self); } + @protected + void sse_encode_u_8_array_32(U8Array32 self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_list_prim_u_8_strict(self.inner, serializer); + } + @protected void sse_encode_unit(void self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs diff --git a/lib/core/frb_generated.io.dart b/lib/core/frb_generated.io.dart index e5c71e93..938d45c5 100644 --- a/lib/core/frb_generated.io.dart +++ b/lib/core/frb_generated.io.dart @@ -5,11 +5,15 @@ import 'bridge.dart'; import 'bridge/callbacks.dart'; +import 'bridge/wrapper/backup.dart'; +import 'bridge/wrapper/key_manager.dart'; import 'bridge/wrapper/user_discovery.dart'; import 'dart:async'; import 'dart:convert'; import 'dart:ffi' as ffi; import 'frb_generated.dart'; +import 'keys/backup_password_keys.dart'; +import 'lib.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_io.dart'; abstract class RustLibApiImplPlatform extends BaseApiImpl { @@ -98,6 +102,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected Object dco_decode_DartOpaque(dynamic raw); + @protected + Map dco_decode_Map_i_64_list_prim_u_8_strict_None( + dynamic raw, + ); + @protected RustStreamSink dco_decode_StreamSink_String_Sse(dynamic raw); @@ -107,17 +116,23 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected AnnouncedUser dco_decode_announced_user(dynamic raw); + @protected + BackupPasswordKeys dco_decode_backup_password_keys(dynamic raw); + @protected bool dco_decode_bool(dynamic raw); @protected AnnouncedUser dco_decode_box_autoadd_announced_user(dynamic raw); + @protected + BackupPasswordKeys dco_decode_box_autoadd_backup_password_keys(dynamic raw); + @protected PlatformInt64 dco_decode_box_autoadd_i_64(dynamic raw); @protected - TwonlyConfig dco_decode_box_autoadd_twonly_config(dynamic raw); + InitConfig dco_decode_box_autoadd_init_config(dynamic raw); @protected FlutterUserDiscovery dco_decode_flutter_user_discovery(dynamic raw); @@ -125,6 +140,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected PlatformInt64 dco_decode_i_64(dynamic raw); + @protected + InitConfig dco_decode_init_config(dynamic raw); + @protected PlatformInt64 dco_decode_isize(dynamic raw); @@ -140,6 +158,13 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected Uint8List dco_decode_list_prim_u_8_strict(dynamic raw); + @protected + List<(PlatformInt64, Uint8List)> + dco_decode_list_record_i_64_list_prim_u_8_strict(dynamic raw); + + @protected + String? dco_decode_opt_String(dynamic raw); + @protected AnnouncedUser? dco_decode_opt_box_autoadd_announced_user(dynamic raw); @@ -159,7 +184,26 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { OtherPromotion dco_decode_other_promotion(dynamic raw); @protected - TwonlyConfig dco_decode_twonly_config(dynamic raw); + (PlatformInt64, Uint8List) dco_decode_record_i_64_list_prim_u_8_strict( + dynamic raw, + ); + + @protected + (Uint8List, PlatformInt64) dco_decode_record_list_prim_u_8_strict_i_64( + dynamic raw, + ); + + @protected + (String, String) dco_decode_record_string_string(dynamic raw); + + @protected + RustBackupArchive dco_decode_rust_backup_archive(dynamic raw); + + @protected + RustBackupIdentity dco_decode_rust_backup_identity(dynamic raw); + + @protected + RustKeyManager dco_decode_rust_key_manager(dynamic raw); @protected int dco_decode_u_32(dynamic raw); @@ -167,6 +211,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected int dco_decode_u_8(dynamic raw); + @protected + U8Array32 dco_decode_u_8_array_32(dynamic raw); + @protected void dco_decode_unit(dynamic raw); @@ -179,6 +226,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected Object sse_decode_DartOpaque(SseDeserializer deserializer); + @protected + Map sse_decode_Map_i_64_list_prim_u_8_strict_None( + SseDeserializer deserializer, + ); + @protected RustStreamSink sse_decode_StreamSink_String_Sse( SseDeserializer deserializer, @@ -190,6 +242,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected AnnouncedUser sse_decode_announced_user(SseDeserializer deserializer); + @protected + BackupPasswordKeys sse_decode_backup_password_keys( + SseDeserializer deserializer, + ); + @protected bool sse_decode_bool(SseDeserializer deserializer); @@ -198,13 +255,16 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); + @protected + BackupPasswordKeys sse_decode_box_autoadd_backup_password_keys( + SseDeserializer deserializer, + ); + @protected PlatformInt64 sse_decode_box_autoadd_i_64(SseDeserializer deserializer); @protected - TwonlyConfig sse_decode_box_autoadd_twonly_config( - SseDeserializer deserializer, - ); + InitConfig sse_decode_box_autoadd_init_config(SseDeserializer deserializer); @protected FlutterUserDiscovery sse_decode_flutter_user_discovery( @@ -214,6 +274,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected PlatformInt64 sse_decode_i_64(SseDeserializer deserializer); + @protected + InitConfig sse_decode_init_config(SseDeserializer deserializer); + @protected PlatformInt64 sse_decode_isize(SseDeserializer deserializer); @@ -233,6 +296,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer); + @protected + List<(PlatformInt64, Uint8List)> + sse_decode_list_record_i_64_list_prim_u_8_strict( + SseDeserializer deserializer, + ); + + @protected + String? sse_decode_opt_String(SseDeserializer deserializer); + @protected AnnouncedUser? sse_decode_opt_box_autoadd_announced_user( SseDeserializer deserializer, @@ -258,7 +330,32 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { OtherPromotion sse_decode_other_promotion(SseDeserializer deserializer); @protected - TwonlyConfig sse_decode_twonly_config(SseDeserializer deserializer); + (PlatformInt64, Uint8List) sse_decode_record_i_64_list_prim_u_8_strict( + SseDeserializer deserializer, + ); + + @protected + (Uint8List, PlatformInt64) sse_decode_record_list_prim_u_8_strict_i_64( + SseDeserializer deserializer, + ); + + @protected + (String, String) sse_decode_record_string_string( + SseDeserializer deserializer, + ); + + @protected + RustBackupArchive sse_decode_rust_backup_archive( + SseDeserializer deserializer, + ); + + @protected + RustBackupIdentity sse_decode_rust_backup_identity( + SseDeserializer deserializer, + ); + + @protected + RustKeyManager sse_decode_rust_key_manager(SseDeserializer deserializer); @protected int sse_decode_u_32(SseDeserializer deserializer); @@ -266,6 +363,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected int sse_decode_u_8(SseDeserializer deserializer); + @protected + U8Array32 sse_decode_u_8_array_32(SseDeserializer deserializer); + @protected void sse_decode_unit(SseDeserializer deserializer); @@ -366,6 +466,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_DartOpaque(Object self, SseSerializer serializer); + @protected + void sse_encode_Map_i_64_list_prim_u_8_strict_None( + Map self, + SseSerializer serializer, + ); + @protected void sse_encode_StreamSink_String_Sse( RustStreamSink self, @@ -378,6 +484,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_announced_user(AnnouncedUser self, SseSerializer serializer); + @protected + void sse_encode_backup_password_keys( + BackupPasswordKeys self, + SseSerializer serializer, + ); + @protected void sse_encode_bool(bool self, SseSerializer serializer); @@ -387,6 +499,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_box_autoadd_backup_password_keys( + BackupPasswordKeys self, + SseSerializer serializer, + ); + @protected void sse_encode_box_autoadd_i_64( PlatformInt64 self, @@ -394,8 +512,8 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { ); @protected - void sse_encode_box_autoadd_twonly_config( - TwonlyConfig self, + void sse_encode_box_autoadd_init_config( + InitConfig self, SseSerializer serializer, ); @@ -408,6 +526,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_i_64(PlatformInt64 self, SseSerializer serializer); + @protected + void sse_encode_init_config(InitConfig self, SseSerializer serializer); + @protected void sse_encode_isize(PlatformInt64 self, SseSerializer serializer); @@ -432,6 +553,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_list_record_i_64_list_prim_u_8_strict( + List<(PlatformInt64, Uint8List)> self, + SseSerializer serializer, + ); + + @protected + void sse_encode_opt_String(String? self, SseSerializer serializer); + @protected void sse_encode_opt_box_autoadd_announced_user( AnnouncedUser? self, @@ -469,7 +599,40 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { ); @protected - void sse_encode_twonly_config(TwonlyConfig self, SseSerializer serializer); + void sse_encode_record_i_64_list_prim_u_8_strict( + (PlatformInt64, Uint8List) self, + SseSerializer serializer, + ); + + @protected + void sse_encode_record_list_prim_u_8_strict_i_64( + (Uint8List, PlatformInt64) self, + SseSerializer serializer, + ); + + @protected + void sse_encode_record_string_string( + (String, String) self, + SseSerializer serializer, + ); + + @protected + void sse_encode_rust_backup_archive( + RustBackupArchive self, + SseSerializer serializer, + ); + + @protected + void sse_encode_rust_backup_identity( + RustBackupIdentity self, + SseSerializer serializer, + ); + + @protected + void sse_encode_rust_key_manager( + RustKeyManager self, + SseSerializer serializer, + ); @protected void sse_encode_u_32(int self, SseSerializer serializer); @@ -477,6 +640,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_u_8(int self, SseSerializer serializer); + @protected + void sse_encode_u_8_array_32(U8Array32 self, SseSerializer serializer); + @protected void sse_encode_unit(void self, SseSerializer serializer); diff --git a/lib/core/frb_generated.web.dart b/lib/core/frb_generated.web.dart index 8c2f83d1..23c61b25 100644 --- a/lib/core/frb_generated.web.dart +++ b/lib/core/frb_generated.web.dart @@ -8,10 +8,14 @@ import 'bridge.dart'; import 'bridge/callbacks.dart'; +import 'bridge/wrapper/backup.dart'; +import 'bridge/wrapper/key_manager.dart'; import 'bridge/wrapper/user_discovery.dart'; import 'dart:async'; import 'dart:convert'; import 'frb_generated.dart'; +import 'keys/backup_password_keys.dart'; +import 'lib.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_web.dart'; abstract class RustLibApiImplPlatform extends BaseApiImpl { @@ -100,6 +104,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected Object dco_decode_DartOpaque(dynamic raw); + @protected + Map dco_decode_Map_i_64_list_prim_u_8_strict_None( + dynamic raw, + ); + @protected RustStreamSink dco_decode_StreamSink_String_Sse(dynamic raw); @@ -109,17 +118,23 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected AnnouncedUser dco_decode_announced_user(dynamic raw); + @protected + BackupPasswordKeys dco_decode_backup_password_keys(dynamic raw); + @protected bool dco_decode_bool(dynamic raw); @protected AnnouncedUser dco_decode_box_autoadd_announced_user(dynamic raw); + @protected + BackupPasswordKeys dco_decode_box_autoadd_backup_password_keys(dynamic raw); + @protected PlatformInt64 dco_decode_box_autoadd_i_64(dynamic raw); @protected - TwonlyConfig dco_decode_box_autoadd_twonly_config(dynamic raw); + InitConfig dco_decode_box_autoadd_init_config(dynamic raw); @protected FlutterUserDiscovery dco_decode_flutter_user_discovery(dynamic raw); @@ -127,6 +142,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected PlatformInt64 dco_decode_i_64(dynamic raw); + @protected + InitConfig dco_decode_init_config(dynamic raw); + @protected PlatformInt64 dco_decode_isize(dynamic raw); @@ -142,6 +160,13 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected Uint8List dco_decode_list_prim_u_8_strict(dynamic raw); + @protected + List<(PlatformInt64, Uint8List)> + dco_decode_list_record_i_64_list_prim_u_8_strict(dynamic raw); + + @protected + String? dco_decode_opt_String(dynamic raw); + @protected AnnouncedUser? dco_decode_opt_box_autoadd_announced_user(dynamic raw); @@ -161,7 +186,26 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { OtherPromotion dco_decode_other_promotion(dynamic raw); @protected - TwonlyConfig dco_decode_twonly_config(dynamic raw); + (PlatformInt64, Uint8List) dco_decode_record_i_64_list_prim_u_8_strict( + dynamic raw, + ); + + @protected + (Uint8List, PlatformInt64) dco_decode_record_list_prim_u_8_strict_i_64( + dynamic raw, + ); + + @protected + (String, String) dco_decode_record_string_string(dynamic raw); + + @protected + RustBackupArchive dco_decode_rust_backup_archive(dynamic raw); + + @protected + RustBackupIdentity dco_decode_rust_backup_identity(dynamic raw); + + @protected + RustKeyManager dco_decode_rust_key_manager(dynamic raw); @protected int dco_decode_u_32(dynamic raw); @@ -169,6 +213,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected int dco_decode_u_8(dynamic raw); + @protected + U8Array32 dco_decode_u_8_array_32(dynamic raw); + @protected void dco_decode_unit(dynamic raw); @@ -181,6 +228,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected Object sse_decode_DartOpaque(SseDeserializer deserializer); + @protected + Map sse_decode_Map_i_64_list_prim_u_8_strict_None( + SseDeserializer deserializer, + ); + @protected RustStreamSink sse_decode_StreamSink_String_Sse( SseDeserializer deserializer, @@ -192,6 +244,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected AnnouncedUser sse_decode_announced_user(SseDeserializer deserializer); + @protected + BackupPasswordKeys sse_decode_backup_password_keys( + SseDeserializer deserializer, + ); + @protected bool sse_decode_bool(SseDeserializer deserializer); @@ -200,13 +257,16 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); + @protected + BackupPasswordKeys sse_decode_box_autoadd_backup_password_keys( + SseDeserializer deserializer, + ); + @protected PlatformInt64 sse_decode_box_autoadd_i_64(SseDeserializer deserializer); @protected - TwonlyConfig sse_decode_box_autoadd_twonly_config( - SseDeserializer deserializer, - ); + InitConfig sse_decode_box_autoadd_init_config(SseDeserializer deserializer); @protected FlutterUserDiscovery sse_decode_flutter_user_discovery( @@ -216,6 +276,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected PlatformInt64 sse_decode_i_64(SseDeserializer deserializer); + @protected + InitConfig sse_decode_init_config(SseDeserializer deserializer); + @protected PlatformInt64 sse_decode_isize(SseDeserializer deserializer); @@ -235,6 +298,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer); + @protected + List<(PlatformInt64, Uint8List)> + sse_decode_list_record_i_64_list_prim_u_8_strict( + SseDeserializer deserializer, + ); + + @protected + String? sse_decode_opt_String(SseDeserializer deserializer); + @protected AnnouncedUser? sse_decode_opt_box_autoadd_announced_user( SseDeserializer deserializer, @@ -260,7 +332,32 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { OtherPromotion sse_decode_other_promotion(SseDeserializer deserializer); @protected - TwonlyConfig sse_decode_twonly_config(SseDeserializer deserializer); + (PlatformInt64, Uint8List) sse_decode_record_i_64_list_prim_u_8_strict( + SseDeserializer deserializer, + ); + + @protected + (Uint8List, PlatformInt64) sse_decode_record_list_prim_u_8_strict_i_64( + SseDeserializer deserializer, + ); + + @protected + (String, String) sse_decode_record_string_string( + SseDeserializer deserializer, + ); + + @protected + RustBackupArchive sse_decode_rust_backup_archive( + SseDeserializer deserializer, + ); + + @protected + RustBackupIdentity sse_decode_rust_backup_identity( + SseDeserializer deserializer, + ); + + @protected + RustKeyManager sse_decode_rust_key_manager(SseDeserializer deserializer); @protected int sse_decode_u_32(SseDeserializer deserializer); @@ -268,6 +365,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected int sse_decode_u_8(SseDeserializer deserializer); + @protected + U8Array32 sse_decode_u_8_array_32(SseDeserializer deserializer); + @protected void sse_decode_unit(SseDeserializer deserializer); @@ -368,6 +468,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_DartOpaque(Object self, SseSerializer serializer); + @protected + void sse_encode_Map_i_64_list_prim_u_8_strict_None( + Map self, + SseSerializer serializer, + ); + @protected void sse_encode_StreamSink_String_Sse( RustStreamSink self, @@ -380,6 +486,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_announced_user(AnnouncedUser self, SseSerializer serializer); + @protected + void sse_encode_backup_password_keys( + BackupPasswordKeys self, + SseSerializer serializer, + ); + @protected void sse_encode_bool(bool self, SseSerializer serializer); @@ -389,6 +501,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_box_autoadd_backup_password_keys( + BackupPasswordKeys self, + SseSerializer serializer, + ); + @protected void sse_encode_box_autoadd_i_64( PlatformInt64 self, @@ -396,8 +514,8 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { ); @protected - void sse_encode_box_autoadd_twonly_config( - TwonlyConfig self, + void sse_encode_box_autoadd_init_config( + InitConfig self, SseSerializer serializer, ); @@ -410,6 +528,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_i_64(PlatformInt64 self, SseSerializer serializer); + @protected + void sse_encode_init_config(InitConfig self, SseSerializer serializer); + @protected void sse_encode_isize(PlatformInt64 self, SseSerializer serializer); @@ -434,6 +555,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_list_record_i_64_list_prim_u_8_strict( + List<(PlatformInt64, Uint8List)> self, + SseSerializer serializer, + ); + + @protected + void sse_encode_opt_String(String? self, SseSerializer serializer); + @protected void sse_encode_opt_box_autoadd_announced_user( AnnouncedUser? self, @@ -471,7 +601,40 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { ); @protected - void sse_encode_twonly_config(TwonlyConfig self, SseSerializer serializer); + void sse_encode_record_i_64_list_prim_u_8_strict( + (PlatformInt64, Uint8List) self, + SseSerializer serializer, + ); + + @protected + void sse_encode_record_list_prim_u_8_strict_i_64( + (Uint8List, PlatformInt64) self, + SseSerializer serializer, + ); + + @protected + void sse_encode_record_string_string( + (String, String) self, + SseSerializer serializer, + ); + + @protected + void sse_encode_rust_backup_archive( + RustBackupArchive self, + SseSerializer serializer, + ); + + @protected + void sse_encode_rust_backup_identity( + RustBackupIdentity self, + SseSerializer serializer, + ); + + @protected + void sse_encode_rust_key_manager( + RustKeyManager self, + SseSerializer serializer, + ); @protected void sse_encode_u_32(int self, SseSerializer serializer); @@ -479,6 +642,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_u_8(int self, SseSerializer serializer); + @protected + void sse_encode_u_8_array_32(U8Array32 self, SseSerializer serializer); + @protected void sse_encode_unit(void self, SseSerializer serializer); diff --git a/lib/core/keys/backup_password_keys.dart b/lib/core/keys/backup_password_keys.dart new file mode 100644 index 00000000..eacdead5 --- /dev/null +++ b/lib/core/keys/backup_password_keys.dart @@ -0,0 +1,29 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.12.0. + +// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import + +import '../frb_generated.dart'; +import '../lib.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; + +class BackupPasswordKeys { + final U8Array32 backupId; + final U8Array32 encryptionKey; + + const BackupPasswordKeys({ + required this.backupId, + required this.encryptionKey, + }); + + @override + int get hashCode => backupId.hashCode ^ encryptionKey.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is BackupPasswordKeys && + runtimeType == other.runtimeType && + backupId == other.backupId && + encryptionKey == other.encryptionKey; +} diff --git a/lib/core/lib.dart b/lib/core/lib.dart new file mode 100644 index 00000000..a0d0fa1d --- /dev/null +++ b/lib/core/lib.dart @@ -0,0 +1,20 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.12.0. + +// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import + +import 'frb_generated.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; + +class U8Array32 extends NonGrowableListView { + static const arraySize = 32; + + @internal + Uint8List get inner => _inner; + final Uint8List _inner; + + U8Array32(this._inner) : assert(_inner.length == arraySize), super(_inner); + + U8Array32.init() : this(Uint8List(arraySize)); +} diff --git a/lib/globals.dart b/lib/globals.dart index 87b0e975..c4caaec2 100644 --- a/lib/globals.dart +++ b/lib/globals.dart @@ -1,13 +1,13 @@ import 'dart:async'; - import 'package:camera/camera.dart'; -import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; import 'package:twonly/src/utils/log.dart'; class AppEnvironment { - static late final String cacheDir; - static late final String supportDir; + static late String cacheDir; + static late String supportDir; + + static bool _isInitialized = false; static bool _isInitialized = false; @@ -22,10 +22,9 @@ class AppEnvironment { _isInitialized = true; } - static void initTesting() { - if (_isInitialized) return; - cacheDir = '/tmp/twonly_cache'; - supportDir = '/tmp/twonly_support'; + static void initTesting({String? customCacheDir, String? customSupportDir}) { + cacheDir = customCacheDir ?? '/tmp/twonly_cache'; + supportDir = customSupportDir ?? '/tmp/twonly_support'; _isInitialized = true; } } @@ -35,9 +34,5 @@ class AppState { static bool isInBackgroundTask = false; static bool allowErrorTrackingViaSentry = false; static bool gotMessageFromServer = false; - static int latestAppVersionId = 110; -} - -class AppGlobalKeys { - static final scaffoldMessengerKey = GlobalKey(); + static int latestAppVersionId = 113; } diff --git a/lib/main.dart b/lib/main.dart index ddd6f46f..77f5db8d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'dart:io'; - +import 'dart:convert'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:mutex/mutex.dart'; @@ -8,11 +8,16 @@ import 'package:provider/provider.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:twonly/app.dart'; import 'package:twonly/core/bridge.dart' as bridge; +import 'package:twonly/core/bridge/wrapper/key_manager.dart'; import 'package:twonly/core/frb_generated.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/callbacks/callbacks.dart'; +import 'package:twonly/src/constants/secure_storage.keys.dart'; +import 'package:twonly/src/database/signal/signal_signed_pre_key_store.dart' + show getSignalSignedPreKeyStoreOld; import 'package:twonly/src/database/tables/contacts.table.dart'; +import 'package:twonly/src/model/json/signal_identity.model.dart'; import 'package:twonly/src/providers/connection.provider.dart'; import 'package:twonly/src/providers/image_editor.provider.dart'; import 'package:twonly/src/providers/purchases.provider.dart'; @@ -21,7 +26,7 @@ import 'package:twonly/src/services/api/mediafiles/download.api.dart'; import 'package:twonly/src/services/api/mediafiles/media_background.api.dart'; import 'package:twonly/src/services/api/mediafiles/upload.api.dart'; import 'package:twonly/src/services/background/callback_dispatcher.background.dart'; -import 'package:twonly/src/services/backup/create.backup.dart'; +import 'package:twonly/src/services/backup.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/notifications/fcm.notifications.dart'; import 'package:twonly/src/services/notifications/setup.notifications.dart'; @@ -38,9 +43,9 @@ final _initMutex = Mutex(); /// This function is used to initialized the absolute minimum so it /// can also be used by the backend without the UI was loaded. -Future twonlyMinimumInitialization() async { +Future twonlyMinimumInitialization() async { Log.info('twonlyMinimumInitialization: called'); - await exclusiveAccess( + final hasStorageError = await exclusiveAccess( lockName: 'init', mutex: _initMutex, action: () async { @@ -54,15 +59,22 @@ Future twonlyMinimumInitialization() async { await initFlutterCallbacksForRust(); Log.info('twonlyMinimumInitialization: bridge.initializeTwonlyFlutter()'); - await bridge.initializeTwonlyFlutter( - config: bridge.TwonlyConfig( - databasePath: '${AppEnvironment.supportDir}/twonly.sqlite', - dataDirectory: AppEnvironment.supportDir, - ), - ); + try { + await bridge.initializeTwonlyFlutter( + config: bridge.InitConfig( + databaseDir: AppEnvironment.supportDir, + dataDir: AppEnvironment.supportDir, + ), + ); + } catch (e) { + Log.error(e); + return true; + } Log.info('twonlyMinimumInitialization: finished'); + return false; }, ); + return hasStorageError; } void main() async { @@ -72,26 +84,30 @@ void main() async { unawaited(StartupGuard.markAppStartup()); - await twonlyMinimumInitialization(); - - unawaited(initFCMService()); + var storageError = await twonlyMinimumInitialization(); + await initFCMService(); var userExists = false; - var storageError = false; - try { - userExists = await userService.tryInit(); - } catch (e) { - Log.error('Failed to initialize user session due to storage error: $e'); - storageError = true; + var recoveryPossible = false; + + if (!storageError) { + try { + userExists = await userService.tryInit(); + } catch (e) { + Log.error('Failed to initialize user session due to storage error: $e'); + storageError = true; + } } - if (Platform.isIOS && userExists) { - final dbFile = File('${AppEnvironment.supportDir}/twonly.sqlite'); - if (!dbFile.existsSync()) { - Log.error('[twonly] IOS: App was removed and then reinstalled again...'); - await SecureStorage.instance.deleteAll(); - userExists = false; + if (!userExists && !storageError) { + try { + final userId = await RustKeyManager.getUserId(); + if (userId != null) { + recoveryPossible = true; + } + } catch (e) { + Log.error('Could not check KeyManager userId for iOS recovery: $e'); } } @@ -139,7 +155,10 @@ void main() async { ChangeNotifierProvider(create: (_) => ImageEditorProvider()), ChangeNotifierProvider(create: (_) => PurchasesProvider()), ], - child: App(storageError: storageError), + child: App( + storageError: storageError, + recoveryPossible: recoveryPossible, + ), ), ); } @@ -178,6 +197,66 @@ Future runMigrations() async { } }); } + if (userService.currentUser.appVersion < 113) { + var migrationSuccess = true; + final signalIdentity = await SecureStorage.instance.read( + // ignore: deprecated_member_use_from_same_package + key: SecureStorageKeys.signalIdentity, + ); + + if (signalIdentity != null) { + try { + final decoded = jsonDecode(signalIdentity); + final identity = SignalIdentity.fromJson( + decoded as Map, + ); + + await RustKeyManager.importSignalIdentity( + identityKeyPairStructure: identity.identityKeyPairU8List, + registrationId: identity.registrationId, + signedPreKeyStore: await getSignalSignedPreKeyStoreOld(), + ); + Log.info('Importing signal identiy to the rust key manager'); + + // Clean up old keys after successful migration + await SecureStorage.instance.delete( + // ignore: deprecated_member_use_from_same_package + key: SecureStorageKeys.signalIdentity, + ); + await SecureStorage.instance.delete( + // ignore: deprecated_member_use_from_same_package + key: SecureStorageKeys.signalSignedPreKey, + ); + } catch (e) { + Log.error('Failed to migrate signal identity: $e'); + migrationSuccess = false; + } + } + + if (migrationSuccess) { + await UserService.update((u) { + u + ..appVersion = 113 + ..canUseLoginTokenForAuth = false + // As usernames changes where not considered in the old version force users + // to reenter there passwords. + // ignore: deprecated_member_use_from_same_package + ..twonlySafeBackup?.encryptionKey = [] + // ignore: deprecated_member_use_from_same_package + ..twonlySafeBackup?.backupId = []; + }); + } + } + if (kDebugMode) { + assert( + AppState.latestAppVersionId == 113, + 'Forgot to update the target version in runMigrations() after incrementing AppState.latestAppVersionId.', + ); + assert( + AppState.latestAppVersionId == userService.currentUser.appVersion, + "Migration incomplete: currentUser.appVersion (${userService.currentUser.appVersion}) does not match AppState.latestAppVersionId (${AppState.latestAppVersionId}). Ensure the user's appVersion is updated in the migration block.", + ); + } } Future postStartupTasks() async { @@ -185,7 +264,6 @@ Future postStartupTasks() async { // 1. Immediate background cleanup (Non-blocking for UI) await twonlyDB.messagesDao.purgeMessageTable(); unawaited(twonlyDB.receiptsDao.purgeReceivedReceipts()); - unawaited(UserDiscoveryService.removeDeletedContacts()); unawaited(MediaFileService.purgeTempFolder()); // 2. Service initializations @@ -193,25 +271,12 @@ Future postStartupTasks() async { unawaited(finishStartedPreprocessing()); unawaited(createPushAvatars()); - if (userService.currentUser.userDiscoveryInitializationError) { - unawaited(() async { - try { - await UserDiscoveryService.initializeOrUpdate( - threshold: userService.currentUser.userDiscoveryThreshold, - sharePromotion: userService.currentUser.userDiscoverySharePromotion, - ); - } catch (e) { - Log.error( - 'Failed to retry UserDiscovery initialization on startup: $e', - ); - } - }()); - } + unawaited(UserDiscoveryService.verifyInitializationOnStartup()); await Future.delayed(const Duration(seconds: 10)); unawaited(initializeBackgroundTaskManager()); // 3. Delayed tasks (Wait for app to settle) await Future.delayed(const Duration(minutes: 2)); - unawaited(performTwonlySafeBackup()); + unawaited(BackupService.makeBackup()); unawaited(cleanLogFile()); } diff --git a/lib/src/callbacks/user_discovery.callbacks.dart b/lib/src/callbacks/user_discovery.callbacks.dart index 3b72cc66..86bfc4df 100644 --- a/lib/src/callbacks/user_discovery.callbacks.dart +++ b/lib/src/callbacks/user_discovery.callbacks.dart @@ -38,21 +38,29 @@ class UserDiscoveryCallbacks { Uint8List pubKey, Uint8List signature, ) async { - return Curve.verifySignature( - IdentityKey.fromBytes(pubKey, 0).publicKey, - inputData, - signature, - ); + try { + return Curve.verifySignature( + IdentityKey.fromBytes(pubKey, 0).publicKey, + inputData, + signature, + ); + } catch (_) { + return false; + } } static Future verifyStoredPubKey( int contactId, Uint8List pubKey, ) async { - final storedPublicKey = await getPublicKeyFromContact(contactId); - if (storedPublicKey != null) { - return storedPublicKey.equals(pubKey); - } else { + try { + final storedPublicKey = await getPublicKeyFromContact(contactId); + if (storedPublicKey != null) { + return storedPublicKey.equals(pubKey); + } else { + return false; + } + } catch (_) { return false; } } diff --git a/lib/src/constants/keyvalue.keys.dart b/lib/src/constants/keyvalue.keys.dart index 4b5e1cfe..57db9890 100644 --- a/lib/src/constants/keyvalue.keys.dart +++ b/lib/src/constants/keyvalue.keys.dart @@ -1,4 +1,6 @@ class KeyValueKeys { static const String lastPeriodicTaskExecution = 'last_periodic_task_execution'; + static const String currentBackupState = 'current_backup_state'; + static const String backupRecoveryState = 'backup_recovery_state'; } diff --git a/lib/src/constants/routes.keys.dart b/lib/src/constants/routes.keys.dart index 66a1fabb..d6690619 100644 --- a/lib/src/constants/routes.keys.dart +++ b/lib/src/constants/routes.keys.dart @@ -25,7 +25,6 @@ class Routes { static const String settingsAccount = '/settings/account'; static const String settingsSubscription = '/settings/subscription'; static const String settingsBackup = '/settings/backup'; - static const String settingsBackupServer = '/settings/backup/server'; static const String settingsBackupRecovery = '/settings/backup/recovery'; static const String settingsBackupSetup = '/settings/backup/setup'; static const String settingsAppearance = '/settings/appearance'; diff --git a/lib/src/constants/secure_storage.keys.dart b/lib/src/constants/secure_storage.keys.dart index 43da0690..37aebba0 100644 --- a/lib/src/constants/secure_storage.keys.dart +++ b/lib/src/constants/secure_storage.keys.dart @@ -1,11 +1,15 @@ class SecureStorageKeys { + @Deprecated('Use the secure storage in rust') static const String signalIdentity = 'signal_identity'; + @Deprecated('Use the secure storage in rust') static const String signalSignedPreKey = 'signed_pre_key_store'; + @Deprecated('Use the login token') static const String apiAuthToken = 'api_auth_token'; - static const String googleFcm = 'google_fcm'; - static const String userData = 'userData'; - static const String twonlySafeLastBackupHash = 'twonly_safe_last_backup_hash'; + @Deprecated('Use user.json file') + static const String userData = 'userData'; + + // Not required for backup... static const String receivingPushKeys = 'push_keys_receiving'; static const String sendingPushKeys = 'push_keys_sending'; } diff --git a/lib/src/database/daos/groups.dao.dart b/lib/src/database/daos/groups.dao.dart index 28fe0883..a9c206e7 100644 --- a/lib/src/database/daos/groups.dao.dart +++ b/lib/src/database/daos/groups.dao.dart @@ -139,15 +139,10 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { } Future _insertGroup(GroupsCompanion group) async { - try { - await into(groups).insert(group); - return await (select( - groups, - )..where((t) => t.groupId.equals(group.groupId.value))).getSingle(); - } catch (e) { - Log.error('Could not insert group: $e'); - return null; - } + await into(groups).insertOnConflictUpdate(group); + return (select( + groups, + )..where((t) => t.groupId.equals(group.groupId.value))).getSingleOrNull(); } Future> getGroupContact(String groupId) async { @@ -277,7 +272,7 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { groups.groupId.equalsExp(groupMembers.groupId), ), ], - )..where(groups.isDirectChat.isNull())); + )..where(groups.isDirectChat.equals(false))); return query.map((row) => row.readTable(groupMembers)).get(); } catch (e) { Log.error(e); diff --git a/lib/src/database/daos/key_verification.dao.dart b/lib/src/database/daos/key_verification.dao.dart index cfcbaf84..1664a3c1 100644 --- a/lib/src/database/daos/key_verification.dao.dart +++ b/lib/src/database/daos/key_verification.dao.dart @@ -6,6 +6,7 @@ import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/database/tables/groups.table.dart'; import 'package:twonly/src/database/tables/user_discovery.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/utils/log.dart'; part 'key_verification.dao.g.dart'; @@ -82,17 +83,19 @@ class KeyVerificationDao extends DatabaseAccessor final query = (select(contacts)..where((u) => u.userId.equals(contactId).not())).join( - [ - innerJoin( - ur, - ur.fromContactId.equalsExp(contacts.userId), - ), - innerJoin(kv, kv.contactId.equalsExp(ur.fromContactId)), - ], - )..where( - ur.announcedUserId.equals(contactId) & - ur.publicKeyVerifiedTimestamp.isNotNull(), - ); + [ + innerJoin( + ur, + ur.fromContactId.equalsExp(contacts.userId), + ), + innerJoin(kv, kv.contactId.equalsExp(ur.fromContactId)), + ], + ) + ..where( + ur.announcedUserId.equals(contactId) & + ur.publicKeyVerifiedTimestamp.isNotNull(), + ) + ..groupBy([contacts.userId]); return query.watch().map((rows) { return rows.map((row) { @@ -116,7 +119,8 @@ class KeyVerificationDao extends DatabaseAccessor ..where( ur.publicKeyVerifiedTimestamp.isNotNull() & ur.announcedUserId.equalsExp(ur.fromContactId).not(), - ); + ) + ..groupBy([ur.announcedUserId]); final rows = await query.get(); return rows.length; @@ -173,17 +177,21 @@ class KeyVerificationDao extends DatabaseAccessor } Future addKeyVerification(int contactId, VerificationType type) async { - await into(keyVerifications).insertOnConflictUpdate( - KeyVerificationsCompanion( - contactId: Value(contactId), - type: Value(type), - ), - ); - if (userService.currentUser.isUserDiscoveryEnabled) { - await FlutterUserDiscovery.updateVerificationStateForUser( - contactId: contactId, - publicKeyVerifiedTimestamp: clock.now().millisecondsSinceEpoch, + try { + await into(keyVerifications).insertOnConflictUpdate( + KeyVerificationsCompanion( + contactId: Value(contactId), + type: Value(type), + ), ); + if (userService.currentUser.isUserDiscoveryEnabled) { + await FlutterUserDiscovery.updateVerificationStateForUser( + contactId: contactId, + publicKeyVerifiedTimestamp: clock.now().millisecondsSinceEpoch, + ); + } + } catch (e) { + Log.error(e); } } } diff --git a/lib/src/database/daos/shortcuts.dao.dart b/lib/src/database/daos/shortcuts.dao.dart new file mode 100644 index 00000000..66938956 --- /dev/null +++ b/lib/src/database/daos/shortcuts.dao.dart @@ -0,0 +1,79 @@ +import 'package:drift/drift.dart'; +import 'package:twonly/src/database/tables/shortcuts.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; + +part 'shortcuts.dao.g.dart'; + +@DriftAccessor( + tables: [ + Shortcuts, + ShortcutMembers, + ], +) +class ShortcutsDao extends DatabaseAccessor with _$ShortcutsDaoMixin { + ShortcutsDao(super.db); + + Stream> watchAllShortcuts() { + return select(shortcuts).watch(); + } + + Future getShortcutByEmoji(String emoji) { + return (select( + shortcuts, + )..where((t) => t.emoji.equals(emoji))).getSingleOrNull(); + } + + Future createShortcut(String emoji) async { + try { + await into(shortcuts).insert( + ShortcutsCompanion.insert(emoji: emoji), + ); + // ignore: empty_catches + } catch (e) {} + } + + Future addShortcutMembers(int shortcutId, List groupIds) async { + await batch((b) { + b.insertAll( + shortcutMembers, + groupIds.map( + (gId) => ShortcutMembersCompanion.insert( + shortcutId: shortcutId, + groupId: gId, + ), + ), + ); + }); + } + + Future> getShortcutMembers(int shortcutId) { + return (select( + shortcutMembers, + )..where((t) => t.shortcutId.equals(shortcutId))).get(); + } + + Future incrementUsage(int shortcutId) async { + await customStatement( + 'UPDATE shortcuts SET usage_counter = usage_counter + 1 WHERE id = ?', + [shortcutId], + ); + // Notify updates to trigger streams + notifyUpdates({TableUpdate.onTable(shortcuts, kind: UpdateKind.update)}); + } + + Future updateShortcut(int shortcutId, String emoji) async { + await (update(shortcuts)..where((t) => t.id.equals(shortcutId))).write( + ShortcutsCompanion(emoji: Value(emoji)), + ); + } + + Future deleteShortcutMembers(int shortcutId) async { + await (delete( + shortcutMembers, + )..where((t) => t.shortcutId.equals(shortcutId))).go(); + } + + Future deleteShortcut(int shortcutId) async { + await (delete(shortcuts)..where((t) => t.id.equals(shortcutId))).go(); + } +} diff --git a/lib/src/database/daos/shortcuts.dao.g.dart b/lib/src/database/daos/shortcuts.dao.g.dart new file mode 100644 index 00000000..36e0a8d5 --- /dev/null +++ b/lib/src/database/daos/shortcuts.dao.g.dart @@ -0,0 +1,25 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'shortcuts.dao.dart'; + +// ignore_for_file: type=lint +mixin _$ShortcutsDaoMixin on DatabaseAccessor { + $ShortcutsTable get shortcuts => attachedDatabase.shortcuts; + $GroupsTable get groups => attachedDatabase.groups; + $ShortcutMembersTable get shortcutMembers => attachedDatabase.shortcutMembers; + ShortcutsDaoManager get managers => ShortcutsDaoManager(this); +} + +class ShortcutsDaoManager { + final _$ShortcutsDaoMixin _db; + ShortcutsDaoManager(this._db); + $$ShortcutsTableTableManager get shortcuts => + $$ShortcutsTableTableManager(_db.attachedDatabase, _db.shortcuts); + $$GroupsTableTableManager get groups => + $$GroupsTableTableManager(_db.attachedDatabase, _db.groups); + $$ShortcutMembersTableTableManager get shortcutMembers => + $$ShortcutMembersTableTableManager( + _db.attachedDatabase, + _db.shortcutMembers, + ); +} diff --git a/lib/src/database/schemas/twonly_db/drift_schema_v13.json b/lib/src/database/schemas/twonly_db/drift_schema_v13.json new file mode 100644 index 00000000..c2309348 --- /dev/null +++ b/lib/src/database/schemas/twonly_db/drift_schema_v13.json @@ -0,0 +1,2901 @@ +{ + "_meta": { + "description": "This file contains a serialized version of schema entities for drift.", + "version": "1.3.0" + }, + "options": { + "store_date_time_values_as_text": false + }, + "entities": [ + { + "id": 0, + "references": [], + "type": "table", + "data": { + "name": "contacts", + "was_declared_in_moor": false, + "columns": [ + { + "name": "user_id", + "getter_name": "userId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "username", + "getter_name": "username", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "display_name", + "getter_name": "displayName", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "nick_name", + "getter_name": "nickName", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "avatar_svg_compressed", + "getter_name": "avatarSvgCompressed", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "sender_profile_counter", + "getter_name": "senderProfileCounter", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "accepted", + "getter_name": "accepted", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"accepted\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"accepted\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "deleted_by_user", + "getter_name": "deletedByUser", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"deleted_by_user\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"deleted_by_user\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "requested", + "getter_name": "requested", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"requested\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"requested\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "blocked", + "getter_name": "blocked", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"blocked\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"blocked\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "verified", + "getter_name": "verified", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"verified\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"verified\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "account_deleted", + "getter_name": "accountDeleted", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"account_deleted\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"account_deleted\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "user_discovery_version", + "getter_name": "userDiscoveryVersion", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "user_discovery_excluded", + "getter_name": "userDiscoveryExcluded", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"user_discovery_excluded\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"user_discovery_excluded\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "user_discovery_manual_approved", + "getter_name": "userDiscoveryManualApproved", + "moor_type": "bool", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "CHECK (\"user_discovery_manual_approved\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"user_discovery_manual_approved\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "media_send_counter", + "getter_name": "mediaSendCounter", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "media_received_counter", + "getter_name": "mediaReceivedCounter", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "user_id" + ] + } + }, + { + "id": 1, + "references": [], + "type": "table", + "data": { + "name": "groups", + "was_declared_in_moor": false, + "columns": [ + { + "name": "group_id", + "getter_name": "groupId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_group_admin", + "getter_name": "isGroupAdmin", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_group_admin\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_group_admin\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_direct_chat", + "getter_name": "isDirectChat", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_direct_chat\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_direct_chat\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "pinned", + "getter_name": "pinned", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"pinned\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"pinned\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "archived", + "getter_name": "archived", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"archived\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"archived\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "joined_group", + "getter_name": "joinedGroup", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"joined_group\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"joined_group\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "left_group", + "getter_name": "leftGroup", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"left_group\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"left_group\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "deleted_content", + "getter_name": "deletedContent", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"deleted_content\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"deleted_content\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "state_version_id", + "getter_name": "stateVersionId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "state_encryption_key", + "getter_name": "stateEncryptionKey", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "my_group_private_key", + "getter_name": "myGroupPrivateKey", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "group_name", + "getter_name": "groupName", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "draft_message", + "getter_name": "draftMessage", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "total_media_counter", + "getter_name": "totalMediaCounter", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "also_best_friend", + "getter_name": "alsoBestFriend", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"also_best_friend\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"also_best_friend\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "delete_messages_after_milliseconds", + "getter_name": "deleteMessagesAfterMilliseconds", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('86400000')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_message_send", + "getter_name": "lastMessageSend", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_message_received", + "getter_name": "lastMessageReceived", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_flame_counter_change", + "getter_name": "lastFlameCounterChange", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_flame_sync", + "getter_name": "lastFlameSync", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "flame_counter", + "getter_name": "flameCounter", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "max_flame_counter", + "getter_name": "maxFlameCounter", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "max_flame_counter_from", + "getter_name": "maxFlameCounterFrom", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_message_exchange", + "getter_name": "lastMessageExchange", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "group_id" + ] + } + }, + { + "id": 2, + "references": [], + "type": "table", + "data": { + "name": "media_files", + "was_declared_in_moor": false, + "columns": [ + { + "name": "media_id", + "getter_name": "mediaId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "type", + "getter_name": "type", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(MediaType.values)", + "dart_type_name": "MediaType" + } + }, + { + "name": "upload_state", + "getter_name": "uploadState", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(UploadState.values)", + "dart_type_name": "UploadState" + } + }, + { + "name": "download_state", + "getter_name": "downloadState", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(DownloadState.values)", + "dart_type_name": "DownloadState" + } + }, + { + "name": "requires_authentication", + "getter_name": "requiresAuthentication", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"requires_authentication\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"requires_authentication\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "stored", + "getter_name": "stored", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"stored\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"stored\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_draft_media", + "getter_name": "isDraftMedia", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_draft_media\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_draft_media\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "pre_progressing_process", + "getter_name": "preProgressingProcess", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "reupload_requested_by", + "getter_name": "reuploadRequestedBy", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "IntListTypeConverter()", + "dart_type_name": "List" + } + }, + { + "name": "display_limit_in_milliseconds", + "getter_name": "displayLimitInMilliseconds", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "remove_audio", + "getter_name": "removeAudio", + "moor_type": "bool", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "CHECK (\"remove_audio\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"remove_audio\" IN (0, 1))" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "download_token", + "getter_name": "downloadToken", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "encryption_key", + "getter_name": "encryptionKey", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "encryption_mac", + "getter_name": "encryptionMac", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "encryption_nonce", + "getter_name": "encryptionNonce", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "stored_file_hash", + "getter_name": "storedFileHash", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "media_id" + ] + } + }, + { + "id": 3, + "references": [ + 1, + 0, + 2 + ], + "type": "table", + "data": { + "name": "messages", + "was_declared_in_moor": false, + "columns": [ + { + "name": "group_id", + "getter_name": "groupId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES \"groups\" (group_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES \"groups\" (group_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "groups", + "column": "group_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "message_id", + "getter_name": "messageId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "sender_id", + "getter_name": "senderId", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "REFERENCES contacts (user_id)", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES contacts (user_id)" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "contacts", + "column": "user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": null + } + } + ] + }, + { + "name": "type", + "getter_name": "type", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "content", + "getter_name": "content", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "media_id", + "getter_name": "mediaId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "REFERENCES media_files (media_id) ON DELETE SET NULL", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES media_files (media_id) ON DELETE SET NULL" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "media_files", + "column": "media_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "setNull" + } + } + ] + }, + { + "name": "additional_message_data", + "getter_name": "additionalMessageData", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "media_stored", + "getter_name": "mediaStored", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"media_stored\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"media_stored\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "media_reopened", + "getter_name": "mediaReopened", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"media_reopened\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"media_reopened\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "download_token", + "getter_name": "downloadToken", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "quotes_message_id", + "getter_name": "quotesMessageId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_deleted_from_sender", + "getter_name": "isDeletedFromSender", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_deleted_from_sender\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_deleted_from_sender\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "opened_at", + "getter_name": "openedAt", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "opened_by_all", + "getter_name": "openedByAll", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "modified_at", + "getter_name": "modifiedAt", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "ack_by_user", + "getter_name": "ackByUser", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "ack_by_server", + "getter_name": "ackByServer", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "message_id" + ] + } + }, + { + "id": 4, + "references": [ + 3, + 0 + ], + "type": "table", + "data": { + "name": "message_histories", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "PRIMARY KEY AUTOINCREMENT", + "dialectAwareDefaultConstraints": { + "sqlite": "PRIMARY KEY AUTOINCREMENT" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "auto-increment" + ] + }, + { + "name": "message_id", + "getter_name": "messageId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES messages (message_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES messages (message_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "messages", + "column": "message_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "contact_id", + "getter_name": "contactId", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "REFERENCES contacts (user_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES contacts (user_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "contacts", + "column": "user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "content", + "getter_name": "content", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 5, + "references": [ + 3, + 0 + ], + "type": "table", + "data": { + "name": "reactions", + "was_declared_in_moor": false, + "columns": [ + { + "name": "message_id", + "getter_name": "messageId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES messages (message_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES messages (message_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "messages", + "column": "message_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "emoji", + "getter_name": "emoji", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "sender_id", + "getter_name": "senderId", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "REFERENCES contacts (user_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES contacts (user_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "contacts", + "column": "user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "message_id", + "sender_id", + "emoji" + ] + } + }, + { + "id": 6, + "references": [ + 1, + 0 + ], + "type": "table", + "data": { + "name": "group_members", + "was_declared_in_moor": false, + "columns": [ + { + "name": "group_id", + "getter_name": "groupId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES \"groups\" (group_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES \"groups\" (group_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "groups", + "column": "group_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "contact_id", + "getter_name": "contactId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES contacts (user_id)", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES contacts (user_id)" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "contacts", + "column": "user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": null + } + } + ] + }, + { + "name": "member_state", + "getter_name": "memberState", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(MemberState.values)", + "dart_type_name": "MemberState" + } + }, + { + "name": "group_public_key", + "getter_name": "groupPublicKey", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_chat_opened", + "getter_name": "lastChatOpened", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_type_indicator", + "getter_name": "lastTypeIndicator", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_message", + "getter_name": "lastMessage", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "group_id", + "contact_id" + ] + } + }, + { + "id": 7, + "references": [ + 0, + 3 + ], + "type": "table", + "data": { + "name": "receipts", + "was_declared_in_moor": false, + "columns": [ + { + "name": "receipt_id", + "getter_name": "receiptId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "contact_id", + "getter_name": "contactId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES contacts (user_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES contacts (user_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "contacts", + "column": "user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "message_id", + "getter_name": "messageId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "REFERENCES messages (message_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES messages (message_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "messages", + "column": "message_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "message", + "getter_name": "message", + "moor_type": "blob", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "contact_will_sends_receipt", + "getter_name": "contactWillSendsReceipt", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"contact_will_sends_receipt\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"contact_will_sends_receipt\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "will_be_retried_by_media_upload", + "getter_name": "willBeRetriedByMediaUpload", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"will_be_retried_by_media_upload\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"will_be_retried_by_media_upload\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "mark_for_retry", + "getter_name": "markForRetry", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "mark_for_retry_after_accepted", + "getter_name": "markForRetryAfterAccepted", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "ack_by_server_at", + "getter_name": "ackByServerAt", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "retry_count", + "getter_name": "retryCount", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_retry", + "getter_name": "lastRetry", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "receipt_id" + ] + } + }, + { + "id": 8, + "references": [], + "type": "table", + "data": { + "name": "received_receipts", + "was_declared_in_moor": false, + "columns": [ + { + "name": "receipt_id", + "getter_name": "receiptId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "receipt_id" + ] + } + }, + { + "id": 9, + "references": [], + "type": "table", + "data": { + "name": "signal_identity_key_stores", + "was_declared_in_moor": false, + "columns": [ + { + "name": "device_id", + "getter_name": "deviceId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "name", + "getter_name": "name", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "identity_key", + "getter_name": "identityKey", + "moor_type": "blob", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "device_id", + "name" + ] + } + }, + { + "id": 10, + "references": [], + "type": "table", + "data": { + "name": "signal_pre_key_stores", + "was_declared_in_moor": false, + "columns": [ + { + "name": "pre_key_id", + "getter_name": "preKeyId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "pre_key", + "getter_name": "preKey", + "moor_type": "blob", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "pre_key_id" + ] + } + }, + { + "id": 11, + "references": [], + "type": "table", + "data": { + "name": "signal_sender_key_stores", + "was_declared_in_moor": false, + "columns": [ + { + "name": "sender_key_name", + "getter_name": "senderKeyName", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "sender_key", + "getter_name": "senderKey", + "moor_type": "blob", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "sender_key_name" + ] + } + }, + { + "id": 12, + "references": [], + "type": "table", + "data": { + "name": "signal_session_stores", + "was_declared_in_moor": false, + "columns": [ + { + "name": "device_id", + "getter_name": "deviceId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "name", + "getter_name": "name", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "session_record", + "getter_name": "sessionRecord", + "moor_type": "blob", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "device_id", + "name" + ] + } + }, + { + "id": 13, + "references": [ + 3, + 0 + ], + "type": "table", + "data": { + "name": "message_actions", + "was_declared_in_moor": false, + "columns": [ + { + "name": "message_id", + "getter_name": "messageId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES messages (message_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES messages (message_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "messages", + "column": "message_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "contact_id", + "getter_name": "contactId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES contacts (user_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES contacts (user_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "contacts", + "column": "user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "type", + "getter_name": "type", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(MessageActionType.values)", + "dart_type_name": "MessageActionType" + } + }, + { + "name": "action_at", + "getter_name": "actionAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "message_id", + "contact_id", + "type" + ] + } + }, + { + "id": 14, + "references": [ + 1, + 0 + ], + "type": "table", + "data": { + "name": "group_histories", + "was_declared_in_moor": false, + "columns": [ + { + "name": "group_history_id", + "getter_name": "groupHistoryId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "group_id", + "getter_name": "groupId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES \"groups\" (group_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES \"groups\" (group_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "groups", + "column": "group_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "contact_id", + "getter_name": "contactId", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "REFERENCES contacts (user_id)", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES contacts (user_id)" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "contacts", + "column": "user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": null + } + } + ] + }, + { + "name": "affected_contact_id", + "getter_name": "affectedContactId", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "old_group_name", + "getter_name": "oldGroupName", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "new_group_name", + "getter_name": "newGroupName", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "new_delete_messages_after_milliseconds", + "getter_name": "newDeleteMessagesAfterMilliseconds", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "type", + "getter_name": "type", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(GroupActionType.values)", + "dart_type_name": "GroupActionType" + } + }, + { + "name": "action_at", + "getter_name": "actionAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "group_history_id" + ] + } + }, + { + "id": 15, + "references": [ + 0 + ], + "type": "table", + "data": { + "name": "key_verifications", + "was_declared_in_moor": false, + "columns": [ + { + "name": "verification_id", + "getter_name": "verificationId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "PRIMARY KEY AUTOINCREMENT", + "dialectAwareDefaultConstraints": { + "sqlite": "PRIMARY KEY AUTOINCREMENT" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "auto-increment" + ] + }, + { + "name": "contact_id", + "getter_name": "contactId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES contacts (user_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES contacts (user_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "contacts", + "column": "user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "type", + "getter_name": "type", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(VerificationType.values)", + "dart_type_name": "VerificationType" + } + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 16, + "references": [], + "type": "table", + "data": { + "name": "verification_tokens", + "was_declared_in_moor": false, + "columns": [ + { + "name": "token_id", + "getter_name": "tokenId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "PRIMARY KEY AUTOINCREMENT", + "dialectAwareDefaultConstraints": { + "sqlite": "PRIMARY KEY AUTOINCREMENT" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "auto-increment" + ] + }, + { + "name": "token", + "getter_name": "token", + "moor_type": "blob", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 17, + "references": [], + "type": "table", + "data": { + "name": "user_discovery_announced_users", + "was_declared_in_moor": false, + "columns": [ + { + "name": "announced_user_id", + "getter_name": "announcedUserId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "announced_public_key", + "getter_name": "announcedPublicKey", + "moor_type": "blob", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "public_id", + "getter_name": "publicId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "UNIQUE", + "dialectAwareDefaultConstraints": { + "sqlite": "UNIQUE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "unique" + ] + }, + { + "name": "username", + "getter_name": "username", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "was_shown_to_the_user", + "getter_name": "wasShownToTheUser", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"was_shown_to_the_user\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"was_shown_to_the_user\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_hidden", + "getter_name": "isHidden", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_hidden\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_hidden\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "announced_user_id" + ] + } + }, + { + "id": 18, + "references": [ + 17, + 0 + ], + "type": "table", + "data": { + "name": "user_discovery_user_relations", + "was_declared_in_moor": false, + "columns": [ + { + "name": "announced_user_id", + "getter_name": "announcedUserId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES user_discovery_announced_users (announced_user_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES user_discovery_announced_users (announced_user_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "user_discovery_announced_users", + "column": "announced_user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "from_contact_id", + "getter_name": "fromContactId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES contacts (user_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES contacts (user_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "contacts", + "column": "user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "public_key_verified_timestamp", + "getter_name": "publicKeyVerifiedTimestamp", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "announced_user_id", + "from_contact_id" + ] + } + }, + { + "id": 19, + "references": [ + 0 + ], + "type": "table", + "data": { + "name": "user_discovery_other_promotions", + "was_declared_in_moor": false, + "columns": [ + { + "name": "from_contact_id", + "getter_name": "fromContactId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES contacts (user_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES contacts (user_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "contacts", + "column": "user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "promotion_id", + "getter_name": "promotionId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "public_id", + "getter_name": "publicId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "threshold", + "getter_name": "threshold", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "announcement_share", + "getter_name": "announcementShare", + "moor_type": "blob", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "public_key_verified_timestamp", + "getter_name": "publicKeyVerifiedTimestamp", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "from_contact_id", + "public_id" + ] + } + }, + { + "id": 20, + "references": [ + 0 + ], + "type": "table", + "data": { + "name": "user_discovery_own_promotions", + "was_declared_in_moor": false, + "columns": [ + { + "name": "version_id", + "getter_name": "versionId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "PRIMARY KEY AUTOINCREMENT", + "dialectAwareDefaultConstraints": { + "sqlite": "PRIMARY KEY AUTOINCREMENT" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "auto-increment" + ] + }, + { + "name": "contact_id", + "getter_name": "contactId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES contacts (user_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES contacts (user_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "contacts", + "column": "user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "promotion", + "getter_name": "promotion", + "moor_type": "blob", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 21, + "references": [ + 0 + ], + "type": "table", + "data": { + "name": "user_discovery_shares", + "was_declared_in_moor": false, + "columns": [ + { + "name": "share_id", + "getter_name": "shareId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "PRIMARY KEY AUTOINCREMENT", + "dialectAwareDefaultConstraints": { + "sqlite": "PRIMARY KEY AUTOINCREMENT" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "auto-increment" + ] + }, + { + "name": "share", + "getter_name": "share", + "moor_type": "blob", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "contact_id", + "getter_name": "contactId", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "REFERENCES contacts (user_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES contacts (user_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "contacts", + "column": "user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 22, + "references": [], + "type": "table", + "data": { + "name": "shortcuts", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "PRIMARY KEY AUTOINCREMENT", + "dialectAwareDefaultConstraints": { + "sqlite": "PRIMARY KEY AUTOINCREMENT" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "auto-increment" + ] + }, + { + "name": "emoji", + "getter_name": "emoji", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "UNIQUE", + "dialectAwareDefaultConstraints": { + "sqlite": "UNIQUE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "unique" + ] + }, + { + "name": "usage_counter", + "getter_name": "usageCounter", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 23, + "references": [ + 22, + 1 + ], + "type": "table", + "data": { + "name": "shortcut_members", + "was_declared_in_moor": false, + "columns": [ + { + "name": "shortcut_id", + "getter_name": "shortcutId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES shortcuts (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES shortcuts (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "shortcuts", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "group_id", + "getter_name": "groupId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES \"groups\" (group_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES \"groups\" (group_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "groups", + "column": "group_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "shortcut_id", + "group_id" + ] + } + } + ], + "fixed_sql": [ + { + "name": "contacts", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"contacts\" (\"user_id\" INTEGER NOT NULL, \"username\" TEXT NOT NULL, \"display_name\" TEXT NULL, \"nick_name\" TEXT NULL, \"avatar_svg_compressed\" BLOB NULL, \"sender_profile_counter\" INTEGER NOT NULL DEFAULT 0, \"accepted\" INTEGER NOT NULL DEFAULT 0 CHECK (\"accepted\" IN (0, 1)), \"deleted_by_user\" INTEGER NOT NULL DEFAULT 0 CHECK (\"deleted_by_user\" IN (0, 1)), \"requested\" INTEGER NOT NULL DEFAULT 0 CHECK (\"requested\" IN (0, 1)), \"blocked\" INTEGER NOT NULL DEFAULT 0 CHECK (\"blocked\" IN (0, 1)), \"verified\" INTEGER NOT NULL DEFAULT 0 CHECK (\"verified\" IN (0, 1)), \"account_deleted\" INTEGER NOT NULL DEFAULT 0 CHECK (\"account_deleted\" IN (0, 1)), \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), \"user_discovery_version\" BLOB NULL, \"user_discovery_excluded\" INTEGER NOT NULL DEFAULT 0 CHECK (\"user_discovery_excluded\" IN (0, 1)), \"user_discovery_manual_approved\" INTEGER NULL DEFAULT 0 CHECK (\"user_discovery_manual_approved\" IN (0, 1)), \"media_send_counter\" INTEGER NOT NULL DEFAULT 0, \"media_received_counter\" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (\"user_id\"));" + } + ] + }, + { + "name": "groups", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"groups\" (\"group_id\" TEXT NOT NULL, \"is_group_admin\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_group_admin\" IN (0, 1)), \"is_direct_chat\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_direct_chat\" IN (0, 1)), \"pinned\" INTEGER NOT NULL DEFAULT 0 CHECK (\"pinned\" IN (0, 1)), \"archived\" INTEGER NOT NULL DEFAULT 0 CHECK (\"archived\" IN (0, 1)), \"joined_group\" INTEGER NOT NULL DEFAULT 0 CHECK (\"joined_group\" IN (0, 1)), \"left_group\" INTEGER NOT NULL DEFAULT 0 CHECK (\"left_group\" IN (0, 1)), \"deleted_content\" INTEGER NOT NULL DEFAULT 0 CHECK (\"deleted_content\" IN (0, 1)), \"state_version_id\" INTEGER NOT NULL DEFAULT 0, \"state_encryption_key\" BLOB NULL, \"my_group_private_key\" BLOB NULL, \"group_name\" TEXT NOT NULL, \"draft_message\" TEXT NULL, \"total_media_counter\" INTEGER NOT NULL DEFAULT 0, \"also_best_friend\" INTEGER NOT NULL DEFAULT 0 CHECK (\"also_best_friend\" IN (0, 1)), \"delete_messages_after_milliseconds\" INTEGER NOT NULL DEFAULT 86400000, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), \"last_message_send\" INTEGER NULL, \"last_message_received\" INTEGER NULL, \"last_flame_counter_change\" INTEGER NULL, \"last_flame_sync\" INTEGER NULL, \"flame_counter\" INTEGER NOT NULL DEFAULT 0, \"max_flame_counter\" INTEGER NOT NULL DEFAULT 0, \"max_flame_counter_from\" INTEGER NULL, \"last_message_exchange\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), PRIMARY KEY (\"group_id\"));" + } + ] + }, + { + "name": "media_files", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"media_files\" (\"media_id\" TEXT NOT NULL, \"type\" TEXT NOT NULL, \"upload_state\" TEXT NULL, \"download_state\" TEXT NULL, \"requires_authentication\" INTEGER NOT NULL DEFAULT 0 CHECK (\"requires_authentication\" IN (0, 1)), \"stored\" INTEGER NOT NULL DEFAULT 0 CHECK (\"stored\" IN (0, 1)), \"is_draft_media\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_draft_media\" IN (0, 1)), \"pre_progressing_process\" INTEGER NULL, \"reupload_requested_by\" TEXT NULL, \"display_limit_in_milliseconds\" INTEGER NULL, \"remove_audio\" INTEGER NULL CHECK (\"remove_audio\" IN (0, 1)), \"download_token\" BLOB NULL, \"encryption_key\" BLOB NULL, \"encryption_mac\" BLOB NULL, \"encryption_nonce\" BLOB NULL, \"stored_file_hash\" BLOB NULL, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), PRIMARY KEY (\"media_id\"));" + } + ] + }, + { + "name": "messages", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"messages\" (\"group_id\" TEXT NOT NULL REFERENCES \"groups\" (group_id) ON DELETE CASCADE, \"message_id\" TEXT NOT NULL, \"sender_id\" INTEGER NULL REFERENCES contacts (user_id), \"type\" TEXT NOT NULL, \"content\" TEXT NULL, \"media_id\" TEXT NULL REFERENCES media_files (media_id) ON DELETE SET NULL, \"additional_message_data\" BLOB NULL, \"media_stored\" INTEGER NOT NULL DEFAULT 0 CHECK (\"media_stored\" IN (0, 1)), \"media_reopened\" INTEGER NOT NULL DEFAULT 0 CHECK (\"media_reopened\" IN (0, 1)), \"download_token\" BLOB NULL, \"quotes_message_id\" TEXT NULL, \"is_deleted_from_sender\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_deleted_from_sender\" IN (0, 1)), \"opened_at\" INTEGER NULL, \"opened_by_all\" INTEGER NULL, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), \"modified_at\" INTEGER NULL, \"ack_by_user\" INTEGER NULL, \"ack_by_server\" INTEGER NULL, PRIMARY KEY (\"message_id\"));" + } + ] + }, + { + "name": "message_histories", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"message_histories\" (\"id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, \"message_id\" TEXT NOT NULL REFERENCES messages (message_id) ON DELETE CASCADE, \"contact_id\" INTEGER NULL REFERENCES contacts (user_id) ON DELETE CASCADE, \"content\" TEXT NULL, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)));" + } + ] + }, + { + "name": "reactions", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"reactions\" (\"message_id\" TEXT NOT NULL REFERENCES messages (message_id) ON DELETE CASCADE, \"emoji\" TEXT NOT NULL, \"sender_id\" INTEGER NULL REFERENCES contacts (user_id) ON DELETE CASCADE, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), PRIMARY KEY (\"message_id\", \"sender_id\", \"emoji\"));" + } + ] + }, + { + "name": "group_members", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"group_members\" (\"group_id\" TEXT NOT NULL REFERENCES \"groups\" (group_id) ON DELETE CASCADE, \"contact_id\" INTEGER NOT NULL REFERENCES contacts (user_id), \"member_state\" TEXT NULL, \"group_public_key\" BLOB NULL, \"last_chat_opened\" INTEGER NULL, \"last_type_indicator\" INTEGER NULL, \"last_message\" INTEGER NULL, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), PRIMARY KEY (\"group_id\", \"contact_id\"));" + } + ] + }, + { + "name": "receipts", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"receipts\" (\"receipt_id\" TEXT NOT NULL, \"contact_id\" INTEGER NOT NULL REFERENCES contacts (user_id) ON DELETE CASCADE, \"message_id\" TEXT NULL REFERENCES messages (message_id) ON DELETE CASCADE, \"message\" BLOB NOT NULL, \"contact_will_sends_receipt\" INTEGER NOT NULL DEFAULT 1 CHECK (\"contact_will_sends_receipt\" IN (0, 1)), \"will_be_retried_by_media_upload\" INTEGER NOT NULL DEFAULT 0 CHECK (\"will_be_retried_by_media_upload\" IN (0, 1)), \"mark_for_retry\" INTEGER NULL, \"mark_for_retry_after_accepted\" INTEGER NULL, \"ack_by_server_at\" INTEGER NULL, \"retry_count\" INTEGER NOT NULL DEFAULT 0, \"last_retry\" INTEGER NULL, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), PRIMARY KEY (\"receipt_id\"));" + } + ] + }, + { + "name": "received_receipts", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"received_receipts\" (\"receipt_id\" TEXT NOT NULL, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), PRIMARY KEY (\"receipt_id\"));" + } + ] + }, + { + "name": "signal_identity_key_stores", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"signal_identity_key_stores\" (\"device_id\" INTEGER NOT NULL, \"name\" TEXT NOT NULL, \"identity_key\" BLOB NOT NULL, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), PRIMARY KEY (\"device_id\", \"name\"));" + } + ] + }, + { + "name": "signal_pre_key_stores", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"signal_pre_key_stores\" (\"pre_key_id\" INTEGER NOT NULL, \"pre_key\" BLOB NOT NULL, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), PRIMARY KEY (\"pre_key_id\"));" + } + ] + }, + { + "name": "signal_sender_key_stores", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"signal_sender_key_stores\" (\"sender_key_name\" TEXT NOT NULL, \"sender_key\" BLOB NOT NULL, PRIMARY KEY (\"sender_key_name\"));" + } + ] + }, + { + "name": "signal_session_stores", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"signal_session_stores\" (\"device_id\" INTEGER NOT NULL, \"name\" TEXT NOT NULL, \"session_record\" BLOB NOT NULL, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), PRIMARY KEY (\"device_id\", \"name\"));" + } + ] + }, + { + "name": "message_actions", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"message_actions\" (\"message_id\" TEXT NOT NULL REFERENCES messages (message_id) ON DELETE CASCADE, \"contact_id\" INTEGER NOT NULL REFERENCES contacts (user_id) ON DELETE CASCADE, \"type\" TEXT NOT NULL, \"action_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), PRIMARY KEY (\"message_id\", \"contact_id\", \"type\"));" + } + ] + }, + { + "name": "group_histories", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"group_histories\" (\"group_history_id\" TEXT NOT NULL, \"group_id\" TEXT NOT NULL REFERENCES \"groups\" (group_id) ON DELETE CASCADE, \"contact_id\" INTEGER NULL REFERENCES contacts (user_id), \"affected_contact_id\" INTEGER NULL, \"old_group_name\" TEXT NULL, \"new_group_name\" TEXT NULL, \"new_delete_messages_after_milliseconds\" INTEGER NULL, \"type\" TEXT NOT NULL, \"action_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), PRIMARY KEY (\"group_history_id\"));" + } + ] + }, + { + "name": "key_verifications", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"key_verifications\" (\"verification_id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, \"contact_id\" INTEGER NOT NULL REFERENCES contacts (user_id) ON DELETE CASCADE, \"type\" TEXT NOT NULL, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)));" + } + ] + }, + { + "name": "verification_tokens", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"verification_tokens\" (\"token_id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, \"token\" BLOB NOT NULL, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)));" + } + ] + }, + { + "name": "user_discovery_announced_users", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"user_discovery_announced_users\" (\"announced_user_id\" INTEGER NOT NULL, \"announced_public_key\" BLOB NOT NULL, \"public_id\" INTEGER NOT NULL UNIQUE, \"username\" TEXT NULL, \"was_shown_to_the_user\" INTEGER NOT NULL DEFAULT 0 CHECK (\"was_shown_to_the_user\" IN (0, 1)), \"is_hidden\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_hidden\" IN (0, 1)), PRIMARY KEY (\"announced_user_id\"));" + } + ] + }, + { + "name": "user_discovery_user_relations", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"user_discovery_user_relations\" (\"announced_user_id\" INTEGER NOT NULL REFERENCES user_discovery_announced_users (announced_user_id) ON DELETE CASCADE, \"from_contact_id\" INTEGER NOT NULL REFERENCES contacts (user_id) ON DELETE CASCADE, \"public_key_verified_timestamp\" INTEGER NULL, PRIMARY KEY (\"announced_user_id\", \"from_contact_id\"));" + } + ] + }, + { + "name": "user_discovery_other_promotions", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"user_discovery_other_promotions\" (\"from_contact_id\" INTEGER NOT NULL REFERENCES contacts (user_id) ON DELETE CASCADE, \"promotion_id\" INTEGER NOT NULL, \"public_id\" INTEGER NOT NULL, \"threshold\" INTEGER NOT NULL, \"announcement_share\" BLOB NOT NULL, \"public_key_verified_timestamp\" INTEGER NULL, PRIMARY KEY (\"from_contact_id\", \"public_id\"));" + } + ] + }, + { + "name": "user_discovery_own_promotions", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"user_discovery_own_promotions\" (\"version_id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, \"contact_id\" INTEGER NOT NULL REFERENCES contacts (user_id) ON DELETE CASCADE, \"promotion\" BLOB NOT NULL);" + } + ] + }, + { + "name": "user_discovery_shares", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"user_discovery_shares\" (\"share_id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, \"share\" BLOB NOT NULL, \"contact_id\" INTEGER NULL REFERENCES contacts (user_id) ON DELETE CASCADE);" + } + ] + }, + { + "name": "shortcuts", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"shortcuts\" (\"id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, \"emoji\" TEXT NOT NULL UNIQUE, \"usage_counter\" INTEGER NOT NULL DEFAULT 0);" + } + ] + }, + { + "name": "shortcut_members", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"shortcut_members\" (\"shortcut_id\" INTEGER NOT NULL REFERENCES shortcuts (id) ON DELETE CASCADE, \"group_id\" TEXT NOT NULL REFERENCES \"groups\" (group_id) ON DELETE CASCADE, PRIMARY KEY (\"shortcut_id\", \"group_id\"));" + } + ] + } + ] +} \ No newline at end of file diff --git a/lib/src/database/signal/signal_signed_pre_key_store.dart b/lib/src/database/signal/signal_signed_pre_key_store.dart index 5017230c..dbdcf970 100644 --- a/lib/src/database/signal/signal_signed_pre_key_store.dart +++ b/lib/src/database/signal/signal_signed_pre_key_store.dart @@ -3,52 +3,43 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; +import 'package:twonly/core/bridge/wrapper/key_manager.dart'; import 'package:twonly/src/constants/secure_storage.keys.dart'; import 'package:twonly/src/utils/secure_storage.dart'; -class SignalSignedPreKeyStore extends SignedPreKeyStore { - Future> getStore() async { - final storeSerialized = await SecureStorage.instance.read( - key: SecureStorageKeys.signalSignedPreKey, - ); - final store = HashMap(); - if (storeSerialized == null) { - return store; - } - final storeHashMap = json.decode(storeSerialized) as List; - for (final item in storeHashMap) { - // ignore: avoid_dynamic_calls - store[item[0] as int] = base64Decode(item[1] as String); - } +Future> getSignalSignedPreKeyStoreOld() async { + final storeSerialized = await SecureStorage.instance.read( + key: SecureStorageKeys.signalSignedPreKey, + ); + final store = HashMap(); + if (storeSerialized == null) { return store; } - - Future safeStore(HashMap store) async { - final storeHashMap = >[]; - for (final item in store.entries) { - storeHashMap.add([item.key, base64Encode(item.value)]); - } - final storeSerialized = json.encode(storeHashMap); - await SecureStorage.instance.write( - key: SecureStorageKeys.signalSignedPreKey, - value: storeSerialized, - ); + final storeHashMap = json.decode(storeSerialized) as List; + for (final item in storeHashMap) { + // ignore: avoid_dynamic_calls + store[item[0] as int] = base64Decode(item[1] as String); } + return store; +} +class SignalSignedPreKeyStore extends SignedPreKeyStore { @override Future loadSignedPreKey(int signedPreKeyId) async { - final store = await getStore(); - if (!store.containsKey(signedPreKeyId)) { + final store = await RustKeyManager.loadSignedPrekey( + signedPreKeyId: signedPreKeyId, + ); + if (store == null) { throw InvalidKeyIdException( 'No such signed prekey record! $signedPreKeyId', ); } - return SignedPreKeyRecord.fromSerialized(store[signedPreKeyId]!); + return SignedPreKeyRecord.fromSerialized(store); } @override Future> loadSignedPreKeys() async { - final store = await getStore(); + final store = await RustKeyManager.loadSignedPrekeys(); final results = []; for (final serialized in store.values) { results.add(SignedPreKeyRecord.fromSerialized(serialized)); @@ -61,19 +52,21 @@ class SignalSignedPreKeyStore extends SignedPreKeyStore { int signedPreKeyId, SignedPreKeyRecord record, ) async { - final store = await getStore(); - store[signedPreKeyId] = record.serialize(); - await safeStore(store); + await RustKeyManager.storeSignedPrekey( + signedPreKeyId: signedPreKeyId, + record: record.serialize(), + ); } @override Future containsSignedPreKey(int signedPreKeyId) async => - (await getStore()).containsKey(signedPreKeyId); + await RustKeyManager.loadSignedPrekey( + signedPreKeyId: signedPreKeyId, + ) != + null; @override Future removeSignedPreKey(int signedPreKeyId) async { - final store = await getStore(); - store.remove(signedPreKeyId); - await safeStore(store); + await RustKeyManager.removeSignedPrekey(signedPreKeyId: signedPreKeyId); } } diff --git a/lib/src/database/tables/shortcuts.table.dart b/lib/src/database/tables/shortcuts.table.dart new file mode 100644 index 00000000..43cda3a7 --- /dev/null +++ b/lib/src/database/tables/shortcuts.table.dart @@ -0,0 +1,26 @@ +import 'package:drift/drift.dart'; +import 'package:twonly/src/database/tables/groups.table.dart'; + +@DataClassName('Shortcut') +class Shortcuts extends Table { + IntColumn get id => integer().autoIncrement()(); + TextColumn get emoji => text().unique()(); + IntColumn get usageCounter => integer().withDefault(const Constant(0))(); +} + +@DataClassName('ShortcutMember') +class ShortcutMembers extends Table { + IntColumn get shortcutId => integer().references( + Shortcuts, + #id, + onDelete: KeyAction.cascade, + )(); + TextColumn get groupId => text().references( + Groups, + #groupId, + onDelete: KeyAction.cascade, + )(); + + @override + Set get primaryKey => {shortcutId, groupId}; +} diff --git a/lib/src/database/twonly.db.dart b/lib/src/database/twonly.db.dart index a53703bb..bba50b61 100644 --- a/lib/src/database/twonly.db.dart +++ b/lib/src/database/twonly.db.dart @@ -10,6 +10,7 @@ import 'package:twonly/src/database/daos/mediafiles.dao.dart'; import 'package:twonly/src/database/daos/messages.dao.dart'; import 'package:twonly/src/database/daos/reactions.dao.dart'; import 'package:twonly/src/database/daos/receipts.dao.dart'; +import 'package:twonly/src/database/daos/shortcuts.dao.dart'; import 'package:twonly/src/database/daos/user_discovery.dao.dart'; import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/database/tables/groups.table.dart'; @@ -17,6 +18,7 @@ import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/tables/reactions.table.dart'; import 'package:twonly/src/database/tables/receipts.table.dart'; +import 'package:twonly/src/database/tables/shortcuts.table.dart'; import 'package:twonly/src/database/tables/signal_identity_key_store.table.dart'; import 'package:twonly/src/database/tables/signal_pre_key_store.table.dart'; import 'package:twonly/src/database/tables/signal_sender_key_store.table.dart'; @@ -52,6 +54,8 @@ part 'twonly.db.g.dart'; UserDiscoveryOtherPromotions, UserDiscoveryOwnPromotions, UserDiscoveryShares, + Shortcuts, + ShortcutMembers, ], daos: [ MessagesDao, @@ -62,6 +66,7 @@ part 'twonly.db.g.dart'; MediaFilesDao, UserDiscoveryDao, KeyVerificationDao, + ShortcutsDao, ], ) class TwonlyDB extends _$TwonlyDB { @@ -74,7 +79,7 @@ class TwonlyDB extends _$TwonlyDB { TwonlyDB.forTesting(DatabaseConnection super.connection); @override - int get schemaVersion => 12; + int get schemaVersion => 13; static QueryExecutor _openConnection() { return driftDatabase( @@ -186,6 +191,10 @@ class TwonlyDB extends _$TwonlyDB { await m.addColumn(schema.contacts, column); } }, + from12To13: (m, schema) async { + await m.createTable(schema.shortcuts); + await m.createTable(schema.shortcutMembers); + }, )(m, from, to); }, ); diff --git a/lib/src/database/twonly.db.g.dart b/lib/src/database/twonly.db.g.dart index 7a2f7f69..2d241408 100644 --- a/lib/src/database/twonly.db.g.dart +++ b/lib/src/database/twonly.db.g.dart @@ -11456,6 +11456,483 @@ class UserDiscoverySharesCompanion extends UpdateCompanion { } } +class $ShortcutsTable extends Shortcuts + with TableInfo<$ShortcutsTable, Shortcut> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $ShortcutsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'PRIMARY KEY AUTOINCREMENT', + ), + ); + static const VerificationMeta _emojiMeta = const VerificationMeta('emoji'); + @override + late final GeneratedColumn emoji = GeneratedColumn( + 'emoji', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('UNIQUE'), + ); + static const VerificationMeta _usageCounterMeta = const VerificationMeta( + 'usageCounter', + ); + @override + late final GeneratedColumn usageCounter = GeneratedColumn( + 'usage_counter', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0), + ); + @override + List get $columns => [id, emoji, usageCounter]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'shortcuts'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } + if (data.containsKey('emoji')) { + context.handle( + _emojiMeta, + emoji.isAcceptableOrUnknown(data['emoji']!, _emojiMeta), + ); + } else if (isInserting) { + context.missing(_emojiMeta); + } + if (data.containsKey('usage_counter')) { + context.handle( + _usageCounterMeta, + usageCounter.isAcceptableOrUnknown( + data['usage_counter']!, + _usageCounterMeta, + ), + ); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + Shortcut map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return Shortcut( + id: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}id'], + )!, + emoji: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}emoji'], + )!, + usageCounter: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}usage_counter'], + )!, + ); + } + + @override + $ShortcutsTable createAlias(String alias) { + return $ShortcutsTable(attachedDatabase, alias); + } +} + +class Shortcut extends DataClass implements Insertable { + final int id; + final String emoji; + final int usageCounter; + const Shortcut({ + required this.id, + required this.emoji, + required this.usageCounter, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['emoji'] = Variable(emoji); + map['usage_counter'] = Variable(usageCounter); + return map; + } + + ShortcutsCompanion toCompanion(bool nullToAbsent) { + return ShortcutsCompanion( + id: Value(id), + emoji: Value(emoji), + usageCounter: Value(usageCounter), + ); + } + + factory Shortcut.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return Shortcut( + id: serializer.fromJson(json['id']), + emoji: serializer.fromJson(json['emoji']), + usageCounter: serializer.fromJson(json['usageCounter']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'emoji': serializer.toJson(emoji), + 'usageCounter': serializer.toJson(usageCounter), + }; + } + + Shortcut copyWith({int? id, String? emoji, int? usageCounter}) => Shortcut( + id: id ?? this.id, + emoji: emoji ?? this.emoji, + usageCounter: usageCounter ?? this.usageCounter, + ); + Shortcut copyWithCompanion(ShortcutsCompanion data) { + return Shortcut( + id: data.id.present ? data.id.value : this.id, + emoji: data.emoji.present ? data.emoji.value : this.emoji, + usageCounter: data.usageCounter.present + ? data.usageCounter.value + : this.usageCounter, + ); + } + + @override + String toString() { + return (StringBuffer('Shortcut(') + ..write('id: $id, ') + ..write('emoji: $emoji, ') + ..write('usageCounter: $usageCounter') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, emoji, usageCounter); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is Shortcut && + other.id == this.id && + other.emoji == this.emoji && + other.usageCounter == this.usageCounter); +} + +class ShortcutsCompanion extends UpdateCompanion { + final Value id; + final Value emoji; + final Value usageCounter; + const ShortcutsCompanion({ + this.id = const Value.absent(), + this.emoji = const Value.absent(), + this.usageCounter = const Value.absent(), + }); + ShortcutsCompanion.insert({ + this.id = const Value.absent(), + required String emoji, + this.usageCounter = const Value.absent(), + }) : emoji = Value(emoji); + static Insertable custom({ + Expression? id, + Expression? emoji, + Expression? usageCounter, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (emoji != null) 'emoji': emoji, + if (usageCounter != null) 'usage_counter': usageCounter, + }); + } + + ShortcutsCompanion copyWith({ + Value? id, + Value? emoji, + Value? usageCounter, + }) { + return ShortcutsCompanion( + id: id ?? this.id, + emoji: emoji ?? this.emoji, + usageCounter: usageCounter ?? this.usageCounter, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (emoji.present) { + map['emoji'] = Variable(emoji.value); + } + if (usageCounter.present) { + map['usage_counter'] = Variable(usageCounter.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ShortcutsCompanion(') + ..write('id: $id, ') + ..write('emoji: $emoji, ') + ..write('usageCounter: $usageCounter') + ..write(')')) + .toString(); + } +} + +class $ShortcutMembersTable extends ShortcutMembers + with TableInfo<$ShortcutMembersTable, ShortcutMember> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $ShortcutMembersTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _shortcutIdMeta = const VerificationMeta( + 'shortcutId', + ); + @override + late final GeneratedColumn shortcutId = GeneratedColumn( + 'shortcut_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES shortcuts (id) ON DELETE CASCADE', + ), + ); + static const VerificationMeta _groupIdMeta = const VerificationMeta( + 'groupId', + ); + @override + late final GeneratedColumn groupId = GeneratedColumn( + 'group_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES "groups" (group_id) ON DELETE CASCADE', + ), + ); + @override + List get $columns => [shortcutId, groupId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'shortcut_members'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('shortcut_id')) { + context.handle( + _shortcutIdMeta, + shortcutId.isAcceptableOrUnknown(data['shortcut_id']!, _shortcutIdMeta), + ); + } else if (isInserting) { + context.missing(_shortcutIdMeta); + } + if (data.containsKey('group_id')) { + context.handle( + _groupIdMeta, + groupId.isAcceptableOrUnknown(data['group_id']!, _groupIdMeta), + ); + } else if (isInserting) { + context.missing(_groupIdMeta); + } + return context; + } + + @override + Set get $primaryKey => {shortcutId, groupId}; + @override + ShortcutMember map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ShortcutMember( + shortcutId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}shortcut_id'], + )!, + groupId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}group_id'], + )!, + ); + } + + @override + $ShortcutMembersTable createAlias(String alias) { + return $ShortcutMembersTable(attachedDatabase, alias); + } +} + +class ShortcutMember extends DataClass implements Insertable { + final int shortcutId; + final String groupId; + const ShortcutMember({required this.shortcutId, required this.groupId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['shortcut_id'] = Variable(shortcutId); + map['group_id'] = Variable(groupId); + return map; + } + + ShortcutMembersCompanion toCompanion(bool nullToAbsent) { + return ShortcutMembersCompanion( + shortcutId: Value(shortcutId), + groupId: Value(groupId), + ); + } + + factory ShortcutMember.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ShortcutMember( + shortcutId: serializer.fromJson(json['shortcutId']), + groupId: serializer.fromJson(json['groupId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'shortcutId': serializer.toJson(shortcutId), + 'groupId': serializer.toJson(groupId), + }; + } + + ShortcutMember copyWith({int? shortcutId, String? groupId}) => ShortcutMember( + shortcutId: shortcutId ?? this.shortcutId, + groupId: groupId ?? this.groupId, + ); + ShortcutMember copyWithCompanion(ShortcutMembersCompanion data) { + return ShortcutMember( + shortcutId: data.shortcutId.present + ? data.shortcutId.value + : this.shortcutId, + groupId: data.groupId.present ? data.groupId.value : this.groupId, + ); + } + + @override + String toString() { + return (StringBuffer('ShortcutMember(') + ..write('shortcutId: $shortcutId, ') + ..write('groupId: $groupId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(shortcutId, groupId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ShortcutMember && + other.shortcutId == this.shortcutId && + other.groupId == this.groupId); +} + +class ShortcutMembersCompanion extends UpdateCompanion { + final Value shortcutId; + final Value groupId; + final Value rowid; + const ShortcutMembersCompanion({ + this.shortcutId = const Value.absent(), + this.groupId = const Value.absent(), + this.rowid = const Value.absent(), + }); + ShortcutMembersCompanion.insert({ + required int shortcutId, + required String groupId, + this.rowid = const Value.absent(), + }) : shortcutId = Value(shortcutId), + groupId = Value(groupId); + static Insertable custom({ + Expression? shortcutId, + Expression? groupId, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (shortcutId != null) 'shortcut_id': shortcutId, + if (groupId != null) 'group_id': groupId, + if (rowid != null) 'rowid': rowid, + }); + } + + ShortcutMembersCompanion copyWith({ + Value? shortcutId, + Value? groupId, + Value? rowid, + }) { + return ShortcutMembersCompanion( + shortcutId: shortcutId ?? this.shortcutId, + groupId: groupId ?? this.groupId, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (shortcutId.present) { + map['shortcut_id'] = Variable(shortcutId.value); + } + if (groupId.present) { + map['group_id'] = Variable(groupId.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ShortcutMembersCompanion(') + ..write('shortcutId: $shortcutId, ') + ..write('groupId: $groupId, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + abstract class _$TwonlyDB extends GeneratedDatabase { _$TwonlyDB(QueryExecutor e) : super(e); $TwonlyDBManager get managers => $TwonlyDBManager(this); @@ -11497,6 +11974,10 @@ abstract class _$TwonlyDB extends GeneratedDatabase { $UserDiscoveryOwnPromotionsTable(this); late final $UserDiscoverySharesTable userDiscoveryShares = $UserDiscoverySharesTable(this); + late final $ShortcutsTable shortcuts = $ShortcutsTable(this); + late final $ShortcutMembersTable shortcutMembers = $ShortcutMembersTable( + this, + ); late final MessagesDao messagesDao = MessagesDao(this as TwonlyDB); late final ContactsDao contactsDao = ContactsDao(this as TwonlyDB); late final ReceiptsDao receiptsDao = ReceiptsDao(this as TwonlyDB); @@ -11509,6 +11990,7 @@ abstract class _$TwonlyDB extends GeneratedDatabase { late final KeyVerificationDao keyVerificationDao = KeyVerificationDao( this as TwonlyDB, ); + late final ShortcutsDao shortcutsDao = ShortcutsDao(this as TwonlyDB); @override Iterable> get allTables => allSchemaEntities.whereType>(); @@ -11536,6 +12018,8 @@ abstract class _$TwonlyDB extends GeneratedDatabase { userDiscoveryOtherPromotions, userDiscoveryOwnPromotions, userDiscoveryShares, + shortcuts, + shortcutMembers, ]; @override StreamQueryUpdateRules get streamUpdateRules => const StreamQueryUpdateRules([ @@ -11673,6 +12157,20 @@ abstract class _$TwonlyDB extends GeneratedDatabase { ), result: [TableUpdate('user_discovery_shares', kind: UpdateKind.delete)], ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'shortcuts', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('shortcut_members', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'groups', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('shortcut_members', kind: UpdateKind.delete)], + ), ]); } @@ -13487,6 +13985,29 @@ final class $$GroupsTableReferences manager.$state.copyWith(prefetchedData: cache), ); } + + static MultiTypedResultKey<$ShortcutMembersTable, List> + _shortcutMembersRefsTable(_$TwonlyDB db) => MultiTypedResultKey.fromTable( + db.shortcutMembers, + aliasName: $_aliasNameGenerator( + db.groups.groupId, + db.shortcutMembers.groupId, + ), + ); + + $$ShortcutMembersTableProcessedTableManager get shortcutMembersRefs { + final manager = + $$ShortcutMembersTableTableManager($_db, $_db.shortcutMembers).filter( + (f) => f.groupId.groupId.sqlEquals($_itemColumn('group_id')!), + ); + + final cache = $_typedResult.readTableOrNull( + _shortcutMembersRefsTable($_db), + ); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } } class $$GroupsTableFilterComposer extends Composer<_$TwonlyDB, $GroupsTable> { @@ -13696,6 +14217,31 @@ class $$GroupsTableFilterComposer extends Composer<_$TwonlyDB, $GroupsTable> { ); return f(composer); } + + Expression shortcutMembersRefs( + Expression Function($$ShortcutMembersTableFilterComposer f) f, + ) { + final $$ShortcutMembersTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.groupId, + referencedTable: $db.shortcutMembers, + getReferencedColumn: (t) => t.groupId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ShortcutMembersTableFilterComposer( + $db: $db, + $table: $db.shortcutMembers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } } class $$GroupsTableOrderingComposer extends Composer<_$TwonlyDB, $GroupsTable> { @@ -14030,6 +14576,31 @@ class $$GroupsTableAnnotationComposer ); return f(composer); } + + Expression shortcutMembersRefs( + Expression Function($$ShortcutMembersTableAnnotationComposer a) f, + ) { + final $$ShortcutMembersTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.groupId, + referencedTable: $db.shortcutMembers, + getReferencedColumn: (t) => t.groupId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ShortcutMembersTableAnnotationComposer( + $db: $db, + $table: $db.shortcutMembers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } } class $$GroupsTableTableManager @@ -14049,6 +14620,7 @@ class $$GroupsTableTableManager bool messagesRefs, bool groupMembersRefs, bool groupHistoriesRefs, + bool shortcutMembersRefs, }) > { $$GroupsTableTableManager(_$TwonlyDB db, $GroupsTable table) @@ -14189,6 +14761,7 @@ class $$GroupsTableTableManager messagesRefs = false, groupMembersRefs = false, groupHistoriesRefs = false, + shortcutMembersRefs = false, }) { return PrefetchHooks( db: db, @@ -14196,6 +14769,7 @@ class $$GroupsTableTableManager if (messagesRefs) db.messages, if (groupMembersRefs) db.groupMembers, if (groupHistoriesRefs) db.groupHistories, + if (shortcutMembersRefs) db.shortcutMembers, ], addJoins: null, getPrefetchedDataCallback: (items) async { @@ -14259,6 +14833,27 @@ class $$GroupsTableTableManager ), typedResults: items, ), + if (shortcutMembersRefs) + await $_getPrefetchedData< + Group, + $GroupsTable, + ShortcutMember + >( + currentTable: table, + referencedTable: $$GroupsTableReferences + ._shortcutMembersRefsTable(db), + managerFromTypedResult: (p0) => + $$GroupsTableReferences( + db, + table, + p0, + ).shortcutMembersRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.groupId == item.groupId, + ), + typedResults: items, + ), ]; }, ); @@ -14283,6 +14878,7 @@ typedef $$GroupsTableProcessedTableManager = bool messagesRefs, bool groupMembersRefs, bool groupHistoriesRefs, + bool shortcutMembersRefs, }) >; typedef $$MediaFilesTableCreateCompanionBuilder = @@ -21859,6 +22455,629 @@ typedef $$UserDiscoverySharesTableProcessedTableManager = UserDiscoveryShare, PrefetchHooks Function({bool contactId}) >; +typedef $$ShortcutsTableCreateCompanionBuilder = + ShortcutsCompanion Function({ + Value id, + required String emoji, + Value usageCounter, + }); +typedef $$ShortcutsTableUpdateCompanionBuilder = + ShortcutsCompanion Function({ + Value id, + Value emoji, + Value usageCounter, + }); + +final class $$ShortcutsTableReferences + extends BaseReferences<_$TwonlyDB, $ShortcutsTable, Shortcut> { + $$ShortcutsTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static MultiTypedResultKey<$ShortcutMembersTable, List> + _shortcutMembersRefsTable(_$TwonlyDB db) => MultiTypedResultKey.fromTable( + db.shortcutMembers, + aliasName: $_aliasNameGenerator( + db.shortcuts.id, + db.shortcutMembers.shortcutId, + ), + ); + + $$ShortcutMembersTableProcessedTableManager get shortcutMembersRefs { + final manager = $$ShortcutMembersTableTableManager( + $_db, + $_db.shortcutMembers, + ).filter((f) => f.shortcutId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull( + _shortcutMembersRefsTable($_db), + ); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } +} + +class $$ShortcutsTableFilterComposer + extends Composer<_$TwonlyDB, $ShortcutsTable> { + $$ShortcutsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get emoji => $composableBuilder( + column: $table.emoji, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get usageCounter => $composableBuilder( + column: $table.usageCounter, + builder: (column) => ColumnFilters(column), + ); + + Expression shortcutMembersRefs( + Expression Function($$ShortcutMembersTableFilterComposer f) f, + ) { + final $$ShortcutMembersTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.shortcutMembers, + getReferencedColumn: (t) => t.shortcutId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ShortcutMembersTableFilterComposer( + $db: $db, + $table: $db.shortcutMembers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$ShortcutsTableOrderingComposer + extends Composer<_$TwonlyDB, $ShortcutsTable> { + $$ShortcutsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get emoji => $composableBuilder( + column: $table.emoji, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get usageCounter => $composableBuilder( + column: $table.usageCounter, + builder: (column) => ColumnOrderings(column), + ); +} + +class $$ShortcutsTableAnnotationComposer + extends Composer<_$TwonlyDB, $ShortcutsTable> { + $$ShortcutsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get emoji => + $composableBuilder(column: $table.emoji, builder: (column) => column); + + GeneratedColumn get usageCounter => $composableBuilder( + column: $table.usageCounter, + builder: (column) => column, + ); + + Expression shortcutMembersRefs( + Expression Function($$ShortcutMembersTableAnnotationComposer a) f, + ) { + final $$ShortcutMembersTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.shortcutMembers, + getReferencedColumn: (t) => t.shortcutId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ShortcutMembersTableAnnotationComposer( + $db: $db, + $table: $db.shortcutMembers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$ShortcutsTableTableManager + extends + RootTableManager< + _$TwonlyDB, + $ShortcutsTable, + Shortcut, + $$ShortcutsTableFilterComposer, + $$ShortcutsTableOrderingComposer, + $$ShortcutsTableAnnotationComposer, + $$ShortcutsTableCreateCompanionBuilder, + $$ShortcutsTableUpdateCompanionBuilder, + (Shortcut, $$ShortcutsTableReferences), + Shortcut, + PrefetchHooks Function({bool shortcutMembersRefs}) + > { + $$ShortcutsTableTableManager(_$TwonlyDB db, $ShortcutsTable table) + : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$ShortcutsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$ShortcutsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$ShortcutsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value id = const Value.absent(), + Value emoji = const Value.absent(), + Value usageCounter = const Value.absent(), + }) => ShortcutsCompanion( + id: id, + emoji: emoji, + usageCounter: usageCounter, + ), + createCompanionCallback: + ({ + Value id = const Value.absent(), + required String emoji, + Value usageCounter = const Value.absent(), + }) => ShortcutsCompanion.insert( + id: id, + emoji: emoji, + usageCounter: usageCounter, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$ShortcutsTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: ({shortcutMembersRefs = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [ + if (shortcutMembersRefs) db.shortcutMembers, + ], + addJoins: null, + getPrefetchedDataCallback: (items) async { + return [ + if (shortcutMembersRefs) + await $_getPrefetchedData< + Shortcut, + $ShortcutsTable, + ShortcutMember + >( + currentTable: table, + referencedTable: $$ShortcutsTableReferences + ._shortcutMembersRefsTable(db), + managerFromTypedResult: (p0) => + $$ShortcutsTableReferences( + db, + table, + p0, + ).shortcutMembersRefs, + referencedItemsForCurrentItem: (item, referencedItems) => + referencedItems.where((e) => e.shortcutId == item.id), + typedResults: items, + ), + ]; + }, + ); + }, + ), + ); +} + +typedef $$ShortcutsTableProcessedTableManager = + ProcessedTableManager< + _$TwonlyDB, + $ShortcutsTable, + Shortcut, + $$ShortcutsTableFilterComposer, + $$ShortcutsTableOrderingComposer, + $$ShortcutsTableAnnotationComposer, + $$ShortcutsTableCreateCompanionBuilder, + $$ShortcutsTableUpdateCompanionBuilder, + (Shortcut, $$ShortcutsTableReferences), + Shortcut, + PrefetchHooks Function({bool shortcutMembersRefs}) + >; +typedef $$ShortcutMembersTableCreateCompanionBuilder = + ShortcutMembersCompanion Function({ + required int shortcutId, + required String groupId, + Value rowid, + }); +typedef $$ShortcutMembersTableUpdateCompanionBuilder = + ShortcutMembersCompanion Function({ + Value shortcutId, + Value groupId, + Value rowid, + }); + +final class $$ShortcutMembersTableReferences + extends BaseReferences<_$TwonlyDB, $ShortcutMembersTable, ShortcutMember> { + $$ShortcutMembersTableReferences( + super.$_db, + super.$_table, + super.$_typedResult, + ); + + static $ShortcutsTable _shortcutIdTable(_$TwonlyDB db) => + db.shortcuts.createAlias( + $_aliasNameGenerator(db.shortcutMembers.shortcutId, db.shortcuts.id), + ); + + $$ShortcutsTableProcessedTableManager get shortcutId { + final $_column = $_itemColumn('shortcut_id')!; + + final manager = $$ShortcutsTableTableManager( + $_db, + $_db.shortcuts, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_shortcutIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } + + static $GroupsTable _groupIdTable(_$TwonlyDB db) => db.groups.createAlias( + $_aliasNameGenerator(db.shortcutMembers.groupId, db.groups.groupId), + ); + + $$GroupsTableProcessedTableManager get groupId { + final $_column = $_itemColumn('group_id')!; + + final manager = $$GroupsTableTableManager( + $_db, + $_db.groups, + ).filter((f) => f.groupId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_groupIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } +} + +class $$ShortcutMembersTableFilterComposer + extends Composer<_$TwonlyDB, $ShortcutMembersTable> { + $$ShortcutMembersTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + $$ShortcutsTableFilterComposer get shortcutId { + final $$ShortcutsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.shortcutId, + referencedTable: $db.shortcuts, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ShortcutsTableFilterComposer( + $db: $db, + $table: $db.shortcuts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$GroupsTableFilterComposer get groupId { + final $$GroupsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.groupId, + referencedTable: $db.groups, + getReferencedColumn: (t) => t.groupId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$GroupsTableFilterComposer( + $db: $db, + $table: $db.groups, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$ShortcutMembersTableOrderingComposer + extends Composer<_$TwonlyDB, $ShortcutMembersTable> { + $$ShortcutMembersTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + $$ShortcutsTableOrderingComposer get shortcutId { + final $$ShortcutsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.shortcutId, + referencedTable: $db.shortcuts, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ShortcutsTableOrderingComposer( + $db: $db, + $table: $db.shortcuts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$GroupsTableOrderingComposer get groupId { + final $$GroupsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.groupId, + referencedTable: $db.groups, + getReferencedColumn: (t) => t.groupId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$GroupsTableOrderingComposer( + $db: $db, + $table: $db.groups, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$ShortcutMembersTableAnnotationComposer + extends Composer<_$TwonlyDB, $ShortcutMembersTable> { + $$ShortcutMembersTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + $$ShortcutsTableAnnotationComposer get shortcutId { + final $$ShortcutsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.shortcutId, + referencedTable: $db.shortcuts, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ShortcutsTableAnnotationComposer( + $db: $db, + $table: $db.shortcuts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$GroupsTableAnnotationComposer get groupId { + final $$GroupsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.groupId, + referencedTable: $db.groups, + getReferencedColumn: (t) => t.groupId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$GroupsTableAnnotationComposer( + $db: $db, + $table: $db.groups, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$ShortcutMembersTableTableManager + extends + RootTableManager< + _$TwonlyDB, + $ShortcutMembersTable, + ShortcutMember, + $$ShortcutMembersTableFilterComposer, + $$ShortcutMembersTableOrderingComposer, + $$ShortcutMembersTableAnnotationComposer, + $$ShortcutMembersTableCreateCompanionBuilder, + $$ShortcutMembersTableUpdateCompanionBuilder, + (ShortcutMember, $$ShortcutMembersTableReferences), + ShortcutMember, + PrefetchHooks Function({bool shortcutId, bool groupId}) + > { + $$ShortcutMembersTableTableManager(_$TwonlyDB db, $ShortcutMembersTable table) + : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$ShortcutMembersTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$ShortcutMembersTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$ShortcutMembersTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value shortcutId = const Value.absent(), + Value groupId = const Value.absent(), + Value rowid = const Value.absent(), + }) => ShortcutMembersCompanion( + shortcutId: shortcutId, + groupId: groupId, + rowid: rowid, + ), + createCompanionCallback: + ({ + required int shortcutId, + required String groupId, + Value rowid = const Value.absent(), + }) => ShortcutMembersCompanion.insert( + shortcutId: shortcutId, + groupId: groupId, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$ShortcutMembersTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: ({shortcutId = false, groupId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: + < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic + > + >(state) { + if (shortcutId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.shortcutId, + referencedTable: + $$ShortcutMembersTableReferences + ._shortcutIdTable(db), + referencedColumn: + $$ShortcutMembersTableReferences + ._shortcutIdTable(db) + .id, + ) + as T; + } + if (groupId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.groupId, + referencedTable: + $$ShortcutMembersTableReferences + ._groupIdTable(db), + referencedColumn: + $$ShortcutMembersTableReferences + ._groupIdTable(db) + .groupId, + ) + as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + ), + ); +} + +typedef $$ShortcutMembersTableProcessedTableManager = + ProcessedTableManager< + _$TwonlyDB, + $ShortcutMembersTable, + ShortcutMember, + $$ShortcutMembersTableFilterComposer, + $$ShortcutMembersTableOrderingComposer, + $$ShortcutMembersTableAnnotationComposer, + $$ShortcutMembersTableCreateCompanionBuilder, + $$ShortcutMembersTableUpdateCompanionBuilder, + (ShortcutMember, $$ShortcutMembersTableReferences), + ShortcutMember, + PrefetchHooks Function({bool shortcutId, bool groupId}) + >; class $TwonlyDBManager { final _$TwonlyDB _db; @@ -21926,4 +23145,8 @@ class $TwonlyDBManager { ); $$UserDiscoverySharesTableTableManager get userDiscoveryShares => $$UserDiscoverySharesTableTableManager(_db, _db.userDiscoveryShares); + $$ShortcutsTableTableManager get shortcuts => + $$ShortcutsTableTableManager(_db, _db.shortcuts); + $$ShortcutMembersTableTableManager get shortcutMembers => + $$ShortcutMembersTableTableManager(_db, _db.shortcutMembers); } diff --git a/lib/src/database/twonly.db.steps.dart b/lib/src/database/twonly.db.steps.dart index 4f8306bc..54bf66ee 100644 --- a/lib/src/database/twonly.db.steps.dart +++ b/lib/src/database/twonly.db.steps.dart @@ -6582,6 +6582,480 @@ i1.GeneratedColumn _column_235(String aliasedName) => type: i1.DriftSqlType.blob, $customConstraints: 'NOT NULL', ); + +final class Schema13 extends i0.VersionedSchema { + Schema13({required super.database}) : super(version: 13); + @override + late final List entities = [ + contacts, + groups, + mediaFiles, + messages, + messageHistories, + reactions, + groupMembers, + receipts, + receivedReceipts, + signalIdentityKeyStores, + signalPreKeyStores, + signalSenderKeyStores, + signalSessionStores, + messageActions, + groupHistories, + keyVerifications, + verificationTokens, + userDiscoveryAnnouncedUsers, + userDiscoveryUserRelations, + userDiscoveryOtherPromotions, + userDiscoveryOwnPromotions, + userDiscoveryShares, + shortcuts, + shortcutMembers, + ]; + late final Shape39 contacts = Shape39( + source: i0.VersionedTable( + entityName: 'contacts', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(user_id)'], + columns: [ + _column_106, + _column_107, + _column_108, + _column_109, + _column_110, + _column_111, + _column_112, + _column_113, + _column_114, + _column_115, + _column_116, + _column_117, + _column_118, + _column_211, + _column_212, + _column_213, + _column_214, + _column_215, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape23 groups = Shape23( + source: i0.VersionedTable( + entityName: 'groups', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(group_id)'], + columns: [ + _column_119, + _column_120, + _column_121, + _column_122, + _column_123, + _column_124, + _column_125, + _column_126, + _column_127, + _column_128, + _column_129, + _column_130, + _column_131, + _column_132, + _column_133, + _column_134, + _column_118, + _column_135, + _column_136, + _column_137, + _column_138, + _column_139, + _column_140, + _column_141, + _column_142, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape36 mediaFiles = Shape36( + source: i0.VersionedTable( + entityName: 'media_files', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(media_id)'], + columns: [ + _column_143, + _column_144, + _column_145, + _column_146, + _column_147, + _column_148, + _column_149, + _column_207, + _column_150, + _column_151, + _column_152, + _column_153, + _column_154, + _column_155, + _column_156, + _column_157, + _column_118, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape25 messages = Shape25( + source: i0.VersionedTable( + entityName: 'messages', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(message_id)'], + columns: [ + _column_158, + _column_159, + _column_160, + _column_144, + _column_161, + _column_162, + _column_163, + _column_164, + _column_165, + _column_153, + _column_166, + _column_167, + _column_168, + _column_169, + _column_118, + _column_170, + _column_171, + _column_172, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape26 messageHistories = Shape26( + source: i0.VersionedTable( + entityName: 'message_histories', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_173, + _column_174, + _column_175, + _column_161, + _column_118, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape27 reactions = Shape27( + source: i0.VersionedTable( + entityName: 'reactions', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(message_id, sender_id, emoji)'], + columns: [_column_174, _column_176, _column_177, _column_118], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape38 groupMembers = Shape38( + source: i0.VersionedTable( + entityName: 'group_members', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(group_id, contact_id)'], + columns: [ + _column_158, + _column_178, + _column_179, + _column_180, + _column_209, + _column_210, + _column_181, + _column_118, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape37 receipts = Shape37( + source: i0.VersionedTable( + entityName: 'receipts', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(receipt_id)'], + columns: [ + _column_182, + _column_183, + _column_184, + _column_185, + _column_186, + _column_208, + _column_187, + _column_188, + _column_189, + _column_190, + _column_191, + _column_118, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape30 receivedReceipts = Shape30( + source: i0.VersionedTable( + entityName: 'received_receipts', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(receipt_id)'], + columns: [_column_182, _column_118], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape31 signalIdentityKeyStores = Shape31( + source: i0.VersionedTable( + entityName: 'signal_identity_key_stores', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(device_id, name)'], + columns: [_column_192, _column_193, _column_194, _column_118], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape32 signalPreKeyStores = Shape32( + source: i0.VersionedTable( + entityName: 'signal_pre_key_stores', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(pre_key_id)'], + columns: [_column_195, _column_196, _column_118], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape11 signalSenderKeyStores = Shape11( + source: i0.VersionedTable( + entityName: 'signal_sender_key_stores', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(sender_key_name)'], + columns: [_column_197, _column_198], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape33 signalSessionStores = Shape33( + source: i0.VersionedTable( + entityName: 'signal_session_stores', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(device_id, name)'], + columns: [_column_192, _column_193, _column_199, _column_118], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape34 messageActions = Shape34( + source: i0.VersionedTable( + entityName: 'message_actions', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(message_id, contact_id, type)'], + columns: [_column_174, _column_183, _column_144, _column_200], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape35 groupHistories = Shape35( + source: i0.VersionedTable( + entityName: 'group_histories', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(group_history_id)'], + columns: [ + _column_201, + _column_158, + _column_202, + _column_203, + _column_204, + _column_205, + _column_206, + _column_144, + _column_200, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape40 keyVerifications = Shape40( + source: i0.VersionedTable( + entityName: 'key_verifications', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [_column_216, _column_183, _column_144, _column_118], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape41 verificationTokens = Shape41( + source: i0.VersionedTable( + entityName: 'verification_tokens', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [_column_217, _column_218, _column_118], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape42 userDiscoveryAnnouncedUsers = Shape42( + source: i0.VersionedTable( + entityName: 'user_discovery_announced_users', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(announced_user_id)'], + columns: [ + _column_219, + _column_220, + _column_221, + _column_222, + _column_223, + _column_224, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape43 userDiscoveryUserRelations = Shape43( + source: i0.VersionedTable( + entityName: 'user_discovery_user_relations', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(announced_user_id, from_contact_id)'], + columns: [_column_225, _column_226, _column_227], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape44 userDiscoveryOtherPromotions = Shape44( + source: i0.VersionedTable( + entityName: 'user_discovery_other_promotions', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(from_contact_id, public_id)'], + columns: [ + _column_226, + _column_228, + _column_229, + _column_230, + _column_231, + _column_227, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape45 userDiscoveryOwnPromotions = Shape45( + source: i0.VersionedTable( + entityName: 'user_discovery_own_promotions', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [_column_232, _column_183, _column_233], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape46 userDiscoveryShares = Shape46( + source: i0.VersionedTable( + entityName: 'user_discovery_shares', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [_column_234, _column_235, _column_175], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape47 shortcuts = Shape47( + source: i0.VersionedTable( + entityName: 'shortcuts', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [_column_173, _column_236, _column_237], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape48 shortcutMembers = Shape48( + source: i0.VersionedTable( + entityName: 'shortcut_members', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(shortcut_id, group_id)'], + columns: [_column_238, _column_158], + attachedDatabase: database, + ), + alias: null, + ); +} + +class Shape47 extends i0.VersionedTable { + Shape47({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get id => + columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get emoji => + columnsByName['emoji']! as i1.GeneratedColumn; + i1.GeneratedColumn get usageCounter => + columnsByName['usage_counter']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_236(String aliasedName) => + i1.GeneratedColumn( + 'emoji', + aliasedName, + false, + type: i1.DriftSqlType.string, + $customConstraints: 'NOT NULL UNIQUE', + ); +i1.GeneratedColumn _column_237(String aliasedName) => + i1.GeneratedColumn( + 'usage_counter', + aliasedName, + false, + type: i1.DriftSqlType.int, + $customConstraints: 'NOT NULL DEFAULT 0', + defaultValue: const i1.CustomExpression('0'), + ); + +class Shape48 extends i0.VersionedTable { + Shape48({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get shortcutId => + columnsByName['shortcut_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get groupId => + columnsByName['group_id']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_238(String aliasedName) => + i1.GeneratedColumn( + 'shortcut_id', + aliasedName, + false, + type: i1.DriftSqlType.int, + $customConstraints: 'NOT NULL REFERENCES shortcuts(id)ON DELETE CASCADE', + ); i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, Schema2 schema) from1To2, required Future Function(i1.Migrator m, Schema3 schema) from2To3, @@ -6594,6 +7068,7 @@ i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, Schema10 schema) from9To10, required Future Function(i1.Migrator m, Schema11 schema) from10To11, required Future Function(i1.Migrator m, Schema12 schema) from11To12, + required Future Function(i1.Migrator m, Schema13 schema) from12To13, }) { return (currentVersion, database) async { switch (currentVersion) { @@ -6652,6 +7127,11 @@ i0.MigrationStepWithVersion migrationSteps({ final migrator = i1.Migrator(database, schema); await from11To12(migrator, schema); return 12; + case 12: + final schema = Schema13(database: database); + final migrator = i1.Migrator(database, schema); + await from12To13(migrator, schema); + return 13; default: throw ArgumentError.value('Unknown migration from $currentVersion'); } @@ -6670,6 +7150,7 @@ i1.OnUpgrade stepByStep({ required Future Function(i1.Migrator m, Schema10 schema) from9To10, required Future Function(i1.Migrator m, Schema11 schema) from10To11, required Future Function(i1.Migrator m, Schema12 schema) from11To12, + required Future Function(i1.Migrator m, Schema13 schema) from12To13, }) => i0.VersionedSchema.stepByStepHelper( step: migrationSteps( from1To2: from1To2, @@ -6683,5 +7164,6 @@ i1.OnUpgrade stepByStep({ from9To10: from9To10, from10To11: from10To11, from11To12: from11To12, + from12To13: from12To13, ), ); diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index a18d6f96..aa0a86f0 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -596,6 +596,18 @@ abstract class AppLocalizations { /// **'Notification'** String get settingsNotification; + /// No description provided for @settingsNotifyPermission. + /// + /// In en, this message translates to: + /// **'Notification permissions'** + String get settingsNotifyPermission; + + /// No description provided for @settingsNotifyPermissionDesc. + /// + /// In en, this message translates to: + /// **'Open system settings to allow push notifications.'** + String get settingsNotifyPermissionDesc; + /// No description provided for @settingsNotifyTroubleshooting. /// /// In en, this message translates to: @@ -1286,18 +1298,6 @@ abstract class AppLocalizations { /// **'Open'** String get open; - /// No description provided for @createVoucher. - /// - /// In en, this message translates to: - /// **'Buy voucher'** - String get createVoucher; - - /// No description provided for @redeemVoucher. - /// - /// In en, this message translates to: - /// **'Redeem voucher'** - String get redeemVoucher; - /// No description provided for @buy. /// /// In en, this message translates to: @@ -1412,23 +1412,17 @@ abstract class AppLocalizations { /// **'Due to twonly\'s security system, there is (currently) no password recovery function. Therefore, you must remember your password or, better yet, write it down.'** String get backupNoPasswordRecovery; - /// No description provided for @backupServer. + /// No description provided for @backupIdentityHeader. /// /// In en, this message translates to: - /// **'Server'** - String get backupServer; + /// **'Identity'** + String get backupIdentityHeader; - /// No description provided for @backupMaxBackupSize. + /// No description provided for @backupArchiveHeader. /// /// In en, this message translates to: - /// **'max. backup size'** - String get backupMaxBackupSize; - - /// No description provided for @backupStorageRetention. - /// - /// In en, this message translates to: - /// **'Storage retention'** - String get backupStorageRetention; + /// **'Contacts, Settings and Messages'** + String get backupArchiveHeader; /// No description provided for @backupLastBackupDate. /// @@ -1448,12 +1442,6 @@ abstract class AppLocalizations { /// **'Result'** String get backupLastBackupResult; - /// No description provided for @backupData. - /// - /// In en, this message translates to: - /// **'Data-Backup'** - String get backupData; - /// No description provided for @backupInsecurePassword. /// /// In en, this message translates to: @@ -1511,39 +1499,15 @@ abstract class AppLocalizations { /// No description provided for @backupPasswordRequirement. /// /// In en, this message translates to: - /// **'Password must be at least 8 characters long.'** + /// **'Password must be at least 10 characters long.'** String get backupPasswordRequirement; - /// No description provided for @backupExpertSettings. - /// - /// In en, this message translates to: - /// **'Expert settings'** - String get backupExpertSettings; - /// No description provided for @backupEnableBackup. /// /// In en, this message translates to: /// **'Activate automatic backup'** String get backupEnableBackup; - /// No description provided for @backupOwnServerDesc. - /// - /// In en, this message translates to: - /// **'Save your twonly Backup at twonly or on any server of your choice.'** - String get backupOwnServerDesc; - - /// No description provided for @backupUseOwnServer. - /// - /// In en, this message translates to: - /// **'Use server'** - String get backupUseOwnServer; - - /// No description provided for @backupResetServer. - /// - /// In en, this message translates to: - /// **'Use standard server'** - String get backupResetServer; - /// No description provided for @backupTwonlySaveNow. /// /// In en, this message translates to: @@ -2330,12 +2294,6 @@ abstract class AppLocalizations { /// **'Open your own QR code'** String get openYourOwnQRcode; - /// No description provided for @skipForNow. - /// - /// In en, this message translates to: - /// **'Skip for now'** - String get skipForNow; - /// No description provided for @finishSetupCardTitle. /// /// In en, this message translates to: @@ -2354,6 +2312,24 @@ abstract class AppLocalizations { /// **'Resume Setup'** String get finishSetupCardAction; + /// No description provided for @missingBackupCardTitle. + /// + /// In en, this message translates to: + /// **'Setup backup'** + String get missingBackupCardTitle; + + /// No description provided for @missingBackupCardDesc. + /// + /// In en, this message translates to: + /// **'We have improved the backup mechanism, which requires you to set it up again.'** + String get missingBackupCardDesc; + + /// No description provided for @missingBackupCardAction. + /// + /// In en, this message translates to: + /// **'Set up now'** + String get missingBackupCardAction; + /// No description provided for @onboardingFinishLater. /// /// In en, this message translates to: @@ -3061,6 +3037,126 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'{maker} changed their display name from {oldName} to {newName}.'** String makerChangedDisplayName(Object maker, Object oldName, Object newName); + + /// No description provided for @recoverErrorNoInternet. + /// + /// In en, this message translates to: + /// **'No internet connection. Please check your network and try again.'** + String get recoverErrorNoInternet; + + /// No description provided for @recoverErrorUsernameNotValid. + /// + /// In en, this message translates to: + /// **'The username provided is not valid or does not exist.'** + String get recoverErrorUsernameNotValid; + + /// No description provided for @recoverErrorPasswordInvalid. + /// + /// In en, this message translates to: + /// **'The password provided is incorrect.'** + String get recoverErrorPasswordInvalid; + + /// No description provided for @recoverErrorTryAgainLater. + /// + /// In en, this message translates to: + /// **'The server is currently unavailable. Please try again later.'** + String get recoverErrorTryAgainLater; + + /// No description provided for @recoverErrorUnknown. + /// + /// In en, this message translates to: + /// **'An unknown error occurred. Please try again.'** + String get recoverErrorUnknown; + + /// No description provided for @recoverSuccessTitle. + /// + /// In en, this message translates to: + /// **'Backup successfully recovered.'** + String get recoverSuccessTitle; + + /// No description provided for @recoverSuccessBody. + /// + /// In en, this message translates to: + /// **'Click here to open the app again'** + String get recoverSuccessBody; + + /// No description provided for @iosRecoveryWelcomeBack. + /// + /// In en, this message translates to: + /// **'Welcome Back'** + String get iosRecoveryWelcomeBack; + + /// No description provided for @iosRecoveryPrompt. + /// + /// In en, this message translates to: + /// **'We detected a previously secured twonly identity on this device. Would you like to automatically download and restore your contacts, messages, and settings from your cloud archive?'** + String get iosRecoveryPrompt; + + /// No description provided for @iosRecoveryNoBackupFound. + /// + /// In en, this message translates to: + /// **'No backup archive could be retrieved from the server for this device.\n\nError: {error}\n\nPlease proceed to register a new twonly account.'** + String iosRecoveryNoBackupFound(Object error); + + /// No description provided for @registerNewAccount. + /// + /// In en, this message translates to: + /// **'Register New Account'** + String get registerNewAccount; + + /// No description provided for @tryRestoreAgain. + /// + /// In en, this message translates to: + /// **'Try Restore Again'** + String get tryRestoreAgain; + + /// No description provided for @registeringNewAccount. + /// + /// In en, this message translates to: + /// **'Registering new account'** + String get registeringNewAccount; + + /// No description provided for @createShortcut. + /// + /// In en, this message translates to: + /// **'Create shortcut'** + String get createShortcut; + + /// No description provided for @editShortcut. + /// + /// In en, this message translates to: + /// **'Edit shortcut'** + String get editShortcut; + + /// No description provided for @deleteShortcut. + /// + /// In en, this message translates to: + /// **'Delete shortcut'** + String get deleteShortcut; + + /// No description provided for @deleteShortcutBody. + /// + /// In en, this message translates to: + /// **'Are you sure you want to delete this shortcut?'** + String get deleteShortcutBody; + + /// No description provided for @updateShortcut. + /// + /// In en, this message translates to: + /// **'Update shortcut'** + String get updateShortcut; + + /// No description provided for @selectEmoji. + /// + /// In en, this message translates to: + /// **'Select Emoji'** + String get selectEmoji; + + /// No description provided for @errorEmojiUsedOrInvalid. + /// + /// In en, this message translates to: + /// **'Emoji already used or invalid'** + String get errorEmojiUsedOrInvalid; } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index d1cfd1c8..2f155db8 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -277,6 +277,13 @@ class AppLocalizationsDe extends AppLocalizations { @override String get settingsNotification => 'Benachrichtigung'; + @override + String get settingsNotifyPermission => 'Benachrichtigungsberechtigung'; + + @override + String get settingsNotifyPermissionDesc => + 'Systemeinstellungen öffnen, um Push-Benachrichtigungen zu erlauben.'; + @override String get settingsNotifyTroubleshooting => 'Fehlersuche'; @@ -658,12 +665,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get open => 'Offene'; - @override - String get createVoucher => 'Gutschein kaufen'; - - @override - String get redeemVoucher => 'Gutschein einlösen'; - @override String get buy => 'Kaufen'; @@ -725,13 +726,10 @@ class AppLocalizationsDe extends AppLocalizations { 'Aufgrund des Sicherheitssystems von twonly gibt es (derzeit) keine Funktion zur Wiederherstellung des Passworts. Daher musst du dir dein Passwort merken oder, besser noch, aufschreiben.'; @override - String get backupServer => 'Server'; + String get backupIdentityHeader => 'Identität'; @override - String get backupMaxBackupSize => 'max. Backup-Größe'; - - @override - String get backupStorageRetention => 'Speicheraufbewahrung'; + String get backupArchiveHeader => 'Kontakte, Einstellungen und Nachrichten'; @override String get backupLastBackupDate => 'Letztes Backup'; @@ -742,9 +740,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get backupLastBackupResult => 'Ergebnis'; - @override - String get backupData => 'Daten-Backup'; - @override String get backupInsecurePassword => 'Unsicheres Passwort'; @@ -777,24 +772,11 @@ class AppLocalizationsDe extends AppLocalizations { @override String get backupPasswordRequirement => - 'Das Passwort muss mindestens 8 Zeichen lang sein.'; - - @override - String get backupExpertSettings => 'Experteneinstellungen'; + 'Das Passwort muss mindestens 10 Zeichen lang sein.'; @override String get backupEnableBackup => 'Automatische Sicherung aktivieren'; - @override - String get backupOwnServerDesc => - 'Speichere dein twonly Backup auf einem Server deiner Wahl.'; - - @override - String get backupUseOwnServer => 'Server verwenden'; - - @override - String get backupResetServer => 'Standardserver verwenden'; - @override String get backupTwonlySaveNow => 'Jetzt speichern'; @@ -1271,9 +1253,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get openYourOwnQRcode => 'Eigenen QR-Code öffnen'; - @override - String get skipForNow => 'Vorerst überspringen'; - @override String get finishSetupCardTitle => 'Profil vervollständigen'; @@ -1284,6 +1263,16 @@ class AppLocalizationsDe extends AppLocalizations { @override String get finishSetupCardAction => 'Setup fortsetzen'; + @override + String get missingBackupCardTitle => 'Backup einrichten'; + + @override + String get missingBackupCardDesc => + 'Wir haben den Backup-Mechanismus verbessert, weshalb du ihn erneut einrichten musst.'; + + @override + String get missingBackupCardAction => 'Jetzt einrichten'; + @override String get onboardingFinishLater => 'Später abschließen'; @@ -1714,11 +1703,81 @@ class AppLocalizationsDe extends AppLocalizations { @override String makerChangedUsername(Object maker, Object oldName, Object newName) { - return '$maker hat seinen Benutzernamen von $oldName zu $newName geändert.'; + return '$maker hat den Benutzernamen von $oldName zu $newName geändert.'; } @override String makerChangedDisplayName(Object maker, Object oldName, Object newName) { - return '$maker hat seinen Anzeigenamen von $oldName zu $newName geändert.'; + return '$maker hat den Anzeigenamen von $oldName zu $newName geändert.'; } + + @override + String get recoverErrorNoInternet => + 'Keine Internetverbindung. Bitte überprüfe deine Netzwerkverbindung und versuche es erneut.'; + + @override + String get recoverErrorUsernameNotValid => + 'Der eingegebene Benutzername ist ungültig oder existiert nicht.'; + + @override + String get recoverErrorPasswordInvalid => + 'Das eingegebene Passwort ist falsch.'; + + @override + String get recoverErrorTryAgainLater => + 'Der Server ist derzeit nicht erreichbar. Bitte versuche es später erneut.'; + + @override + String get recoverErrorUnknown => + 'Ein unbekannter Fehler ist aufgetreten. Bitte versuche es erneut.'; + + @override + String get recoverSuccessTitle => 'Backup erfolgreich wiederhergestellt.'; + + @override + String get recoverSuccessBody => 'Klicke hier, um die App wieder zu öffnen'; + + @override + String get iosRecoveryWelcomeBack => 'Willkommen zurück'; + + @override + String get iosRecoveryPrompt => + 'Wir haben eine zuvor gesicherte twonly-Identität auf diesem Gerät erkannt. Möchtest du deine Kontakte, Nachrichten und Einstellungen automatisch aus deinem Cloud-Archiv herunterladen und wiederherstellen?'; + + @override + String iosRecoveryNoBackupFound(Object error) { + return 'Für dieses Gerät konnte kein Backup-Archiv vom Server abgerufen werden.\n\nFehler: $error\n\nBitte fahre mit der Registrierung eines neuen twonly-Kontos fort.'; + } + + @override + String get registerNewAccount => 'Neues Konto registrieren'; + + @override + String get tryRestoreAgain => 'Wiederherstellung erneut versuchen'; + + @override + String get registeringNewAccount => 'Neues Konto wird registriert'; + + @override + String get createShortcut => 'Shortcut erstellen'; + + @override + String get editShortcut => 'Shortcut bearbeiten'; + + @override + String get deleteShortcut => 'Shortcut löschen'; + + @override + String get deleteShortcutBody => + 'Bist du sicher, dass du diesen Shortcut löschen möchtest?'; + + @override + String get updateShortcut => 'Shortcut aktualisieren'; + + @override + String get selectEmoji => 'Emoji auswählen'; + + @override + String get errorEmojiUsedOrInvalid => + 'Emoji wird bereits verwendet oder ist ungültig'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 555d59c5..330e9c77 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -273,6 +273,13 @@ class AppLocalizationsEn extends AppLocalizations { @override String get settingsNotification => 'Notification'; + @override + String get settingsNotifyPermission => 'Notification permissions'; + + @override + String get settingsNotifyPermissionDesc => + 'Open system settings to allow push notifications.'; + @override String get settingsNotifyTroubleshooting => 'Troubleshooting'; @@ -652,12 +659,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get open => 'Open'; - @override - String get createVoucher => 'Buy voucher'; - - @override - String get redeemVoucher => 'Redeem voucher'; - @override String get buy => 'Buy'; @@ -719,13 +720,10 @@ class AppLocalizationsEn extends AppLocalizations { 'Due to twonly\'s security system, there is (currently) no password recovery function. Therefore, you must remember your password or, better yet, write it down.'; @override - String get backupServer => 'Server'; + String get backupIdentityHeader => 'Identity'; @override - String get backupMaxBackupSize => 'max. backup size'; - - @override - String get backupStorageRetention => 'Storage retention'; + String get backupArchiveHeader => 'Contacts, Settings and Messages'; @override String get backupLastBackupDate => 'Last backup'; @@ -736,9 +734,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get backupLastBackupResult => 'Result'; - @override - String get backupData => 'Data-Backup'; - @override String get backupInsecurePassword => 'Insecure password'; @@ -771,24 +766,11 @@ class AppLocalizationsEn extends AppLocalizations { @override String get backupPasswordRequirement => - 'Password must be at least 8 characters long.'; - - @override - String get backupExpertSettings => 'Expert settings'; + 'Password must be at least 10 characters long.'; @override String get backupEnableBackup => 'Activate automatic backup'; - @override - String get backupOwnServerDesc => - 'Save your twonly Backup at twonly or on any server of your choice.'; - - @override - String get backupUseOwnServer => 'Use server'; - - @override - String get backupResetServer => 'Use standard server'; - @override String get backupTwonlySaveNow => 'Save now'; @@ -1262,9 +1244,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get openYourOwnQRcode => 'Open your own QR code'; - @override - String get skipForNow => 'Skip for now'; - @override String get finishSetupCardTitle => 'Complete your profile'; @@ -1275,6 +1254,16 @@ class AppLocalizationsEn extends AppLocalizations { @override String get finishSetupCardAction => 'Resume Setup'; + @override + String get missingBackupCardTitle => 'Setup backup'; + + @override + String get missingBackupCardDesc => + 'We have improved the backup mechanism, which requires you to set it up again.'; + + @override + String get missingBackupCardAction => 'Set up now'; + @override String get onboardingFinishLater => 'Finish later'; @@ -1706,4 +1695,73 @@ class AppLocalizationsEn extends AppLocalizations { String makerChangedDisplayName(Object maker, Object oldName, Object newName) { return '$maker changed their display name from $oldName to $newName.'; } + + @override + String get recoverErrorNoInternet => + 'No internet connection. Please check your network and try again.'; + + @override + String get recoverErrorUsernameNotValid => + 'The username provided is not valid or does not exist.'; + + @override + String get recoverErrorPasswordInvalid => + 'The password provided is incorrect.'; + + @override + String get recoverErrorTryAgainLater => + 'The server is currently unavailable. Please try again later.'; + + @override + String get recoverErrorUnknown => + 'An unknown error occurred. Please try again.'; + + @override + String get recoverSuccessTitle => 'Backup successfully recovered.'; + + @override + String get recoverSuccessBody => 'Click here to open the app again'; + + @override + String get iosRecoveryWelcomeBack => 'Welcome Back'; + + @override + String get iosRecoveryPrompt => + 'We detected a previously secured twonly identity on this device. Would you like to automatically download and restore your contacts, messages, and settings from your cloud archive?'; + + @override + String iosRecoveryNoBackupFound(Object error) { + return 'No backup archive could be retrieved from the server for this device.\n\nError: $error\n\nPlease proceed to register a new twonly account.'; + } + + @override + String get registerNewAccount => 'Register New Account'; + + @override + String get tryRestoreAgain => 'Try Restore Again'; + + @override + String get registeringNewAccount => 'Registering new account'; + + @override + String get createShortcut => 'Create shortcut'; + + @override + String get editShortcut => 'Edit shortcut'; + + @override + String get deleteShortcut => 'Delete shortcut'; + + @override + String get deleteShortcutBody => + 'Are you sure you want to delete this shortcut?'; + + @override + String get updateShortcut => 'Update shortcut'; + + @override + String get selectEmoji => 'Select Emoji'; + + @override + String get errorEmojiUsedOrInvalid => 'Emoji already used or invalid'; } diff --git a/lib/src/localization/translations b/lib/src/localization/translations index fccd366e..9218abf0 160000 --- a/lib/src/localization/translations +++ b/lib/src/localization/translations @@ -1 +1 @@ -Subproject commit fccd366e119671b96730cb09d8bb8aa1057bd1c5 +Subproject commit 9218abf0961c072edd2f8aa5035d06a331b853c6 diff --git a/lib/src/model/json/backup.model.dart b/lib/src/model/json/backup.model.dart new file mode 100644 index 00000000..e40f0738 --- /dev/null +++ b/lib/src/model/json/backup.model.dart @@ -0,0 +1,51 @@ +import 'package:json_annotation/json_annotation.dart'; +part 'backup.model.g.dart'; + +enum LastBackupUploadState { none, pending, failed, success } + +@JsonSerializable() +class CurrentBackupStatus { + CurrentBackupStatus(); + factory CurrentBackupStatus.fromJson(Map json) => + _$CurrentBackupStatusFromJson(json); + + LastBackupUploadState identityState = LastBackupUploadState.none; + DateTime? identityLastSuccessFull; + int? identitySize; + + LastBackupUploadState archiveState = LastBackupUploadState.none; + + DateTime? archiveLastSuccessFull; + int? archiveSize; + + Map toJson() => _$CurrentBackupStatusToJson(this); +} + +enum BackupRecoveryState { + // The userId was loaded from the server and the user is asked to enter his password. + identityBackupStarted, + // -> Download identity, replace keymanager + + // Identity was downloaded and Keymanager was updated + archiveBackupStarted, + // -> Download archive, replace files, restart app +} + +@JsonSerializable() +class BackupRecovery { + BackupRecovery({ + required this.username, + required this.password, + required this.userId, + }); + + factory BackupRecovery.fromJson(Map json) => + _$BackupRecoveryFromJson(json); + + String username; + String password; + int userId; + BackupRecoveryState state = BackupRecoveryState.identityBackupStarted; + + Map toJson() => _$BackupRecoveryToJson(this); +} diff --git a/lib/src/model/json/backup.model.g.dart b/lib/src/model/json/backup.model.g.dart new file mode 100644 index 00000000..7693b0e3 --- /dev/null +++ b/lib/src/model/json/backup.model.g.dart @@ -0,0 +1,65 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'backup.model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +CurrentBackupStatus _$CurrentBackupStatusFromJson(Map json) => + CurrentBackupStatus() + ..identityState = $enumDecode( + _$LastBackupUploadStateEnumMap, + json['identityState'], + ) + ..identityLastSuccessFull = json['identityLastSuccessFull'] == null + ? null + : DateTime.parse(json['identityLastSuccessFull'] as String) + ..identitySize = (json['identitySize'] as num?)?.toInt() + ..archiveState = $enumDecode( + _$LastBackupUploadStateEnumMap, + json['archiveState'], + ) + ..archiveLastSuccessFull = json['archiveLastSuccessFull'] == null + ? null + : DateTime.parse(json['archiveLastSuccessFull'] as String) + ..archiveSize = (json['archiveSize'] as num?)?.toInt(); + +Map _$CurrentBackupStatusToJson( + CurrentBackupStatus instance, +) => { + 'identityState': _$LastBackupUploadStateEnumMap[instance.identityState]!, + 'identityLastSuccessFull': instance.identityLastSuccessFull + ?.toIso8601String(), + 'identitySize': instance.identitySize, + 'archiveState': _$LastBackupUploadStateEnumMap[instance.archiveState]!, + 'archiveLastSuccessFull': instance.archiveLastSuccessFull?.toIso8601String(), + 'archiveSize': instance.archiveSize, +}; + +const _$LastBackupUploadStateEnumMap = { + LastBackupUploadState.none: 'none', + LastBackupUploadState.pending: 'pending', + LastBackupUploadState.failed: 'failed', + LastBackupUploadState.success: 'success', +}; + +BackupRecovery _$BackupRecoveryFromJson(Map json) => + BackupRecovery( + username: json['username'] as String, + password: json['password'] as String, + userId: (json['userId'] as num).toInt(), + )..state = $enumDecode(_$BackupRecoveryStateEnumMap, json['state']); + +Map _$BackupRecoveryToJson(BackupRecovery instance) => + { + 'username': instance.username, + 'password': instance.password, + 'userId': instance.userId, + 'state': _$BackupRecoveryStateEnumMap[instance.state]!, + }; + +const _$BackupRecoveryStateEnumMap = { + BackupRecoveryState.identityBackupStarted: 'identityBackupStarted', + BackupRecoveryState.archiveBackupStarted: 'archiveBackupStarted', +}; diff --git a/lib/src/model/json/userdata.model.dart b/lib/src/model/json/userdata.model.dart index 52b9857b..e39fac5e 100644 --- a/lib/src/model/json/userdata.model.dart +++ b/lib/src/model/json/userdata.model.dart @@ -128,12 +128,20 @@ class UserData { @JsonKey(defaultValue: true) bool updateFCMToken = true; + @JsonKey(defaultValue: true) + bool canUseLoginTokenForAuth = true; + // --- BACKUP --- - DateTime? nextTimeToShowBackupNotice; - BackupServer? backupServer; + @Deprecated('Use the secure storage in rust') TwonlySafeBackup? twonlySafeBackup; + @JsonKey(defaultValue: false) + bool isBackupEnabled = false; + + // Used for push notifcation via FCM. + String? fcmToken; + // For my master thesis I want to create a anonymous user study: // - users in the "Tester" Plan can, if they want, take part of the user study @@ -175,19 +183,3 @@ class TwonlySafeBackup { List encryptionKey; Map toJson() => _$TwonlySafeBackupToJson(this); } - -@JsonSerializable() -class BackupServer { - BackupServer({ - required this.serverUrl, - required this.retentionDays, - required this.maxBackupBytes, - }); - factory BackupServer.fromJson(Map json) => - _$BackupServerFromJson(json); - - String serverUrl; - int retentionDays; - int maxBackupBytes; - Map toJson() => _$BackupServerToJson(this); -} diff --git a/lib/src/model/json/userdata.model.g.dart b/lib/src/model/json/userdata.model.g.dart index 3b028b00..10a70f94 100644 --- a/lib/src/model/json/userdata.model.g.dart +++ b/lib/src/model/json/userdata.model.g.dart @@ -71,6 +71,8 @@ UserData _$UserDataFromJson(Map json) => json['userDiscoveryRequiresManualApproval'] as bool? ?? false ..userDiscoverySharePromotion = json['userDiscoverySharePromotion'] as bool? ?? true + ..userDiscoveryInitializationError = + json['userDiscoveryInitializationError'] as bool? ?? false ..currentPreKeyIndexStart = (json['currentPreKeyIndexStart'] as num?)?.toInt() ?? 100000 ..currentSignedPreKeyIndexStart = @@ -80,17 +82,15 @@ UserData _$UserDataFromJson(Map json) => .toList() ..hideChangeLog = json['hideChangeLog'] as bool? ?? true ..updateFCMToken = json['updateFCMToken'] as bool? ?? true - ..nextTimeToShowBackupNotice = json['nextTimeToShowBackupNotice'] == null - ? null - : DateTime.parse(json['nextTimeToShowBackupNotice'] as String) - ..backupServer = json['backupServer'] == null - ? null - : BackupServer.fromJson(json['backupServer'] as Map) + ..canUseLoginTokenForAuth = + json['canUseLoginTokenForAuth'] as bool? ?? true ..twonlySafeBackup = json['twonlySafeBackup'] == null ? null : TwonlySafeBackup.fromJson( json['twonlySafeBackup'] as Map, ) + ..isBackupEnabled = json['isBackupEnabled'] as bool? ?? false + ..fcmToken = json['fcmToken'] as String? ..askedForUserStudyPermission = json['askedForUserStudyPermission'] as bool? ?? false ..userStudyParticipantsToken = @@ -142,15 +142,16 @@ Map _$UserDataToJson(UserData instance) => { 'userDiscoveryRequiresManualApproval': instance.userDiscoveryRequiresManualApproval, 'userDiscoverySharePromotion': instance.userDiscoverySharePromotion, + 'userDiscoveryInitializationError': instance.userDiscoveryInitializationError, 'currentPreKeyIndexStart': instance.currentPreKeyIndexStart, 'currentSignedPreKeyIndexStart': instance.currentSignedPreKeyIndexStart, 'lastChangeLogHash': instance.lastChangeLogHash, 'hideChangeLog': instance.hideChangeLog, 'updateFCMToken': instance.updateFCMToken, - 'nextTimeToShowBackupNotice': instance.nextTimeToShowBackupNotice - ?.toIso8601String(), - 'backupServer': instance.backupServer, + 'canUseLoginTokenForAuth': instance.canUseLoginTokenForAuth, 'twonlySafeBackup': instance.twonlySafeBackup, + 'isBackupEnabled': instance.isBackupEnabled, + 'fcmToken': instance.fcmToken, 'askedForUserStudyPermission': instance.askedForUserStudyPermission, 'userStudyParticipantsToken': instance.userStudyParticipantsToken, 'userStudyCountNewFriendsViaSuggestion': @@ -201,16 +202,3 @@ const _$LastBackupUploadStateEnumMap = { LastBackupUploadState.failed: 'failed', LastBackupUploadState.success: 'success', }; - -BackupServer _$BackupServerFromJson(Map json) => BackupServer( - serverUrl: json['serverUrl'] as String, - retentionDays: (json['retentionDays'] as num).toInt(), - maxBackupBytes: (json['maxBackupBytes'] as num).toInt(), -); - -Map _$BackupServerToJson(BackupServer instance) => - { - 'serverUrl': instance.serverUrl, - 'retentionDays': instance.retentionDays, - 'maxBackupBytes': instance.maxBackupBytes, - }; diff --git a/lib/src/model/protobuf/api/websocket/client_to_server.pb.dart b/lib/src/model/protobuf/api/websocket/client_to_server.pb.dart index f5547c51..dc424110 100644 --- a/lib/src/model/protobuf/api/websocket/client_to_server.pb.dart +++ b/lib/src/model/protobuf/api/websocket/client_to_server.pb.dart @@ -258,6 +258,7 @@ class Handshake_Register extends $pb.GeneratedMessage { $core.bool? isIos, $core.String? langCode, $fixnum.Int64? proofOfWork, + $core.List<$core.int>? loginToken, }) { final result = create(); if (username != null) result.username = username; @@ -271,6 +272,7 @@ class Handshake_Register extends $pb.GeneratedMessage { if (isIos != null) result.isIos = isIos; if (langCode != null) result.langCode = langCode; if (proofOfWork != null) result.proofOfWork = proofOfWork; + if (loginToken != null) result.loginToken = loginToken; return result; } @@ -301,6 +303,8 @@ class Handshake_Register extends $pb.GeneratedMessage { ..aOB(8, _omitFieldNames ? '' : 'isIos') ..aOS(9, _omitFieldNames ? '' : 'langCode') ..aInt64(10, _omitFieldNames ? '' : 'proofOfWork') + ..a<$core.List<$core.int>>( + 11, _omitFieldNames ? '' : 'loginToken', $pb.PbFieldType.OY) ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -412,6 +416,15 @@ class Handshake_Register extends $pb.GeneratedMessage { $core.bool hasProofOfWork() => $_has(9); @$pb.TagNumber(10) void clearProofOfWork() => $_clearField(10); + + @$pb.TagNumber(11) + $core.List<$core.int> get loginToken => $_getN(10); + @$pb.TagNumber(11) + set loginToken($core.List<$core.int> value) => $_setBytes(10, value); + @$pb.TagNumber(11) + $core.bool hasLoginToken() => $_has(10); + @$pb.TagNumber(11) + void clearLoginToken() => $_clearField(11); } class Handshake_GetAuthChallenge extends $pb.GeneratedMessage { @@ -455,6 +468,64 @@ class Handshake_GetAuthChallenge extends $pb.GeneratedMessage { static Handshake_GetAuthChallenge? _defaultInstance; } +class Handshake_GetUserIdByUsername extends $pb.GeneratedMessage { + factory Handshake_GetUserIdByUsername({ + $core.String? username, + }) { + final result = create(); + if (username != null) result.username = username; + return result; + } + + Handshake_GetUserIdByUsername._(); + + factory Handshake_GetUserIdByUsername.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory Handshake_GetUserIdByUsername.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Handshake.GetUserIdByUsername', + package: + const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'username') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Handshake_GetUserIdByUsername clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Handshake_GetUserIdByUsername copyWith( + void Function(Handshake_GetUserIdByUsername) updates) => + super.copyWith( + (message) => updates(message as Handshake_GetUserIdByUsername)) + as Handshake_GetUserIdByUsername; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Handshake_GetUserIdByUsername create() => + Handshake_GetUserIdByUsername._(); + @$core.override + Handshake_GetUserIdByUsername createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static Handshake_GetUserIdByUsername getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static Handshake_GetUserIdByUsername? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get username => $_getSZ(0); + @$pb.TagNumber(1) + set username($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasUsername() => $_has(0); + @$pb.TagNumber(1) + void clearUsername() => $_clearField(1); +} + class Handshake_GetAuthToken extends $pb.GeneratedMessage { factory Handshake_GetAuthToken({ $fixnum.Int64? userId, @@ -629,12 +700,123 @@ class Handshake_Authenticate extends $pb.GeneratedMessage { void clearInBackground() => $_clearField(5); } +class Handshake_AuthenticateWithLoginToken extends $pb.GeneratedMessage { + factory Handshake_AuthenticateWithLoginToken({ + $fixnum.Int64? userId, + $core.List<$core.int>? secretLoginToken, + $core.String? appVersion, + $fixnum.Int64? deviceId, + $core.bool? inBackground, + }) { + final result = create(); + if (userId != null) result.userId = userId; + if (secretLoginToken != null) result.secretLoginToken = secretLoginToken; + if (appVersion != null) result.appVersion = appVersion; + if (deviceId != null) result.deviceId = deviceId; + if (inBackground != null) result.inBackground = inBackground; + return result; + } + + Handshake_AuthenticateWithLoginToken._(); + + factory Handshake_AuthenticateWithLoginToken.fromBuffer( + $core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory Handshake_AuthenticateWithLoginToken.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Handshake.AuthenticateWithLoginToken', + package: + const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'), + createEmptyInstance: create) + ..aInt64(1, _omitFieldNames ? '' : 'userId') + ..a<$core.List<$core.int>>( + 2, _omitFieldNames ? '' : 'secretLoginToken', $pb.PbFieldType.OY) + ..aOS(3, _omitFieldNames ? '' : 'appVersion') + ..aInt64(4, _omitFieldNames ? '' : 'deviceId') + ..aOB(5, _omitFieldNames ? '' : 'inBackground') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Handshake_AuthenticateWithLoginToken clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Handshake_AuthenticateWithLoginToken copyWith( + void Function(Handshake_AuthenticateWithLoginToken) updates) => + super.copyWith((message) => + updates(message as Handshake_AuthenticateWithLoginToken)) + as Handshake_AuthenticateWithLoginToken; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Handshake_AuthenticateWithLoginToken create() => + Handshake_AuthenticateWithLoginToken._(); + @$core.override + Handshake_AuthenticateWithLoginToken createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static Handshake_AuthenticateWithLoginToken getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor< + Handshake_AuthenticateWithLoginToken>(create); + static Handshake_AuthenticateWithLoginToken? _defaultInstance; + + @$pb.TagNumber(1) + $fixnum.Int64 get userId => $_getI64(0); + @$pb.TagNumber(1) + set userId($fixnum.Int64 value) => $_setInt64(0, value); + @$pb.TagNumber(1) + $core.bool hasUserId() => $_has(0); + @$pb.TagNumber(1) + void clearUserId() => $_clearField(1); + + @$pb.TagNumber(2) + $core.List<$core.int> get secretLoginToken => $_getN(1); + @$pb.TagNumber(2) + set secretLoginToken($core.List<$core.int> value) => $_setBytes(1, value); + @$pb.TagNumber(2) + $core.bool hasSecretLoginToken() => $_has(1); + @$pb.TagNumber(2) + void clearSecretLoginToken() => $_clearField(2); + + @$pb.TagNumber(3) + $core.String get appVersion => $_getSZ(2); + @$pb.TagNumber(3) + set appVersion($core.String value) => $_setString(2, value); + @$pb.TagNumber(3) + $core.bool hasAppVersion() => $_has(2); + @$pb.TagNumber(3) + void clearAppVersion() => $_clearField(3); + + @$pb.TagNumber(4) + $fixnum.Int64 get deviceId => $_getI64(3); + @$pb.TagNumber(4) + set deviceId($fixnum.Int64 value) => $_setInt64(3, value); + @$pb.TagNumber(4) + $core.bool hasDeviceId() => $_has(3); + @$pb.TagNumber(4) + void clearDeviceId() => $_clearField(4); + + @$pb.TagNumber(5) + $core.bool get inBackground => $_getBF(4); + @$pb.TagNumber(5) + set inBackground($core.bool value) => $_setBool(4, value); + @$pb.TagNumber(5) + $core.bool hasInBackground() => $_has(4); + @$pb.TagNumber(5) + void clearInBackground() => $_clearField(5); +} + enum Handshake_Handshake { register, getAuthChallenge, getAuthToken, authenticate, requestPOW, + authenticateWithLoginToken, + getUseridByUsername, notSet } @@ -645,6 +827,8 @@ class Handshake extends $pb.GeneratedMessage { Handshake_GetAuthToken? getAuthToken, Handshake_Authenticate? authenticate, Handshake_RequestPOW? requestPOW, + Handshake_AuthenticateWithLoginToken? authenticateWithLoginToken, + Handshake_GetUserIdByUsername? getUseridByUsername, }) { final result = create(); if (register != null) result.register = register; @@ -652,6 +836,10 @@ class Handshake extends $pb.GeneratedMessage { if (getAuthToken != null) result.getAuthToken = getAuthToken; if (authenticate != null) result.authenticate = authenticate; if (requestPOW != null) result.requestPOW = requestPOW; + if (authenticateWithLoginToken != null) + result.authenticateWithLoginToken = authenticateWithLoginToken; + if (getUseridByUsername != null) + result.getUseridByUsername = getUseridByUsername; return result; } @@ -671,6 +859,8 @@ class Handshake extends $pb.GeneratedMessage { 3: Handshake_Handshake.getAuthToken, 4: Handshake_Handshake.authenticate, 5: Handshake_Handshake.requestPOW, + 6: Handshake_Handshake.authenticateWithLoginToken, + 7: Handshake_Handshake.getUseridByUsername, 0: Handshake_Handshake.notSet }; static final $pb.BuilderInfo _i = $pb.BuilderInfo( @@ -678,7 +868,7 @@ class Handshake extends $pb.GeneratedMessage { package: const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'), createEmptyInstance: create) - ..oo(0, [1, 2, 3, 4, 5]) + ..oo(0, [1, 2, 3, 4, 5, 6, 7]) ..aOM(1, _omitFieldNames ? '' : 'register', subBuilder: Handshake_Register.create) ..aOM( @@ -691,6 +881,12 @@ class Handshake extends $pb.GeneratedMessage { subBuilder: Handshake_Authenticate.create) ..aOM(5, _omitFieldNames ? '' : 'requestPOW', protoName: 'requestPOW', subBuilder: Handshake_RequestPOW.create) + ..aOM( + 6, _omitFieldNames ? '' : 'authenticateWithLoginToken', + subBuilder: Handshake_AuthenticateWithLoginToken.create) + ..aOM( + 7, _omitFieldNames ? '' : 'getUseridByUsername', + subBuilder: Handshake_GetUserIdByUsername.create) ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -716,6 +912,8 @@ class Handshake extends $pb.GeneratedMessage { @$pb.TagNumber(3) @$pb.TagNumber(4) @$pb.TagNumber(5) + @$pb.TagNumber(6) + @$pb.TagNumber(7) Handshake_Handshake whichHandshake() => _Handshake_HandshakeByTag[$_whichOneof(0)]!; @$pb.TagNumber(1) @@ -723,6 +921,8 @@ class Handshake extends $pb.GeneratedMessage { @$pb.TagNumber(3) @$pb.TagNumber(4) @$pb.TagNumber(5) + @$pb.TagNumber(6) + @$pb.TagNumber(7) void clearHandshake() => $_clearField($_whichOneof(0)); @$pb.TagNumber(1) @@ -780,6 +980,32 @@ class Handshake extends $pb.GeneratedMessage { void clearRequestPOW() => $_clearField(5); @$pb.TagNumber(5) Handshake_RequestPOW ensureRequestPOW() => $_ensure(4); + + @$pb.TagNumber(6) + Handshake_AuthenticateWithLoginToken get authenticateWithLoginToken => + $_getN(5); + @$pb.TagNumber(6) + set authenticateWithLoginToken(Handshake_AuthenticateWithLoginToken value) => + $_setField(6, value); + @$pb.TagNumber(6) + $core.bool hasAuthenticateWithLoginToken() => $_has(5); + @$pb.TagNumber(6) + void clearAuthenticateWithLoginToken() => $_clearField(6); + @$pb.TagNumber(6) + Handshake_AuthenticateWithLoginToken ensureAuthenticateWithLoginToken() => + $_ensure(5); + + @$pb.TagNumber(7) + Handshake_GetUserIdByUsername get getUseridByUsername => $_getN(6); + @$pb.TagNumber(7) + set getUseridByUsername(Handshake_GetUserIdByUsername value) => + $_setField(7, value); + @$pb.TagNumber(7) + $core.bool hasGetUseridByUsername() => $_has(6); + @$pb.TagNumber(7) + void clearGetUseridByUsername() => $_clearField(7); + @$pb.TagNumber(7) + Handshake_GetUserIdByUsername ensureGetUseridByUsername() => $_ensure(6); } class ApplicationData_TextMessage extends $pb.GeneratedMessage { @@ -1102,350 +1328,6 @@ class ApplicationData_GetUserById extends $pb.GeneratedMessage { void clearUserId() => $_clearField(1); } -class ApplicationData_RedeemVoucher extends $pb.GeneratedMessage { - factory ApplicationData_RedeemVoucher({ - $core.String? voucher, - }) { - final result = create(); - if (voucher != null) result.voucher = voucher; - return result; - } - - ApplicationData_RedeemVoucher._(); - - factory ApplicationData_RedeemVoucher.fromBuffer($core.List<$core.int> data, - [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(data, registry); - factory ApplicationData_RedeemVoucher.fromJson($core.String json, - [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(json, registry); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'ApplicationData.RedeemVoucher', - package: - const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'), - createEmptyInstance: create) - ..aOS(1, _omitFieldNames ? '' : 'voucher') - ..hasRequiredFields = false; - - @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') - ApplicationData_RedeemVoucher clone() => deepCopy(); - @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') - ApplicationData_RedeemVoucher copyWith( - void Function(ApplicationData_RedeemVoucher) updates) => - super.copyWith( - (message) => updates(message as ApplicationData_RedeemVoucher)) - as ApplicationData_RedeemVoucher; - - @$core.override - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static ApplicationData_RedeemVoucher create() => - ApplicationData_RedeemVoucher._(); - @$core.override - ApplicationData_RedeemVoucher createEmptyInstance() => create(); - @$core.pragma('dart2js:noInline') - static ApplicationData_RedeemVoucher getDefault() => _defaultInstance ??= - $pb.GeneratedMessage.$_defaultFor(create); - static ApplicationData_RedeemVoucher? _defaultInstance; - - @$pb.TagNumber(1) - $core.String get voucher => $_getSZ(0); - @$pb.TagNumber(1) - set voucher($core.String value) => $_setString(0, value); - @$pb.TagNumber(1) - $core.bool hasVoucher() => $_has(0); - @$pb.TagNumber(1) - void clearVoucher() => $_clearField(1); -} - -class ApplicationData_SwitchToPayedPlan extends $pb.GeneratedMessage { - factory ApplicationData_SwitchToPayedPlan({ - $core.String? planId, - $core.bool? payMonthly, - $core.bool? autoRenewal, - }) { - final result = create(); - if (planId != null) result.planId = planId; - if (payMonthly != null) result.payMonthly = payMonthly; - if (autoRenewal != null) result.autoRenewal = autoRenewal; - return result; - } - - ApplicationData_SwitchToPayedPlan._(); - - factory ApplicationData_SwitchToPayedPlan.fromBuffer( - $core.List<$core.int> data, - [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(data, registry); - factory ApplicationData_SwitchToPayedPlan.fromJson($core.String json, - [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(json, registry); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'ApplicationData.SwitchToPayedPlan', - package: - const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'), - createEmptyInstance: create) - ..aOS(1, _omitFieldNames ? '' : 'planId') - ..aOB(2, _omitFieldNames ? '' : 'payMonthly') - ..aOB(3, _omitFieldNames ? '' : 'autoRenewal') - ..hasRequiredFields = false; - - @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') - ApplicationData_SwitchToPayedPlan clone() => deepCopy(); - @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') - ApplicationData_SwitchToPayedPlan copyWith( - void Function(ApplicationData_SwitchToPayedPlan) updates) => - super.copyWith((message) => - updates(message as ApplicationData_SwitchToPayedPlan)) - as ApplicationData_SwitchToPayedPlan; - - @$core.override - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static ApplicationData_SwitchToPayedPlan create() => - ApplicationData_SwitchToPayedPlan._(); - @$core.override - ApplicationData_SwitchToPayedPlan createEmptyInstance() => create(); - @$core.pragma('dart2js:noInline') - static ApplicationData_SwitchToPayedPlan getDefault() => _defaultInstance ??= - $pb.GeneratedMessage.$_defaultFor( - create); - static ApplicationData_SwitchToPayedPlan? _defaultInstance; - - @$pb.TagNumber(1) - $core.String get planId => $_getSZ(0); - @$pb.TagNumber(1) - set planId($core.String value) => $_setString(0, value); - @$pb.TagNumber(1) - $core.bool hasPlanId() => $_has(0); - @$pb.TagNumber(1) - void clearPlanId() => $_clearField(1); - - @$pb.TagNumber(2) - $core.bool get payMonthly => $_getBF(1); - @$pb.TagNumber(2) - set payMonthly($core.bool value) => $_setBool(1, value); - @$pb.TagNumber(2) - $core.bool hasPayMonthly() => $_has(1); - @$pb.TagNumber(2) - void clearPayMonthly() => $_clearField(2); - - @$pb.TagNumber(3) - $core.bool get autoRenewal => $_getBF(2); - @$pb.TagNumber(3) - set autoRenewal($core.bool value) => $_setBool(2, value); - @$pb.TagNumber(3) - $core.bool hasAutoRenewal() => $_has(2); - @$pb.TagNumber(3) - void clearAutoRenewal() => $_clearField(3); -} - -class ApplicationData_UpdatePlanOptions extends $pb.GeneratedMessage { - factory ApplicationData_UpdatePlanOptions({ - $core.bool? autoRenewal, - }) { - final result = create(); - if (autoRenewal != null) result.autoRenewal = autoRenewal; - return result; - } - - ApplicationData_UpdatePlanOptions._(); - - factory ApplicationData_UpdatePlanOptions.fromBuffer( - $core.List<$core.int> data, - [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(data, registry); - factory ApplicationData_UpdatePlanOptions.fromJson($core.String json, - [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(json, registry); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'ApplicationData.UpdatePlanOptions', - package: - const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'), - createEmptyInstance: create) - ..aOB(1, _omitFieldNames ? '' : 'autoRenewal') - ..hasRequiredFields = false; - - @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') - ApplicationData_UpdatePlanOptions clone() => deepCopy(); - @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') - ApplicationData_UpdatePlanOptions copyWith( - void Function(ApplicationData_UpdatePlanOptions) updates) => - super.copyWith((message) => - updates(message as ApplicationData_UpdatePlanOptions)) - as ApplicationData_UpdatePlanOptions; - - @$core.override - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static ApplicationData_UpdatePlanOptions create() => - ApplicationData_UpdatePlanOptions._(); - @$core.override - ApplicationData_UpdatePlanOptions createEmptyInstance() => create(); - @$core.pragma('dart2js:noInline') - static ApplicationData_UpdatePlanOptions getDefault() => _defaultInstance ??= - $pb.GeneratedMessage.$_defaultFor( - create); - static ApplicationData_UpdatePlanOptions? _defaultInstance; - - @$pb.TagNumber(1) - $core.bool get autoRenewal => $_getBF(0); - @$pb.TagNumber(1) - set autoRenewal($core.bool value) => $_setBool(0, value); - @$pb.TagNumber(1) - $core.bool hasAutoRenewal() => $_has(0); - @$pb.TagNumber(1) - void clearAutoRenewal() => $_clearField(1); -} - -class ApplicationData_CreateVoucher extends $pb.GeneratedMessage { - factory ApplicationData_CreateVoucher({ - $core.int? valueCents, - }) { - final result = create(); - if (valueCents != null) result.valueCents = valueCents; - return result; - } - - ApplicationData_CreateVoucher._(); - - factory ApplicationData_CreateVoucher.fromBuffer($core.List<$core.int> data, - [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(data, registry); - factory ApplicationData_CreateVoucher.fromJson($core.String json, - [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(json, registry); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'ApplicationData.CreateVoucher', - package: - const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'), - createEmptyInstance: create) - ..aI(1, _omitFieldNames ? '' : 'valueCents', fieldType: $pb.PbFieldType.OU3) - ..hasRequiredFields = false; - - @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') - ApplicationData_CreateVoucher clone() => deepCopy(); - @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') - ApplicationData_CreateVoucher copyWith( - void Function(ApplicationData_CreateVoucher) updates) => - super.copyWith( - (message) => updates(message as ApplicationData_CreateVoucher)) - as ApplicationData_CreateVoucher; - - @$core.override - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static ApplicationData_CreateVoucher create() => - ApplicationData_CreateVoucher._(); - @$core.override - ApplicationData_CreateVoucher createEmptyInstance() => create(); - @$core.pragma('dart2js:noInline') - static ApplicationData_CreateVoucher getDefault() => _defaultInstance ??= - $pb.GeneratedMessage.$_defaultFor(create); - static ApplicationData_CreateVoucher? _defaultInstance; - - @$pb.TagNumber(1) - $core.int get valueCents => $_getIZ(0); - @$pb.TagNumber(1) - set valueCents($core.int value) => $_setUnsignedInt32(0, value); - @$pb.TagNumber(1) - $core.bool hasValueCents() => $_has(0); - @$pb.TagNumber(1) - void clearValueCents() => $_clearField(1); -} - -class ApplicationData_GetLocation extends $pb.GeneratedMessage { - factory ApplicationData_GetLocation() => create(); - - ApplicationData_GetLocation._(); - - factory ApplicationData_GetLocation.fromBuffer($core.List<$core.int> data, - [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(data, registry); - factory ApplicationData_GetLocation.fromJson($core.String json, - [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(json, registry); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'ApplicationData.GetLocation', - package: - const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'), - createEmptyInstance: create) - ..hasRequiredFields = false; - - @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') - ApplicationData_GetLocation clone() => deepCopy(); - @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') - ApplicationData_GetLocation copyWith( - void Function(ApplicationData_GetLocation) updates) => - super.copyWith( - (message) => updates(message as ApplicationData_GetLocation)) - as ApplicationData_GetLocation; - - @$core.override - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static ApplicationData_GetLocation create() => - ApplicationData_GetLocation._(); - @$core.override - ApplicationData_GetLocation createEmptyInstance() => create(); - @$core.pragma('dart2js:noInline') - static ApplicationData_GetLocation getDefault() => _defaultInstance ??= - $pb.GeneratedMessage.$_defaultFor(create); - static ApplicationData_GetLocation? _defaultInstance; -} - -class ApplicationData_GetVouchers extends $pb.GeneratedMessage { - factory ApplicationData_GetVouchers() => create(); - - ApplicationData_GetVouchers._(); - - factory ApplicationData_GetVouchers.fromBuffer($core.List<$core.int> data, - [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(data, registry); - factory ApplicationData_GetVouchers.fromJson($core.String json, - [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(json, registry); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'ApplicationData.GetVouchers', - package: - const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'), - createEmptyInstance: create) - ..hasRequiredFields = false; - - @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') - ApplicationData_GetVouchers clone() => deepCopy(); - @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') - ApplicationData_GetVouchers copyWith( - void Function(ApplicationData_GetVouchers) updates) => - super.copyWith( - (message) => updates(message as ApplicationData_GetVouchers)) - as ApplicationData_GetVouchers; - - @$core.override - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static ApplicationData_GetVouchers create() => - ApplicationData_GetVouchers._(); - @$core.override - ApplicationData_GetVouchers createEmptyInstance() => create(); - @$core.pragma('dart2js:noInline') - static ApplicationData_GetVouchers getDefault() => _defaultInstance ??= - $pb.GeneratedMessage.$_defaultFor(create); - static ApplicationData_GetVouchers? _defaultInstance; -} - class ApplicationData_GetAvailablePlans extends $pb.GeneratedMessage { factory ApplicationData_GetAvailablePlans() => create(); @@ -1578,66 +1460,6 @@ class ApplicationData_GetCurrentPlanInfos extends $pb.GeneratedMessage { static ApplicationData_GetCurrentPlanInfos? _defaultInstance; } -class ApplicationData_RedeemAdditionalCode extends $pb.GeneratedMessage { - factory ApplicationData_RedeemAdditionalCode({ - $core.String? inviteCode, - }) { - final result = create(); - if (inviteCode != null) result.inviteCode = inviteCode; - return result; - } - - ApplicationData_RedeemAdditionalCode._(); - - factory ApplicationData_RedeemAdditionalCode.fromBuffer( - $core.List<$core.int> data, - [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(data, registry); - factory ApplicationData_RedeemAdditionalCode.fromJson($core.String json, - [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(json, registry); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'ApplicationData.RedeemAdditionalCode', - package: - const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'), - createEmptyInstance: create) - ..aOS(2, _omitFieldNames ? '' : 'inviteCode') - ..hasRequiredFields = false; - - @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') - ApplicationData_RedeemAdditionalCode clone() => deepCopy(); - @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') - ApplicationData_RedeemAdditionalCode copyWith( - void Function(ApplicationData_RedeemAdditionalCode) updates) => - super.copyWith((message) => - updates(message as ApplicationData_RedeemAdditionalCode)) - as ApplicationData_RedeemAdditionalCode; - - @$core.override - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static ApplicationData_RedeemAdditionalCode create() => - ApplicationData_RedeemAdditionalCode._(); - @$core.override - ApplicationData_RedeemAdditionalCode createEmptyInstance() => create(); - @$core.pragma('dart2js:noInline') - static ApplicationData_RedeemAdditionalCode getDefault() => - _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor< - ApplicationData_RedeemAdditionalCode>(create); - static ApplicationData_RedeemAdditionalCode? _defaultInstance; - - @$pb.TagNumber(2) - $core.String get inviteCode => $_getSZ(0); - @$pb.TagNumber(2) - set inviteCode($core.String value) => $_setString(0, value); - @$pb.TagNumber(2) - $core.bool hasInviteCode() => $_has(0); - @$pb.TagNumber(2) - void clearInviteCode() => $_clearField(2); -} - class ApplicationData_RemoveAdditionalUser extends $pb.GeneratedMessage { factory ApplicationData_RemoveAdditionalUser({ $fixnum.Int64? userId, @@ -2260,23 +2082,123 @@ class ApplicationData_AddAdditionalUser extends $pb.GeneratedMessage { void clearUserId() => $_clearField(1); } +class ApplicationData_SetLoginToken extends $pb.GeneratedMessage { + factory ApplicationData_SetLoginToken({ + $core.List<$core.int>? loginToken, + }) { + final result = create(); + if (loginToken != null) result.loginToken = loginToken; + return result; + } + + ApplicationData_SetLoginToken._(); + + factory ApplicationData_SetLoginToken.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory ApplicationData_SetLoginToken.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'ApplicationData.SetLoginToken', + package: + const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'), + createEmptyInstance: create) + ..a<$core.List<$core.int>>( + 1, _omitFieldNames ? '' : 'loginToken', $pb.PbFieldType.OY) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ApplicationData_SetLoginToken clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ApplicationData_SetLoginToken copyWith( + void Function(ApplicationData_SetLoginToken) updates) => + super.copyWith( + (message) => updates(message as ApplicationData_SetLoginToken)) + as ApplicationData_SetLoginToken; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ApplicationData_SetLoginToken create() => + ApplicationData_SetLoginToken._(); + @$core.override + ApplicationData_SetLoginToken createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static ApplicationData_SetLoginToken getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static ApplicationData_SetLoginToken? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$core.int> get loginToken => $_getN(0); + @$pb.TagNumber(1) + set loginToken($core.List<$core.int> value) => $_setBytes(0, value); + @$pb.TagNumber(1) + $core.bool hasLoginToken() => $_has(0); + @$pb.TagNumber(1) + void clearLoginToken() => $_clearField(1); +} + +class ApplicationData_Deprecated extends $pb.GeneratedMessage { + factory ApplicationData_Deprecated() => create(); + + ApplicationData_Deprecated._(); + + factory ApplicationData_Deprecated.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory ApplicationData_Deprecated.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'ApplicationData.Deprecated', + package: + const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'), + createEmptyInstance: create) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ApplicationData_Deprecated clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ApplicationData_Deprecated copyWith( + void Function(ApplicationData_Deprecated) updates) => + super.copyWith( + (message) => updates(message as ApplicationData_Deprecated)) + as ApplicationData_Deprecated; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ApplicationData_Deprecated create() => ApplicationData_Deprecated._(); + @$core.override + ApplicationData_Deprecated createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static ApplicationData_Deprecated getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static ApplicationData_Deprecated? _defaultInstance; +} + enum ApplicationData_ApplicationData { textMessage, getUserByUsername, getPrekeysByUserId, getUserById, updateGoogleFcmToken, - getLocation, + deprecated9, getCurrentPlanInfos, - redeemVoucher, + deprecated11, getAvailablePlans, - createVoucher, - getVouchers, - switchtoPayedPlan, - getAddaccountsInvites, - redeemAdditionalCode, + deprecated13, + deprecated14, + deprecated15, + deprecated16, + deprecated17, removeAdditionalUser, - updatePlanOptions, + deprecated19, downloadDone, getSignedPrekeyByUserid, updateSignedPrekey, @@ -2286,6 +2208,7 @@ enum ApplicationData_ApplicationData { ipaPurchase, ipaForceCheck, addAdditionalUser, + setLoginToken, notSet } @@ -2296,17 +2219,17 @@ class ApplicationData extends $pb.GeneratedMessage { ApplicationData_GetPrekeysByUserId? getPrekeysByUserId, ApplicationData_GetUserById? getUserById, ApplicationData_UpdateGoogleFcmToken? updateGoogleFcmToken, - ApplicationData_GetLocation? getLocation, + ApplicationData_Deprecated? deprecated9, ApplicationData_GetCurrentPlanInfos? getCurrentPlanInfos, - ApplicationData_RedeemVoucher? redeemVoucher, + ApplicationData_Deprecated? deprecated11, ApplicationData_GetAvailablePlans? getAvailablePlans, - ApplicationData_CreateVoucher? createVoucher, - ApplicationData_GetVouchers? getVouchers, - ApplicationData_SwitchToPayedPlan? switchtoPayedPlan, - ApplicationData_GetAddAccountsInvites? getAddaccountsInvites, - ApplicationData_RedeemAdditionalCode? redeemAdditionalCode, + ApplicationData_Deprecated? deprecated13, + ApplicationData_Deprecated? deprecated14, + ApplicationData_Deprecated? deprecated15, + ApplicationData_Deprecated? deprecated16, + ApplicationData_Deprecated? deprecated17, ApplicationData_RemoveAdditionalUser? removeAdditionalUser, - ApplicationData_UpdatePlanOptions? updatePlanOptions, + ApplicationData_Deprecated? deprecated19, ApplicationData_DownloadDone? downloadDone, ApplicationData_GetSignedPreKeyByUserId? getSignedPrekeyByUserid, ApplicationData_UpdateSignedPreKey? updateSignedPrekey, @@ -2316,6 +2239,7 @@ class ApplicationData extends $pb.GeneratedMessage { ApplicationData_IPAPurchase? ipaPurchase, ApplicationData_IPAForceCheck? ipaForceCheck, ApplicationData_AddAdditionalUser? addAdditionalUser, + ApplicationData_SetLoginToken? setLoginToken, }) { final result = create(); if (textMessage != null) result.textMessage = textMessage; @@ -2325,21 +2249,19 @@ class ApplicationData extends $pb.GeneratedMessage { if (getUserById != null) result.getUserById = getUserById; if (updateGoogleFcmToken != null) result.updateGoogleFcmToken = updateGoogleFcmToken; - if (getLocation != null) result.getLocation = getLocation; + if (deprecated9 != null) result.deprecated9 = deprecated9; if (getCurrentPlanInfos != null) result.getCurrentPlanInfos = getCurrentPlanInfos; - if (redeemVoucher != null) result.redeemVoucher = redeemVoucher; + if (deprecated11 != null) result.deprecated11 = deprecated11; if (getAvailablePlans != null) result.getAvailablePlans = getAvailablePlans; - if (createVoucher != null) result.createVoucher = createVoucher; - if (getVouchers != null) result.getVouchers = getVouchers; - if (switchtoPayedPlan != null) result.switchtoPayedPlan = switchtoPayedPlan; - if (getAddaccountsInvites != null) - result.getAddaccountsInvites = getAddaccountsInvites; - if (redeemAdditionalCode != null) - result.redeemAdditionalCode = redeemAdditionalCode; + if (deprecated13 != null) result.deprecated13 = deprecated13; + if (deprecated14 != null) result.deprecated14 = deprecated14; + if (deprecated15 != null) result.deprecated15 = deprecated15; + if (deprecated16 != null) result.deprecated16 = deprecated16; + if (deprecated17 != null) result.deprecated17 = deprecated17; if (removeAdditionalUser != null) result.removeAdditionalUser = removeAdditionalUser; - if (updatePlanOptions != null) result.updatePlanOptions = updatePlanOptions; + if (deprecated19 != null) result.deprecated19 = deprecated19; if (downloadDone != null) result.downloadDone = downloadDone; if (getSignedPrekeyByUserid != null) result.getSignedPrekeyByUserid = getSignedPrekeyByUserid; @@ -2351,6 +2273,7 @@ class ApplicationData extends $pb.GeneratedMessage { if (ipaPurchase != null) result.ipaPurchase = ipaPurchase; if (ipaForceCheck != null) result.ipaForceCheck = ipaForceCheck; if (addAdditionalUser != null) result.addAdditionalUser = addAdditionalUser; + if (setLoginToken != null) result.setLoginToken = setLoginToken; return result; } @@ -2370,17 +2293,17 @@ class ApplicationData extends $pb.GeneratedMessage { 3: ApplicationData_ApplicationData.getPrekeysByUserId, 6: ApplicationData_ApplicationData.getUserById, 8: ApplicationData_ApplicationData.updateGoogleFcmToken, - 9: ApplicationData_ApplicationData.getLocation, + 9: ApplicationData_ApplicationData.deprecated9, 10: ApplicationData_ApplicationData.getCurrentPlanInfos, - 11: ApplicationData_ApplicationData.redeemVoucher, + 11: ApplicationData_ApplicationData.deprecated11, 12: ApplicationData_ApplicationData.getAvailablePlans, - 13: ApplicationData_ApplicationData.createVoucher, - 14: ApplicationData_ApplicationData.getVouchers, - 15: ApplicationData_ApplicationData.switchtoPayedPlan, - 16: ApplicationData_ApplicationData.getAddaccountsInvites, - 17: ApplicationData_ApplicationData.redeemAdditionalCode, + 13: ApplicationData_ApplicationData.deprecated13, + 14: ApplicationData_ApplicationData.deprecated14, + 15: ApplicationData_ApplicationData.deprecated15, + 16: ApplicationData_ApplicationData.deprecated16, + 17: ApplicationData_ApplicationData.deprecated17, 18: ApplicationData_ApplicationData.removeAdditionalUser, - 19: ApplicationData_ApplicationData.updatePlanOptions, + 19: ApplicationData_ApplicationData.deprecated19, 20: ApplicationData_ApplicationData.downloadDone, 22: ApplicationData_ApplicationData.getSignedPrekeyByUserid, 23: ApplicationData_ApplicationData.updateSignedPrekey, @@ -2390,6 +2313,7 @@ class ApplicationData extends $pb.GeneratedMessage { 27: ApplicationData_ApplicationData.ipaPurchase, 28: ApplicationData_ApplicationData.ipaForceCheck, 29: ApplicationData_ApplicationData.addAdditionalUser, + 30: ApplicationData_ApplicationData.setLoginToken, 0: ApplicationData_ApplicationData.notSet }; static final $pb.BuilderInfo _i = $pb.BuilderInfo( @@ -2422,7 +2346,8 @@ class ApplicationData extends $pb.GeneratedMessage { 26, 27, 28, - 29 + 29, + 30 ]) ..aOM(1, _omitFieldNames ? '' : 'textMessage', protoName: 'textMessage', @@ -2442,48 +2367,42 @@ class ApplicationData extends $pb.GeneratedMessage { 8, _omitFieldNames ? '' : 'updateGoogleFcmToken', protoName: 'updateGoogleFcmToken', subBuilder: ApplicationData_UpdateGoogleFcmToken.create) - ..aOM(9, _omitFieldNames ? '' : 'getLocation', - protoName: 'getLocation', - subBuilder: ApplicationData_GetLocation.create) + ..aOM(9, _omitFieldNames ? '' : 'deprecated9', + protoName: 'deprecated_9', + subBuilder: ApplicationData_Deprecated.create) ..aOM( 10, _omitFieldNames ? '' : 'getCurrentPlanInfos', protoName: 'getCurrentPlanInfos', subBuilder: ApplicationData_GetCurrentPlanInfos.create) - ..aOM( - 11, _omitFieldNames ? '' : 'redeemVoucher', - protoName: 'redeemVoucher', - subBuilder: ApplicationData_RedeemVoucher.create) + ..aOM(11, _omitFieldNames ? '' : 'deprecated11', + protoName: 'deprecated_11', + subBuilder: ApplicationData_Deprecated.create) ..aOM( 12, _omitFieldNames ? '' : 'getAvailablePlans', protoName: 'getAvailablePlans', subBuilder: ApplicationData_GetAvailablePlans.create) - ..aOM( - 13, _omitFieldNames ? '' : 'createVoucher', - protoName: 'createVoucher', - subBuilder: ApplicationData_CreateVoucher.create) - ..aOM(14, _omitFieldNames ? '' : 'getVouchers', - protoName: 'getVouchers', - subBuilder: ApplicationData_GetVouchers.create) - ..aOM( - 15, _omitFieldNames ? '' : 'switchtoPayedPlan', - protoName: 'switchtoPayedPlan', - subBuilder: ApplicationData_SwitchToPayedPlan.create) - ..aOM( - 16, _omitFieldNames ? '' : 'getAddaccountsInvites', - protoName: 'getAddaccountsInvites', - subBuilder: ApplicationData_GetAddAccountsInvites.create) - ..aOM( - 17, _omitFieldNames ? '' : 'redeemAdditionalCode', - protoName: 'redeemAdditionalCode', - subBuilder: ApplicationData_RedeemAdditionalCode.create) + ..aOM(13, _omitFieldNames ? '' : 'deprecated13', + protoName: 'deprecated_13', + subBuilder: ApplicationData_Deprecated.create) + ..aOM(14, _omitFieldNames ? '' : 'deprecated14', + protoName: 'deprecated_14', + subBuilder: ApplicationData_Deprecated.create) + ..aOM(15, _omitFieldNames ? '' : 'deprecated15', + protoName: 'deprecated_15', + subBuilder: ApplicationData_Deprecated.create) + ..aOM(16, _omitFieldNames ? '' : 'deprecated16', + protoName: 'deprecated_16', + subBuilder: ApplicationData_Deprecated.create) + ..aOM(17, _omitFieldNames ? '' : 'deprecated17', + protoName: 'deprecated_17', + subBuilder: ApplicationData_Deprecated.create) ..aOM( 18, _omitFieldNames ? '' : 'removeAdditionalUser', protoName: 'removeAdditionalUser', subBuilder: ApplicationData_RemoveAdditionalUser.create) - ..aOM( - 19, _omitFieldNames ? '' : 'updatePlanOptions', - protoName: 'updatePlanOptions', - subBuilder: ApplicationData_UpdatePlanOptions.create) + ..aOM(19, _omitFieldNames ? '' : 'deprecated19', + protoName: 'deprecated_19', + subBuilder: ApplicationData_Deprecated.create) ..aOM( 20, _omitFieldNames ? '' : 'downloadDone', protoName: 'downloadDone', @@ -2517,6 +2436,9 @@ class ApplicationData extends $pb.GeneratedMessage { 29, _omitFieldNames ? '' : 'addAdditionalUser', protoName: 'addAdditionalUser', subBuilder: ApplicationData_AddAdditionalUser.create) + ..aOM( + 30, _omitFieldNames ? '' : 'setLoginToken', + subBuilder: ApplicationData_SetLoginToken.create) ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -2563,6 +2485,7 @@ class ApplicationData extends $pb.GeneratedMessage { @$pb.TagNumber(27) @$pb.TagNumber(28) @$pb.TagNumber(29) + @$pb.TagNumber(30) ApplicationData_ApplicationData whichApplicationData() => _ApplicationData_ApplicationDataByTag[$_whichOneof(0)]!; @$pb.TagNumber(1) @@ -2590,6 +2513,7 @@ class ApplicationData extends $pb.GeneratedMessage { @$pb.TagNumber(27) @$pb.TagNumber(28) @$pb.TagNumber(29) + @$pb.TagNumber(30) void clearApplicationData() => $_clearField($_whichOneof(0)); @$pb.TagNumber(1) @@ -2652,15 +2576,15 @@ class ApplicationData extends $pb.GeneratedMessage { $_ensure(4); @$pb.TagNumber(9) - ApplicationData_GetLocation get getLocation => $_getN(5); + ApplicationData_Deprecated get deprecated9 => $_getN(5); @$pb.TagNumber(9) - set getLocation(ApplicationData_GetLocation value) => $_setField(9, value); + set deprecated9(ApplicationData_Deprecated value) => $_setField(9, value); @$pb.TagNumber(9) - $core.bool hasGetLocation() => $_has(5); + $core.bool hasDeprecated9() => $_has(5); @$pb.TagNumber(9) - void clearGetLocation() => $_clearField(9); + void clearDeprecated9() => $_clearField(9); @$pb.TagNumber(9) - ApplicationData_GetLocation ensureGetLocation() => $_ensure(5); + ApplicationData_Deprecated ensureDeprecated9() => $_ensure(5); @$pb.TagNumber(10) ApplicationData_GetCurrentPlanInfos get getCurrentPlanInfos => $_getN(6); @@ -2676,16 +2600,15 @@ class ApplicationData extends $pb.GeneratedMessage { $_ensure(6); @$pb.TagNumber(11) - ApplicationData_RedeemVoucher get redeemVoucher => $_getN(7); + ApplicationData_Deprecated get deprecated11 => $_getN(7); @$pb.TagNumber(11) - set redeemVoucher(ApplicationData_RedeemVoucher value) => - $_setField(11, value); + set deprecated11(ApplicationData_Deprecated value) => $_setField(11, value); @$pb.TagNumber(11) - $core.bool hasRedeemVoucher() => $_has(7); + $core.bool hasDeprecated11() => $_has(7); @$pb.TagNumber(11) - void clearRedeemVoucher() => $_clearField(11); + void clearDeprecated11() => $_clearField(11); @$pb.TagNumber(11) - ApplicationData_RedeemVoucher ensureRedeemVoucher() => $_ensure(7); + ApplicationData_Deprecated ensureDeprecated11() => $_ensure(7); @$pb.TagNumber(12) ApplicationData_GetAvailablePlans get getAvailablePlans => $_getN(8); @@ -2700,65 +2623,59 @@ class ApplicationData extends $pb.GeneratedMessage { ApplicationData_GetAvailablePlans ensureGetAvailablePlans() => $_ensure(8); @$pb.TagNumber(13) - ApplicationData_CreateVoucher get createVoucher => $_getN(9); + ApplicationData_Deprecated get deprecated13 => $_getN(9); @$pb.TagNumber(13) - set createVoucher(ApplicationData_CreateVoucher value) => - $_setField(13, value); + set deprecated13(ApplicationData_Deprecated value) => $_setField(13, value); @$pb.TagNumber(13) - $core.bool hasCreateVoucher() => $_has(9); + $core.bool hasDeprecated13() => $_has(9); @$pb.TagNumber(13) - void clearCreateVoucher() => $_clearField(13); + void clearDeprecated13() => $_clearField(13); @$pb.TagNumber(13) - ApplicationData_CreateVoucher ensureCreateVoucher() => $_ensure(9); + ApplicationData_Deprecated ensureDeprecated13() => $_ensure(9); @$pb.TagNumber(14) - ApplicationData_GetVouchers get getVouchers => $_getN(10); + ApplicationData_Deprecated get deprecated14 => $_getN(10); @$pb.TagNumber(14) - set getVouchers(ApplicationData_GetVouchers value) => $_setField(14, value); + set deprecated14(ApplicationData_Deprecated value) => $_setField(14, value); @$pb.TagNumber(14) - $core.bool hasGetVouchers() => $_has(10); + $core.bool hasDeprecated14() => $_has(10); @$pb.TagNumber(14) - void clearGetVouchers() => $_clearField(14); + void clearDeprecated14() => $_clearField(14); @$pb.TagNumber(14) - ApplicationData_GetVouchers ensureGetVouchers() => $_ensure(10); + ApplicationData_Deprecated ensureDeprecated14() => $_ensure(10); @$pb.TagNumber(15) - ApplicationData_SwitchToPayedPlan get switchtoPayedPlan => $_getN(11); + ApplicationData_Deprecated get deprecated15 => $_getN(11); @$pb.TagNumber(15) - set switchtoPayedPlan(ApplicationData_SwitchToPayedPlan value) => - $_setField(15, value); + set deprecated15(ApplicationData_Deprecated value) => $_setField(15, value); @$pb.TagNumber(15) - $core.bool hasSwitchtoPayedPlan() => $_has(11); + $core.bool hasDeprecated15() => $_has(11); @$pb.TagNumber(15) - void clearSwitchtoPayedPlan() => $_clearField(15); + void clearDeprecated15() => $_clearField(15); @$pb.TagNumber(15) - ApplicationData_SwitchToPayedPlan ensureSwitchtoPayedPlan() => $_ensure(11); + ApplicationData_Deprecated ensureDeprecated15() => $_ensure(11); @$pb.TagNumber(16) - ApplicationData_GetAddAccountsInvites get getAddaccountsInvites => $_getN(12); + ApplicationData_Deprecated get deprecated16 => $_getN(12); @$pb.TagNumber(16) - set getAddaccountsInvites(ApplicationData_GetAddAccountsInvites value) => - $_setField(16, value); + set deprecated16(ApplicationData_Deprecated value) => $_setField(16, value); @$pb.TagNumber(16) - $core.bool hasGetAddaccountsInvites() => $_has(12); + $core.bool hasDeprecated16() => $_has(12); @$pb.TagNumber(16) - void clearGetAddaccountsInvites() => $_clearField(16); + void clearDeprecated16() => $_clearField(16); @$pb.TagNumber(16) - ApplicationData_GetAddAccountsInvites ensureGetAddaccountsInvites() => - $_ensure(12); + ApplicationData_Deprecated ensureDeprecated16() => $_ensure(12); @$pb.TagNumber(17) - ApplicationData_RedeemAdditionalCode get redeemAdditionalCode => $_getN(13); + ApplicationData_Deprecated get deprecated17 => $_getN(13); @$pb.TagNumber(17) - set redeemAdditionalCode(ApplicationData_RedeemAdditionalCode value) => - $_setField(17, value); + set deprecated17(ApplicationData_Deprecated value) => $_setField(17, value); @$pb.TagNumber(17) - $core.bool hasRedeemAdditionalCode() => $_has(13); + $core.bool hasDeprecated17() => $_has(13); @$pb.TagNumber(17) - void clearRedeemAdditionalCode() => $_clearField(17); + void clearDeprecated17() => $_clearField(17); @$pb.TagNumber(17) - ApplicationData_RedeemAdditionalCode ensureRedeemAdditionalCode() => - $_ensure(13); + ApplicationData_Deprecated ensureDeprecated17() => $_ensure(13); @$pb.TagNumber(18) ApplicationData_RemoveAdditionalUser get removeAdditionalUser => $_getN(14); @@ -2774,16 +2691,15 @@ class ApplicationData extends $pb.GeneratedMessage { $_ensure(14); @$pb.TagNumber(19) - ApplicationData_UpdatePlanOptions get updatePlanOptions => $_getN(15); + ApplicationData_Deprecated get deprecated19 => $_getN(15); @$pb.TagNumber(19) - set updatePlanOptions(ApplicationData_UpdatePlanOptions value) => - $_setField(19, value); + set deprecated19(ApplicationData_Deprecated value) => $_setField(19, value); @$pb.TagNumber(19) - $core.bool hasUpdatePlanOptions() => $_has(15); + $core.bool hasDeprecated19() => $_has(15); @$pb.TagNumber(19) - void clearUpdatePlanOptions() => $_clearField(19); + void clearDeprecated19() => $_clearField(19); @$pb.TagNumber(19) - ApplicationData_UpdatePlanOptions ensureUpdatePlanOptions() => $_ensure(15); + ApplicationData_Deprecated ensureDeprecated19() => $_ensure(15); @$pb.TagNumber(20) ApplicationData_DownloadDone get downloadDone => $_getN(16); @@ -2891,6 +2807,18 @@ class ApplicationData extends $pb.GeneratedMessage { void clearAddAdditionalUser() => $_clearField(29); @$pb.TagNumber(29) ApplicationData_AddAdditionalUser ensureAddAdditionalUser() => $_ensure(24); + + @$pb.TagNumber(30) + ApplicationData_SetLoginToken get setLoginToken => $_getN(25); + @$pb.TagNumber(30) + set setLoginToken(ApplicationData_SetLoginToken value) => + $_setField(30, value); + @$pb.TagNumber(30) + $core.bool hasSetLoginToken() => $_has(25); + @$pb.TagNumber(30) + void clearSetLoginToken() => $_clearField(30); + @$pb.TagNumber(30) + ApplicationData_SetLoginToken ensureSetLoginToken() => $_ensure(25); } class Response_PreKey extends $pb.GeneratedMessage { diff --git a/lib/src/model/protobuf/api/websocket/client_to_server.pbjson.dart b/lib/src/model/protobuf/api/websocket/client_to_server.pbjson.dart index a0b6812a..9ea3c1f3 100644 --- a/lib/src/model/protobuf/api/websocket/client_to_server.pbjson.dart +++ b/lib/src/model/protobuf/api/websocket/client_to_server.pbjson.dart @@ -134,13 +134,33 @@ const Handshake$json = { '9': 0, '10': 'requestPOW' }, + { + '1': 'authenticate_with_login_token', + '3': 6, + '4': 1, + '5': 11, + '6': '.client_to_server.Handshake.AuthenticateWithLoginToken', + '9': 0, + '10': 'authenticateWithLoginToken' + }, + { + '1': 'get_userid_by_username', + '3': 7, + '4': 1, + '5': 11, + '6': '.client_to_server.Handshake.GetUserIdByUsername', + '9': 0, + '10': 'getUseridByUsername' + }, ], '3': [ Handshake_RequestPOW$json, Handshake_Register$json, Handshake_GetAuthChallenge$json, + Handshake_GetUserIdByUsername$json, Handshake_GetAuthToken$json, - Handshake_Authenticate$json + Handshake_Authenticate$json, + Handshake_AuthenticateWithLoginToken$json ], '8': [ {'1': 'Handshake'}, @@ -186,9 +206,19 @@ const Handshake_Register$json = { {'1': 'is_ios', '3': 8, '4': 1, '5': 8, '10': 'isIos'}, {'1': 'lang_code', '3': 9, '4': 1, '5': 9, '10': 'langCode'}, {'1': 'proof_of_work', '3': 10, '4': 1, '5': 3, '10': 'proofOfWork'}, + { + '1': 'login_token', + '3': 11, + '4': 1, + '5': 12, + '9': 1, + '10': 'loginToken', + '17': true + }, ], '8': [ {'1': '_invite_code'}, + {'1': '_login_token'}, ], }; @@ -197,6 +227,14 @@ const Handshake_GetAuthChallenge$json = { '1': 'GetAuthChallenge', }; +@$core.Deprecated('Use handshakeDescriptor instead') +const Handshake_GetUserIdByUsername$json = { + '1': 'GetUserIdByUsername', + '2': [ + {'1': 'username', '3': 1, '4': 1, '5': 9, '10': 'username'}, + ], +}; + @$core.Deprecated('Use handshakeDescriptor instead') const Handshake_GetAuthToken$json = { '1': 'GetAuthToken', @@ -247,6 +285,24 @@ const Handshake_Authenticate$json = { ], }; +@$core.Deprecated('Use handshakeDescriptor instead') +const Handshake_AuthenticateWithLoginToken$json = { + '1': 'AuthenticateWithLoginToken', + '2': [ + {'1': 'user_id', '3': 1, '4': 1, '5': 3, '10': 'userId'}, + { + '1': 'secret_login_token', + '3': 2, + '4': 1, + '5': 12, + '10': 'secretLoginToken' + }, + {'1': 'app_version', '3': 3, '4': 1, '5': 9, '10': 'appVersion'}, + {'1': 'device_id', '3': 4, '4': 1, '5': 3, '10': 'deviceId'}, + {'1': 'in_background', '3': 5, '4': 1, '5': 8, '10': 'inBackground'}, + ], +}; + /// Descriptor for `Handshake`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List handshakeDescriptor = $convert.base64Decode( 'CglIYW5kc2hha2USQgoIcmVnaXN0ZXIYASABKAsyJC5jbGllbnRfdG9fc2VydmVyLkhhbmRzaG' @@ -256,20 +312,30 @@ final $typed_data.Uint8List handshakeDescriptor = $convert.base64Decode( 'LkdldEF1dGhUb2tlbkgAUgxnZXRBdXRoVG9rZW4STgoMYXV0aGVudGljYXRlGAQgASgLMiguY2' 'xpZW50X3RvX3NlcnZlci5IYW5kc2hha2UuQXV0aGVudGljYXRlSABSDGF1dGhlbnRpY2F0ZRJI' 'CgpyZXF1ZXN0UE9XGAUgASgLMiYuY2xpZW50X3RvX3NlcnZlci5IYW5kc2hha2UuUmVxdWVzdF' - 'BPV0gAUgpyZXF1ZXN0UE9XGgwKClJlcXVlc3RQT1calAMKCFJlZ2lzdGVyEhoKCHVzZXJuYW1l' - 'GAEgASgJUgh1c2VybmFtZRIkCgtpbnZpdGVfY29kZRgCIAEoCUgAUgppbnZpdGVDb2RliAEBEi' - '4KE3B1YmxpY19pZGVudGl0eV9rZXkYAyABKAxSEXB1YmxpY0lkZW50aXR5S2V5EiMKDXNpZ25l' - 'ZF9wcmVrZXkYBCABKAxSDHNpZ25lZFByZWtleRI2ChdzaWduZWRfcHJla2V5X3NpZ25hdHVyZR' - 'gFIAEoDFIVc2lnbmVkUHJla2V5U2lnbmF0dXJlEigKEHNpZ25lZF9wcmVrZXlfaWQYBiABKANS' - 'DnNpZ25lZFByZWtleUlkEicKD3JlZ2lzdHJhdGlvbl9pZBgHIAEoA1IOcmVnaXN0cmF0aW9uSW' - 'QSFQoGaXNfaW9zGAggASgIUgVpc0lvcxIbCglsYW5nX2NvZGUYCSABKAlSCGxhbmdDb2RlEiIK' - 'DXByb29mX29mX3dvcmsYCiABKANSC3Byb29mT2ZXb3JrQg4KDF9pbnZpdGVfY29kZRoSChBHZX' - 'RBdXRoQ2hhbGxlbmdlGkMKDEdldEF1dGhUb2tlbhIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQS' - 'GgoIcmVzcG9uc2UYAiABKAxSCHJlc3BvbnNlGugBCgxBdXRoZW50aWNhdGUSFwoHdXNlcl9pZB' - 'gBIAEoA1IGdXNlcklkEh0KCmF1dGhfdG9rZW4YAiABKAxSCWF1dGhUb2tlbhIkCgthcHBfdmVy' - 'c2lvbhgDIAEoCUgAUgphcHBWZXJzaW9uiAEBEiAKCWRldmljZV9pZBgEIAEoA0gBUghkZXZpY2' - 'VJZIgBARIoCg1pbl9iYWNrZ3JvdW5kGAUgASgISAJSDGluQmFja2dyb3VuZIgBAUIOCgxfYXBw' - 'X3ZlcnNpb25CDAoKX2RldmljZV9pZEIQCg5faW5fYmFja2dyb3VuZEILCglIYW5kc2hha2U='); + 'BPV0gAUgpyZXF1ZXN0UE9XEnsKHWF1dGhlbnRpY2F0ZV93aXRoX2xvZ2luX3Rva2VuGAYgASgL' + 'MjYuY2xpZW50X3RvX3NlcnZlci5IYW5kc2hha2UuQXV0aGVudGljYXRlV2l0aExvZ2luVG9rZW' + '5IAFIaYXV0aGVudGljYXRlV2l0aExvZ2luVG9rZW4SZgoWZ2V0X3VzZXJpZF9ieV91c2VybmFt' + 'ZRgHIAEoCzIvLmNsaWVudF90b19zZXJ2ZXIuSGFuZHNoYWtlLkdldFVzZXJJZEJ5VXNlcm5hbW' + 'VIAFITZ2V0VXNlcmlkQnlVc2VybmFtZRoMCgpSZXF1ZXN0UE9XGsoDCghSZWdpc3RlchIaCgh1' + 'c2VybmFtZRgBIAEoCVIIdXNlcm5hbWUSJAoLaW52aXRlX2NvZGUYAiABKAlIAFIKaW52aXRlQ2' + '9kZYgBARIuChNwdWJsaWNfaWRlbnRpdHlfa2V5GAMgASgMUhFwdWJsaWNJZGVudGl0eUtleRIj' + 'Cg1zaWduZWRfcHJla2V5GAQgASgMUgxzaWduZWRQcmVrZXkSNgoXc2lnbmVkX3ByZWtleV9zaW' + 'duYXR1cmUYBSABKAxSFXNpZ25lZFByZWtleVNpZ25hdHVyZRIoChBzaWduZWRfcHJla2V5X2lk' + 'GAYgASgDUg5zaWduZWRQcmVrZXlJZBInCg9yZWdpc3RyYXRpb25faWQYByABKANSDnJlZ2lzdH' + 'JhdGlvbklkEhUKBmlzX2lvcxgIIAEoCFIFaXNJb3MSGwoJbGFuZ19jb2RlGAkgASgJUghsYW5n' + 'Q29kZRIiCg1wcm9vZl9vZl93b3JrGAogASgDUgtwcm9vZk9mV29yaxIkCgtsb2dpbl90b2tlbh' + 'gLIAEoDEgBUgpsb2dpblRva2VuiAEBQg4KDF9pbnZpdGVfY29kZUIOCgxfbG9naW5fdG9rZW4a' + 'EgoQR2V0QXV0aENoYWxsZW5nZRoxChNHZXRVc2VySWRCeVVzZXJuYW1lEhoKCHVzZXJuYW1lGA' + 'EgASgJUgh1c2VybmFtZRpDCgxHZXRBdXRoVG9rZW4SFwoHdXNlcl9pZBgBIAEoA1IGdXNlcklk' + 'EhoKCHJlc3BvbnNlGAIgASgMUghyZXNwb25zZRroAQoMQXV0aGVudGljYXRlEhcKB3VzZXJfaW' + 'QYASABKANSBnVzZXJJZBIdCgphdXRoX3Rva2VuGAIgASgMUglhdXRoVG9rZW4SJAoLYXBwX3Zl' + 'cnNpb24YAyABKAlIAFIKYXBwVmVyc2lvbogBARIgCglkZXZpY2VfaWQYBCABKANIAVIIZGV2aW' + 'NlSWSIAQESKAoNaW5fYmFja2dyb3VuZBgFIAEoCEgCUgxpbkJhY2tncm91bmSIAQFCDgoMX2Fw' + 'cF92ZXJzaW9uQgwKCl9kZXZpY2VfaWRCEAoOX2luX2JhY2tncm91bmQaxgEKGkF1dGhlbnRpY2' + 'F0ZVdpdGhMb2dpblRva2VuEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBIsChJzZWNyZXRfbG9n' + 'aW5fdG9rZW4YAiABKAxSEHNlY3JldExvZ2luVG9rZW4SHwoLYXBwX3ZlcnNpb24YAyABKAlSCm' + 'FwcFZlcnNpb24SGwoJZGV2aWNlX2lkGAQgASgDUghkZXZpY2VJZBIjCg1pbl9iYWNrZ3JvdW5k' + 'GAUgASgIUgxpbkJhY2tncm91bmRCCwoJSGFuZHNoYWtl'); @$core.Deprecated('Use applicationDataDescriptor instead') const ApplicationData$json = { @@ -321,13 +387,13 @@ const ApplicationData$json = { '10': 'updateGoogleFcmToken' }, { - '1': 'getLocation', + '1': 'deprecated_9', '3': 9, '4': 1, '5': 11, - '6': '.client_to_server.ApplicationData.GetLocation', + '6': '.client_to_server.ApplicationData.Deprecated', '9': 0, - '10': 'getLocation' + '10': 'deprecated9' }, { '1': 'getCurrentPlanInfos', @@ -339,13 +405,13 @@ const ApplicationData$json = { '10': 'getCurrentPlanInfos' }, { - '1': 'redeemVoucher', + '1': 'deprecated_11', '3': 11, '4': 1, '5': 11, - '6': '.client_to_server.ApplicationData.RedeemVoucher', + '6': '.client_to_server.ApplicationData.Deprecated', '9': 0, - '10': 'redeemVoucher' + '10': 'deprecated11' }, { '1': 'getAvailablePlans', @@ -357,58 +423,58 @@ const ApplicationData$json = { '10': 'getAvailablePlans' }, { - '1': 'createVoucher', + '1': 'deprecated_13', '3': 13, '4': 1, '5': 11, - '6': '.client_to_server.ApplicationData.CreateVoucher', + '6': '.client_to_server.ApplicationData.Deprecated', '9': 0, - '10': 'createVoucher' + '10': 'deprecated13' }, { - '1': 'getVouchers', + '1': 'deprecated_14', '3': 14, '4': 1, '5': 11, - '6': '.client_to_server.ApplicationData.GetVouchers', + '6': '.client_to_server.ApplicationData.Deprecated', '9': 0, - '10': 'getVouchers' + '10': 'deprecated14' }, { - '1': 'switchtoPayedPlan', + '1': 'deprecated_15', '3': 15, '4': 1, '5': 11, - '6': '.client_to_server.ApplicationData.SwitchToPayedPlan', + '6': '.client_to_server.ApplicationData.Deprecated', '9': 0, - '10': 'switchtoPayedPlan' + '10': 'deprecated15' }, { - '1': 'getAddaccountsInvites', + '1': 'deprecated_16', '3': 16, '4': 1, '5': 11, - '6': '.client_to_server.ApplicationData.GetAddAccountsInvites', + '6': '.client_to_server.ApplicationData.Deprecated', '9': 0, - '10': 'getAddaccountsInvites' + '10': 'deprecated16' }, { - '1': 'redeemAdditionalCode', + '1': 'deprecated_17', '3': 17, '4': 1, '5': 11, - '6': '.client_to_server.ApplicationData.RedeemAdditionalCode', + '6': '.client_to_server.ApplicationData.Deprecated', '9': 0, - '10': 'redeemAdditionalCode' + '10': 'deprecated17' }, { - '1': 'updatePlanOptions', + '1': 'deprecated_19', '3': 19, '4': 1, '5': 11, - '6': '.client_to_server.ApplicationData.UpdatePlanOptions', + '6': '.client_to_server.ApplicationData.Deprecated', '9': 0, - '10': 'updatePlanOptions' + '10': 'deprecated19' }, { '1': 'downloadDone', @@ -500,6 +566,15 @@ const ApplicationData$json = { '9': 0, '10': 'addAdditionalUser' }, + { + '1': 'set_login_token', + '3': 30, + '4': 1, + '5': 11, + '6': '.client_to_server.ApplicationData.SetLoginToken', + '9': 0, + '10': 'setLoginToken' + }, ], '3': [ ApplicationData_TextMessage$json, @@ -507,16 +582,9 @@ const ApplicationData$json = { ApplicationData_ChangeUsername$json, ApplicationData_UpdateGoogleFcmToken$json, ApplicationData_GetUserById$json, - ApplicationData_RedeemVoucher$json, - ApplicationData_SwitchToPayedPlan$json, - ApplicationData_UpdatePlanOptions$json, - ApplicationData_CreateVoucher$json, - ApplicationData_GetLocation$json, - ApplicationData_GetVouchers$json, ApplicationData_GetAvailablePlans$json, ApplicationData_GetAddAccountsInvites$json, ApplicationData_GetCurrentPlanInfos$json, - ApplicationData_RedeemAdditionalCode$json, ApplicationData_RemoveAdditionalUser$json, ApplicationData_GetPrekeysByUserId$json, ApplicationData_GetSignedPreKeyByUserId$json, @@ -526,7 +594,9 @@ const ApplicationData$json = { ApplicationData_IPAPurchase$json, ApplicationData_IPAForceCheck$json, ApplicationData_DeleteAccount$json, - ApplicationData_AddAdditionalUser$json + ApplicationData_AddAdditionalUser$json, + ApplicationData_SetLoginToken$json, + ApplicationData_Deprecated$json ], '8': [ {'1': 'ApplicationData'}, @@ -586,50 +656,6 @@ const ApplicationData_GetUserById$json = { ], }; -@$core.Deprecated('Use applicationDataDescriptor instead') -const ApplicationData_RedeemVoucher$json = { - '1': 'RedeemVoucher', - '2': [ - {'1': 'voucher', '3': 1, '4': 1, '5': 9, '10': 'voucher'}, - ], -}; - -@$core.Deprecated('Use applicationDataDescriptor instead') -const ApplicationData_SwitchToPayedPlan$json = { - '1': 'SwitchToPayedPlan', - '2': [ - {'1': 'plan_id', '3': 1, '4': 1, '5': 9, '10': 'planId'}, - {'1': 'pay_monthly', '3': 2, '4': 1, '5': 8, '10': 'payMonthly'}, - {'1': 'auto_renewal', '3': 3, '4': 1, '5': 8, '10': 'autoRenewal'}, - ], -}; - -@$core.Deprecated('Use applicationDataDescriptor instead') -const ApplicationData_UpdatePlanOptions$json = { - '1': 'UpdatePlanOptions', - '2': [ - {'1': 'auto_renewal', '3': 1, '4': 1, '5': 8, '10': 'autoRenewal'}, - ], -}; - -@$core.Deprecated('Use applicationDataDescriptor instead') -const ApplicationData_CreateVoucher$json = { - '1': 'CreateVoucher', - '2': [ - {'1': 'value_cents', '3': 1, '4': 1, '5': 13, '10': 'valueCents'}, - ], -}; - -@$core.Deprecated('Use applicationDataDescriptor instead') -const ApplicationData_GetLocation$json = { - '1': 'GetLocation', -}; - -@$core.Deprecated('Use applicationDataDescriptor instead') -const ApplicationData_GetVouchers$json = { - '1': 'GetVouchers', -}; - @$core.Deprecated('Use applicationDataDescriptor instead') const ApplicationData_GetAvailablePlans$json = { '1': 'GetAvailablePlans', @@ -645,14 +671,6 @@ const ApplicationData_GetCurrentPlanInfos$json = { '1': 'GetCurrentPlanInfos', }; -@$core.Deprecated('Use applicationDataDescriptor instead') -const ApplicationData_RedeemAdditionalCode$json = { - '1': 'RedeemAdditionalCode', - '2': [ - {'1': 'invite_code', '3': 2, '4': 1, '5': 9, '10': 'inviteCode'}, - ], -}; - @$core.Deprecated('Use applicationDataDescriptor instead') const ApplicationData_RemoveAdditionalUser$json = { '1': 'RemoveAdditionalUser', @@ -744,6 +762,19 @@ const ApplicationData_AddAdditionalUser$json = { ], }; +@$core.Deprecated('Use applicationDataDescriptor instead') +const ApplicationData_SetLoginToken$json = { + '1': 'SetLoginToken', + '2': [ + {'1': 'login_token', '3': 1, '4': 1, '5': 12, '10': 'loginToken'}, + ], +}; + +@$core.Deprecated('Use applicationDataDescriptor instead') +const ApplicationData_Deprecated$json = { + '1': 'Deprecated', +}; + /// Descriptor for `ApplicationData`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List applicationDataDescriptor = $convert.base64Decode( 'Cg9BcHBsaWNhdGlvbkRhdGESUQoLdGV4dE1lc3NhZ2UYASABKAsyLS5jbGllbnRfdG9fc2Vydm' @@ -755,66 +786,61 @@ final $typed_data.Uint8List applicationDataDescriptor = $convert.base64Decode( 'bnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5HZXRVc2VyQnlJZEgAUgtnZXRVc2VyQnlJZB' 'JsChR1cGRhdGVHb29nbGVGY21Ub2tlbhgIIAEoCzI2LmNsaWVudF90b19zZXJ2ZXIuQXBwbGlj' 'YXRpb25EYXRhLlVwZGF0ZUdvb2dsZUZjbVRva2VuSABSFHVwZGF0ZUdvb2dsZUZjbVRva2VuEl' - 'EKC2dldExvY2F0aW9uGAkgASgLMi0uY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEu' - 'R2V0TG9jYXRpb25IAFILZ2V0TG9jYXRpb24SaQoTZ2V0Q3VycmVudFBsYW5JbmZvcxgKIAEoCz' + 'EKDGRlcHJlY2F0ZWRfORgJIAEoCzIsLmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRh' + 'LkRlcHJlY2F0ZWRIAFILZGVwcmVjYXRlZDkSaQoTZ2V0Q3VycmVudFBsYW5JbmZvcxgKIAEoCz' 'I1LmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLkdldEN1cnJlbnRQbGFuSW5mb3NI' - 'AFITZ2V0Q3VycmVudFBsYW5JbmZvcxJXCg1yZWRlZW1Wb3VjaGVyGAsgASgLMi8uY2xpZW50X3' - 'RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuUmVkZWVtVm91Y2hlckgAUg1yZWRlZW1Wb3VjaGVy' - 'EmMKEWdldEF2YWlsYWJsZVBsYW5zGAwgASgLMjMuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdG' - 'lvbkRhdGEuR2V0QXZhaWxhYmxlUGxhbnNIAFIRZ2V0QXZhaWxhYmxlUGxhbnMSVwoNY3JlYXRl' - 'Vm91Y2hlchgNIAEoCzIvLmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLkNyZWF0ZV' - 'ZvdWNoZXJIAFINY3JlYXRlVm91Y2hlchJRCgtnZXRWb3VjaGVycxgOIAEoCzItLmNsaWVudF90' - 'b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLkdldFZvdWNoZXJzSABSC2dldFZvdWNoZXJzEmMKEX' - 'N3aXRjaHRvUGF5ZWRQbGFuGA8gASgLMjMuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRh' - 'dGEuU3dpdGNoVG9QYXllZFBsYW5IAFIRc3dpdGNodG9QYXllZFBsYW4SbwoVZ2V0QWRkYWNjb3' - 'VudHNJbnZpdGVzGBAgASgLMjcuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuR2V0' - 'QWRkQWNjb3VudHNJbnZpdGVzSABSFWdldEFkZGFjY291bnRzSW52aXRlcxJsChRyZWRlZW1BZG' - 'RpdGlvbmFsQ29kZRgRIAEoCzI2LmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLlJl' - 'ZGVlbUFkZGl0aW9uYWxDb2RlSABSFHJlZGVlbUFkZGl0aW9uYWxDb2RlEmMKEXVwZGF0ZVBsYW' - '5PcHRpb25zGBMgASgLMjMuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuVXBkYXRl' - 'UGxhbk9wdGlvbnNIAFIRdXBkYXRlUGxhbk9wdGlvbnMSVAoMZG93bmxvYWREb25lGBQgASgLMi' - '4uY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuRG93bmxvYWREb25lSABSDGRvd25s' - 'b2FkRG9uZRJ1ChdnZXRTaWduZWRQcmVrZXlCeVVzZXJpZBgWIAEoCzI5LmNsaWVudF90b19zZX' - 'J2ZXIuQXBwbGljYXRpb25EYXRhLkdldFNpZ25lZFByZUtleUJ5VXNlcklkSABSF2dldFNpZ25l' - 'ZFByZWtleUJ5VXNlcmlkEmYKEnVwZGF0ZVNpZ25lZFByZWtleRgXIAEoCzI0LmNsaWVudF90b1' - '9zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLlVwZGF0ZVNpZ25lZFByZUtleUgAUhJ1cGRhdGVTaWdu' - 'ZWRQcmVrZXkSVwoNZGVsZXRlQWNjb3VudBgYIAEoCzIvLmNsaWVudF90b19zZXJ2ZXIuQXBwbG' - 'ljYXRpb25EYXRhLkRlbGV0ZUFjY291bnRIAFINZGVsZXRlQWNjb3VudBJOCgpyZXBvcnRVc2Vy' - 'GBkgASgLMiwuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuUmVwb3J0VXNlckgAUg' - 'pyZXBvcnRVc2VyEloKDmNoYW5nZVVzZXJuYW1lGBogASgLMjAuY2xpZW50X3RvX3NlcnZlci5B' - 'cHBsaWNhdGlvbkRhdGEuQ2hhbmdlVXNlcm5hbWVIAFIOY2hhbmdlVXNlcm5hbWUSUQoLaXBhUH' - 'VyY2hhc2UYGyABKAsyLS5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5JUEFQdXJj' - 'aGFzZUgAUgtpcGFQdXJjaGFzZRJXCg1pcGFGb3JjZUNoZWNrGBwgASgLMi8uY2xpZW50X3RvX3' - 'NlcnZlci5BcHBsaWNhdGlvbkRhdGEuSVBBRm9yY2VDaGVja0gAUg1pcGFGb3JjZUNoZWNrEmwK' - 'FHJlbW92ZUFkZGl0aW9uYWxVc2VyGBIgASgLMjYuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdG' - 'lvbkRhdGEuUmVtb3ZlQWRkaXRpb25hbFVzZXJIAFIUcmVtb3ZlQWRkaXRpb25hbFVzZXISYwoR' - 'YWRkQWRkaXRpb25hbFVzZXIYHSABKAsyMy5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRG' - 'F0YS5BZGRBZGRpdGlvbmFsVXNlckgAUhFhZGRBZGRpdGlvbmFsVXNlchpqCgtUZXh0TWVzc2Fn' + 'AFITZ2V0Q3VycmVudFBsYW5JbmZvcxJTCg1kZXByZWNhdGVkXzExGAsgASgLMiwuY2xpZW50X3' + 'RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuRGVwcmVjYXRlZEgAUgxkZXByZWNhdGVkMTESYwoR' + 'Z2V0QXZhaWxhYmxlUGxhbnMYDCABKAsyMy5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRG' + 'F0YS5HZXRBdmFpbGFibGVQbGFuc0gAUhFnZXRBdmFpbGFibGVQbGFucxJTCg1kZXByZWNhdGVk' + 'XzEzGA0gASgLMiwuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuRGVwcmVjYXRlZE' + 'gAUgxkZXByZWNhdGVkMTMSUwoNZGVwcmVjYXRlZF8xNBgOIAEoCzIsLmNsaWVudF90b19zZXJ2' + 'ZXIuQXBwbGljYXRpb25EYXRhLkRlcHJlY2F0ZWRIAFIMZGVwcmVjYXRlZDE0ElMKDWRlcHJlY2' + 'F0ZWRfMTUYDyABKAsyLC5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5EZXByZWNh' + 'dGVkSABSDGRlcHJlY2F0ZWQxNRJTCg1kZXByZWNhdGVkXzE2GBAgASgLMiwuY2xpZW50X3RvX3' + 'NlcnZlci5BcHBsaWNhdGlvbkRhdGEuRGVwcmVjYXRlZEgAUgxkZXByZWNhdGVkMTYSUwoNZGVw' + 'cmVjYXRlZF8xNxgRIAEoCzIsLmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLkRlcH' + 'JlY2F0ZWRIAFIMZGVwcmVjYXRlZDE3ElMKDWRlcHJlY2F0ZWRfMTkYEyABKAsyLC5jbGllbnRf' + 'dG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5EZXByZWNhdGVkSABSDGRlcHJlY2F0ZWQxORJUCg' + 'xkb3dubG9hZERvbmUYFCABKAsyLi5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5E' + 'b3dubG9hZERvbmVIAFIMZG93bmxvYWREb25lEnUKF2dldFNpZ25lZFByZWtleUJ5VXNlcmlkGB' + 'YgASgLMjkuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuR2V0U2lnbmVkUHJlS2V5' + 'QnlVc2VySWRIAFIXZ2V0U2lnbmVkUHJla2V5QnlVc2VyaWQSZgoSdXBkYXRlU2lnbmVkUHJla2' + 'V5GBcgASgLMjQuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuVXBkYXRlU2lnbmVk' + 'UHJlS2V5SABSEnVwZGF0ZVNpZ25lZFByZWtleRJXCg1kZWxldGVBY2NvdW50GBggASgLMi8uY2' + 'xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuRGVsZXRlQWNjb3VudEgAUg1kZWxldGVB' + 'Y2NvdW50Ek4KCnJlcG9ydFVzZXIYGSABKAsyLC5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW' + '9uRGF0YS5SZXBvcnRVc2VySABSCnJlcG9ydFVzZXISWgoOY2hhbmdlVXNlcm5hbWUYGiABKAsy' + 'MC5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5DaGFuZ2VVc2VybmFtZUgAUg5jaG' + 'FuZ2VVc2VybmFtZRJRCgtpcGFQdXJjaGFzZRgbIAEoCzItLmNsaWVudF90b19zZXJ2ZXIuQXBw' + 'bGljYXRpb25EYXRhLklQQVB1cmNoYXNlSABSC2lwYVB1cmNoYXNlElcKDWlwYUZvcmNlQ2hlY2' + 'sYHCABKAsyLy5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5JUEFGb3JjZUNoZWNr' + 'SABSDWlwYUZvcmNlQ2hlY2sSbAoUcmVtb3ZlQWRkaXRpb25hbFVzZXIYEiABKAsyNi5jbGllbn' + 'RfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5SZW1vdmVBZGRpdGlvbmFsVXNlckgAUhRyZW1v' + 'dmVBZGRpdGlvbmFsVXNlchJjChFhZGRBZGRpdGlvbmFsVXNlchgdIAEoCzIzLmNsaWVudF90b1' + '9zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLkFkZEFkZGl0aW9uYWxVc2VySABSEWFkZEFkZGl0aW9u' + 'YWxVc2VyElkKD3NldF9sb2dpbl90b2tlbhgeIAEoCzIvLmNsaWVudF90b19zZXJ2ZXIuQXBwbG' + 'ljYXRpb25EYXRhLlNldExvZ2luVG9rZW5IAFINc2V0TG9naW5Ub2tlbhpqCgtUZXh0TWVzc2Fn' 'ZRIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQSEgoEYm9keRgDIAEoDFIEYm9keRIgCglwdXNoX2' 'RhdGEYBCABKAxIAFIIcHVzaERhdGGIAQFCDAoKX3B1c2hfZGF0YRovChFHZXRVc2VyQnlVc2Vy' 'bmFtZRIaCgh1c2VybmFtZRgBIAEoCVIIdXNlcm5hbWUaLAoOQ2hhbmdlVXNlcm5hbWUSGgoIdX' 'Nlcm5hbWUYASABKAlSCHVzZXJuYW1lGjUKFFVwZGF0ZUdvb2dsZUZjbVRva2VuEh0KCmdvb2ds' 'ZV9mY20YASABKAlSCWdvb2dsZUZjbRomCgtHZXRVc2VyQnlJZBIXCgd1c2VyX2lkGAEgASgDUg' - 'Z1c2VySWQaKQoNUmVkZWVtVm91Y2hlchIYCgd2b3VjaGVyGAEgASgJUgd2b3VjaGVyGnAKEVN3' - 'aXRjaFRvUGF5ZWRQbGFuEhcKB3BsYW5faWQYASABKAlSBnBsYW5JZBIfCgtwYXlfbW9udGhseR' - 'gCIAEoCFIKcGF5TW9udGhseRIhCgxhdXRvX3JlbmV3YWwYAyABKAhSC2F1dG9SZW5ld2FsGjYK' - 'EVVwZGF0ZVBsYW5PcHRpb25zEiEKDGF1dG9fcmVuZXdhbBgBIAEoCFILYXV0b1JlbmV3YWwaMA' - 'oNQ3JlYXRlVm91Y2hlchIfCgt2YWx1ZV9jZW50cxgBIAEoDVIKdmFsdWVDZW50cxoNCgtHZXRM' - 'b2NhdGlvbhoNCgtHZXRWb3VjaGVycxoTChFHZXRBdmFpbGFibGVQbGFucxoXChVHZXRBZGRBY2' - 'NvdW50c0ludml0ZXMaFQoTR2V0Q3VycmVudFBsYW5JbmZvcxo3ChRSZWRlZW1BZGRpdGlvbmFs' - 'Q29kZRIfCgtpbnZpdGVfY29kZRgCIAEoCVIKaW52aXRlQ29kZRovChRSZW1vdmVBZGRpdGlvbm' - 'FsVXNlchIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQaLQoSR2V0UHJla2V5c0J5VXNlcklkEhcK' - 'B3VzZXJfaWQYASABKANSBnVzZXJJZBoyChdHZXRTaWduZWRQcmVLZXlCeVVzZXJJZBIXCgd1c2' - 'VyX2lkGAEgASgDUgZ1c2VySWQamwEKElVwZGF0ZVNpZ25lZFByZUtleRIoChBzaWduZWRfcHJl' - 'a2V5X2lkGAEgASgDUg5zaWduZWRQcmVrZXlJZBIjCg1zaWduZWRfcHJla2V5GAIgASgMUgxzaW' - 'duZWRQcmVrZXkSNgoXc2lnbmVkX3ByZWtleV9zaWduYXR1cmUYAyABKAxSFXNpZ25lZFByZWtl' - 'eVNpZ25hdHVyZRo1CgxEb3dubG9hZERvbmUSJQoOZG93bmxvYWRfdG9rZW4YASABKAxSDWRvd2' - '5sb2FkVG9rZW4aTgoKUmVwb3J0VXNlchIoChByZXBvcnRlZF91c2VyX2lkGAEgASgDUg5yZXBv' - 'cnRlZFVzZXJJZBIWCgZyZWFzb24YAiABKAlSBnJlYXNvbhpxCgtJUEFQdXJjaGFzZRIdCgpwcm' - '9kdWN0X2lkGAEgASgJUglwcm9kdWN0SWQSFgoGc291cmNlGAIgASgJUgZzb3VyY2USKwoRdmVy' - 'aWZpY2F0aW9uX2RhdGEYAyABKAlSEHZlcmlmaWNhdGlvbkRhdGEaDwoNSVBBRm9yY2VDaGVjax' - 'oPCg1EZWxldGVBY2NvdW50GiwKEUFkZEFkZGl0aW9uYWxVc2VyEhcKB3VzZXJfaWQYASABKANS' - 'BnVzZXJJZEIRCg9BcHBsaWNhdGlvbkRhdGE='); + 'Z1c2VySWQaEwoRR2V0QXZhaWxhYmxlUGxhbnMaFwoVR2V0QWRkQWNjb3VudHNJbnZpdGVzGhUK' + 'E0dldEN1cnJlbnRQbGFuSW5mb3MaLwoUUmVtb3ZlQWRkaXRpb25hbFVzZXISFwoHdXNlcl9pZB' + 'gBIAEoA1IGdXNlcklkGi0KEkdldFByZWtleXNCeVVzZXJJZBIXCgd1c2VyX2lkGAEgASgDUgZ1' + 'c2VySWQaMgoXR2V0U2lnbmVkUHJlS2V5QnlVc2VySWQSFwoHdXNlcl9pZBgBIAEoA1IGdXNlck' + 'lkGpsBChJVcGRhdGVTaWduZWRQcmVLZXkSKAoQc2lnbmVkX3ByZWtleV9pZBgBIAEoA1IOc2ln' + 'bmVkUHJla2V5SWQSIwoNc2lnbmVkX3ByZWtleRgCIAEoDFIMc2lnbmVkUHJla2V5EjYKF3NpZ2' + '5lZF9wcmVrZXlfc2lnbmF0dXJlGAMgASgMUhVzaWduZWRQcmVrZXlTaWduYXR1cmUaNQoMRG93' + 'bmxvYWREb25lEiUKDmRvd25sb2FkX3Rva2VuGAEgASgMUg1kb3dubG9hZFRva2VuGk4KClJlcG' + '9ydFVzZXISKAoQcmVwb3J0ZWRfdXNlcl9pZBgBIAEoA1IOcmVwb3J0ZWRVc2VySWQSFgoGcmVh' + 'c29uGAIgASgJUgZyZWFzb24acQoLSVBBUHVyY2hhc2USHQoKcHJvZHVjdF9pZBgBIAEoCVIJcH' + 'JvZHVjdElkEhYKBnNvdXJjZRgCIAEoCVIGc291cmNlEisKEXZlcmlmaWNhdGlvbl9kYXRhGAMg' + 'ASgJUhB2ZXJpZmljYXRpb25EYXRhGg8KDUlQQUZvcmNlQ2hlY2saDwoNRGVsZXRlQWNjb3VudB' + 'osChFBZGRBZGRpdGlvbmFsVXNlchIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQaMAoNU2V0TG9n' + 'aW5Ub2tlbhIfCgtsb2dpbl90b2tlbhgBIAEoDFIKbG9naW5Ub2tlbhoMCgpEZXByZWNhdGVkQh' + 'EKD0FwcGxpY2F0aW9uRGF0YQ=='); @$core.Deprecated('Use responseDescriptor instead') const Response$json = { diff --git a/lib/src/model/protobuf/api/websocket/server_to_client.pb.dart b/lib/src/model/protobuf/api/websocket/server_to_client.pb.dart index 499450f2..6c10ff5d 100644 --- a/lib/src/model/protobuf/api/websocket/server_to_client.pb.dart +++ b/lib/src/model/protobuf/api/websocket/server_to_client.pb.dart @@ -16,12 +16,9 @@ import 'package:fixnum/fixnum.dart' as $fixnum; import 'package:protobuf/protobuf.dart' as $pb; import 'error.pbenum.dart' as $0; -import 'server_to_client.pbenum.dart'; export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions; -export 'server_to_client.pbenum.dart'; - enum ServerToClient_V { v0, notSet } class ServerToClient extends $pb.GeneratedMessage { @@ -752,90 +749,6 @@ class Response_AddAccountsInvites extends $pb.GeneratedMessage { $pb.PbList get invites => $_getList(0); } -class Response_Transaction extends $pb.GeneratedMessage { - factory Response_Transaction({ - $fixnum.Int64? depositCents, - Response_TransactionTypes? transactionType, - $fixnum.Int64? createdAtUnixTimestamp, - }) { - final result = create(); - if (depositCents != null) result.depositCents = depositCents; - if (transactionType != null) result.transactionType = transactionType; - if (createdAtUnixTimestamp != null) - result.createdAtUnixTimestamp = createdAtUnixTimestamp; - return result; - } - - Response_Transaction._(); - - factory Response_Transaction.fromBuffer($core.List<$core.int> data, - [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(data, registry); - factory Response_Transaction.fromJson($core.String json, - [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(json, registry); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'Response.Transaction', - package: - const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'), - createEmptyInstance: create) - ..aInt64(1, _omitFieldNames ? '' : 'depositCents') - ..aE(2, _omitFieldNames ? '' : 'transactionType', - enumValues: Response_TransactionTypes.values) - ..aInt64(3, _omitFieldNames ? '' : 'createdAtUnixTimestamp') - ..hasRequiredFields = false; - - @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') - Response_Transaction clone() => deepCopy(); - @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') - Response_Transaction copyWith(void Function(Response_Transaction) updates) => - super.copyWith((message) => updates(message as Response_Transaction)) - as Response_Transaction; - - @$core.override - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static Response_Transaction create() => Response_Transaction._(); - @$core.override - Response_Transaction createEmptyInstance() => create(); - @$core.pragma('dart2js:noInline') - static Response_Transaction getDefault() => _defaultInstance ??= - $pb.GeneratedMessage.$_defaultFor(create); - static Response_Transaction? _defaultInstance; - - @$pb.TagNumber(1) - $fixnum.Int64 get depositCents => $_getI64(0); - @$pb.TagNumber(1) - set depositCents($fixnum.Int64 value) => $_setInt64(0, value); - @$pb.TagNumber(1) - $core.bool hasDepositCents() => $_has(0); - @$pb.TagNumber(1) - void clearDepositCents() => $_clearField(1); - - @$pb.TagNumber(2) - Response_TransactionTypes get transactionType => $_getN(1); - @$pb.TagNumber(2) - set transactionType(Response_TransactionTypes value) => $_setField(2, value); - @$pb.TagNumber(2) - $core.bool hasTransactionType() => $_has(1); - @$pb.TagNumber(2) - void clearTransactionType() => $_clearField(2); - - /// Represents seconds of UTC time since Unix epoch - /// 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to - /// 9999-12-31T23:59:59Z inclusive. - @$pb.TagNumber(3) - $fixnum.Int64 get createdAtUnixTimestamp => $_getI64(2); - @$pb.TagNumber(3) - set createdAtUnixTimestamp($fixnum.Int64 value) => $_setInt64(2, value); - @$pb.TagNumber(3) - $core.bool hasCreatedAtUnixTimestamp() => $_has(2); - @$pb.TagNumber(3) - void clearCreatedAtUnixTimestamp() => $_clearField(3); -} - class Response_AdditionalAccount extends $pb.GeneratedMessage { factory Response_AdditionalAccount({ $fixnum.Int64? userId, @@ -905,158 +818,82 @@ class Response_AdditionalAccount extends $pb.GeneratedMessage { void clearPlanId() => $_clearField(3); } -class Response_Voucher extends $pb.GeneratedMessage { - factory Response_Voucher({ - $core.String? voucherId, - $fixnum.Int64? valueCents, - $core.bool? redeemed, - $core.bool? requested, - $fixnum.Int64? createdAtUnixTimestamp, - }) { - final result = create(); - if (voucherId != null) result.voucherId = voucherId; - if (valueCents != null) result.valueCents = valueCents; - if (redeemed != null) result.redeemed = redeemed; - if (requested != null) result.requested = requested; - if (createdAtUnixTimestamp != null) - result.createdAtUnixTimestamp = createdAtUnixTimestamp; - return result; - } +class Response_Deprecated extends $pb.GeneratedMessage { + factory Response_Deprecated() => create(); - Response_Voucher._(); + Response_Deprecated._(); - factory Response_Voucher.fromBuffer($core.List<$core.int> data, + factory Response_Deprecated.fromBuffer($core.List<$core.int> data, [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(data, registry); - factory Response_Voucher.fromJson($core.String json, + factory Response_Deprecated.fromJson($core.String json, [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(json, registry); static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'Response.Voucher', + _omitMessageNames ? '' : 'Response.Deprecated', package: const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'), createEmptyInstance: create) - ..aOS(1, _omitFieldNames ? '' : 'voucherId') - ..aInt64(2, _omitFieldNames ? '' : 'valueCents') - ..aOB(3, _omitFieldNames ? '' : 'redeemed') - ..aOB(4, _omitFieldNames ? '' : 'requested') - ..aInt64(5, _omitFieldNames ? '' : 'createdAtUnixTimestamp') ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') - Response_Voucher clone() => deepCopy(); + Response_Deprecated clone() => deepCopy(); @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') - Response_Voucher copyWith(void Function(Response_Voucher) updates) => - super.copyWith((message) => updates(message as Response_Voucher)) - as Response_Voucher; + Response_Deprecated copyWith(void Function(Response_Deprecated) updates) => + super.copyWith((message) => updates(message as Response_Deprecated)) + as Response_Deprecated; @$core.override $pb.BuilderInfo get info_ => _i; @$core.pragma('dart2js:noInline') - static Response_Voucher create() => Response_Voucher._(); + static Response_Deprecated create() => Response_Deprecated._(); @$core.override - Response_Voucher createEmptyInstance() => create(); + Response_Deprecated createEmptyInstance() => create(); @$core.pragma('dart2js:noInline') - static Response_Voucher getDefault() => _defaultInstance ??= - $pb.GeneratedMessage.$_defaultFor(create); - static Response_Voucher? _defaultInstance; - - @$pb.TagNumber(1) - $core.String get voucherId => $_getSZ(0); - @$pb.TagNumber(1) - set voucherId($core.String value) => $_setString(0, value); - @$pb.TagNumber(1) - $core.bool hasVoucherId() => $_has(0); - @$pb.TagNumber(1) - void clearVoucherId() => $_clearField(1); - - @$pb.TagNumber(2) - $fixnum.Int64 get valueCents => $_getI64(1); - @$pb.TagNumber(2) - set valueCents($fixnum.Int64 value) => $_setInt64(1, value); - @$pb.TagNumber(2) - $core.bool hasValueCents() => $_has(1); - @$pb.TagNumber(2) - void clearValueCents() => $_clearField(2); - - @$pb.TagNumber(3) - $core.bool get redeemed => $_getBF(2); - @$pb.TagNumber(3) - set redeemed($core.bool value) => $_setBool(2, value); - @$pb.TagNumber(3) - $core.bool hasRedeemed() => $_has(2); - @$pb.TagNumber(3) - void clearRedeemed() => $_clearField(3); - - @$pb.TagNumber(4) - $core.bool get requested => $_getBF(3); - @$pb.TagNumber(4) - set requested($core.bool value) => $_setBool(3, value); - @$pb.TagNumber(4) - $core.bool hasRequested() => $_has(3); - @$pb.TagNumber(4) - void clearRequested() => $_clearField(4); - - @$pb.TagNumber(5) - $fixnum.Int64 get createdAtUnixTimestamp => $_getI64(4); - @$pb.TagNumber(5) - set createdAtUnixTimestamp($fixnum.Int64 value) => $_setInt64(4, value); - @$pb.TagNumber(5) - $core.bool hasCreatedAtUnixTimestamp() => $_has(4); - @$pb.TagNumber(5) - void clearCreatedAtUnixTimestamp() => $_clearField(5); + static Response_Deprecated getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static Response_Deprecated? _defaultInstance; } -class Response_Vouchers extends $pb.GeneratedMessage { - factory Response_Vouchers({ - $core.Iterable? vouchers, - }) { - final result = create(); - if (vouchers != null) result.vouchers.addAll(vouchers); - return result; - } +class Response_Transaction extends $pb.GeneratedMessage { + factory Response_Transaction() => create(); - Response_Vouchers._(); + Response_Transaction._(); - factory Response_Vouchers.fromBuffer($core.List<$core.int> data, + factory Response_Transaction.fromBuffer($core.List<$core.int> data, [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(data, registry); - factory Response_Vouchers.fromJson($core.String json, + factory Response_Transaction.fromJson($core.String json, [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(json, registry); static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'Response.Vouchers', + _omitMessageNames ? '' : 'Response.Transaction', package: const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'), createEmptyInstance: create) - ..pPM(1, _omitFieldNames ? '' : 'vouchers', - subBuilder: Response_Voucher.create) ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') - Response_Vouchers clone() => deepCopy(); + Response_Transaction clone() => deepCopy(); @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') - Response_Vouchers copyWith(void Function(Response_Vouchers) updates) => - super.copyWith((message) => updates(message as Response_Vouchers)) - as Response_Vouchers; + Response_Transaction copyWith(void Function(Response_Transaction) updates) => + super.copyWith((message) => updates(message as Response_Transaction)) + as Response_Transaction; @$core.override $pb.BuilderInfo get info_ => _i; @$core.pragma('dart2js:noInline') - static Response_Vouchers create() => Response_Vouchers._(); + static Response_Transaction create() => Response_Transaction._(); @$core.override - Response_Vouchers createEmptyInstance() => create(); + Response_Transaction createEmptyInstance() => create(); @$core.pragma('dart2js:noInline') - static Response_Vouchers getDefault() => _defaultInstance ??= - $pb.GeneratedMessage.$_defaultFor(create); - static Response_Vouchers? _defaultInstance; - - @$pb.TagNumber(1) - $pb.PbList get vouchers => $_getList(0); + static Response_Transaction getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static Response_Transaction? _defaultInstance; } class Response_PlanBallance extends $pb.GeneratedMessage { @@ -1195,85 +1032,6 @@ class Response_PlanBallance extends $pb.GeneratedMessage { void clearAdditionalAccountOwnerId() => $_clearField(8); } -class Response_Location extends $pb.GeneratedMessage { - factory Response_Location({ - $core.String? county, - $core.String? region, - $core.String? city, - }) { - final result = create(); - if (county != null) result.county = county; - if (region != null) result.region = region; - if (city != null) result.city = city; - return result; - } - - Response_Location._(); - - factory Response_Location.fromBuffer($core.List<$core.int> data, - [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(data, registry); - factory Response_Location.fromJson($core.String json, - [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(json, registry); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'Response.Location', - package: - const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'), - createEmptyInstance: create) - ..aOS(1, _omitFieldNames ? '' : 'county') - ..aOS(2, _omitFieldNames ? '' : 'region') - ..aOS(3, _omitFieldNames ? '' : 'city') - ..hasRequiredFields = false; - - @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') - Response_Location clone() => deepCopy(); - @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') - Response_Location copyWith(void Function(Response_Location) updates) => - super.copyWith((message) => updates(message as Response_Location)) - as Response_Location; - - @$core.override - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static Response_Location create() => Response_Location._(); - @$core.override - Response_Location createEmptyInstance() => create(); - @$core.pragma('dart2js:noInline') - static Response_Location getDefault() => _defaultInstance ??= - $pb.GeneratedMessage.$_defaultFor(create); - static Response_Location? _defaultInstance; - - @$pb.TagNumber(1) - $core.String get county => $_getSZ(0); - @$pb.TagNumber(1) - set county($core.String value) => $_setString(0, value); - @$pb.TagNumber(1) - $core.bool hasCounty() => $_has(0); - @$pb.TagNumber(1) - void clearCounty() => $_clearField(1); - - @$pb.TagNumber(2) - $core.String get region => $_getSZ(1); - @$pb.TagNumber(2) - set region($core.String value) => $_setString(1, value); - @$pb.TagNumber(2) - $core.bool hasRegion() => $_has(1); - @$pb.TagNumber(2) - void clearRegion() => $_clearField(2); - - @$pb.TagNumber(3) - $core.String get city => $_getSZ(2); - @$pb.TagNumber(3) - set city($core.String value) => $_setString(2, value); - @$pb.TagNumber(3) - $core.bool hasCity() => $_has(2); - @$pb.TagNumber(3) - void clearCity() => $_clearField(3); -} - class Response_PreKey extends $pb.GeneratedMessage { factory Response_PreKey({ $fixnum.Int64? id, @@ -1754,11 +1512,11 @@ enum Response_Ok_Ok { uploadtoken, userdata, authtoken, - location, + deprecated7, authenticated, plans, planballance, - vouchers, + deprecated11, addaccountsinvites, downloadtokens, signedprekey, @@ -1774,11 +1532,11 @@ class Response_Ok extends $pb.GeneratedMessage { Response_UploadToken? uploadtoken, Response_UserData? userdata, $core.List<$core.int>? authtoken, - Response_Location? location, + Response_Deprecated? deprecated7, Response_Authenticated? authenticated, Response_Plans? plans, Response_PlanBallance? planballance, - Response_Vouchers? vouchers, + Response_Deprecated? deprecated11, Response_AddAccountsInvites? addaccountsinvites, Response_DownloadTokens? downloadtokens, Response_SignedPreKey? signedprekey, @@ -1791,11 +1549,11 @@ class Response_Ok extends $pb.GeneratedMessage { if (uploadtoken != null) result.uploadtoken = uploadtoken; if (userdata != null) result.userdata = userdata; if (authtoken != null) result.authtoken = authtoken; - if (location != null) result.location = location; + if (deprecated7 != null) result.deprecated7 = deprecated7; if (authenticated != null) result.authenticated = authenticated; if (plans != null) result.plans = plans; if (planballance != null) result.planballance = planballance; - if (vouchers != null) result.vouchers = vouchers; + if (deprecated11 != null) result.deprecated11 = deprecated11; if (addaccountsinvites != null) result.addaccountsinvites = addaccountsinvites; if (downloadtokens != null) result.downloadtokens = downloadtokens; @@ -1820,11 +1578,11 @@ class Response_Ok extends $pb.GeneratedMessage { 4: Response_Ok_Ok.uploadtoken, 5: Response_Ok_Ok.userdata, 6: Response_Ok_Ok.authtoken, - 7: Response_Ok_Ok.location, + 7: Response_Ok_Ok.deprecated7, 8: Response_Ok_Ok.authenticated, 9: Response_Ok_Ok.plans, 10: Response_Ok_Ok.planballance, - 11: Response_Ok_Ok.vouchers, + 11: Response_Ok_Ok.deprecated11, 12: Response_Ok_Ok.addaccountsinvites, 13: Response_Ok_Ok.downloadtokens, 14: Response_Ok_Ok.signedprekey, @@ -1847,16 +1605,16 @@ class Response_Ok extends $pb.GeneratedMessage { subBuilder: Response_UserData.create) ..a<$core.List<$core.int>>( 6, _omitFieldNames ? '' : 'authtoken', $pb.PbFieldType.OY) - ..aOM(7, _omitFieldNames ? '' : 'location', - subBuilder: Response_Location.create) + ..aOM(7, _omitFieldNames ? '' : 'deprecated7', + protoName: 'deprecated_7', subBuilder: Response_Deprecated.create) ..aOM(8, _omitFieldNames ? '' : 'authenticated', subBuilder: Response_Authenticated.create) ..aOM(9, _omitFieldNames ? '' : 'plans', subBuilder: Response_Plans.create) ..aOM(10, _omitFieldNames ? '' : 'planballance', subBuilder: Response_PlanBallance.create) - ..aOM(11, _omitFieldNames ? '' : 'vouchers', - subBuilder: Response_Vouchers.create) + ..aOM(11, _omitFieldNames ? '' : 'deprecated11', + protoName: 'deprecated_11', subBuilder: Response_Deprecated.create) ..aOM( 12, _omitFieldNames ? '' : 'addaccountsinvites', subBuilder: Response_AddAccountsInvites.create) @@ -1979,15 +1737,15 @@ class Response_Ok extends $pb.GeneratedMessage { void clearAuthtoken() => $_clearField(6); @$pb.TagNumber(7) - Response_Location get location => $_getN(6); + Response_Deprecated get deprecated7 => $_getN(6); @$pb.TagNumber(7) - set location(Response_Location value) => $_setField(7, value); + set deprecated7(Response_Deprecated value) => $_setField(7, value); @$pb.TagNumber(7) - $core.bool hasLocation() => $_has(6); + $core.bool hasDeprecated7() => $_has(6); @$pb.TagNumber(7) - void clearLocation() => $_clearField(7); + void clearDeprecated7() => $_clearField(7); @$pb.TagNumber(7) - Response_Location ensureLocation() => $_ensure(6); + Response_Deprecated ensureDeprecated7() => $_ensure(6); @$pb.TagNumber(8) Response_Authenticated get authenticated => $_getN(7); @@ -2023,15 +1781,15 @@ class Response_Ok extends $pb.GeneratedMessage { Response_PlanBallance ensurePlanballance() => $_ensure(9); @$pb.TagNumber(11) - Response_Vouchers get vouchers => $_getN(10); + Response_Deprecated get deprecated11 => $_getN(10); @$pb.TagNumber(11) - set vouchers(Response_Vouchers value) => $_setField(11, value); + set deprecated11(Response_Deprecated value) => $_setField(11, value); @$pb.TagNumber(11) - $core.bool hasVouchers() => $_has(10); + $core.bool hasDeprecated11() => $_has(10); @$pb.TagNumber(11) - void clearVouchers() => $_clearField(11); + void clearDeprecated11() => $_clearField(11); @$pb.TagNumber(11) - Response_Vouchers ensureVouchers() => $_ensure(10); + Response_Deprecated ensureDeprecated11() => $_ensure(10); @$pb.TagNumber(12) Response_AddAccountsInvites get addaccountsinvites => $_getN(11); diff --git a/lib/src/model/protobuf/api/websocket/server_to_client.pbenum.dart b/lib/src/model/protobuf/api/websocket/server_to_client.pbenum.dart index 6f9554a5..86f414a5 100644 --- a/lib/src/model/protobuf/api/websocket/server_to_client.pbenum.dart +++ b/lib/src/model/protobuf/api/websocket/server_to_client.pbenum.dart @@ -9,48 +9,3 @@ // ignore_for_file: curly_braces_in_flow_control_structures // ignore_for_file: deprecated_member_use_from_same_package, library_prefixes // ignore_for_file: non_constant_identifier_names, prefer_relative_imports - -import 'dart:core' as $core; - -import 'package:protobuf/protobuf.dart' as $pb; - -class Response_TransactionTypes extends $pb.ProtobufEnum { - static const Response_TransactionTypes Refund = - Response_TransactionTypes._(0, _omitEnumNames ? '' : 'Refund'); - static const Response_TransactionTypes VoucherRedeemed = - Response_TransactionTypes._(1, _omitEnumNames ? '' : 'VoucherRedeemed'); - static const Response_TransactionTypes VoucherCreated = - Response_TransactionTypes._(2, _omitEnumNames ? '' : 'VoucherCreated'); - static const Response_TransactionTypes Cash = - Response_TransactionTypes._(3, _omitEnumNames ? '' : 'Cash'); - static const Response_TransactionTypes PlanUpgrade = - Response_TransactionTypes._(4, _omitEnumNames ? '' : 'PlanUpgrade'); - static const Response_TransactionTypes Unknown = - Response_TransactionTypes._(5, _omitEnumNames ? '' : 'Unknown'); - static const Response_TransactionTypes ThanksForTesting = - Response_TransactionTypes._(6, _omitEnumNames ? '' : 'ThanksForTesting'); - static const Response_TransactionTypes AutoRenewal = - Response_TransactionTypes._(7, _omitEnumNames ? '' : 'AutoRenewal'); - - static const $core.List values = - [ - Refund, - VoucherRedeemed, - VoucherCreated, - Cash, - PlanUpgrade, - Unknown, - ThanksForTesting, - AutoRenewal, - ]; - - static final $core.List _byValue = - $pb.ProtobufEnum.$_initByValueList(values, 7); - static Response_TransactionTypes? valueOf($core.int value) => - value < 0 || value >= _byValue.length ? null : _byValue[value]; - - const Response_TransactionTypes._(super.value, super.name); -} - -const $core.bool _omitEnumNames = - $core.bool.fromEnvironment('protobuf.omit_enum_names'); diff --git a/lib/src/model/protobuf/api/websocket/server_to_client.pbjson.dart b/lib/src/model/protobuf/api/websocket/server_to_client.pbjson.dart index d71c7ea1..5a936e2b 100644 --- a/lib/src/model/protobuf/api/websocket/server_to_client.pbjson.dart +++ b/lib/src/model/protobuf/api/websocket/server_to_client.pbjson.dart @@ -166,12 +166,10 @@ const Response$json = { Response_Plans$json, Response_AddAccountsInvite$json, Response_AddAccountsInvites$json, - Response_Transaction$json, Response_AdditionalAccount$json, - Response_Voucher$json, - Response_Vouchers$json, + Response_Deprecated$json, + Response_Transaction$json, Response_PlanBallance$json, - Response_Location$json, Response_PreKey$json, Response_SignedPreKey$json, Response_UserData$json, @@ -180,7 +178,6 @@ const Response$json = { Response_ProofOfWork$json, Response_Ok$json ], - '4': [Response_TransactionTypes$json], '8': [ {'1': 'Response'}, ], @@ -285,29 +282,6 @@ const Response_AddAccountsInvites$json = { ], }; -@$core.Deprecated('Use responseDescriptor instead') -const Response_Transaction$json = { - '1': 'Transaction', - '2': [ - {'1': 'deposit_cents', '3': 1, '4': 1, '5': 3, '10': 'depositCents'}, - { - '1': 'transaction_type', - '3': 2, - '4': 1, - '5': 14, - '6': '.server_to_client.Response.TransactionTypes', - '10': 'transactionType' - }, - { - '1': 'created_at_unix_timestamp', - '3': 3, - '4': 1, - '5': 3, - '10': 'createdAtUnixTimestamp' - }, - ], -}; - @$core.Deprecated('Use responseDescriptor instead') const Response_AdditionalAccount$json = { '1': 'AdditionalAccount', @@ -318,36 +292,13 @@ const Response_AdditionalAccount$json = { }; @$core.Deprecated('Use responseDescriptor instead') -const Response_Voucher$json = { - '1': 'Voucher', - '2': [ - {'1': 'voucher_id', '3': 1, '4': 1, '5': 9, '10': 'voucherId'}, - {'1': 'value_cents', '3': 2, '4': 1, '5': 3, '10': 'valueCents'}, - {'1': 'redeemed', '3': 3, '4': 1, '5': 8, '10': 'redeemed'}, - {'1': 'requested', '3': 4, '4': 1, '5': 8, '10': 'requested'}, - { - '1': 'created_at_unix_timestamp', - '3': 5, - '4': 1, - '5': 3, - '10': 'createdAtUnixTimestamp' - }, - ], +const Response_Deprecated$json = { + '1': 'Deprecated', }; @$core.Deprecated('Use responseDescriptor instead') -const Response_Vouchers$json = { - '1': 'Vouchers', - '2': [ - { - '1': 'vouchers', - '3': 1, - '4': 3, - '5': 11, - '6': '.server_to_client.Response.Voucher', - '10': 'vouchers' - }, - ], +const Response_Transaction$json = { + '1': 'Transaction', }; @$core.Deprecated('Use responseDescriptor instead') @@ -429,16 +380,6 @@ const Response_PlanBallance$json = { ], }; -@$core.Deprecated('Use responseDescriptor instead') -const Response_Location$json = { - '1': 'Location', - '2': [ - {'1': 'county', '3': 1, '4': 1, '5': 9, '10': 'county'}, - {'1': 'region', '3': 2, '4': 1, '5': 9, '10': 'region'}, - {'1': 'city', '3': 3, '4': 1, '5': 9, '10': 'city'}, - ], -}; - @$core.Deprecated('Use responseDescriptor instead') const Response_PreKey$json = { '1': 'PreKey', @@ -602,13 +543,13 @@ const Response_Ok$json = { }, {'1': 'authtoken', '3': 6, '4': 1, '5': 12, '9': 0, '10': 'authtoken'}, { - '1': 'location', + '1': 'deprecated_7', '3': 7, '4': 1, '5': 11, - '6': '.server_to_client.Response.Location', + '6': '.server_to_client.Response.Deprecated', '9': 0, - '10': 'location' + '10': 'deprecated7' }, { '1': 'authenticated', @@ -638,13 +579,13 @@ const Response_Ok$json = { '10': 'planballance' }, { - '1': 'vouchers', + '1': 'deprecated_11', '3': 11, '4': 1, '5': 11, - '6': '.server_to_client.Response.Vouchers', + '6': '.server_to_client.Response.Deprecated', '9': 0, - '10': 'vouchers' + '10': 'deprecated11' }, { '1': 'addaccountsinvites', @@ -688,21 +629,6 @@ const Response_Ok$json = { ], }; -@$core.Deprecated('Use responseDescriptor instead') -const Response_TransactionTypes$json = { - '1': 'TransactionTypes', - '2': [ - {'1': 'Refund', '2': 0}, - {'1': 'VoucherRedeemed', '2': 1}, - {'1': 'VoucherCreated', '2': 2}, - {'1': 'Cash', '2': 3}, - {'1': 'PlanUpgrade', '2': 4}, - {'1': 'Unknown', '2': 5}, - {'1': 'ThanksForTesting', '2': 6}, - {'1': 'AutoRenewal', '2': 7}, - ], -}; - /// Descriptor for `Response`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List responseDescriptor = $convert.base64Decode( 'CghSZXNwb25zZRIvCgJvaxgBIAEoCzIdLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuT2tIAF' @@ -720,64 +646,53 @@ final $typed_data.Uint8List responseDescriptor = $convert.base64Decode( 'bnQuUmVzcG9uc2UuUGxhblIFcGxhbnMaTQoRQWRkQWNjb3VudHNJbnZpdGUSFwoHcGxhbl9pZB' 'gBIAEoCVIGcGxhbklkEh8KC2ludml0ZV9jb2RlGAIgASgJUgppbnZpdGVDb2RlGlwKEkFkZEFj' 'Y291bnRzSW52aXRlcxJGCgdpbnZpdGVzGAEgAygLMiwuc2VydmVyX3RvX2NsaWVudC5SZXNwb2' - '5zZS5BZGRBY2NvdW50c0ludml0ZVIHaW52aXRlcxrFAQoLVHJhbnNhY3Rpb24SIwoNZGVwb3Np' - 'dF9jZW50cxgBIAEoA1IMZGVwb3NpdENlbnRzElYKEHRyYW5zYWN0aW9uX3R5cGUYAiABKA4yKy' - '5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlRyYW5zYWN0aW9uVHlwZXNSD3RyYW5zYWN0aW9u' - 'VHlwZRI5ChljcmVhdGVkX2F0X3VuaXhfdGltZXN0YW1wGAMgASgDUhZjcmVhdGVkQXRVbml4VG' - 'ltZXN0YW1wGkUKEUFkZGl0aW9uYWxBY2NvdW50EhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBIX' - 'CgdwbGFuX2lkGAMgASgJUgZwbGFuSWQavgEKB1ZvdWNoZXISHQoKdm91Y2hlcl9pZBgBIAEoCV' - 'IJdm91Y2hlcklkEh8KC3ZhbHVlX2NlbnRzGAIgASgDUgp2YWx1ZUNlbnRzEhoKCHJlZGVlbWVk' - 'GAMgASgIUghyZWRlZW1lZBIcCglyZXF1ZXN0ZWQYBCABKAhSCXJlcXVlc3RlZBI5ChljcmVhdG' - 'VkX2F0X3VuaXhfdGltZXN0YW1wGAUgASgDUhZjcmVhdGVkQXRVbml4VGltZXN0YW1wGkoKCFZv' - 'dWNoZXJzEj4KCHZvdWNoZXJzGAEgAygLMiIuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5Wb3' - 'VjaGVyUgh2b3VjaGVycxqXBQoMUGxhbkJhbGxhbmNlEkAKHXVzZWRfZGFpbHlfbWVkaWFfdXBs' - 'b2FkX2xpbWl0GAEgASgDUhl1c2VkRGFpbHlNZWRpYVVwbG9hZExpbWl0Ej4KHHVzZWRfdXBsb2' - 'FkX21lZGlhX3NpemVfbGltaXQYAiABKANSGHVzZWRVcGxvYWRNZWRpYVNpemVMaW1pdBIzChNw' - 'YXltZW50X3BlcmlvZF9kYXlzGAMgASgDSABSEXBheW1lbnRQZXJpb2REYXlziAEBEksKIGxhc3' - 'RfcGF5bWVudF9kb25lX3VuaXhfdGltZXN0YW1wGAQgASgDSAFSHGxhc3RQYXltZW50RG9uZVVu' - 'aXhUaW1lc3RhbXCIAQESSgoMdHJhbnNhY3Rpb25zGAUgAygLMiYuc2VydmVyX3RvX2NsaWVudC' - '5SZXNwb25zZS5UcmFuc2FjdGlvblIMdHJhbnNhY3Rpb25zEl0KE2FkZGl0aW9uYWxfYWNjb3Vu' - 'dHMYBiADKAsyLC5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLkFkZGl0aW9uYWxBY2NvdW50Uh' - 'JhZGRpdGlvbmFsQWNjb3VudHMSJgoMYXV0b19yZW5ld2FsGAcgASgISAJSC2F1dG9SZW5ld2Fs' - 'iAEBEkIKG2FkZGl0aW9uYWxfYWNjb3VudF9vd25lcl9pZBgIIAEoA0gDUhhhZGRpdGlvbmFsQW' - 'Njb3VudE93bmVySWSIAQFCFgoUX3BheW1lbnRfcGVyaW9kX2RheXNCIwohX2xhc3RfcGF5bWVu' - 'dF9kb25lX3VuaXhfdGltZXN0YW1wQg8KDV9hdXRvX3JlbmV3YWxCHgocX2FkZGl0aW9uYWxfYW' - 'Njb3VudF9vd25lcl9pZBpOCghMb2NhdGlvbhIWCgZjb3VudHkYASABKAlSBmNvdW50eRIWCgZy' - 'ZWdpb24YAiABKAlSBnJlZ2lvbhISCgRjaXR5GAMgASgJUgRjaXR5GjAKBlByZUtleRIOCgJpZB' - 'gBIAEoA1ICaWQSFgoGcHJla2V5GAIgASgMUgZwcmVrZXkalQEKDFNpZ25lZFByZUtleRIoChBz' - 'aWduZWRfcHJla2V5X2lkGAEgASgDUg5zaWduZWRQcmVrZXlJZBIjCg1zaWduZWRfcHJla2V5GA' - 'IgASgMUgxzaWduZWRQcmVrZXkSNgoXc2lnbmVkX3ByZWtleV9zaWduYXR1cmUYAyABKAxSFXNp' - 'Z25lZFByZWtleVNpZ25hdHVyZRr2AwoIVXNlckRhdGESFwoHdXNlcl9pZBgBIAEoA1IGdXNlck' - 'lkEjsKB3ByZWtleXMYAiADKAsyIS5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlByZUtleVIH' - 'cHJla2V5cxIfCgh1c2VybmFtZRgHIAEoDEgAUgh1c2VybmFtZYgBARIzChNwdWJsaWNfaWRlbn' - 'RpdHlfa2V5GAMgASgMSAFSEXB1YmxpY0lkZW50aXR5S2V5iAEBEigKDXNpZ25lZF9wcmVrZXkY' - 'BCABKAxIAlIMc2lnbmVkUHJla2V5iAEBEjsKF3NpZ25lZF9wcmVrZXlfc2lnbmF0dXJlGAUgAS' - 'gMSANSFXNpZ25lZFByZWtleVNpZ25hdHVyZYgBARItChBzaWduZWRfcHJla2V5X2lkGAYgASgD' - 'SARSDnNpZ25lZFByZWtleUlkiAEBEiwKD3JlZ2lzdHJhdGlvbl9pZBgIIAEoA0gFUg5yZWdpc3' - 'RyYXRpb25JZIgBAUILCglfdXNlcm5hbWVCFgoUX3B1YmxpY19pZGVudGl0eV9rZXlCEAoOX3Np' - 'Z25lZF9wcmVrZXlCGgoYX3NpZ25lZF9wcmVrZXlfc2lnbmF0dXJlQhMKEV9zaWduZWRfcHJla2' - 'V5X2lkQhIKEF9yZWdpc3RyYXRpb25faWQaWQoLVXBsb2FkVG9rZW4SIQoMdXBsb2FkX3Rva2Vu' - 'GAEgASgMUgt1cGxvYWRUb2tlbhInCg9kb3dubG9hZF90b2tlbnMYAiADKAxSDmRvd25sb2FkVG' - '9rZW5zGjkKDkRvd25sb2FkVG9rZW5zEicKD2Rvd25sb2FkX3Rva2VucxgBIAMoDFIOZG93bmxv' - 'YWRUb2tlbnMaRQoLUHJvb2ZPZldvcmsSFgoGcHJlZml4GAEgASgJUgZwcmVmaXgSHgoKZGlmZm' - 'ljdWx0eRgCIAEoA1IKZGlmZmljdWx0eRrDBwoCT2sSFAoETm9uZRgBIAEoCEgAUgROb25lEhgK' - 'BnVzZXJpZBgCIAEoA0gAUgZ1c2VyaWQSJgoNYXV0aGNoYWxsZW5nZRgDIAEoDEgAUg1hdXRoY2' - 'hhbGxlbmdlEkoKC3VwbG9hZHRva2VuGAQgASgLMiYuc2VydmVyX3RvX2NsaWVudC5SZXNwb25z' - 'ZS5VcGxvYWRUb2tlbkgAUgt1cGxvYWR0b2tlbhJBCgh1c2VyZGF0YRgFIAEoCzIjLnNlcnZlcl' - '90b19jbGllbnQuUmVzcG9uc2UuVXNlckRhdGFIAFIIdXNlcmRhdGESHgoJYXV0aHRva2VuGAYg' - 'ASgMSABSCWF1dGh0b2tlbhJBCghsb2NhdGlvbhgHIAEoCzIjLnNlcnZlcl90b19jbGllbnQuUm' - 'VzcG9uc2UuTG9jYXRpb25IAFIIbG9jYXRpb24SUAoNYXV0aGVudGljYXRlZBgIIAEoCzIoLnNl' - 'cnZlcl90b19jbGllbnQuUmVzcG9uc2UuQXV0aGVudGljYXRlZEgAUg1hdXRoZW50aWNhdGVkEj' - 'gKBXBsYW5zGAkgASgLMiAuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5QbGFuc0gAUgVwbGFu' - 'cxJNCgxwbGFuYmFsbGFuY2UYCiABKAsyJy5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlBsYW' - '5CYWxsYW5jZUgAUgxwbGFuYmFsbGFuY2USQQoIdm91Y2hlcnMYCyABKAsyIy5zZXJ2ZXJfdG9f' - 'Y2xpZW50LlJlc3BvbnNlLlZvdWNoZXJzSABSCHZvdWNoZXJzEl8KEmFkZGFjY291bnRzaW52aX' - 'RlcxgMIAEoCzItLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuQWRkQWNjb3VudHNJbnZpdGVz' - 'SABSEmFkZGFjY291bnRzaW52aXRlcxJTCg5kb3dubG9hZHRva2VucxgNIAEoCzIpLnNlcnZlcl' - '90b19jbGllbnQuUmVzcG9uc2UuRG93bmxvYWRUb2tlbnNIAFIOZG93bmxvYWR0b2tlbnMSTQoM' - 'c2lnbmVkcHJla2V5GA4gASgLMicuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5TaWduZWRQcm' - 'VLZXlIAFIMc2lnbmVkcHJla2V5EkoKC3Byb29mT2ZXb3JrGA8gASgLMiYuc2VydmVyX3RvX2Ns' - 'aWVudC5SZXNwb25zZS5Qcm9vZk9mV29ya0gAUgtwcm9vZk9mV29ya0IECgJPayKWAQoQVHJhbn' - 'NhY3Rpb25UeXBlcxIKCgZSZWZ1bmQQABITCg9Wb3VjaGVyUmVkZWVtZWQQARISCg5Wb3VjaGVy' - 'Q3JlYXRlZBACEggKBENhc2gQAxIPCgtQbGFuVXBncmFkZRAEEgsKB1Vua25vd24QBRIUChBUaG' - 'Fua3NGb3JUZXN0aW5nEAYSDwoLQXV0b1JlbmV3YWwQB0IKCghSZXNwb25zZQ=='); + '5zZS5BZGRBY2NvdW50c0ludml0ZVIHaW52aXRlcxpFChFBZGRpdGlvbmFsQWNjb3VudBIXCgd1' + 'c2VyX2lkGAEgASgDUgZ1c2VySWQSFwoHcGxhbl9pZBgDIAEoCVIGcGxhbklkGgwKCkRlcHJlY2' + 'F0ZWQaDQoLVHJhbnNhY3Rpb24alwUKDFBsYW5CYWxsYW5jZRJACh11c2VkX2RhaWx5X21lZGlh' + 'X3VwbG9hZF9saW1pdBgBIAEoA1IZdXNlZERhaWx5TWVkaWFVcGxvYWRMaW1pdBI+Chx1c2VkX3' + 'VwbG9hZF9tZWRpYV9zaXplX2xpbWl0GAIgASgDUhh1c2VkVXBsb2FkTWVkaWFTaXplTGltaXQS' + 'MwoTcGF5bWVudF9wZXJpb2RfZGF5cxgDIAEoA0gAUhFwYXltZW50UGVyaW9kRGF5c4gBARJLCi' + 'BsYXN0X3BheW1lbnRfZG9uZV91bml4X3RpbWVzdGFtcBgEIAEoA0gBUhxsYXN0UGF5bWVudERv' + 'bmVVbml4VGltZXN0YW1wiAEBEkoKDHRyYW5zYWN0aW9ucxgFIAMoCzImLnNlcnZlcl90b19jbG' + 'llbnQuUmVzcG9uc2UuVHJhbnNhY3Rpb25SDHRyYW5zYWN0aW9ucxJdChNhZGRpdGlvbmFsX2Fj' + 'Y291bnRzGAYgAygLMiwuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5BZGRpdGlvbmFsQWNjb3' + 'VudFISYWRkaXRpb25hbEFjY291bnRzEiYKDGF1dG9fcmVuZXdhbBgHIAEoCEgCUgthdXRvUmVu' + 'ZXdhbIgBARJCChthZGRpdGlvbmFsX2FjY291bnRfb3duZXJfaWQYCCABKANIA1IYYWRkaXRpb2' + '5hbEFjY291bnRPd25lcklkiAEBQhYKFF9wYXltZW50X3BlcmlvZF9kYXlzQiMKIV9sYXN0X3Bh' + 'eW1lbnRfZG9uZV91bml4X3RpbWVzdGFtcEIPCg1fYXV0b19yZW5ld2FsQh4KHF9hZGRpdGlvbm' + 'FsX2FjY291bnRfb3duZXJfaWQaMAoGUHJlS2V5Eg4KAmlkGAEgASgDUgJpZBIWCgZwcmVrZXkY' + 'AiABKAxSBnByZWtleRqVAQoMU2lnbmVkUHJlS2V5EigKEHNpZ25lZF9wcmVrZXlfaWQYASABKA' + 'NSDnNpZ25lZFByZWtleUlkEiMKDXNpZ25lZF9wcmVrZXkYAiABKAxSDHNpZ25lZFByZWtleRI2' + 'ChdzaWduZWRfcHJla2V5X3NpZ25hdHVyZRgDIAEoDFIVc2lnbmVkUHJla2V5U2lnbmF0dXJlGv' + 'YDCghVc2VyRGF0YRIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQSOwoHcHJla2V5cxgCIAMoCzIh' + 'LnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuUHJlS2V5UgdwcmVrZXlzEh8KCHVzZXJuYW1lGA' + 'cgASgMSABSCHVzZXJuYW1liAEBEjMKE3B1YmxpY19pZGVudGl0eV9rZXkYAyABKAxIAVIRcHVi' + 'bGljSWRlbnRpdHlLZXmIAQESKAoNc2lnbmVkX3ByZWtleRgEIAEoDEgCUgxzaWduZWRQcmVrZX' + 'mIAQESOwoXc2lnbmVkX3ByZWtleV9zaWduYXR1cmUYBSABKAxIA1IVc2lnbmVkUHJla2V5U2ln' + 'bmF0dXJliAEBEi0KEHNpZ25lZF9wcmVrZXlfaWQYBiABKANIBFIOc2lnbmVkUHJla2V5SWSIAQ' + 'ESLAoPcmVnaXN0cmF0aW9uX2lkGAggASgDSAVSDnJlZ2lzdHJhdGlvbklkiAEBQgsKCV91c2Vy' + 'bmFtZUIWChRfcHVibGljX2lkZW50aXR5X2tleUIQCg5fc2lnbmVkX3ByZWtleUIaChhfc2lnbm' + 'VkX3ByZWtleV9zaWduYXR1cmVCEwoRX3NpZ25lZF9wcmVrZXlfaWRCEgoQX3JlZ2lzdHJhdGlv' + 'bl9pZBpZCgtVcGxvYWRUb2tlbhIhCgx1cGxvYWRfdG9rZW4YASABKAxSC3VwbG9hZFRva2VuEi' + 'cKD2Rvd25sb2FkX3Rva2VucxgCIAMoDFIOZG93bmxvYWRUb2tlbnMaOQoORG93bmxvYWRUb2tl' + 'bnMSJwoPZG93bmxvYWRfdG9rZW5zGAEgAygMUg5kb3dubG9hZFRva2VucxpFCgtQcm9vZk9mV2' + '9yaxIWCgZwcmVmaXgYASABKAlSBnByZWZpeBIeCgpkaWZmaWN1bHR5GAIgASgDUgpkaWZmaWN1' + 'bHR5GtcHCgJPaxIUCgROb25lGAEgASgISABSBE5vbmUSGAoGdXNlcmlkGAIgASgDSABSBnVzZX' + 'JpZBImCg1hdXRoY2hhbGxlbmdlGAMgASgMSABSDWF1dGhjaGFsbGVuZ2USSgoLdXBsb2FkdG9r' + 'ZW4YBCABKAsyJi5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlVwbG9hZFRva2VuSABSC3VwbG' + '9hZHRva2VuEkEKCHVzZXJkYXRhGAUgASgLMiMuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5V' + 'c2VyRGF0YUgAUgh1c2VyZGF0YRIeCglhdXRodG9rZW4YBiABKAxIAFIJYXV0aHRva2VuEkoKDG' + 'RlcHJlY2F0ZWRfNxgHIAEoCzIlLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuRGVwcmVjYXRl' + 'ZEgAUgtkZXByZWNhdGVkNxJQCg1hdXRoZW50aWNhdGVkGAggASgLMiguc2VydmVyX3RvX2NsaW' + 'VudC5SZXNwb25zZS5BdXRoZW50aWNhdGVkSABSDWF1dGhlbnRpY2F0ZWQSOAoFcGxhbnMYCSAB' + 'KAsyIC5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlBsYW5zSABSBXBsYW5zEk0KDHBsYW5iYW' + 'xsYW5jZRgKIAEoCzInLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuUGxhbkJhbGxhbmNlSABS' + 'DHBsYW5iYWxsYW5jZRJMCg1kZXByZWNhdGVkXzExGAsgASgLMiUuc2VydmVyX3RvX2NsaWVudC' + '5SZXNwb25zZS5EZXByZWNhdGVkSABSDGRlcHJlY2F0ZWQxMRJfChJhZGRhY2NvdW50c2ludml0' + 'ZXMYDCABKAsyLS5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLkFkZEFjY291bnRzSW52aXRlc0' + 'gAUhJhZGRhY2NvdW50c2ludml0ZXMSUwoOZG93bmxvYWR0b2tlbnMYDSABKAsyKS5zZXJ2ZXJf' + 'dG9fY2xpZW50LlJlc3BvbnNlLkRvd25sb2FkVG9rZW5zSABSDmRvd25sb2FkdG9rZW5zEk0KDH' + 'NpZ25lZHByZWtleRgOIAEoCzInLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuU2lnbmVkUHJl' + 'S2V5SABSDHNpZ25lZHByZWtleRJKCgtwcm9vZk9mV29yaxgPIAEoCzImLnNlcnZlcl90b19jbG' + 'llbnQuUmVzcG9uc2UuUHJvb2ZPZldvcmtIAFILcHJvb2ZPZldvcmtCBAoCT2tCCgoIUmVzcG9u' + 'c2U='); diff --git a/lib/src/providers/routing.provider.dart b/lib/src/providers/routing.provider.dart index cccb4c4c..648ba97f 100644 --- a/lib/src/providers/routing.provider.dart +++ b/lib/src/providers/routing.provider.dart @@ -16,7 +16,6 @@ import 'package:twonly/src/visual/views/onboarding/recover.view.dart'; import 'package:twonly/src/visual/views/public_profile.view.dart'; import 'package:twonly/src/visual/views/settings/account.view.dart'; import 'package:twonly/src/visual/views/settings/appearance.view.dart'; -import 'package:twonly/src/visual/views/settings/backup/backup_server.view.dart'; import 'package:twonly/src/visual/views/settings/backup/backup_settings.view.dart'; import 'package:twonly/src/visual/views/settings/backup/backup_setup.view.dart'; import 'package:twonly/src/visual/views/settings/chat/chat_reactions.view.dart'; @@ -165,10 +164,6 @@ final routerProvider = GoRouter( path: 'backup', builder: (context, state) => const BackupView(), routes: [ - GoRoute( - path: 'server', - builder: (context, state) => const BackupServerView(), - ), GoRoute( path: 'recovery', builder: (context, state) => const BackupRecoveryView(), diff --git a/lib/src/services/api.service.dart b/lib/src/services/api.service.dart index 7cebbbad..64224cdb 100644 --- a/lib/src/services/api.service.dart +++ b/lib/src/services/api.service.dart @@ -15,6 +15,7 @@ import 'package:flutter/foundation.dart'; import 'package:libsignal_protocol_dart/src/ecc/ed25519.dart'; import 'package:mutex/mutex.dart'; import 'package:package_info_plus/package_info_plus.dart'; +import 'package:twonly/core/bridge/wrapper/key_manager.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/constants/secure_storage.keys.dart'; @@ -31,7 +32,7 @@ import 'package:twonly/src/services/api/messages.api.dart'; import 'package:twonly/src/services/api/server_messages.api.dart'; import 'package:twonly/src/services/api/utils.api.dart'; import 'package:twonly/src/services/flame.service.dart'; -import 'package:twonly/src/services/group.services.dart'; +import 'package:twonly/src/services/group.service.dart'; import 'package:twonly/src/services/notifications/fcm.notifications.dart'; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/services/signal/identity.signal.dart'; @@ -60,6 +61,8 @@ class ApiService { // final String apiHost = kReleaseMode ? 'api.twonly.eu' : 'dev.twonly.eu'; final String apiSecure = kReleaseMode ? 's' : 's'; + String get apiEndpoint => 'http$apiSecure://$apiHost/api/'; + final _planUpdateController = StreamController.broadcast(); Stream get onPlanUpdated => _planUpdateController.stream; @@ -92,6 +95,7 @@ class ApiService { try { final channel = IOWebSocketChannel.connect( Uri.parse(apiUrl), + pingInterval: const Duration(seconds: 30), ); _channel = channel; _channel!.stream.listen(_onData, onDone: _onDone, onError: _onError); @@ -122,7 +126,7 @@ class ApiService { twonlyDB.markUpdated(); unawaited(syncFlameCounters()); unawaited(setupNotificationWithUsers()); - unawaited(signalHandleNewServerConnection()); + unawaited(SignalIdentityService.onAuthenticated()); resetResyncedUsers(); resetUserDiscoveryRequestUpdates(); unawaited(fetchGroupStatesForUnjoinedGroups()); @@ -244,11 +248,11 @@ class ApiService { try { final msg = server.ServerToClient.fromBuffer(msgBuffer as Uint8List); if (msg.v0.hasResponse()) { - await removeFromRetransmissionBuffer(msg.v0.seq); final completer = _pendingRequests.remove(msg.v0.seq); if (completer != null && !completer.isCompleted) { completer.complete(msg); } + unawaited(removeFromRetransmissionBuffer(msg.v0.seq)); } else { unawaited(handleServerMessage(msg)); } @@ -414,6 +418,7 @@ class ApiService { ), ); } + await twonlyDB.receiptsDao.deleteReceiptForUser(contactId); } } return res; @@ -450,6 +455,21 @@ class ApiService { await onAuthenticated(); } else { unawaited(onAuthenticated()); + + try { + Log.info('Switching authentication to login token'); + final loginToken = await RustKeyManager.getLoginToken(); + final res = await _setLoginToken(loginToken); + if (res.isSuccess) { + Log.info('Switch was successfully.'); + await UserService.update((u) => u.canUseLoginTokenForAuth = true); + await SecureStorage.instance.delete( + key: SecureStorageKeys.apiAuthToken, + ); + } + } catch (e) { + Log.error(e); + } } return true; } @@ -466,16 +486,62 @@ class ApiService { return false; } + Future tryAuthenticateWithLoginToken() async { + try { + final loginToken = await RustKeyManager.getLoginToken(); + + final authenticate = Handshake_AuthenticateWithLoginToken() + ..userId = Int64(userService.currentUser.userId) + ..appVersion = (await PackageInfo.fromPlatform()).version + ..deviceId = Int64(userService.currentUser.deviceId) + ..inBackground = AppState.isInBackgroundTask + ..secretLoginToken = loginToken.toList(); + + final handshake = Handshake()..authenticateWithLoginToken = authenticate; + final req = createClientToServerFromHandshake(handshake); + + final result = await sendRequestSync(req, authenticated: false); + + if (result.isSuccess) { + Log.info('websocket is authenticated'); + isAuthenticated = true; + if (AppState.isInBackgroundTask) { + await onAuthenticated(); + } else { + unawaited(onAuthenticated()); + } + return true; + } + if (result.isError) { + if (result.error != ErrorCode.AuthTokenNotValid && + result.error != ErrorCode.ForegroundSessionConnected) { + Log.error( + 'got error while authenticating to the server: ${result.error}', + ); + return false; + } + } + } catch (e) { + Log.error(e); + } + return false; + } + Future authenticate() async { return lockAuthentication.protect(() async { if (isAuthenticated) return; - if (await getSignalIdentity() == null) { - Log.error('Signal identity not found.'); + + if (!userService.isUserCreated) { return; } if (!userService.isUserCreated) return; + if (userService.currentUser.canUseLoginTokenForAuth) { + await tryAuthenticateWithLoginToken(); + return; + } + if (await tryAuthenticateWithToken()) { return; } @@ -542,6 +608,8 @@ class ApiService { final signedPreKey = (await signalStore.loadSignedPreKeys())[0]; + final loginToken = await RustKeyManager.getLoginToken(); + final register = Handshake_Register() ..username = username ..publicIdentityKey = (await signalStore.getIdentityKeyPair()) @@ -552,6 +620,7 @@ class ApiService { ..signedPrekeySignature = signedPreKey.signature ..signedPrekeyId = Int64(signedPreKey.id) ..langCode = ui.PlatformDispatcher.instance.locale.languageCode + ..loginToken = loginToken ..proofOfWork = Int64(proofOfWorkResult) ..isIos = Platform.isIOS; @@ -617,13 +686,28 @@ class ApiService { return sendRequestSync(req, ensureRetransmission: true); } - Future getCurrentLocation() async { - final get = ApplicationData_GetLocation(); - final appData = ApplicationData()..getLocation = get; + Future _setLoginToken(List token) async { + final get = ApplicationData_SetLoginToken()..loginToken = token; + final appData = ApplicationData()..setLoginToken = get; final req = createClientToServerFromApplicationData(appData); return sendRequestSync(req); } + Future getUserIdFromUsername(String username) async { + final appData = Handshake( + getUseridByUsername: Handshake_GetUserIdByUsername(username: username), + ); + final req = createClientToServerFromHandshake(appData); + final res = await sendRequestSync(req); + if (res.isSuccess) { + final ok = res.value as server.Response_Ok; + if (ok.hasUserid()) { + return ok.userid.toInt(); + } + } + return null; + } + Future getUserData(String username) async { final get = ApplicationData_GetUserByUsername()..username = username; final appData = ApplicationData()..getUserByUsername = get; @@ -652,27 +736,6 @@ class ApiService { return null; } - Future getVoucherList() async { - final get = ApplicationData_GetVouchers(); - final appData = ApplicationData()..getVouchers = get; - final req = createClientToServerFromApplicationData(appData); - final res = await sendRequestSync(req); - if (res.isSuccess) { - final ok = res.value as server.Response_Ok; - if (ok.hasVouchers()) { - return ok.vouchers; - } - } - return null; - } - - Future updatePlanOptions(bool autoRenewal) async { - final get = ApplicationData_UpdatePlanOptions()..autoRenewal = autoRenewal; - final appData = ApplicationData()..updatePlanOptions = get; - final req = createClientToServerFromApplicationData(appData); - return sendRequestSync(req); - } - Future removeAdditionalUser(Int64 userId) async { final get = ApplicationData_RemoveAdditionalUser()..userId = userId; final appData = ApplicationData()..removeAdditionalUser = get; @@ -687,34 +750,6 @@ class ApiService { return sendRequestSync(req, contactId: userId.toInt()); } - Future buyVoucher(int valueInCents) async { - final get = ApplicationData_CreateVoucher()..valueCents = valueInCents; - final appData = ApplicationData()..createVoucher = get; - final req = createClientToServerFromApplicationData(appData); - return sendRequestSync(req); - } - - Future switchToPayedPlan( - String planId, - bool payMonthly, - bool autoRenewal, - ) async { - final get = ApplicationData_SwitchToPayedPlan() - ..planId = planId - ..payMonthly = payMonthly - ..autoRenewal = autoRenewal; - final appData = ApplicationData()..switchtoPayedPlan = get; - final req = createClientToServerFromApplicationData(appData); - return sendRequestSync(req); - } - - Future redeemVoucher(String voucher) async { - final get = ApplicationData_RedeemVoucher()..voucher = voucher; - final appData = ApplicationData()..redeemVoucher = get; - final req = createClientToServerFromApplicationData(appData); - return sendRequestSync(req); - } - Future reportUser(int userId, String reason) async { final get = ApplicationData_ReportUser() ..reportedUserId = Int64(userId) @@ -731,13 +766,6 @@ class ApiService { return sendRequestSync(req); } - Future redeemUserInviteCode(String inviteCode) async { - final get = ApplicationData_RedeemAdditionalCode()..inviteCode = inviteCode; - final appData = ApplicationData()..redeemAdditionalCode = get; - final req = createClientToServerFromApplicationData(appData); - return sendRequestSync(req); - } - Future updateFCMToken(String googleFcm) async { final get = ApplicationData_UpdateGoogleFcmToken()..googleFcm = googleFcm; final appData = ApplicationData()..updateGoogleFcmToken = get; diff --git a/lib/src/services/api/client2client/contact.c2c.dart b/lib/src/services/api/client2client/contact.c2c.dart index 2d300bcd..af306db3 100644 --- a/lib/src/services/api/client2client/contact.c2c.dart +++ b/lib/src/services/api/client2client/contact.c2c.dart @@ -60,6 +60,15 @@ Future handleNewContactRequest(int fromUserId) async { } Future handleContactAccept(int fromUserId) async { + final contact = await twonlyDB.contactsDao + .getContactByUserId(fromUserId) + .getSingleOrNull(); + if (contact == null) return; + if (contact.requested || contact.deletedByUser) { + Log.error('User has never send an request. So ignore the Accept.'); + return; + } + await twonlyDB.contactsDao.updateContact( fromUserId, const ContactsCompanion( @@ -68,17 +77,12 @@ Future handleContactAccept(int fromUserId) async { deletedByUser: Value(false), ), ); - final contact = await twonlyDB.contactsDao - .getContactByUserId(fromUserId) - .getSingleOrNull(); - if (contact != null) { - await twonlyDB.groupsDao.createNewDirectChat( - fromUserId, - GroupsCompanion( - groupName: Value(getContactDisplayName(contact)), - ), - ); - } + await twonlyDB.groupsDao.createNewDirectChat( + fromUserId, + GroupsCompanion( + groupName: Value(getContactDisplayName(contact)), + ), + ); } Future handleContactRequest( @@ -143,8 +147,8 @@ Future handleContactUpdate( groupId: Value(group.groupId), type: const Value(GroupActionType.updatedContactUsername), contactId: Value(fromUserId), - oldGroupName: Value('@${contact.username}'), - newGroupName: Value('@${contactUpdate.username}'), + oldGroupName: Value(contact.username), + newGroupName: Value(contactUpdate.username), ), ); } @@ -157,7 +161,7 @@ Future handleContactUpdate( groupId: Value(group.groupId), type: const Value(GroupActionType.updatedContactDisplayName), contactId: Value(fromUserId), - oldGroupName: Value(contact.displayName ?? ''), + oldGroupName: Value(contact.displayName ?? contact.username), newGroupName: Value(contactUpdate.displayName), ), ); diff --git a/lib/src/services/api/client2client/groups.c2c.dart b/lib/src/services/api/client2client/groups.c2c.dart index cf3be4a0..f9fb9a1e 100644 --- a/lib/src/services/api/client2client/groups.c2c.dart +++ b/lib/src/services/api/client2client/groups.c2c.dart @@ -8,7 +8,7 @@ import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/services/api/messages.api.dart'; import 'package:twonly/src/services/api/utils.api.dart'; -import 'package:twonly/src/services/group.services.dart'; +import 'package:twonly/src/services/group.service.dart'; import 'package:twonly/src/utils/log.dart'; Future handleGroupCreate( diff --git a/lib/src/services/api/mediafiles/download.api.dart b/lib/src/services/api/mediafiles/download.api.dart index f7e8bd27..916d45cd 100644 --- a/lib/src/services/api/mediafiles/download.api.dart +++ b/lib/src/services/api/mediafiles/download.api.dart @@ -267,13 +267,13 @@ Future requestMediaReupload(String mediaId) async { final messages = await twonlyDB.messagesDao.getMessagesByMediaId(mediaId); for (final message in messages) { - if (message.openedAt != null) continue; + if (message.openedAt != null || message.senderId == null) continue; await sendCipherText( - messages.first.senderId!, + message.senderId!, EncryptedContent( mediaUpdate: EncryptedContent_MediaUpdate( type: EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR, - targetMessageId: messages.first.messageId, + targetMessageId: message.messageId, ), ), ); @@ -356,8 +356,6 @@ Future handleEncryptedFile(String mediaId) async { Log.info('Decryption of $mediaId was successful'); mediaService.encryptedPath.deleteSync(); - - unawaited(apiService.downloadDone(mediaService.mediaFile.downloadToken!)); }, ); } diff --git a/lib/src/services/api/mediafiles/media_background.api.dart b/lib/src/services/api/mediafiles/media_background.api.dart index 7de58a96..d736e2d1 100644 --- a/lib/src/services/api/mediafiles/media_background.api.dart +++ b/lib/src/services/api/mediafiles/media_background.api.dart @@ -8,7 +8,7 @@ import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/services/api/mediafiles/download.api.dart'; import 'package:twonly/src/services/api/mediafiles/upload.api.dart'; -import 'package:twonly/src/services/backup/create.backup.dart'; +import 'package:twonly/src/services/backup.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/log.dart'; @@ -22,8 +22,11 @@ Future initFileDownloader() async { if (update.task.taskId.contains('download_')) { await handleDownloadStatusUpdate(update); } - if (update.task.taskId.contains('backup')) { - await handleBackupStatusUpdate(update); + if (update.task.taskId.contains('backup_')) { + await BackupService.handleBackupStatusUpdate( + update.task.taskId, + update, + ); } case TaskProgressUpdate(): Log.info( diff --git a/lib/src/services/api/mediafiles/upload.api.dart b/lib/src/services/api/mediafiles/upload.api.dart index e84a77dc..eb304f42 100644 --- a/lib/src/services/api/mediafiles/upload.api.dart +++ b/lib/src/services/api/mediafiles/upload.api.dart @@ -1,6 +1,4 @@ import 'dart:async'; -import 'dart:convert'; - import 'package:background_downloader/background_downloader.dart'; import 'package:clock/clock.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; @@ -12,7 +10,6 @@ import 'package:http/http.dart' as http; import 'package:mutex/mutex.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart'; -import 'package:twonly/src/constants/secure_storage.keys.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; @@ -21,12 +18,12 @@ import 'package:twonly/src/model/protobuf/client/generated/data.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/services/api/mediafiles/media_background.api.dart'; import 'package:twonly/src/services/api/messages.api.dart'; +import 'package:twonly/src/services/api/utils.api.dart'; import 'package:twonly/src/services/flame.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/exclusive_access.utils.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/utils/secure_storage.dart'; import 'package:workmanager/workmanager.dart' hide TaskStatus; final lockRetransmission = Mutex(); @@ -620,22 +617,17 @@ Future _uploadUploadRequest(MediaFileService media) async { return null; } - final apiAuthTokenRaw = await SecureStorage.instance.read( - key: SecureStorageKeys.apiAuthToken, - ); - - if (apiAuthTokenRaw == null) { - Log.error('api auth token not defined.'); - return null; - } - final apiAuthToken = uint8ListToHex(base64Decode(apiAuthTokenRaw)); - final apiUrl = 'http${apiService.apiSecure}://${apiService.apiHost}/api/upload'; - // try { Log.info('Starting upload from ${media.mediaFile.mediaId}'); + final headers = await getAuthenticationHeader(); + if (headers == null) { + Log.error('Auth headers are empty. Returning'); + return; + } + final task = UploadTask.fromFile( taskId: 'upload_${media.mediaFile.mediaId}', displayName: media.mediaFile.type.name, @@ -643,9 +635,7 @@ Future _uploadUploadRequest(MediaFileService media) async { url: apiUrl, priority: 0, retries: 10, - headers: { - 'x-twonly-auth-token': apiAuthToken, - }, + headers: headers, ); final connectivityResult = await Connectivity().checkConnectivity(); diff --git a/lib/src/services/api/messages.api.dart b/lib/src/services/api/messages.api.dart index 93885682..a9eabd3a 100644 --- a/lib/src/services/api/messages.api.dart +++ b/lib/src/services/api/messages.api.dart @@ -67,8 +67,9 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({ Receipt? receipt, bool onlyReturnEncryptedData = false, bool blocking = true, - bool useLock = true, }) async { + if (apiService.appIsOutdated) return null; + try { if (receiptId == null && receipt == null) return null; if (receipt == null) { @@ -133,7 +134,6 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({ final cipherText = await signalEncryptMessage( receipt.contactId, Uint8List.fromList(message.encryptedContent), - useLock: useLock, ); if (cipherText == null) { Log.error('Could not encrypt the message. Aborting and trying again.'); @@ -338,7 +338,6 @@ Future<(Uint8List, Uint8List?)?> sendCipherText( bool blocking = true, String? messageId, bool onlySendIfNoReceiptsAreOpen = false, - bool useLock = true, }) async { if (onlySendIfNoReceiptsAreOpen) { final openReceipts = await twonlyDB.receiptsDao.getReceiptCountForContact( @@ -400,7 +399,6 @@ Future<(Uint8List, Uint8List?)?> sendCipherText( receipt: receipt, onlyReturnEncryptedData: onlyReturnEncryptedData, blocking: blocking, - useLock: useLock, ); if (!blocking) { return null; diff --git a/lib/src/services/api/server_messages.api.dart b/lib/src/services/api/server_messages.api.dart index b3f68a11..99ce4e36 100644 --- a/lib/src/services/api/server_messages.api.dart +++ b/lib/src/services/api/server_messages.api.dart @@ -4,7 +4,6 @@ import 'dart:io'; import 'package:clock/clock.dart'; import 'package:drift/drift.dart'; import 'package:hashlib/random.dart'; -import 'package:mutex/mutex.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; @@ -28,7 +27,7 @@ import 'package:twonly/src/services/api/client2client/reaction.c2c.dart'; import 'package:twonly/src/services/api/client2client/text_message.c2c.dart'; import 'package:twonly/src/services/api/client2client/user_discovery.c2c.dart'; import 'package:twonly/src/services/api/messages.api.dart'; -import 'package:twonly/src/services/group.services.dart'; +import 'package:twonly/src/services/group.service.dart'; import 'package:twonly/src/services/key_verification.service.dart'; import 'package:twonly/src/services/notifications/background.notifications.dart'; import 'package:twonly/src/services/signal/encryption.signal.dart'; @@ -36,48 +35,44 @@ import 'package:twonly/src/services/signal/session.signal.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; -final lockHandleServerMessage = Mutex(); - Future handleServerMessage(server.ServerToClient msg) async { - return lockHandleServerMessage.protect(() async { - Log.info('Processing a message from the server.'); + Log.info('Processing a message from the server.'); - /// Returns means, that the server can delete the message from the server. - final ok = client.Response_Ok()..none = true; - var response = client.Response()..ok = ok; + /// Returns means, that the server can delete the message from the server. + final ok = client.Response_Ok()..none = true; + var response = client.Response()..ok = ok; - try { - if (msg.v0.hasRequestNewPreKeys()) { - response = await handleRequestNewPreKey(); - } else if (msg.v0.hasNewMessage()) { - Log.info('Got 1 message from the server.'); - await handleClient2ClientMessage(msg.v0.newMessage); - } else if (msg.v0.hasNewMessages()) { - Log.info( - 'Got ${msg.v0.newMessages.newMessages.length} messages from the server.', - ); - for (final newMessage in msg.v0.newMessages.newMessages) { - try { - await handleClient2ClientMessage(newMessage); - } catch (e) { - Log.error(e); - } + try { + if (msg.v0.hasRequestNewPreKeys()) { + response = await handleRequestNewPreKey(); + } else if (msg.v0.hasNewMessage()) { + Log.info('Got 1 message from the server.'); + await handleClient2ClientMessage(msg.v0.newMessage); + } else if (msg.v0.hasNewMessages()) { + Log.info( + 'Got ${msg.v0.newMessages.newMessages.length} messages from the server.', + ); + for (final newMessage in msg.v0.newMessages.newMessages) { + try { + await handleClient2ClientMessage(newMessage); + } catch (e) { + Log.error(e); } - } else { - Log.error('Unknown server message: $msg'); } - } catch (e) { - Log.error(e); + } else { + Log.error('Unknown server message: $msg'); } + } catch (e) { + Log.error(e); + } - final v0 = client.V0() - ..seq = msg.v0.seq - ..response = response; + final v0 = client.V0() + ..seq = msg.v0.seq + ..response = response; - await apiService.sendResponse(ClientToServer()..v0 = v0); - AppState.gotMessageFromServer = true; - Log.info('Message from server proccessed.'); - }); + await apiService.sendResponse(ClientToServer()..v0 = v0); + AppState.gotMessageFromServer = true; + Log.info('Message from server proccessed.'); } DateTime lastPushKeyRequest = clock.now().subtract(const Duration(hours: 1)); diff --git a/lib/src/services/api/utils.api.dart b/lib/src/services/api/utils.api.dart index 13ca8f05..c3f9a459 100644 --- a/lib/src/services/api/utils.api.dart +++ b/lib/src/services/api/utils.api.dart @@ -1,6 +1,10 @@ +import 'dart:convert'; + import 'package:drift/drift.dart'; import 'package:fixnum/fixnum.dart'; +import 'package:twonly/core/bridge/wrapper/key_manager.dart'; import 'package:twonly/locator.dart'; +import 'package:twonly/src/constants/secure_storage.keys.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pb.dart' @@ -14,6 +18,9 @@ import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dar import 'package:twonly/src/services/api/messages.api.dart'; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/services/signal/session.signal.dart'; +import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/utils/secure_storage.dart'; class Result { Result.error(this.error) : value = null; @@ -106,3 +113,36 @@ Future importSignalContactAndCreateRequest( return true; } + +Future?> getAuthenticationHeader() async { + var headers = {}; + + if (userService.currentUser.canUseLoginTokenForAuth) { + final loginToken = await RustKeyManager.getLoginToken(); + + headers = { + 'x-twonly-user-id': userService.currentUser.userId + .toRadixString(16) + .padLeft(16, '0') + .toUpperCase(), + 'x-twonly-login-token': uint8ListToHex(loginToken), + }; + } else { + final apiAuthTokenRaw = await SecureStorage.instance.read( + key: SecureStorageKeys.apiAuthToken, + ); + + if (apiAuthTokenRaw == null) { + Log.error('api auth token not defined.'); + return null; + } + + final apiAuthToken = uint8ListToHex(base64Decode(apiAuthTokenRaw)); + + headers = { + 'x-twonly-auth-token': apiAuthToken, + }; + } + + return headers; +} diff --git a/lib/src/services/background/callback_dispatcher.background.dart b/lib/src/services/background/callback_dispatcher.background.dart index 6aba3078..3b689615 100644 --- a/lib/src/services/background/callback_dispatcher.background.dart +++ b/lib/src/services/background/callback_dispatcher.background.dart @@ -34,12 +34,15 @@ Future initializeBackgroundTaskManager() async { void callbackDispatcher() { Workmanager().executeTask((task, inputData) async { SentryWidgetsFlutterBinding.ensureInitialized(); + await AppEnvironment.init(); switch (task) { case 'eu.twonly.periodic_task': - // if (await initBackgroundExecution()) { - // await handlePeriodicTask(); - // } + // if (await initBackgroundExecution()) { + // await handlePeriodicTask(); + // } + break; case 'eu.twonly.processing_task': + case _ when task.startsWith('progressing_finish_uploads_'): if (await initBackgroundExecution()) { await handleProcessingTask(); } @@ -58,7 +61,6 @@ Future initBackgroundExecution() async { return false; } - await AppEnvironment.init(); AppState.isInBackgroundTask = true; if (await StartupGuard.isAppStarting()) { @@ -130,25 +132,27 @@ Future handlePeriodicTask({int lastExecutionInSecondsLimit = 120}) async { return; } - while (!AppState.gotMessageFromServer) { - if (stopwatch.elapsed.inSeconds >= 15) { - Log.info('No new message from the server after 15 seconds.'); - break; + try { + while (!AppState.gotMessageFromServer) { + if (stopwatch.elapsed.inSeconds >= 15) { + Log.info('No new message from the server after 15 seconds.'); + break; + } + await Future.delayed(const Duration(milliseconds: 500)); } - await Future.delayed(const Duration(milliseconds: 500)); + + if (AppState.gotMessageFromServer) { + Log.info('Received a server message from the server.'); + } + + await finishStartedPreprocessing(); + + await Future.delayed(const Duration(milliseconds: 2000)); + } finally { + await apiService.close(() {}); + stopwatch.stop(); } - if (AppState.gotMessageFromServer) { - Log.info('Received a server message from the server.'); - } - - await finishStartedPreprocessing(); - - await Future.delayed(const Duration(milliseconds: 2000)); - - await apiService.close(() {}); - stopwatch.stop(); - Log.info('eu.twonly.periodic_task finished after ${stopwatch.elapsed}.'); return; } diff --git a/lib/src/services/backup.service.dart b/lib/src/services/backup.service.dart new file mode 100644 index 00000000..952869d8 --- /dev/null +++ b/lib/src/services/backup.service.dart @@ -0,0 +1,363 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:background_downloader/background_downloader.dart'; +import 'package:clock/clock.dart' as clock; +import 'package:http/http.dart' as http; +import 'package:mutex/mutex.dart'; +import 'package:twonly/core/bridge/wrapper/backup.dart'; +import 'package:twonly/core/bridge/wrapper/key_manager.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/locator.dart'; +import 'package:twonly/src/constants/keyvalue.keys.dart'; +import 'package:twonly/src/model/json/backup.model.dart'; +import 'package:twonly/src/services/api/utils.api.dart'; +import 'package:twonly/src/services/user.service.dart'; +import 'package:twonly/src/utils/keyvalue.dart'; +import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/utils/storage.dart'; + +class BackupService { + static final Mutex _protected = Mutex(); + + static String _getIdentityBackupUrl(String backupId) => + '${apiService.apiEndpoint}/backup/identity/$backupId'; + + static String _getArchiveBackupUrl(String backupDownloadToken, int? userId) => + '${apiService.apiEndpoint}/backup/archive/${userId == null ? '' : '${userId.toRadixString(16).padLeft(16, '0').toUpperCase()}/'}$backupDownloadToken'; + + static final _backupUpdateController = StreamController.broadcast(); + static Stream get onBackupUpdated => _backupUpdateController.stream; + + static Future getData() async { + return CurrentBackupStatus.fromJson( + (await KeyValueStore.get(KeyValueKeys.currentBackupState)) ?? + CurrentBackupStatus().toJson(), + ); + } + + static Future updateBackupPassword(String password) async { + // Set or reset the backup data... + await KeyValueStore.put( + KeyValueKeys.currentBackupState, + CurrentBackupStatus().toJson(), + ); + _backupUpdateController.add(null); + + await RustBackupIdentity.setBackupPasswordKeys( + password: password, + // Using the userId is this will never change in a users lifecycle + userId: userService.currentUser.userId, + ); + + await UserService.update((u) => u.isBackupEnabled = true); + + unawaited(makeBackup(force: true)); + } + + static Future handleBackupStatusUpdate( + String taskId, + TaskStatusUpdate update, + ) async { + var status = LastBackupUploadState.success; + + if (update.status == TaskStatus.failed || + update.status == TaskStatus.canceled) { + status = LastBackupUploadState.failed; + } else if (update.status != TaskStatus.complete) { + Log.info('Backup is in state: ${update.status}'); + return; + } + await _protected.protect(() async { + final backup = await getData(); + if (taskId == 'backup_identity') { + backup + ..identityLastSuccessFull = clock.clock.now() + ..identityState = status; + } else { + backup + ..archiveLastSuccessFull = clock.clock.now() + ..archiveState = status; + } + await KeyValueStore.put( + KeyValueKeys.currentBackupState, + backup.toJson(), + ); + _backupUpdateController.add(null); + }); + } + + static Future makeBackup({bool force = false}) async { + await _protected.protect(() async { + final backup = await getData(); + + final lastDay = clock.clock.now().subtract(const Duration(days: 1)); + final lastWeek = clock.clock.now().subtract(const Duration(days: 7)); + + if (force || + backup.identityLastSuccessFull == null || + (backup.identityState != LastBackupUploadState.pending && + backup.identityLastSuccessFull!.isBefore(lastWeek) || + backup.identityLastSuccessFull!.isBefore( + lastWeek.subtract(const Duration(days: 1)), + ))) { + final backupId = await RustBackupIdentity.getBackupId(); + if (backupId == null) { + Log.error('No backup password was set by the user.'); + backup.identityState = LastBackupUploadState.failed; + } else { + Log.info('Performing a identity backup.'); + final encryptedBackup = + await RustBackupIdentity.getIdentityBackupBytes(); + + final backupTempFile = File( + '${AppEnvironment.cacheDir}/identity_backup.bin', + )..writeAsBytesSync(encryptedBackup); + + Log.info( + 'Identity backup has a size of ${backupTempFile.statSync().size}.', + ); + + final task = UploadTask.fromFile( + taskId: 'backup_identity', + httpRequestMethod: 'PUT', + file: backupTempFile, + url: _getIdentityBackupUrl(backupId), + post: 'binary', + retries: 2, + headers: { + 'Content-Type': 'application/octet-stream', + }, + ); + if (await FileDownloader().enqueue(task)) { + Log.info('Starting upload from backup identity.'); + backup + ..identityState = LastBackupUploadState.pending + ..identityLastSuccessFull = clock.clock.now() + ..identitySize = encryptedBackup.length; + await KeyValueStore.put( + KeyValueKeys.currentBackupState, + backup.toJson(), + ); + _backupUpdateController.add(null); + } else { + Log.error('Error starting upload task for backup identity.'); + } + } + } + + if (force || + backup.archiveLastSuccessFull == null || + (backup.archiveState != LastBackupUploadState.pending && + backup.archiveLastSuccessFull!.isBefore(lastDay) || + backup.archiveLastSuccessFull!.isBefore( + lastDay.subtract(const Duration(days: 1)), + ))) { + Log.info('Creating a archive backup.'); + late final String backupArchive; + late final String backupDownloadToken; + try { + (backupDownloadToken, backupArchive) = + await RustBackupArchive.createBackupArchive(); + } catch (e) { + Log.error(e); + return; + } + Log.info( + 'Archive backup has a size of ${File(backupArchive).statSync().size}.', + ); + + final headers = await getAuthenticationHeader(); + if (headers == null) { + Log.error('Auth headers are empty. Returning'); + return; + } + + final task = UploadTask.fromFile( + taskId: 'backup_archive', + file: File(backupArchive), + url: _getArchiveBackupUrl(backupDownloadToken, null), + priority: 0, + retries: 10, + headers: headers, + ); + if (await FileDownloader().enqueue(task)) { + Log.info('Uploading backup archive.'); + backup + ..archiveState = LastBackupUploadState.pending + ..archiveLastSuccessFull = clock.clock.now() + ..archiveSize = File(backupArchive).statSync().size; + await KeyValueStore.put( + KeyValueKeys.currentBackupState, + backup.toJson(), + ); + _backupUpdateController.add(null); + } else { + Log.error('Error starting upload task for backup archive.'); + } + } + }); + } + + static Future getBackupRecoveryData() async { + final stateJson = await KeyValueStore.get(KeyValueKeys.backupRecoveryState); + if (stateJson == null) return null; + return BackupRecovery.fromJson(stateJson); + } + + static Future _nextBackupStage() async { + return _protected.protect(() async { + final recoveryData = await getBackupRecoveryData(); + if (recoveryData == null) return null; + + if (recoveryData.state == BackupRecoveryState.identityBackupStarted) { + // First start to download the identity to restore the KeyManager + final backupKeys = await RustBackupIdentity.getBackupPasswordKeys( + userId: recoveryData.userId, + password: recoveryData.password, + ); + final backupId = uint8ListToHex(backupKeys.backupId); + final backupServerUrl = _getIdentityBackupUrl(backupId); + final (encryptedBytes, error) = await _downloadBackup(backupServerUrl); + if (error != null || encryptedBytes == null) { + Log.error(error); + return error; + } + + Log.info('Restored identity.'); + + try { + await RustBackupIdentity.restoreIdentityBackup( + keys: backupKeys, + encryptedBytes: encryptedBytes, + ); + recoveryData.state = BackupRecoveryState.archiveBackupStarted; + await KeyValueStore.put( + KeyValueKeys.backupRecoveryState, + recoveryData.toJson(), + ); + _backupUpdateController.add(null); + } catch (e) { + Log.error(e); + return RecoveryError.unkownError; + } + } + + if (recoveryData.state == BackupRecoveryState.archiveBackupStarted) { + // The KeyManager was restored sucessfully, restore the archive now. + try { + final downloadToken = + await RustBackupArchive.getBackupDownloadToken(); + if (downloadToken == null) { + // identity was not restored correctly try this again. + recoveryData.state = BackupRecoveryState.identityBackupStarted; + await KeyValueStore.put( + KeyValueKeys.backupRecoveryState, + recoveryData.toJson(), + ); + return RecoveryError.tryAgainLater; + } + + final backupServerUrl = _getArchiveBackupUrl( + downloadToken, + recoveryData.userId, + ); + final backupArchive = await _downloadBackup(backupServerUrl); + if (backupArchive.$2 != null || backupArchive.$1 == null) { + return backupArchive.$2; + } + + final archiveFile = File('${AppEnvironment.cacheDir}/archive.bin') + ..writeAsBytesSync(backupArchive.$1!); + + await RustBackupArchive.restoreBackupArchive( + filePath: archiveFile.path, + ); + await UserService.update((u) { + u.deviceId += 1; + }); + await KeyValueStore.delete( + KeyValueKeys.backupRecoveryState, + ); + } catch (e) { + Log.error(e); + return RecoveryError.unkownError; + } + } + + return null; + }); + } + + static Future tryToReinstallTheArchive() async { + final userId = await RustKeyManager.getUserId(); + if (userId == null) return null; + + final state = BackupRecovery( + username: '', + userId: userId, + password: '', + )..state = BackupRecoveryState.archiveBackupStarted; + await KeyValueStore.put(KeyValueKeys.backupRecoveryState, state.toJson()); + return _nextBackupStage(); + } + + static Future startFullBackupRecovery( + String username, + String password, + ) async { + final userId = await apiService.getUserIdFromUsername(username); + if (userId == null) { + return RecoveryError.usernameNotValid; + } + + final state = BackupRecovery( + username: username, + userId: userId, + password: password, + ); + + await deleteLocalUserData(); + await KeyValueStore.put(KeyValueKeys.backupRecoveryState, state.toJson()); + return _nextBackupStage(); + } + + static Future<(Uint8List?, RecoveryError?)> _downloadBackup( + String backupServerUrl, + ) async { + late http.Response response; + + try { + response = await http.get( + Uri.parse(backupServerUrl), + headers: { + HttpHeaders.acceptHeader: 'application/octet-stream', + }, + ); + } catch (e) { + Log.error('Error fetching backup: $e'); + return (null, RecoveryError.noInternet); + } + + Log.info('Backup downlaod status: ${response.statusCode}'); + + switch (response.statusCode) { + case 200: + return (response.bodyBytes, null); + case 404: + return (null, RecoveryError.passwordInvalid); + default: + return (null, RecoveryError.tryAgainLater); + } + } +} + +enum RecoveryError { + usernameNotValid, + passwordInvalid, + tryAgainLater, + noInternet, + unkownError, +} diff --git a/lib/src/services/backup/common.backup.dart b/lib/src/services/backup/common.backup.dart deleted file mode 100644 index 0751a0c1..00000000 --- a/lib/src/services/backup/common.backup.dart +++ /dev/null @@ -1,89 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; - -import 'package:drift/drift.dart'; -import 'package:hashlib/hashlib.dart'; -import 'package:http/http.dart' as http; -import 'package:twonly/locator.dart'; -import 'package:twonly/src/model/json/userdata.model.dart'; -import 'package:twonly/src/services/backup/create.backup.dart'; -import 'package:twonly/src/services/user.service.dart'; -import 'package:twonly/src/utils/log.dart'; -import 'package:twonly/src/utils/misc.dart'; - -Future enableTwonlySafe(String password) async { - final (backupId, encryptionKey) = await getMasterKey( - password, - userService.currentUser.username, - ); - - await UserService.update((user) { - user.twonlySafeBackup = TwonlySafeBackup( - encryptionKey: encryptionKey, - backupId: backupId, - ); - }); - unawaited(performTwonlySafeBackup(force: true)); -} - -Future removeTwonlySafeFromServer() async { - final serverUrl = getTwonlySafeBackupUrl(); - if (serverUrl == null) { - Log.error('Could not remove twonly safe as serverUrl is null'); - return; - } - try { - final response = await http.delete( - Uri.parse(serverUrl), - headers: { - 'Content-Type': 'application/json', // Set the content type if needed - // Add any other headers if required - }, - ); - Log.info('Download deleted with: ${response.statusCode}'); - } catch (e) { - Log.error('Could not connect upload the backup.'); - } -} - -Future<(Uint8List, Uint8List)> getMasterKey( - String password, - String username, -) async { - final List passwordBytes = utf8.encode(password); - final List saltBytes = utf8.encode(username); - - // Values are derived from the Threema Whitepaper - // https://threema.com/assets/documents/cryptography_whitepaper.pdf - - final scrypt = Scrypt( - cost: 65536, - salt: saltBytes, - ); - - final key = scrypt.convert(passwordBytes).bytes; - return (key.sublist(0, 32), key.sublist(32, 64)); -} - -String? getTwonlySafeBackupUrl() { - if (userService.currentUser.twonlySafeBackup == null) return null; - return getTwonlySafeBackupUrlFromServer( - userService.currentUser.twonlySafeBackup!.backupId, - userService.currentUser.backupServer, - ); -} - -String? getTwonlySafeBackupUrlFromServer( - List backupId, - BackupServer? backupServer, -) { - var backupServerUrl = 'https://safe.twonly.eu/'; - - if (backupServer != null) { - backupServerUrl = backupServer.serverUrl; - } - - final backupIdHex = uint8ListToHex(backupId).toLowerCase(); - - return '${backupServerUrl}backups/$backupIdHex'; -} diff --git a/lib/src/services/backup/create.backup.dart b/lib/src/services/backup/create.backup.dart deleted file mode 100644 index f633677c..00000000 --- a/lib/src/services/backup/create.backup.dart +++ /dev/null @@ -1,238 +0,0 @@ -// ignore_for_file: parameter_assignments - -import 'dart:convert'; -import 'dart:io'; - -import 'package:background_downloader/background_downloader.dart'; -import 'package:clock/clock.dart'; -import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart'; -import 'package:cryptography_plus/cryptography_plus.dart'; -import 'package:drift/drift.dart'; -import 'package:drift_flutter/drift_flutter.dart'; -import 'package:path/path.dart'; -import 'package:twonly/globals.dart'; -import 'package:twonly/locator.dart'; -import 'package:twonly/src/constants/secure_storage.keys.dart'; -import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/model/json/userdata.model.dart'; -import 'package:twonly/src/model/protobuf/client/generated/backup.pb.dart'; -import 'package:twonly/src/services/backup/common.backup.dart'; -import 'package:twonly/src/services/user.service.dart'; -import 'package:twonly/src/utils/log.dart'; -import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/utils/secure_storage.dart'; - -Future performTwonlySafeBackup({bool force = false}) async { - if (userService.currentUser.twonlySafeBackup == null) { - return; - } - - if (userService.currentUser.twonlySafeBackup!.backupUploadState == - LastBackupUploadState.pending) { - Log.warn('Backup upload is already pending.'); - return; - } - - final lastUpdateTime = - userService.currentUser.twonlySafeBackup!.lastBackupDone; - if (!force && lastUpdateTime != null) { - if (lastUpdateTime.isAfter(clock.now().subtract(const Duration(days: 1)))) { - return; - } - } - - Log.info('Starting new twonly Backup!'); - - final backupDir = Directory( - join(AppEnvironment.supportDir, 'backup_twonly_safe/'), - ); - await backupDir.create(recursive: true); - - final backupDatabaseFile = File(join(backupDir.path, 'twonly.backup.sqlite')); - - final backupDatabaseFileCleaned = File( - join(backupDir.path, 'twonly.backup.cleaned.sqlite'), - ); - - // copy database - final originalDatabase = File( - join(AppEnvironment.supportDir, 'twonly.sqlite'), - ); - await originalDatabase.copy(backupDatabaseFile.path); - - driftRuntimeOptions.dontWarnAboutMultipleDatabases = true; - final backupDB = TwonlyDB( - driftDatabase( - name: 'twonly.backup', - native: DriftNativeOptions( - databaseDirectory: () async { - return backupDir; - }, - ), - ), - ); - - await backupDB.deleteDataForTwonlySafe(); - - await backupDB.customStatement('VACUUM INTO ?', [ - backupDatabaseFileCleaned.path, - ]); - - await backupDB.printTableSizes(); - - await backupDB.close(); - - // ignore: inference_failure_on_collection_literal - final secureStorageBackup = {}; - secureStorageBackup[SecureStorageKeys.signalIdentity] = await SecureStorage - .instance - .read( - key: SecureStorageKeys.signalIdentity, - ); - secureStorageBackup[SecureStorageKeys.signalSignedPreKey] = - await SecureStorage.instance.read( - key: SecureStorageKeys.signalSignedPreKey, - ); - - final userBackup = await UserService.getUser(); - if (userBackup == null) return; - // FILTER settings which should not be in the backup - userBackup - ..twonlySafeBackup = null - ..lastImageSend = null - ..todaysImageCounter = null - ..lastPlanBallance = '' - ..additionalUserInvites = '' - ..signalLastSignedPreKeyUpdated = null; - - secureStorageBackup[SecureStorageKeys.userData] = jsonEncode(userBackup); - - // Compress and convert backup data - - final twonlyDatabaseBytes = await backupDatabaseFileCleaned.readAsBytes(); - await backupDatabaseFile.delete(); - await backupDatabaseFileCleaned.delete(); - - Log.info('twonlyDatabaseLength = ${twonlyDatabaseBytes.lengthInBytes}'); - Log.info('secureStorageLength = ${jsonEncode(secureStorageBackup).length}'); - - final backupProto = TwonlySafeBackupContent( - secureStorageJson: jsonEncode(secureStorageBackup), - twonlyDatabase: twonlyDatabaseBytes, - ); - - final backupBytes = gzip.encode(backupProto.writeToBuffer()); - - final backupHash = uint8ListToHex((await Sha256().hash(backupBytes)).bytes); - - if (userService.currentUser.twonlySafeBackup!.lastBackupDone == null || - userService.currentUser.twonlySafeBackup!.lastBackupDone!.isAfter( - clock.now().subtract(const Duration(days: 90)), - )) { - force = true; - } - - final lastHash = await SecureStorage.instance.read( - key: SecureStorageKeys.twonlySafeLastBackupHash, - ); - - if (lastHash != null && !force) { - if (backupHash == lastHash) { - Log.info('Since last backup nothing has changed.'); - return; - } - } - - await SecureStorage.instance.write( - key: SecureStorageKeys.twonlySafeLastBackupHash, - value: backupHash, - ); - - // Encrypt backup data - - final chacha20 = FlutterChacha20.poly1305Aead(); - final nonce = chacha20.newNonce(); - - final secretBox = await chacha20.encrypt( - backupBytes, - secretKey: SecretKey( - userService.currentUser.twonlySafeBackup!.encryptionKey, - ), - nonce: nonce, - ); - - final encryptedBackupBytes = TwonlySafeBackupEncrypted( - mac: secretBox.mac.bytes, - nonce: nonce, - cipherText: secretBox.cipherText, - ).writeToBuffer(); - - Log.info('Backup files created.'); - - final encryptedBackupBytesFile = File( - join(backupDir.path, 'twonly_safe.backup'), - ); - - await encryptedBackupBytesFile.writeAsBytes(encryptedBackupBytes); - - Log.info( - 'Create twonly Backup with a size of ${encryptedBackupBytes.length} bytes.', - ); - - if (userService.currentUser.backupServer != null) { - if (encryptedBackupBytes.length > - userService.currentUser.backupServer!.maxBackupBytes) { - Log.error('Backup is to big for the alternative backup server.'); - await UserService.update((user) { - user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.failed; - }); - return; - } - } - - final task = UploadTask.fromFile( - taskId: 'backup', - file: encryptedBackupBytesFile, - httpRequestMethod: 'PUT', - url: getTwonlySafeBackupUrl()!, - post: 'binary', - retries: 2, - headers: { - 'Content-Type': 'application/octet-stream', - }, - ); - if (await FileDownloader().enqueue(task)) { - Log.info('Starting upload from twonly Backup.'); - await UserService.update((user) { - user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.pending; - user.twonlySafeBackup!.lastBackupDone = clock.now(); - user.twonlySafeBackup!.lastBackupSize = encryptedBackupBytes.length; - }); - } else { - Log.error('Error starting UploadTask for twonly Backup.'); - } -} - -Future handleBackupStatusUpdate(TaskStatusUpdate update) async { - if (update.status == TaskStatus.failed || - update.status == TaskStatus.canceled) { - await UserService.update((user) { - if (user.twonlySafeBackup != null) { - user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.failed; - } - }); - } else if (update.status == TaskStatus.complete) { - Log.info( - 'twonly Backup uploaded with status code ${update.responseStatusCode}', - ); - await UserService.update((user) { - if (user.twonlySafeBackup != null) { - user.twonlySafeBackup!.backupUploadState = - LastBackupUploadState.success; - } - }); - } else { - Log.info('Backup is in state: ${update.status}'); - return; - } -} diff --git a/lib/src/services/backup/restore.backup.dart b/lib/src/services/backup/restore.backup.dart deleted file mode 100644 index 3ca90187..00000000 --- a/lib/src/services/backup/restore.backup.dart +++ /dev/null @@ -1,118 +0,0 @@ -// ignore_for_file: avoid_dynamic_calls - -import 'dart:convert'; -import 'dart:io'; - -import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart'; -import 'package:cryptography_plus/cryptography_plus.dart'; -import 'package:drift/drift.dart'; -import 'package:http/http.dart' as http; -import 'package:path/path.dart'; -import 'package:twonly/globals.dart'; -import 'package:twonly/src/constants/secure_storage.keys.dart'; -import 'package:twonly/src/model/json/userdata.model.dart'; -import 'package:twonly/src/model/protobuf/client/generated/backup.pb.dart'; -import 'package:twonly/src/services/backup/common.backup.dart'; -import 'package:twonly/src/services/user.service.dart'; -import 'package:twonly/src/utils/log.dart'; -import 'package:twonly/src/utils/secure_storage.dart'; - -Future recoverBackup( - String username, - String password, - BackupServer? server, -) async { - final (backupId, encryptionKey) = await getMasterKey(password, username); - - final backupServerUrl = getTwonlySafeBackupUrlFromServer(backupId, server); - - if (backupServerUrl == null) { - Log.error('Could not create backup url'); - throw Exception('Could not create backup server url'); - } - - late Uint8List backupData; - late http.Response response; - - try { - response = await http.get( - Uri.parse(backupServerUrl), - headers: { - HttpHeaders.acceptHeader: 'application/octet-stream', - }, - ); - } catch (e) { - Log.error('Error fetching backup: $e'); - throw Exception('Backup server could not be reached. ($e)'); - } - - switch (response.statusCode) { - case 200: - backupData = response.bodyBytes; - case 400: - throw Exception('Bad Request: Validation failed.'); - case 404: - throw Exception('No backup was found.'); - case 429: - throw Exception('Too Many Requests: Rate limit reached.'); - default: - throw Exception('Unexpected error: ${response.statusCode}'); - } - - return handleBackupData(encryptionKey, backupData); -} - -Future handleBackupData( - Uint8List encryptionKey, - Uint8List backupData, -) async { - final encryptedBackup = TwonlySafeBackupEncrypted.fromBuffer( - backupData, - ); - - final secretBox = SecretBox( - encryptedBackup.cipherText, - nonce: encryptedBackup.nonce, - mac: Mac(encryptedBackup.mac), - ); - - final compressedBytes = await FlutterChacha20.poly1305Aead().decrypt( - secretBox, - secretKey: SecretKeyData(encryptionKey), - ); - - final plaintextBytes = gzip.decode(compressedBytes); - - final backupContent = TwonlySafeBackupContent.fromBuffer( - plaintextBytes, - ); - - final originalDatabase = File( - join(AppEnvironment.supportDir, 'twonly.sqlite'), - ); - - // in case there was only a secure storage error, do not replace the original database - if (!originalDatabase.existsSync()) { - await originalDatabase.writeAsBytes(backupContent.twonlyDatabase); - } - - const storage = SecureStorage.instance; - - final secureStorage = jsonDecode(backupContent.secureStorageJson); - - await storage.write( - key: SecureStorageKeys.signalIdentity, - value: secureStorage[SecureStorageKeys.signalIdentity] as String, - ); - await storage.write( - key: SecureStorageKeys.signalSignedPreKey, - value: secureStorage[SecureStorageKeys.signalSignedPreKey] as String, - ); - await storage.write( - key: SecureStorageKeys.userData, - value: secureStorage[SecureStorageKeys.userData] as String, - ); - await UserService.update((u) { - u.deviceId += 1; - }); -} diff --git a/lib/src/services/flame.service.dart b/lib/src/services/flame.service.dart index 75c40db8..1c86b101 100644 --- a/lib/src/services/flame.service.dart +++ b/lib/src/services/flame.service.dart @@ -96,23 +96,21 @@ Future incFlameCounter( final group = await twonlyDB.groupsDao.getGroup(groupId); if (group == null) return; - if (group.isDirectChat) { - final contacts = await twonlyDB.groupsDao.getGroupContact( - group.groupId, - ); - for (final contact in contacts) { - await twonlyDB.contactsDao.updateContact( - contact.userId, - ContactsCompanion( - mediaReceivedCounter: Value( - contacts.first.mediaReceivedCounter + (received ? 1 : 0), - ), - mediaSendCounter: Value( - contacts.first.mediaSendCounter + (received ? 0 : 1), - ), + final contacts = await twonlyDB.groupsDao.getGroupContact( + group.groupId, + ); + for (final contact in contacts) { + await twonlyDB.contactsDao.updateContact( + contact.userId, + ContactsCompanion( + mediaReceivedCounter: Value( + contacts.first.mediaReceivedCounter + (received ? 1 : 0), ), - ); - } + mediaSendCounter: Value( + contacts.first.mediaSendCounter + (received ? 0 : 1), + ), + ), + ); } final totalMediaCounter = group.totalMediaCounter + 1; diff --git a/lib/src/services/group.services.dart b/lib/src/services/group.service.dart similarity index 100% rename from lib/src/services/group.services.dart rename to lib/src/services/group.service.dart diff --git a/lib/src/services/mediafiles/thumbnail.service.dart b/lib/src/services/mediafiles/thumbnail.service.dart index c6c0d81d..9f8f2b75 100644 --- a/lib/src/services/mediafiles/thumbnail.service.dart +++ b/lib/src/services/mediafiles/thumbnail.service.dart @@ -31,7 +31,7 @@ Future createThumbnailsForVideo( 'It took ${stopwatch.elapsedMilliseconds}ms to create the thumbnail.', ); } else { - Log.error( + Log.warn( 'Thumbnail creation failed for the video with exit code.', ); } diff --git a/lib/src/services/notifications/background.notifications.dart b/lib/src/services/notifications/background.notifications.dart index 45ab7a0f..e2c7e16a 100644 --- a/lib/src/services/notifications/background.notifications.dart +++ b/lib/src/services/notifications/background.notifications.dart @@ -5,7 +5,7 @@ import 'dart:math'; import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart'; import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; -import 'package:path_provider/path_provider.dart'; +import 'package:twonly/globals.dart'; import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/constants/secure_storage.keys.dart'; import 'package:twonly/src/localization/generated/app_localizations.dart'; @@ -266,8 +266,7 @@ Future showLocalPushNotificationWithoutUserId( } Future getAvatarIcon(int contactId) async { - final directory = await getApplicationCacheDirectory(); - final avatarsDirectory = Directory('${directory.path}/avatars'); + final avatarsDirectory = Directory('${AppEnvironment.cacheDir}/avatars'); final filePath = '${avatarsDirectory.path}/$contactId.png'; final file = File(filePath); if (file.existsSync()) { diff --git a/lib/src/services/notifications/fcm.notifications.dart b/lib/src/services/notifications/fcm.notifications.dart index 8c5a7009..f7059ea1 100644 --- a/lib/src/services/notifications/fcm.notifications.dart +++ b/lib/src/services/notifications/fcm.notifications.dart @@ -6,13 +6,12 @@ import 'dart:io' show Platform; import 'package:firebase_app_installations/firebase_app_installations.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart'; -import 'package:twonly/src/constants/secure_storage.keys.dart'; import 'package:twonly/src/services/background/callback_dispatcher.background.dart'; import 'package:twonly/src/services/notifications/background.notifications.dart'; +import 'package:twonly/src/services/notifications/setup.notifications.dart'; import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/utils/log.dart'; @@ -21,11 +20,8 @@ import '../../../firebase_options.dart'; // see more here: https://firebase.google.com/docs/cloud-messaging/flutter/receive?hl=de Future checkForTokenUpdates() async { - const storage = FlutterSecureStorage(); - - final storedToken = await storage.read(key: SecureStorageKeys.googleFcm); - try { + if (!userService.isUserCreated) return; if (Platform.isIOS) { var apnsToken = await FirebaseMessaging.instance.getAPNSToken(); for (var i = 0; i < 20; i++) { @@ -47,23 +43,22 @@ Future checkForTokenUpdates() async { Log.info('Loaded FCM token.'); - if (storedToken == null || fcmToken != storedToken) { - Log.info('Got new FCM TOKEN.'); - await storage.write(key: SecureStorageKeys.googleFcm, value: fcmToken); + if (userService.currentUser.fcmToken == null || + fcmToken != userService.currentUser.fcmToken) { + Log.info('Got new FCM token.'); await UserService.update((u) { - u.updateFCMToken = true; + u + ..updateFCMToken = true + ..fcmToken = fcmToken; }); } FirebaseMessaging.instance.onTokenRefresh .listen((fcmToken) async { - Log.info('Got new FCM TOKEN.'); - await storage.write( - key: SecureStorageKeys.googleFcm, - value: fcmToken, - ); await UserService.update((u) { - u.updateFCMToken = true; + u + ..updateFCMToken = true + ..fcmToken = fcmToken; }); }) .onError((err) { @@ -75,21 +70,23 @@ Future checkForTokenUpdates() async { } Future initFCMAfterAuthenticated({bool force = false}) async { + final fcmToken = userService.currentUser.fcmToken; if (userService.currentUser.updateFCMToken || force) { - const storage = FlutterSecureStorage(); - final storedToken = await storage.read(key: SecureStorageKeys.googleFcm); - if (storedToken != null) { - final res = await apiService.updateFCMToken(storedToken); - if (res.isSuccess) { - Log.info('Uploaded new FCM token!'); - await UserService.update((u) { - u.updateFCMToken = false; - }); - } else { - Log.error('Could not update FCM token!'); - } + if (fcmToken == null) { + Log.error('FCM token could not be updated as it is empty'); + await checkForTokenUpdates(); + return; + } + final res = await apiService.updateFCMToken( + fcmToken, + ); + if (res.isSuccess) { + Log.info('Uploaded new FCM token!'); + await UserService.update((u) { + u.updateFCMToken = false; + }); } else { - Log.error('Could not send FCM update to server as token is empty.'); + Log.error('Could not update FCM token!'); } } } @@ -99,7 +96,7 @@ Future resetFCMTokens() async { Log.info('Firebase Installation successfully deleted.'); await FirebaseMessaging.instance.deleteToken(); Log.info('Old FCM deleted.'); - await const FlutterSecureStorage().delete(key: SecureStorageKeys.googleFcm); + await UserService.update((u) => u.fcmToken = null); await checkForTokenUpdates(); await initFCMAfterAuthenticated(force: true); } @@ -119,7 +116,9 @@ Future initFCMService() async { @pragma('vm:entry-point') Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { SentryWidgetsFlutterBinding.ensureInitialized(); + await AppEnvironment.init(); final isInitialized = await initBackgroundExecution(); + await setupPushNotification(); Log.info('Handling a background message: ${message.messageId}'); await handleRemoteMessage(message); diff --git a/lib/src/services/signal/encryption.signal.dart b/lib/src/services/signal/encryption.signal.dart index d8b4e1b5..0065eb4a 100644 --- a/lib/src/services/signal/encryption.signal.dart +++ b/lib/src/services/signal/encryption.signal.dart @@ -12,15 +12,11 @@ import 'package:twonly/src/utils/log.dart'; Future signalEncryptMessage( int target, - Uint8List plaintextContent, { - bool useLock = true, -}) async { - if (useLock) { - return lockingSignalProtocol.protect(() async { - return _signalEncryptMessage(target, plaintextContent); - }); - } - return _signalEncryptMessage(target, plaintextContent); + Uint8List plaintextContent, +) async { + return lockingSignalProtocol.protect(() async { + return _signalEncryptMessage(target, plaintextContent); + }); } Future _signalEncryptMessage( @@ -44,63 +40,83 @@ signalDecryptMessage( Uint8List encryptedContentRaw, int type, ) async { - return lockingSignalProtocol.protect(() async { - try { - final session = SessionCipher.fromStore( - (await getSignalStore())!, - getSignalAddress(fromUserId), - ); - - Uint8List plaintext; - - switch (type) { - case CiphertextMessage.prekeyType: - plaintext = await session.decrypt( - PreKeySignalMessage(encryptedContentRaw), + // Hold the lock only for the cryptographic operation, not for network I/O + final (decryptedContent, errorType, needsResync) = await lockingSignalProtocol + .protect(() async { + try { + final session = SessionCipher.fromStore( + (await getSignalStore())!, + getSignalAddress(fromUserId), ); - case CiphertextMessage.whisperType: - plaintext = await session.decryptFromSignal( - SignalMessage.fromSerialized(encryptedContentRaw), + + Uint8List plaintext; + + switch (type) { + case CiphertextMessage.prekeyType: + plaintext = await session.decrypt( + PreKeySignalMessage(encryptedContentRaw), + ); + case CiphertextMessage.whisperType: + plaintext = await session.decryptFromSignal( + SignalMessage.fromSerialized(encryptedContentRaw), + ); + default: + Log.error('Unknown Message Decryption Type: $type'); + return ( + null, + PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN, + false, + ); + } + + return (EncryptedContent.fromBuffer(plaintext), null, false); + } on InvalidKeyIdException catch (e) { + Log.warn(e); + return ( + null, + PlaintextContent_DecryptionErrorMessage_Type.PREKEY_UNKNOWN, + false, ); - default: - Log.error('Unknown Message Decryption Type: $type'); - return (null, PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN); - } - - return (EncryptedContent.fromBuffer(plaintext), null); - } on InvalidKeyIdException catch (e) { - Log.warn(e); - return ( - null, - PlaintextContent_DecryptionErrorMessage_Type.PREKEY_UNKNOWN, - ); - } on DuplicateMessageException catch (e) { - Log.info(e.toString()); - return (null, null); - } on InvalidMessageException catch (e) { - Log.warn(e); - if (!resyncedUsers.contains(fromUserId)) { - if (await handleSessionResync(fromUserId, useLock: false)) { - // This flag prevents from resyncing the session the client received multiple new - // messages from the server he could not decrypt - resyncedUsers.add(fromUserId); - - // This message contains a new PreKeyBundle establishing a new signal session - await sendCipherText( - fromUserId, - EncryptedContent( - errorMessages: EncryptedContent_ErrorMessages( - type: EncryptedContent_ErrorMessages_Type.SESSION_OUT_OF_SYNC, - ), - ), - useLock: false, + } on DuplicateMessageException catch (e) { + Log.info(e.toString()); + return (null, null, false); + } on InvalidMessageException catch (e) { + Log.warn(e); + return ( + null, + PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN, + true, + ); + } catch (e) { + Log.error(e); + return ( + null, + PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN, + false, ); } - } - return (null, PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN); - } catch (e) { - Log.error(e); - return (null, PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN); + }); + + // Handle session resync OUTSIDE the lock to avoid holding it during + // network round-trips (which can block for up to 60 seconds) + if (needsResync && !resyncedUsers.contains(fromUserId)) { + if (await handleSessionResync(fromUserId)) { + // This flag prevents from resyncing the session the client received + // multiple new messages from the server he could not decrypt + resyncedUsers.add(fromUserId); + + // This message contains a new PreKeyBundle establishing a new signal + // session + await sendCipherText( + fromUserId, + EncryptedContent( + errorMessages: EncryptedContent_ErrorMessages( + type: EncryptedContent_ErrorMessages_Type.SESSION_OUT_OF_SYNC, + ), + ), + ); } - }); + } + + return (decryptedContent, errorType); } diff --git a/lib/src/services/signal/identity.signal.dart b/lib/src/services/signal/identity.signal.dart index 6aadec4c..8c7846db 100644 --- a/lib/src/services/signal/identity.signal.dart +++ b/lib/src/services/signal/identity.signal.dart @@ -1,59 +1,51 @@ -import 'dart:convert'; import 'dart:typed_data'; - import 'package:clock/clock.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; +import 'package:twonly/core/bridge/wrapper/key_manager.dart'; import 'package:twonly/locator.dart'; -import 'package:twonly/src/constants/secure_storage.keys.dart'; -import 'package:twonly/src/database/signal/signal_protocol_store.dart'; import 'package:twonly/src/model/json/signal_identity.model.dart'; import 'package:twonly/src/services/signal/consts.signal.dart'; import 'package:twonly/src/services/signal/protocol_state.signal.dart'; import 'package:twonly/src/services/signal/utils.signal.dart'; import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/utils/log.dart'; -import 'package:twonly/src/utils/secure_storage.dart'; -Future getSignalIdentityKeyPair() async { - final signalIdentity = await getSignalIdentity(); - if (signalIdentity == null) return null; - return IdentityKeyPair.fromSerialized(signalIdentity.identityKeyPairU8List); -} - -// This function runs after the clients authenticated with the server. -// It then checks if it should update a new session key -Future signalHandleNewServerConnection() async { - if (userService.currentUser.signalLastSignedPreKeyUpdated != null) { - final fortyEightHoursAgo = clock.now().subtract(const Duration(hours: 48)); - final isYoungerThan48Hours = - (userService.currentUser.signalLastSignedPreKeyUpdated!).isAfter( - fortyEightHoursAgo, - ); - if (isYoungerThan48Hours) { - // The key does live for 48 hours then it expires and a new key is generated. +class SignalIdentityService { + static Future onAuthenticated() async { + if (userService.currentUser.signalLastSignedPreKeyUpdated != null) { + final fortyEightHoursAgo = clock.now().subtract( + const Duration(hours: 48), + ); + final isYoungerThan48Hours = + (userService.currentUser.signalLastSignedPreKeyUpdated!).isAfter( + fortyEightHoursAgo, + ); + if (isYoungerThan48Hours) { + // The key does live for 48 hours then it expires and a new key is generated. + return; + } + } + final signedPreKey = await _getNewSignalSignedPreKey(); + if (signedPreKey == null) { + Log.error('could not generate a new signed pre key!'); return; } - } - final signedPreKey = await _getNewSignalSignedPreKey(); - if (signedPreKey == null) { - Log.error('could not generate a new signed pre key!'); - return; - } - await UserService.update((user) { - user.signalLastSignedPreKeyUpdated = clock.now(); - }); - final res = await apiService.updateSignedPreKey( - signedPreKey.id, - signedPreKey.getKeyPair().publicKey.serialize(), - signedPreKey.signature, - ); - if (res.isError) { - Log.error('could not update the signed pre key: ${res.error}'); await UserService.update((user) { - user.signalLastSignedPreKeyUpdated = null; + user.signalLastSignedPreKeyUpdated = clock.now(); }); - } else { - Log.info('updated signed pre key'); + final res = await apiService.updateSignedPreKey( + signedPreKey.id, + signedPreKey.getKeyPair().publicKey.serialize(), + signedPreKey.signature, + ); + if (res.isError) { + Log.error('could not update the signed pre key: ${res.error}'); + await UserService.update((user) { + user.signalLastSignedPreKeyUpdated = null; + }); + } else { + Log.info('updated signed pre key'); + } } } @@ -75,64 +67,45 @@ Future> signalGetPreKeys() async { Future getSignalIdentity() async { try { - var signalIdentityJson = await SecureStorage.instance.read( - key: SecureStorageKeys.signalIdentity, + final identity = await RustKeyManager.getSignalIdentity(); + return SignalIdentity( + identityKeyPairU8List: identity.$1, + registrationId: identity.$2, ); - if (signalIdentityJson == null) { - return null; - } - final decoded = jsonDecode(signalIdentityJson); - signalIdentityJson = null; - return SignalIdentity.fromJson(decoded as Map); } catch (e) { Log.error('could not load signal identity: $e'); return null; } } +Future getSignalIdentityKeyPair() async { + final signalIdentity = await getSignalIdentity(); + if (signalIdentity == null) return null; + return IdentityKeyPair.fromSerialized(signalIdentity.identityKeyPairU8List); +} + Future getUserPublicKey() async { - Log.info('getUserPublicKey: getting identity'); final signalIdentity = (await getSignalIdentity())!; - Log.info('getUserPublicKey: getting signal store'); final signalStore = await getSignalStoreFromIdentity(signalIdentity); - Log.info('getUserPublicKey: getting key pair'); final keyPair = await signalStore.getIdentityKeyPair(); - Log.info('getUserPublicKey: serializing public key'); return keyPair.getPublicKey().serialize(); } Future createIfNotExistsSignalIdentity() async { - final signalIdentity = await SecureStorage.instance.read( - key: SecureStorageKeys.signalIdentity, - ); - - if (signalIdentity != null) { - return; - } + // check if identity already exists + if (await getSignalIdentity() != null) return; final identityKeyPair = generateIdentityKeyPair(); final registrationId = generateRegistrationId(true); - final signalStore = SignalSignalProtocolStore( - identityKeyPair, - registrationId, - ); - final signedPreKey = generateSignedPreKey(identityKeyPair, defaultDeviceId); + final signedPreKeyStore = {}; + signedPreKeyStore[signedPreKey.id] = signedPreKey.serialize(); - await signalStore.signedPreKeyStore.storeSignedPreKey( - signedPreKey.id, - signedPreKey, - ); - - final storedSignalIdentity = SignalIdentity( - identityKeyPairU8List: identityKeyPair.serialize(), + await RustKeyManager.importSignalIdentity( + identityKeyPairStructure: identityKeyPair.serialize(), registrationId: registrationId, - ); - - await SecureStorage.instance.write( - key: SecureStorageKeys.signalIdentity, - value: jsonEncode(storedSignalIdentity), + signedPreKeyStore: signedPreKeyStore, ); } diff --git a/lib/src/services/signal/session.signal.dart b/lib/src/services/signal/session.signal.dart index f23236b5..a26d4b4c 100644 --- a/lib/src/services/signal/session.signal.dart +++ b/lib/src/services/signal/session.signal.dart @@ -8,16 +8,10 @@ import 'package:twonly/src/services/signal/protocol_state.signal.dart'; import 'package:twonly/src/services/signal/utils.signal.dart'; import 'package:twonly/src/utils/log.dart'; -Future processSignalUserData( - Response_UserData userData, { - bool useLock = true, -}) async { - if (useLock) { - return lockingSignalProtocol.protect(() async { - return _processSignalUserData(userData); - }); - } - return _processSignalUserData(userData); +Future processSignalUserData(Response_UserData userData) async { + return lockingSignalProtocol.protect(() async { + return _processSignalUserData(userData); + }); } Future _processSignalUserData(Response_UserData userData) async { @@ -106,14 +100,11 @@ Future getPublicKeyFromContact(int contactId) async { } } -Future handleSessionResync( - int fromUserId, { - bool useLock = true, -}) async { +Future handleSessionResync(int fromUserId) async { final userData = await apiService.getUserById(fromUserId); if (userData != null) { Log.info('Got new session data from the server to re-sync the session'); - return processSignalUserData(userData, useLock: useLock); + return processSignalUserData(userData); } Log.info('Could not download userdata from the server.'); return false; diff --git a/lib/src/services/user.service.dart b/lib/src/services/user.service.dart index 6bdb53fa..700e01bb 100644 --- a/lib/src/services/user.service.dart +++ b/lib/src/services/user.service.dart @@ -2,9 +2,11 @@ import 'dart:async'; import 'dart:convert'; import 'package:mutex/mutex.dart'; +import 'package:twonly/core/bridge/wrapper/key_manager.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/constants/secure_storage.keys.dart'; import 'package:twonly/src/model/json/userdata.model.dart'; +import 'package:twonly/src/utils/keyvalue.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/secure_storage.dart'; @@ -26,21 +28,49 @@ class UserService { static Future getUser() async { try { + // 1. Try to load from KeyValueStore (user.json) + final userDataMap = await KeyValueStore.get('user'); + if (userDataMap != null) { + final userData = UserData.fromJson(userDataMap); + await RustKeyManager.setUserId(userId: userData.userId); + return userData; + } + + // 2. If not found, try to load from SecureStorage (Migration path) final userDataJson = await SecureStorage.instance.read( key: SecureStorageKeys.userData, ); - if (userDataJson == null) { - return null; + + if (userDataJson != null) { + final userData = UserData.fromJson( + jsonDecode(userDataJson) as Map, + ); + + // 3. Run migration + await _migrateFromSecureStorage(userData); + return userData; } - return UserData.fromJson( - jsonDecode(userDataJson) as Map, - ); + + return null; } catch (e) { Log.error('could not load user: $e'); - rethrow; // Rethrow instead of returning null to distinguish error from missing user + rethrow; } } + static Future _migrateFromSecureStorage(UserData userData) async { + // Currently empty migration logic as requested, but we MUST store the data + await KeyValueStore.put('user', userData.toJson()); + try { + await RustKeyManager.setUserId(userId: userData.userId); + } catch (e) { + Log.error('Could not set userId in RustKeyManager during migration: $e'); + } + + // Optional: Log migration + Log.info('Migrated user data from SecureStorage to KeyValueStore'); + } + static Future update( void Function(UserData userData) updateUser, ) async { @@ -53,10 +83,7 @@ class UserService { user.defaultShowTime = null; } updateUser(user); - await SecureStorage.instance.write( - key: SecureStorageKeys.userData, - value: jsonEncode(user), - ); + await KeyValueStore.put('user', user.toJson()); userService.currentUser = user; } catch (e) { Log.error('Could not update the user: $e'); @@ -66,6 +93,16 @@ class UserService { userService.triggerUserUpdate(); } + static Future save(UserData user) async { + await KeyValueStore.put('user', user.toJson()); + try { + await RustKeyManager.setUserId(userId: user.userId); + } catch (e) { + Log.error('Could not set userId in RustKeyManager during save: $e'); + } + await userService.tryInit(); + } + void triggerUserUpdate() { _userDataUpdateController.add(null); } diff --git a/lib/src/services/user_discovery.service.dart b/lib/src/services/user_discovery.service.dart index 0161c1f0..2d4486e5 100644 --- a/lib/src/services/user_discovery.service.dart +++ b/lib/src/services/user_discovery.service.dart @@ -1,8 +1,11 @@ +import 'dart:async'; import 'dart:convert'; +import 'dart:io'; import 'package:collection/collection.dart'; import 'package:drift/drift.dart'; import 'package:flutter/foundation.dart'; import 'package:twonly/core/bridge/wrapper/user_discovery.dart'; +import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/protobuf/client/generated/user_discovery/types.pb.dart'; @@ -177,7 +180,7 @@ class UserDiscoveryService { } } - static Future removeDeletedContacts() async { + static Future _removeDeletedContacts() async { final subquery = twonlyDB.selectOnly(twonlyDB.contacts) ..addColumns([twonlyDB.contacts.userId]) ..where(twonlyDB.contacts.accountDeleted.equals(true)); @@ -216,4 +219,35 @@ class UserDiscoveryService { u.isUserDiscoveryEnabled = false; }); } + + static Future verifyInitializationOnStartup() async { + await _removeDeletedContacts(); + final configExists = File( + '${AppEnvironment.supportDir}/user_discovery_config.json', + ).existsSync(); + final hasShares = await (twonlyDB.select( + twonlyDB.userDiscoveryShares, + )..limit(1)).get().then((list) => list.isNotEmpty); + + if (userService.currentUser.isUserDiscoveryEnabled && + (userService.currentUser.userDiscoveryInitializationError || + !configExists || + !hasShares)) { + unawaited(() async { + try { + Log.info( + 'Retrying UserDiscovery initialization on startup (configExists: $configExists, hasShares: $hasShares)', + ); + await initializeOrUpdate( + threshold: userService.currentUser.userDiscoveryThreshold, + sharePromotion: userService.currentUser.userDiscoverySharePromotion, + ); + } catch (e) { + Log.error( + 'Failed to retry UserDiscovery initialization on startup: $e', + ); + } + }()); + } + } } diff --git a/lib/src/utils/keyvalue.dart b/lib/src/utils/keyvalue.dart index b8f5ce5d..f879a208 100644 --- a/lib/src/utils/keyvalue.dart +++ b/lib/src/utils/keyvalue.dart @@ -7,7 +7,11 @@ import 'package:twonly/src/utils/exclusive_access.utils.dart'; import 'package:twonly/src/utils/log.dart'; class KeyValueStore { - static final Mutex _mutex = Mutex(); + static final Map _mutexes = {}; + + static Mutex _getMutex(String key) { + return _mutexes.putIfAbsent(key, Mutex.new); + } static Future _getFilePath(String key) async { return File('${AppEnvironment.supportDir}/keyvalue/$key.json'); @@ -16,7 +20,7 @@ class KeyValueStore { static Future _exclusive(String key, Future Function() action) { return exclusiveAccess( lockName: 'keyvalue-$key', - mutex: _mutex, + mutex: _getMutex(key), action: action, ); } @@ -32,31 +36,33 @@ class KeyValueStore { } }); - static Future?> get(String key) => - _exclusive(key, () async { - final file = await _getFilePath(key); - try { - if (file.existsSync()) { - final contents = await file.readAsString(); - return jsonDecode(contents) as Map; - } else { - return null; - } - } catch (e) { - Log.warn('Error reading file. Deleting it.: $e'); - file.deleteSync(); + static Future?> get(String key) async { + return _exclusive(key, () async { + final file = await _getFilePath(key); + try { + if (file.existsSync()) { + final contents = await file.readAsString(); + return jsonDecode(contents) as Map; + } else { return null; } - }); + } catch (e) { + Log.warn('Error reading file. Deleting it.: $e'); + file.deleteSync(); + return null; + } + }); + } - static Future put(String key, Map value) => - _exclusive(key, () async { - try { - final file = await _getFilePath(key); - await file.parent.create(recursive: true); - await file.writeAsString(jsonEncode(value)); - } catch (e) { - Log.error('Error writing file: $e'); - } - }); + static Future put(String key, Map value) async { + return _exclusive(key, () async { + try { + final file = await _getFilePath(key); + await file.parent.create(recursive: true); + await file.writeAsString(jsonEncode(value)); + } catch (e) { + Log.error('Error writing file: $e'); + } + }); + } } diff --git a/lib/src/utils/log.dart b/lib/src/utils/log.dart index 22f3ec94..ae44ca31 100644 --- a/lib/src/utils/log.dart +++ b/lib/src/utils/log.dart @@ -29,7 +29,10 @@ class Log { static String filterLogMessage(String msg) { if (msg.contains('SqliteException')) { // Do not log data which would be inserted into the DB. - return msg.substring(0, msg.indexOf('parameters: ')); + final paramIndex = msg.indexOf('parameters: '); + if (paramIndex != -1) { + return msg.substring(0, paramIndex); + } } return msg; } diff --git a/lib/src/utils/misc.dart b/lib/src/utils/misc.dart index 33874fc9..873bfae3 100644 --- a/lib/src/utils/misc.dart +++ b/lib/src/utils/misc.dart @@ -197,12 +197,12 @@ String formatDateTime(BuildContext context, DateTime? dateTime) { } } -String formatBytes(int bytes, {int decimalPlaces = 2}) { +String formatBytes(int bytes) { if (bytes <= 0) return '0 Bytes'; const units = ['Bytes', 'KB', 'MB', 'GB', 'TB']; final unitIndex = (log(bytes) / log(1000)).floor(); final formattedSize = bytes / pow(1000, unitIndex); - return '${formattedSize.toStringAsFixed(decimalPlaces)} ${units[unitIndex]}'; + return '${formattedSize.ceil()} ${units[unitIndex]}'; } bool isUUIDNewer(String uuid1, String uuid2) { diff --git a/lib/src/visual/components/animate_icon.comp.dart b/lib/src/visual/components/animate_icon.comp.dart index 3f2edc9e..e0f24440 100644 --- a/lib/src/visual/components/animate_icon.comp.dart +++ b/lib/src/visual/components/animate_icon.comp.dart @@ -9,6 +9,9 @@ RegExp emojiRegex() => RegExp( ); bool isOneEmoji(String character) { + if (EmojiAnimationComp.animatedIcons.containsKey(character)) { + return true; + } final matches = emojiRegex().allMatches(character); if (matches.length == 1) { final match = matches.first; @@ -82,6 +85,7 @@ class EmojiAnimationComp extends StatelessWidget { '😴': 'sleep.lottie', '🤒': 'thermometer-face.lottie', '🤕': 'bandage-face.lottie', + '🫪': 'distorted_face.json', '🤥': 'liar.lottie', '😇': 'halo.lottie', '🤠': 'cowboy.lottie', diff --git a/lib/src/visual/components/select_chat_deletion_time.comp.dart b/lib/src/visual/components/select_chat_deletion_time.comp.dart index bae7ec3e..4151344f 100644 --- a/lib/src/visual/components/select_chat_deletion_time.comp.dart +++ b/lib/src/visual/components/select_chat_deletion_time.comp.dart @@ -9,7 +9,7 @@ import 'package:twonly/src/database/tables/groups.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/services/api/messages.api.dart'; -import 'package:twonly/src/services/group.services.dart'; +import 'package:twonly/src/services/group.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/views/groups/group.view.dart'; diff --git a/lib/src/visual/components/snackbar.dart b/lib/src/visual/components/snackbar.dart new file mode 100644 index 00000000..e182fa9a --- /dev/null +++ b/lib/src/visual/components/snackbar.dart @@ -0,0 +1,258 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +enum SnackbarLevel { + info, + success, + warning, + error, +} + +void showSnackbar( + BuildContext context, + String message, { + SnackbarLevel level = SnackbarLevel.error, +}) { + Color backgroundColor; + IconData iconData; + + switch (level) { + case SnackbarLevel.info: + backgroundColor = Colors.blue.shade700; + iconData = Icons.info_outline; + case SnackbarLevel.success: + backgroundColor = Colors.green.shade700; + iconData = Icons.check_circle_outline; + case SnackbarLevel.warning: + backgroundColor = Colors.orange.shade800; + iconData = Icons.warning_amber_rounded; + case SnackbarLevel.error: + backgroundColor = Colors.red.shade700; + iconData = Icons.error_outline; + } + + AnimationController? localAnimationController; + + _showOverlay( + context: context, + animationDuration: const Duration(milliseconds: 1000), + reverseAnimationDuration: const Duration(milliseconds: 350), + displayDuration: const Duration(milliseconds: 3000), + onAnimationControllerInit: (controller) => + localAnimationController = controller, + child: _SnackbarWidget( + message: message, + backgroundColor: backgroundColor, + icon: Icon(iconData, color: Colors.white, size: 28), + onCloseClick: () { + localAnimationController?.reverse(); + }, + ), + ); +} + +OverlayEntry? _previousEntry; + +void _showOverlay({ + required BuildContext context, + required Widget child, + required Duration animationDuration, + required Duration reverseAnimationDuration, + required Duration displayDuration, + required void Function(AnimationController) onAnimationControllerInit, +}) { + final overlayState = Overlay.maybeOf(context); + if (overlayState == null) return; + + late OverlayEntry overlayEntry; + overlayEntry = OverlayEntry( + builder: (_) => _AnimatedSnackbar( + animationDuration: animationDuration, + reverseAnimationDuration: reverseAnimationDuration, + displayDuration: displayDuration, + onAnimationControllerInit: onAnimationControllerInit, + onDismissed: () { + if (overlayEntry.mounted) { + overlayEntry.remove(); + } + if (_previousEntry == overlayEntry) { + _previousEntry = null; + } + }, + child: child, + ), + ); + + if (_previousEntry != null && _previousEntry!.mounted) { + _previousEntry?.remove(); + } + + overlayState.insert(overlayEntry); + _previousEntry = overlayEntry; +} + +class _SnackbarWidget extends StatelessWidget { + const _SnackbarWidget({ + required this.message, + required this.backgroundColor, + required this.icon, + required this.onCloseClick, + }); + final String message; + final Color backgroundColor; + final Icon icon; + final VoidCallback onCloseClick; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Container( + clipBehavior: Clip.hardEdge, + constraints: const BoxConstraints(minHeight: 70), + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: const [ + BoxShadow( + color: Colors.black26, + spreadRadius: 1, + blurRadius: 30, + ), + ], + ), + width: double.infinity, + child: Row( + children: [ + const SizedBox(width: 16), + icon, + const SizedBox(width: 12), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Text( + message, + style: theme.textTheme.bodyMedium?.merge( + const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16, + color: Colors.white, + ), + ), + textAlign: TextAlign.start, + ), + ), + ), + GestureDetector( + onTap: onCloseClick, + behavior: HitTestBehavior.opaque, + child: const Padding( + padding: EdgeInsets.all(16), + child: Icon(Icons.close, color: Colors.white70, size: 20), + ), + ), + ], + ), + ); + } +} + +class _AnimatedSnackbar extends StatefulWidget { + const _AnimatedSnackbar({ + required this.child, + required this.onDismissed, + required this.animationDuration, + required this.reverseAnimationDuration, + required this.displayDuration, + required this.onAnimationControllerInit, + }); + final Widget child; + final VoidCallback onDismissed; + final Duration animationDuration; + final Duration reverseAnimationDuration; + final Duration displayDuration; + final void Function(AnimationController) onAnimationControllerInit; + + @override + State<_AnimatedSnackbar> createState() => _AnimatedSnackbarState(); +} + +class _AnimatedSnackbarState extends State<_AnimatedSnackbar> + with SingleTickerProviderStateMixin { + late final AnimationController _animationController; + late final Animation _offsetAnimation; + Timer? _timer; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + vsync: this, + duration: widget.animationDuration, + reverseDuration: widget.reverseAnimationDuration, + ); + + _animationController.addStatusListener(_handleAnimationStatus); + widget.onAnimationControllerInit(_animationController); + + _offsetAnimation = + Tween( + begin: const Offset(0, -1), + end: Offset.zero, + ).animate( + CurvedAnimation( + parent: _animationController, + curve: Curves.elasticOut, + reverseCurve: Curves.linearToEaseOut, + ), + ); + + _animationController.forward(); + } + + void _handleAnimationStatus(AnimationStatus status) { + if (status == AnimationStatus.completed) { + _timer = Timer(widget.displayDuration, () { + if (mounted) { + _animationController.reverse(); + } + }); + } else if (status == AnimationStatus.dismissed) { + _timer?.cancel(); + widget.onDismissed(); + } + } + + @override + void dispose() { + _animationController.dispose(); + _timer?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Positioned( + top: 16, + left: 16, + right: 16, + child: SlideTransition( + position: _offsetAnimation, + child: SafeArea( + child: Dismissible( + key: UniqueKey(), + direction: DismissDirection.up, + dismissThresholds: const {DismissDirection.up: 0.2}, + confirmDismiss: (_) async { + if (mounted) { + await _animationController.reverse(); + } + return false; + }, + child: widget.child, + ), + ), + ), + ); + } +} diff --git a/lib/src/visual/views/camera/add_new_shortcut.view.dart b/lib/src/visual/views/camera/add_new_shortcut.view.dart new file mode 100644 index 00000000..11a611e4 --- /dev/null +++ b/lib/src/visual/views/camera/add_new_shortcut.view.dart @@ -0,0 +1,292 @@ +import 'dart:async'; +import 'dart:collection'; +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/locator.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; +import 'package:twonly/src/visual/components/emoji_picker.bottom.dart'; +import 'package:twonly/src/visual/components/flame_counter.comp.dart'; +import 'package:twonly/src/visual/components/snackbar.dart'; +import 'package:twonly/src/visual/decorations/input_text.decoration.dart'; +import 'package:twonly/src/visual/views/camera/share_image_editor_components/layer_data.dart'; + +class AddNewShortcutView extends StatefulWidget { + const AddNewShortcutView({this.shortcut, super.key}); + final Shortcut? shortcut; + @override + State createState() => _StartNewChatView(); +} + +class _StartNewChatView extends State { + List _groups = []; + List _allGroups = []; + final TextEditingController _searchGroupName = TextEditingController(); + late StreamSubscription> _groupSub; + + final HashSet _selectedGroups = HashSet(); + String? shortcutEmoji; + + @override + void initState() { + super.initState(); + + if (widget.shortcut != null) { + shortcutEmoji = widget.shortcut!.emoji; + twonlyDB.shortcutsDao.getShortcutMembers(widget.shortcut!.id).then(( + members, + ) { + if (mounted) { + setState(() { + for (final m in members) { + _selectedGroups.add(m.groupId); + } + }); + } + }); + } + + final stream = twonlyDB.groupsDao.watchGroupsForChatList(); + + _groupSub = stream.listen((update) async { + update.sort( + (a, b) => a.groupName.compareTo(b.groupName), + ); + setState(() { + _allGroups = update; + }); + await filterUsers(); + }); + } + + @override + void dispose() { + unawaited(_groupSub.cancel()); + super.dispose(); + } + + Future filterUsers() async { + if (_searchGroupName.value.text.isEmpty) { + setState(() { + _groups = _allGroups; + }); + return; + } + final usersFiltered = _allGroups + .where( + (group) => group.groupName.toLowerCase().contains( + _searchGroupName.value.text.toLowerCase(), + ), + ) + .toList(); + setState(() { + _groups = usersFiltered; + }); + } + + void toggleSelectedGroup(String groupId) { + if (!_selectedGroups.contains(groupId)) { + if (_selectedGroups.length > 256) { + showSnackbar(context, context.lang.groupSizeLimitError(256)); + return; + } + _selectedGroups.add(groupId); + } else { + _selectedGroups.remove(groupId); + } + setState(() {}); + } + + Future submitChanges() async { + try { + if (widget.shortcut != null) { + await twonlyDB.shortcutsDao.updateShortcut( + widget.shortcut!.id, + shortcutEmoji!, + ); + await twonlyDB.shortcutsDao.deleteShortcutMembers(widget.shortcut!.id); + await twonlyDB.shortcutsDao.addShortcutMembers( + widget.shortcut!.id, + _selectedGroups.toList(), + ); + } else { + await twonlyDB.shortcutsDao.createShortcut( + shortcutEmoji!, + ); + final shortcutId = (await twonlyDB.shortcutsDao.getShortcutByEmoji( + shortcutEmoji!, + ))!.id; + await twonlyDB.shortcutsDao.deleteShortcutMembers(shortcutId); + await twonlyDB.shortcutsDao.addShortcutMembers( + shortcutId, + _selectedGroups.toList(), + ); + } + if (mounted) Navigator.pop(context); + } catch (e) { + Log.error(e); + if (mounted) { + showSnackbar(context, context.lang.errorEmojiUsedOrInvalid); + } + } + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () => FocusScope.of(context).unfocus(), + child: Scaffold( + appBar: AppBar( + title: Text( + widget.shortcut == null + ? context.lang.createShortcut + : context.lang.editShortcut, + ), + actions: [ + if (widget.shortcut != null) + IconButton( + icon: const FaIcon( + FontAwesomeIcons.trashCan, + size: 18, + color: Colors.red, + ), + onPressed: () async { + final confirm = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(context.lang.deleteShortcut), + content: Text(context.lang.deleteShortcutBody), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: Text(context.lang.cancel), + ), + FilledButton( + onPressed: () => Navigator.pop(context, true), + child: Text(context.lang.delete), + ), + ], + ), + ); + if (confirm == true) { + await twonlyDB.shortcutsDao.deleteShortcut( + widget.shortcut!.id, + ); + if (context.mounted) Navigator.pop(context); + } + }, + ), + TextButton( + onPressed: () async { + // ignore: inference_failure_on_function_invocation + final result = await showModalBottomSheet( + context: context, + backgroundColor: Colors.black, + builder: (context) => const EmojiPickerBottom(), + ); + if (result is EmojiLayerData) { + setState(() { + shortcutEmoji = result.text; + }); + } + }, + child: Text( + shortcutEmoji ?? context.lang.selectEmoji, + style: TextStyle( + fontSize: shortcutEmoji == null ? 14 : 22, + ), + ), + ), + const SizedBox(width: 8), + ], + ), + floatingActionButton: FilledButton.icon( + onPressed: (_selectedGroups.isEmpty || shortcutEmoji == null) + ? null + : submitChanges, + label: Text( + widget.shortcut == null + ? context.lang.createShortcut + : context.lang.updateShortcut, + ), + icon: const FaIcon(FontAwesomeIcons.check), + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.only( + bottom: 40, + left: 10, + top: 20, + right: 10, + ), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: TextField( + onChanged: (_) async { + await filterUsers(); + }, + controller: _searchGroupName, + decoration: getInputDecoration( + context, + context.lang.shareImageSearchAllContacts, + ), + ), + ), + const SizedBox(height: 10), + Expanded( + child: ListView.builder( + restorationId: 'new_message_users_list', + itemCount: _groups.length, + itemBuilder: (context, i) { + final group = _groups[i]; + return ListTile( + key: ValueKey(group.groupId), + title: Row( + children: [ + Text(substringBy(group.groupName, 12)), + FlameCounterWidget( + groupId: group.groupId, + prefix: true, + ), + ], + ), + leading: AvatarIcon( + group: group, + fontSize: 15, + ), + trailing: Checkbox( + value: _selectedGroups.contains(group.groupId), + side: WidgetStateBorderSide.resolveWith( + (states) { + if (states.contains(WidgetState.selected)) { + return const BorderSide(width: 0); + } + return BorderSide( + color: Theme.of(context).colorScheme.outline, + ); + }, + ), + onChanged: (value) { + toggleSelectedGroup(group.groupId); + }, + ), + onTap: () { + toggleSelectedGroup(group.groupId); + }, + ); + }, + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_components/camera_scanned_overlay.dart b/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_components/camera_scanned_overlay.dart index 05ee4ff3..30de85cf 100644 --- a/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_components/camera_scanned_overlay.dart +++ b/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_components/camera_scanned_overlay.dart @@ -5,6 +5,7 @@ import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/qr.utils.dart'; import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; +import 'package:twonly/src/visual/components/snackbar.dart'; import 'package:twonly/src/visual/views/camera/camera_preview_components/main_camera_controller.dart'; import 'package:url_launcher/url_launcher_string.dart'; @@ -46,16 +47,19 @@ class CameraScannedOverlay extends StatelessWidget { onTap: () async { c.isLoading = true; mainController.setState(); + + showSnackbar( + context, + context.lang.requestedUserToastText(c.profile.username), + level: SnackbarLevel.success, + ); if (await addNewContactFromPublicProfile(c.profile) && context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.lang.requestedUserToastText(c.profile.username), - ), - duration: const Duration(seconds: 8), - ), - ); + // showSnackbar( + // context, + // context.lang.requestedUserToastText(c.profile.username), + // level: SnackbarLevel.success, + // ); } }, child: Container( diff --git a/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart b/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart index 6b7a8a1c..954b4483 100644 --- a/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart +++ b/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart @@ -19,6 +19,7 @@ import 'package:twonly/src/services/api/mediafiles/upload.api.dart'; import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/components/snackbar.dart'; import 'package:twonly/src/visual/helpers/media_view_sizing.helper.dart'; import 'package:twonly/src/visual/helpers/screenshot.helper.dart'; import 'package:twonly/src/visual/loader/three_rotating_dots.loader.dart'; @@ -254,14 +255,12 @@ class _CameraPreviewViewState extends State { await File(picture.path).delete(); return imageBytes; } catch (e) { - if (context.mounted) { - // ignore: use_build_context_synchronously - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Error loading picture: $e'), - duration: const Duration(seconds: 3), - ), + if (mounted) { + showSnackbar( + context, + 'Error loading picture: $e', ); + Log.error(e); } return null; } @@ -284,6 +283,8 @@ class _CameraPreviewViewState extends State { await Future.delayed(const Duration(milliseconds: 1000)); } + if (!mounted) return; + await mc.cameraController?.pausePreview(); if (!mounted) { return; @@ -342,6 +343,9 @@ class _CameraPreviewViewState extends State { await _deInitVolumeControl(); if (!mounted) return true; + // Cache active camera ID since ShareImageEditorView closes the camera and resets state parameters. + final initialCameraId = mc.selectedCameraDetails.cameraId; + final shouldReturn = await Navigator.push( context, @@ -382,7 +386,7 @@ class _CameraPreviewViewState extends State { return true; } await mc.selectCamera( - mc.selectedCameraDetails.cameraId, + initialCameraId, false, ); return false; @@ -606,17 +610,7 @@ class _CameraPreviewViewState extends State { void _showCameraException(dynamic e) { Log.error('$e'); - try { - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Error: $e'), - duration: const Duration(seconds: 3), - ), - ); - } - // ignore: empty_catches - } catch (e) {} + if (mounted) showSnackbar(context, 'Error: $e'); } @override diff --git a/lib/src/visual/views/camera/camera_preview_components/main_camera_controller.dart b/lib/src/visual/views/camera/camera_preview_components/main_camera_controller.dart index d0d0672a..ec5a8206 100644 --- a/lib/src/visual/views/camera/camera_preview_components/main_camera_controller.dart +++ b/lib/src/visual/views/camera/camera_preview_components/main_camera_controller.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:camera/camera.dart'; import 'package:clock/clock.dart'; +import 'package:drift/drift.dart' show Value; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -17,6 +18,7 @@ import 'package:twonly/src/model/protobuf/client/generated/qr.pb.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/qr.utils.dart'; +import 'package:twonly/src/visual/components/snackbar.dart'; import 'package:twonly/src/visual/helpers/screenshot.helper.dart'; import 'package:twonly/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart'; import 'package:twonly/src/visual/views/camera/camera_preview_components/face_filters.dart'; @@ -83,6 +85,8 @@ class MainCameraController { FaceFilterType _currentFilterType = FaceFilterType.none; FaceFilterType get currentFilterType => _currentFilterType; + Future? _pendingDisposal; + Future closeCamera() async { contactsVerified = {}; scannedNewProfiles = {}; @@ -94,14 +98,18 @@ class MainCameraController { final cameraControllerTemp = cameraController; cameraController = null; // prevents: CameraException(Disposed CameraController, buildPreview() was called on a disposed CameraController.) - Future.delayed(const Duration(milliseconds: 100), () async { - await cameraControllerTemp?.dispose(); - }); + _pendingDisposal = Future.delayed( + const Duration(milliseconds: 100), + () async { + await cameraControllerTemp?.dispose(); + }, + ); initCameraStarted = false; selectedCameraDetails = SelectedCameraDetails(); } Future selectCamera(int sCameraId, bool init) async { + await _pendingDisposal; initCameraStarted = true; if (AppEnvironment.cameras.isEmpty) { @@ -136,7 +144,13 @@ class MainCameraController { ? ImageFormatGroup.nv21 : ImageFormatGroup.bgra8888, ); - await cameraController?.initialize(); + try { + await cameraController?.initialize(); + } catch (e) { + Log.error(e); + cameraController = null; // ensure uninitialized controller is not reused + return; + } await cameraController?.startImageStream(_processCameraImage); await cameraController?.setZoomLevel(selectedCameraDetails.scaleFactor); if (userService.currentUser.videoStabilizationEnabled && !kDebugMode) { @@ -216,7 +230,7 @@ class MainCameraController { (e.code == 'setFocusPointFailed' || e.code == 'setFocusModeFailed')) { Log.info('Focus point or mode not supported on this device'); } else { - Log.error(e); + Log.warn(e); } } @@ -356,7 +370,14 @@ class MainCameraController { if (res == null) continue; final (profile, contact, verificationOk) = res; - if (contact == null) { + if (contact?.blocked ?? false) { + await twonlyDB.contactsDao.updateContact( + contact!.userId, + const ContactsCompanion(blocked: Value(false)), + ); + } + + if (contact == null || contact.deletedByUser) { if (scannedNewProfiles[profile.userId.toInt()] == null) { await HapticFeedback.heavyImpact(); scannedNewProfiles[profile.userId.toInt()] = ScannedNewProfile( @@ -373,18 +394,14 @@ class MainCameraController { ); await HapticFeedback.heavyImpact(); - if (verificationOk) { - AppGlobalKeys.scaffoldMessengerKey.currentState?.showSnackBar( - SnackBar( - content: Text( - AppGlobalKeys.scaffoldMessengerKey.currentContext?.lang - .verifiedPublicKey( - getContactDisplayName(contact), - ) ?? - '', - ), - duration: const Duration(seconds: 6), + final context = cameraPreviewKey.currentContext; + if (verificationOk && context != null && context.mounted) { + showSnackbar( + context, + context.lang.verifiedPublicKey( + getContactDisplayName(contact), ), + level: SnackbarLevel.success, ); } } diff --git a/lib/src/visual/views/camera/camera_preview_components/save_to_gallery.dart b/lib/src/visual/views/camera/camera_preview_components/save_to_gallery.dart index 8b70c224..de92b0e6 100644 --- a/lib/src/visual/views/camera/camera_preview_components/save_to_gallery.dart +++ b/lib/src/visual/views/camera/camera_preview_components/save_to_gallery.dart @@ -77,10 +77,12 @@ class SaveToGalleryButtonState extends State { await newService.storeMediaFile(); } - setState(() { - _imageSaved = true; - _imageSaving = false; - }); + if (mounted) { + setState(() { + _imageSaved = true; + _imageSaving = false; + }); + } }, child: Row( children: [ diff --git a/lib/src/visual/views/camera/share_image_contact_selection.view.dart b/lib/src/visual/views/camera/share_image_contact_selection.view.dart index 74433e08..318fd5a6 100644 --- a/lib/src/visual/views/camera/share_image_contact_selection.view.dart +++ b/lib/src/visual/views/camera/share_image_contact_selection.view.dart @@ -20,6 +20,7 @@ import 'package:twonly/src/visual/decorations/input_text.decoration.dart'; import 'package:twonly/src/visual/elements/headline.element.dart'; import 'package:twonly/src/visual/helpers/screenshot.helper.dart'; import 'package:twonly/src/visual/views/camera/share_image_contact_selection_components/best_friends_selector.dart'; +import 'package:twonly/src/visual/views/camera/share_image_contact_selection_components/shortcut_row.comp.dart'; import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/background.layer.dart'; class ShareImageView extends StatefulWidget { @@ -194,6 +195,11 @@ class _ShareImageView extends State { ), ), ), + const SizedBox(height: 10), + ShortcutRowComp( + selectedGroupIds: widget.selectedGroupIds, + updateSelectedGroupIds: updateSelectedGroupIds, + ), if (_pinnedContacts.isNotEmpty) const SizedBox(height: 10), BestFriendsSelector( groups: _pinnedContacts, diff --git a/lib/src/visual/views/camera/share_image_contact_selection_components/shortcut_row.comp.dart b/lib/src/visual/views/camera/share_image_contact_selection_components/shortcut_row.comp.dart new file mode 100644 index 00000000..101094a1 --- /dev/null +++ b/lib/src/visual/views/camera/share_image_contact_selection_components/shortcut_row.comp.dart @@ -0,0 +1,115 @@ +import 'dart:async'; +import 'dart:collection'; +import 'package:flutter/material.dart'; +import 'package:twonly/locator.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/views/camera/add_new_shortcut.view.dart'; + +class ShortcutRowComp extends StatefulWidget { + const ShortcutRowComp({ + required this.selectedGroupIds, + required this.updateSelectedGroupIds, + super.key, + }); + + final HashSet selectedGroupIds; + final void Function(String, bool) updateSelectedGroupIds; + + @override + State createState() => _ShortcutRowCompState(); +} + +class _ShortcutRowCompState extends State { + List _shortcuts = []; + late StreamSubscription> shortcutSub; + + @override + void initState() { + super.initState(); + unawaited(initAsync()); + } + + Future initAsync() async { + shortcutSub = twonlyDB.shortcutsDao.watchAllShortcuts().listen((shortcuts) { + if (_shortcuts.isEmpty) { + shortcuts.sort((a, b) => b.usageCounter.compareTo(a.usageCounter)); + _shortcuts = shortcuts; + } else { + final map = {for (final s in shortcuts) s.id: s}; + final updated = []; + for (final old in _shortcuts) { + if (map.containsKey(old.id)) { + updated.add(map.remove(old.id)!); + } + } + updated.addAll(map.values); + _shortcuts = updated; + } + if (mounted) setState(() {}); + }); + } + + @override + void dispose() { + unawaited(shortcutSub.cancel()); + super.dispose(); + } + + Future _openCreateDialog() async { + await context.navPush(const AddNewShortcutView()); + } + + Future _applyShortcut(Shortcut shortcut) async { + await twonlyDB.shortcutsDao.incrementUsage(shortcut.id); + final members = await twonlyDB.shortcutsDao.getShortcutMembers(shortcut.id); + for (final groupId in widget.selectedGroupIds.toList()) { + widget.updateSelectedGroupIds(groupId, false); + } + for (final m in members) { + widget.updateSelectedGroupIds(m.groupId, true); + } + } + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 40, + child: ListView( + scrollDirection: Axis.horizontal, + children: [ + Row( + children: [ + ActionChip( + padding: EdgeInsets.zero, + onPressed: _openCreateDialog, + label: _shortcuts.isEmpty + ? Text( + context.lang.createShortcut, + style: const TextStyle(fontSize: 9), + ) + : const Icon(Icons.add_reaction_outlined, size: 20), + shape: const StadiumBorder(), + ), + for (final shortcut in _shortcuts) + GestureDetector( + onLongPress: () { + context.navPush(AddNewShortcutView(shortcut: shortcut)); + }, + child: ActionChip( + padding: EdgeInsets.zero, + onPressed: () => _applyShortcut(shortcut), + label: Text( + shortcut.emoji, + style: const TextStyle(fontSize: 18), + ), + shape: const StadiumBorder(), + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/src/visual/views/camera/share_image_editor_components/layers/filter.layer.dart b/lib/src/visual/views/camera/share_image_editor_components/layers/filter.layer.dart index 954d34c3..75e032fc 100644 --- a/lib/src/visual/views/camera/share_image_editor_components/layers/filter.layer.dart +++ b/lib/src/visual/views/camera/share_image_editor_components/layers/filter.layer.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; import 'package:twonly/src/visual/views/camera/share_image_editor_components/layer_data.dart'; import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/filters/datetime_filter.dart'; import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/filters/image_filter.dart'; -import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/filters/location_filter.dart'; +import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/filters/stickers.dart'; /// Main layer class FilterLayer extends StatefulWidget { @@ -75,7 +75,6 @@ class _FilterLayerState extends State { List pages = [ const FilterSkeleton(), const DateTimeFilter(), - // const LocationFilter(), const FilterSkeleton(), ]; diff --git a/lib/src/visual/views/camera/share_image_editor_components/layers/filters/location_filter.dart b/lib/src/visual/views/camera/share_image_editor_components/layers/filters/location_filter.dart deleted file mode 100644 index 55021aa8..00000000 --- a/lib/src/visual/views/camera/share_image_editor_components/layers/filters/location_filter.dart +++ /dev/null @@ -1,164 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:clock/clock.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:http/http.dart' as http; -import 'package:path_provider/path_provider.dart'; -import 'package:twonly/locator.dart'; -import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'; -import 'package:twonly/src/utils/log.dart'; -import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/filter.layer.dart'; -import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/filters/datetime_filter.dart'; - -class LocationFilter extends StatefulWidget { - const LocationFilter({super.key}); - - @override - State createState() => _LocationFilterState(); -} - -class _LocationFilterState extends State { - String? _imageUrl; - Response_Location? location; - - @override - void initState() { - super.initState(); - unawaited(initAsync()); - } - - Future initAsync() async { - final res = await apiService.getCurrentLocation(); - if (res.isSuccess) { - // ignore: avoid_dynamic_calls - location = res.value.location as Response_Location?; - await _searchForImage(); - if (mounted) setState(() {}); - } - } - - Future _searchForImage() async { - if (location == null) return; - final imageIndex = await getStickerIndex(); - // Normalize the city and country for search - final normalizedCity = location!.city.toLowerCase().replaceAll(' ', '_'); - final normalizedCountry = location!.county.toLowerCase(); - - // Search for the city first - for (final item in imageIndex) { - if (item.imageSrc.contains('/cities/$normalizedCountry/')) { - // Check if the item matches the normalized city - if (item.imageSrc.contains('$normalizedCity.')) { - if (item.imageSrc.startsWith('/api/')) { - _imageUrl = 'https://twonly.eu/${item.imageSrc}'; - if (mounted) setState(() {}); - } - return; - } - } - } - - // If city not found, search for the country - if (_imageUrl == null) { - for (final item in imageIndex) { - if (item.imageSrc.contains('/countries/') && - item.imageSrc.contains(normalizedCountry)) { - if (item.imageSrc.startsWith('/api/')) { - _imageUrl = 'https://twonly.eu/${item.imageSrc}'; - if (mounted) setState(() {}); - } - break; - } - } - } - } - - @override - Widget build(BuildContext context) { - if (_imageUrl != null) { - return FilterSkeleton( - child: Positioned( - bottom: 0, - left: 40, - right: 40, - child: Center( - child: CachedNetworkImage( - imageUrl: _imageUrl!, - ), - ), - ), - ); - } - - if (location != null) { - if (location!.county != '-') { - return FilterSkeleton( - child: Positioned( - bottom: 50, - left: 40, - child: Column( - children: [ - FilterText(location!.city), - FilterText(location!.county), - ], - ), - ), - ); - } - } - - return const DateTimeFilter(color: Colors.black); - } -} - -class Sticker { - Sticker({required this.imageSrc, required this.source}); - factory Sticker.fromJson(Map json) { - return Sticker( - imageSrc: json['imageSrc'] as String, - source: json['source'] as String? ?? '', - ); - } - final String imageSrc; - final String source; -} - -Future> getStickerIndex() async { - final directory = await getApplicationCacheDirectory(); - final indexFile = File('${directory.path}/stickers.json'); - var res = []; - - if (indexFile.existsSync() && kReleaseMode) { - final lastModified = indexFile.lastModifiedSync(); - final difference = clock.now().difference(lastModified); - final content = await indexFile.readAsString(); - final jsonList = json.decode(content) as List; - res = jsonList - .map((json) => Sticker.fromJson(json as Map)) - .toList(); - if (difference.inHours < 2) { - return res; - } - } - try { - final response = await http.get( - Uri.parse('https://twonly.eu/api/sticker/stickers.json'), - ); - if (response.statusCode == 200) { - await indexFile.writeAsString(response.body); - final jsonList = json.decode(response.body) as List; - return jsonList - .map((json) => Sticker.fromJson(json as Map)) - .toList(); - } else { - return res; - } - } catch (e) { - Log.error('$e'); - return res; - } -} diff --git a/lib/src/visual/views/camera/share_image_editor_components/layers/filters/stickers.dart b/lib/src/visual/views/camera/share_image_editor_components/layers/filters/stickers.dart new file mode 100644 index 00000000..946d0408 --- /dev/null +++ b/lib/src/visual/views/camera/share_image_editor_components/layers/filters/stickers.dart @@ -0,0 +1,56 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:clock/clock.dart'; +import 'package:flutter/foundation.dart'; +import 'package:http/http.dart' as http; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/utils/log.dart'; + +class Sticker { + Sticker({required this.imageSrc, required this.source}); + factory Sticker.fromJson(Map json) { + return Sticker( + imageSrc: json['imageSrc'] as String, + source: json['source'] as String? ?? '', + ); + } + final String imageSrc; + final String source; +} + +Future> getStickerIndex() async { + final indexFile = File('${AppEnvironment.cacheDir}/stickers.json'); + var res = []; + + if (indexFile.existsSync() && kReleaseMode) { + final lastModified = indexFile.lastModifiedSync(); + final difference = clock.now().difference(lastModified); + final content = await indexFile.readAsString(); + final jsonList = json.decode(content) as List; + res = jsonList + .map((json) => Sticker.fromJson(json as Map)) + .toList(); + if (difference.inHours < 2) { + return res; + } + } + try { + final response = await http.get( + Uri.parse('https://twonly.eu/api/sticker/stickers.json'), + ); + if (response.statusCode == 200) { + await indexFile.writeAsString(response.body); + final jsonList = json.decode(response.body) as List; + return jsonList + .map((json) => Sticker.fromJson(json as Map)) + .toList(); + } else { + return res; + } + } catch (e) { + Log.error('$e'); + return res; + } +} diff --git a/lib/src/visual/views/chats/chat_list.view.dart b/lib/src/visual/views/chats/chat_list.view.dart index 64aba01d..8a889073 100644 --- a/lib/src/visual/views/chats/chat_list.view.dart +++ b/lib/src/visual/views/chats/chat_list.view.dart @@ -21,6 +21,7 @@ import 'package:twonly/src/visual/themes/light.dart'; import 'package:twonly/src/visual/views/chats/chat_list_components/feedback_btn.comp.dart'; import 'package:twonly/src/visual/views/chats/chat_list_components/group_list_item.comp.dart'; import 'package:twonly/src/visual/views/onboarding/setup/components/finish_setup.comp.dart'; +import 'package:twonly/src/visual/views/settings/backup/components/missing_backup_setup.comp.dart'; class ChatListView extends StatefulWidget { const ChatListView({super.key}); @@ -215,6 +216,7 @@ class _ChatListViewState extends State { child: Column( children: [ const FinishSetupComp(), + const MissingBackupComp(), if (_groupsNotPinned.isEmpty && _groupsPinned.isEmpty && _groupsArchived.isEmpty) diff --git a/lib/src/visual/views/chats/chat_messages_components/chat_group_action.dart b/lib/src/visual/views/chats/chat_messages_components/chat_group_action.dart index 9ed5d44a..eeb14636 100644 --- a/lib/src/visual/views/chats/chat_messages_components/chat_group_action.dart +++ b/lib/src/visual/views/chats/chat_messages_components/chat_group_action.dart @@ -125,44 +125,6 @@ class _ChatGroupActionState extends State { } } - // switch (widget.action.type) { - // case GroupActionType.updatedGroupName: - // text = (contact == null) - // ? 'You have changed the group name to "${widget.action.newGroupName}".' - // : '$maker has changed the group name to "${widget.action.newGroupName}".'; - // icon = FontAwesomeIcons.pencil; - // case GroupActionType.createdGroup: - // icon = FontAwesomeIcons.penToSquare; - // text = (contact == null) - // ? 'You have created the group.' - // : '$maker has created the group.'; - // case GroupActionType.removedMember: - // icon = FontAwesomeIcons.userMinus; - // text = (contact == null) - // ? 'You have removed $affected from the group.' - // : '$maker has removed $affected from the group.'; - // case GroupActionType.addMember: - // icon = FontAwesomeIcons.userPlus; - // text = (contact == null) - // ? 'You have added $affected to the group.' - // : '$maker has added $affected to the group.'; - // case GroupActionType.promoteToAdmin: - // icon = FontAwesomeIcons.key; - // text = (contact == null) - // ? 'You made $affected an admin.' - // : '$maker made $affected an admin.'; - // case GroupActionType.demoteToMember: - // icon = FontAwesomeIcons.key; - // text = (contact == null) - // ? 'You revoked $affectedR admin rights.' - // : '$maker revoked $affectedR admin rights.'; - // case GroupActionType.leftGroup: - // icon = FontAwesomeIcons.userMinus; - // text = (contact == null) - // ? 'You have left the group.' - // : '$maker has left the group.'; - // } - return Padding( padding: const EdgeInsets.all(8), child: Center( diff --git a/lib/src/visual/views/chats/chat_messages_components/message_input.dart b/lib/src/visual/views/chats/chat_messages_components/message_input.dart index d00143fa..cc54579c 100644 --- a/lib/src/visual/views/chats/chat_messages_components/message_input.dart +++ b/lib/src/visual/views/chats/chat_messages_components/message_input.dart @@ -7,8 +7,8 @@ import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; +import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; @@ -131,8 +131,7 @@ class _MessageInputState extends State { _currentDuration = 0; }); await HapticFeedback.heavyImpact(); - final audioTmpPath = - '${(await getApplicationCacheDirectory()).path}/recording.m4a'; + final audioTmpPath = '${AppEnvironment.cacheDir}/recording.m4a'; unawaited( recorderController.record( path: audioTmpPath, diff --git a/lib/src/visual/views/chats/media_viewer.view.dart b/lib/src/visual/views/chats/media_viewer.view.dart index 5158ada4..16ca569c 100644 --- a/lib/src/visual/views/chats/media_viewer.view.dart +++ b/lib/src/visual/views/chats/media_viewer.view.dart @@ -61,7 +61,7 @@ class _MediaViewerViewState extends State { Message? currentMessage; DateTime? canBeSeenUntil; - double progress = 0; + final ValueNotifier progress = ValueNotifier(0); bool showSendTextMessageInput = false; final GlobalKey mediaWidgetKey = GlobalKey(); @@ -100,6 +100,7 @@ class _MediaViewerViewState extends State { progressTimer?.cancel(); _subscription?.cancel(); downloadStateListener?.cancel(); + progress.dispose(); ScreenProtector.preventScreenshotOff(); @@ -226,7 +227,7 @@ class _MediaViewerViewState extends State { canBeSeenUntil = null; imageSaving = false; imageSaved = false; - progress = 0; + progress.value = 0; showSendTextMessageInput = false; }); @@ -351,6 +352,11 @@ class _MediaViewerViewState extends State { return nextMediaOrExit(); } + // The server can now delete the encrypted bytes, as the users has sucessfully opened it. + unawaited( + apiService.downloadDone(currentMediaLocal.mediaFile.downloadToken!), + ); + var timerRequired = false; if (currentMediaLocal.mediaFile.type == MediaType.video) { @@ -388,9 +394,7 @@ class _MediaViewerViewState extends State { final duration = ctrl.value.duration.inSeconds; if (duration > 0) { - setState(() { - progress = 1 - ctrl.value.position.inSeconds / duration; - }); + progress.value = 1 - ctrl.value.position.inSeconds / duration; } if (currentMediaLocal.mediaFile.displayLimitInMilliseconds != @@ -450,9 +454,8 @@ class _MediaViewerViewState extends State { } final difference = canBeSeenUntil!.difference(clock.now()); // Calculate the progress as a value between 0.0 and 1.0 - progress = + progress.value = difference.inMilliseconds / (mediaFile.displayLimitInMilliseconds!); - setState(() {}); }); } } @@ -647,7 +650,7 @@ class _MediaViewerViewState extends State { children: [ if (_showDownloadingLoader) _loader(), if ((currentMedia != null || videoController != null) && - (canBeSeenUntil == null || progress >= 0)) + (canBeSeenUntil == null || progress.value >= 0)) GestureDetector( onTap: onTap, onDoubleTap: (videoController == null) ? null : onTap, @@ -717,7 +720,7 @@ class _MediaViewerViewState extends State { if (currentMedia != null && currentMedia?.mediaFile.downloadState != DownloadState.ready) Positioned.fill(child: _loader()), - if (canBeSeenUntil != null || progress >= 0) + if (canBeSeenUntil != null || progress.value >= 0) Positioned( right: 20, top: 27, @@ -726,9 +729,14 @@ class _MediaViewerViewState extends State { SizedBox( width: 20, height: 20, - child: CircularProgressIndicator( - value: progress, - strokeWidth: 2, + child: ValueListenableBuilder( + valueListenable: progress, + builder: (context, value, child) { + return CircularProgressIndicator( + value: value, + strokeWidth: 2, + ); + }, ), ), ], diff --git a/lib/src/visual/views/contact/add_contact_via_qr_link.view.dart b/lib/src/visual/views/contact/add_contact_via_qr_link.view.dart index cd23bc7e..50ce2c6a 100644 --- a/lib/src/visual/views/contact/add_contact_via_qr_link.view.dart +++ b/lib/src/visual/views/contact/add_contact_via_qr_link.view.dart @@ -10,8 +10,10 @@ import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart as server; import 'package:twonly/src/model/protobuf/client/generated/qr.pb.dart'; import 'package:twonly/src/services/api/utils.api.dart'; +import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/qr.utils.dart'; +import 'package:twonly/src/visual/components/snackbar.dart'; class AddContactViaQrLinkView extends StatefulWidget { const AddContactViaQrLinkView({ @@ -69,11 +71,8 @@ class _AddContactViaQrLinkViewState extends State { context.pop(); } } catch (e) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Error: $e')), - ); - } + if (mounted) showSnackbar(context, 'Error: $e'); + Log.error(e); } finally { if (mounted) { setState(() { diff --git a/lib/src/visual/views/contact/contact.view.dart b/lib/src/visual/views/contact/contact.view.dart index 61633e1f..4d6b5ad1 100644 --- a/lib/src/visual/views/contact/contact.view.dart +++ b/lib/src/visual/views/contact/contact.view.dart @@ -16,6 +16,7 @@ import 'package:twonly/src/visual/components/alert.dialog.dart'; import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; import 'package:twonly/src/visual/components/flame_counter.comp.dart'; import 'package:twonly/src/visual/components/select_chat_deletion_time.comp.dart'; +import 'package:twonly/src/visual/components/snackbar.dart'; import 'package:twonly/src/visual/components/verification_badge.comp.dart'; import 'package:twonly/src/visual/elements/better_list_title.element.dart'; import 'package:twonly/src/visual/views/contact/contact_components/restore_flame.comp.dart'; @@ -102,12 +103,7 @@ class _ContactViewState extends State { if (!mounted) return; if (!delete) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.lang.deleteUserErrorMessage), - duration: const Duration(seconds: 8), - ), - ); + showSnackbar(context, context.lang.deleteUserErrorMessage); return; } @@ -157,11 +153,10 @@ class _ContactViewState extends State { final res = await apiService.reportUser(contact.userId, reason); if (!mounted) return; if (res.isSuccess) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.lang.userGotReported), - duration: const Duration(seconds: 3), - ), + showSnackbar( + context, + context.lang.userGotReported, + level: SnackbarLevel.info, ); } else { showNetworkIssue(context); diff --git a/lib/src/visual/views/groups/group.view.dart b/lib/src/visual/views/groups/group.view.dart index 228a23a3..8aef9679 100644 --- a/lib/src/visual/views/groups/group.view.dart +++ b/lib/src/visual/views/groups/group.view.dart @@ -7,12 +7,13 @@ import 'package:twonly/locator.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/tables/groups.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/services/group.services.dart'; +import 'package:twonly/src/services/group.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/alert.dialog.dart'; import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; import 'package:twonly/src/visual/components/flame_counter.comp.dart'; import 'package:twonly/src/visual/components/select_chat_deletion_time.comp.dart'; +import 'package:twonly/src/visual/components/snackbar.dart'; import 'package:twonly/src/visual/components/verification_badge.comp.dart'; import 'package:twonly/src/visual/elements/better_list_title.element.dart'; import 'package:twonly/src/visual/views/contact/contact.view.dart'; @@ -343,10 +344,8 @@ Future showGroupNameChangeDialog( } void showNetworkIssue(BuildContext context) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.lang.groupNetworkIssue), - duration: const Duration(seconds: 3), - ), + showSnackbar( + context, + context.lang.groupNetworkIssue, ); } diff --git a/lib/src/visual/views/groups/group_create_select_group_name.view.dart b/lib/src/visual/views/groups/group_create_select_group_name.view.dart index 310da8c8..974e6fc8 100644 --- a/lib/src/visual/views/groups/group_create_select_group_name.view.dart +++ b/lib/src/visual/views/groups/group_create_select_group_name.view.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/services/group.services.dart'; +import 'package:twonly/src/services/group.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; import 'package:twonly/src/visual/components/flame_counter.comp.dart'; diff --git a/lib/src/visual/views/groups/group_create_select_members.view.dart b/lib/src/visual/views/groups/group_create_select_members.view.dart index 1abc5c71..5c6a0d0d 100644 --- a/lib/src/visual/views/groups/group_create_select_members.view.dart +++ b/lib/src/visual/views/groups/group_create_select_members.view.dart @@ -11,6 +11,7 @@ import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; import 'package:twonly/src/visual/components/flame_counter.comp.dart'; +import 'package:twonly/src/visual/components/snackbar.dart'; import 'package:twonly/src/visual/context_menu/user.context_menu.dart'; import 'package:twonly/src/visual/decorations/input_text.decoration.dart'; import 'package:twonly/src/visual/views/groups/group_create_select_group_name.view.dart'; @@ -88,12 +89,7 @@ class _StartNewChatView extends State { if (alreadyInGroup.contains(userId)) return; if (!selectedUsers.contains(userId)) { if (selectedUsers.length + alreadyInGroup.length > 256) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.lang.groupSizeLimitError(256)), - duration: const Duration(seconds: 3), - ), - ); + showSnackbar(context, context.lang.groupSizeLimitError(256)); return; } selectedUsers.add(userId); diff --git a/lib/src/visual/views/groups/group_member.context.dart b/lib/src/visual/views/groups/group_member.context.dart index 7905871f..a8a820d3 100644 --- a/lib/src/visual/views/groups/group_member.context.dart +++ b/lib/src/visual/views/groups/group_member.context.dart @@ -9,9 +9,10 @@ import 'package:twonly/src/database/tables/groups.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/services/api/messages.api.dart'; -import 'package:twonly/src/services/group.services.dart'; +import 'package:twonly/src/services/group.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/alert.dialog.dart'; +import 'package:twonly/src/visual/components/snackbar.dart'; import 'package:twonly/src/visual/context_menu/context_menu.helper.dart'; import 'package:twonly/src/visual/views/groups/group.view.dart'; @@ -107,11 +108,10 @@ class GroupMemberContextMenu extends StatelessWidget { ), ); if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.lang.contactRequestSend), - duration: const Duration(seconds: 3), - ), + showSnackbar( + context, + context.lang.contactRequestSend, + level: SnackbarLevel.success, ); } } diff --git a/lib/src/visual/views/home.view.dart b/lib/src/visual/views/home.view.dart index 73a6fbf1..e5afc67b 100644 --- a/lib/src/visual/views/home.view.dart +++ b/lib/src/visual/views/home.view.dart @@ -1,7 +1,9 @@ import 'dart:async'; import 'package:app_links/app_links.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_sharing_intent/model/sharing_file.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/locator.dart'; @@ -39,8 +41,11 @@ class HomeViewState extends State { final MainCameraController _mainCameraController = MainCameraController(); final PageController _homeViewPageController = PageController(initialPage: 1); - late StreamSubscription> _intentStreamSub; - late StreamSubscription _deepLinkSub; + StreamSubscription>? _intentStreamSub; + StreamSubscription? _deepLinkSub; + StreamSubscription? _onMessageOpenedAppSub; + StreamSubscription? _homeViewPageIndexSub; + StreamSubscription? _selectNotificationSub; static final streamHomeViewPageIndex = StreamController.broadcast(); @@ -51,14 +56,16 @@ class HomeViewState extends State { if (mounted) setState(() {}); }; - streamHomeViewPageIndex.stream.listen((index) { + _homeViewPageIndexSub = streamHomeViewPageIndex.stream.listen((index) { _homeViewPageController.jumpToPage(index); setState(() { _activePageIdx = index; }); }); - selectNotificationStream.stream.listen((response) async { + _selectNotificationSub = selectNotificationStream.stream.listen(( + response, + ) async { if (response.payload != null && response.payload!.startsWith(Routes.chats) && response.payload! != Routes.chats) { @@ -67,6 +74,13 @@ class HomeViewState extends State { streamHomeViewPageIndex.add(0); }); + _onMessageOpenedAppSub = FirebaseMessaging.onMessageOpenedApp.listen(( + message, + ) { + Log.info('Opened app from iOS/Remote push notification tap.'); + streamHomeViewPageIndex.add(0); + }); + unawaited(_mainCameraController.selectCamera(0, true)); unawaited(_initAsync()); @@ -99,10 +113,23 @@ class HomeViewState extends State { final notificationAppLaunchDetails = await flutterLocalNotificationsPlugin .getNotificationAppLaunchDetails(); + RemoteMessage? initialRemoteMessage; + try { + initialRemoteMessage = await FirebaseMessaging.instance + .getInitialMessage(); + } catch (e) { + Log.error('Could not get initial Firebase message: $e'); + } + if (widget.initialPage == 0 || + initialRemoteMessage != null || (notificationAppLaunchDetails != null && notificationAppLaunchDetails.didNotificationLaunchApp)) { - if (notificationAppLaunchDetails?.didNotificationLaunchApp ?? false) { + if (initialRemoteMessage != null) { + Log.info('App launched from iOS/Remote push notification tap.'); + streamHomeViewPageIndex.add(0); + } else if (notificationAppLaunchDetails?.didNotificationLaunchApp ?? + false) { final payload = notificationAppLaunchDetails?.notificationResponse?.payload; if (payload != null && @@ -134,12 +161,13 @@ class HomeViewState extends State { @override void dispose() { - selectNotificationStream.close(); - streamHomeViewPageIndex.close(); + _onMessageOpenedAppSub?.cancel(); + _homeViewPageIndexSub?.cancel(); + _selectNotificationSub?.cancel(); _disableCameraTimer?.cancel(); _mainCameraController.closeCamera(); - _intentStreamSub.cancel(); - _deepLinkSub.cancel(); + _intentStreamSub?.cancel(); + _deepLinkSub?.cancel(); super.dispose(); } diff --git a/lib/src/visual/views/onboarding/recover.view.dart b/lib/src/visual/views/onboarding/recover.view.dart index 6599fe45..7f13ecb2 100644 --- a/lib/src/visual/views/onboarding/recover.view.dart +++ b/lib/src/visual/views/onboarding/recover.view.dart @@ -1,13 +1,11 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:restart_app/restart_app.dart'; -import 'package:twonly/src/model/json/userdata.model.dart'; -import 'package:twonly/src/services/backup/restore.backup.dart'; -import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/services/backup.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/alert.dialog.dart'; +import 'package:twonly/src/visual/components/snackbar.dart'; import 'package:twonly/src/visual/decorations/input_text.decoration.dart'; -import 'package:twonly/src/visual/views/settings/backup/backup_server.view.dart'; class BackupRecoveryView extends StatefulWidget { const BackupRecoveryView({super.key}); @@ -19,7 +17,6 @@ class BackupRecoveryView extends StatefulWidget { class _BackupRecoveryViewState extends State { bool obscureText = true; bool isLoading = false; - BackupServer? backupServer; final TextEditingController usernameCtrl = TextEditingController(); final TextEditingController passwordCtrl = TextEditingController(); @@ -28,31 +25,38 @@ class _BackupRecoveryViewState extends State { isLoading = true; }); - try { - await recoverBackup( - usernameCtrl.text, - passwordCtrl.text, - backupServer, - ); + final error = await BackupService.startFullBackupRecovery( + usernameCtrl.text, + passwordCtrl.text, + ); + if (!mounted) return; - await Restart.restartApp( - notificationTitle: 'Backup successfully recovered.', - notificationBody: 'Click here to open the app again', - forceKill: true, - ); - } catch (e) { - // in case something was already written from the backup... - Log.error('$e'); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('$e'), - duration: const Duration(seconds: 3), - ), - ); + if (error != null) { + String errorMessage; + switch (error) { + case RecoveryError.noInternet: + errorMessage = context.lang.recoverErrorNoInternet; + case RecoveryError.usernameNotValid: + errorMessage = context.lang.recoverErrorUsernameNotValid; + case RecoveryError.passwordInvalid: + errorMessage = context.lang.recoverErrorPasswordInvalid; + case RecoveryError.tryAgainLater: + errorMessage = context.lang.recoverErrorTryAgainLater; + case RecoveryError.unkownError: + errorMessage = context.lang.recoverErrorUnknown; } + setState(() { + isLoading = false; + }); + return showSnackbar(context, errorMessage); } + await Restart.restartApp( + notificationTitle: context.lang.recoverSuccessTitle, + notificationBody: context.lang.recoverSuccessBody, + forceKill: true, + ); + setState(() { isLoading = false; }); @@ -135,20 +139,6 @@ class _BackupRecoveryViewState extends State { ), ], ), - const SizedBox(height: 30), - Center( - child: OutlinedButton( - onPressed: () async { - backupServer = - await context.navPush( - const BackupServerView(), - ) - as BackupServer?; - setState(() {}); - }, - child: Text(context.lang.backupExpertSettings), - ), - ), const SizedBox(height: 10), Center( child: FilledButton.icon( diff --git a/lib/src/visual/views/onboarding/register.view.dart b/lib/src/visual/views/onboarding/register.view.dart index 01deebc8..1a02b505 100644 --- a/lib/src/visual/views/onboarding/register.view.dart +++ b/lib/src/visual/views/onboarding/register.view.dart @@ -1,22 +1,19 @@ // ignore_for_file: avoid_dynamic_calls import 'dart:async'; -import 'dart:convert'; - import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:go_router/go_router.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/constants/routes.keys.dart'; -import 'package:twonly/src/constants/secure_storage.keys.dart'; import 'package:twonly/src/model/json/userdata.model.dart'; import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; import 'package:twonly/src/services/signal/identity.signal.dart'; +import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/pow.dart'; -import 'package:twonly/src/utils/secure_storage.dart'; import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/visual/components/alert.dialog.dart'; import 'package:twonly/src/visual/views/groups/group.view.dart'; @@ -141,12 +138,7 @@ class _RegisterViewState extends State { currentSetupPage: SetupPages.profile.name, )..appVersion = AppState.latestAppVersionId; - await SecureStorage.instance.write( - key: SecureStorageKeys.userData, - value: jsonEncode(userData), - ); - - await userService.tryInit(); + await UserService.save(userData); await apiService.authenticate(); widget.callbackOnSuccess(); diff --git a/lib/src/visual/views/onboarding/setup/backup.setup.dart b/lib/src/visual/views/onboarding/setup/backup.setup.dart index 659c0357..aeff4d5c 100644 --- a/lib/src/visual/views/onboarding/setup/backup.setup.dart +++ b/lib/src/visual/views/onboarding/setup/backup.setup.dart @@ -1,9 +1,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:go_router/go_router.dart'; -import 'package:twonly/src/constants/routes.keys.dart'; -import 'package:twonly/src/services/backup/common.backup.dart'; +import 'package:twonly/src/services/backup.service.dart'; import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/alert.dialog.dart'; @@ -19,16 +17,16 @@ class BackupSetupPage extends StatefulWidget { } class _BackupSetupPageState extends State { - bool isLoading = false; - final TextEditingController passwordCtrl = TextEditingController(); - final TextEditingController repeatedPasswordCtrl = TextEditingController(); + bool _isLoading = false; + final TextEditingController _passwordCtrl = TextEditingController(); + final TextEditingController _repeatedPasswordCtrl = TextEditingController(); Future onPressedEnableTwonlySafe() async { setState(() { - isLoading = true; + _isLoading = true; }); - if (!await isSecurePassword(passwordCtrl.text)) { + if (!await isSecurePassword(_passwordCtrl.text)) { if (!mounted) return true; final ignore = await showAlertDialog( context, @@ -40,14 +38,14 @@ class _BackupSetupPageState extends State { if (!mounted) return true; if (ignore) { setState(() { - isLoading = false; + _isLoading = false; }); return true; } } await Future.delayed(const Duration(milliseconds: 100)); - await enableTwonlySafe(passwordCtrl.text); + await BackupService.updateBackupPassword(_passwordCtrl.text); await UserService.update((user) { user.currentSetupPage = SetupPages.backup.next()?.name; @@ -55,25 +53,25 @@ class _BackupSetupPageState extends State { if (!mounted) return true; setState(() { - isLoading = false; + _isLoading = false; }); return false; } @override void dispose() { - passwordCtrl.dispose(); - repeatedPasswordCtrl.dispose(); + _passwordCtrl.dispose(); + _repeatedPasswordCtrl.dispose(); super.dispose(); } @override Widget build(BuildContext context) { - final isPasswordValid = passwordCtrl.text.length >= 10; + final isPasswordValid = _passwordCtrl.text.length >= 10; final isRepeatedPasswordValid = - passwordCtrl.text == repeatedPasswordCtrl.text; + _passwordCtrl.text == _repeatedPasswordCtrl.text; final canSubmit = - !isLoading && + !_isLoading && (isPasswordValid && isRepeatedPasswordValid || !kReleaseMode); return Column( @@ -95,24 +93,24 @@ class _BackupSetupPageState extends State { ), const SizedBox(height: 32), BackupPasswordTextField( - controller: passwordCtrl, + controller: _passwordCtrl, labelText: context.lang.password, onChanged: (_) => setState(() {}), ), PasswordRequirementText( text: context.lang.backupPasswordRequirement, - showError: passwordCtrl.text.isNotEmpty && !isPasswordValid, + showError: _passwordCtrl.text.isNotEmpty && !isPasswordValid, ), const SizedBox(height: 8), BackupPasswordTextField( - controller: repeatedPasswordCtrl, + controller: _repeatedPasswordCtrl, labelText: context.lang.passwordRepeated, onChanged: (_) => setState(() {}), ), PasswordRequirementText( text: context.lang.passwordRepeatedNotEqual, showError: - repeatedPasswordCtrl.text.isNotEmpty && !isRepeatedPasswordValid, + _repeatedPasswordCtrl.text.isNotEmpty && !isRepeatedPasswordValid, ), const SizedBox(height: 10), Row( @@ -131,16 +129,9 @@ class _BackupSetupPageState extends State { ), ], ), - const SizedBox(height: 20), - Center( - child: TextButton( - onPressed: () => context.push(Routes.settingsBackupServer), - child: Text(context.lang.backupExpertSettings), - ), - ), const SizedBox(height: 40), NextButtonComp( - isLoading: isLoading, + isLoading: _isLoading, canSubmit: canSubmit, onPressed: onPressedEnableTwonlySafe, ), diff --git a/lib/src/visual/views/public_profile.view.dart b/lib/src/visual/views/public_profile.view.dart index fa39166d..693be045 100644 --- a/lib/src/visual/views/public_profile.view.dart +++ b/lib/src/visual/views/public_profile.view.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:typed_data'; @@ -12,7 +13,9 @@ import 'package:twonly/src/services/signal/identity.signal.dart'; import 'package:twonly/src/utils/avatars.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/qr.utils.dart'; +import 'package:twonly/src/visual/components/notification_badge.comp.dart'; import 'package:twonly/src/visual/elements/better_list_title.element.dart'; +import 'package:twonly/src/visual/themes/light.dart'; class PublicProfileView extends StatefulWidget { const PublicProfileView({super.key}); @@ -25,6 +28,8 @@ class _PublicProfileViewState extends State { String? _qrCode; Uint8List? _userAvatar; Uint8List? _publicKey; + int _countContactRequest = 0; + late StreamSubscription _countContactRequestStream; @override void initState() { @@ -37,12 +42,72 @@ class _PublicProfileViewState extends State { _userAvatar = await getUserAvatar(); _publicKey = await getUserPublicKey(); if (mounted) setState(() {}); + + _countContactRequestStream = twonlyDB.contactsDao + .watchContactsRequestedCount() + .listen((update) { + if (update != null) { + if (!mounted) return; + setState(() { + _countContactRequest = update; + }); + } + }); + } + + @override + void dispose() { + _countContactRequestStream.cancel(); + super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(), + appBar: AppBar( + actions: [ + Stack( + children: (_countContactRequest == 0) + ? [] + : [ + Positioned.fill( + child: Center( + child: Container( + width: 40, + height: 40, + decoration: const BoxDecoration( + color: primaryColor, + shape: BoxShape.circle, + ), + ), + ), + ), + Center( + child: NotificationBadgeComp( + backgroundColor: isDarkMode(context) + ? Colors.white + : Colors.black, + textColor: isDarkMode(context) + ? Colors.black + : Colors.white, + count: (_countContactRequest).toString(), + child: IconButton( + color: (_countContactRequest > 0) + ? Colors.black + : null, + icon: const FaIcon( + FontAwesomeIcons.userPlus, + size: 18, + ), + onPressed: () => context.push(Routes.chatsAddNewUser), + ), + ), + ), + ], + ), + const SizedBox(width: 15), + ], + ), body: Column( children: [ Container(width: double.infinity), diff --git a/lib/src/visual/views/recovery.view.dart b/lib/src/visual/views/recovery.view.dart new file mode 100644 index 00000000..0708262d --- /dev/null +++ b/lib/src/visual/views/recovery.view.dart @@ -0,0 +1,178 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:restart_app/restart_app.dart'; +import 'package:twonly/core/bridge/wrapper/key_manager.dart'; +import 'package:twonly/locator.dart'; +import 'package:twonly/src/services/backup.service.dart'; +import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/utils/storage.dart'; + +class RecoveryView extends StatefulWidget { + const RecoveryView({super.key}); + + @override + State createState() => _RecoveryViewState(); +} + +class _RecoveryViewState extends State { + bool _isLoading = false; + String? _errorMessage; + bool _showRegisterNewPrompt = false; + + Future _startRestore() async { + setState(() { + _isLoading = true; + _errorMessage = null; + }); + + final error = await BackupService.tryToReinstallTheArchive(); + if (!mounted) return; + + if (error != null) { + String msg; + switch (error) { + case RecoveryError.noInternet: + msg = context.lang.recoverErrorNoInternet; + case RecoveryError.usernameNotValid: + msg = context.lang.recoverErrorUsernameNotValid; + case RecoveryError.passwordInvalid: + msg = context.lang.recoverErrorPasswordInvalid; + case RecoveryError.tryAgainLater: + msg = context.lang.recoverErrorTryAgainLater; + case RecoveryError.unkownError: + msg = context.lang.recoverErrorUnknown; + } + setState(() { + _isLoading = false; + _errorMessage = msg; + _showRegisterNewPrompt = true; + }); + return; + } + + final userExists = await userService.tryInit(); + if (userExists && mounted) { + await Restart.restartApp( + notificationTitle: context.lang.recoverSuccessTitle, + notificationBody: context.lang.recoverSuccessBody, + forceKill: true, + ); + } else { + setState(() { + _isLoading = false; + _errorMessage = context.lang.recoverErrorUnknown; + _showRegisterNewPrompt = true; + }); + } + } + + Future _registerNewAccount() async { + try { + await RustKeyManager.removeKeyManager(); + } catch (e) { + Log.error('Could not remove KeyManager during account reset: $e'); + } + await deleteLocalUserData(); + if (!mounted) return; + await Restart.restartApp( + notificationTitle: 'twonly', + notificationBody: context.lang.registeringNewAccount, + forceKill: true, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(context.lang.twonlySafeRecoverTitle), + ), + body: Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: ListView( + children: [ + const SizedBox(height: 100), + Center( + child: FaIcon( + FontAwesomeIcons.cloudArrowDown, + size: 80, + color: context.color.primary, + ), + ), + const SizedBox(height: 24), + Text( + context.lang.iosRecoveryWelcomeBack, + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + Text( + _showRegisterNewPrompt + ? context.lang.iosRecoveryNoBackupFound( + _errorMessage ?? '', + ) + : context.lang.iosRecoveryPrompt, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 15), + ), + const SizedBox(height: 32), + if (!_showRegisterNewPrompt) ...[ + FilledButton.icon( + onPressed: _isLoading ? null : _startRestore, + icon: _isLoading + ? const SizedBox( + height: 16, + width: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.restore_rounded), + style: FilledButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 32, + vertical: 12, + ), + ), + label: Text( + context.lang.twonlySafeRecoverBtn, + style: const TextStyle(fontSize: 16), + ), + ), + const SizedBox(height: 16), + TextButton( + onPressed: _isLoading ? null : _registerNewAccount, + child: Text(context.lang.registerNewAccount), + ), + ] else ...[ + FilledButton.icon( + onPressed: _registerNewAccount, + icon: const Icon(Icons.person_add_rounded), + style: FilledButton.styleFrom( + backgroundColor: Colors.redAccent, + padding: const EdgeInsets.symmetric( + horizontal: 32, + vertical: 12, + ), + ), + label: Text( + context.lang.registerNewAccount, + style: const TextStyle(fontSize: 16), + ), + ), + const SizedBox(height: 16), + OutlinedButton.icon( + onPressed: _startRestore, + icon: const Icon(Icons.refresh_rounded), + label: Text(context.lang.tryRestoreAgain), + ), + ], + ], + ), + ), + ), + ); + } +} diff --git a/lib/src/visual/views/settings/account.view.dart b/lib/src/visual/views/settings/account.view.dart index 8f1fc07c..6dd33624 100644 --- a/lib/src/visual/views/settings/account.view.dart +++ b/lib/src/visual/views/settings/account.view.dart @@ -4,6 +4,7 @@ import 'package:twonly/locator.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/visual/components/alert.dialog.dart'; +import 'package:twonly/src/visual/components/snackbar.dart'; class AccountView extends StatelessWidget { const AccountView({super.key}); @@ -58,13 +59,9 @@ class AccountView extends StatelessWidget { final res = await apiService.deleteAccount(); if (res.isError) { if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - 'Could not delete the account. Please ensure you have a internet connection!', - ), - duration: Duration(seconds: 3), - ), + showSnackbar( + context, + 'Could not delete the account. Please ensure you have a internet connection!', ); return; } diff --git a/lib/src/visual/views/settings/backup/backup_server.view.dart b/lib/src/visual/views/settings/backup/backup_server.view.dart deleted file mode 100644 index 161ce65e..00000000 --- a/lib/src/visual/views/settings/backup/backup_server.view.dart +++ /dev/null @@ -1,182 +0,0 @@ -// ignore_for_file: parameter_assignments, avoid_dynamic_calls - -import 'dart:async'; -import 'dart:convert'; - -import 'package:flutter/material.dart'; -import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:http/http.dart' as http; -import 'package:twonly/locator.dart'; -import 'package:twonly/src/model/json/userdata.model.dart'; -import 'package:twonly/src/services/user.service.dart'; -import 'package:twonly/src/utils/log.dart'; -import 'package:twonly/src/utils/misc.dart'; - -class BackupServerView extends StatefulWidget { - const BackupServerView({super.key}); - - @override - State createState() => _BackupServerViewState(); -} - -class _BackupServerViewState extends State { - final TextEditingController _urlController = TextEditingController(); - final TextEditingController _usernameController = TextEditingController(); - final TextEditingController _passwordController = TextEditingController(); - - @override - void initState() { - super.initState(); - _urlController.text = 'https://'; - unawaited(initAsync()); - } - - Future initAsync() async { - if (userService.currentUser.backupServer != null) { - final uri = Uri.parse(userService.currentUser.backupServer!.serverUrl); - // remove user auth data - final serverUrl = Uri( - scheme: uri.scheme, - host: uri.host, - port: uri.port, - path: uri.path, - query: uri.query, - ); - _urlController.text = serverUrl.toString(); - _usernameController.text = serverUrl.userInfo.split(':')[0]; - } - setState(() {}); - } - - Future checkAndUpdateBackupServer() async { - var serverUrl = _urlController.text; - if (!serverUrl.endsWith('/')) { - serverUrl += '/'; - } - - final username = _usernameController.text; - final password = _passwordController.text; - - if (username.isNotEmpty || password.isNotEmpty) { - serverUrl = serverUrl.replaceAll('https://', ''); - serverUrl = 'https://$username@$password$serverUrl'; - } - - try { - final uri = Uri.parse('${serverUrl}config'); - final response = await http.get( - uri, - headers: { - 'User-Agent': 'twonly', - 'Accept': 'application/json', - }, - ); - if (response.statusCode == 200) { - // If the server returns a 200 OK response, parse the JSON. - final data = jsonDecode(response.body); - - final backupServer = BackupServer( - serverUrl: serverUrl, - retentionDays: data['retentionDays']! as int, - maxBackupBytes: data['maxBackupBytes']! as int, - ); - await UserService.update((user) { - user.backupServer = backupServer; - }); - if (mounted) Navigator.pop(context, backupServer); - } else { - // If the server did not return a 200 OK response, throw an exception. - throw Exception( - 'Got invalid status code ${response.statusCode} from server.', - ); - } - } catch (e) { - Log.error('$e'); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('$e'), - duration: const Duration(seconds: 3), - ), - ); - } - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('twonly Backup Server'), - ), - body: Padding( - padding: const EdgeInsets.all(40), - child: ListView( - children: [ - Text( - context.lang.backupOwnServerDesc, - textAlign: TextAlign.center, - ), - const SizedBox(height: 30), - TextField( - controller: _urlController, - onChanged: (value) { - if (value.length < 8) { - value = ''; - } - value = value.replaceAll('https://', ''); - value = value.replaceAll('http://', ''); - value = 'https://$value'; - _urlController.text = value; - setState(() {}); - }, - decoration: const InputDecoration( - labelText: 'Server URL', - border: OutlineInputBorder(), - ), - ), - const SizedBox(height: 16), - TextField( - controller: _usernameController, - decoration: const InputDecoration( - labelText: 'Username (optional)', - border: OutlineInputBorder(), - ), - ), - const SizedBox(height: 16), - TextField( - controller: _passwordController, - decoration: const InputDecoration( - labelText: 'Password (optional)', - border: OutlineInputBorder(), - ), - obscureText: true, - ), - const SizedBox(height: 20), - Center( - child: FilledButton.icon( - onPressed: (_urlController.text.length > 8) - ? checkAndUpdateBackupServer - : null, - icon: const FaIcon(FontAwesomeIcons.server), - label: Text(context.lang.backupUseOwnServer), - ), - ), - const SizedBox(height: 10), - Center( - child: OutlinedButton( - onPressed: () async { - await UserService.update((user) { - user.backupServer = null; - }); - if (context.mounted) Navigator.pop(context); - }, - child: Text(context.lang.backupResetServer), - ), - ), - ], - ), - ), - ); - } -} 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 e321debf..2622345e 100644 --- a/lib/src/visual/views/settings/backup/backup_settings.view.dart +++ b/lib/src/visual/views/settings/backup/backup_settings.view.dart @@ -1,9 +1,11 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/constants/routes.keys.dart'; -import 'package:twonly/src/model/json/userdata.model.dart'; -import 'package:twonly/src/services/backup/create.backup.dart'; +import 'package:twonly/src/model/json/backup.model.dart'; +import 'package:twonly/src/services/backup.service.dart'; import 'package:twonly/src/utils/misc.dart'; class BackupView extends StatefulWidget { @@ -13,16 +15,37 @@ class BackupView extends StatefulWidget { State createState() => _BackupViewState(); } -BackupServer _defaultBackupServer = BackupServer( - serverUrl: 'Default', - retentionDays: 180, - maxBackupBytes: 2097152, -); - class _BackupViewState extends State { bool _isLoading = false; + CurrentBackupStatus? _backupStatus; + StreamSubscription? _backupUpdateSub; - String _backupStatus(LastBackupUploadState status) { + @override + void initState() { + super.initState(); + _loadBackupStatus(); + _backupUpdateSub = BackupService.onBackupUpdated.listen((_) { + _loadBackupStatus(); + }); + } + + @override + void dispose() { + _backupUpdateSub?.cancel(); + super.dispose(); + } + + Future _loadBackupStatus() async { + setState(() => _isLoading = true); + final status = await BackupService.getData(); + if (!mounted) return; + setState(() { + _backupStatus = status; + _isLoading = false; + }); + } + + String _getBackupStatusString(LastBackupUploadState status) { switch (status) { case LastBackupUploadState.none: return context.lang.backupPending; @@ -35,21 +58,41 @@ class _BackupViewState extends State { } } + List _buildTableRows(List<(String, String)> rows) { + return rows.map((pair) { + return TableRow( + children: [ + TableCell( + child: Text(pair.$1), + ), + TableCell( + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 4, + ), + child: Text( + pair.$2, + textAlign: TextAlign.right, + ), + ), + ), + ], + ); + }).toList(); + } + @override Widget build(BuildContext context) { return StreamBuilder( stream: userService.onUserUpdated, builder: (context, _) { - final backupServer = - userService.currentUser.backupServer ?? _defaultBackupServer; return Scaffold( appBar: AppBar( title: Text(context.lang.settingsBackup), ), body: Padding( padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + child: ListView( children: [ const SizedBox(height: 8), Text( @@ -57,81 +100,80 @@ class _BackupViewState extends State { textAlign: TextAlign.center, ), const SizedBox(height: 8), - if (userService.currentUser.twonlySafeBackup != null) + if (userService.currentUser.isBackupEnabled) Column( children: [ const SizedBox(height: 32), + Center( + child: Text( + context.lang.backupIdentityHeader, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ), + const SizedBox(height: 8), Table( defaultVerticalAlignment: TableCellVerticalAlignment.middle, - children: [ - ...[ - ( - context.lang.backupServer, - (backupServer.serverUrl.contains('@')) - ? backupServer.serverUrl.split('@')[1] - : backupServer.serverUrl.replaceAll( - 'https://', - '', - ), + children: _buildTableRows([ + ( + context.lang.backupLastBackupDate, + _backupStatus?.identityLastSuccessFull != null + ? formatDateTime( + context, + _backupStatus!.identityLastSuccessFull, + ) + : '-', + ), + ( + context.lang.backupLastBackupSize, + _backupStatus?.identitySize != null + ? formatBytes(_backupStatus!.identitySize!) + : '-', + ), + ( + context.lang.backupLastBackupResult, + _getBackupStatusString( + _backupStatus?.identityState ?? + LastBackupUploadState.none, ), - ( - context.lang.backupMaxBackupSize, - formatBytes(backupServer.maxBackupBytes), + ), + ]), + ), + const SizedBox(height: 24), + Center( + child: Text( + context.lang.backupArchiveHeader, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ), + const SizedBox(height: 8), + Table( + defaultVerticalAlignment: + TableCellVerticalAlignment.middle, + children: _buildTableRows([ + ( + context.lang.backupLastBackupDate, + _backupStatus?.archiveLastSuccessFull != null + ? formatDateTime( + context, + _backupStatus!.archiveLastSuccessFull, + ) + : '-', + ), + ( + context.lang.backupLastBackupSize, + _backupStatus?.archiveSize != null + ? formatBytes(_backupStatus!.archiveSize!) + : '-', + ), + ( + context.lang.backupLastBackupResult, + _getBackupStatusString( + _backupStatus?.archiveState ?? + LastBackupUploadState.none, ), - ( - context.lang.backupStorageRetention, - '${backupServer.retentionDays} Days', - ), - ( - context.lang.backupLastBackupDate, - formatDateTime( - context, - userService - .currentUser - .twonlySafeBackup! - .lastBackupDone, - ), - ), - ( - context.lang.backupLastBackupSize, - formatBytes( - userService - .currentUser - .twonlySafeBackup! - .lastBackupSize, - ), - ), - ( - context.lang.backupLastBackupResult, - _backupStatus( - userService - .currentUser - .twonlySafeBackup! - .backupUploadState, - ), - ), - ].map((pair) { - return TableRow( - children: [ - TableCell( - child: Text(pair.$1), - ), - TableCell( - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 4, - ), - child: Text( - pair.$2, - textAlign: TextAlign.right, - ), - ), - ), - ], - ); - }), - ], + ), + ]), ), const SizedBox(height: 10), OutlinedButton( @@ -141,7 +183,7 @@ class _BackupViewState extends State { setState(() { _isLoading = true; }); - await performTwonlySafeBackup(force: true); + await BackupService.makeBackup(force: true); setState(() { _isLoading = false; }); @@ -156,7 +198,7 @@ class _BackupViewState extends State { onPressed: () => context.push(Routes.settingsBackupSetup, extra: true), child: Text( - userService.currentUser.twonlySafeBackup == null + !userService.currentUser.isBackupEnabled ? context.lang.backupEnableBackup : context.lang.backupChangePassword, ), diff --git a/lib/src/visual/views/settings/backup/backup_setup.view.dart b/lib/src/visual/views/settings/backup/backup_setup.view.dart index 52968435..4b09a0d9 100644 --- a/lib/src/visual/views/settings/backup/backup_setup.view.dart +++ b/lib/src/visual/views/settings/backup/backup_setup.view.dart @@ -1,10 +1,9 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:go_router/go_router.dart'; import 'package:twonly/locator.dart'; -import 'package:twonly/src/constants/routes.keys.dart'; -import 'package:twonly/src/services/backup/common.backup.dart'; +import 'package:twonly/src/services/backup.service.dart'; +import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/alert.dialog.dart'; import 'package:twonly/src/visual/views/settings/backup/components/backup_setup.comp.dart'; @@ -24,15 +23,16 @@ class SetupBackupView extends StatefulWidget { } class _SetupBackupViewState extends State { - bool isLoading = false; - final TextEditingController passwordCtrl = TextEditingController(); - final TextEditingController repeatedPasswordCtrl = TextEditingController(); + bool _isLoading = false; + final TextEditingController _passwordController = TextEditingController(); + final TextEditingController _repeadedController = TextEditingController(); - Future onPressedEnableTwonlySafe() async { + Future _updateBackupPassword() async { setState(() { - isLoading = true; + _isLoading = true; }); - if (!await isSecurePassword(passwordCtrl.text)) { + + if (!await isSecurePassword(_passwordController.text)) { if (!mounted) return; final ignore = await showAlertDialog( context, @@ -44,7 +44,7 @@ class _SetupBackupViewState extends State { if (ignore) { if (mounted) { setState(() { - isLoading = false; + _isLoading = false; }); } return; @@ -52,11 +52,12 @@ class _SetupBackupViewState extends State { } await Future.delayed(const Duration(milliseconds: 100)); - await enableTwonlySafe(passwordCtrl.text); + await BackupService.updateBackupPassword(_passwordController.text); + await UserService.update((u) => u.isBackupEnabled = true); if (!mounted) return; setState(() { - isLoading = false; + _isLoading = false; }); if (widget.callBack != null) { @@ -100,34 +101,27 @@ class _SetupBackupViewState extends State { ), const SizedBox(height: 30), BackupPasswordTextField( - controller: passwordCtrl, + controller: _passwordController, labelText: context.lang.password, onChanged: (value) => setState(() {}), ), PasswordRequirementText( text: context.lang.backupPasswordRequirement, showError: - passwordCtrl.text.length < 8 && - passwordCtrl.text.isNotEmpty, + _passwordController.text.length < 8 && + _passwordController.text.isNotEmpty, ), const SizedBox(height: 5), BackupPasswordTextField( - controller: repeatedPasswordCtrl, + controller: _repeadedController, labelText: context.lang.passwordRepeated, onChanged: (value) => setState(() {}), ), PasswordRequirementText( text: context.lang.passwordRepeatedNotEqual, showError: - passwordCtrl.text != repeatedPasswordCtrl.text && - repeatedPasswordCtrl.text.isNotEmpty, - ), - const SizedBox(height: 10), - Center( - child: OutlinedButton( - onPressed: () => context.push(Routes.settingsBackupServer), - child: Text(context.lang.backupExpertSettings), - ), + _passwordController.text != _repeadedController.text && + _repeadedController.text.isNotEmpty, ), const SizedBox(height: 10), Text( @@ -139,13 +133,14 @@ class _SetupBackupViewState extends State { Center( child: FilledButton.icon( onPressed: - (!isLoading && - (passwordCtrl.text == repeatedPasswordCtrl.text && - passwordCtrl.text.length >= 8 || + (!_isLoading && + (_passwordController.text == + _repeadedController.text && + _passwordController.text.length >= 8 || !kReleaseMode)) - ? onPressedEnableTwonlySafe + ? _updateBackupPassword : null, - icon: isLoading + icon: _isLoading ? const SizedBox( height: 12, width: 12, @@ -159,21 +154,6 @@ class _SetupBackupViewState extends State { ), ), ), - const SizedBox(height: 12), - GestureDetector( - onTap: () { - if (widget.callBack != null) { - widget.callBack!(); - } else { - Navigator.pop(context); - } - }, - child: Text( - context.lang.skipForNow, - textAlign: TextAlign.center, - style: const TextStyle(fontSize: 8, color: Colors.grey), - ), - ), ], ), ), diff --git a/lib/src/visual/views/settings/backup/components/missing_backup_setup.comp.dart b/lib/src/visual/views/settings/backup/components/missing_backup_setup.comp.dart new file mode 100644 index 00000000..92402b41 --- /dev/null +++ b/lib/src/visual/views/settings/backup/components/missing_backup_setup.comp.dart @@ -0,0 +1,139 @@ +import 'package:flutter/material.dart'; +import 'package:twonly/locator.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/views/settings/backup/backup_settings.view.dart'; + +class MissingBackupComp extends StatefulWidget { + const MissingBackupComp({super.key}); + + @override + State createState() => _MissingBackupCompState(); +} + +class _MissingBackupCompState extends State { + Future onTap() async { + await context.navPush(const BackupView()); + } + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: userService.onUserUpdated, + builder: (context, snapshot) { + final user = userService.currentUser; + + if (user.currentSetupPage != null || user.isBackupEnabled) { + return const SizedBox.shrink(); + } + + return Container( + margin: const EdgeInsets.all(16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24), + gradient: LinearGradient( + colors: [ + context.color.primaryContainer.withValues(alpha: 0.2), + context.color.primaryContainer.withValues(alpha: 0.1), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + border: Border.all( + color: context.color.primary.withValues(alpha: 0.15), + width: 1.5, + ), + boxShadow: [ + BoxShadow( + color: context.color.shadow.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(24), + clipBehavior: Clip.antiAlias, + child: InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + SizedBox( + width: 68, + height: 68, + child: Container( + decoration: BoxDecoration( + color: context.color.primary.withValues(alpha: 0.1), + shape: BoxShape.circle, + ), + child: Icon( + Icons.shield_rounded, + size: 32, + color: context.color.primary, + ), + ), + ), + const SizedBox(width: 20), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.lang.missingBackupCardTitle, + style: TextStyle( + fontWeight: FontWeight.w900, + fontSize: 17, + color: context.color.onSurface, + letterSpacing: -0.2, + ), + ), + const SizedBox(height: 6), + Text( + context.lang.missingBackupCardDesc, + style: TextStyle( + fontSize: 13, + color: context.color.onSurfaceVariant, + height: 1.3, + ), + ), + const SizedBox(height: 14), + FilledButton.icon( + onPressed: onTap, + icon: const Icon( + Icons.arrow_forward_rounded, + size: 18, + ), + label: Text( + context.lang.missingBackupCardAction, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + style: FilledButton.styleFrom( + backgroundColor: context.color.primary, + foregroundColor: context.color.onPrimary, + minimumSize: const Size(0, 40), + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 0, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/src/visual/views/settings/chat/chat_reactions.view.dart b/lib/src/visual/views/settings/chat/chat_reactions.view.dart index c43dabe6..45719eeb 100644 --- a/lib/src/visual/views/settings/chat/chat_reactions.view.dart +++ b/lib/src/visual/views/settings/chat/chat_reactions.view.dart @@ -3,6 +3,7 @@ import 'package:twonly/locator.dart'; import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/animate_icon.comp.dart'; +import 'package:twonly/src/visual/components/snackbar.dart'; class ChatReactionSelectionView extends StatefulWidget { const ChatReactionSelectionView({super.key}); @@ -39,12 +40,7 @@ class _ChatReactionSelectionView extends State { user.preSelectedEmojies = _selectedEmojis; }); } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.lang.settingsPreSelectedReactionsError), - duration: const Duration(seconds: 3), - ), - ); + showSnackbar(context, context.lang.settingsPreSelectedReactionsError); } } setState(() {}); diff --git a/lib/src/visual/views/settings/help/contact_us.view.dart b/lib/src/visual/views/settings/help/contact_us.view.dart index 26840839..47916631 100644 --- a/lib/src/visual/views/settings/help/contact_us.view.dart +++ b/lib/src/visual/views/settings/help/contact_us.view.dart @@ -1,5 +1,3 @@ -import 'dart:convert'; - import 'package:device_info_plus/device_info_plus.dart'; import 'package:fixnum/fixnum.dart'; import 'package:flutter/material.dart'; @@ -9,11 +7,11 @@ import 'package:http/http.dart' as http; import 'package:package_info_plus/package_info_plus.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/constants/routes.keys.dart'; -import 'package:twonly/src/constants/secure_storage.keys.dart'; import 'package:twonly/src/model/protobuf/api/http/http_requests.pb.dart'; +import 'package:twonly/src/services/api/utils.api.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/utils/secure_storage.dart'; +import 'package:twonly/src/visual/components/snackbar.dart'; import 'package:twonly/src/visual/views/settings/help/contact_us/submit_message.view.dart'; import 'package:twonly/src/visual/views/settings/help/faq.view.dart'; @@ -50,23 +48,18 @@ class _ContactUsState extends State { final uploadRequestBytes = uploadRequest.writeToBuffer(); - final apiAuthTokenRaw = await SecureStorage.instance.read( - key: SecureStorageKeys.apiAuthToken, - ); - if (apiAuthTokenRaw == null) { - Log.error('api auth token not defined.'); - return null; - } - final apiAuthToken = uint8ListToHex(base64Decode(apiAuthTokenRaw)); - final apiUrl = 'http${apiService.apiSecure}://${apiService.apiHost}/api/upload'; - final requestMultipart = http.MultipartRequest( - 'POST', - Uri.parse(apiUrl), - ); - requestMultipart.headers['x-twonly-auth-token'] = apiAuthToken; + final requestMultipart = http.MultipartRequest('POST', Uri.parse(apiUrl)); + + final headers = await getAuthenticationHeader(); + if (headers == null) { + Log.error('Auth headers are empty. Returning'); + return null; + } + + requestMultipart.headers.addAll(headers); requestMultipart.files.add( http.MultipartFile.fromBytes( @@ -123,9 +116,7 @@ class _ContactUsState extends State { } if (token == null) { if (!mounted) return null; - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Could not upload the debug log!')), - ); + showSnackbar(context, 'Could not upload the debug log!'); setState(() { isLoading = false; }); diff --git a/lib/src/visual/views/settings/help/contact_us/submit_message.view.dart b/lib/src/visual/views/settings/help/contact_us/submit_message.view.dart index 91281ba3..104cc236 100644 --- a/lib/src/visual/views/settings/help/contact_us/submit_message.view.dart +++ b/lib/src/visual/views/settings/help/contact_us/submit_message.view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/components/snackbar.dart'; class SubmitMessage extends StatefulWidget { const SubmitMessage({required this.fullMessage, super.key}); @@ -28,8 +29,10 @@ class _ContactUsState extends State { }); if (feedback.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Please enter your message.')), + showSnackbar( + context, + 'Please enter a message.', + level: SnackbarLevel.info, ); return; } @@ -49,15 +52,16 @@ class _ContactUsState extends State { }); if (response.statusCode == 200) { - // Handle successful response - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.lang.contactUsSuccess)), + showSnackbar( + context, + context.lang.contactUsSuccess, + level: SnackbarLevel.success, ); Navigator.pop(context, true); } else { - // Handle error response - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Failed to submit feedback.')), + showSnackbar( + context, + 'Failed to submit feedback.', ); } } diff --git a/lib/src/visual/views/settings/help/credits.view.dart b/lib/src/visual/views/settings/help/credits.view.dart index dbd27058..a49c4ccf 100644 --- a/lib/src/visual/views/settings/help/credits.view.dart +++ b/lib/src/visual/views/settings/help/credits.view.dart @@ -4,7 +4,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/filters/location_filter.dart'; +import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/filters/stickers.dart'; import 'package:url_launcher/url_launcher.dart'; class UrlListTitle extends StatelessWidget { diff --git a/lib/src/visual/views/settings/help/diagnostics.view.dart b/lib/src/visual/views/settings/help/diagnostics.view.dart index f664368f..44645598 100644 --- a/lib/src/visual/views/settings/help/diagnostics.view.dart +++ b/lib/src/visual/views/settings/help/diagnostics.view.dart @@ -5,6 +5,7 @@ import 'package:go_router/go_router.dart'; import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/components/snackbar.dart'; import 'package:twonly/src/visual/loader/three_rotating_dots.loader.dart'; class DiagnosticsView extends StatefulWidget { @@ -29,21 +30,9 @@ class _DiagnosticsViewState extends State { } Future _deleteDebugLog() async { - if (await deleteLogFile()) { - if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Log file deleted!'), - ), - ); - } else { - if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Log file does not exist.'), - ), - ); - } + await deleteLogFile(); + if (!mounted) return; + showSnackbar(context, 'Log file deleted!', level: SnackbarLevel.info); } @override @@ -244,8 +233,10 @@ class _LogViewerWidgetState extends State { return InkWell( onLongPress: () { Clipboard.setData(ClipboardData(text: e.line)); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Copied line')), + showSnackbar( + context, + 'Copied line', + level: SnackbarLevel.info, ); }, child: Padding( @@ -307,8 +298,9 @@ class _LogEntry { var msg = trimmed; // Try to parse leading timestamp (YYYY-MM-DD HH:MM:SS.mmmmmm) - final tsRegex = - RegExp(r'^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)?)\s+(.*)$'); + final tsRegex = RegExp( + r'^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)?)\s+(.*)$', + ); final mTs = tsRegex.firstMatch(trimmed); if (mTs != null) { try { diff --git a/lib/src/visual/views/settings/notification.view.dart b/lib/src/visual/views/settings/notification.view.dart index 4c33d9ee..2a5343f9 100644 --- a/lib/src/visual/views/settings/notification.view.dart +++ b/lib/src/visual/views/settings/notification.view.dart @@ -4,13 +4,12 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:hashlib/random.dart'; +import 'package:permission_handler/permission_handler.dart'; import 'package:twonly/locator.dart'; -import 'package:twonly/src/constants/secure_storage.keys.dart'; import 'package:twonly/src/model/protobuf/client/generated/push_notification.pb.dart'; import 'package:twonly/src/services/notifications/fcm.notifications.dart'; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/utils/secure_storage.dart'; import 'package:twonly/src/visual/components/alert.dialog.dart'; class NotificationView extends StatefulWidget { @@ -24,6 +23,22 @@ class _NotificationViewState extends State { bool _isLoadingTroubleshooting = false; bool _isLoadingReset = false; bool _troubleshootingDidRun = false; + bool? _hasNotificationPermission; + + @override + void initState() { + super.initState(); + _checkPermission(); + } + + Future _checkPermission() async { + final isGranted = await Permission.notification.isGranted; + if (mounted) { + setState(() { + _hasNotificationPermission = isGranted; + }); + } + } Future _troubleshooting() async { setState(() { @@ -32,15 +47,11 @@ class _NotificationViewState extends State { await initFCMAfterAuthenticated(force: true); - final storedToken = await SecureStorage.instance.read( - key: SecureStorageKeys.googleFcm, - ); - await setupNotificationWithUsers(force: true); if (!mounted) return; - if (storedToken == null) { + if (userService.currentUser.fcmToken == null) { final platform = Platform.isAndroid ? "Google's" : "Apple's"; await showAlertDialog( context, @@ -99,6 +110,12 @@ class _NotificationViewState extends State { ), body: ListView( children: [ + if (_hasNotificationPermission == false) + ListTile( + title: Text(context.lang.settingsNotifyPermission), + subtitle: Text(context.lang.settingsNotifyPermissionDesc), + onTap: openAppSettings, + ), ListTile( title: Text(context.lang.settingsNotifyTroubleshooting), subtitle: Text(context.lang.settingsNotifyTroubleshootingDesc), diff --git a/lib/src/visual/views/settings/profile/profile.view.dart b/lib/src/visual/views/settings/profile/profile.view.dart index 100a9811..18b6781d 100644 --- a/lib/src/visual/views/settings/profile/profile.view.dart +++ b/lib/src/visual/views/settings/profile/profile.view.dart @@ -8,10 +8,9 @@ import 'package:go_router/go_router.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; -import 'package:twonly/src/services/backup/common.backup.dart'; -import 'package:twonly/src/services/backup/create.backup.dart'; import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/components/snackbar.dart'; import 'package:twonly/src/visual/elements/better_list_title.element.dart'; import 'package:twonly/src/visual/views/groups/group.view.dart'; @@ -71,15 +70,11 @@ class _ProfileViewState extends State { if (result.error == ErrorCode.UsernameAlreadyTaken || result.error == ErrorCode.UsernameNotValid) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - result.error == ErrorCode.UsernameAlreadyTaken - ? context.lang.errorUsernameAlreadyTaken - : context.lang.errorUsernameNotValid, - ), - duration: const Duration(seconds: 3), - ), + showSnackbar( + context, + result.error == ErrorCode.UsernameAlreadyTaken + ? context.lang.errorUsernameAlreadyTaken + : context.lang.errorUsernameNotValid, ); return; } @@ -89,10 +84,6 @@ class _ProfileViewState extends State { return; } - // as the username has changes, remove the old from the server and then upload it again. - await removeTwonlySafeFromServer(); - unawaited(performTwonlySafeBackup(force: true)); - await UserService.update( (u) => u ..username = username diff --git a/lib/src/visual/views/settings/subscription/additional_users.view.dart b/lib/src/visual/views/settings/subscription/additional_users.view.dart index 14cd4353..485bb8ea 100644 --- a/lib/src/visual/views/settings/subscription/additional_users.view.dart +++ b/lib/src/visual/views/settings/subscription/additional_users.view.dart @@ -12,6 +12,7 @@ import 'package:twonly/src/providers/purchases.provider.dart'; import 'package:twonly/src/services/subscription.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/alert.dialog.dart'; +import 'package:twonly/src/visual/components/snackbar.dart'; import 'package:twonly/src/visual/views/settings/subscription/select_additional_users.view.dart'; class AdditionalUsersView extends StatefulWidget { @@ -80,24 +81,20 @@ class _AdditionalUsersViewState extends State { ); if (contact != null && mounted) { if (res.error == ErrorCode.UserIsNotInFreePlan) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.lang.additionalUserAddErrorNotInFreePlan( - getContactDisplayName(contact), - ), - ), + showSnackbar( + context, + context.lang.additionalUserAddErrorNotInFreePlan( + getContactDisplayName(contact), ), + level: SnackbarLevel.info, ); } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.lang.additionalUserAddError( - getContactDisplayName(contact), - ), - ), + showSnackbar( + context, + context.lang.additionalUserAddError( + getContactDisplayName(contact), ), + level: SnackbarLevel.info, ); } } @@ -231,14 +228,11 @@ class _AdditionalAccountState extends State { if (res.isSuccess) { widget.refresh(); } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - errorCodeToText( - context, - res.error as ErrorCode, - ), - ), + showSnackbar( + context, + errorCodeToText( + context, + res.error as ErrorCode, ), ); } diff --git a/lib/src/visual/views/shared/memory_item_slider.view.dart b/lib/src/visual/views/shared/memory_item_slider.view.dart index 1a13c11f..752dd775 100644 --- a/lib/src/visual/views/shared/memory_item_slider.view.dart +++ b/lib/src/visual/views/shared/memory_item_slider.view.dart @@ -9,6 +9,7 @@ import 'package:twonly/src/services/api/mediafiles/upload.api.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/alert.dialog.dart'; +import 'package:twonly/src/visual/components/snackbar.dart'; import 'package:twonly/src/visual/helpers/media_view_sizing.helper.dart'; import 'package:twonly/src/visual/helpers/video_player_file.helper.dart'; import 'package:twonly/src/visual/views/camera/camera_preview_components/save_to_gallery.dart'; @@ -82,13 +83,17 @@ class _MemoriesPhotoSliderViewState extends State { await saveImageToGallery(imageBytes); } if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.lang.galleryExportSuccess)), + showSnackbar( + context, + context.lang.galleryExportSuccess, + level: SnackbarLevel.success, ); } catch (e) { if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('$e')), + showSnackbar( + context, + e.toString(), + level: SnackbarLevel.success, ); } } diff --git a/lib/src/visual/views/user_study/user_study_questionnaire.view.dart b/lib/src/visual/views/user_study/user_study_questionnaire.view.dart index 180c58ab..ed86d82e 100644 --- a/lib/src/visual/views/user_study/user_study_questionnaire.view.dart +++ b/lib/src/visual/views/user_study/user_study_questionnaire.view.dart @@ -7,6 +7,7 @@ import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/services/user_study.service.dart'; import 'package:twonly/src/utils/keyvalue.dart'; import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/components/snackbar.dart'; class UserStudyQuestionnaireView extends StatefulWidget { const UserStudyQuestionnaireView({super.key}); @@ -60,10 +61,11 @@ class _UserStudyQuestionnaireViewState await handleUserStudyUpload(); if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Vielen Dank für deine Teilnahme!')), + showSnackbar( + context, + 'Vielen Dank für deine Teilnahme!', + level: SnackbarLevel.success, ); - context.pop(); } diff --git a/metadata/de-DE/images/phoneScreenshots/01_share_moments.png b/metadata/de-DE/images/phoneScreenshots/01_share_moments.png new file mode 100644 index 00000000..3863adb6 Binary files /dev/null and b/metadata/de-DE/images/phoneScreenshots/01_share_moments.png differ diff --git a/metadata/de-DE/images/phoneScreenshots/02_chat_list.png b/metadata/de-DE/images/phoneScreenshots/02_chat_list.png new file mode 100644 index 00000000..408a1d58 Binary files /dev/null and b/metadata/de-DE/images/phoneScreenshots/02_chat_list.png differ diff --git a/metadata/de-DE/images/phoneScreenshots/03_groups.png b/metadata/de-DE/images/phoneScreenshots/03_groups.png new file mode 100644 index 00000000..3c0005ff Binary files /dev/null and b/metadata/de-DE/images/phoneScreenshots/03_groups.png differ diff --git a/metadata/de-DE/images/phoneScreenshots/1.png b/metadata/de-DE/images/phoneScreenshots/1.png deleted file mode 100644 index a8a40b0b..00000000 Binary files a/metadata/de-DE/images/phoneScreenshots/1.png and /dev/null differ diff --git a/metadata/de-DE/images/phoneScreenshots/2.png b/metadata/de-DE/images/phoneScreenshots/2.png deleted file mode 100644 index 86358641..00000000 Binary files a/metadata/de-DE/images/phoneScreenshots/2.png and /dev/null differ diff --git a/metadata/en-US/images/phoneScreenshots/01_share_moments.png b/metadata/en-US/images/phoneScreenshots/01_share_moments.png new file mode 100644 index 00000000..367bd821 Binary files /dev/null and b/metadata/en-US/images/phoneScreenshots/01_share_moments.png differ diff --git a/metadata/en-US/images/phoneScreenshots/02_chat_list.png b/metadata/en-US/images/phoneScreenshots/02_chat_list.png new file mode 100644 index 00000000..c6b27c57 Binary files /dev/null and b/metadata/en-US/images/phoneScreenshots/02_chat_list.png differ diff --git a/metadata/en-US/images/phoneScreenshots/03_groups.png b/metadata/en-US/images/phoneScreenshots/03_groups.png new file mode 100644 index 00000000..00823f0f Binary files /dev/null and b/metadata/en-US/images/phoneScreenshots/03_groups.png differ diff --git a/metadata/en-US/images/phoneScreenshots/1.png b/metadata/en-US/images/phoneScreenshots/1.png deleted file mode 100644 index ce0672f5..00000000 Binary files a/metadata/en-US/images/phoneScreenshots/1.png and /dev/null differ diff --git a/metadata/en-US/images/phoneScreenshots/2.png b/metadata/en-US/images/phoneScreenshots/2.png deleted file mode 100644 index 18455ec8..00000000 Binary files a/metadata/en-US/images/phoneScreenshots/2.png and /dev/null differ diff --git a/pubspec.lock b/pubspec.lock index fb45dac6..8b6957f8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -443,8 +443,8 @@ packages: dependency: "direct main" description: path: "." - ref: HEAD - resolved-ref: c5bffd3414c1e640389b41165b831df7df1cf517 + ref: "23a0595f7dde50728afce917c1c58f284ccbb495" + resolved-ref: "23a0595f7dde50728afce917c1c58f284ccbb495" url: "https://github.com/otsmr/emoji_picker_flutter.git" source: git version: "4.4.0" diff --git a/pubspec.yaml b/pubspec.yaml index 61a7d45b..9a41298b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec publish_to: 'none' -version: 0.2.10+119 +version: 0.2.11+120 environment: sdk: ^3.11.0 @@ -167,6 +167,7 @@ dependency_overrides: # Using override until this gets merged. git: url: https://github.com/otsmr/emoji_picker_flutter.git + ref: 23a0595f7dde50728afce917c1c58f284ccbb495 flutter_android_volume_keydown: git: url: https://github.com/yenchieh/flutter_android_volume_keydown.git diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 0fccb096..bbf942dc 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -17,6 +17,41 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common 0.1.7", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher 0.4.4", + "cpufeatures 0.2.17", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher 0.4.4", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -43,6 +78,24 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "android-native-keyring-store" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c6349ddff23194f8fdce2ea8849380f5a4868c1648965b70e801e104cba9b3" +dependencies = [ + "base64", + "jni", + "keyring-core", + "log", + "ndk-context", + "regex", + "serde", + "serde_json", + "thiserror 2.0.18", + "tracing", +] + [[package]] name = "android_log-sys" version = "0.3.2" @@ -75,6 +128,26 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "apple-native-keyring-store" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7be2f067ccd8d4b4d4a66ddafe0f32a5dff31732f32dbff85fefc40929b1f72" +dependencies = [ + "keyring-core", + "log", + "security-framework", +] + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "atoi" version = "2.0.0" @@ -90,6 +163,15 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + [[package]] name = "autocfg" version = "1.5.0" @@ -201,6 +283,12 @@ dependencies = [ "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cfg-if" version = "1.0.4" @@ -225,16 +313,59 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", + "js-sys", "num-traits", + "serde", + "wasm-bindgen", "windows-link", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.7", + "inout 0.1.4", +] + +[[package]] +name = "cipher" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34d8227fe1ba289043aeb13792056ff80fd6de1a9f49137a5f499de8e8c78ea" +dependencies = [ + "block-buffer 0.12.0", + "crypto-common 0.2.1", + "inout 0.2.2", +] + [[package]] name = "cmov" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746" +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.18", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -266,6 +397,16 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -305,6 +446,21 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + [[package]] name = "crossbeam-channel" version = "0.5.15" @@ -336,6 +492,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] @@ -348,6 +505,15 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher 0.4.4", +] + [[package]] name = "ctutils" version = "0.4.2" @@ -410,6 +576,17 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "digest" version = "0.10.7" @@ -460,6 +637,18 @@ dependencies = [ "serde", ] +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + [[package]] name = "env_filter" version = "0.1.4" @@ -539,6 +728,16 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "flume" version = "0.11.1" @@ -747,12 +946,31 @@ dependencies = [ "wasip3", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "gimli" version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -796,6 +1014,20 @@ dependencies = [ "hashbrown 0.15.5", ] +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "serde", + "spin", + "stable_deref_trait", +] + [[package]] name = "heck" version = "0.5.0" @@ -1010,6 +1242,24 @@ dependencies = [ "serde_core", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "hybrid-array", +] + [[package]] name = "is-terminal" version = "0.4.17" @@ -1036,6 +1286,50 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "js-sys" version = "0.3.95" @@ -1048,6 +1342,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "keyring-core" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb1e621458ca9c51aa110bd0339d4751a056b9576bf1253aee1aa560dda0fc9d" +dependencies = [ + "log", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1089,11 +1392,12 @@ dependencies = [ [[package]] name = "libsqlite3-sys" -version = "0.30.1" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f" dependencies = [ "cc", + "openssl-sys", "pkg-config", "vcpkg", ] @@ -1157,6 +1461,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] @@ -1176,6 +1481,12 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -1262,6 +1573,34 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl-src" +version = "300.5.0+3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8ce546f549326b0e6052b649198487d91320875da901e7bd11a06d1ee3f9c2f" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.115" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "158fe5b292746440aa6e7a7e690e55aeb72d41505e2804c23c6973ad0e9c9781" +dependencies = [ + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + [[package]] name = "oslog" version = "0.2.0" @@ -1308,6 +1647,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pbkdf2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629" +dependencies = [ + "digest 0.11.2", + "hmac 0.13.0", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -1373,12 +1722,37 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + [[package]] name = "portable-atomic" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "heapless", + "serde", +] + [[package]] name = "potential_utf" version = "0.1.5" @@ -1497,7 +1871,7 @@ dependencies = [ "serde_json", "sha2 0.11.0", "sqlx", - "thiserror", + "thiserror 2.0.18", "tokio", "tracing", ] @@ -1635,19 +2009,34 @@ dependencies = [ name = "rust_lib_twonly" version = "0.1.0" dependencies = [ + "aes-gcm", + "android-native-keyring-store", + "apple-native-keyring-store", + "chrono", "flutter_rust_bridge", + "hex", + "hkdf", + "keyring-core", + "libsqlite3-sys", "paste", + "postcard", "pretty_env_logger", "prost-build", "protocols", "rand 0.10.1", + "scrypt", + "serde", + "sha2 0.10.9", "sqlx", "tempfile", - "thiserror", + "thiserror 2.0.18", "tokio", "tracing", "tracing-appender", "tracing-subscriber", + "walkdir", + "zeroize", + "zip", ] [[package]] @@ -1656,6 +2045,15 @@ version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "1.1.4" @@ -1681,12 +2079,66 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "salsa20" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f874456e72520ff1375a06c588eaf074b0f01f9e9e1aada45bd9b7954a6e42c" +dependencies = [ + "cfg-if", + "cipher 0.5.1", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "scrypt" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87af57419b594aa23fa95f09f0e06d80d84ba01c26148c43844cad6ff4485f0" +dependencies = [ + "cfg-if", + "pbkdf2", + "salsa20", + "sha2 0.11.0", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.28" @@ -1816,6 +2268,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + [[package]] name = "slab" version = "0.4.12" @@ -1901,7 +2359,7 @@ dependencies = [ "serde_json", "sha2 0.10.9", "smallvec", - "thiserror", + "thiserror 2.0.18", "tokio", "tokio-stream", "tracing", @@ -1942,7 +2400,7 @@ dependencies = [ "sqlx-postgres", "sqlx-sqlite", "syn", - "thiserror", + "thiserror 2.0.18", "tokio", "url", ] @@ -1984,7 +2442,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 2.0.18", "tracing", "whoami", ] @@ -2021,7 +2479,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 2.0.18", "tracing", "whoami", ] @@ -2046,7 +2504,7 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", - "thiserror", + "thiserror 2.0.18", "tracing", "url", ] @@ -2124,13 +2582,33 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -2277,7 +2755,7 @@ checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c" dependencies = [ "crossbeam-channel", "symlink", - "thiserror", + "thiserror 2.0.18", "time", "tracing-subscriber", ] @@ -2371,6 +2849,16 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common 0.1.7", + "subtle", +] + [[package]] name = "url" version = "2.5.8" @@ -2407,6 +2895,16 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -2614,13 +3112,22 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -2632,34 +3139,67 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -2672,24 +3212,48 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -2913,8 +3477,37 @@ dependencies = [ "syn", ] +[[package]] +name = "zip" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" +dependencies = [ + "arbitrary", + "crc32fast", + "crossbeam-utils", + "displaydoc", + "flate2", + "indexmap", + "memchr", + "thiserror 2.0.18", + "zopfli", +] + [[package]] name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 47ab6cf6..26ca7546 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -18,13 +18,46 @@ sqlx = { version = "0.9.0-alpha.1", default-features = false, features = [ "derive", "json", ] } +# mdk-core = { version = "0.8.0", git = "https://github.com/marmot-protocol/mdk", rev = "7f809f8549458a0d7f7d885bcdd694023abf299c", features = [ +# "mip04", +# "mip05", +# ] } +# mdk-sqlite-storage = { version = "0.8.0", git = "https://github.com/marmot-protocol/mdk", rev = "7f809f8549458a0d7f7d885bcdd694023abf299c" } +# mdk-storage-traits = { version = "0.8.0", git = "https://github.com/marmot-protocol/mdk", rev = "7f809f8549458a0d7f7d885bcdd694023abf299c" } +# nostr-sdk = { version = "0.44", features = [ +# "nip04", +# "nip44", +# "nip47", +# "nip59", +# ] } +libsqlite3-sys = { version = "0.35.0", features = [ + "bundled-sqlcipher-vendored-openssl", +] } tokio = { version = "1.44", features = ["full"] } tracing = "0.1.44" rand = "0.10.1" protocols = { path = "../rust_dependencies/protocols" } +hkdf = "0.12.4" +sha2 = "0.10.8" +aes-gcm = "0.10.3" tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-appender = "0.2.5" paste = "1.0.15" +serde = { version = "1.0", features = ["derive"] } +zeroize = { version = "1.8", features = ["derive"] } +hex = "0.4.3" +keyring-core = "1" +postcard = { version = "1.0", features = ["alloc"] } +chrono = { version = "0.4", features = ["serde"] } +zip = { version = "2.2.2", default-features = false, features = ["deflate"] } +scrypt = { version = "0.12", default-features = false } +walkdir = "2.5.0" +[target.'cfg(target_os = "ios")'.dependencies] +# iOS backend: Requires the 'protected' feature for Data Protection Keychain +apple-native-keyring-store = { version = "1", features = ["protected"] } +[target.'cfg(target_os = "android")'.dependencies] +# Android backend: Interfaces with the Android Keystore +android-native-keyring-store = "1" [dev-dependencies] pretty_env_logger = "0.5.0" diff --git a/rust/src/backup/backup_archive.rs b/rust/src/backup/backup_archive.rs new file mode 100644 index 00000000..bcbd73a4 --- /dev/null +++ b/rust/src/backup/backup_archive.rs @@ -0,0 +1,260 @@ +use crate::context::Context; +use crate::database::Database; +use crate::error::Result; +use crate::keys::{DatabaseKey, KeyManager}; +use std::fs::{remove_file, File}; +use std::io::{copy, Cursor}; +use std::path::{Path, PathBuf}; +use walkdir::WalkDir; +use zeroize::Zeroize; +use zip::write::SimpleFileOptions; +use zip::{CompressionMethod, ZipArchive, ZipWriter}; + +pub(crate) struct BackupArchive {} + +impl BackupArchive { + fn get_backup_files( + ctx: &Context, + keys: &KeyManager, + ) -> Result)>> { + let config = ctx.get_config()?; + let database_dir = PathBuf::from(&config.database_dir); + let data_dir = PathBuf::from(&config.data_dir); + let rust_db_key = keys.main_key.get_database_key(DatabaseKey::RustDb); + + Ok(vec![ + ("twonly.sqlite", database_dir.clone(), true, None), + ("rust_db.sqlite", database_dir, true, Some(rust_db_key)), + ("user_discovery_config.json", data_dir.clone(), false, None), + ("user.json", data_dir.join("keyvalue"), false, None), + ]) + } + + pub(crate) async fn create_backup(ctx: &Context) -> Result { + let config = ctx.get_config()?; + let data_dir = PathBuf::from(&config.data_dir); + + let backup_data_dir = data_dir.join("temp_backup_dir"); + if backup_data_dir.is_dir() { + std::fs::remove_dir_all(&backup_data_dir)?; + } + std::fs::create_dir_all(&backup_data_dir)?; + + let keys = ctx.get_key_manager().await?; + + for (file_name, source_dir, is_db, mut encryption_key) in + Self::get_backup_files(ctx, &keys)? + { + let file_path = source_dir.join(file_name); + if !file_path.exists() { + tracing::warn!( + "Could not backup {} as it does not exist.", + file_path.display() + ); + continue; + } + + if is_db { + let db = Database::new( + &file_path.display().to_string(), + encryption_key.as_deref(), + false, + ) + .await?; + let backup_database_file = backup_data_dir.join(file_name).display().to_string(); + db.create_backup(backup_database_file.as_str(), encryption_key.as_deref()) + .await?; + } else { + let file_backup = backup_data_dir.join(file_name); + std::fs::copy(file_path, file_backup)?; + } + encryption_key.zeroize(); + } + + let mut zip_data = Vec::new(); + + { + let mut zip = ZipWriter::new(Cursor::new(&mut zip_data)); + let options = + SimpleFileOptions::default().compression_method(CompressionMethod::Deflated); + + for entry in WalkDir::new(&backup_data_dir) { + let entry = entry?; + let path = entry.path(); + + if !path.is_file() { + continue; + } + + if let Ok(name) = path.strip_prefix(&backup_data_dir) { + zip.start_file(name.to_string_lossy(), options)?; + copy(&mut File::open(path)?, &mut zip)?; + } + } + zip.finish()?; + } + + let zip_path = data_dir.join("temp_backup.zip"); + std::fs::write(&zip_path, keys.main_key.encrypt_backup(&zip_data))?; + + std::fs::remove_dir_all(&backup_data_dir)?; + + Ok(zip_path) + } + + pub(crate) async fn restore_from_backup(ctx: &Context, file_path: &Path) -> Result<()> { + let data_dir = PathBuf::from(&ctx.get_config()?.data_dir); + let key_manager = ctx.get_key_manager().await?; + + let encrypted_zip = std::fs::read(file_path)?; + let zip_content = key_manager.main_key.decrypt_backup(&encrypted_zip)?; + + let restore_temp_dir = data_dir.join("restore_temp"); + + if restore_temp_dir.exists() { + std::fs::remove_dir_all(&restore_temp_dir)?; + } + + std::fs::create_dir_all(&restore_temp_dir)?; + + let mut archive = ZipArchive::new(Cursor::new(zip_content))?; + + for i in 0..archive.len() { + let mut file = archive.by_index(i)?; + + if file.is_file() { + let enclosed_name = file.enclosed_name(); + if let Some(name) = enclosed_name.as_ref().and_then(|p| p.file_name()) { + let restored_file = restore_temp_dir.join(name); + copy(&mut file, &mut File::create(&restored_file)?)?; + }; + } + } + + for (file_name, target_dir, is_db, _) in Self::get_backup_files(ctx, &key_manager)? { + let src = restore_temp_dir.join(file_name); + if src.exists() { + std::fs::create_dir_all(&target_dir)?; + let dst = target_dir.join(file_name); + if is_db { + // Remove existing database and its temporary files (WAL, SHM) + let _ = remove_file(&dst); + let _ = remove_file(target_dir.join(format!("{}-wal", file_name))); + let _ = remove_file(target_dir.join(format!("{}-shm", file_name))); + } + + std::fs::copy(src, dst)?; + } + } + + std::fs::remove_dir_all(&restore_temp_dir)?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::{ + database::tables::received_messages::ReceivedMessage, secure_storage::SecureStorage, + }; + + use super::*; + use tempfile::tempdir; + + #[tokio::test] + async fn test_backup_and_restore() { + let _ = pretty_env_logger::try_init(); + + let temp_dir = tempdir().unwrap(); + + let ctx = Context::init_for_testing( + temp_dir.path().join("database"), + temp_dir.path().join("data"), + ) + .await + .unwrap(); + + // 1. Add some data + let original_login_token = { + let secure_storage = SecureStorage::new("testing"); + let config = ctx.get_config().unwrap(); + let rust_db_path = PathBuf::from(&config.database_dir).join("rust_db.sqlite"); + let key_manager = ctx.get_key_manager().await.unwrap(); + key_manager.store_to_keychain(&secure_storage).unwrap(); + + let db = Database::new( + &rust_db_path.display().to_string(), + Some(&key_manager.main_key.get_database_key(DatabaseKey::RustDb)), + false, + ) + .await + .unwrap(); + + ReceivedMessage::insert(&db.pool, 1, b"original message") + .await + .unwrap(); + + // Add a file + let config_file = PathBuf::from(&config.data_dir).join("user_discovery_config.json"); + std::fs::write(config_file, "original config").unwrap(); + key_manager.main_key.get_login_token() + }; + + // 2. Create backup + let backup_path = BackupArchive::create_backup(&ctx).await.unwrap(); + assert!(backup_path.exists()); + + // 3. Modify data (to simulate state before restore) + { + let config = ctx.get_config().unwrap(); + let rust_db_path = PathBuf::from(&config.database_dir).join("rust_db.sqlite"); + let key_manager = ctx.get_key_manager().await.unwrap(); + let db = Database::new( + &rust_db_path.display().to_string(), + Some(&key_manager.main_key.get_database_key(DatabaseKey::RustDb)), + false, + ) + .await + .unwrap(); + + ReceivedMessage::insert(&db.pool, 2, b"new message") + .await + .unwrap(); + + let config_file = PathBuf::from(&config.data_dir).join("user_discovery_config.json"); + std::fs::write(config_file, "new config").unwrap(); + } + + // 4. Restore backup + BackupArchive::restore_from_backup(&ctx, &backup_path) + .await + .unwrap(); + + // 5. Verify restored data + { + let config = ctx.get_config().unwrap(); + let rust_db_path = PathBuf::from(&config.database_dir).join("rust_db.sqlite"); + let key_manager = ctx.get_key_manager().await.unwrap(); + let db = Database::new( + &rust_db_path.display().to_string(), + Some(&key_manager.main_key.get_database_key(DatabaseKey::RustDb)), + false, + ) + .await + .unwrap(); + + let messages = ReceivedMessage::get_all(&db.pool).await.unwrap(); + // Should only have the original message because restore overwrites + assert_eq!(messages.len(), 1); + assert_eq!(messages[0].sender_id, 1); + assert_eq!(messages[0].content, b"original message"); + + let config_file = PathBuf::from(&config.data_dir).join("user_discovery_config.json"); + let config_content = std::fs::read_to_string(config_file).unwrap(); + assert_eq!(config_content, "original config"); + + assert_eq!(key_manager.main_key.get_login_token(), original_login_token); + } + } +} diff --git a/rust/src/backup/backup_identity.rs b/rust/src/backup/backup_identity.rs new file mode 100644 index 00000000..1fa49bd7 --- /dev/null +++ b/rust/src/backup/backup_identity.rs @@ -0,0 +1,83 @@ +use crate::error::{Result, TwonlyError}; +use crate::keys::{BackupPasswordKeys, KeyManager}; +use crate::secure_storage::SecureStorage; +use aes_gcm::aead::rand_core::RngCore; +use aes_gcm::aead::{Aead, KeyInit, OsRng}; +use aes_gcm::{Aes256Gcm, Nonce}; + +pub(crate) struct BackupIdentity(); + +impl BackupIdentity { + pub(crate) fn encrypt_key_manager(key_manager: &KeyManager) -> Result> { + let Some(keys) = &key_manager.backup_password else { + return Err(TwonlyError::Generic("No backup password".into())); + }; + + let serialized_bytes = postcard::to_allocvec(key_manager)?; + + let key = aes_gcm::Key::::from_slice(&keys.encryption_key); + let cipher = Aes256Gcm::new(key); + + let mut nonce_bytes = [0u8; 12]; + OsRng.fill_bytes(&mut nonce_bytes); + let nonce = Nonce::from_slice(&nonce_bytes); + + let ciphertext = cipher.encrypt(nonce, serialized_bytes.as_slice())?; + + let mut encrypted_bytes = vec![]; + encrypted_bytes.extend_from_slice(&nonce_bytes); + encrypted_bytes.extend_from_slice(&ciphertext); + + Ok(encrypted_bytes) + } + + pub(crate) fn restore_key_manager( + secure_storage: &SecureStorage, + backup_password_keys: &BackupPasswordKeys, + encrypted_bytes: &[u8], + ) -> Result<()> { + if encrypted_bytes.len() < 12 { + return Err(TwonlyError::Generic( + "Invalid encrypted backup length".into(), + )); + } + + let (nonce_bytes, ciphertext) = encrypted_bytes.split_at(12); + let nonce = Nonce::from_slice(nonce_bytes); + + let key = aes_gcm::Key::::from_slice(&backup_password_keys.encryption_key); + let cipher = Aes256Gcm::new(key); + + let decrypted_bytes = cipher.decrypt(nonce, ciphertext)?; + + let key_manager: KeyManager = postcard::from_bytes(&decrypted_bytes)?; + + key_manager.store_to_keychain(&secure_storage)?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_backup_encryption_decryption() { + let secure_storage = SecureStorage::new("testing"); + let mut key_manager = KeyManager::generate().unwrap(); + let password = "my_secure_password"; + let salt = 10; + + let backup_keys = BackupPasswordKeys::from_password(password, salt).unwrap(); + key_manager.backup_password = Some(backup_keys.clone()); + + let encrypted = BackupIdentity::encrypt_key_manager(&key_manager).unwrap(); + + BackupIdentity::restore_key_manager(&secure_storage, &backup_keys, &encrypted).unwrap(); + + let restored = KeyManager::try_from_keychain(&secure_storage).unwrap(); + + assert_eq!(restored, key_manager); + } +} diff --git a/rust/src/backup/backup_passwordless/mod.rs b/rust/src/backup/backup_passwordless/mod.rs new file mode 100644 index 00000000..6f8b3dee --- /dev/null +++ b/rust/src/backup/backup_passwordless/mod.rs @@ -0,0 +1 @@ +mod types; diff --git a/rust/src/backup/backup_passwordless/types.rs b/rust/src/backup/backup_passwordless/types.rs new file mode 100644 index 00000000..8cba833c --- /dev/null +++ b/rust/src/backup/backup_passwordless/types.rs @@ -0,0 +1,84 @@ +#![allow(dead_code)] +use serde::{Deserialize, Serialize}; + +/// Send from the person who tries to recover their account. +/// This can be done via a link, which will then be opened in the app of the contact. +/// The contact then has to manually select from which user he got the request. +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct RecoveryRequest { + pub temp_id: i64, + pub public_key: Vec, +} + +/// Used as envelope for TrustedFriendShare and RecoveryData +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct EncryptedEnvelope { + pub encrypted_data: Vec, + pub iv: Vec, + pub mac: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct User { + pub user_id: i64, + pub display_name: String, + pub avatar: Vec, +} + +/// Send from the trusted friend. +/// This is encrypted with the received public key. +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct TrustedFriendShare { + /// This allows to display the user which user has send him his recovery data. + pub trusted_friend: User, + /// This allows to display the userdata, showing that he is recovering the correct person. + pub share_user: User, + /// The minimum threshold required to decrypt the shares. + pub threshold: i32, + /// The actual share which will become: SecretSharedData + pub share: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct SecondFactorPin { + /// Required to try the PIN to get the share from the server. + /// This prevents that someone else can lock the pin, as the server only + /// allows 3 tries then after 1 day again 3 tries until the key is deleted. + pub unlock_token: Vec, + /// This never is send to the server but used to hash the pin before sending it to the server. + /// This prevents that the server every knows the short 4-digit PIN. + pub pin_seed: Vec, + /// The recovery data in case a second factor was used + /// The decryption key is loaded from the server either using the PIN or the MAIL + pub recovery_data_encrypted: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct SecondFactorMail { + /// The users selected mail which will be send to the server + /// To this mail the encryption key for the recovery_data is send + pub mail: String, + /// Required to try the PIN to get the share from the server. + /// This prevents that someone else can lock the pin, as the server only + /// allows 3 tries then after 1 day again 3 tries until the key is deleted. + pub unlock_token: Vec, + /// The recovery data in case a second factor was used + /// The decryption key is loaded from the server either using the PIN or the MAIL + pub recovery_data_encrypted: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum SecretSharedData { + None(RecoveryData), + Mail(SecondFactorMail), + Pin(SecondFactorPin), +} + +/// The data which is recovered at the end. +/// The backup_master_key allows to recover the actual backup uploaded in the background to the server. +/// In case the backup is not available any more the user can use its user_id and his private_key to register as a new user. +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct RecoveryData { + pub user_id: i64, + pub master_key: Vec, +} diff --git a/rust/src/backup/mod.rs b/rust/src/backup/mod.rs new file mode 100644 index 00000000..512a160c --- /dev/null +++ b/rust/src/backup/mod.rs @@ -0,0 +1,3 @@ +pub(crate) mod backup_archive; +pub(crate) mod backup_identity; +pub(crate) mod backup_passwordless; diff --git a/rust/src/bridge/callbacks.rs b/rust/src/bridge/callbacks.rs index a2c81111..7db2b450 100644 --- a/rust/src/bridge/callbacks.rs +++ b/rust/src/bridge/callbacks.rs @@ -5,12 +5,10 @@ pub(crate) mod user_discovery; use flutter_rust_bridge::DartFnFuture; use protocols::user_discovery::traits::{AnnouncedUser, OtherPromotion}; -use super::error::Result; +use crate::error::{Result, TwonlyError}; use crate::{callback_generator, frb_generated::StreamSink}; use std::sync::{Arc, OnceLock}; -use crate::bridge::error::TwonlyError; - static FLUTTER_CALLBACKS: OnceLock = OnceLock::new(); // This will also generate the function init_flutter_callbacks which MUST be called from Flutter to initialize the callbacks diff --git a/rust/src/bridge/callbacks/user_discovery.rs b/rust/src/bridge/callbacks/user_discovery.rs index 1a24fb94..c9b390b7 100644 --- a/rust/src/bridge/callbacks/user_discovery.rs +++ b/rust/src/bridge/callbacks/user_discovery.rs @@ -1,6 +1,6 @@ use crate::bridge::callbacks::get_callbacks; -use crate::bridge::error::TwonlyError; use crate::bridge::get_twonly_flutter; +use crate::error::TwonlyError; use protocols::user_discovery::error::{Result, UserDiscoveryError}; use protocols::user_discovery::traits::UserDiscoveryUtils; use protocols::user_discovery::traits::{AnnouncedUser, OtherPromotion, UserDiscoveryStore}; @@ -47,8 +47,7 @@ impl UserDiscoveryUtils for UserDiscoveryUtilsFlutter { impl UserDiscoveryStore for UserDiscoveryStoreFlutter { async fn get_config(&self) -> Result { let ws = get_twonly_flutter()?; - let config_path = - PathBuf::from(&ws.config.data_directory).join("user_discovery_config.json"); + let config_path = PathBuf::from(&ws.config.data_dir).join("user_discovery_config.json"); if !config_path.is_file() { return Err(UserDiscoveryError::NotInitialized); @@ -60,8 +59,7 @@ impl UserDiscoveryStore for UserDiscoveryStoreFlutter { async fn update_config(&self, update: String) -> Result<()> { tracing::debug!("Updating configuration file."); let ws = get_twonly_flutter()?; - let config_path = - PathBuf::from(&ws.config.data_directory).join("user_discovery_config.json"); + let config_path = PathBuf::from(&ws.config.data_dir).join("user_discovery_config.json"); std::fs::write(config_path, &update)?; Ok(()) } diff --git a/rust/src/bridge/error.rs b/rust/src/bridge/error.rs deleted file mode 100644 index 7ad74628..00000000 --- a/rust/src/bridge/error.rs +++ /dev/null @@ -1,26 +0,0 @@ -use protocols::user_discovery::error::UserDiscoveryError; -use thiserror::Error; - -pub type Result = core::result::Result; - -#[derive(Error, Debug)] -pub enum TwonlyError { - #[error("global twonly is not initialized")] - Initialization, - #[error("init_flutter_callbacks was not called")] - MissingCallbackInitialization, - #[error("Could not find the given database")] - DatabaseNotFound, - #[error("{0}")] - UserDiscoveryError(#[from] UserDiscoveryError), - #[error("Error in dart callback")] - DartError, - #[error("{0}")] - SqliteError(#[from] sqlx::Error), -} - -impl From for UserDiscoveryError { - fn from(error: TwonlyError) -> Self { - UserDiscoveryError::Store(error.to_string()) - } -} diff --git a/rust/src/bridge/mod.rs b/rust/src/bridge/mod.rs index dd4a7aa0..f675cbb7 100644 --- a/rust/src/bridge/mod.rs +++ b/rust/src/bridge/mod.rs @@ -1,23 +1,30 @@ #![allow(unexpected_cfgs)] pub mod callbacks; -pub mod error; -pub mod log; pub mod wrapper; +use std::sync::Arc; + use crate::bridge::callbacks::user_discovery::{ UserDiscoveryStoreFlutter, UserDiscoveryUtilsFlutter, }; -use crate::bridge::log::init_tracing; +use crate::context::Context; +use crate::database::Database; +use crate::error::Result; +use crate::error::TwonlyError; +use crate::keys::KeyManager; +use crate::secure_storage::SecureStorage; use crate::utils::Shared; -use error::Result; -use error::TwonlyError; use flutter_rust_bridge::frb; use protocols::user_discovery::UserDiscovery; -use std::path::PathBuf; -use tokio::sync::OnceCell; pub use protocols::user_discovery::traits::AnnouncedUser; pub use protocols::user_discovery::traits::OtherPromotion; +use tokio::sync::Mutex; + +pub struct InitConfig { + pub database_dir: String, + pub data_dir: String, +} #[frb(mirror(OtherPromotion))] pub struct _OtherPromotion { @@ -36,58 +43,27 @@ pub struct _AnnouncedUser { pub public_id: i64, } -pub struct TwonlyConfig { - pub database_path: String, - pub data_directory: String, -} - pub(crate) struct TwonlyFlutter { #[allow(dead_code)] - pub(crate) config: TwonlyConfig, - // /// Rust runs in the same process as drift, the database can only be opened in readonly mode - // pub(crate) twonly_db_readonly: Arc, + pub(crate) config: InitConfig, pub(crate) user_discovery: Shared>, + #[allow(dead_code)] + pub(crate) rust_db: Arc, + pub(crate) secure_storage: SecureStorage, + pub(crate) key_manager: Arc>, } -static GLOBAL_TWONLY: OnceCell = OnceCell::const_new(); - pub(super) fn get_twonly_flutter() -> Result<&'static TwonlyFlutter> { - GLOBAL_TWONLY.get().ok_or(TwonlyError::Initialization) + let ctx = Context::get_static()?; + if let Context::Flutter(twonly) = ctx { + return Ok(twonly); + } else { + return Err(TwonlyError::Initialization); + } } -pub async fn initialize_twonly_flutter(config: TwonlyConfig) -> Result<()> { - if GLOBAL_TWONLY.initialized() { - tracing::info!("twonly already initialized."); - return Ok(()); - } - let log_dir = PathBuf::from(&config.data_directory).join("log"); - init_tracing(&log_dir, true).await; - tracing::info!("Initialized twonly workspace."); - let twonly_res: Result<&'static TwonlyFlutter> = GLOBAL_TWONLY - .get_or_try_init(|| async { - // let database_dir = PathBuf::from(&config.database_path.clone()); - // let Some(rust_db_path) = database_dir.parent() else { - // return Err(TwonlyError::DatabaseNotFound); - // }; - // let rust_db_path = rust_db_path.join("rust_db.sqlite").display().to_string(); - - // let twonly_db_readonly = Arc::new(Database::new(&config.database_path, true).await?); - // let rust_db = Arc::new(Database::new(&rust_db_path, false).await?); - - Ok(TwonlyFlutter { - config, - // twonly_db_readonly, - // rust_db, - user_discovery: Shared::new(UserDiscovery::new( - UserDiscoveryStoreFlutter {}, - UserDiscoveryUtilsFlutter {}, - )?), - }) - }) - .await; - - twonly_res?; - +pub async fn initialize_twonly_flutter(config: InitConfig) -> Result<()> { + Context::init_flutter(config).await?; Ok(()) } diff --git a/rust/src/bridge/wrapper/backup.rs b/rust/src/bridge/wrapper/backup.rs new file mode 100644 index 00000000..26f92441 --- /dev/null +++ b/rust/src/bridge/wrapper/backup.rs @@ -0,0 +1,90 @@ +use std::path::PathBuf; + +use crate::backup::backup_archive::BackupArchive; +use crate::backup::backup_identity::BackupIdentity; +use crate::bridge::get_twonly_flutter; +use crate::context::Context; +use crate::error::{Result, TwonlyError}; +pub use crate::keys::backup_password_keys::BackupPasswordKeys; + +pub struct RustBackupIdentity(); +pub struct RustBackupArchive(); + +impl RustBackupIdentity { + pub async fn get_backup_password_keys( + user_id: i64, + password: String, + ) -> Result { + BackupPasswordKeys::from_password(&password, user_id) + } + pub async fn get_backup_id() -> Option { + let key_manager = get_twonly_flutter().ok()?.key_manager.lock().await; + Some(hex::encode(key_manager.backup_password.clone()?.backup_id)) + } + + pub async fn set_backup_password_keys(user_id: i64, password: String) -> Result<()> { + let backup_keys = BackupPasswordKeys::from_password(&password, user_id)?; + let ctx = get_twonly_flutter()?; + let mut key_manager = ctx.key_manager.lock().await; + key_manager.backup_password = Some(backup_keys); + key_manager.store_to_keychain(&ctx.secure_storage)?; + Ok(()) + } + + pub async fn import_backup_password_keys( + backup_id: Vec, + encryption_key: Vec, + ) -> Result<()> { + let backup_id: [u8; 32] = backup_id + .try_into() + .map_err(|a: Vec| TwonlyError::WronKeySize(2, a.len()))?; + + let encryption_key: [u8; 32] = encryption_key + .try_into() + .map_err(|a: Vec| TwonlyError::WronKeySize(2, a.len()))?; + + let backup_keys = BackupPasswordKeys::new(backup_id, encryption_key); + let ctx = get_twonly_flutter()?; + let mut key_manager = ctx.key_manager.lock().await; + key_manager.backup_password = Some(backup_keys); + key_manager.store_to_keychain(&ctx.secure_storage)?; + Ok(()) + } + + pub async fn get_identity_backup_bytes() -> Result> { + let key_manager = get_twonly_flutter()?.key_manager.lock().await; + return BackupIdentity::encrypt_key_manager(&key_manager); + } + + pub async fn restore_identity_backup( + keys: BackupPasswordKeys, + encrypted_bytes: Vec, + ) -> Result<()> { + let ctx = get_twonly_flutter()?; + BackupIdentity::restore_key_manager(&ctx.secure_storage, &keys, &encrypted_bytes)?; + let restored = crate::keys::KeyManager::try_from_keychain(&ctx.secure_storage)?; + *ctx.key_manager.lock().await = restored; + Ok(()) + } +} +impl RustBackupArchive { + pub async fn create_backup_archive() -> Result<(String, String)> { + let ctx = Context::get_static()?; + let path = BackupArchive::create_backup(&ctx).await?; + let key_manager = get_twonly_flutter()?.key_manager.lock().await; + let token = hex::encode(key_manager.main_key.get_backup_download_token()); + Ok((token, path.canonicalize()?.to_string_lossy().to_string())) + } + + pub async fn restore_backup_archive(file_path: String) -> Result<()> { + let ctx = Context::get_static()?; + BackupArchive::restore_from_backup(ctx, &PathBuf::from(file_path)).await + } + + pub async fn get_backup_download_token() -> Option { + let key_manager = get_twonly_flutter().ok()?.key_manager.lock().await; + Some(hex::encode( + key_manager.main_key.get_backup_download_token(), + )) + } +} diff --git a/rust/src/bridge/wrapper/key_manager.rs b/rust/src/bridge/wrapper/key_manager.rs new file mode 100644 index 00000000..d7ba4b14 --- /dev/null +++ b/rust/src/bridge/wrapper/key_manager.rs @@ -0,0 +1,111 @@ +use std::collections::HashMap; + +use crate::bridge::get_twonly_flutter; +use crate::error::{Result, TwonlyError}; +use crate::keys::SignalIdentityKey; + +pub struct RustKeyManager {} + +impl RustKeyManager { + pub async fn get_login_token() -> Result> { + let key_manager = get_twonly_flutter()?.key_manager.lock().await; + Ok(key_manager.main_key.get_login_token().to_vec()) + } + + pub async fn get_user_id() -> Result> { + let key_manager = get_twonly_flutter()?.key_manager.lock().await; + Ok(key_manager.user_id) + } + + pub async fn set_user_id(user_id: i64) -> Result<()> { + let ctx = get_twonly_flutter()?; + let mut key_manager = ctx.key_manager.lock().await; + key_manager.user_id = Some(user_id); + key_manager.store_to_keychain(&ctx.secure_storage)?; + Ok(()) + } + + pub async fn import_signal_identity( + identity_key_pair_structure: Vec, + registration_id: i64, + signed_pre_key_store: HashMap>, + ) -> Result<()> { + let ctx = get_twonly_flutter()?; + let mut key_manager = ctx.key_manager.lock().await; + key_manager.signal_identity = Some(SignalIdentityKey { + identity_key_pair_structure, + registration_id, + pre_key_store: signed_pre_key_store, + }); + key_manager.store_to_keychain(&ctx.secure_storage)?; + Ok(()) + } + + pub async fn get_signal_identity() -> Result<(Vec, i64)> { + let ctx = get_twonly_flutter()?; + let key_manager = ctx.key_manager.lock().await; + if let Some(signal_identity) = &key_manager.signal_identity { + Ok(( + signal_identity.identity_key_pair_structure.to_owned(), + signal_identity.registration_id, + )) + } else { + Err(TwonlyError::SignalIdentityNotFound) + } + } + + pub async fn load_signed_prekey(signed_pre_key_id: i64) -> Result>> { + let ctx = get_twonly_flutter()?; + let key_manager = ctx.key_manager.lock().await; + if let Some(signal_identity) = &key_manager.signal_identity { + Ok(signal_identity + .pre_key_store + .get(&signed_pre_key_id) + .cloned()) + } else { + Err(TwonlyError::SignalIdentityNotFound) + } + } + + pub async fn store_signed_prekey(signed_pre_key_id: i64, record: Vec) -> Result<()> { + let ctx = get_twonly_flutter()?; + let mut key_manager = ctx.key_manager.lock().await; + if let Some(signal_identity) = &mut key_manager.signal_identity { + signal_identity + .pre_key_store + .insert(signed_pre_key_id, record); + key_manager.store_to_keychain(&ctx.secure_storage)?; + Ok(()) + } else { + Err(TwonlyError::SignalIdentityNotFound) + } + } + + pub async fn remove_signed_prekey(signed_pre_key_id: i64) -> Result<()> { + let ctx = get_twonly_flutter()?; + let mut key_manager = ctx.key_manager.lock().await; + if let Some(signal_identity) = &mut key_manager.signal_identity { + signal_identity.pre_key_store.remove(&signed_pre_key_id); + key_manager.store_to_keychain(&ctx.secure_storage)?; + Ok(()) + } else { + Err(TwonlyError::SignalIdentityNotFound) + } + } + + pub async fn load_signed_prekeys() -> Result>> { + let ctx = get_twonly_flutter()?; + let key_manager = ctx.key_manager.lock().await; + if let Some(signal_identity) = &key_manager.signal_identity { + Ok(signal_identity.pre_key_store.to_owned()) + } else { + Err(TwonlyError::SignalIdentityNotFound) + } + } + + pub async fn remove_key_manager() -> Result<()> { + let ctx = get_twonly_flutter()?; + crate::keys::KeyManager::remove_from_keychain(&ctx.secure_storage)?; + Ok(()) + } +} diff --git a/rust/src/bridge/wrapper/mod.rs b/rust/src/bridge/wrapper/mod.rs index 16456710..75173c53 100644 --- a/rust/src/bridge/wrapper/mod.rs +++ b/rust/src/bridge/wrapper/mod.rs @@ -1 +1,3 @@ +pub mod backup; +pub mod key_manager; pub mod user_discovery; diff --git a/rust/src/bridge/wrapper/user_discovery.rs b/rust/src/bridge/wrapper/user_discovery.rs index 6c50a7f8..4ecb59c2 100644 --- a/rust/src/bridge/wrapper/user_discovery.rs +++ b/rust/src/bridge/wrapper/user_discovery.rs @@ -1,5 +1,5 @@ -use crate::bridge::error::Result; use crate::bridge::get_twonly_flutter; +use crate::error::Result; pub struct FlutterUserDiscovery {} diff --git a/rust/src/context.rs b/rust/src/context.rs new file mode 100644 index 00000000..53361e38 --- /dev/null +++ b/rust/src/context.rs @@ -0,0 +1,183 @@ +use crate::{ + bridge::{ + callbacks::user_discovery::{UserDiscoveryStoreFlutter, UserDiscoveryUtilsFlutter}, + InitConfig, + }, + database::Database, + error::{Result, TwonlyError}, + keys::{DatabaseKey, KeyManager}, + log::init_tracing, + utils::Shared, +}; +use protocols::user_discovery::UserDiscovery; +use std::{path::PathBuf, sync::Arc}; +use tokio::sync::{Mutex, OnceCell}; +use zeroize::Zeroize; + +use crate::{bridge::TwonlyFlutter, secure_storage::SecureStorage, standalone::TwonlyStandalone}; + +pub(crate) enum Context { + Flutter(TwonlyFlutter), + Standalone(TwonlyStandalone), +} + +impl Context { + #[cfg(test)] + pub(crate) fn from_standalone(standalone: TwonlyStandalone) -> Self { + Self::Standalone(standalone) + } +} + +static GLOBAL_CONTEXT: OnceCell = OnceCell::const_new(); + +impl Context { + pub(crate) async fn init_flutter(config: InitConfig) -> Result<()> { + Self::init_common(config, true).await + } + + #[allow(dead_code)] + pub(crate) async fn init_standalone(config: InitConfig) -> Result<()> { + Self::init_common(config, false).await + } + + #[cfg(test)] + pub(crate) async fn init_for_testing( + database_dir: PathBuf, + data_dir: PathBuf, + ) -> Result { + use tokio::sync::Mutex; + + std::fs::create_dir_all(&database_dir)?; + std::fs::create_dir_all(&data_dir)?; + + let config = InitConfig { + database_dir: database_dir.display().to_string(), + data_dir: data_dir.display().to_string(), + }; + + // Initialize tracing and secure storage if not already done + let _ = SecureStorage::init(); + let secure_storage = SecureStorage::new("eu.twonly.testing"); + + let key_manager = KeyManager::generate()?; + key_manager.store_to_keychain(&secure_storage)?; + + let rust_db_path = database_dir.join("rust_db.sqlite"); + let rust_db = Arc::new( + Database::new( + &rust_db_path.display().to_string(), + Some(&key_manager.main_key.get_database_key(DatabaseKey::RustDb)), + false, + ) + .await?, + ); + + Ok(Context::from_standalone(TwonlyStandalone { + config, + rust_db, + secure_storage, + key_manager: Arc::new(Mutex::new(key_manager)), + })) + } + + async fn init_common(config: InitConfig, is_flutter: bool) -> Result<()> { + if GLOBAL_CONTEXT.initialized() { + tracing::info!("twonly already initialized. Ensuring storage directories exist."); + std::fs::create_dir_all(&config.database_dir)?; + std::fs::create_dir_all(&config.data_dir)?; + return Ok(()); + } + + std::fs::create_dir_all(&config.database_dir)?; + std::fs::create_dir_all(&config.data_dir)?; + + let log_dir = PathBuf::from(&config.data_dir).join("log"); + init_tracing(&log_dir, is_flutter).await; + + SecureStorage::init()?; + let secure_storage = SecureStorage::new("eu.twonly"); + + let database_dir = PathBuf::from(&config.database_dir.clone()); + let rust_db_path = database_dir.join("rust_db.sqlite"); + + tracing::info!("Initialized twonly workspace."); + let res: Result<&'static Context> = GLOBAL_CONTEXT + .get_or_try_init(|| async { + let key_manager = match KeyManager::try_from_keychain(&secure_storage) { + Ok(key) => key, + Err(err) => { + tracing::error!("{err}"); + if rust_db_path.exists() { + tracing::error!("Rust Database exists, while the key manager not. This must be a secure storage error."); + return Err(TwonlyError::SecureStorageError); + } + tracing::info!("Generating a new key manager."); + let new = KeyManager::generate()?; + new.store_to_keychain(&secure_storage)?; + new + } + }; + + let mut rust_db_key = key_manager.main_key.get_database_key(DatabaseKey::RustDb); + + let rust_db = Arc::new( + Database::new( + &rust_db_path.display().to_string(), + Some(rust_db_key.as_str()), + false, + ) + .await?, + ); + + rust_db_key.zeroize(); + + if is_flutter { + Ok(Context::Flutter(TwonlyFlutter { + config, + secure_storage, + rust_db, + key_manager: Arc::new(Mutex::new(key_manager)), + user_discovery: Shared::new(UserDiscovery::new( + UserDiscoveryStoreFlutter {}, + UserDiscoveryUtilsFlutter {}, + )?), + })) + } else { + Ok(Context::Standalone(TwonlyStandalone { + config, + rust_db, + key_manager: Arc::new(Mutex::new(key_manager)), + secure_storage, + })) + } + }) + .await; + res?; + Ok(()) + } + + pub(super) fn get_static() -> Result<&'static Context> { + GLOBAL_CONTEXT.get().ok_or(TwonlyError::Initialization) + } + + // pub(crate) fn get_secure_storage(&self) -> Result<&SecureStorage> { + // match self { + // Self::Flutter(twonly) => Ok(&twonly.secure_storage), + // Self::Standalone(twonly) => Ok(&twonly.secure_storage), + // } + // } + + pub(crate) fn get_config(&self) -> Result<&InitConfig> { + match self { + Self::Flutter(twonly) => Ok(&twonly.config), + Self::Standalone(twonly) => Ok(&twonly.config), + } + } + + pub(crate) async fn get_key_manager(&self) -> Result> { + match self { + Self::Flutter(twonly) => Ok(twonly.key_manager.lock().await), + Self::Standalone(twonly) => Ok(twonly.key_manager.lock().await), + } + } +} diff --git a/rust/src/database/migrations/0001_initial.sql b/rust/src/database/migrations/0001_initial.sql new file mode 100644 index 00000000..715bcda8 --- /dev/null +++ b/rust/src/database/migrations/0001_initial.sql @@ -0,0 +1,8 @@ +-- Initial migration: Create received_messages and sending_messages tables + +CREATE TABLE IF NOT EXISTS received_messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + sender_id BIGINT NOT NULL, + content BLOB NOT NULL, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP +); diff --git a/rust/src/database/mod.rs b/rust/src/database/mod.rs index ee29f7e9..46721db9 100644 --- a/rust/src/database/mod.rs +++ b/rust/src/database/mod.rs @@ -1,65 +1,180 @@ -// use crate::bridge::error::{Result, TwonlyError}; -// use sqlx::migrate::MigrateDatabase; -// use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions}; -// use sqlx::{ConnectOptions, Sqlite, SqlitePool}; -// use std::time::Duration; +use crate::error::{Result, TwonlyError}; +use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions}; +use sqlx::{ConnectOptions, SqlitePool}; +use std::time::Duration; -// pub(crate) struct Database { -// pub(crate) pool: SqlitePool, -// } +pub(crate) mod tables; -// impl Database { -// pub(crate) async fn new(db_path: &String, read_only: bool) -> Result { -// let db_url = format!("sqlite://{}", db_path); +pub(crate) struct Database { + pub(crate) pool: SqlitePool, +} -// match Sqlite::database_exists(&db_url).await { -// Ok(true) => { -// tracing::debug!("database exists"); -// } -// Ok(false) => { -// tracing::error!("could not open the sqlite3 database"); -// return Err(TwonlyError::DatabaseNotFound); -// } -// Err(e) => { -// tracing::error!( -// "Could not check if database exists: {:?}, attempting to create", -// e -// ); -// return Err(TwonlyError::DatabaseNotFound); -// } -// } +impl Database { + pub(crate) async fn new( + db_path: &String, + encryption_key: Option<&str>, + read_only: bool, + ) -> Result { + let db_url = format!("sqlite://{}", db_path); -// tracing::debug!("Creating database connection pool"); + let log_statements_level = if std::env::var("SQLX_LOG_STATEMENTS").is_ok() { + tracing::log::LevelFilter::Info + } else { + tracing::log::LevelFilter::Off + }; -// let log_statements_level = if std::env::var("SQLX_LOG_STATEMENTS").is_ok() { -// tracing::log::LevelFilter::Info -// } else { -// tracing::log::LevelFilter::Off -// }; + let mut connect_options = format!("{db_url}?mode=rwc") + .parse::()? + .log_statements(log_statements_level) + .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal) + .foreign_keys(true) + .read_only(read_only) + .busy_timeout(Duration::from_millis(5000)) + .pragma("recursive_triggers", "ON") + .log_slow_statements(tracing::log::LevelFilter::Warn, Duration::from_millis(500)); -// let connect_options = format!("{db_url}?mode=rwc") -// .parse::()? -// .log_statements(log_statements_level) -// .read_only(read_only) -// .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal) -// .foreign_keys(true) -// .busy_timeout(Duration::from_millis(5000)) -// .pragma("recursive_triggers", "ON") -// .log_slow_statements(tracing::log::LevelFilter::Warn, Duration::from_millis(500)); + if let Some(encryption_key) = encryption_key { + connect_options = connect_options.pragma("key", format!("'{}'", encryption_key)); + } -// let pool = SqlitePoolOptions::new() -// .acquire_timeout(Duration::from_secs(5)) -// .max_connections(10) -// .connect_with(connect_options) -// .await?; + let pool = SqlitePoolOptions::new() + .acquire_timeout(Duration::from_secs(5)) + .max_connections(10) + .connect_with(connect_options) + .await?; -// let row: (String, String) = sqlx::query_as("SELECT sqlite_version(), sqlite_source_id()") -// .fetch_one(&pool) -// .await?; + sqlx::migrate!("./src/database/migrations") + .run(&pool) + .await + .map_err(|e| { + tracing::error!("migration error: {:?}", e); + TwonlyError::Generic(format!("Migration error: {}", e)) + })?; -// tracing::info!("Rust SQLite Version: {}", row.0); -// tracing::info!("Rust SQLite Source ID: {}", row.1); + Ok(Self { pool }) + } -// Ok(Self { pool: pool }) -// } -// } + pub(crate) async fn create_backup( + &self, + output_path: &str, + encryption_key: Option<&str>, + ) -> Result<()> { + if let Some(key) = encryption_key { + let mut conn = self + .pool + .acquire() + .await + .map_err(|e| TwonlyError::Generic(e.to_string()))?; + + sqlx::query("ATTACH DATABASE ? AS backup KEY ?") + .bind(output_path) + .bind(key) + .execute(&mut *conn) + .await + .map_err(|e| TwonlyError::Generic(format!("Attach failed: {}", e)))?; + + sqlx::query("SELECT sqlcipher_export('backup')") + .execute(&mut *conn) + .await + .map_err(|e| TwonlyError::Generic(format!("Export failed: {}", e)))?; + + sqlx::query("DETACH DATABASE backup") + .execute(&mut *conn) + .await + .map_err(|e| TwonlyError::Generic(format!("Detach failed: {}", e)))?; + } else { + sqlx::query("VACUUM INTO ?") + .bind(output_path) + .execute(&self.pool) + .await + .map_err(|e| TwonlyError::Generic(format!("Backup failed: {}", e)))?; + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::database::tables::received_messages::ReceivedMessage; + + use super::*; + use tempfile::tempdir; + + #[tokio::test] + async fn test_database_encryption_and_migrations() { + let _ = pretty_env_logger::try_init(); + let dir = tempdir().unwrap(); + let db_path = dir.path().join("test.sqlite").display().to_string(); + let key = "secure_password"; + + // 1. Create and initialize database with key + let db = Database::new(&db_path, Some(key), false).await.unwrap(); + ReceivedMessage::insert(&db.pool, 1, b"hello world") + .await + .unwrap(); + + // 2. Try to open with WRONG key + let result = Database::new(&db_path, Some("wrong_password"), false).await; + assert!( + result.is_err(), + "Opening with wrong key should fail. If this passes, the database might not be encrypted!" + ); + + // 3. Open with CORRECT key again + let db = Database::new(&db_path, Some(key), false).await.unwrap(); + let messages = ReceivedMessage::get_all(&db.pool).await.unwrap(); + assert_eq!(messages.len(), 1); + assert_eq!(messages[0].sender_id, 1); + assert_eq!(messages[0].content, b"hello world"); + } + + #[tokio::test] + async fn test_database_backup_encrypted() { + let _ = pretty_env_logger::try_init(); + let dir = tempdir().unwrap(); + let db_path = dir.path().join("test_enc.sqlite").display().to_string(); + let backup_path = dir.path().join("backup_enc.sqlite").display().to_string(); + let key = "secure_password"; + + let db = Database::new(&db_path, Some(key), false).await.unwrap(); + ReceivedMessage::insert(&db.pool, 1, b"hello world") + .await + .unwrap(); + + db.create_backup(&backup_path, Some(key)).await.unwrap(); + + // 1. Verify it cannot be opened with wrong key + let result = Database::new(&backup_path, Some("wrong_password"), false).await; + assert!( + result.is_err(), + "Encrypted backup should fail with wrong key" + ); + + // 2. Open backup with correct key and verify data + let backup_db = Database::new(&backup_path, Some(key), false).await.unwrap(); + let messages = ReceivedMessage::get_all(&backup_db.pool).await.unwrap(); + assert_eq!(messages.len(), 1); + assert_eq!(messages[0].sender_id, 1); + } + + #[tokio::test] + async fn test_database_backup_plaintext() { + let _ = pretty_env_logger::try_init(); + let dir = tempdir().unwrap(); + let db_path = dir.path().join("test_plain.sqlite").display().to_string(); + let backup_path = dir.path().join("backup_plain.sqlite").display().to_string(); + + let db = Database::new(&db_path, None, false).await.unwrap(); + ReceivedMessage::insert(&db.pool, 1, b"hello world") + .await + .unwrap(); + + db.create_backup(&backup_path, None).await.unwrap(); + + // Open backup and verify + let backup_db = Database::new(&backup_path, None, false).await.unwrap(); + let messages = ReceivedMessage::get_all(&backup_db.pool).await.unwrap(); + assert_eq!(messages.len(), 1); + assert_eq!(messages[0].sender_id, 1); + } +} diff --git a/rust/src/database/tables/mod.rs b/rust/src/database/tables/mod.rs new file mode 100644 index 00000000..c8a04d57 --- /dev/null +++ b/rust/src/database/tables/mod.rs @@ -0,0 +1,102 @@ +pub mod received_messages; + +#[macro_export] +macro_rules! generate_insert { + ($table:literal, $fn_name:ident, $($field:ident : $ty:ty),+) => { + pub async fn $fn_name( + pool: &sqlx::SqlitePool, + $($field: $ty),+ + ) -> crate::error::Result { + let sql = format!( + "INSERT INTO {} ({}) VALUES ({}) RETURNING id", + $table, + vec![$(stringify!($field)),+].join(", "), + vec!["?"; [$({stringify!($field); 1}),+].len()].join(", ") + ); + + let row: (i64,) = sqlx::query_as(sqlx::AssertSqlSafe(sql)) + $(.bind($field))+ + .fetch_one(pool) + .await?; + + Ok(row.0) + } + }; +} + +#[macro_export] +macro_rules! generate_select { + ($table:literal, $fn_name:ident) => { + pub async fn $fn_name(pool: &sqlx::SqlitePool) -> crate::error::Result> { + let sql = format!("SELECT * FROM {}", $table); + let results = sqlx::query_as::<_, Self>(sqlx::AssertSqlSafe(sql)) + .fetch_all(pool) + .await?; + Ok(results) + } + }; + ($table:literal, $fn_name:ident, $($field:ident : $ty:ty),+) => { + pub async fn $fn_name(pool: &sqlx::SqlitePool, $($field: $ty),+) -> crate::error::Result> { + let mut sql = format!("SELECT * FROM {} WHERE ", $table); + let mut filters = Vec::new(); + $( + filters.push(format!("{} = ?", stringify!($field))); + )+ + sql.push_str(&filters.join(" AND ")); + + let results = sqlx::query_as::<_, Self>(sqlx::AssertSqlSafe(sql)) + $(.bind($field))+ + .fetch_all(pool) + .await?; + Ok(results) + } + }; +} + +#[macro_export] +macro_rules! generate_table_tests { + ( + $struct:ident, + $insert_fn:ident ($($arg:expr),+), + $select_all_fn:ident + ) => { + #[cfg(test)] + mod tests { + use super::*; + use crate::database::Database; + use tempfile::tempdir; + + #[tokio::test] + async fn test_generated_basic() { + let dir = tempdir().unwrap(); + let db_path = dir.path().join("test.sqlite").display().to_string(); + let db = Database::new(&db_path, None, false).await.unwrap(); + + let _id = $struct::$insert_fn(&db.pool, $($arg),+).await.unwrap(); + let all = $struct::$select_all_fn(&db.pool).await.unwrap(); + assert_eq!(all.len(), 1); + } + } + }; +} + +#[macro_export] +macro_rules! generate_test_select { + ($struct:ident, $insert_fn:ident ($($arg:expr),+), $select_fn:ident ($($sel_arg:expr),+)) => { + paste::paste! { + #[cfg(test)] + #[tokio::test] + async fn []() { + use crate::database::Database; + use tempfile::tempdir; + let dir = tempdir().unwrap(); + let db_path = dir.path().join("test.sqlite").display().to_string(); + let db = Database::new(&db_path, None, false).await.unwrap(); + + $struct::$insert_fn(&db.pool, $($arg),+).await.unwrap(); + let results = $struct::$select_fn(&db.pool, $($sel_arg),+).await.unwrap(); + assert_eq!(results.len(), 1); + } + } + }; +} diff --git a/rust/src/database/tables/received_messages.rs b/rust/src/database/tables/received_messages.rs new file mode 100644 index 00000000..6a4520de --- /dev/null +++ b/rust/src/database/tables/received_messages.rs @@ -0,0 +1,26 @@ +#![allow(dead_code)] +use chrono::{DateTime, Utc}; +use sqlx::FromRow; + +#[derive(Debug, FromRow, PartialEq, Clone)] +pub struct ReceivedMessage { + pub id: i64, + pub sender_id: i64, + pub content: Vec, + pub timestamp: DateTime, +} + +impl ReceivedMessage { + crate::generate_insert!( + "received_messages", + insert, + sender_id: i64, + content: &[u8] + ); + crate::generate_select!("received_messages", get_all); + crate::generate_select!("received_messages", get_by_sender, sender_id: i64); +} + +crate::generate_table_tests!(ReceivedMessage, insert(1, b"hello world"), get_all); + +crate::generate_test_select!(ReceivedMessage, insert(1, b"hello world"), get_by_sender(1)); diff --git a/rust/src/error.rs b/rust/src/error.rs new file mode 100644 index 00000000..7a688384 --- /dev/null +++ b/rust/src/error.rs @@ -0,0 +1,88 @@ +use hex::FromHexError; +use protocols::user_discovery::error::UserDiscoveryError; +use scrypt::errors::{InvalidOutputLen, InvalidParams}; +use thiserror::Error; +use zip::result::ZipError; + +pub type Result = core::result::Result; + +#[derive(Error, Debug)] +pub enum TwonlyError { + #[error("global twonly is not initialized")] + Initialization, + + #[error("Tried to access the wrong context")] + WrongContext, + + #[error("Tried to access signal identity while it does not exists")] + SignalIdentityNotFound, + + #[error("init_flutter_callbacks was not called")] + MissingCallbackInitialization, + + #[error("wrong input key size. expected: {0} but got {1}")] + WronKeySize(usize, usize), + + #[error("Could not find the given database")] + DatabaseNotFound, + + #[error("main_key could not be loaded from the key_chain")] + MissingMainKey, + + #[error("{0}")] + UserDiscoveryError(#[from] UserDiscoveryError), + + #[error("Error in dart callback")] + DartError, + + #[error( + "Storage error: database exists but master key could not be loaded from secure storage" + )] + SecureStorageError, + + #[error("{0}")] + SqliteError(#[from] sqlx::Error), + + #[error("{0}")] + Generic(String), + + #[error("{0}")] + IoError(#[from] std::io::Error), + + #[error("{0}")] + ZipError(#[from] ZipError), + + #[error("{0}")] + Walkdir(#[from] walkdir::Error), + + #[error("{0}")] + Postcard(#[from] postcard::Error), + + #[error("{0}")] + HexError(#[from] FromHexError), + + #[error("{0}")] + InvalidParams(#[from] InvalidParams), + #[error("{0}")] + InvalidOutputLen(#[from] InvalidOutputLen), + #[error("AES-GCM error")] + AesGcm, +} + +impl From for TwonlyError { + fn from(error: String) -> Self { + TwonlyError::Generic(error) + } +} + +impl From for UserDiscoveryError { + fn from(error: TwonlyError) -> Self { + UserDiscoveryError::Store(error.to_string()) + } +} + +impl From for TwonlyError { + fn from(_: aes_gcm::Error) -> Self { + TwonlyError::AesGcm + } +} diff --git a/rust/src/frb_generated.rs b/rust/src/frb_generated.rs index 5e0cb33a..0be3e628 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 = 1680338106; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -1867463121; // Section: executor @@ -195,7 +195,7 @@ fn wire__crate__bridge__initialize_twonly_flutter_impl( }; let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_config = ::sse_decode(&mut deserializer); + let api_config = ::sse_decode(&mut deserializer); deserializer.end(); move |context| async move { transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( @@ -210,6 +210,390 @@ fn wire__crate__bridge__initialize_twonly_flutter_impl( }, ) } +fn wire__crate__bridge__wrapper__backup__rust_backup_archive_create_backup_archive_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: "rust_backup_archive_create_backup_archive", 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); + deserializer.end(); move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>((move || async move { + let output_ok = crate::bridge::wrapper::backup::RustBackupArchive::create_backup_archive().await?; Ok(output_ok) + })().await) + } }) +} +fn wire__crate__bridge__wrapper__backup__rust_backup_archive_get_backup_download_token_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: "rust_backup_archive_get_backup_download_token", 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); + deserializer.end(); move |context| async move { + transform_result_sse::<_, ()>((move || async move { + let output_ok = Result::<_,()>::Ok(crate::bridge::wrapper::backup::RustBackupArchive::get_backup_download_token().await)?; Ok(output_ok) + })().await) + } }) +} +fn wire__crate__bridge__wrapper__backup__rust_backup_archive_restore_backup_archive_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: "rust_backup_archive_restore_backup_archive", 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_file_path = ::sse_decode(&mut deserializer);deserializer.end(); move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>((move || async move { + let output_ok = crate::bridge::wrapper::backup::RustBackupArchive::restore_backup_archive(api_file_path).await?; Ok(output_ok) + })().await) + } }) +} +fn wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_id_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: "rust_backup_identity_get_backup_id", + 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); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, ()>( + (move || async move { + let output_ok = Result::<_, ()>::Ok( + crate::bridge::wrapper::backup::RustBackupIdentity::get_backup_id() + .await, + )?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_password_keys_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: "rust_backup_identity_get_backup_password_keys", 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_user_id = ::sse_decode(&mut deserializer); +let api_password = ::sse_decode(&mut deserializer);deserializer.end(); move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>((move || async move { + let output_ok = crate::bridge::wrapper::backup::RustBackupIdentity::get_backup_password_keys(api_user_id, api_password).await?; Ok(output_ok) + })().await) + } }) +} +fn wire__crate__bridge__wrapper__backup__rust_backup_identity_get_identity_backup_bytes_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: "rust_backup_identity_get_identity_backup_bytes", 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); + deserializer.end(); move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>((move || async move { + let output_ok = crate::bridge::wrapper::backup::RustBackupIdentity::get_identity_backup_bytes().await?; Ok(output_ok) + })().await) + } }) +} +fn wire__crate__bridge__wrapper__backup__rust_backup_identity_import_backup_password_keys_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: "rust_backup_identity_import_backup_password_keys", 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_backup_id = >::sse_decode(&mut deserializer); +let api_encryption_key = >::sse_decode(&mut deserializer);deserializer.end(); move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>((move || async move { + let output_ok = crate::bridge::wrapper::backup::RustBackupIdentity::import_backup_password_keys(api_backup_id, api_encryption_key).await?; Ok(output_ok) + })().await) + } }) +} +fn wire__crate__bridge__wrapper__backup__rust_backup_identity_restore_identity_backup_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: "rust_backup_identity_restore_identity_backup", 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_keys = ::sse_decode(&mut deserializer); +let api_encrypted_bytes = >::sse_decode(&mut deserializer);deserializer.end(); move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>((move || async move { + let output_ok = crate::bridge::wrapper::backup::RustBackupIdentity::restore_identity_backup(api_keys, api_encrypted_bytes).await?; Ok(output_ok) + })().await) + } }) +} +fn wire__crate__bridge__wrapper__backup__rust_backup_identity_set_backup_password_keys_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: "rust_backup_identity_set_backup_password_keys", 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_user_id = ::sse_decode(&mut deserializer); +let api_password = ::sse_decode(&mut deserializer);deserializer.end(); move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>((move || async move { + let output_ok = crate::bridge::wrapper::backup::RustBackupIdentity::set_backup_password_keys(api_user_id, api_password).await?; Ok(output_ok) + })().await) + } }) +} +fn wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_login_token_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: "rust_key_manager_get_login_token", + 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); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = + crate::bridge::wrapper::key_manager::RustKeyManager::get_login_token() + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_signal_identity_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: "rust_key_manager_get_signal_identity", 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); + deserializer.end(); move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>((move || async move { + let output_ok = crate::bridge::wrapper::key_manager::RustKeyManager::get_signal_identity().await?; Ok(output_ok) + })().await) + } }) +} +fn wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_user_id_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: "rust_key_manager_get_user_id", + 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); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = + crate::bridge::wrapper::key_manager::RustKeyManager::get_user_id() + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_signal_identity_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: "rust_key_manager_import_signal_identity", 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_identity_key_pair_structure = >::sse_decode(&mut deserializer); +let api_registration_id = ::sse_decode(&mut deserializer); +let api_signed_pre_key_store = >>::sse_decode(&mut deserializer);deserializer.end(); move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>((move || async move { + let output_ok = crate::bridge::wrapper::key_manager::RustKeyManager::import_signal_identity(api_identity_key_pair_structure, api_registration_id, api_signed_pre_key_store).await?; Ok(output_ok) + })().await) + } }) +} +fn wire__crate__bridge__wrapper__key_manager__rust_key_manager_load_signed_prekey_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: "rust_key_manager_load_signed_prekey", 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_signed_pre_key_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::wrapper::key_manager::RustKeyManager::load_signed_prekey(api_signed_pre_key_id).await?; Ok(output_ok) + })().await) + } }) +} +fn wire__crate__bridge__wrapper__key_manager__rust_key_manager_load_signed_prekeys_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: "rust_key_manager_load_signed_prekeys", 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); + deserializer.end(); move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>((move || async move { + let output_ok = crate::bridge::wrapper::key_manager::RustKeyManager::load_signed_prekeys().await?; Ok(output_ok) + })().await) + } }) +} +fn wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_key_manager_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: "rust_key_manager_remove_key_manager", 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); + deserializer.end(); move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>((move || async move { + let output_ok = crate::bridge::wrapper::key_manager::RustKeyManager::remove_key_manager().await?; Ok(output_ok) + })().await) + } }) +} +fn wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_signed_prekey_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: "rust_key_manager_remove_signed_prekey", 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_signed_pre_key_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::wrapper::key_manager::RustKeyManager::remove_signed_prekey(api_signed_pre_key_id).await?; Ok(output_ok) + })().await) + } }) +} +fn wire__crate__bridge__wrapper__key_manager__rust_key_manager_set_user_id_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: "rust_key_manager_set_user_id", + 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_user_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::wrapper::key_manager::RustKeyManager::set_user_id( + api_user_id, + ) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__bridge__wrapper__key_manager__rust_key_manager_store_signed_prekey_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: "rust_key_manager_store_signed_prekey", 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_signed_pre_key_id = ::sse_decode(&mut deserializer); +let api_record = >::sse_decode(&mut deserializer);deserializer.end(); move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>((move || async move { + let output_ok = crate::bridge::wrapper::key_manager::RustKeyManager::store_signed_prekey(api_signed_pre_key_id, api_record).await?; Ok(output_ok) + })().await) + } }) +} // Section: static_checks @@ -692,6 +1076,14 @@ impl SseDecode for flutter_rust_bridge::DartOpaque { } } +impl SseDecode for std::collections::HashMap> { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = )>>::sse_decode(deserializer); + return inner.into_iter().collect(); + } +} + impl SseDecode for StreamSink { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -722,6 +1114,18 @@ impl SseDecode for crate::bridge::AnnouncedUser { } } +impl SseDecode for crate::keys::backup_password_keys::BackupPasswordKeys { + // 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_backupId = <[u8; 32]>::sse_decode(deserializer); + let mut var_encryptionKey = <[u8; 32]>::sse_decode(deserializer); + return crate::keys::backup_password_keys::BackupPasswordKeys { + backup_id: var_backupId, + encryption_key: var_encryptionKey, + }; + } +} + impl SseDecode for bool { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -743,6 +1147,18 @@ impl SseDecode for i64 { } } +impl SseDecode for crate::bridge::InitConfig { + // 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_databaseDir = ::sse_decode(deserializer); + let mut var_dataDir = ::sse_decode(deserializer); + return crate::bridge::InitConfig { + database_dir: var_databaseDir, + data_dir: var_dataDir, + }; + } +} + impl SseDecode for isize { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -786,6 +1202,29 @@ impl SseDecode for Vec { } } +impl SseDecode for Vec<(i64, 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(<(i64, Vec)>::sse_decode(deserializer)); + } + return ans_; + } +} + +impl SseDecode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + if (::sse_decode(deserializer)) { + return Some(::sse_decode(deserializer)); + } else { + return None; + } + } +} + impl SseDecode for Option { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -863,15 +1302,51 @@ impl SseDecode for crate::bridge::OtherPromotion { } } -impl SseDecode for crate::bridge::TwonlyConfig { +impl SseDecode for (i64, 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 var_databasePath = ::sse_decode(deserializer); - let mut var_dataDirectory = ::sse_decode(deserializer); - return crate::bridge::TwonlyConfig { - database_path: var_databasePath, - data_directory: var_dataDirectory, - }; + let mut var_field0 = ::sse_decode(deserializer); + let mut var_field1 = >::sse_decode(deserializer); + return (var_field0, var_field1); + } +} + +impl SseDecode for (Vec, i64) { + // 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_field0 = >::sse_decode(deserializer); + let mut var_field1 = ::sse_decode(deserializer); + return (var_field0, var_field1); + } +} + +impl SseDecode for (String, String) { + // 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_field0 = ::sse_decode(deserializer); + let mut var_field1 = ::sse_decode(deserializer); + return (var_field0, var_field1); + } +} + +impl SseDecode for crate::bridge::wrapper::backup::RustBackupArchive { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + return crate::bridge::wrapper::backup::RustBackupArchive(); + } +} + +impl SseDecode for crate::bridge::wrapper::backup::RustBackupIdentity { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + return crate::bridge::wrapper::backup::RustBackupIdentity(); + } +} + +impl SseDecode for crate::bridge::wrapper::key_manager::RustKeyManager { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + return crate::bridge::wrapper::key_manager::RustKeyManager {}; } } @@ -889,6 +1364,14 @@ impl SseDecode for u8 { } } +impl SseDecode for [u8; 32] { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = >::sse_decode(deserializer); + return flutter_rust_bridge::for_generated::from_vec_to_array(inner); + } +} + impl SseDecode for () { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {} @@ -925,6 +1408,25 @@ fn pde_ffi_dispatcher_primary_impl( 6 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_update_verification_state_for_user_impl(port, ptr, rust_vec_len, data_len), 7 => wire__crate__bridge__callbacks__init_flutter_callbacks_impl(port, ptr, rust_vec_len, data_len), 8 => wire__crate__bridge__initialize_twonly_flutter_impl(port, ptr, rust_vec_len, data_len), +9 => wire__crate__bridge__wrapper__backup__rust_backup_archive_create_backup_archive_impl(port, ptr, rust_vec_len, data_len), +10 => wire__crate__bridge__wrapper__backup__rust_backup_archive_get_backup_download_token_impl(port, ptr, rust_vec_len, data_len), +11 => wire__crate__bridge__wrapper__backup__rust_backup_archive_restore_backup_archive_impl(port, ptr, rust_vec_len, data_len), +12 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_id_impl(port, ptr, rust_vec_len, data_len), +13 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), +14 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_identity_backup_bytes_impl(port, ptr, rust_vec_len, data_len), +15 => wire__crate__bridge__wrapper__backup__rust_backup_identity_import_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), +16 => wire__crate__bridge__wrapper__backup__rust_backup_identity_restore_identity_backup_impl(port, ptr, rust_vec_len, data_len), +17 => wire__crate__bridge__wrapper__backup__rust_backup_identity_set_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), +18 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_login_token_impl(port, ptr, rust_vec_len, data_len), +19 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_signal_identity_impl(port, ptr, rust_vec_len, data_len), +20 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_user_id_impl(port, ptr, rust_vec_len, data_len), +21 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_signal_identity_impl(port, ptr, rust_vec_len, data_len), +22 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_load_signed_prekey_impl(port, ptr, rust_vec_len, data_len), +23 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_load_signed_prekeys_impl(port, ptr, rust_vec_len, data_len), +24 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_key_manager_impl(port, ptr, rust_vec_len, data_len), +25 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_signed_prekey_impl(port, ptr, rust_vec_len, data_len), +26 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_set_user_id_impl(port, ptr, rust_vec_len, data_len), +27 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_store_signed_prekey_impl(port, ptr, rust_vec_len, data_len), _ => unreachable!(), } } @@ -966,6 +1468,27 @@ impl flutter_rust_bridge::IntoIntoDart> } } // Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::keys::backup_password_keys::BackupPasswordKeys { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.backup_id.into_into_dart().into_dart(), + self.encryption_key.into_into_dart().into_dart(), + ] + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::keys::backup_password_keys::BackupPasswordKeys +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::keys::backup_password_keys::BackupPasswordKeys +{ + fn into_into_dart(self) -> crate::keys::backup_password_keys::BackupPasswordKeys { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs impl flutter_rust_bridge::IntoDart for crate::bridge::wrapper::user_discovery::FlutterUserDiscovery { @@ -985,6 +1508,22 @@ impl flutter_rust_bridge::IntoIntoDart flutter_rust_bridge::for_generated::DartAbi { + [ + self.database_dir.into_into_dart().into_dart(), + self.data_dir.into_into_dart().into_dart(), + ] + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for crate::bridge::InitConfig {} +impl flutter_rust_bridge::IntoIntoDart for crate::bridge::InitConfig { + fn into_into_dart(self) -> crate::bridge::InitConfig { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs impl flutter_rust_bridge::IntoDart for FrbWrapper { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { [ @@ -1013,20 +1552,53 @@ impl flutter_rust_bridge::IntoIntoDart } } // Codec=Dco (DartCObject based), see doc to use other codecs -impl flutter_rust_bridge::IntoDart for crate::bridge::TwonlyConfig { +impl flutter_rust_bridge::IntoDart for crate::bridge::wrapper::backup::RustBackupArchive { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { - [ - self.database_path.into_into_dart().into_dart(), - self.data_directory.into_into_dart().into_dart(), - ] - .into_dart() + Vec::::new().into_dart() } } -impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for crate::bridge::TwonlyConfig {} -impl flutter_rust_bridge::IntoIntoDart - for crate::bridge::TwonlyConfig +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::bridge::wrapper::backup::RustBackupArchive { - fn into_into_dart(self) -> crate::bridge::TwonlyConfig { +} +impl flutter_rust_bridge::IntoIntoDart + for crate::bridge::wrapper::backup::RustBackupArchive +{ + fn into_into_dart(self) -> crate::bridge::wrapper::backup::RustBackupArchive { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::bridge::wrapper::backup::RustBackupIdentity { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + Vec::::new().into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::bridge::wrapper::backup::RustBackupIdentity +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::bridge::wrapper::backup::RustBackupIdentity +{ + fn into_into_dart(self) -> crate::bridge::wrapper::backup::RustBackupIdentity { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::bridge::wrapper::key_manager::RustKeyManager { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + Vec::::new().into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::bridge::wrapper::key_manager::RustKeyManager +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::bridge::wrapper::key_manager::RustKeyManager +{ + fn into_into_dart(self) -> crate::bridge::wrapper::key_manager::RustKeyManager { self } } @@ -1045,6 +1617,13 @@ impl SseEncode for flutter_rust_bridge::DartOpaque { } } +impl SseEncode for std::collections::HashMap> { + // 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.into_iter().collect(), serializer); + } +} + impl SseEncode for StreamSink { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -1068,6 +1647,14 @@ impl SseEncode for crate::bridge::AnnouncedUser { } } +impl SseEncode for crate::keys::backup_password_keys::BackupPasswordKeys { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + <[u8; 32]>::sse_encode(self.backup_id, serializer); + <[u8; 32]>::sse_encode(self.encryption_key, serializer); + } +} + impl SseEncode for bool { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -1087,6 +1674,14 @@ impl SseEncode for i64 { } } +impl SseEncode for crate::bridge::InitConfig { + // 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.database_dir, serializer); + ::sse_encode(self.data_dir, serializer); + } +} + impl SseEncode for isize { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -1127,6 +1722,26 @@ impl SseEncode for Vec { } } +impl SseEncode for Vec<(i64, 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 { + <(i64, Vec)>::sse_encode(item, serializer); + } + } +} + +impl SseEncode for Option { + // 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.is_some(), serializer); + if let Some(value) = self { + ::sse_encode(value, serializer); + } + } +} + impl SseEncode for Option { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -1189,14 +1804,45 @@ impl SseEncode for crate::bridge::OtherPromotion { } } -impl SseEncode for crate::bridge::TwonlyConfig { +impl SseEncode for (i64, 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.database_path, serializer); - ::sse_encode(self.data_directory, serializer); + ::sse_encode(self.0, serializer); + >::sse_encode(self.1, serializer); } } +impl SseEncode for (Vec, i64) { + // 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.0, serializer); + ::sse_encode(self.1, serializer); + } +} + +impl SseEncode for (String, String) { + // 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.0, serializer); + ::sse_encode(self.1, serializer); + } +} + +impl SseEncode for crate::bridge::wrapper::backup::RustBackupArchive { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {} +} + +impl SseEncode for crate::bridge::wrapper::backup::RustBackupIdentity { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {} +} + +impl SseEncode for crate::bridge::wrapper::key_manager::RustKeyManager { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {} +} + impl SseEncode for u32 { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -1211,6 +1857,19 @@ impl SseEncode for u8 { } } +impl SseEncode for [u8; 32] { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + >::sse_encode( + { + let boxed: Box<[_]> = Box::new(self); + boxed.into_vec() + }, + serializer, + ); + } +} + impl SseEncode for () { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {} diff --git a/rust/src/key_manager.rs b/rust/src/key_manager.rs deleted file mode 100644 index 6423303e..00000000 --- a/rust/src/key_manager.rs +++ /dev/null @@ -1,32 +0,0 @@ -struct TwonlyIdentity {} - -struct NostrIdentity {} - -struct KeyManager { - main_key: [u8; 32], -} - -impl KeyManager { - fn try_from_keychain() -> KeyManager { - todo!(); - } - - fn create_new() { - // generates main_key - - // generates signal identity - // generates nostr identity - } - - fn get_signal_identity() {} - - fn recover_from_trusted_friends() { - // - } - - fn generate_backup_key() {} - - fn recover_from_backup() { - // - } -} diff --git a/rust/src/keys/README.md b/rust/src/keys/README.md new file mode 100644 index 00000000..e801a428 --- /dev/null +++ b/rust/src/keys/README.md @@ -0,0 +1,73 @@ +# Cryptographic Architecture + +## 1. Main Key +A cryptographically secure, immutable master key. Loss of this key, in the absence of valid backups, results in permanent loss of account access. + +*Key Derivation*: Utilizes HKDF to derive subordinate keys. + +- Authentication Token: Uploaded to the server for session authentication. +- Backup Key: Used to encrypt a backup + - Backup Content: The encrypted backup encompasses + - Main Key + - Identity keys + - Local database (including contacts, public identities, memories (only references), and messages). + - Lifecycle: Backups are refreshed daily and deleted after one year. +- Media Main Key: Used to wrap media-specific keys. + - A new, cryptographically secure key is generated for every media file. + - The media key is wrapped using AES-GCM and the Main Media Key and stored in the online database along side to the uploaded media file database entry. + - The original media file is encrypted using AES-GCM and uploaded to the designated storage bucket. + +## 3. Identity Keys +- Signal Identity + - Generates a private and public key pair for secure communication. +- Nostr Identity + - Generates a private and public key pair for Nostr network interactions. + + +## 1. Backup Keys +Independent, securely generated keys used to wrap the primary backup key. + +### 1.1. Password-Based Backup +1. Derivation + - Utilizes scrypt with the username as the salt (cost 65536) to derive a 64-byte sequence. +2. Allocation: + - 32 bytes: Backup ID, used as the identifier to locate the backup on the server. + - 32 bytes: Backup wrapper key. +3. Content + - The payload contains the main key required to generate the auth token and the backup key. +4. Operation + - The backup wrapper key encrypts the main key. The ciphertext is uploaded anonymously to the server, indexed by the Backup ID. +5. Security Measures + - The server enforces strict rate limiting per IP address to prevent brute-force attacks. +6. Lifecycle + - These backup keys require a monthly refresh; otherwise, they are scheduled for deletion after two years. + +### 1.2. Trusted Friends Keys (Passwordless Recovery) +1. Initiation + - The recovering user generates a temporary ID (TempID) and a new ephemeral asymmetric key pair. +2. Request + - A recovery request containing the TempID and the public key is transmitted to a trusted contact via a secure link. +3. Verification + - The contact manually verifies the requestor's identity within their application to mitigate phishing risks. +4. Share Transmission + - The contact encrypts a trusted friend share using the provided public key. This share includes the user IDs, the minimum threshold required for decryption, and the cryptographic share (utilizing Shamir's Secret Sharing). +5. Reconstruction + - Upon receiving the required threshold of shares, the user reconstructs the shared secret data. +6. Second Factor (Optional) + - The shared secret data may mandate an additional factor (PIN or Email). For a PIN factor, an unlock token and a PIN seed are used to securely retrieve the remaining share from the server without exposing the raw PIN. +7. Final Recovery + - The decrypted recovery data provides the User ID, private key, and the backup master key necessary to restore the account and its backups. + +## 4. Web Portal Upload Protocol +1. Initialization + - The web portal generates a cryptographically secure symmetric key for end-to-end encrypted (E2EE) communication with the mobile application, alongside a newly registered session token. +2. Handshake + - The mobile application scans the QR code containing the session token and the symmetric key. +3. Authorization + - The application signals readiness via the server using the session token and securely provisions a temporary authentication token for media uploads over the established symmetric E2EE channel. +4. Key Exchange + - The web portal encrypts the media file using a newly generated media key. It transmits this media key to the application (encrypted via the E2EE symmetric key) and receives the wrapped media key in return. +5. Upload + - The web portal uploads the encrypted media file to the server, assigning it a device ID of 0. +6. Synchronization + - Finally, the application requests all memories with a device ID lower than its current one (the device ID increments after a backup restoration). diff --git a/rust/src/keys/backup_password_keys.rs b/rust/src/keys/backup_password_keys.rs new file mode 100644 index 00000000..81612f05 --- /dev/null +++ b/rust/src/keys/backup_password_keys.rs @@ -0,0 +1,38 @@ +use crate::error::Result; +use scrypt::{scrypt, Params}; +use serde::{Deserialize, Serialize}; +use zeroize::{Zeroize, ZeroizeOnDrop}; + +#[derive(Debug, Clone, PartialEq, Zeroize, ZeroizeOnDrop, Serialize, Deserialize)] +pub struct BackupPasswordKeys { + pub backup_id: [u8; 32], + pub encryption_key: [u8; 32], +} + +impl BackupPasswordKeys { + pub(crate) fn new(backup_id: [u8; 32], encryption_key: [u8; 32]) -> Self { + Self { + backup_id, + encryption_key, + } + } + + pub(crate) fn from_password(password: &str, user_id: i64) -> Result { + let params = Params::new(16, 8, 1)?; + let mut output = [0u8; 64]; + + scrypt( + password.as_bytes(), + &user_id.to_be_bytes(), + ¶ms, + &mut output, + )?; + + let mut backup_id = [0u8; 32]; + let mut encryption_key = [0u8; 32]; + backup_id.copy_from_slice(&output[0..32]); + encryption_key.copy_from_slice(&output[32..64]); + + Ok(Self::new(backup_id, encryption_key)) + } +} diff --git a/rust/src/keys/identity_key/mod.rs b/rust/src/keys/identity_key/mod.rs new file mode 100644 index 00000000..06cfb6c0 --- /dev/null +++ b/rust/src/keys/identity_key/mod.rs @@ -0,0 +1 @@ +pub(crate) mod signal_identity_key; diff --git a/rust/src/keys/identity_key/signal_identity_key.rs b/rust/src/keys/identity_key/signal_identity_key.rs new file mode 100644 index 00000000..d3b0ae7b --- /dev/null +++ b/rust/src/keys/identity_key/signal_identity_key.rs @@ -0,0 +1,33 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; +use zeroize::{Zeroize, ZeroizeOnDrop}; + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub(crate) struct SignalIdentityKey { + // https://github.com/MixinNetwork/libsignal_protocol_dart/blob/c95a1586057022acdbb9c76b1692d94cc549bcc7/protobuf/LocalStorageProtocol.proto#L85 + pub(crate) identity_key_pair_structure: Vec, + pub(crate) registration_id: i64, + pub(crate) pre_key_store: HashMap>, +} + +impl SignalIdentityKey {} + +impl Zeroize for SignalIdentityKey { + fn zeroize(&mut self) { + self.identity_key_pair_structure.zeroize(); + self.registration_id.zeroize(); + for value in self.pre_key_store.values_mut() { + value.zeroize(); + } + self.pre_key_store.clear(); + } +} + +impl Drop for SignalIdentityKey { + fn drop(&mut self) { + self.zeroize(); + } +} + +impl ZeroizeOnDrop for SignalIdentityKey {} diff --git a/rust/src/keys/main_key.rs b/rust/src/keys/main_key.rs new file mode 100644 index 00000000..0612a127 --- /dev/null +++ b/rust/src/keys/main_key.rs @@ -0,0 +1,233 @@ +use crate::error::Result; +use aes_gcm::aead::rand_core::RngCore; +use aes_gcm::aead::{Aead, AeadCore, KeyInit, OsRng}; +use aes_gcm::{Aes256Gcm, Key, Nonce}; +use hkdf::Hkdf; +use serde::{Deserialize, Serialize}; +use sha2::Sha256; +use zeroize::{Zeroize, ZeroizeOnDrop}; + +/// `MainKey` is responsible for handling the cryptographically secure, immutable master key. +/// It uses HKDF to derive subordinate keys (Authentication Token, Backup Key, Media Main Key). +#[derive(Debug, PartialEq, Zeroize, ZeroizeOnDrop, Serialize, Deserialize)] +pub struct MainKey { + /// The 32-byte main master key + main_key: [u8; 32], +} + +#[derive(Debug)] +pub(crate) enum DatabaseKey { + RustDb, +} + +impl MainKey { + /// Generates a new cryptographically secure MainKey. + pub fn generate() -> Self { + let mut main_key = [0u8; 32]; + OsRng.fill_bytes(&mut main_key); + Self { main_key } + } + + /// Download token required to download a backup. + /// This ensures that the user who tries to download the backup must have knowledge over the + /// main key + pub fn get_backup_download_token(&self) -> [u8; 32] { + self.derive_key(b"backup_download_token") + } + + /// Uses as a password to authenitcate agains the server + pub fn get_login_token(&self) -> [u8; 32] { + self.derive_key(b"server_auth_token") + } + + /// Derives the database encryption key. + pub(crate) fn get_database_key(&self, db: DatabaseKey) -> String { + let db_name = match db { + DatabaseKey::RustDb => b"rust_db", + }; + let info = [b"database_key_", db_name as &[u8]].concat(); + let key = self.derive_key(&info); + hex::encode(key) + } + + /// Encrypts a backup payload. + /// The backup key is derived using HKDF from the main key. + pub fn encrypt_backup(&self, backup_payload: &[u8]) -> Vec { + self.encrypt_with_info(b"backup_key", backup_payload) + } + + /// Decrypts a backup payload. + pub fn decrypt_backup(&self, encrypted_backup: &[u8]) -> Result> { + self.decrypt_with_info(b"backup_key", encrypted_backup) + } + + /// Encrypts a newly generated media key using the derived Media Main Key. + // pub fn encrypt_media_key(&self, media_key: &[u8; 32]) -> Vec { + // self.encrypt_with_info(b"media_main_key", media_key) + // } + + /// Decrypts a wrapped media key using the derived Media Main Key. + // pub fn decrypt_media_key(&self, wrapped_media_key: &[u8]) -> Result<[u8; 32]> { + // let decrypted = self.decrypt_with_info(b"media_main_key", wrapped_media_key)?; + + // if decrypted.len() != 32 { + // return Err("Invalid decrypted key length".to_string())?; + // } + + // let mut result = [0u8; 32]; + // result.copy_from_slice(&decrypted); + // Ok(result) + // } + + fn derive_key(&self, info: &[u8]) -> [u8; 32] { + let hk = Hkdf::::new(None, &self.main_key); + let mut okm = [0u8; 32]; + hk.expand(info, &mut okm).expect("HKDF expand failed"); + okm + } + + fn encrypt_with_info(&self, info: &[u8], payload: &[u8]) -> Vec { + let derived_key = self.derive_key(info); + let key = Key::::from_slice(&derived_key); + let cipher = Aes256Gcm::new(key); + let nonce = Aes256Gcm::generate_nonce(&mut OsRng); + let ciphertext = cipher + .encrypt(&nonce, payload) + .expect("encryption failure!"); + + let mut result = nonce.to_vec(); + result.extend_from_slice(&ciphertext); + result + } + + fn decrypt_with_info(&self, info: &[u8], encrypted_data: &[u8]) -> Result> { + if encrypted_data.len() < 12 { + return Err("Invalid encrypted data length".to_string())?; + } + + let derived_key = self.derive_key(info); + let key = Key::::from_slice(&derived_key); + let cipher = Aes256Gcm::new(key); + let nonce = Nonce::from_slice(&encrypted_data[..12]); + let ciphertext = &encrypted_data[12..]; + + Ok(cipher + .decrypt(nonce, ciphertext) + .map_err(|_| "Decryption failure".to_string())?) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_backup_encryption_decryption_success() { + let km = MainKey::generate(); + let payload = b"this is a secret backup payload"; + + let encrypted = km.encrypt_backup(payload); + let decrypted = km.decrypt_backup(&encrypted).unwrap(); + + assert_eq!(payload.as_slice(), decrypted.as_slice()); + } + + #[test] + fn test_backup_decryption_tampered_payload_fails() { + let km = MainKey::generate(); + let payload = b"this is a secret backup payload"; + let mut encrypted = km.encrypt_backup(payload); + + // Tamper with the ciphertext (assuming length > 12) + let last_idx = encrypted.len() - 1; + encrypted[last_idx] ^= 1; // Flip a bit + + let result = km.decrypt_backup(&encrypted); + assert!(result.is_err()); + assert_eq!(result.unwrap_err().to_string(), "Decryption failure"); + } + + #[test] + fn test_backup_decryption_too_short_fails() { + let km = MainKey::generate(); + let short_payload = vec![0u8; 10]; // Less than 12 bytes nonce + + let result = km.decrypt_backup(&short_payload); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().to_string(), + "Invalid encrypted data length" + ); + } + + // #[test] + // fn test_media_key_encryption_decryption_success() { + // let km = MainKey::generate(); + // let mut media_key = [0u8; 32]; + // OsRng.fill_bytes(&mut media_key); + + // let encrypted = km.encrypt_media_key(&media_key); + // let decrypted = km.decrypt_media_key(&encrypted).unwrap(); + + // assert_eq!(media_key, decrypted); + // } + + // #[test] + // fn test_media_key_decryption_tampered_payload_fails() { + // let km = MainKey::generate(); + // let mut media_key = [0u8; 32]; + // OsRng.fill_bytes(&mut media_key); + + // let mut encrypted = km.encrypt_media_key(&media_key); + + // // Tamper with the ciphertext + // let last_idx = encrypted.len() - 1; + // encrypted[last_idx] ^= 1; + + // let result = km.decrypt_media_key(&encrypted); + // assert!(result.is_err()); + // assert_eq!(result.unwrap_err().to_string(), "Decryption failure"); + // } + + // #[test] + // fn test_media_key_decryption_too_short_fails() { + // let km = MainKey::generate(); + // let short_payload = vec![0u8; 10]; // Less than 12 bytes nonce + + // let result = km.decrypt_media_key(&short_payload); + // assert!(result.is_err()); + // assert_eq!( + // result.unwrap_err().to_string(), + // "Invalid encrypted data length" + // ); + // } + + // #[test] + // fn test_media_key_decryption_wrong_decrypted_length_fails() { + // let km = MainKey::generate(); + + // // Manually encrypt a 31 byte payload + // let hk = Hkdf::::new(None, &km.main_key); + // let mut media_main_key = [0u8; 32]; + // hk.expand(b"media_main_key", &mut media_main_key) + // .expect("HKDF expand failed"); + + // let key = Key::::from_slice(&media_main_key); + // let cipher = Aes256Gcm::new(key); + // let nonce = Aes256Gcm::generate_nonce(&mut OsRng); + // let payload = vec![0u8; 31]; + // let ciphertext = cipher + // .encrypt(&nonce, payload.as_ref()) + // .expect("encryption failure"); + + // let mut encrypted = nonce.to_vec(); + // encrypted.extend_from_slice(&ciphertext); + + // let result = km.decrypt_media_key(&encrypted); + // assert!(result.is_err()); + // assert_eq!( + // result.unwrap_err().to_string(), + // "Invalid decrypted key length" + // ); + // } +} diff --git a/rust/src/keys/mod.rs b/rust/src/keys/mod.rs new file mode 100644 index 00000000..2fcfedc1 --- /dev/null +++ b/rust/src/keys/mod.rs @@ -0,0 +1,62 @@ +pub(crate) mod backup_password_keys; +mod identity_key; +mod main_key; + +use crate::error::Result; +use crate::error::TwonlyError; +pub(crate) use crate::keys::backup_password_keys::BackupPasswordKeys; +pub(crate) use crate::keys::identity_key::signal_identity_key::SignalIdentityKey; +pub(crate) use crate::keys::main_key::{DatabaseKey, MainKey}; +use crate::secure_storage::SecureStorage; +use serde::{Deserialize, Serialize}; +use zeroize::{Zeroize, ZeroizeOnDrop}; + +const KEY_MANAGER_ID: &str = "twonly_key_manager"; + +#[derive(Debug, PartialEq, Zeroize, ZeroizeOnDrop, Serialize, Deserialize)] +pub(crate) struct KeyManager { + pub(crate) user_id: Option, + pub(crate) main_key: MainKey, + pub(crate) signal_identity: Option, + pub(crate) backup_password: Option, +} + +impl KeyManager { + pub fn generate() -> Result { + Ok(KeyManager { + main_key: MainKey::generate(), + signal_identity: None, + backup_password: None, + user_id: None, + }) + } + + /// Tries to load the KeyManager from the secure keychain/local storage. + pub fn try_from_keychain(storage: &SecureStorage) -> Result { + let hex_key = storage + .read(KEY_MANAGER_ID)? + .ok_or_else(|| TwonlyError::MissingMainKey)?; + + let bytes = hex::decode(hex_key)?; + + let main_key: KeyManager = postcard::from_bytes(&bytes)?; + + Ok(main_key) + } + + /// Stores the main key into the secure keychain/local storage. + pub fn store_to_keychain(&self, storage: &SecureStorage) -> Result<()> { + let serialized = postcard::to_allocvec(self)?; + + let hex_key = hex::encode(serialized); + storage.write(KEY_MANAGER_ID, &hex_key)?; + + Ok(()) + } + + /// Removes the KeyManager from the secure keychain/local storage. + pub fn remove_from_keychain(storage: &SecureStorage) -> Result<()> { + storage.delete(KEY_MANAGER_ID)?; + Ok(()) + } +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 39c49fb0..255e19a3 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -1,5 +1,11 @@ +mod backup; pub mod bridge; +mod context; mod database; +mod error; mod frb_generated; +mod keys; +mod log; +mod secure_storage; mod standalone; mod utils; diff --git a/rust/src/bridge/log.rs b/rust/src/log.rs similarity index 97% rename from rust/src/bridge/log.rs rename to rust/src/log.rs index fb41ffd6..884aa04f 100644 --- a/rust/src/bridge/log.rs +++ b/rust/src/log.rs @@ -68,7 +68,7 @@ fn build_writers(logs_dir: &std::path::Path) -> (NonBlocking, NonBlocking) { } Err(e) => { eprintln!("Failed to create file appender: {}", e); - let (nb, guard) = tracing_appender::non_blocking(std::io::sink()); + let (nb, _guard) = tracing_appender::non_blocking(std::io::sink()); (nb, None) } }; diff --git a/rust/src/secure_storage.rs b/rust/src/secure_storage.rs new file mode 100644 index 00000000..0d44de4a --- /dev/null +++ b/rust/src/secure_storage.rs @@ -0,0 +1,151 @@ +use keyring_core::{Entry, Error as KeyringError}; + +/// A simple wrapper around `keyring-core` for secure storage on iOS, Android, and other platforms. +/// +/// IMPORTANT: This struct assumes that a `keyring-core` default store has been initialized +/// (e.g., via `keyring_core::set_default_store`). In the White Noise project, this is handled +/// during application startup in `Whitenoise::initialize_keyring_store`. +pub struct SecureStorage { + service_name: String, +} + +impl SecureStorage { + /// Creates a new `SecureStorage` instance with the specified service name. + /// The service name is used as a namespace in the system keyring. + pub fn new(service_name: &str) -> Self { + Self { + service_name: service_name.to_string(), + } + } + + /// Initializes the platform-native secure storage backend for iOS and Android. + /// + /// # Arguments + /// * `group_id` - (iOS only) Optional App Group ID to allow cross-process keychain access. + /// + /// This function registers the appropriate credential store (Protected Store for iOS, + /// Keystore for Android) with `keyring-core`. It is safe to call multiple times. + pub fn init() -> Result<(), String> { + if keyring_core::get_default_store().is_some() { + return Ok(()); + } + + #[cfg(target_os = "ios")] + { + use std::collections::HashMap; + let group = "CN332ZUGRP.eu.twonly.shared"; + let mut config = HashMap::new(); + config.insert("access-group", group); + let store = + apple_native_keyring_store::protected::Store::new_with_configuration(&config) + .map_err(|e| format!("Failed to init iOS Protected Store: {}", e))?; + keyring_core::set_default_store(store); + } + + #[cfg(target_os = "android")] + { + let store = android_native_keyring_store::Store::new() + .map_err(|e| format!("Failed to init Android Store: {}", e))?; + keyring_core::set_default_store(store); + } + + #[cfg(not(any(target_os = "ios", target_os = "android")))] + { + let store = keyring_core::mock::Store::new() + .map_err(|e| format!("Failed to init Mock Store: {}", e))?; + keyring_core::set_default_store(store); + tracing::warn!("Using mock store as default keyring store!"); + } + + Ok(()) + } + + /// Writes a secret value to the secure keyring associated with the given key. + /// + /// # Arguments + /// * `key` - The identifier (account name) for the secret. + /// * `value` - The secret string to store. + pub fn write(&self, key: &str, value: &str) -> Result<(), String> { + let entry = self.get_entry(key)?; + + entry + .set_password(value) + .map_err(|e| format!("Failed to write secret to keyring: {}", e))?; + + Ok(()) + } + + /// Reads a secret value from the secure keyring associated with the given key. + /// + /// Returns `Ok(Some(String))` if the key exists, `Ok(None)` if it doesn't, + /// or an `Err` if a system error occurs. + pub fn read(&self, key: &str) -> Result, String> { + let entry = self.get_entry(key)?; + + match entry.get_password() { + Ok(password) => Ok(Some(password)), + Err(KeyringError::NoEntry) => Ok(None), + Err(e) => Err(format!("Failed to read secret from keyring: {}", e)), + } + } + + /// Deletes the secret associated with the given key from the secure keyring. + /// + /// If the key does not exist, this function returns `Ok(())` (idempotent). + pub fn delete(&self, key: &str) -> Result<(), String> { + let entry = self.get_entry(key)?; + + match entry.delete_credential() { + Ok(()) => Ok(()), + Err(KeyringError::NoEntry) => Ok(()), + Err(e) => Err(format!("Failed to delete secret from keyring: {}", e)), + } + } + + /// Helper to create a keyring entry with the appropriate platform modifiers. + fn get_entry(&self, key: &str) -> Result { + #[cfg(target_os = "ios")] + { + use std::collections::HashMap; + let mut modifiers = HashMap::new(); + modifiers.insert("access-policy", "AfterFirstUnlock"); + Entry::new_with_modifiers(&self.service_name, key, &modifiers) + .map_err(|e| format!("Failed to create keyring entry with modifiers: {}", e)) + } + + #[cfg(not(target_os = "ios"))] + { + Entry::new(&self.service_name, key) + .map_err(|e| format!("Failed to create keyring entry: {}", e)) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_secure_storage_flow() { + // Initialize the store (will use MockStore on non-mobile platforms) + SecureStorage::init().unwrap(); + + let storage = SecureStorage::new("eu.twonly.test"); + let key = "test_secret_key"; + let secret = "my_awesome_secret_123"; + + // 1. Write the secret + storage.write(key, secret).expect("Failed to write secret"); + + // 2. Read the secret and verify it matches + let read_val = storage.read(key).expect("Failed to read secret"); + assert_eq!(read_val, Some(secret.to_string())); + + // 3. Delete the secret + storage.delete(key).expect("Failed to delete secret"); + + // 4. Verify the secret is gone + let after_delete = storage.read(key).expect("Failed to read after delete"); + assert_eq!(after_delete, None); + } +} diff --git a/rust/src/standalone.rs b/rust/src/standalone.rs new file mode 100644 index 00000000..ee5479ba --- /dev/null +++ b/rust/src/standalone.rs @@ -0,0 +1,17 @@ +use tokio::sync::Mutex; + +use crate::bridge::InitConfig; +use crate::database::Database; +use crate::keys::KeyManager; +use crate::secure_storage::SecureStorage; +use std::sync::Arc; + +pub(crate) struct TwonlyStandalone { + #[allow(dead_code)] + pub(crate) config: InitConfig, + #[allow(dead_code)] + pub(crate) rust_db: Arc, + #[allow(dead_code)] + pub(crate) secure_storage: SecureStorage, + pub(crate) key_manager: Arc>, +} diff --git a/rust/src/standalone/mod.rs b/rust/src/standalone/mod.rs deleted file mode 100644 index c5ece6b4..00000000 --- a/rust/src/standalone/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -// use crate::{bridge::TwonlyConfig, database::Database}; -// use std::sync::Arc; - -// pub(crate) struct TwonlyStandalone { -// #[allow(dead_code)] -// pub(crate) config: TwonlyConfig, -// /// Because Rust is called from a different process it is safe to write to the twonly_db. -// pub(crate) twonly_db: Arc, -// } diff --git a/test/drift/twonly_db/generated/schema.dart b/test/drift/twonly_db/generated/schema.dart index 49f49d7b..ba56c197 100644 --- a/test/drift/twonly_db/generated/schema.dart +++ b/test/drift/twonly_db/generated/schema.dart @@ -16,6 +16,7 @@ import 'schema_v9.dart' as v9; import 'schema_v10.dart' as v10; import 'schema_v11.dart' as v11; import 'schema_v12.dart' as v12; +import 'schema_v13.dart' as v13; class GeneratedHelper implements SchemaInstantiationHelper { @override @@ -45,10 +46,12 @@ class GeneratedHelper implements SchemaInstantiationHelper { return v11.DatabaseAtV11(db); case 12: return v12.DatabaseAtV12(db); + case 13: + return v13.DatabaseAtV13(db); default: throw MissingSchemaException(version, versions); } } - static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; + static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]; } diff --git a/test/drift/twonly_db/generated/schema_v13.dart b/test/drift/twonly_db/generated/schema_v13.dart index 4f14bd6f..42a936b7 100644 --- a/test/drift/twonly_db/generated/schema_v13.dart +++ b/test/drift/twonly_db/generated/schema_v13.dart @@ -135,6 +135,27 @@ class Contacts extends Table with TableInfo { requiredDuringInsert: false, $customConstraints: 'NULL', ); + late final GeneratedColumn userDiscoveryExcluded = GeneratedColumn( + 'user_discovery_excluded', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT 0 CHECK (user_discovery_excluded IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn userDiscoveryManualApproved = + GeneratedColumn( + 'user_discovery_manual_approved', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NULL DEFAULT 0 CHECK (user_discovery_manual_approved IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); late final GeneratedColumn mediaSendCounter = GeneratedColumn( 'media_send_counter', aliasedName, @@ -169,6 +190,8 @@ class Contacts extends Table with TableInfo { accountDeleted, createdAt, userDiscoveryVersion, + userDiscoveryExcluded, + userDiscoveryManualApproved, mediaSendCounter, mediaReceivedCounter, ]; @@ -239,6 +262,14 @@ class Contacts extends Table with TableInfo { DriftSqlType.blob, data['${effectivePrefix}user_discovery_version'], ), + userDiscoveryExcluded: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}user_discovery_excluded'], + )!, + userDiscoveryManualApproved: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}user_discovery_manual_approved'], + ), mediaSendCounter: attachedDatabase.typeMapping.read( DriftSqlType.int, data['${effectivePrefix}media_send_counter'], @@ -276,6 +307,8 @@ class ContactsData extends DataClass implements Insertable { final int accountDeleted; final int createdAt; final i2.Uint8List? userDiscoveryVersion; + final int userDiscoveryExcluded; + final int? userDiscoveryManualApproved; final int mediaSendCounter; final int mediaReceivedCounter; const ContactsData({ @@ -293,6 +326,8 @@ class ContactsData extends DataClass implements Insertable { required this.accountDeleted, required this.createdAt, this.userDiscoveryVersion, + required this.userDiscoveryExcluded, + this.userDiscoveryManualApproved, required this.mediaSendCounter, required this.mediaReceivedCounter, }); @@ -325,6 +360,12 @@ class ContactsData extends DataClass implements Insertable { userDiscoveryVersion, ); } + map['user_discovery_excluded'] = Variable(userDiscoveryExcluded); + if (!nullToAbsent || userDiscoveryManualApproved != null) { + map['user_discovery_manual_approved'] = Variable( + userDiscoveryManualApproved, + ); + } map['media_send_counter'] = Variable(mediaSendCounter); map['media_received_counter'] = Variable(mediaReceivedCounter); return map; @@ -354,6 +395,11 @@ class ContactsData extends DataClass implements Insertable { userDiscoveryVersion: userDiscoveryVersion == null && nullToAbsent ? const Value.absent() : Value(userDiscoveryVersion), + userDiscoveryExcluded: Value(userDiscoveryExcluded), + userDiscoveryManualApproved: + userDiscoveryManualApproved == null && nullToAbsent + ? const Value.absent() + : Value(userDiscoveryManualApproved), mediaSendCounter: Value(mediaSendCounter), mediaReceivedCounter: Value(mediaReceivedCounter), ); @@ -385,6 +431,12 @@ class ContactsData extends DataClass implements Insertable { userDiscoveryVersion: serializer.fromJson( json['userDiscoveryVersion'], ), + userDiscoveryExcluded: serializer.fromJson( + json['userDiscoveryExcluded'], + ), + userDiscoveryManualApproved: serializer.fromJson( + json['userDiscoveryManualApproved'], + ), mediaSendCounter: serializer.fromJson(json['mediaSendCounter']), mediaReceivedCounter: serializer.fromJson( json['mediaReceivedCounter'], @@ -413,6 +465,10 @@ class ContactsData extends DataClass implements Insertable { 'userDiscoveryVersion': serializer.toJson( userDiscoveryVersion, ), + 'userDiscoveryExcluded': serializer.toJson(userDiscoveryExcluded), + 'userDiscoveryManualApproved': serializer.toJson( + userDiscoveryManualApproved, + ), 'mediaSendCounter': serializer.toJson(mediaSendCounter), 'mediaReceivedCounter': serializer.toJson(mediaReceivedCounter), }; @@ -433,6 +489,8 @@ class ContactsData extends DataClass implements Insertable { int? accountDeleted, int? createdAt, Value userDiscoveryVersion = const Value.absent(), + int? userDiscoveryExcluded, + Value userDiscoveryManualApproved = const Value.absent(), int? mediaSendCounter, int? mediaReceivedCounter, }) => ContactsData( @@ -454,6 +512,10 @@ class ContactsData extends DataClass implements Insertable { userDiscoveryVersion: userDiscoveryVersion.present ? userDiscoveryVersion.value : this.userDiscoveryVersion, + userDiscoveryExcluded: userDiscoveryExcluded ?? this.userDiscoveryExcluded, + userDiscoveryManualApproved: userDiscoveryManualApproved.present + ? userDiscoveryManualApproved.value + : this.userDiscoveryManualApproved, mediaSendCounter: mediaSendCounter ?? this.mediaSendCounter, mediaReceivedCounter: mediaReceivedCounter ?? this.mediaReceivedCounter, ); @@ -485,6 +547,12 @@ class ContactsData extends DataClass implements Insertable { userDiscoveryVersion: data.userDiscoveryVersion.present ? data.userDiscoveryVersion.value : this.userDiscoveryVersion, + userDiscoveryExcluded: data.userDiscoveryExcluded.present + ? data.userDiscoveryExcluded.value + : this.userDiscoveryExcluded, + userDiscoveryManualApproved: data.userDiscoveryManualApproved.present + ? data.userDiscoveryManualApproved.value + : this.userDiscoveryManualApproved, mediaSendCounter: data.mediaSendCounter.present ? data.mediaSendCounter.value : this.mediaSendCounter, @@ -511,6 +579,8 @@ class ContactsData extends DataClass implements Insertable { ..write('accountDeleted: $accountDeleted, ') ..write('createdAt: $createdAt, ') ..write('userDiscoveryVersion: $userDiscoveryVersion, ') + ..write('userDiscoveryExcluded: $userDiscoveryExcluded, ') + ..write('userDiscoveryManualApproved: $userDiscoveryManualApproved, ') ..write('mediaSendCounter: $mediaSendCounter, ') ..write('mediaReceivedCounter: $mediaReceivedCounter') ..write(')')) @@ -533,6 +603,8 @@ class ContactsData extends DataClass implements Insertable { accountDeleted, createdAt, $driftBlobEquality.hash(userDiscoveryVersion), + userDiscoveryExcluded, + userDiscoveryManualApproved, mediaSendCounter, mediaReceivedCounter, ); @@ -560,6 +632,9 @@ class ContactsData extends DataClass implements Insertable { other.userDiscoveryVersion, this.userDiscoveryVersion, ) && + other.userDiscoveryExcluded == this.userDiscoveryExcluded && + other.userDiscoveryManualApproved == + this.userDiscoveryManualApproved && other.mediaSendCounter == this.mediaSendCounter && other.mediaReceivedCounter == this.mediaReceivedCounter); } @@ -579,6 +654,8 @@ class ContactsCompanion extends UpdateCompanion { final Value accountDeleted; final Value createdAt; final Value userDiscoveryVersion; + final Value userDiscoveryExcluded; + final Value userDiscoveryManualApproved; final Value mediaSendCounter; final Value mediaReceivedCounter; const ContactsCompanion({ @@ -596,6 +673,8 @@ class ContactsCompanion extends UpdateCompanion { this.accountDeleted = const Value.absent(), this.createdAt = const Value.absent(), this.userDiscoveryVersion = const Value.absent(), + this.userDiscoveryExcluded = const Value.absent(), + this.userDiscoveryManualApproved = const Value.absent(), this.mediaSendCounter = const Value.absent(), this.mediaReceivedCounter = const Value.absent(), }); @@ -614,6 +693,8 @@ class ContactsCompanion extends UpdateCompanion { this.accountDeleted = const Value.absent(), this.createdAt = const Value.absent(), this.userDiscoveryVersion = const Value.absent(), + this.userDiscoveryExcluded = const Value.absent(), + this.userDiscoveryManualApproved = const Value.absent(), this.mediaSendCounter = const Value.absent(), this.mediaReceivedCounter = const Value.absent(), }) : username = Value(username); @@ -632,6 +713,8 @@ class ContactsCompanion extends UpdateCompanion { Expression? accountDeleted, Expression? createdAt, Expression? userDiscoveryVersion, + Expression? userDiscoveryExcluded, + Expression? userDiscoveryManualApproved, Expression? mediaSendCounter, Expression? mediaReceivedCounter, }) { @@ -653,6 +736,10 @@ class ContactsCompanion extends UpdateCompanion { if (createdAt != null) 'created_at': createdAt, if (userDiscoveryVersion != null) 'user_discovery_version': userDiscoveryVersion, + if (userDiscoveryExcluded != null) + 'user_discovery_excluded': userDiscoveryExcluded, + if (userDiscoveryManualApproved != null) + 'user_discovery_manual_approved': userDiscoveryManualApproved, if (mediaSendCounter != null) 'media_send_counter': mediaSendCounter, if (mediaReceivedCounter != null) 'media_received_counter': mediaReceivedCounter, @@ -674,6 +761,8 @@ class ContactsCompanion extends UpdateCompanion { Value? accountDeleted, Value? createdAt, Value? userDiscoveryVersion, + Value? userDiscoveryExcluded, + Value? userDiscoveryManualApproved, Value? mediaSendCounter, Value? mediaReceivedCounter, }) { @@ -692,6 +781,10 @@ class ContactsCompanion extends UpdateCompanion { accountDeleted: accountDeleted ?? this.accountDeleted, createdAt: createdAt ?? this.createdAt, userDiscoveryVersion: userDiscoveryVersion ?? this.userDiscoveryVersion, + userDiscoveryExcluded: + userDiscoveryExcluded ?? this.userDiscoveryExcluded, + userDiscoveryManualApproved: + userDiscoveryManualApproved ?? this.userDiscoveryManualApproved, mediaSendCounter: mediaSendCounter ?? this.mediaSendCounter, mediaReceivedCounter: mediaReceivedCounter ?? this.mediaReceivedCounter, ); @@ -746,6 +839,16 @@ class ContactsCompanion extends UpdateCompanion { userDiscoveryVersion.value, ); } + if (userDiscoveryExcluded.present) { + map['user_discovery_excluded'] = Variable( + userDiscoveryExcluded.value, + ); + } + if (userDiscoveryManualApproved.present) { + map['user_discovery_manual_approved'] = Variable( + userDiscoveryManualApproved.value, + ); + } if (mediaSendCounter.present) { map['media_send_counter'] = Variable(mediaSendCounter.value); } @@ -772,6 +875,8 @@ class ContactsCompanion extends UpdateCompanion { ..write('accountDeleted: $accountDeleted, ') ..write('createdAt: $createdAt, ') ..write('userDiscoveryVersion: $userDiscoveryVersion, ') + ..write('userDiscoveryExcluded: $userDiscoveryExcluded, ') + ..write('userDiscoveryManualApproved: $userDiscoveryManualApproved, ') ..write('mediaSendCounter: $mediaSendCounter, ') ..write('mediaReceivedCounter: $mediaReceivedCounter') ..write(')')) @@ -7416,12 +7521,21 @@ class KeyVerifications extends Table final GeneratedDatabase attachedDatabase; final String? _alias; KeyVerifications(this.attachedDatabase, [this._alias]); + late final GeneratedColumn verificationId = GeneratedColumn( + 'verification_id', + aliasedName, + false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL PRIMARY KEY AUTOINCREMENT', + ); late final GeneratedColumn contactId = GeneratedColumn( 'contact_id', aliasedName, false, type: DriftSqlType.int, - requiredDuringInsert: false, + requiredDuringInsert: true, $customConstraints: 'NOT NULL REFERENCES contacts(user_id)ON DELETE CASCADE', ); @@ -7446,18 +7560,27 @@ class KeyVerifications extends Table ), ); @override - List get $columns => [contactId, type, createdAt]; + List get $columns => [ + verificationId, + contactId, + type, + createdAt, + ]; @override String get aliasedName => _alias ?? actualTableName; @override String get actualTableName => $name; static const String $name = 'key_verifications'; @override - Set get $primaryKey => {contactId}; + Set get $primaryKey => {verificationId}; @override KeyVerificationsData map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return KeyVerificationsData( + verificationId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}verification_id'], + )!, contactId: attachedDatabase.typeMapping.read( DriftSqlType.int, data['${effectivePrefix}contact_id'], @@ -7478,18 +7601,18 @@ class KeyVerifications extends Table return KeyVerifications(attachedDatabase, alias); } - @override - List get customConstraints => const ['PRIMARY KEY(contact_id)']; @override bool get dontWriteConstraints => true; } class KeyVerificationsData extends DataClass implements Insertable { + final int verificationId; final int contactId; final String type; final int createdAt; const KeyVerificationsData({ + required this.verificationId, required this.contactId, required this.type, required this.createdAt, @@ -7497,6 +7620,7 @@ class KeyVerificationsData extends DataClass @override Map toColumns(bool nullToAbsent) { final map = {}; + map['verification_id'] = Variable(verificationId); map['contact_id'] = Variable(contactId); map['type'] = Variable(type); map['created_at'] = Variable(createdAt); @@ -7505,6 +7629,7 @@ class KeyVerificationsData extends DataClass KeyVerificationsCompanion toCompanion(bool nullToAbsent) { return KeyVerificationsCompanion( + verificationId: Value(verificationId), contactId: Value(contactId), type: Value(type), createdAt: Value(createdAt), @@ -7517,6 +7642,7 @@ class KeyVerificationsData extends DataClass }) { serializer ??= driftRuntimeOptions.defaultSerializer; return KeyVerificationsData( + verificationId: serializer.fromJson(json['verificationId']), contactId: serializer.fromJson(json['contactId']), type: serializer.fromJson(json['type']), createdAt: serializer.fromJson(json['createdAt']), @@ -7526,6 +7652,7 @@ class KeyVerificationsData extends DataClass Map toJson({ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; return { + 'verificationId': serializer.toJson(verificationId), 'contactId': serializer.toJson(contactId), 'type': serializer.toJson(type), 'createdAt': serializer.toJson(createdAt), @@ -7533,16 +7660,21 @@ class KeyVerificationsData extends DataClass } KeyVerificationsData copyWith({ + int? verificationId, int? contactId, String? type, int? createdAt, }) => KeyVerificationsData( + verificationId: verificationId ?? this.verificationId, contactId: contactId ?? this.contactId, type: type ?? this.type, createdAt: createdAt ?? this.createdAt, ); KeyVerificationsData copyWithCompanion(KeyVerificationsCompanion data) { return KeyVerificationsData( + verificationId: data.verificationId.present + ? data.verificationId.value + : this.verificationId, contactId: data.contactId.present ? data.contactId.value : this.contactId, type: data.type.present ? data.type.value : this.type, createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, @@ -7552,6 +7684,7 @@ class KeyVerificationsData extends DataClass @override String toString() { return (StringBuffer('KeyVerificationsData(') + ..write('verificationId: $verificationId, ') ..write('contactId: $contactId, ') ..write('type: $type, ') ..write('createdAt: $createdAt') @@ -7560,36 +7693,43 @@ class KeyVerificationsData extends DataClass } @override - int get hashCode => Object.hash(contactId, type, createdAt); + int get hashCode => Object.hash(verificationId, contactId, type, createdAt); @override bool operator ==(Object other) => identical(this, other) || (other is KeyVerificationsData && + other.verificationId == this.verificationId && other.contactId == this.contactId && other.type == this.type && other.createdAt == this.createdAt); } class KeyVerificationsCompanion extends UpdateCompanion { + final Value verificationId; final Value contactId; final Value type; final Value createdAt; const KeyVerificationsCompanion({ + this.verificationId = const Value.absent(), this.contactId = const Value.absent(), this.type = const Value.absent(), this.createdAt = const Value.absent(), }); KeyVerificationsCompanion.insert({ - this.contactId = const Value.absent(), + this.verificationId = const Value.absent(), + required int contactId, required String type, this.createdAt = const Value.absent(), - }) : type = Value(type); + }) : contactId = Value(contactId), + type = Value(type); static Insertable custom({ + Expression? verificationId, Expression? contactId, Expression? type, Expression? createdAt, }) { return RawValuesInsertable({ + if (verificationId != null) 'verification_id': verificationId, if (contactId != null) 'contact_id': contactId, if (type != null) 'type': type, if (createdAt != null) 'created_at': createdAt, @@ -7597,11 +7737,13 @@ class KeyVerificationsCompanion extends UpdateCompanion { } KeyVerificationsCompanion copyWith({ + Value? verificationId, Value? contactId, Value? type, Value? createdAt, }) { return KeyVerificationsCompanion( + verificationId: verificationId ?? this.verificationId, contactId: contactId ?? this.contactId, type: type ?? this.type, createdAt: createdAt ?? this.createdAt, @@ -7611,6 +7753,9 @@ class KeyVerificationsCompanion extends UpdateCompanion { @override Map toColumns(bool nullToAbsent) { final map = {}; + if (verificationId.present) { + map['verification_id'] = Variable(verificationId.value); + } if (contactId.present) { map['contact_id'] = Variable(contactId.value); } @@ -7626,6 +7771,7 @@ class KeyVerificationsCompanion extends UpdateCompanion { @override String toString() { return (StringBuffer('KeyVerificationsCompanion(') + ..write('verificationId: $verificationId, ') ..write('contactId: $contactId, ') ..write('type: $type, ') ..write('createdAt: $createdAt') @@ -7894,11 +8040,41 @@ class UserDiscoveryAnnouncedUsers extends Table requiredDuringInsert: true, $customConstraints: 'NOT NULL UNIQUE', ); + late final GeneratedColumn username = GeneratedColumn( + 'username', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn wasShownToTheUser = GeneratedColumn( + 'was_shown_to_the_user', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT 0 CHECK (was_shown_to_the_user IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn isHidden = GeneratedColumn( + 'is_hidden', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_hidden IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); @override List get $columns => [ announcedUserId, announcedPublicKey, publicId, + username, + wasShownToTheUser, + isHidden, ]; @override String get aliasedName => _alias ?? actualTableName; @@ -7926,6 +8102,18 @@ class UserDiscoveryAnnouncedUsers extends Table DriftSqlType.int, data['${effectivePrefix}public_id'], )!, + username: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}username'], + ), + wasShownToTheUser: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}was_shown_to_the_user'], + )!, + isHidden: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}is_hidden'], + )!, ); } @@ -7947,10 +8135,16 @@ class UserDiscoveryAnnouncedUsersData extends DataClass final int announcedUserId; final i2.Uint8List announcedPublicKey; final int publicId; + final String? username; + final int wasShownToTheUser; + final int isHidden; const UserDiscoveryAnnouncedUsersData({ required this.announcedUserId, required this.announcedPublicKey, required this.publicId, + this.username, + required this.wasShownToTheUser, + required this.isHidden, }); @override Map toColumns(bool nullToAbsent) { @@ -7958,6 +8152,11 @@ class UserDiscoveryAnnouncedUsersData extends DataClass map['announced_user_id'] = Variable(announcedUserId); map['announced_public_key'] = Variable(announcedPublicKey); map['public_id'] = Variable(publicId); + if (!nullToAbsent || username != null) { + map['username'] = Variable(username); + } + map['was_shown_to_the_user'] = Variable(wasShownToTheUser); + map['is_hidden'] = Variable(isHidden); return map; } @@ -7966,6 +8165,11 @@ class UserDiscoveryAnnouncedUsersData extends DataClass announcedUserId: Value(announcedUserId), announcedPublicKey: Value(announcedPublicKey), publicId: Value(publicId), + username: username == null && nullToAbsent + ? const Value.absent() + : Value(username), + wasShownToTheUser: Value(wasShownToTheUser), + isHidden: Value(isHidden), ); } @@ -7980,6 +8184,9 @@ class UserDiscoveryAnnouncedUsersData extends DataClass json['announcedPublicKey'], ), publicId: serializer.fromJson(json['publicId']), + username: serializer.fromJson(json['username']), + wasShownToTheUser: serializer.fromJson(json['wasShownToTheUser']), + isHidden: serializer.fromJson(json['isHidden']), ); } @override @@ -7989,6 +8196,9 @@ class UserDiscoveryAnnouncedUsersData extends DataClass 'announcedUserId': serializer.toJson(announcedUserId), 'announcedPublicKey': serializer.toJson(announcedPublicKey), 'publicId': serializer.toJson(publicId), + 'username': serializer.toJson(username), + 'wasShownToTheUser': serializer.toJson(wasShownToTheUser), + 'isHidden': serializer.toJson(isHidden), }; } @@ -7996,10 +8206,16 @@ class UserDiscoveryAnnouncedUsersData extends DataClass int? announcedUserId, i2.Uint8List? announcedPublicKey, int? publicId, + Value username = const Value.absent(), + int? wasShownToTheUser, + int? isHidden, }) => UserDiscoveryAnnouncedUsersData( announcedUserId: announcedUserId ?? this.announcedUserId, announcedPublicKey: announcedPublicKey ?? this.announcedPublicKey, publicId: publicId ?? this.publicId, + username: username.present ? username.value : this.username, + wasShownToTheUser: wasShownToTheUser ?? this.wasShownToTheUser, + isHidden: isHidden ?? this.isHidden, ); UserDiscoveryAnnouncedUsersData copyWithCompanion( UserDiscoveryAnnouncedUsersCompanion data, @@ -8012,6 +8228,11 @@ class UserDiscoveryAnnouncedUsersData extends DataClass ? data.announcedPublicKey.value : this.announcedPublicKey, publicId: data.publicId.present ? data.publicId.value : this.publicId, + username: data.username.present ? data.username.value : this.username, + wasShownToTheUser: data.wasShownToTheUser.present + ? data.wasShownToTheUser.value + : this.wasShownToTheUser, + isHidden: data.isHidden.present ? data.isHidden.value : this.isHidden, ); } @@ -8020,7 +8241,10 @@ class UserDiscoveryAnnouncedUsersData extends DataClass return (StringBuffer('UserDiscoveryAnnouncedUsersData(') ..write('announcedUserId: $announcedUserId, ') ..write('announcedPublicKey: $announcedPublicKey, ') - ..write('publicId: $publicId') + ..write('publicId: $publicId, ') + ..write('username: $username, ') + ..write('wasShownToTheUser: $wasShownToTheUser, ') + ..write('isHidden: $isHidden') ..write(')')) .toString(); } @@ -8030,6 +8254,9 @@ class UserDiscoveryAnnouncedUsersData extends DataClass announcedUserId, $driftBlobEquality.hash(announcedPublicKey), publicId, + username, + wasShownToTheUser, + isHidden, ); @override bool operator ==(Object other) => @@ -8040,7 +8267,10 @@ class UserDiscoveryAnnouncedUsersData extends DataClass other.announcedPublicKey, this.announcedPublicKey, ) && - other.publicId == this.publicId); + other.publicId == this.publicId && + other.username == this.username && + other.wasShownToTheUser == this.wasShownToTheUser && + other.isHidden == this.isHidden); } class UserDiscoveryAnnouncedUsersCompanion @@ -8048,27 +8278,42 @@ class UserDiscoveryAnnouncedUsersCompanion final Value announcedUserId; final Value announcedPublicKey; final Value publicId; + final Value username; + final Value wasShownToTheUser; + final Value isHidden; const UserDiscoveryAnnouncedUsersCompanion({ this.announcedUserId = const Value.absent(), this.announcedPublicKey = const Value.absent(), this.publicId = const Value.absent(), + this.username = const Value.absent(), + this.wasShownToTheUser = const Value.absent(), + this.isHidden = const Value.absent(), }); UserDiscoveryAnnouncedUsersCompanion.insert({ this.announcedUserId = const Value.absent(), required i2.Uint8List announcedPublicKey, required int publicId, + this.username = const Value.absent(), + this.wasShownToTheUser = const Value.absent(), + this.isHidden = const Value.absent(), }) : announcedPublicKey = Value(announcedPublicKey), publicId = Value(publicId); static Insertable custom({ Expression? announcedUserId, Expression? announcedPublicKey, Expression? publicId, + Expression? username, + Expression? wasShownToTheUser, + Expression? isHidden, }) { return RawValuesInsertable({ if (announcedUserId != null) 'announced_user_id': announcedUserId, if (announcedPublicKey != null) 'announced_public_key': announcedPublicKey, if (publicId != null) 'public_id': publicId, + if (username != null) 'username': username, + if (wasShownToTheUser != null) 'was_shown_to_the_user': wasShownToTheUser, + if (isHidden != null) 'is_hidden': isHidden, }); } @@ -8076,11 +8321,17 @@ class UserDiscoveryAnnouncedUsersCompanion Value? announcedUserId, Value? announcedPublicKey, Value? publicId, + Value? username, + Value? wasShownToTheUser, + Value? isHidden, }) { return UserDiscoveryAnnouncedUsersCompanion( announcedUserId: announcedUserId ?? this.announcedUserId, announcedPublicKey: announcedPublicKey ?? this.announcedPublicKey, publicId: publicId ?? this.publicId, + username: username ?? this.username, + wasShownToTheUser: wasShownToTheUser ?? this.wasShownToTheUser, + isHidden: isHidden ?? this.isHidden, ); } @@ -8098,6 +8349,15 @@ class UserDiscoveryAnnouncedUsersCompanion if (publicId.present) { map['public_id'] = Variable(publicId.value); } + if (username.present) { + map['username'] = Variable(username.value); + } + if (wasShownToTheUser.present) { + map['was_shown_to_the_user'] = Variable(wasShownToTheUser.value); + } + if (isHidden.present) { + map['is_hidden'] = Variable(isHidden.value); + } return map; } @@ -8106,7 +8366,10 @@ class UserDiscoveryAnnouncedUsersCompanion return (StringBuffer('UserDiscoveryAnnouncedUsersCompanion(') ..write('announcedUserId: $announcedUserId, ') ..write('announcedPublicKey: $announcedPublicKey, ') - ..write('publicId: $publicId') + ..write('publicId: $publicId, ') + ..write('username: $username, ') + ..write('wasShownToTheUser: $wasShownToTheUser, ') + ..write('isHidden: $isHidden') ..write(')')) .toString(); } @@ -8458,7 +8721,7 @@ class UserDiscoveryOtherPromotions extends Table String get actualTableName => $name; static const String $name = 'user_discovery_other_promotions'; @override - Set get $primaryKey => {fromContactId, promotionId}; + Set get $primaryKey => {fromContactId, publicId}; @override UserDiscoveryOtherPromotionsData map( Map data, { @@ -8500,7 +8763,7 @@ class UserDiscoveryOtherPromotions extends Table @override List get customConstraints => const [ - 'PRIMARY KEY(from_contact_id, promotion_id)', + 'PRIMARY KEY(from_contact_id, public_id)', ]; @override bool get dontWriteConstraints => true; @@ -9235,6 +9498,419 @@ class UserDiscoverySharesCompanion } } +class Shortcuts extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Shortcuts(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL PRIMARY KEY AUTOINCREMENT', + ); + late final GeneratedColumn emoji = GeneratedColumn( + 'emoji', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL UNIQUE', + ); + late final GeneratedColumn usageCounter = GeneratedColumn( + 'usage_counter', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0', + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [id, emoji, usageCounter]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'shortcuts'; + @override + Set get $primaryKey => {id}; + @override + ShortcutsData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ShortcutsData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}id'], + )!, + emoji: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}emoji'], + )!, + usageCounter: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}usage_counter'], + )!, + ); + } + + @override + Shortcuts createAlias(String alias) { + return Shortcuts(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class ShortcutsData extends DataClass implements Insertable { + final int id; + final String emoji; + final int usageCounter; + const ShortcutsData({ + required this.id, + required this.emoji, + required this.usageCounter, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['emoji'] = Variable(emoji); + map['usage_counter'] = Variable(usageCounter); + return map; + } + + ShortcutsCompanion toCompanion(bool nullToAbsent) { + return ShortcutsCompanion( + id: Value(id), + emoji: Value(emoji), + usageCounter: Value(usageCounter), + ); + } + + factory ShortcutsData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ShortcutsData( + id: serializer.fromJson(json['id']), + emoji: serializer.fromJson(json['emoji']), + usageCounter: serializer.fromJson(json['usageCounter']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'emoji': serializer.toJson(emoji), + 'usageCounter': serializer.toJson(usageCounter), + }; + } + + ShortcutsData copyWith({int? id, String? emoji, int? usageCounter}) => + ShortcutsData( + id: id ?? this.id, + emoji: emoji ?? this.emoji, + usageCounter: usageCounter ?? this.usageCounter, + ); + ShortcutsData copyWithCompanion(ShortcutsCompanion data) { + return ShortcutsData( + id: data.id.present ? data.id.value : this.id, + emoji: data.emoji.present ? data.emoji.value : this.emoji, + usageCounter: data.usageCounter.present + ? data.usageCounter.value + : this.usageCounter, + ); + } + + @override + String toString() { + return (StringBuffer('ShortcutsData(') + ..write('id: $id, ') + ..write('emoji: $emoji, ') + ..write('usageCounter: $usageCounter') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, emoji, usageCounter); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ShortcutsData && + other.id == this.id && + other.emoji == this.emoji && + other.usageCounter == this.usageCounter); +} + +class ShortcutsCompanion extends UpdateCompanion { + final Value id; + final Value emoji; + final Value usageCounter; + const ShortcutsCompanion({ + this.id = const Value.absent(), + this.emoji = const Value.absent(), + this.usageCounter = const Value.absent(), + }); + ShortcutsCompanion.insert({ + this.id = const Value.absent(), + required String emoji, + this.usageCounter = const Value.absent(), + }) : emoji = Value(emoji); + static Insertable custom({ + Expression? id, + Expression? emoji, + Expression? usageCounter, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (emoji != null) 'emoji': emoji, + if (usageCounter != null) 'usage_counter': usageCounter, + }); + } + + ShortcutsCompanion copyWith({ + Value? id, + Value? emoji, + Value? usageCounter, + }) { + return ShortcutsCompanion( + id: id ?? this.id, + emoji: emoji ?? this.emoji, + usageCounter: usageCounter ?? this.usageCounter, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (emoji.present) { + map['emoji'] = Variable(emoji.value); + } + if (usageCounter.present) { + map['usage_counter'] = Variable(usageCounter.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ShortcutsCompanion(') + ..write('id: $id, ') + ..write('emoji: $emoji, ') + ..write('usageCounter: $usageCounter') + ..write(')')) + .toString(); + } +} + +class ShortcutMembers extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + ShortcutMembers(this.attachedDatabase, [this._alias]); + late final GeneratedColumn shortcutId = GeneratedColumn( + 'shortcut_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL REFERENCES shortcuts(id)ON DELETE CASCADE', + ); + late final GeneratedColumn groupId = GeneratedColumn( + 'group_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES "groups"(group_id)ON DELETE CASCADE', + ); + @override + List get $columns => [shortcutId, groupId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'shortcut_members'; + @override + Set get $primaryKey => {shortcutId, groupId}; + @override + ShortcutMembersData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ShortcutMembersData( + shortcutId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}shortcut_id'], + )!, + groupId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}group_id'], + )!, + ); + } + + @override + ShortcutMembers createAlias(String alias) { + return ShortcutMembers(attachedDatabase, alias); + } + + @override + List get customConstraints => const [ + 'PRIMARY KEY(shortcut_id, group_id)', + ]; + @override + bool get dontWriteConstraints => true; +} + +class ShortcutMembersData extends DataClass + implements Insertable { + final int shortcutId; + final String groupId; + const ShortcutMembersData({required this.shortcutId, required this.groupId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['shortcut_id'] = Variable(shortcutId); + map['group_id'] = Variable(groupId); + return map; + } + + ShortcutMembersCompanion toCompanion(bool nullToAbsent) { + return ShortcutMembersCompanion( + shortcutId: Value(shortcutId), + groupId: Value(groupId), + ); + } + + factory ShortcutMembersData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ShortcutMembersData( + shortcutId: serializer.fromJson(json['shortcutId']), + groupId: serializer.fromJson(json['groupId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'shortcutId': serializer.toJson(shortcutId), + 'groupId': serializer.toJson(groupId), + }; + } + + ShortcutMembersData copyWith({int? shortcutId, String? groupId}) => + ShortcutMembersData( + shortcutId: shortcutId ?? this.shortcutId, + groupId: groupId ?? this.groupId, + ); + ShortcutMembersData copyWithCompanion(ShortcutMembersCompanion data) { + return ShortcutMembersData( + shortcutId: data.shortcutId.present + ? data.shortcutId.value + : this.shortcutId, + groupId: data.groupId.present ? data.groupId.value : this.groupId, + ); + } + + @override + String toString() { + return (StringBuffer('ShortcutMembersData(') + ..write('shortcutId: $shortcutId, ') + ..write('groupId: $groupId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(shortcutId, groupId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ShortcutMembersData && + other.shortcutId == this.shortcutId && + other.groupId == this.groupId); +} + +class ShortcutMembersCompanion extends UpdateCompanion { + final Value shortcutId; + final Value groupId; + final Value rowid; + const ShortcutMembersCompanion({ + this.shortcutId = const Value.absent(), + this.groupId = const Value.absent(), + this.rowid = const Value.absent(), + }); + ShortcutMembersCompanion.insert({ + required int shortcutId, + required String groupId, + this.rowid = const Value.absent(), + }) : shortcutId = Value(shortcutId), + groupId = Value(groupId); + static Insertable custom({ + Expression? shortcutId, + Expression? groupId, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (shortcutId != null) 'shortcut_id': shortcutId, + if (groupId != null) 'group_id': groupId, + if (rowid != null) 'rowid': rowid, + }); + } + + ShortcutMembersCompanion copyWith({ + Value? shortcutId, + Value? groupId, + Value? rowid, + }) { + return ShortcutMembersCompanion( + shortcutId: shortcutId ?? this.shortcutId, + groupId: groupId ?? this.groupId, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (shortcutId.present) { + map['shortcut_id'] = Variable(shortcutId.value); + } + if (groupId.present) { + map['group_id'] = Variable(groupId.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ShortcutMembersCompanion(') + ..write('shortcutId: $shortcutId, ') + ..write('groupId: $groupId, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + class DatabaseAtV13 extends GeneratedDatabase { DatabaseAtV13(QueryExecutor e) : super(e); late final Contacts contacts = Contacts(this); @@ -9269,6 +9945,8 @@ class DatabaseAtV13 extends GeneratedDatabase { late final UserDiscoveryShares userDiscoveryShares = UserDiscoveryShares( this, ); + late final Shortcuts shortcuts = Shortcuts(this); + late final ShortcutMembers shortcutMembers = ShortcutMembers(this); @override Iterable> get allTables => allSchemaEntities.whereType>(); @@ -9296,6 +9974,8 @@ class DatabaseAtV13 extends GeneratedDatabase { userDiscoveryOtherPromotions, userDiscoveryOwnPromotions, userDiscoveryShares, + shortcuts, + shortcutMembers, ]; @override StreamQueryUpdateRules get streamUpdateRules => const StreamQueryUpdateRules([ @@ -9433,6 +10113,20 @@ class DatabaseAtV13 extends GeneratedDatabase { ), result: [TableUpdate('user_discovery_shares', kind: UpdateKind.delete)], ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'shortcuts', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('shortcut_members', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'groups', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('shortcut_members', kind: UpdateKind.delete)], + ), ]); @override int get schemaVersion => 13; diff --git a/test/services/backup_service_test.dart b/test/services/backup_service_test.dart new file mode 100644 index 00000000..7f3c428c --- /dev/null +++ b/test/services/backup_service_test.dart @@ -0,0 +1,233 @@ +import 'dart:io'; + +import 'package:background_downloader/background_downloader.dart'; +import 'package:drift/native.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:twonly/core/bridge.dart' as bridge; +import 'package:twonly/core/bridge/wrapper/backup.dart'; +import 'package:twonly/core/frb_generated.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/locator.dart'; +import 'package:twonly/src/callbacks/callbacks.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/json/backup.model.dart'; +import 'package:twonly/src/model/json/userdata.model.dart' + hide LastBackupUploadState; +import 'package:twonly/src/services/api.service.dart'; +import 'package:twonly/src/services/backup.service.dart'; +import 'package:twonly/src/services/user.service.dart'; +import 'package:twonly/src/utils/keyvalue.dart'; + +void main() { + if (!Platform.isMacOS) { + return; + } + + TestWidgetsFlutterBinding.ensureInitialized(); + + late Directory tempDir; + late Map initialUserData; + + setUpAll(() async { + const channel = MethodChannel('com.bbflight.background_downloader'); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (methodCall) async { + if (methodCall.method == 'enqueue') { + return true; + } + return null; + }); + + final dylibPath = + '${Directory.current.path}/rust/target/debug/librust_lib_twonly.dylib'; + if (File(dylibPath).existsSync()) { + await RustLib.init( + externalLibrary: ExternalLibrary.open(dylibPath), + ); + } else { + await RustLib.init(); + } + await initFlutterCallbacksForRust(); + + tempDir = Directory.systemTemp.createTempSync('twonly_backup_test_'); + AppEnvironment.initTesting( + customCacheDir: tempDir.path, + customSupportDir: tempDir.path, + ); + + await bridge.initializeTwonlyFlutter( + config: bridge.InitConfig( + databaseDir: tempDir.path, + dataDir: tempDir.path, + ), + ); + }); + + setUp(() async { + await locator.reset(); + final dbFile = File('${tempDir.path}/twonly.sqlite'); + locator + ..registerSingleton( + TwonlyDB(NativeDatabase(dbFile)), + ) + ..registerSingleton(UserService()) + ..registerSingleton(ApiService()); + + userService.currentUser = UserData( + userId: 1, + username: 'test_user', + displayName: 'Test User', + subscriptionPlan: 'Free', + currentSetupPage: null, + )..appVersion = 100; + userService.isUserCreated = true; + await UserService.save(userService.currentUser); + initialUserData = (await KeyValueStore.get('user'))!; + + await RustBackupIdentity.setBackupPasswordKeys( + password: 'strong_password', + userId: 1, + ); + }); + + tearDown(() async { + try { + await twonlyDB.close(); + } catch (_) {} + }); + + tearDownAll(() async { + if (tempDir.existsSync()) { + try { + tempDir.deleteSync(recursive: true); + } catch (_) {} + } + }); + + group('BackupService Tests', () { + test('getData returns default backup status initially', () async { + final data = await BackupService.getData(); + expect(data.identityState, LastBackupUploadState.none); + expect(data.archiveState, LastBackupUploadState.none); + expect(data.identityLastSuccessFull, isNull); + expect(data.archiveLastSuccessFull, isNull); + }); + + test( + 'onBackupUpdated stream emits events when backup status changes', + () async { + var eventEmitted = false; + final subscription = BackupService.onBackupUpdated.listen((_) { + eventEmitted = true; + }); + + final dummyTask = UploadTask(url: 'http://localhost', filename: 'test'); + await BackupService.handleBackupStatusUpdate( + 'backup_identity', + TaskStatusUpdate(dummyTask, TaskStatus.complete), + ); + + await Future.delayed(Duration.zero); + expect(eventEmitted, isTrue); + await subscription.cancel(); + }, + ); + + test( + 'handleBackupStatusUpdate updates identity and archive status correctly', + () async { + // Test success update for identity status + final dummyTask1 = UploadTask( + url: 'http://localhost', + filename: 'test', + ); + await BackupService.handleBackupStatusUpdate( + 'backup_identity', + TaskStatusUpdate(dummyTask1, TaskStatus.complete), + ); + + var data = await BackupService.getData(); + expect(data.identityState, LastBackupUploadState.success); + expect(data.identityLastSuccessFull, isNotNull); + + // Test failure update for archive status + final dummyTask2 = UploadTask( + url: 'http://localhost', + filename: 'test', + ); + await BackupService.handleBackupStatusUpdate( + 'backup_archive', + TaskStatusUpdate(dummyTask2, TaskStatus.failed), + ); + + data = await BackupService.getData(); + expect(data.archiveState, LastBackupUploadState.failed); + expect(data.archiveLastSuccessFull, isNotNull); + }, + ); + + test( + 'startFullBackupRecovery returns usernameNotValid for offline/unknown user', + () async { + final error = await BackupService.startFullBackupRecovery( + 'unknown_user', + 'password', + ); + expect(error, RecoveryError.usernameNotValid); + }, + ); + + test( + 'Full backup recovery flow restores identity and user.json archive successfully', + () async { + final initialBackupIdStr = await RustBackupIdentity.getBackupId(); + + // 1. Create backups of baseline state purely natively to avoid background backup races + final identityBytes = await RustBackupIdentity.getIdentityBackupBytes(); + final (_, archivePath) = await RustBackupArchive.createBackupArchive(); + + // 2. Tamper with user.json data and verify alteration + await KeyValueStore.put( + 'user', + {'changed': true, 'username': 'tampered'}, + ); + + final changedUserData = await KeyValueStore.get('user'); + expect(changedUserData?['changed'], isTrue); + + // 3. Trigger a change of the key_manager before restoring + await RustBackupIdentity.importBackupPasswordKeys( + backupId: List.filled(32, 1), + encryptionKey: List.filled(32, 1), + ); + + final changedBackupIdStr = await RustBackupIdentity.getBackupId(); + expect(changedBackupIdStr, isNot(equals(initialBackupIdStr))); + + // 4. Restore identity and archive + final backupKeys = await RustBackupIdentity.getBackupPasswordKeys( + userId: 1, + password: 'strong_password', + ); + await RustBackupIdentity.restoreIdentityBackup( + keys: backupKeys, + encryptedBytes: identityBytes, + ); + + await RustBackupArchive.restoreBackupArchive(filePath: archivePath); + + final restoredBackupIdStr = await RustBackupIdentity.getBackupId(); + expect(restoredBackupIdStr, equals(initialBackupIdStr)); + + // 5. Verify user.json data is fully restored + final restoredUserData = await KeyValueStore.get('user'); + expect( + restoredUserData?['username'], + equals(initialUserData['username']), + ); + }, + ); + }); +} diff --git a/test/services/group_services_test.dart b/test/services/group_service_test.dart similarity index 98% rename from test/services/group_services_test.dart rename to test/services/group_service_test.dart index ffe8f65f..2d603ae4 100644 --- a/test/services/group_services_test.dart +++ b/test/services/group_service_test.dart @@ -10,7 +10,7 @@ import 'package:twonly/src/database/tables/groups.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/json/userdata.model.dart'; import 'package:twonly/src/services/api.service.dart'; -import 'package:twonly/src/services/group.services.dart'; +import 'package:twonly/src/services/group.service.dart'; import 'package:twonly/src/services/user.service.dart'; class RealHttpOverrides extends HttpOverrides { diff --git a/test/unit_test.dart b/test/unit_test.dart index 73032a8c..76ea33fb 100644 --- a/test/unit_test.dart +++ b/test/unit_test.dart @@ -13,6 +13,9 @@ void main() { expect(isOneEmoji('😂'), true); expect(isOneEmoji('😂😂'), false); expect(isOneEmoji('Hallo 😂'), false); + for (final icon in EmojiAnimationComp.animatedIcons.keys) { + expect(isOneEmoji(icon), true); + } }); test('test proof-of-work simple', () async { diff --git a/test/utils/key_value_test.dart b/test/utils/key_value_test.dart new file mode 100644 index 00000000..a6a47983 --- /dev/null +++ b/test/utils/key_value_test.dart @@ -0,0 +1,86 @@ +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/utils/keyvalue.dart'; + +void main() { + late Directory tempDir; + + setUp(() { + tempDir = Directory.systemTemp.createTempSync('twonly_keyvalue_test_'); + AppEnvironment.initTesting( + customCacheDir: tempDir.path, + customSupportDir: tempDir.path, + ); + }); + + tearDown(() { + if (tempDir.existsSync()) { + try { + tempDir.deleteSync(recursive: true); + } catch (_) {} + } + }); + + group('KeyValueStore Tests', () { + test('get returns null for non-existent key', () async { + final result = await KeyValueStore.get('non_existent'); + expect(result, isNull); + }); + + test('put stores value and get retrieves it correctly', () async { + const key = 'test_key'; + final value = {'name': 'twonly', 'version': 1}; + + await KeyValueStore.put(key, value); + + final retrieved = await KeyValueStore.get(key); + expect(retrieved, isNotNull); + expect(retrieved?['name'], equals('twonly')); + expect(retrieved?['version'], equals(1)); + }); + + test('delete removes stored value successfully', () async { + const key = 'delete_key'; + final value = {'data': 'to_be_deleted'}; + + await KeyValueStore.put(key, value); + expect(await KeyValueStore.get(key), isNotNull); + + await KeyValueStore.delete(key); + expect(await KeyValueStore.get(key), isNull); + }); + + test('delete on non-existent key completes without error', () async { + await expectLater(KeyValueStore.delete('non_existent'), completes); + }); + + test('put overwrites existing value', () async { + const key = 'overwrite_key'; + final initialValue = {'status': 'initial'}; + final updatedValue = {'status': 'updated'}; + + await KeyValueStore.put(key, initialValue); + var retrieved = await KeyValueStore.get(key); + expect(retrieved?['status'], equals('initial')); + + await KeyValueStore.put(key, updatedValue); + retrieved = await KeyValueStore.get(key); + expect(retrieved?['status'], equals('updated')); + }); + + test('get handles corrupted JSON file gracefully by deleting it', () async { + const key = 'corrupt_key'; + final file = File('${tempDir.path}/keyvalue/$key.json'); + await file.parent.create(recursive: true); + await file.writeAsString('invalid json content'); + + expect(file.existsSync(), isTrue); + + final retrieved = await KeyValueStore.get(key); + expect(retrieved, isNull); + expect(file.existsSync(), isFalse); + }); + }); +}